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

FPGA : tutoriel pour s’initier au langage Verilog

Programmation d’un pilote pour le capteur de température Si7021

La carte de développement utilisée dans ce tutoriel est une carte FPGA Alchitry Au que je vous avais déjà présentée dans un tutoriel précédent. Un module avec un capteur de température et d’humidité Si7021 (Silicon Labs) est connecté à la carte.

Après avoir fait vos premiers pas avec Verilog, vous allez apprendre à programmer un pilote qui va gérer le dialogue I2C avec le module pour acquérir la température ambiante à intervalles réguliers. Dans cette démonstration, la valeur de la température sera retournée via le câble USB pour affichage dans un terminal série.

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

Article lu   fois.

L'auteur

Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction : microcontrôleur ou FPGA ?

Autant le dire tout de suite… Programmer un microcontrôleur pour communiquer avec un capteur de température en liaison I2C est une activité assez simple avec une bibliothèque adaptée pour ce protocole. Avec une carte Arduino et la bibliothèque du capteur à importer, quelques lignes de programme dans une boucle suffisent. Alors, pourquoi proposer un tutoriel avec un matériel plus coûteux, qui va généralement vider plus rapidement la batterie de votre système embarqué, programmé dans un langage HDLHardware Description Language que vous mettrez plus de temps à assimiler qu’un langage procédural comme C ou C++, et qui vous vaudra des heures supplémentaires en débogage et mise au point ?

Bien entendu, les FPGAField-Programmable Gate Array ont leurs avantages quand la performance et la flexibilité d’une architecture reconfigurable sont requises. Un microcontrôleur ne peut exécuter des instructions que l’une après l’autre, alors que le fonctionnement des FPGA est fondamentalement parallèle. Mais avant d’en profiter pleinement, il faudra commencer par découvrir les bases d’un langage de description de matériel tel que Verilog. Et c’est précisément l’objectif de ce tutoriel qui utilise une plateforme pour débutants…

II. La carte Alchitry Au

On rappelle les caractéristiques principales de la carte Alchitry Au :

Image non disponible
FPGA Alchitry Au - Image Sparkfun Licence C.C

Caractéristiques principales

  • FPGA
  • Xilinx Artix 7 XC7A35T-1C (33 280 cellules logiques)
  • Mémoire
  • 256 Mo DDR3 via interface 16 bits (~ 10 Gbit/s)
  • Entrées-sorties
  • 102 broches à 3,3 V, dont 20 peuvent être configurées pour de la transmission différentielle basse-tension 1,8 V
  • 9 entrées différentielles analogiques (1 dédiée et 8 partagées avec les entrées-sorties numériques)
  • Périphériques en surface de la carte
  • 1 jeu de 8 LED vertes
  • 1 bouton-poussoir à appui momentané « Reset »
  • 1 LED de statut
  • 1 horloge 100 MHz
  • 1 interface série port USB type C via FT2232H (12 MBauds maxi.)
  • 1 connecteur Qwiic (I2C) SparkFun
  • Programmation
  • JTAG via port USB, interface FT2232H
  • Alimentation
  • 5 V par port USB type C, trou métallisé x 2 0,1”
  • Dimensions
  • 65 mm x 45 mm
   

Notez les quatre plots de connexion femelle qui permettent d’enficher (et même empiler) des cartes d’extension à la façon des shields Arduino (ou des HATHardware Attached on Top de la carte Raspberry Pi). Vous avez un aperçu d’une de ces cartes dans ce billet qui met en œuvre un afficheur 7-segments.

III. L’EDI Alchitry Labs

L’EDI Alchitry Labs vient en renfort du débutant en complément de la suite Vivado de Xilinx qui supporte gratuitement la puce FPGA Xilinx Artix-7 de la carte Alchitry.

EDI Alchitry Labs

L’EDI est plutôt rudimentaire, un éditeur basique et un bouton pour lancer la chaîne des outils de synthèse : analyse, synthèse logique, jusqu’à la génération du fichier bitstream. Prenez en compte les deux autres boutons pour téléverser le fichier du projet en RAM ou en ROM qui va configurer la puce FPGA et vous savez l’essentiel.

L’EDI peut toutefois ne servir que d’intermédiaire étant donné qu’il s’appuie en arrière-plan sur les outils de la suite Vivado. Une fois votre projet généré, tous les fichiers nécessaires sont dans un dossier spécial pour poursuivre le travail dans l’environnement professionnel de la suite (pour faire différents types d’analyses, visualiser des schémas, faire des simulations, etc.).

Vivado Suite
Le même projet sous la suite Vivado

À la création d’un nouveau projet Verilog dans Alchitry Labs, le code du module principal au_top.v se présente comme suit :

