IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

FPGA : programmation d'un transmetteur UART en SystemVerilog

Carte FPGA Intel DE0-Nano dans l’environnement Quartus Prime

Un UART (Universal Asynchronous Receiver Transmitter) est à la fois un composant matériel et un protocole universel pour transmettre ou recevoir des données à travers une liaison série, qui n’utilise qu’un seul fil pour faire passer tous les bits de données. Aujourd’hui, les UART sont largement intégrés comme périphériques dans les microcontrôleurs.

Ce tutoriel vous permettra de comprendre (ou de réviser) le protocole et de synthétiser un transmetteur UART dans une puce FPGA. SystemVerilog sera utilisé comme langage de description de matériel dans l’environnement Intel Quartus Prime.

3 commentaires Donner une note à l´article (5)

Article lu   fois.

L'auteur

Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

1. Rôle d’un récepteur-transmetteur universel UART

Un récepteur-transmetteur asynchrone universel (UARTUniversal Asynchronous Receiver Transmitter) est un bloc de circuits permettant la communication série. L’UART est comme une passerelle agissant dans les deux sens pour faire passer les données entre les deux interfaces : parallèle et série. À une extrémité de l’UART se trouvent un bus parallèle avec sept ou huit lignes de données, ainsi que des broches de contrôle des entrées-sorties. À l’autre se trouvent les deux fils série : Rx pour la réception et Tx pour la transmission des données.

Image non disponible

2. Constitution d’une trame série UART

Une trame série, selon le protocole UART, ressemble à ceci :

