Apprendre à développer sur FPGA avec Intel Quartus Prime

Communication SPI avec un convertisseur Analogique-Numérique, simulation fonctionnelle et analyse des signaux

f-leb

La carte FPGA du kit DE0-Nano intègre en surface un convertisseur Analogique-Numérique en liaison SPI (Serial Peripheral Interface) avec la puce FPGA (Cyclone IV). On propose de s’exercer en langage Verilog pour communiquer avec le convertisseur en s’aidant des outils d’Intel Quartus Prime pour faire des simulations et analyser les signaux logiques.

Commentez

- Titre : Apprendre à développer sur FPGA avec Intel Quartus Prime

- Auteur : f-leb

- Parution : 22 août 2023

- Licence : 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 ® 2023 f-leb. Aucune reproduction, même partielle, ne peut être faite de ce site et 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.

Voir également :

- Débuter avec les FPGA dans l’environnement Intel Quartus Prime

- Programmer un contrôleur pour écran VGA avec une carte de développement FPGA

- Cours et tutoriels FPGA - contributions personnelles

Sommaire

- I. Introduction

- II. La cible FPGA et le convertisseur A/N

- III. Communication SPI avec le convertisseur A/N

- IV. Configuration initiale du projet Quartus Prime

- V. Création du module d’interface SPI et premières simulations

- V-A. Déclaration du module

- V-B. Les bancs de test (ou test benches)

- V-C. Simulation fonctionnelle

- VI. Description du projet par schéma-blocs

- VII. Utilisation de l’analyseur logique intégré à Quartus Prime

- VIII. Module d’interface SPI au complet

- IX. Le « banc de test » complet

- X. Finalisation du projet : visualisation avec le bandeau de LED

- XI. Conclusion

- XII. Annexe : fichiers sources

I - Introduction

Pour commencer, voici une courte vidéo de démonstration du projet réalisé dans ce tutoriel (les fichiers sources sont fournis en annexe) :

Animation en format "youtube" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://www.youtube.com/watch?v=adZ5xRQz5lg

Un potentiomètre rotatif est connecté à un port situé sous la carte. Son curseur est relié à une entrée analogique d’un convertisseur Analogique-Numérique (A/N) soudé en surface de la carte FPGA DE0-Nano. En tournant le bouton du potentiomètre, on active les LED qui s’animent comme un bargraphe pour indiquer le niveau de tension sur l’entrée analogique.

Voilà le genre de projets sans réelles difficultés si la carte était une carte à microcontrôleur, surtout si c’était une Arduino. La programmation d’un FPGA est bien différente de celle d’un microcontrôleur, mais j’ai déjà évoqué aussi ses avantages, ici et là, notamment en matière de flexibilité et de traitements parallèles. Dans le cas d’un contrôleur pour écran VGA (640x480@60Hz) par exemple, un FPGA est un excellent choix.

La vidéo montre également en arrière-plan l’écran du PC de développement avec les chronogrammes des signaux comme sur l’écran d’un analyseur logique ou d’un oscilloscope.

Pour vous familiariser avec l’environnement de développement sur FPGA tel que celui d’Intel Quartus Prime, je vous recommande de commencer avec le tutoriel Débuter avec les FPGA dans l’environnement Intel Quartus Prime.

II - La cible FPGA et le convertisseur A/N

J’ai déjà présenté le kit DE0-Nano lors d’un tutoriel précédent. La carte intègre en surface un convertisseur Analogique-Numérique 12 bits (réf : ADC128S022) à 8 entrées, en surbrillance et en bas à droite sur l’image ci-dessous.

Image d’après « DE0-Nano User Manual »

La puce FPGA Cyclone IV communique avec le convertisseur A/N grâce aux quatre fils ADC_CS_N, ADC_SADDR, ADC_SDAT, et ADC_SCLK permettant d’établir une communication série compatible SPI (Serial Peripheral Interface) :

Image d'après « DE0-Nano User Manual »

On voit aussi sur le schéma que les huit entrées analogiques du convertisseur A/N (IN0 à IN7) sont accessibles par un port (header) 2x13 broches sur le dessous de la carte. Pour la démonstration, dans ce tutoriel, on pourra câbler un potentiomètre rotatif 10k, par exemple sur l’entrée analogique 5, et faire évoluer la tension entre 0 et 3,3 V en tournant le bouton :

Image d'après « DE0-Nano User Manual »

III - Communication SPI avec le convertisseur A/N

Comme tout développeur sur systèmes embarqués, vous allez commencer par parcourir la documentation (datasheet) du convertisseur A/N : ADC128S022 datasheet. Le protocole de communication qui nous intéresse est décrit par des chronogrammes :

d'après Fig.1 ADC 128S022 Operational Timing Diagram