au_top.v
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
module au_top(
    input clk,              // 100MHz clock
    input rst_n,            // reset button (active low)
    output [7:0] led,       // 8 user controllable LEDs
    input usb_rx,           // USB->Serial input
    output usb_tx           // USB->Serial output
    );
    
    wire rst;
    
    // The reset conditioner is used to synchronize the reset signal to the FPGA
    // clock. This ensures the entire FPGA comes out of reset at the same time.
    reset_conditioner reset_conditioner(.clk(clk), .in(!rst_n), .out(rst));
    
    assign led = 8'h00;      // turn LEDs off

    assign usb_tx = usb_rx;  // echo the serial data
    
endmodule

Les premières lignes décrivent les entrées-sorties des périphériques de la carte : l’horloge principale à 100 MHz, l’entrée du bouton reset et les sorties vers le jeu de 8 LED vertes en surface de la carte, ainsi que les connexions série Rx et Tx du port USB.

En fin de module, des assignations continues assign :

  • pour mettre l’anode des LED à l’état bas afin de les éteindre ;
  • pour renvoyer en écho les données reçues en raccordant l’entrée Rx et la sortie Tx de la liaison série USB.

Cette description est résumée dans le schéma structurel ci-dessous :

Schéma structurel
Schéma structurel

Le bloc reset_conditioner au centre du schéma est instancié avec ses connexions à ligne 13 du code précédent.

reset_conditioner reset_conditioner(.clk(clk), .in(!rst_n), .out(rst));

On voit par exemple dans cette description structurelle que l’entrée in de l’instance est raccordée au signal inversé (avec !, comme en langage C) du bouton-poussoir rst_n (suffixe n pour négatif, car le signal est à l’état bas lorsque le bouton est pressé).

reset_conditioner est donc un module complémentaire, ajouté par défaut au projet, et qui permet de conditionner et synchroniser avec l’horloge, le signal issu de l’appui sur le bouton-poussoir reset en surface de la carte. Le nœud rst en sortie de ce bloc est pour l’instant non connecté, mais il pourra servir de signal de réinitialisation synchrone lorsqu’il sera dirigé vers certaines parties du futur système.

IV. Premiers pas en Verilog

IV-A. Un premier blink

Avant de s’attaquer au capteur de température, on propose de découvrir le langage Verilog sur une simple application de clignotement de LED (une fois n’est pas coutume !)

Le code du module principal au_top.v qui suit fait clignoter une des huit LED du bandeau en surface de la carte en la dirigeant vers un signal carré de fréquence 1 Hz :