Image non disponible
Format d'une trame UART - d'après documentation Arduino, licence C.C (https://docs.arduino.cc/learn/communication/uart/)

L’image montre les 7 bits de données (historiquement, ceux d’un « caractère » ASCII) émis en série en commençant par le bit de poids faible. Le signal est cadencé par une horloge, mais le signal d’horloge n’est pas transmis : UART est un protocole de liaison série asynchrone.

Avant d’établir la communication, les deux parties (transmetteur et récepteur) doivent donc partager la même vitesse de transmission, et l’interface série avec le protocole doit leur permettre, au moins par moment, de se synchroniser. Les marqueurs START et STOP en début et fin de caractères contribuent à cela.

Le bit de parité, facultatif, est un moyen simple de détection d’erreurs dans la transmission.

En résumé :

  • la liaison série UART est asynchrone, transmetteur et récepteur doivent partager une même vitesse de transmission exprimée en bauds (ou bits par seconde). Les vitesses de transmission normalisées sont en principe des multiples ou sous-multiples de 9600 :
    1200, 2400, 4800, 9600 …, 115 200, 230 400 bauds, etc. On peut aller jusqu’au Mbit/s si le matériel le permet (921 600 bauds exactement) et même plus encore ;
  • en 3,3 ou 5 V (technologie TTLTransistor-Transistor logic ou CMOSComplementary Metal Oxide Semiconductor), le signal est à l’état haut au repos, et la transmission commence par un bit de START (passage à l’état bas du signal) ;
  • s’ensuivent les bits de données, 8 bits de données le plus souvent de nos jours, en commençant par le bit de poids faible ;
  • le bit supplémentaire de parité, fixé en fonction du nombre de bits de données à 1, est facultatif. Si la parité est incluse dans la trame, il existe deux types de parités :

    • parité paire (Even parity) : le nombre de bits à 1, en prenant en compte les bits de données ainsi que le bit de parité, doit être pair. Si le récepteur compte les bits à 1 et en trouve un nombre impair, une erreur de parité sera détectée,
    • parité impaire (Odd parity) : le nombre de bits à 1, en prenant en compte les bits de données ainsi que le bit de parité, doit être impair. Si le récepteur compte les bits à 1 et en trouve un nombre pair, une erreur de parité sera détectée ;
  • la transmission se termine généralement par un seul bit de STOP, et le système se retrouve à nouveau à l’état repos, prêt pour la transmission de nouvelles données.

Usuellement, la configuration de la transmission UART est décrite par des expressions du type :

  • 9600-8-N-1 : vitesse de transmission à 9 600 bauds, 8 bits de données, pas de parité (None), un seul bit de STOP ;
  • 115200-8-E-1 : vitesse de transmission à 115 200 bauds, 8 bits de données, parité paire (Even), un seul bit de STOP ;
  • etc.

Parfois, les appareils communicants sont de nature différente, et les risques d’erreurs de transmission sont élevés, par exemple parce que le récepteur est trop lent pour traiter le flot de données. En plus des fils Tx et Rx utilisés pour la Transmission ou la Réception des données série (transmission full-duplexCommunication dans les deux sens simultanément.), des fils supplémentaires sont alors utilisés pour faire du contrôle de flux matériel. Les signaux transportés par ces fils de contrôle servent à gérer le transfert de données entre les deux appareils : l’appareil transmetteur prévient d’un envoi de données, le récepteur indique qu’il est prêt à les recevoir, prévient quand il a bien reçu les données, indique qu’il est prêt à en recevoir de nouvelles, etc. Dans ce tutoriel, et comme souvent de nos jours avec les équipements de type microcontrôleur, il n’y aura pas de contrôle de flux matériel (ni logiciel non plus).

3. Conception d’un transmetteur UART

Le schéma-bloc du transmetteur UART se présente ainsi :

Image non disponible
Transmetteur UART

L’entrée data_in[7..0] est le bus parallèle 8 bits où doit être présenté l’octet à transmettre. L’état de cette entrée doit être maintenu du début de la transmission, donné par une impulsion sur l’entrée send, jusqu’à la fin de la transmission signalée par une impulsion sur la sortie sent.

Les données en série selon le protocole UART sont présentées sur la sortie data_out.

L’entrée rst_n est dédiée au signal Reset (n pour negative, car le Reset sera actif sur le niveau bas du signal). Cette entrée sera reliée à un bouton-poussoir de la carte FPGA et forcera l’état repos du transmetteur sur appui du bouton (data_out forcée au niveau haut).

Le bus 4 bits config_uart[3..0] sera relié à quatre interrupteurs de type micro-switches de la carte et permettra de donner des éléments de configuration du transmetteur.
Les deux premiers interrupteurs sur config_uart[3..2] renseignent la vitesse de transmission parmi les valeurs normalisées suivantes :

  • 00 : 9600 bauds ;
  • 01 : 38 400 bauds ;
  • 10 : 115 200 bauds ;
  • 11 : 230 400 bauds.

Le bloc transmetteur se charge d’appliquer le diviseur de fréquence approprié au signal d’horloge clk_uart en entrée, fixée à 1 843 200 Hz (multiple de 9600), pour transmettre à la vitesse sélectionnée.

Les deux autres interrupteurs sur config_uart[1..0] renseignent le type de parité :

  • 00 : sans parité ;
  • 01 ou 11 : parité impaire (valeurs 1 et 3 en décimal, des valeurs impaires) ;
  • 10 : parité paire (valeur 2 en décimal, valeur paire).

Par exemple, avec la position des interrupteurs suivante :

Image non disponible
micro-switches

Dans ce cas, config_uart[3..0] = 4'b0001, et on aura la configuration 9600-8-O-1 (parité impaire Odd).

4. La carte et l’environnement de développement FPGA

Je ne vous présente plus la carte DE0-NanoPrésentation de la cible Terasic DE0-Nano, la cible FPGA déjà utilisée dans de précédents tutoriels :

Je profite également de l’environnement Quartus Prime et la dernière version à ce jour (23.1.1) de l’édition Lite (gratuite) : Quartus® Prime Design Software.

L’environnement Quartus Prime permet, pour l’essentiel dans ce tutoriel, de :

  • programmer des modules (des portions de circuits logiques dédiés à des fonctionnalités précises) dans un langage de description matériel comme SystemVerilog ou de générer des modules depuis des bibliothèques grâce à des assistants ;
  • instancier les modules et de relier leurs entrées-sorties à la souris dans une interface graphique ;
  • réaliser des testbenches simples (bancs de tests, ici à base de simulations fonctionnelles) et visualiser les signaux simulés dans des chronogrammes, là aussi à la souris dans une interface graphique ;
  • synthétiser et transférer le design dans la cible DE0-Nano.
Image non disponible
Programmation de modules en SystemVerilog
Image non disponible
Instanciation de modules et liaison des entrées-sorties à la souris
Image non disponible
Simulation fonctionnelle, visualisation des signaux simulés dans des chronogrammes
Image non disponible
Transfert du design dans la cible FPGA

5. Description modulaire du transmetteur UART

La description d’un transmetteur UART n’est pas si compliquée et pourrait très bien être contenue dans un seul fichier Verilog/SystemVerilog sans même passer par un assistant graphique. Mais le choix qui a été fait ici est de décomposer la description en plusieurs modules, chaque module répondant à un sous-problème plus simple à décrire. Les assistants graphiques nous aideront à mieux visualiser l’organisation du circuit qui sera synthétisé.

5-1. Un premier module datapath

Le datapath sera la portion de circuit qui opérera sur l’octet de données en entrée pour élaborer le signal série en sortie :

Image non disponible

Entrées-sorties principales :

  • data_in[7..0] : l’octet à transmettre (bus parallèle 8 bits) qui doit être présent sur le bus tout le temps de la transmission ;
  • clk_uart : l’horloge principale qui sera fixée à 1 843 200 Hz, un multiple de 9 600, et qui va cadencer les événements ;
  • data_out : signal série UART.

On propose le code SystemVerilog suivant pour le module datapath :

datapath.sv
Sélectionnez
module datapath (
    input logic clk_uart,               // signal d'horloge
    input logic [7:0] data_in,          // octet à transmettre
    input logic start_bit, data_bit, parity_bit, stop_bit,  // nature du bit à activer
    input logic [2:0] bit_num,          // numéro du bit en cours
    input logic [1:0] config_parity,    // type de parité
    output logic data_out               // sortie série
);

    logic parity_even, xor_data_in;
    
    always_comb begin
        parity_even = config_parity == 2'b10;   // =1'b1 si parité paire, =1'b0 sinon
        xor_data_in = ^data_in;                 // xor entre tous les bits de data_in
    end

    always_ff @(posedge clk_uart) begin
        if (start_bit)      // si bit de Start
            data_out <= 1'b0;
        else if (data_bit)  // si bit de données
            data_out <= data_in[bit_num];
        else if (parity_bit) // si bit de parité (paire ou impaire)
            data_out <= parity_even ? xor_data_in : ~xor_data_in;
        else if (stop_bit)  // si bit de Stop
            data_out <= 1'b1;
        else
            data_out <= 1'b1;
    end
    
endmodule

Le bloc always_comb va inférer de la logique combinatoire, alors que le bloc always_ff @(posedge clk_uart) va inférer de la logique séquentielle avec une bascule D sensible au front montant de l'horloge.

Les signaux de contrôle et d’activation :

  • start_bit : activation du bit de START. On maintient à 1 cette entrée d’activation durant le nombre de cycles d’horloge nécessaires pour produire un bit de START (data_out à 0) ;
  • stop_bit : activation du bit de STOP. On maintient à 1 cette entrée d’activation durant le nombre de cycles d’horloge nécessaires pour produire un bit de STOP (data_out à 1) ;
  • parity_bit : activation du bit de parité. Cette entrée ne sera pas forcément activée puisque ce bit est facultatif. Mais si elle est activée durant le nombre de cycles d’horloge nécessaires, le bit de parité généré en sortie est calculé en fonction du nombre de bits à 1 de l’octet de données data_in[7..0] et selon le type de parité (paire ou impaire) à lire sur le bus config_parity[1..0] ;
  • config_parity[1..0] : type de parité codé sur deux bits - 00 : pas de parité, 01 ou 11 : parité impaire, et 10 : parité paire.
  • bit_num[2..0] : le numéro en cours du bit de l’octet de données data_in[7..0]. 0 pour le bit de poids faible, jusqu’à 7 pour le dernier bit de poids fort.
  • data_bit : activation des bits de données. Le numéro du bit en cours, entre 0 et 7, est à lire sur le bus bit_num[2..0]. On maintient à 1 cette entrée d’activation durant le nombre de cycles d’horloge nécessaires pour générer les bits de données en sortie ;

Il est toujours intéressant de jeter un œil sur le bloc logique généré par ce module (Register Transfer-Level View) : des multiplexeurs (ou sélecteurs), un comparateur, une cellule XOR (OU exclusif) à 8 entrées, une bascule D :

Image non disponible
Génération du bit de parité

Vous pouvez suivre en bleu le flux du bit généré en sortie lorsque l’entrée parity_bit est activée, et que la parité choisie est paire. Le circuit fait un OU exclusif (XOR) entre tous les bits de l’octet de données data_in[7..0]. Si le nombre de 1 est pair, le OU exclusif produira un bit de parité à 0 qui sera dirigé vers la sortie au prochain front d’horloge. Et si le nombre de bits à 1 est impair, le OU exclusif produira un bit de parité à 1.

Sur la vue ci-dessous, c’est un bit de données qui est dirigé vers la sortie (data_bit activé). Le multiplexeur repéré Mux0 va diriger un des huit bits de données vers la sortie selon la valeur de l’entrée de sélection bit_num[2:0] entre 0 et 7.

Image non disponible
Génération du bit de données

Un nouveau bloc logique est nécessaire pour produire la séquence des différents signaux de contrôle et d’activation.

5-2. Le contrôleur UART – description par une machine à états finis

Une séquence doit être produite : après une impulsion du signal send, il faut activer le bit de START en premier lieu (start_bit=1), puis les bits de données (data_bit=1) en commençant par transmettre le bit de poids faible avec bit_num=3’b000 jusqu’au bit de poids fort où bit_num=3’b111, puis éventuellement activer le bit de parité (parity_bit=1), et enfin pour terminer le bit de STOP (stop_bit=1).

La séquence sera générée par un bloc logique uart_transmitter_controller, à relier au datapath.

Image non disponible
Contrôleur UART

Toute la séquence peut-être produite par une machine à états finis (Finite State Machine) et décrite par un diagramme d’états-transitions. La machine est constituée d'états stables, et elle passe d'état en état, suivant les transitions.

Machine à états finis : machine de Moore vs machine de Mealy

Ces « machines » réalisent des actions déterminées en fonction des événements qui se présentent.

À un moment donné, la machine se trouve dans un état (l’état « courant »), le passage d’un état à un autre (la transition) est activé par un événement ou une condition.

Image non disponible
Machine de Mealy, d'après https://www.emse.fr/~dutertre/documents/machines_a_etats.pdf

Dans un FPGA :

  • les états sont synchrones, mémorisés sur front montant de l’horloge ;
  • un processus combinatoire calcule l’état futur à partir de l’état présent et des entrées ;
  • dans une machine de Mealy, un processus combinatoire calcule les sorties à partir de l’état présent et des entrées (alors que dans une machine de Moore, les sorties ne dépendent QUE de l’état présent).

Le diagramme d’états-transitions ci-dessous est celui de notre contrôleur UART :

Image non disponible
Diagramme états-transitions du contrôleur UART

Vous pouvez constater que les sorties en rouge ne dépendent en général que de l’état en cours comme dans une machine de Moore. Deux signaux en sortie, inc_bit et clr_bit, échappent à la règle puisqu’ils dépendent également de signaux en entrée.

Le signal clr_bit par exemple est activé sur la transition entre les états START et DATA, c’est-à-dire lorsque le signal ticks_done est activé.

Résumons la manière de coder ce graphe…

Six états sont codés, et la mise à jour de l’état présent (state) par un état futur (state_next) est déclenchée sur front montant de l’horloge :

 
Sélectionnez
// ...
       typedef enum logic[2:0] { IDLE, START, DATA, PAR, STOP, ACK } state_t;
       state_t state, state_next;
// ...


  always_ff @(posedge clk_uart) begin
    if (!rst_n)  // reset synchrone
      state <= IDLE;
    else
      state <= state_next;  // Mise à jour de l’état présent par l’état futur sur front montant de l’horloge
  end

Un processus combinatoire calcule l’état futur (state_next) en fonction de l’état présent et des entrées. Par exemple, pour le passage de l’état repos IDLE à l’état START lorsque le signal d’entrée send est activé (démarrage de la transmission) :

Image non disponible

 
Sélectionnez
// ...
  always_comb begin

    {clr_bit, start_bit, data_bit, inc_bit, parity_bit, stop_bit, sent} = 7'b0000000; // par défaut
    state_next = state; // état conservé par défaut

    case (state)
      IDLE : begin
               if (send) 
                 state_next = START;
             end

      // ...

    endcase
  end

Dans ce même processus, on calcule aussi les sorties. Dans l’extrait ci-dessous, la sortie start_bit est activée dans l’état START, mais la sortie clr_bit est activée sur la transition avec l’état DATA lorsque l’entrée ticks_done est activée.

Image non disponible

 
Sélectionnez
// ...
  always_comb begin

    {clr_bit, start_bit, data_bit, inc_bit, parity_bit, stop_bit, sent} = 7'b0000000; // par défaut
    state_next = state; // état conservé par défaut

    case (state)

      // ...

      START: begin
               start_bit = 1;
               if (ticks_done) begin
                 clr_bit = 1;
                 state_next = DATA;
               end
             end

      // ...

    endcase
  end

Plusieurs signaux n’ont pas été évoqués jusqu’à présent :

  • clr_bit et inc_bit sont deux signaux internes au bloc qui agissent directement sur le numéro du bit de données en cours de transmission, entre 0 (bit de poids faible) et 7 : num_bits[2:0]. clr_bit remet le numéro du bit à zéro, inc_bit l’incrémente. Un signal data_done est activé pour informer que le dernier bit (le numéro 7) de données est en cours de transmission :
 
Sélectionnez
      // **** Numéro du bit de données ********************************************************** 
      logic inc_bit, clr_bit; // commande incrémentation numéro bit de données et remise à zéro
      logic data_done; // =1'b1 si dernier bit de données

      always_ff @(posedge clk_uart) begin

            if (clr_bit)
              bit_num <= 3'd0;
            else if (inc_bit) begin
              bit_num <= bit_num + 3'd1;
            end

      end

      assign data_done = (bit_num == 3'b111); // dernier bit de données
      // ****************************************************************************************
  • le signal ticks_done qui figure dans la majorité des transitions du diagramme est aussi un signal interne au bloc du contrôleur. Il fixe la durée du bit qui dépend de la vitesse de transmission choisie. La fréquence de l’horloge clk_uart est fixée à 1 843 200 Hz et doit être divisée pour des nombres de bauds compris entre 9600 et 230 400.

    Image non disponible

    Un compteur ticks_counter s’incrémente à chaque front montant de l’horloge et repart à zéro à chaque bit transmis. Si par exemple la vitesse de transmission choisie est 230 400 bauds, le compteur ticks_counter doit évoluer entre 0 et 7, car 1 843 200 / 8 = 230 400. Quand ticks_done est activé, c’est que la durée du bit a été atteinte et qu’il faut passer au bit suivant :
 
Sélectionnez
       // **** diviseur de fréquence **********************************
       logic [7:0] ticks_counter, ticks_per_bit;
       logic ticks_done;

       always_comb begin
         case (config_uart[3:2])
           2'b00 :  ticks_per_bit = (CLK_UART / 9_600 - 1);    // 9600 bauds = 1843200/192
           2'b01 :  ticks_per_bit = (CLK_UART / 38_400 - 1);    // 38400 bauds = 1843200/48
           2'b10 :  ticks_per_bit = (CLK_UART / 115_200 - 1);    // 115200 bauds = 1843200/16
           2'b11 :  ticks_per_bit = (CLK_UART / 230_400 - 1);    // 230400 bauds = 1843200/8
           default: ticks_per_bit = (CLK_UART / 9_600 - 1);    // 9600 bauds = 1843200/192
         endcase
       end

       always_ff @(posedge clk_uart) begin
         if ((state != state_next) || inc_bit) begin // remise à zéro du compteur à chaque changement d'état et au changement du bit à transmettre
           ticks_counter <= 0;
         end else begin
           ticks_counter <= ticks_counter + 7'd1;
         end                                                                                    
       end

       assign ticks_done = ticks_counter == ticks_per_bit;

       // *************************************************************



Ce qui donne le code complet du contrôleur uart_transmitter_controller.sv :

uart_transmitter_controller.sv
Cacher/Afficher le codeSélectionnez

5-3. Le module transmetteur complet

Pour terminer ce bloc transmetteur, il faut créer un module de niveau supérieur où les deux modules datapath et uart_transmetter_controller sont reliés par leurs entrées-sorties, ce qui peut se faire à la souris dans l’environnement Quartus Prime :

Image non disponible

On peut même générer un schéma de ce module Block_uart4dvp pour une future exploitation dans un nouveau circuit :

Image non disponible

Ce bloc consomme 54 éléments logiques (sur 22 320) et 18 registres de la puce FPGA Intel Cyclone IV E.

Image non disponible
Compilation report

5-4. Validation par simulation

Le module général peut-être simulé pour valider son fonctionnement. Avec l’outil Simulation WaveForm Editor intégré, vous construisez les stimuli à la souris (signaux clk_uart, config_uart[3:0], rst_n, send, et data_in[7:0]) et vous démarrez une simulation fonctionnelle.

Voici un exemple renvoyé à la fin de la simulation pour le caractère « M » transmis avec la configuration 230400-8-O-1 (parité impaire) :

Image non disponible

La transmission démarre bien avec un bit de START après l’impulsion du signal send. Le code ASCII du « M » étant 0100 1101 en binaire, les bits de données sont transmis dans le bon ordre (bit de poids faible en premier). Le code binaire du « M » comprend quatre bits à 1, le bit de parité doit être à 1 pour une parité impaire. Une impulsion sur le signal sent est bien générée après le bit de STOP.

Si on fait un zoom sur le bit de START :

Image non disponible

Il faut 8 cycles d’horloge pour transmettre un bit, ce qui est bien conforme, car la fréquence du signal clk_uart est de 1 843 200 Hz, et il faut la diviser par 8 pour une vitesse de 230 400 bauds.

Cela s’annonce bien…

6. Test avec un transmetteur « Hello World! »

Pour ne pas déroger à la règle de la démonstration « Hello World! », un transmetteur de message « Hello World! » est proposé pour conclure cet article !

On donne le schéma complet du test ci-dessous :

Image non disponible

Block_uart4dvp : le bloc transmetteur UART étudié précédemment ;

send_helloworld : le bloc qui génère les caractères du message « Hello World! » à envoyer en boucle au transmetteur UART ;

Block_baudrate_generator : le bloc générateur du signal d’horloge clk_uart à 1 843 200 Hz.

6-1. Description du module send_helloworld

Le module send_helloworld devra envoyer un par un (et en boucle infinie) les caractères de la chaîne « Hello World! » au transmetteur UART. À chaque caractère transmis sur la sortie série, l’impulsion sur la sortie sent du transmetteur est redirigée sur l’entrée char_ack du module send_helloworld pour acquitter de la transmission du caractère et lancer le caractère suivant. La séquence est dirigée, là aussi, par une machine à états finis.

send_helloworld.sv
Sélectionnez
module send_helloworld #(
  parameter SIZE = 14,
  parameter logic [7:0] char_array [0:SIZE-1] = '{ "H", "e", "l", "l", "o", " ", 
                                                   "W", "o", "r", "l", "d", "!",
                                                   "\r", "\n" }
)
(
  input logic clk_uart, char_ack, rst_n,
  output logic [7:0] char, // caractère
  output logic send
);

  logic clr_idx, inc_idx;
  logic [3:0] index;
  
  
     typedef enum logic[1:0] { IDLE, TRANSMIT, ACK } state_t;
     state_t state, state_next;


     always_ff @(posedge clk_uart) begin

       if (!rst_n) begin
         state <= IDLE;
       end else begin
         state <= state_next;
         char <= char_array[index];
         if (clr_idx)
           index <= 0;
         else if (inc_idx)
           index <= index + 4'd1;
       end
     end


     always_comb begin

       {clr_idx, inc_idx, send} = 3'b000; //par défaut
          state_next = state; // état conservé par défaut

          case (state)
            IDLE:       begin
                          clr_idx = 1;
                          state_next = TRANSMIT;
                        end

            TRANSMIT:   begin
                          send = 1;
                          state_next = ACK;
                        end

            ACK:        begin
                          if (char_ack) begin
                            if (index == SIZE-1)
                              state_next = IDLE;
                            else begin
                              inc_idx = 1;
                              state_next = TRANSMIT;
                            end
                          end
                        end

          endcase
     end

