Mesure des performances de vitesse de PySerial

J’ai recommencé à jouer avec la communication USB du TinyFPGA, à l’aide de ce programme : https://github.com/davidthings/tinyfpga_bx_usbserial

Je n’ai pas tout saisi le fonctionnement du pipeline, mais ce que j’ai pu observer, c’est qu’en maintenant le signal « uart_valid » de la transmission à 1, le caractère placé dans uart_data est transmis à l’infini, le plus rapidement possible. (J’envoie ici 97, ou « a » en ASCII).

Pour n’envoyer qu’une seule fois le caractère, je compte 2 transitions d’horloge où je maintiens uart_valid à 1. Voici la section du code Verilog (code complet à la fin de l’article) :

    reg[26:0] count1hz;

    always @(posedge clk_48mhz) begin
        if (count1hz == 0)
            uart_in_valid <= 1'b1;
        if (count1hz >= 10000) //valid doit être à 1 pendant n+1 cycles complets de clock pour transférer n byte
            uart_in_valid <= 1'b0;
    end

    always @(posedge clk_48mhz) begin
        if (count1hz >= 47999999) //tick 1Hz
            count1hz <= 0;
        else
            count1hz <= count1hz + 1;
    end

    assign uart_in_data = 97;

Et voici le programme python . J’utilise python 3.11.3 sur Fedora 38 et pyserial 3.5

import serial

ser = serial.Serial("/dev/ttyACM0")

idx = 0
while(True):
    msg_lenght = ser.in_waiting
    if msg_lenght > 0:
        idx = idx + 1
        print(idx)
        ser.read(msg_lenght)

Première constatation, le buffer est optimisé pour 64 caractères. Si je maintiens uart_valid pendant plus de 64 transitions d’horloge, seulement 64 caractères se rendent jusqu’à python. Pour arriver à 128 (donc 2 paquets USB), il faut attendre entre 5000 et 10000 transitions. En approximant vers le haut, cela signifie une fréquence de ~5kHz (L’horloge va à 48Mhz). Donc le taux de répétition des paquets est de 10kHz, ce qui fait 640ko/s, ou 6,4 Mbaud.

Honnêtement, je m’attendais à pire. Je lui ai quand même donné une chance en lisant d’un seul coup les 64 octets de son buffer, à l’aide de la commande in_waiting, au lieu d’une boucle for et d’un read sur chaque octet individuel (à proscrire).

Ma mesure concorde avec d’autres sur internet (voir par exemple https://stackoverflow.com/a/56240817, où cela donne 790ko/s).

Comparé au potentiel théorique de l’USB 2.0 de 48Mo/s, on est loin du compte. Toutefois, pour des applications où le débit d’information est bas, pyserial (et python en général) est très simple à utiliser.

Je suis à la recherche d’alternatives pour aller plus vite. Il y a la librairie pyserialtranfer (https://github.com/PowerBroker2/pySerialTransfer), mais je ne suis même pas sûr qu’il y aura un gain de vitesse. Sinon, essayer de voir ce qu’il se fait avec c++. Autre truc intéressant à comprendre, comment être sûr que le protocole à 480Mbit/s est utilisé. Parce que USB1 a la spécification d’aller à 12Mbit/s, ce qui est beaucoup plus dans l’ordre de grandeur que ce que je mesure (6Mbit/s). Donc peut-être que c’est aussi un problème de kernel et d’identification de la part du tinyFPGA. Peut-être (probablement) que l’implémentation de davidthings se base sur le standard USB1. Je ne connais pas les différences fondamentales au niveau du hardware.

EDIT : j’ai vérifié dans le kernel (avec la commande lsusb, puis lsusb -t) et le tinyFPGA est listé comme un device avec 12M de vitesse (USB1) :

Port 3: Dev 102, If 1, Class=CDC Data, Driver=cdc_acm, 12M

Ceci explique cela. La quête vers l’USB2 sera peut-être plus ardue que je le pensais. Cela change un peu ma conclusion : pyserial est une librairie parfaitement adaptée pour l’utilisation avec des devices suivant le protocole USB1.

Le code verilog complet :

/*
Voir le code pour le usb serial ici
https://github.com/davidthings/tinyfpga_bx_usbserial
*/

module uart_top (
        input  pin_clk,

        inout  pin_usb_p,
        inout  pin_usb_n,
        output pin_pu,

        output pin_led,
    );

    wire clk_48mhz;
    wire clk_locked;

    // Use an icepll generated pll
    pll pll48( .clock_in(pin_clk), .clock_out(clk_48mhz), .locked( clk_locked ) );

    // // LED
    // reg [22:0] ledCounter;
    // always @(posedge clk_48mhz) begin
    //     ledCounter <= ledCounter + 1;
    // end
    // assign pin_led = ledCounter[ 22 ];

    // Generate reset signal
    reg [5:0] reset_cnt = 0;
    wire reset = ~reset_cnt[5];
    always @(posedge clk_48mhz)
        if ( clk_locked )
            reset_cnt <= reset_cnt + reset;

    // uart pipeline in
    reg [7:0] uart_in_data;
    reg       uart_in_valid;
    reg       uart_in_ready;
    
    wire [7:0] uart_out_data;
    wire       uart_out_valid;
    wire       uart_out_ready;
    

    // usb uart - this instanciates the entire USB device.
    usb_uart uart (
        .clk_48mhz  (clk_48mhz),
        .reset      (reset),

        // pins
        .pin_usb_p( pin_usb_p ),
        .pin_usb_n( pin_usb_n ),

        // uart pipeline in
        .uart_in_data(uart_in_data),
        .uart_in_valid(uart_in_valid),
        .uart_in_ready(uart_in_ready),

        //.uart_out_data(uart_in_data),
        //.uart_out_valid(uart_in_valid),
        //.uart_out_ready(uart_in_ready)
    );

    // USB Host Detect Pull Up
    assign pin_pu = 1'b1;

    reg[26:0] count1khz;

    always @(posedge clk_48mhz) begin
        if (count1khz == 0)
            uart_in_valid <= 1'b1;
        if (count1khz >= 10000) //valid doit être à 1 pendant n+1 cycles complets de clock pour transférer n byte
            uart_in_valid <= 1'b0;
    end

    always @(posedge clk_48mhz) begin
        if (count1khz >= 47999999) //tick 1Hz
            count1khz <= 0;
        else
            count1khz <= count1khz + 1;
    end

    assign uart_in_data = 97;


endmodule