au_top.v
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
module au_top(
    input clk,              // 100MHz clock
    input rst_n,            // reset button (active low)
    output [7:0] led,       // 8 user controllable LEDs
    input usb_rx,           // USB->Serial input
    output usb_tx           // USB->Serial output
    );
    
    wire rst;
    
    // The reset conditioner is used to synchronize the reset signal to the FPGA
    // clock. This ensures the entire FPGA comes out of reset at the same time.
    reset_conditioner reset_conditioner(.clk(clk), .in(!rst_n), .out(rst));

    assign usb_tx = usb_rx;  // echo the serial data
    
    reg [27:0] counter;
    wire out;
    localparam PERIOD = 100_000_000; // 1s period
    
    assign out = counter > (PERIOD >> 1); // PERIOD >> 1, division by 2
    assign led = {7'h0, out};
    
    always @(posedge clk or posedge rst) begin
      if (rst) begin
        counter <= 0;
      end else begin
        if (counter < PERIOD) begin
          counter <= counter + 1;
        end else begin
          counter <= 0;
        end
      end
    end
    
endmodule
  • Ligne 17 : avec le type reg, on déclare ce qui se rapproche du concept de « variable » dans les langages procéduraux traditionnels. Ici, un vecteur de largeur 28 bits.
  • Ligne 18 : le type wire, lui, n’a pas de capacité de mémorisation et désigne des nœuds représentant des connexions entre éléments physiques.
  • Ligne 19 : PERIOD est un paramètre local initialisé à 100 000 000, le nombre de cycles par seconde de l’horloge principale.
  • Ligne 21 et 22 : des assignations continues pour donner des valeurs aux nœuds. Dans une assignation continue, tout changement dans l’expression à droite du signe = est répercuté sur le terme assigné à gauche du signe =. Ainsi, le signal Out basculera à 1 quand le compteur aura dépassé la moitié de la valeur maximale (au bout de 0,5 s). Avec l’assignation continue qui suit, sept des huit LED du bandeau en surface de la carte seront éteintes, tandis que la huitième clignotera. Les accolades { } réalisent une concaténation de bits.
  • Lignes 24 à 34 : un processus always exécuté continuellement. La « liste de sensibilité » est définie sur front montant de l’horloge principale clk et front montant du signal rst (appui sur le bouton Reset). En d’autres termes, ce processus ne s’exécutera que lors d’un événement sur l’un des signaux de la liste. Le processus de clignotement de la LED est ensuite décrit de façon comportementale dans le bloc beginend exécuté séquentiellement. L’interprétation de ces lignes ne pose pas de difficultés. On incrémente un compteur qui repasse à zéro soit parce qu’il atteint sa valeur maximale, soit parce que le bouton Reset est appuyé.

La génération complète du projet (build) peut prendre plusieurs minutes…

Dans la suite Vivado, on peut visualiser une première analyse du projet générée sous forme schématique :

Image non disponible
Vivado RTL Analysis

On voit sur ce schéma que le compteur counter est modélisé avec des bascules D (ou flip-flop). La sortie Q d’une bascule est remise à zéro si le signal CLR est actif. Sinon, elle reproduit l’état de son entrée D en ne répercutant les changements que sur front montant de l’horloge. Cette partie du circuit à logique séquentielle est donc capable de « mémoriser » un état jusqu’à un moment précis qui est le prochain front montant de l’horloge.

La condition if du code s’est traduite par un multiplexeur (ou sélecteur) repéré RTL_MUX et qui dirige l’une de ses deux entrées vers la sortie en fonction de l’état 0 ou 1 du signal de sélection S.

Une fois le projet synthétisé, ces blocs fonctionnels deviennent essentiellement des LUTLookup Table et des bascules flip-flop. 90 cellules logiques sont nécessaires pour ce projet :

Image non disponible
Vivado Synthesis

Le schéma n’est pas très sympathique… Si on zoome sur l’état des LED à droite du schéma, on voit que le système a décidé de diriger la sortie O d’une LUT à 5 entrées vers la LED qui clignote :

Image non disponible
Vivado Synthesis - vue détaillée

L’équation logique de cette LUT est même donnée : O = I0 + I1 & I4 + !I1 & I2 & I3 & I4

Évidemment, il faudra creuser le schéma en amont pour comprendre d’où vient cette équation.

Le schéma (partiel) d’implémentation physique ci-dessous montre en surbrillance la LUT précédente et sa localisation dans le réseau des cellules logiques de la puce FPGA :

Image non disponible
Vivado implementation

Verilog est donc un langage de description de matériel. Une fois le code passé aux mains des outils de synthèse, la configuration du circuit qui sera implémenté physiquement dans la puce FPGA répondra aux effets décrits par votre code. Si pour un microcontrôleur le code compilé est traduit en suite d’instructions CPU, le synthétiseur FPGA traduit le code en circuit logique matériel.

IV-B. Des processus concurrentiels

Et si on voulait faire clignoter deux LED, « en même temps » (c’est-à-dire en synchronisant les signaux sur la même horloge), mais à des fréquences différentes ? On pourrait dupliquer le bloc always, mettre un autre paramètre pour une fréquence de clignotement différente et diriger la sortie vers une autre LED du bandeau. Les processus always fonctionnent effectivement de façon concurrente (en parallèle).

Mais plutôt que dupliquer du code, on le mettra dans un sous-module paramétré. Dans les sources du projet, on ajoutera le sous-module blinker.v suivant :

blinker.v
Sélectionnez
module blinker #(parameter PERIOD = 100_000_000) (
    input clk,  // clock
    input rst,  // reset
    output out
  );

    localparam WIDTH = 28; // 28 bits, compteur jusqu'à 2^28 - 1 maximum
    reg [WIDTH-1 : 0] counter; 
    
    assign out = counter > (PERIOD >> 1);
    
    always @(posedge clk or posedge rst) begin
      if (rst) begin
        counter <= 0;
      end else begin
        if (counter < PERIOD) begin
          counter <= counter + 1;
        end else begin
          counter <= 0;
        end
      end
    end
  
endmodule

Dans le module principal, on instancie deux blinkers (nommés blinker_slow et blinker_fast) dont on connecte les entrées-sorties (description structurelle) :