endmodule

6-2. Génération du signal d’horloge clk_uart

Il reste à générer le signal d’horloge avec le module Block_baud_rate_generator, en invoquant un circuit spécialisé disponible dans la bibliothèque de composants de Quartus Prime (IP Catalog → Library → Basic Functions → Clocks, PLLs and Resets → PLL → ALTPLL) : une boucle à verrouillage de phase ou PLL (phase-locked loop) pour asservir la fréquence de sortie sur un multiple de la fréquence d’entrée. L’entrée du bloc est raccordée à l’horloge principale 50 MHz (entrée CLOCK_50) de la carte FPGA. Le ratio de fréquence proposé par l’assistant est 71 / 1926 pour réduire la fréquence de l’horloge principale : (71 / 1926) x 50.106 ≈ 1 843 200 Hz (à 2 Hz près).

Image non disponible
Image non disponible
Configuration du circuit PLL

Voir un exemple de configuration de circuit PLL.

6-3. L’analyseur logique

Pour analyser les signaux, j’utilise ce petit analyseur logique à l’origine produit par le constructeur Saleae :

Image non disponible
Analyseur logique (compatible Saleae)

Ce modèle à bas coût n’est plus produit par Saleae, mais on trouve nombre de constructeurs vendant leurs modèles censés être compatibles (clones) pour 10-15 € (et même moins en cherchant bien sur les sites marchands).