La communication peut débuter en abaissant le signal /CS (Chip Select) du composant avec qui l’on veut communiquer. Le signal d’horloge SCLK (Serial Clock) pour synchroniser les événements est initialement au repos à l’état haut, puis est activé. À l’entrée DIN du convertisseur A/N, on doit présenter les trois bits de sélection de l’entrée analogique sur front descendant de l’horloge (ADD2, ADD1, et ADD0, les 3 bits pour sélectionner l’une des 2^3=8 entrées du convertisseur A/N), et à partir du front descendant repéré 3. Le contrôleur du convertisseur A/N va acquérir l’état de ces bits sur front montant de l’horloge. Pendant ce temps-là, le convertisseur A/N « pousse » des zéros sur sa sortie DOUT. À partir du front descendant repéré 5, les 12 bits DB11 à DB0 du résultat de la conversion sur l’entrée sélectionnée sont « poussés » sur DOUT, et il faut récupérer l’état des bits sur front montant de l’horloge. Le processus d’acquisition de la conversion a consommé ainsi 16 cycles d’horloge et peut reprendre indéfiniment.
Le constructeur recommandant une fréquence d’horloge maximale à 3,2 MHz, on peut acquérir ainsi jusqu’à 3,2.10^6 / 16 = 200 000 échantillons par seconde (200 ksps, ou kilo samples per second).

Même si le protocole est assez simple à s’approprier, il reste que la difficulté de l’exercice est de respecter au cycle d’horloge près le timing de ces diagrammes.

IV - Configuration initiale du projet Quartus Prime

Le constructeur Altera (racheté par Intel) du kit DE0-Nano propose un utilitaire de configuration initiale d’un projet sous Quartus Prime avec l’exécutable DE0_Nano_SystemBuilder.exe.

On coche l’horloge CLOCK, le convertisseur ADC (Analog-Digital Converter) et le bandeau de LED x 8. Les LED serviront à reproduire visuellement l’état de l’entrée analogique sous forme de bargraphe.

En cliquant sur le bouton [Generate], vous créez un nouveau dossier (ici adc_project4dvp) avec quelques fichiers portant la configuration minimale du projet. Vous trouverez plus de détails au chapitre III. Configuration initiale du projet d’un précédent tutoriel.

Vous pouvez maintenant ouvrir le fichier du projet adc_project4dvp.qpf (Quartus Project File).

J’utilise à ce jour Quartus Prime 20.1 Lite. Il y aura peut-être quelques adaptations à faire si vous prenez une version plus récente. Voir Télécharger le logiciel Intel® Quartus® Prime.

Si vous allez dans le menu Assignments → Pin planner, vous constaterez que la cible et les entrées-sorties utiles du projet sont déjà configurées. Vous pouvez maintenant faire référence à une entrée-sortie en citant nommément le nœud : CLOCK_50 pour l’horloge principale 50 MHz, LED[0] à LED[7] pour les sorties dirigées vers le bandeau de LED et ADC_xxxxx pour les entrées-sorties dirigées vers le convertisseur A/N.

Assignments → Pin planner

V - Création du module d’interface SPI et premières simulations

On commence par un premier module en Verilog : menu File → New… → Verilog HDL File que l’on sauvegardera sous le nom spi_interface.v (menu File → Save As…).

Schématiquement, ce module se présente de la façon suivante :

Les entrées :

- clk : l’horloge à 3,2 MHz dérivée de l’horloge principale 50 MHz ;

- sdat : le signal des données poussées par le convertisseur A/N sur sa sortie DOUT.

Les sorties :

- cs_n : signal Chip Select ;

- data_out[11..0] : bus 12 bits, résultat de la conversion A/N ;

- end_of_conversion : une impulsion générée tous les 16 cycles d’horloge quand une nouvelle donnée issue de la conversion est disponible sur le bus data_out[11..0] ;

- saddr : signal de sélection de l’entrée analogique (les 3 bits ADD2, ADD1 et ADD0) à diriger vers l’entrée DIN du convertisseur A/N ;

- sclk : le signal d’horloge vers le convertisseur A/N pour la synchronisation.

Sur la figure ci-dessous, on montre schématiquement l’implémentation du bloc spi_interface du module dans la puce FPGA, ainsi que le raccordement des entrées et sorties au convertisseur A/N.

V-A - Déclaration du module

En Verilog, on déclare le module comme suit :

spi-interface.v

module spi_interface
#(
parameter [2:0] ADD = 3'b101    // <ADD2><ADD1><ADD0> = 101, soit Analog Input 5
)

(
input clk,                      // horloge jusqu'à 3,2MHz maxi
input sdat,                     // ADC Digital Data Output

output reg saddr,               // ADC Digital Data Input
output wire sclk,               // ADC Digital Clock Input
output wire cs_n,               // ADC Chip Select
output wire [11:0] data_out,    // Résultat de la conversion 12 bits
output wire end_of_conversion   // Impulsion synchro. End Of Conversion
);

// ---------------------------------------------------------------------------

// corps du module ici...

endmodule

Le paramètre ADD[2..0] contient le numéro de l’entrée analogique. Quand on instancie le module, l’entrée par défaut est la n° 5 (en binaire, ADD = 3'b101 ).

On complète le module avec ses premiers signaux générés en sortie :

spi_interface.v

module spi_interface
#(
parameter [2:0] ADD = 3'b101    // <ADD2><ADD1><ADD0> = 101, soit Analog Input 5
)