au_top.v
Sélectionnez
module au_top(
    input clk,              // 100MHz clock
    input rst_n,            // reset button (active low)
    output [7:0] led,       // 8 user controllable LEDs
    input usb_rx,           // USB->Serial input
    output usb_tx           // USB->Serial output
    );
    
    wire rst;
    
    // The reset conditioner is used to synchronize the reset signal to the FPGA
    // clock. This ensures the entire FPGA comes out of reset at the same time.
    reset_conditioner reset_conditioner(.clk(clk), .in(!rst_n), .out(rst));

    assign usb_tx = usb_rx;  // echo the serial data
    
    wire slow_blink, fast_blink;
    
    blinker #(.PERIOD(100_000_000)) blinker_slow (.clk(clk), .rst(rst), .out(slow_blink));
    blinker #(.PERIOD(50_000_000))  blinker_fast (.clk(clk), .rst(rst), .out(fast_blink));
    
    assign led = {slow_blink, 6'h0, fast_blink};
    
endmodule

Le schéma-blocs sous Vivado donne la structure suivante :

Image non disponible

Par contre, même si on évite la duplication de code, il y aura bien deux circuits quasiment identiques qui seront synthétisés.

Avec la même facilité, vous pourriez faire clignoter en parallèle une troisième, une quatrième LED, voire plus, à des fréquences différentes…

Exercice : faire le même exercice de clignotement de LED en parallèle avec un microcontrôleur ?

La LED en bas clignote à une fréquence de 1 Hz. Celle du haut clignote deux fois plus vite.

Verilog permet donc d’implanter des traitements dans des circuits logiques matériels, sans CPU, mais programmés de façon algorithmique.

V. Programmation d’un pilote en Verilog pour le capteur Si7021

L’étude qui suit concerne maintenant la description d’un pilote gérant la communication I2C avec le capteur de température (et d’humidité) Si7021.

Le code source complet de l’étude est donné en fin de chapitreSources du projet complet.

On commence par une photo du montage :

Photo du montage : carte Alchitry Au  et module Adafruit Si7021

Le module Si7021 du fabricant Adafruit comporte des connecteurs au format Qwiic de SparkFun. Un connecteur Qwiic est également présent sur la carte Alchitry Au depuis que la production des cartes de la plateforme Alchitry a été confiée au groupe SparkFun.

Avec le cordon Qwiic approprié (4 broches JST), la connexion I2C devient aisée. Le deuxième connecteur sur le module Si7021 peut servir à relier un autre composant sur le bus I2C. Dans ce tutoriel, il aura servi à connecter l’un de ces analyseurs logiques à bas coût pour observer les trames échangées sur le bus. Les copies d’écran des trames qui suivent ont été obtenues par ce moyen.

V-A. Ajout des ports supplémentaires SCL et SDA

D’après la documentation, les broches SCL (Serial Clock) et SDA (Serial Data) du connecteur Qwiic sont connectées respectivement aux ports référencés A24 et A23. Comme ce connecteur Qwiic a été rajouté plus tard par SparkFun, cette contrainte n’est pas renseignée par défaut dans l’EDI.

Afin d’y remédier, commencez par rajouter au projet un fichier dit « des contraintes » au format ACF (Alchitry Constraints File), avec ces deux lignes :

 
Sélectionnez
pin scl A24;
pin sda A23;
Alchitry Constraints File

Dans la définition des entrées-sorties du module principal, vous pouvez maintenant rajouter les deux ports scl et sda. Le système sait maintenant que ces deux ports sont bien dirigés vers les connecteurs Qwiic de la carte :

au_top.v
Sélectionnez
module au_top (
    input clk,              // horloge 100 MHz
    input rst_n,            // bouton Reset, actif à l'état bas
    output [7:0] led,       // jeu de LED x 8, non utilisé
    input  usb_rx,          // liaison série USB : Rx, non utilisé          
    output usb_tx,          // liaison série USB : Tx
    output scl,             // Serial Clock I2C, générée par le maître
    inout sda               // Serial Data I2C, /!\ SDA, port bidirectionnel
  );

// etc.

La broche SDA est bidirectionnelle (inout), les données peuvent circuler du maître vers l’esclave et inversement.

V-B. Composants supplémentaires

Vous avez déjà rencontré précédemment le module reset_conditioner, mais deux autres modules sont nécessaires encore :

  • i2c_controller : contrôleur I2C pour communiquer avec le capteur ;
  • uart_tx : contrôleur UART pour transmettre des données série via le câble USB.
Component selector
Sélection de composants supplémentaires

Le code de ces modules est aussi sur github/alchitry/Alchitry-Labs/library/components/. Ces codes sont écrits en langage Lucid, mais transpilés en Verilog à la génération du projet dans l’EDI Alchitry Labs. La suite Vivado propose également ses bibliothèques certifiées de composants réutilisables (les Intellectual property (IP) cores). Les composants réécrits pour Alchitry Labs en sont souvent des versions simplifiées, plus facilement accessibles aux débutants.

V-C. Dialogue I2C

La séquence pour obtenir la température est décrite dans la datasheet du capteur Si7021 :

Image non disponible

Les parties de la séquence avec un fond blanc sont générées par le composant maître (master), c’est-à-dire la carte FPGA toujours à l’initiative de la communication. Les parties avec un fond gris sont générées par le composant esclave (slave), c’est-à-dire le capteur de température Si7021 qui répond aux sollicitations du maître.

  • S : Start.
  • P : Stop.
  • Sr : Start Repeated, un Start qui n’est pas précédé d’un Stop.
  • A, NA : acquittement ou non-acquittement.
  • Slave Adress : adresse du composant sur 7 bits, ici 0x40.
  • Slave Adress + W : demande d’accès au composant en écriture.
  • Slave Adress + R : demande d’accès au composant en lecture.
  • Measure Cmd : le code de la commande pour obtenir la température, ici 0xF3.

Vous trouverez des explications plus détaillées dans le tutoriel de Michel Semal.

Par exemple, le début de la séquence avec le code 0xF3 pour obtenir la température donnera la trame suivante :

Image non disponible
Image non disponible

Le point vert marque le bit de Start généré par le maître, c’est-à-dire un front descendant du signal SDA lorsque l’horloge SCL est à l’état haut. Une demande d’accès en écriture est ensuite réclamée au capteur Si7021 à l’adresse 0x40. Le composant esclave doit une première fois acquitter, informant ainsi le composant maître qu’il a bien reçu la demande et qu’il est disposé à recevoir des données. Pour acquitter, une période d’horloge supplémentaire est générée par le composant maître, et le composant esclave doit abaisser l’état de la ligne SDA.

Le code 0xF3 (Measure Temperature) est alors transmis au capteur. Lorsque l’octet est transmis, le module acquitte (ACK) à nouveau pour accuser réception.

Vous trouverez les spécifications du bus I2C en recherchant le document I2C-bus specification and user manual (Rev. 6 — 4 April 2014) sur le site du fabricant de semi-conducteurs NXP qui maintient la norme.

Le contrôleur I2C proposé dans la bibliothèque de composants de l’EDI Alchitry Labs qui va générer les trames se présente comme suit :

Image non disponible
Contrôleur I2C : à gauche les entrées, à droite les sorties (SDA est un port bidirectionnel)

Fonctionnement du contrôleur I2C

Chaque transaction I2C commence par un Start que l’on déclenche en mettant à l’état haut l’entrée start pendant une période de l’horloge clk. Vous pouvez alors écrire ou lire sur le bus autant de fois que nécessaire. Pour terminer la transaction, vous devez mettre l’entrée stop à l’état haut pendant une période de l’horloge clk.

Chaque fois que vous voulez effectuer une opération sur le bus (start, stop, read, write), vous devez vous assurer au préalable en regardant l’état du signal busy que la ligne n’est pas déjà occupée avec une autre opération. Si vous ne tenez pas compte de cela, l’opération demandée risque d’être ignorée.

Classiquement, une transaction démarre par une opération d’écriture d’un octet comprenant l’adresse du composant esclave sur 7 bits, suivie d’un bit pour indiquer si l’opération est une lecture ou une écriture sur le bus. Voir la documentation du composant esclave pour comprendre la séquence des échanges. Les échanges en écriture ou lecture se poursuivent.

Quand le composant maître opère en lecture sur la ligne, vous devez préciser si celui-ci doit acquitter ou non à chaque octet reçu (ack_read à l’état haut si oui, à l’état bas sinon).

Quand le composant maître opère en écriture sur la ligne, l’état du bit ack_write vous indiquera si le composant esclave a bien acquitté ou non. (Attention : ack_write à l’état bas si acquittement, la documentation annonce l’état inverse ?!).

Code d’instanciation du contrôleur en Verilog :

au_top.v (extrait)
Sélectionnez
/* ---- Instantiacion & connexion du contrôleur I2C ----------------------------------*/  
    reg i2c_start;            // signal Start (S) ou Start Repeated (Sr)
    reg i2c_stop;             // signal Stop  (P)
    wire i2c_busy;            // =1 si le bus I2C est occupé
    wire i2c_out_valid;       // =1 quand la donnée renvoyée (1 octet) en sortie est valide et disponible
    reg [7:0] i2c_data_in;    // octet à transmettre (maître vers esclave)
    wire [7:0] i2c_data_out;  // octet renvoyé (esclave vers maître)
    reg i2c_write;            // =1, demande d'écriture par le maître sur le bus
    reg i2c_read;             // =1, demande de lecture par le maître sur le bus
    reg i2c_ack_read;         // =1 si le maître doit acquitter
    wire i2c_ack_write_n, i2c_ack_write;  // sortie ack_write du contrôleur=0 si l'esclave acquitte, /!\ erreur dans la doc.
    
    assign i2c_ack_write = !i2c_ack_write_n;
       
    i2c_controller #(.CLK_DIV(9)) Si7021_controller ( // fréquence clk = 100_000_000 / 2^9 = 195,3 kHz
      .clk(clk),
      .rst(rst),
      .scl(scl),
      .sda(sda),
      .start(i2c_start),
      .stop(i2c_stop),
      .busy(i2c_busy),
      .data_in(i2c_data_in),
      .data_out(i2c_data_out),
      .write(i2c_write),
      .read(i2c_read),
      .ack_write(i2c_ack_write_n),
      .ack_read(i2c_ack_read),
      .out_valid(i2c_out_valid)
    );   
    
    localparam [7:0] I2C_ADR_Si7021 = 8'h40, // adresse du composant I2C Si7021 = 0x40
                     I2C_ADR_Si7021_Read  = (I2C_ADR_Si7021 << 1) + 1, 
                     I2C_ADR_Si7021_Write = (I2C_ADR_Si7021 << 1);
    /* ------------------------------------------------------------------------------- */