Normalement, le logiciel fourni par Saleae (gratuit) devrait tourner aussi sur ces modèles, mais la documentation vous dirigera sans doute vers une alternative opensource comme PulseView de Sigrok. Bien entendu, le logiciel comprend un décodeur pour les signaux UART.

6-4. Signaux du transmetteur UART à l’analyseur logique

Les signaux qui doivent être relevés par l’analyseur logique doivent être dirigés vers les ports GPIO de la carte FPGA (voir Affectation des broches et programmation du FPGA).

Image non disponible
Pin Planner dans Quartus Prime
Image non disponible
Image non disponible
L'analyseur logique relève les signaux dirigés vers les ports GPIO de la carte FPGA

Pour ce premier test, les micro-switches de configuration UART sont dans la position [1001], soit 115200-8-O-1 (parité impaire), et on saisit cette même configuration pour le décodeur de l’analyseur logique :

Image non disponible

Le design complet du transmetteur « Hello world! » étant transféré dans la carte FPGA, on peut lancer l’acquisition…

Image non disponible

… Le message « Hello World! » est bien décodé, les signaux du transmetteur sont bien conformes au protocole UART. On voit aussi les impulsions des signaux send et sent qui marquent le début et la fin de transmission de chaque caractère.

Si l’on zoome sur la lettre « H »:

Image non disponible

Après le START, suivent les 8 bits de données (les petits points blancs marquent le moment de l’acquisition), le bit de parité (le petit carré blanc) qui est bien à 1 pour avoir un nombre impair de bits à 1, et l’on termine par un STOP. L’octet envoyé est bien le code ASCII de la lettre « H » (0b0100 1000).

Et si l’on essaye d’estimer la vitesse de transmission avec l’outil :

Image non disponible

Le logiciel nous indique fbaud = 115.385 kHz à comparer à la vitesse attendue de 115 200 bauds. Il est difficile d’estimer la précision de l’outil, mais l’erreur relative reste inférieure à 0,2%.

Si l’on règle la parité à « Even parity bit » dans le logiciel de l’analyseur (parité paire), alors que le FPGA continue de transmettre avec une parité impaire, on obtient :

Image non disponible

Le message est bien décodé, mais une erreur de parité est indiquée pour chacun des bits, ce qui est là aussi conforme à l’attendu.

7. Conclusion

Au terme de cet article, nous avons le bilan suivant :

  • un circuit logique capable de transmettre des données en série selon le protocole UART a été conçu et synthétisé. Certains paramètres de la liaison série sont même configurables : vitesse de la transmission (parmi 9600, 38 400, 115 200 et 230 400 bauds) et type de parité (sans parité, parité paire ou impaire) ;
  • le circuit a été validé par étape grâce à des simulations fonctionnelles d’abord. Puis dans un dernier temps, les signaux dirigés vers des ports GPIO de la carte FPGA sont passés au crible d’un analyseur logique (à bas coûts) capable de décoder les signaux UART.