(
input clk,                      // horloge jusqu'à 3,2MHz maxi
input sdat,                     // ADC Digital Data Output

output reg saddr,               // ADC Digital Data Input
output wire sclk,               // ADC Digital Clock Input
output wire cs_n,               // ADC Chip Select
output wire [11:0] data_out,    // Résultat de la conversion 12 bits
output wire end_of_conversion   // Impulsion synchro. End Of Conversion
);

// ---------------------------------------------------------------------------

reg [3:0] counter;  // compteur 4 bits interne
reg cs = 1'b0;

initial begin
counter = 0;
end

assign cs_n = ~cs;

assign sclk = cs_n ? 1'b1 : clk;

always @(negedge clk) begin
if (counter == 4'd1) begin
cs <= 1'b1;   // active Chip Select
end
end

always @(posedge clk) begin
counter <= counter + 4'd1;
end

endmodule

- Un compteur 4 bits counter démarre à zéro et s’incrémente à chaque front montant de l’horloge.

- Le signal /CS sera abaissé sur front descendant de l’horloge, lorsque le compteur sera égal à 1. Comme il n’est jamais remonté à l’état haut, il restera abaissé en permanence (verrou).

- L’assignation assign sclk = cs_n ? 1'b1 : clk; permet de produire le signal d’horloge à partir du moment où le signal /CS est abaissé (grâce à l’opérateur ternaire condition ? Valeur_si_vrai : Valeur_si_faux, comme en langage C).

V-B - Les bancs de test (ou test benches)

Pour tester le fonctionnement de cette première version du module, on peut faire une simulation fonctionnelle. La simulation permet de vérifier le comportement du circuit logique qui sera implémenté plus tard dans la puce FPGA en réponse à des stimuli en entrée, sans avoir recours à la cible FPGA matérielle. Elle est très utilisée pour détecter les erreurs de conception ou pour optimiser les performances.

Afin d’éprouver ou optimiser un module par simulation, on a besoin de le tester avec différents scénarios, en générant et traitant des signaux et stimuli un peu complexes. On peut alors recourir à des « bancs de test » ou test benches.

Notre test bench sera créé dans un module testbench.v qui va instancier le module à tester spi_interface, raccorder les signaux et stimuler les entrées :

testbench.v

`timescale 10ns/100ps
// unité de temps = 10ns, résolution = 100ps

module testbench;

reg clock;
wire sclk_sig;
wire cs_n_sig;

reg  sdat_sig;
wire [11:0] data_out_sig;
wire  end_of_conversion_sig;
wire saddr_sig;

localparam period = 100;  //période = 100 unités de temps, soit 100 x 10ns = 1us

initial begin
clock = 1'b0;
sdat_sig = 1'b0;
end

always begin
#(period/2) clock = ~clock; // génération signal d'horloge
end

spi_interface spi_inst
(
.clk(clock),
.sclk(sclk_sig),
.cs_n(cs_n_sig),
.sdat(sdat_sig),
.data_out(data_out_sig),
.end_of_conversion(end_of_conversion_sig),
.saddr(saddr_sig)
);

endmodule

On stimule l’instance avec un signal d’horloge simulé (qui n’est pas à la fréquence souhaitée d’ailleurs, mais cela n’a pas d’importance ici, il s’agit uniquement de voir si les événements sont bien synchronisés sur les fronts de l’horloge), et on espère visualiser des signaux en sortie afin de les comparer aux signaux attendus.

Le projet comporte déjà deux fichiers sources en Verilog : testbench.v et spi_interface.v. C’est notre « banc de test » qui doit prendre le contrôle, pour cela, faites un clic droit sur testbench.v dans le navigateur à gauche, puis sélectionnez l’option Set as Top-Level Entity.

Vous devez ensuite cliquer sur le bouton Start Analysis & Elaboration de la barre d’outils, ce qui va lancer l’analyse du code source et l’élaboration d’un modèle fonctionnel du circuit logique.

Message : Analysis & Elaboration was successful

Ne vous inquiétez pas des avertissements (warnings) à cette étape, notre interface SPI et son « banc de test » sont encore incomplets avec des signaux non connectés. Ce qui est important, c’est d’avoir le message « Quartus Prime Analysis & Elaboration was successful. 0 errors… ». Si vous avez des erreurs signalées avec des messages en rouge, il faudra analyser les messages d’erreur et débusquer les bogues. Sans doute un oubli ou une erreur de syntaxe…

Un test bench est prévu pour être simulé, et non pour être synthétisé, c.-à-d qu’il ne peut pas être traduit en termes de circuit matériel implémenté physiquement dans la puce FPGA. La partie du code qui génère le signal d’horloge par exemple n’est pas « synthétisable ». Même si tout est décrit en langage Verilog, la sémantique du code est bien différente selon l’objectif : simulation logicielle ou synthèse pour implémentation dans le FPGA.

V-C - Simulation fonctionnelle

On utilisera ici le simulateur ModelSim qui est fourni avec Quartus Prime Lite, mais il y en a d’autres.

Commencez par vérifier l’outil de simulation et sa configuration en suivant les étapes ci-dessous :

Menu Tools → Options…

Menu Assignments → Settings…
Catégorie EDA Tool Settings

Menu Assignments → Settings…
Catégorie EDA Tool Settings → Simulation
Il faut renseigner ici le fichier testbench.v à prendre en compte lors du lancement de la simulation.

Pour démarrer la simulation, allez dans le menu Tools → Run Simulation Tool → RTL Simulation.

ModelSim devrait s’ouvrir et exécuter automatiquement le test bench renseigné. En zoomant sur les premières étapes de la simulation dans la fenêtre Wave, les chronogrammes montrent les résultats espérés :

- /testbench/clock, en vert, est le signal d’horloge (stimulus).

- /testbench/cs_n_sig, en jaune, est le signal /CS (Chip Select) et il est bien abaissé au cycle d’horloge suivant (lorsque le compteur counter est égal à 1) sur front descendant de l’horloge.

- /testbench/sclk_sig, en rouge, est bien le signal d’horloge SCLK envoyé au convertisseur A/N. Au repos à l’état haut, l’horloge démarre quand /CS est abaissé.

Exercice

L’entrée analogique sélectionnée est ADD = 3'b101, soit l’entrée n° 5 par défaut. Complétez le code spi_interface.v avec un nouveau processus always de façon à produire le signal saddr de sélection de l’entrée analogique. On commence par le bit ADD[2] de poids fort qui doit être présenté sur front descendant de l’horloge lorsque le compteur counter est égal à 3.

Le résultat attendu en simulation doit être conforme aux chronogrammes suivants :

Signal saddr_sig avec la séquence 3'b101 de sélection de l'entrée analogique

Solution :

spi_interface.v

module spi_interface
#(
parameter [2:0] ADD = 3'b101    // <ADD2><ADD1><ADD0> = 101, soit Analog Input 5
)

(
input clk,                      // horloge jusqu'à 3,2MHz maxi
input sdat,                     // ADC Digital Data Output

output reg saddr,               // ADC Digital Data Input
output wire sclk,               // ADC Digital Clock Input
output wire cs_n,               // ADC Chip Select
output wire [11:0] data_out,    // Résultat de la conversion 12 bits
output wire end_of_conversion   // Impulsion synchro. End Of Conversion
);

// ---------------------------------------------------------------------------

reg [3:0] counter;  // compteur 4 bits interne
reg cs = 1'b0;

initial begin
counter = 0;
end

assign cs_n = ~cs;

assign sclk = cs_n ? 1'b1 : clk;

always @(negedge clk) begin
if (counter == 4'd1) begin
cs <= 1'b1;   // active Chip Select
end
end

always @(posedge clk) begin
counter <= counter + 4'd1;
end

always @(negedge clk) begin
saddr <= 1'b0;
case(counter)
3: begin
saddr <= ADD[2];
end
4: begin
saddr <= ADD[1];
end
5: begin
saddr <= ADD[0];
end
endcase
end

endmodule

Les simulations sur notre « banc de test » montrent de premiers résultats concluants, mais le travail n’est pas encore abouti, car le module sous test du fichier spi_interface.v reste à compléter.

Mais ici, il est plus pertinent de poursuivre en implémentant physiquement le projet dans la puce FPGA pour voir comment réagit réellement le convertisseur A/N aux signaux d’entrées.

On reviendra alors au « banc de test » au chapitre Le « banc de test » complet.

VI - Description du projet par schéma-blocs

Le projet à implémenter peut être décrit par schéma-blocs.

Allez dans le menu File → New… → Block Diagram/Schematic File, puis sauvegardez (File → Save As…) sous le nom Block-adc-project4dvp.bdf (Block Diagram File).

Faites un clic droit sur le fichier spi_interface.v dans le navigateur, puis sélectionnez Create Symbol Files for Current File :

Le message « Quartus Prime Create Symbol File was successful. 0 errors, 0 warnings » devrait apparaître dans la console.

Double-cliquez dans la fenêtre de travail du schéma-blocs pour aller chercher le symbole de l’interface SPI et déposez-le dans la fenêtre de travail :

L’horloge principale provient d’un oscillateur 50 MHz, mais le convertisseur A/N se synchronise avec un signal d’horloge qui ne doit pas dépasser 3,2 MHz. Il faut mettre en œuvre un circuit spécialisé appelé PLL (Phase-Locked Loop ou boucle à verrouillage de phase) pour asservir la fréquence de sortie à une fraction de la fréquence d’entrée.

Pour cela, allez dans le catalogue des IP (Intellectual Property) qui comprend le composant ALTPLL répondant au besoin :

Le composant est configurable grâce à un assistant. Il vous suffit de renseigner les différents onglets comme décrit ci-dessous, en laissant les options non mentionnées à leur valeur par défaut :

La fréquence d’entrée du bloc est de 50 MHz.

Le bloc ne comportera qu’une seule entrée, le signal inclk0. En sortie, le signal c0 sera le signal d’horloge à une fréquence réduite. Il faut donc décocher les options en surbrillance proposées par défaut.

On veut en sortie une fréquence réduite à 3,2 MHz. L’assistant calcule le facteur de réduction de la fréquence = 8 / 125.

Il faut générer le symbole du composant pour pouvoir le déposer à la souris dans la description par schéma-blocs.

En double-cliquant à nouveau dans la fenêtre de travail, on peut aller chercher le composant PLL et déposer son symbole :

Pour les symboles des entrées-sorties, il y a une icône dédiée dans la barre d’outils :

Il reste à déposer les symboles des entrées et sorties et faire les connexions à la souris :

L’entrée raccordée au signal inclk0 du composant PLL doit être nommée CLOCK_50 pour être dirigée vers le signal d’horloge 50 MHz conformément au Pin Planner. Les trois sorties raccordées au bloc de l’interface SPI doivent être nommées ADC_SADDR, ADC_SCLK et ADC_CS_N pour être dirigées vers les entrées du convertisseur A/N. Enfin, l’entrée ADC_SDAT d’où arrivent les données en sortie du convertisseur A/N doit être raccordée en entrée du bloc de l’interface SPI.

Avant de lancer la compilation, il faut penser à mettre le schéma-bloc comme composant maître du projet. Faites un clic droit sur le fichier Block-adc-project4dvp.bdf du schéma-blocs et sélectionnez Set as Top-Level Entity.

Vous pouvez maintenant lancer la compilation du projet en cliquant sur le bouton Start Compilation. La console devrait finir par afficher le message :

« Quartus Prime Full Compilation was successful. 0 errors, 22 warnings »

Ne tenez pas compte des warnings, ils ne sont pas critiques et toutes les entrées-sorties ne sont pas renseignées.

De nouvelles étiquettes PIN_XXX apparaissent sur le schéma-blocs et montrent que les noms symboliques des entrées-sorties ont bien été identifiés conformément au Pin planner :

Le menu Tools → Net List Viewers → RTL Viewer permet de rendre compte de la structure actuelle du projet sous forme de schéma logique :

VII - Utilisation de l’analyseur logique intégré à Quartus Prime

Quartus Prime Lite intègre un outil nommé Signal Tap Logic Analyser permettant d’implémenter un module supplémentaire dans votre projet qui va se connecter aux signaux souhaités et faire remonter les données vers le PC de développement par l’interface JTAG puis le câble USB. On pourra ainsi visualiser les signaux sur son ordinateur comme le ferait un analyseur logique connecté aux signaux de la puce FPGA.

Allez dans le menu Tools → Signal Tap Logic Analyser. La fenêtre de l’outil s’ouvre :

Double-cliquez au centre dans la zone où est indiqué « Double-click to add nodes ». Sélectionnez le filtre « SignalTap : pre-synthesis » et cliquez sur le bouton [List] :

Sélectionnez les nœuds de l’interface SPI à analyser (cs_n, sclk, counter, saddr et sdat), puis cliquez sur le bouton [Insert].

Dans la partie « Signal Configuration », sélectionnez l’horloge principale CLOCK_50 et un échantillonnage 4K :

Vous constatez aussi sur la copie d’écran ci-dessus, dans la partie Instance Manager, l’instance supplémentaire auto_signal_tap_0 à implémenter dans la puce FPGA. Cette instance est connectée aux signaux à analyser et gère leur remontée au PC de développement. Elle va consommer 610 éléments logiques et de la mémoire supplémentaire.

En lançant à nouveau la compilation avec le bouton Start Compilation, l’outil vous propose de sauver les données capturées par l’analyseur avec un fichier au format .stp (Signal TaP) lié au projet et que vous retrouverez dans le navigateur du projet.

Enfin, on branche la carte FPGA sur le port USB. Dans la partie « JTAG Chain Configuration », allez chercher l’interface USB-Blaster. La carte devrait être reconnue. Cliquez sur le bouton avec les trois points […] et sélectionnez le fichier binaire adc_project4dvp.sof du projet.
Transférez le binaire .sof (SRAM Object File) du projet dans la puce FPGA en cliquant sur le bouton Program Device.

Pour lancer les acquisitions en continu et visualiser les signaux, cliquez sur le bouton Autorun Analysis.

Vous devriez voir les graphes des signaux s’animer en continu au fur et à mesure des acquisitions.

On peut voir ci-dessus en jaune l’évolution du compteur 4 bits counter entre 0 et 15. La séquence binaire 3’b101 de sélection de l’entrée analogique n°5 apparaît en bleu et le dernier chronogramme avec la séquence en vert semble montrer une réponse du convertisseur sur 12 bits. Si vous tournez le bouton du potentiomètre, vous devriez voir la réponse évoluer. Si vous tournez le bouton au maximum dans un sens et que vous voyez un signal sdat plat à l’état bas (soit, 0 en décimal) ou un signal plat à l’état haut pendant 12 cycles (soit, 4095 en décimal), c’est bon signe !

Il faut maintenant finaliser le projet en récupérant bit par bit cette valeur de conversion sur 12 bits pour la restituer sous forme de bus.

VIII - Module d’interface SPI au complet

On rajoute quelques signaux et processus supplémentaires pour compléter le module du fichier spi-interface.v. Les résultats sont présentés sous forme de chronogrammes obtenus à l’analyseur logique de Quartus Prime Lite en reprenant la démarche du chapitre précédent.

Le nœud dout_available fournit un signal à l’état haut pendant l’intervalle défini par le compteur (qui prend successivement les valeurs 5, 6, 7 …, 14, 15, 0) où le convertisseur A/N pousse les données utiles sur sa sortie dout :

wire dout_available;
assign dout_available = ((counter >= 5 ) || (counter == 0)) && (~cs_n);

Signaux dout et dout-available en vert

Le nœud end_of_conversion en sortie du module est une impulsion (tous les 16 cycles d’horloge) pour prévenir que la conversion A/N est terminée et que le résultat de la conversion est disponible.

assign end_of_conversion = (counter == 4'd1) && (~cs_n);

Signal end_of_conversion en rose

Et le plus important, voici le code qui permet de récupérer les bits du résultat de la conversion et de présenter le résultat 12 bits sur le bus data_out[11:0] en sortie du module :

reg [11:0] data_temp;   // donnée temporaire 12bits
always @(posedge clk) begin
if (dout_available)
data_temp <= {data_temp[10:0], sdat};
end

assign data_out = end_of_conversion ? data_temp : data_out;

À la ligne data_temp <= {data_temp[10:0], sdat};, on décale à gauche les bits d’un registre temporaire 12 bits data_temp[11:0] pour y insérer à droite le bit en cours (par concaténation : les bits 10 à 0 sont récupérés, et on ajoute le bit en cours à la fin). L’opération est répétée 12 fois, tant que dout_available est à l’état haut.

Quand la conversion est terminée, on dirige la valeur du registre temporaire vers la sortie data_out[11:0].

data_out[11:0] en orange, la valeur est disponible sur l’impulsion du signal end_of_conversion en rose

En résumé :

- la séquence 3’b101 en bleu renseigne le convertisseur sur l’entrée analogique sélectionnée, ici la n°5 où est branchée le potentiomètre rotatif ;

- le convertisseur A/N pousse les 12 bits du résultat sur sa sortie, soit ici : 1000 1011 1101 = 2237 en décimal ;

- une impulsion end_of_conversion, en rose, signale la fin de la conversion, et la disponibilité du résultat sur le bus 12 bits data_out[11:0] en orange.

Le résultat 2237 est l’image de la tension (2237 / 2^12) x 3,3 = 1,80 V. Une mesure au voltmètre entre les broches du potentiomètre devrait confirmer que vous êtes dans le vrai.

Si vous aviez obtenu une valeur erronée, probablement due à un décalage sur la position d’un bit (les valeurs du chronogramme peuvent être affichées en binaire pour s’en rendre compte), vous auriez tôt fait de vous en apercevoir d’après les chronogrammes et de corriger ce décalage dans le code.

IX - Le « banc de test » complet

Comme dit précédemment, un « banc de test » complet doit éprouver le module du fichier spi_interface.v. Et puisque les données poussées par le convertisseur A/N sont en entrée du module sous test, il faut simuler ces données.

On propose de compléter le test bench de la façon suivante :

testbench.v

`timescale 10ns/100ps
// unité de temps = 10ns, résolution = 100ps

module testbench;

reg clock = 1'b0;
wire sclk_sig;
wire cs_n_sig;

reg  sdat_sig;
wire [11:0] data_out_sig;
wire  end_of_conversion_sig;
wire saddr_sig;

localparam period = 100;  //période = 100 unités de temps, soit 100 x 10ns = 1us

integer i;

always begin
#(period/2) clock = ~clock; // génération signal d'horloge
end

spi_interface spi_inst
(
.clk(clock),
.sclk(sclk_sig),
.cs_n(cs_n_sig),
.sdat(sdat_sig),
.data_out(data_out_sig),
.end_of_conversion(end_of_conversion_sig),
.saddr(saddr_sig)
);

task generate_adc_value(input [11:0] v);
// simulation des signaux en sortie dout du convertisseur A/N
begin
#period     sdat_sig = v[11];   // DB11
#period     sdat_sig = v[10];   // DB10
#period     sdat_sig = v[9];    // DB9
#period     sdat_sig = v[8];    // DB8
#period     sdat_sig = v[7];    // DB7
#period     sdat_sig = v[6];    // DB6
#period     sdat_sig = v[5];    // DB5
#period     sdat_sig = v[4];    // DB4
#period     sdat_sig = v[3];    // DB3
#period     sdat_sig = v[2];    // DB2
#period     sdat_sig = v[1];    // DB1
#period     sdat_sig = v[0];    // DB0

#period     sdat_sig = 1'b0;
#(3*period) ;
end
endtask

initial begin
sdat_sig = 1'b0;

#(4*period) // temporiser pendant 4 périodes

for (i = 0; i < 4096; i = i + 1) begin
generate_adc_value(i);
end

end

endmodule

Une procédure generate_adc_value (tasken Verilog) permet de générer le signal série d’une valeur v de taille 12 bits passée en paramètre selon le timing décrit par la documentation du convertisseur A/N.

Par exemple, la ligne #period sdat_sig = v[11]; signifie qu’après un délai de 100 unités de temps correspondant à la période de l’horloge (period = 100), le bit 11 de v est « écrit » sur la ligne du signal sdat_sig. Et si on poursuit à la ligne suivante, le bit 10 de v sera « écrit » au cycle d’horloge suivant, etc.

Les quelques lignes ci-dessous du test bench constituent le scénario de test, très simple pour commencer :

for (i = 0; i < 4096; i = i + 1) begin
generate_adc_value(i);
end

On simule ici une rampe de tension entre 0 et 3,3 V (soit une valeur retournée entre 0 et 2^12 - 1) :

Simulation complète sous ModelSim

La simulation obtenue ci-dessus montre un signal de sortie sur le bus data_out_sig en rouge avec les valeurs attendues.

À vous de voir s’il faut un scénario de test plus complet avec d’autres valeurs, des valeurs aléatoires, etc. Les simulations sont aussi faites pour éprouver des modules avec des scénarios difficiles à reproduire physiquement.

X - Finalisation du projet : visualisation avec le bandeau de LED

On finira ce projet avec un bargraphe composé des huit LED de la carte qui évolue en fonction de la rotation du bouton du potentiomètre.

On ajoute un nouveau module au projet dans le fichier led_interface.v. En fonction de la valeur 12 bits issue de la conversion A/N, le module produira une valeur 8 bits dirigée vers les LED (bus LED[7:0]). Le schéma ci-dessous montre en surbrillance ce nouveau module sous forme de bloc et ses connexions avec les autres blocs :

Le code du module led-interface.v est très simple :

led_interface.v

module led_interface
(
input clk,
input new_data,
input wire [11:0] data,
output reg [7:0] led_out
);

always @(posedge clk) begin
if (new_data) begin
led_out[0] <= (data >= 256);
led_out[1] <= (data >= 768);
led_out[2] <= (data >= 1280);
led_out[3] <= (data >= 1792);
led_out[4] <= (data >= 2304);
led_out[5] <= (data >= 2816);
led_out[6] <= (data >= 3328);
led_out[7] <= (data >= 3840);
end
end

endmodule

Une fois le fichier du module sauvegardé, faites un clic droit sur le fichier led_interface.v dans le navigateur, puis sélectionnez Create Symbol Files for Current File. Il reste à déposer le symbole dans le schéma-blocs et faire les connexions à la souris comme indiqué ci-dessous.
La sortie vers le bandeau de LED doit être nommée LED[7..0] :

Relancez la compilation, et transférez le binaire soit à nouveau depuis l’analyseur logique, soit depuis le menu Tools → Programmer.

XI - Conclusion

Beaucoup de choses ont été vues dans ce petit projet qui est un bon prétexte pour étudier la démarche de conception d’un projet FPGA. On a commencé par la description (le design) du projet de façon architecturale ou comportementale avec un langage HDL (Hardware Description Language) tels que Verilog ou VHDL. Quartus Prime permet également d’élaborer des descriptions de façon graphique, notamment par schéma-blocs.

La phase de compilation d'un projet FPGA consiste à transformer la description à haut niveau écrite par le concepteur en un fichier binaire qui peut être transféré sur le circuit programmable. Cette phase largement automatisée comprend plusieurs étapes, telles que l’analyse, la synthèse logique, jusqu’au placement et routage du modèle structurel sur FPGA, pour finalement générer le bitstream, le fichier binaire qui contient toutes les informations pour configurer la puce FPGA.

Flot de conception simplifié d'après Intel « DE0-Nano User Manual »

Comme vous pouvez le voir sur le schéma précédent, la démarche n’est pas linéaire et le concepteur devra souvent revenir sur ses pas pour corriger ou optimiser la conception jusqu’à obtenir les fonctionnalités et les performances attendues.

Parmi les outils à disposition du concepteur pour la mise au point, le débogage et l’optimisation :

- le simulateur logiciel ModelSim : les simulations fonctionnelles permettent de vérifier la conformité des signaux aux spécifications sans avoir recours à la cible FPGA matérielle. Tous les signaux et stimuli sont générés par logiciel pour reproduire les conditions réelles. Le module sous test est soumis à ces stimuli et l’on observe les signaux en sortie. Un « banc de test » ou test bench permet de mettre en œuvre des scénarios de tests parfois complexes pour prendre tous les cas en considération ;

- l’outil Signal Tap Logic Analyzer : cet outil intégré à Quartus Prime permet d’implémenter dans la puce FPGA un modèle physique d’analyseur logique avec des sondes reliées aux signaux à vérifier. Les données des sondes seront remontées jusqu’au PC de développement par le câble USB afin d’être collectées et observées sous forme de chronogrammes.

Animation en format "mp4" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
./videos/capture-signalTapLogicAnalyser.mp4

Si la mise au point de votre projet s’est bien déroulée jusque-là, sachez toutefois que certaines situations sont plus complexes. Le concepteur d’un projet arrivera toujours à faire une simulation qui répond aux spécifications, mais il peut arriver que le projet synthétisé ne se comporte pas comme la simulation fonctionnelle.

Cela est dû en général à de mauvaises pratiques de conception. Le cas classique : des signaux non initialisés ou indéterminés à certains moments et prenant des valeurs par défaut en simulation, mais ces signaux peuvent prendre des états arbitraires une fois le projet synthétisé.

Dans d’autres cas, les simulations ne rendent pas compte de problèmes d’implémentation ou liés à l’environnement réel : des incohérences d’horloge, la présence de délais engendrés par les signaux qui traversent de nombreux composants logiques et autres défauts dans le timing, des glitches, des problèmes de routage, de métastabilité, de conflits de ressources, etc. D’autres outils seront alors nécessaires pour résoudre ces problèmes.

Pour conclure, je remercie les membres de developpez pour leur travail de relecture de cet article et leurs propositions d’amélioration, en particulier : laurent_ott, User, LittleWhite et Escartefigue.

XII - Annexe : fichiers sources

Archive du projet de ce tutoriel au format Quartus Archive, à importer directement depuis Quartus Prime (version 20.1.1 Lite Edition) : spi-adc12bits.qar

_______________

spi_interface.v

module spi_interface
#(
parameter [2:0] ADD = 3'b101    // <ADD2><ADD1><ADD0> = 101, soit Analog Input 5
)