V-D. Description à la façon d’une machine à états finis

Si on reprend l’extrait de la séquence précédente, on peut en faire une description avec le diagramme d’états-transitions suivant :

Image non disponible
Image non disponible

Les rectangles définissent les états de la machine. La transition entre deux états s’opère si la condition portant sur les entrées, à gauche du /, est vérifiée. À droite du signe /, on trouve l’état des sorties sur la transition. Par exemple, en supposant que la machine rentre à l’état state_I2C_START : après une temporisation de 1 s, on met la sortie start à l’état haut, et la machine passe à l’état suivant state_I2C_ADRW. Lorsque la condition est à true, la transition s’opère toujours, quel que soit l’état des entrées. Ici, on ne vérifie pas si le module a bien acquitté ou non.

Pour obtenir la valeur de température, il faut ensuite faire une demande en lecture (après un Start Repeated). Mais la conversion analogique-numérique prend un peu de temps (7 ms d’après la documentation), et le module refuse d’acquitter (NA) tant qu’elle n’est pas terminée.

Image non disponible

Pour ce tutoriel, on choisit de renouveler la demande de lecture toutes les 2 ms, jusqu’à ce que le module finisse par acquitter (typiquement au bout de quatre tentatives, soit 8 ms). Ce qui donne la description suivante :