Le transmetteur est maintenant réutilisable pour de futurs projets où une communication série s’avère nécessaire pour transmettre les données d’un capteur ou envoyer des ordres à un actionneur.

Pour être complet, après le transmetteur UART, un récepteur UART doit encore être développé avec de nouveaux problèmes à résoudre (synchronisme en lecture afin d’acquérir les 0 et les 1 « au bon moment », gestion de la parité, etc.).

Le fait de « programmer » des descriptions de circuits logiques donne une grande flexibilité aux FPGA. Vous pouvez par exemple combiner plusieurs transmetteurs ou récepteurs UART. Si vous avez cinq capteurs ou plus communiquant avec le protocole UART, vous pouvez configurer autant de blocs récepteurs que nécessaire (dans la limite du nombre d’éléments logiques disponibles sur la puce FPGA). Il suffit de dupliquer le bloc et raccorder ses entrées-sorties, les récepteurs UART vont alors tourner en parallèle. Si les capteurs ne sont pas identiques, chaque bloc peut même travailler avec des configurations différentes en termes de nombre de bauds ou de parité. De nouveaux circuits permettront de sélectionner et diriger les sorties des transmetteurs, faire des traitements particuliers pour reconstituer un message avec les données des capteurs qui sera dirigé vers un transmetteur UART. La sortie du transmetteur sera connectée au port série d’une carte Arduino par exemple pour traiter les messages.

Image non disponible
Démonstration avec instanciation de deux blocs transmetteurs UART qui fonctionneront en parallèle.

Je remercie Auteur pour ses suggestions. Merci également à escartefigue pour sa relecture orthographique.

8. Annexe : archive Quartus Prime

Archive du projet de ce tutoriel au format Quartus Archive, à importer directement depuis Quartus Prime :

Version de Quartus Prime : 23.1.1 Lite Edition

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2024 f-leb. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.