(
input clk,                          // horloge jusqu'à 3,2MHz maxi
input sdat,                         // ADC Digital Data Output

output reg saddr,                   // ADC Digital Data Input
output wire sclk,                   // ADC Digital Clock Input
output wire cs_n,                   // ADC Chip Select
output wire [11:0] data_out,    // Résultat de la conversion 12 bits
output wire end_of_conversion   // Impulsion synchro. End Of Conversion
);

// ---------------------------------------------------------------------------

reg [3:0] counter;          // compteur 4 bits interne
reg cs = 1'b0;

initial begin
counter = 0;
end

assign cs_n = ~cs;

assign sclk = cs_n ? 1'b1 : clk;

always @(negedge clk) begin
if (counter == 4'd1) begin
cs <= 1'b1; // active Chip Select
end
end

always @(posedge clk) begin
counter <= counter + 4'd1;
end

always @(negedge clk) begin
saddr <= 1'b0;
case(counter)
3: begin
saddr <= ADD[2];
end
4: begin
saddr <= ADD[1];
end
5: begin
saddr <= ADD[0];
end
endcase
end

wire dout_available;
assign dout_available = ((counter >= 5 ) || (counter == 0)) && (~cs_n);

assign end_of_conversion = (counter == 4'd1) && (~cs_n);

reg [11:0] data_temp;   // donnée temporaire 12bits
always @(posedge clk) begin
if (dout_available)
data_temp <= {data_temp[10:0], sdat};
end

assign data_out = end_of_conversion ? data_temp : data_out;

endmodule

led_interface.v

module led_interface
(
input clk,
input new_data,
input wire [11:0] data,
output reg [7:0] led_out
);

always @(posedge clk) begin
if (new_data) begin
led_out[0] <= (data >= 256);
led_out[1] <= (data >= 768);
led_out[2] <= (data >= 1280);
led_out[3] <= (data >= 1792);
led_out[4] <= (data >= 2304);
led_out[5] <= (data >= 2816);
led_out[6] <= (data >= 3328);
led_out[7] <= (data >= 3840);
end
end

endmodule

testbench.v

`timescale 10ns/100ps
// unité de temps = 10ns, résolution = 100ps

module testbench;

reg clock = 1'b0;
wire sclk_sig;
wire cs_n_sig;

reg  sdat_sig;
wire [11:0] data_out_sig;
wire  end_of_conversion_sig;
wire saddr_sig;

localparam period = 100;  //période = 100 unités de temps, soit 100 x 10ns = 1us

integer i;

always begin
#(period/2) clock = ~clock; // génération signal d'horloge
end

spi_interface spi_inst
(
.clk(clock),
.sclk(sclk_sig),
.cs_n(cs_n_sig),
.sdat(sdat_sig),
.data_out(data_out_sig),
.end_of_conversion(end_of_conversion_sig),
.saddr(saddr_sig)
);

task generate_adc_value(input [11:0] v);
// simulation des signaux en sortie dout du convertisseur A/N
begin
#period     sdat_sig = v[11];   // DB11
#period     sdat_sig = v[10];   // DB10
#period     sdat_sig = v[9];    // DB9
#period     sdat_sig = v[8];    // DB8
#period     sdat_sig = v[7];    // DB7
#period     sdat_sig = v[6];    // DB6
#period     sdat_sig = v[5];    // DB5
#period     sdat_sig = v[4];    // DB4
#period     sdat_sig = v[3];    // DB3
#period     sdat_sig = v[2];    // DB2
#period     sdat_sig = v[1];    // DB1
#period     sdat_sig = v[0];    // DB0

#period     sdat_sig = 1'b0;
#(3*period) ;
end
endtask

initial begin
sdat_sig = 1'b0;

#(4*period) // temporiser pendant 4 périodes

for (i = 0; i < 4096; i = i + 1) begin
generate_adc_value(i);
end

end

endmodule