Image non disponible

On voit dans cette description que les sorties de la machine dépendent de l’état en cours, mais aussi des entrées. Cette description correspond à celle d’une machine de Mealy.

Quand le module acquitte (i2c_ack_write), le composant maître passe en lecture pour récupérer les données (les trois octets MSB, LSB et CHKSUM), puis marque la fin de la transaction :

Image non disponible

Le document suivant récapitule le diagramme états-transitions complet de la machine de Mealy utilisé dans ce tutoriel, accompagné des trames I2C obtenues à l’analyseur logique :

Machine à états finis - dialogue Si7021 : format svg - format png

V-E. Programmation de la machine à états finis en Verilog

Le code Verilog ci-dessous est un gabarit pour gérer une machine de Mealy :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
module au_top (
    input clk,              // horloge 100 MHz
    input rst_n,            // bouton Reset, actif à l'état bas
    output [7:0] led,       // jeu de LED x 8, non utilisé
    input  usb_rx,          // liaison série USB : Rx, non utilisé          
    output usb_tx,          // liaison série USB : Tx
    output scl,             // Serial Clock I2C, générée par le maître
    inout sda               // Serial Data I2C, /!\ SDA, port bidirectionnel
  );

  // etc.

 /* ------ Gestion de la machine à états finis ------------------------------------ */
    reg [3:0] I2C_State, I2C_State_Next;
    
    localparam state_I2C_RESET_START   = 4'd0,
               state_I2C_RESET_ADRW    = 4'd1,
               state_I2C_RESET_CMD     = 4'd2,
               state_I2C_RESET_STOP    = 4'd3,    
               state_I2C_START         = 4'd4,
               state_I2C_ADRW          = 4'd5,
               state_I2C_START_RE      = 4'd6,        
               state_I2C_ADRR          = 4'd7,
               state_I2C_MEASURE_CMD   = 4'd8,
               state_I2C_READ_MSB      = 4'd9,
               state_I2C_READ_LSB      = 4'd10,
               state_I2C_READ_CHKSUM   = 4'd11,
               state_I2C_STOP          = 4'd12;

    always @(posedge clk or posedge rst) begin
      if (rst) begin
        I2C_State <= state_I2C_RESET_START;
      end else begin
        if (!i2c_busy) begin            // si pas d'action en cours sur le bus I2C,
          I2C_State <= I2C_State_Next;  // passage à l'état suivant, synchronisé avec l'horloge
        end
      end
    end

    always @(I2C_state, ...) begin
      I2C_State_Next = I2C_State; // par défaut, conserver l’état
      
      // valeurs par défaut 
      i2c_stop = 0;
      i2c_write = 0;
      i2c_read = 0;
      i2c_start = 0;
      i2c_data_in = 0;
      i2c_ack_read = 0;
      temperature_enable = 0;
          
      case (I2C_State)
                  
        state_I2C_RESET_START : begin
          if (<condition>) begin
            // ...
            I2C_State_Next = state_I2C_RESET_ADRW;
            // ...
          end // else begin ... 
        end       
  
        state_I2C_RESET_ADRW : begin
          if (<condition>) begin
            // ...
            I2C_State_Next = state_I2C_RESET_CMD;
            // …
          end // else begin ...                     
        end
          
        // etc. pour les autres états       
          
        state_I2C_STOP : begin
          if (<condition>) begin
            // ...
            I2C_State_Next = state_I2C_START;
            // …
          end // else begin ... 
        end
          
      endcase
 
    end 
           
endmodule

Deux processus always s’exécutent en parallèle (Verilog est un langage « concurrent » contrairement à la plupart des langages comme C, C++, Java, etc. qui sont des langages séquentiels par nature).

  • Lignes 30 à 38 : le premier processus always est défini avec la « liste de sensibilité » posedge clk or posedge rst. Ce processus est donc évalué continuellement à chaque front montant de l’horloge principale ou chaque front montant du signal rst (détection d’appui sur le bouton-poussoir Reset). Ce processus séquentiel met à jour l’état présent (I2C_State) par l’état futur (I2C_State_Next) sur front montant de l’horloge principale 100 MHz, et à condition que le bus I2C soit libre (i2c_busy).
  • Lignes 40 à 82 : ce deuxième processus combinatoire suit la logique du diagramme états-transitions avec une instruction case. Il calcule l’état des sorties ainsi que l’état futur à partir des entrées et de l’état présent. Par exemple, avec l’extrait du diagramme suivant :
Image non disponible

Pour l’état State_I2C_READ_MSB, on voit deux situations selon que le module acquitte ou non. Si le module acquitte, on descend dans le diagramme pour récupérer les données disponibles de température en commençant par l’octet MSB de poids fort. Sinon, on doit remonter à un état antérieur pour demander à nouveau si la température est disponible. Les valeurs des sorties dépendent donc de l’état en cours, mais aussi des valeurs des entrées que l’on teste avec un if. Ce qui donne pour cet état :

 
Sélectionnez
      case (I2C_State)

        // …
                
        state_I2C_READ_MSB : begin
            if (!i2c_ack_write) begin // Si NAK, la conversion n'est pas terminée
              I2C_State_Next = state_I2C_START_RE;
            end else begin  // Sinon si ACK, la conversion est terminée
              i2c_read = 1; // Lecture Temperature MSB
              i2c_ack_read = 1;
              I2C_State_Next = state_I2C_READ_LSB;
            end
        end

        // …

      endcase

V-F. États temporisés

Dans le diagramme d’états-transitions, vous remarquerez plusieurs états qui sont temporisés.

Par exemple, à l’état state_I2C_START ci-dessous, il y a une temporisation de 1 s avant de passer à l’état suivant. Cette temporisation permet ainsi de cadencer les acquisitions de température à intervalles réguliers :

Image non disponible

Pour programmer cette temporisation en Verilog, on passera par un troisième processus concurrent :

 
Sélectionnez
    always @(posedge clk) begin
      if (I2C_State != I2C_State_Next) begin // remise à zéro du chrono à chaque changement d'état
        delay <= 0;
      end else begin
        delay <= delay + 1;
      end                                                                                    
    end

Un compteur delay est incrémenté à chaque front montant de l’horloge principale 100 MHz, et remis à zéro à chaque changement d’état. Pour temporiser un état, il suffit de tester la valeur du compteur à l’entrée de l’état :

 
Sélectionnez
        // ...
        
        state_I2C_START : begin
          if (delay > 100_000_000) begin // 1 mesure par seconde
            i2c_start = 1;
            I2C_State_Next = state_I2C_ADRW;
          end                 
        end

        // ...

V-G. Fonctionnement du système

Pour vérifier le fonctionnement du système, on visualise les températures dans un terminal série comme RealTerm (115 200 bauds, 8 bits de données, 1 bit de stop, sans parité).

Image non disponible

Une acquisition de température est lancée chaque seconde avant de s’afficher sur une ligne du terminal série.

L’analyseur logique à bas coût ci-dessous (Saleae ou compatible, 8 canaux, 24 MHz) permet de visualiser les trames échangées :

Image non disponible

On voit dans la trame qui suit qu’une acquisition de température est lancée toutes les secondes :

Image non disponible

Une acquisition prend 8 ms environ (relance de lecture des données toutes les 2 ms) :

Image non disponible

Au début, le module n’acquitte pas (NAK), car la conversion analogique-numérique de la température n’est pas terminée :

Image non disponible

Au bout de 8 ms, le module finit par acquitter, les données image de la température sont alors lues :

Image non disponible

Les deux premiers octets (CodeMeasure = 0x6610) permettent de calculer la valeur de température en °C avec la formule donnée dans la documentation du capteur Si7021 :

kitxmlcodelatexdvpTemperature(^{o}C) = \frac{175,72 \times CodeMeasure}{65536}-46,85 finkitxmlcodelatexdvp

Soit environ 23,2 °C.

Verilog peut synthétiser des formules complexes :

 
Sélectionnez
    always @(posedge i2c_out_valid) begin // si 1 octet est retourné par le capteur 
      raw_data[7:0] <= i2c_data_out;      // on le décale dans le registre raw_data
      raw_data[23:8] <= raw_data[15:0];
    end         
      
    always @(posedge clk) begin
      if (temperature_enable) begin
        temperature_32bits = ((raw_data[23:8] * 1757) >> 16) - 469; // calcul de T(°C)x10, voir datasheet Si7021
        // Au besoin, CheckSum est dans raw_data[7:0]
      end 
    end

Quand un octet est retourné par le capteur, il est décalé dans un registre 24 bits raw_data[23:0]. Quand la transaction est terminée, temperature_enable passe à 1, et on calcule une valeur entière 32 bits donnant la température multipliée par 10. Par exemple, pour une température de 23,21 °C, la valeur calculée sera égale à 232. Ce sera au dispositif client recevant et affichant les données de faire la division par 10, les arrondis nécessaires ou de placer la virgule.

Le troisième octet est une somme de contrôle (CheckSum), non exploitée dans ce tutoriel, mais qui sert normalement à vérifier l’intégrité des données transmises.

V-H. Sources du projet complet

On rappelle l’architecture du projet dans Alchitry Labs :

Image non disponible

Dans les sources ci-dessous :

  • au_top.v : module principal en Verilog.
  • serialTemperatureDisplay.v : module prenant en charge la communication série via le port USB pour affichage de la température (115 200 bauds) dans un terminal. La séquence de préparation et d’envoi des données est aussi gérée à la façon d’une machine à états finis.

Dans les composants (Components), les modules sont écrits en Lucid (langage Verilog simplifié), mais transpilés en Verilog à la génération du projet. :

  • reset_conditioner.luc : conditionnement et synchronisation du signal du bouton Reset en surface de la carte.
  • i2c_controller.luc : contrôleur pour la communication I2C avec la capteur.
  • uart_tx.luc : contrôleur pour l’envoi de données série UART.

Dans les contraintes : en plus des fichiers constraints par défaut, le fichier io.acf donne la localisation des broches SDA et SCL du connecteur Qwiic.

io.acf
Sélectionnez
// I2C Serial Clock (scl) & Serial Data (sda)
pin scl A24;
pin sda A23;
au_top.v
Cacher/Afficher le codeSélectionnez
SerialTemperatureDisplay.v
Cacher/Afficher le codeSélectionnez

VI. Conclusion

Une conséquence de la nature des FPGA est que les tâches configurées dans les circuits peuvent être hautement parallélisées. Rien ne vous empêche de configurer votre FPGA avec 20 générateurs de signaux PWM ou 5 ports série UART si vous en avez besoin, alors que le nombre de ces périphériques est figé et limité dans un microcontrôleur classique. Sur microcontrôleur, pour simuler un fonctionnement multitâche, vous devez jouer avec les interruptions, programmer des machines à états finis ou passer par un OS temps réel, car les traitements du processeur sont séquentiels par nature. Beaucoup d’algorithmes de traitement d’images par exemple sont naturellement parallèles et méritent une mise en œuvre sur FPGA.

La dernière version du langage Verilog est sortie en 2005, mais il y a toutefois des évolutions récentes du langage avec SystemVerilog, dont Verilog est maintenant un sous-ensemble. Même si les européens lui préfèrent le langage VHDL, la syntaxe de certaines expressions Verilog est très appréciée des développeurs familiers avec les langages C ou C++.

Dans ce tutoriel, vous avez pu aussi vous rendre compte du haut niveau d’abstraction permis par Verilog et découvrir différents styles de description dans les projets allant de la description structurelle jusqu’au plus haut niveau d’abstraction avec la description comportementale :

  • description structurelle : assemblage de primitives et blocs logiques que l’on interconnecte ;
  • description comportementale : le circuit est décrit non plus par sa structure, mais par son comportement qui peut être spécifié de façon procédurale, comme les algorithmes programmés sur les ordinateurs.

Notez que les codes ont été écrits en Verilog 2001 standard, et que les sources du projet peuvent être repris intégralement pour toute autre carte FPGA configurable avec ce langage (à condition de redéfinir la configuration et la localisation des entrées-sorties de la carte et tenir compte des nouvelles caractéristiques d’horloge).

Les FPGA ne conviennent pas forcément à toutes les utilisations, mais ils constituent de puissants outils quand les applications exigent de la vitesse de traitement et de la flexibilité (interface intelligente, intelligence artificielle, apprentissage automatique, traitement d’images et de vidéos, accélération d’algorithme, etc.) Les solutions optimales reposent maintenant sur la combinaison de processeurs et de FPGA, voire des FPGA intégrant des cœurs de processeurs matériels.

Vous êtes maintenant prêts à vous lancer dans de nouveaux projets…

Je remercie escartefigue pour la relecture orthographique.

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 © 2021 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.