Bonjour, Habr! Je veux apporter ma contribution à l'avancement des FPGA. Dans cet article, je vais essayer d'expliquer comment décrire en VHDL un périphérique qui contrôle un affichage à sept segments. Mais avant de commencer, je veux parler brièvement de comment je suis arrivé au FPGA et pourquoi j'ai choisi le langage VHDL.
Il y a environ six mois, j'ai décidé de m'essayer à la programmation de FPGA. Avant cela, je n'avais jamais rencontré de circuits. Il y avait peu d'expérience avec les microcontrôleurs (Atmega328p, STM32). Immédiatement après la décision de se familiariser avec les FPGA, la question s'est posée de choisir la langue que j'utiliserais. Le choix s'est porté sur VHDL en raison de sa frappe stricte. En tant que débutant, je voulais attraper autant de problèmes que possible au stade de la synthèse, et non sur un appareil qui fonctionne.
Pourquoi exactement un affichage à sept segments? Le LED clignotant est déjà fatigué, et la logique du clignotement ne représente rien d'intéressant. La logique de contrôle de l'affichage est d'une part plus compliquée que de faire clignoter une LED (c'est-à -dire l'écrire est plus intéressante), et d'autre part, elle est assez simple à mettre en œuvre.
Ce que j'ai utilisé dans le processus de création de l'appareil:
- FPGA Altera Cyclone II (je sais qu'il est désespérément dépassé, mais les Chinois peuvent l'acheter pour un sou)
- Quartus II version 13.0.0 (pour autant que je sache, c'est la dernière version prenant en charge Cyclone II)
- Simulator ModelSim
- Affichage à sept segments avec registre à décalage
Défi
Créez un appareil qui affichera les nombres de 0 à 9 dans un cycle. Une fois par seconde, la valeur affichée à l'écran devrait augmenter de 1.
Il existe de nombreuses façons de mettre en œuvre cette logique. Je vais diviser cet appareil en modules, chacun effectuant une action et le résultat de cette action sera transmis au module suivant.
Modules
- Cet appareil devrait pouvoir compter le temps. Pour compter le temps, j'ai créé un module «delay». Ce module a 1 signal entrant et 1 signal sortant. Le module reçoit le signal de fréquence FPGA et, après un nombre spécifié de périodes du signal entrant, change la valeur du signal sortant à l'opposé.
- L'appareil doit lire de 0 à 9. Le module bcd_counter sera utilisé pour cela.
- Pour éclairer un segment sur l'affichage, vous devez mettre le bit correspondant au segment à 0 dans le registre à décalage de l'affichage, et pour effacer le segment en bits, écrivez 1 (mon affichage a une logique inversée). Le décodeur bcd_2_7seg s'occupe de l'installation et de la réinitialisation des bits souhaités.
- Le module émetteur sera responsable du transfert des données.
Le dispositif hôte contrôlera la transmission correcte des signaux entre les modules, ainsi que générera un signal rclk à la fin du transfert de données.
Pour plus de clarté, je donne un schéma de cet appareil Comme vous pouvez le voir sur le schéma, l'appareil a 1 signal entrant (clk) et 3 signaux sortants (sclk, dio, rclk). Le signal clk est disponible en 2 diviseurs de signal (sec_delay et transfer_delay). Un signal sortant avec une période de 1s quitte le périphérique sec_delay. Sur le front montant de ce signal, le compteur (bcd_counter1) commence à générer le prochain numéro à afficher. Une fois le nombre généré, le décodeur (bcd_2_7seg1) convertit la représentation binaire du nombre en segments allumés et non allumés sur l'affichage. Qui, à l'aide de l'émetteur (émetteur1), sont transmis à l'écran. L'émetteur est cadencé à l'aide du dispositif transfer_delay.
Code
Pour créer un périphérique en VHDL, une construction de deux composants d'entité et d'architecture est utilisée. L'entité déclare une interface pour travailler avec le périphérique. L'architecture décrit la logique de l'appareil.
Voici à quoi ressemble l'entité du dispositif à retard Grâce au champ générique, nous pouvons régler l'appareil sur le délai souhaité. Et dans le domaine des ports, nous décrivons les signaux entrants et sortants de l'appareil.
L'architecture du dispositif à retard est la suivante Le code à l'intérieur de la section de processus est exécuté séquentiellement, tout autre code est exécuté en parallèle. Entre parenthèses, après le mot-clé process, les signaux sont indiqués en changeant le processus donné (liste de sensibilité).
Le dispositif bcd_counter, en termes de logique d'exécution, est identique au dispositif à retard. Je ne m'attarderai donc pas là -dessus en détail.
Voici l'entité et l'architecture du décodeur entity bcd_to_7seg is port(bcd: in std_logic_vector(3 downto 0) := X"0"; disp_out: out std_logic_vector(7 downto 0) := X"00"); end entity bcd_to_7seg; architecture bcd_to_7seg_arch of bcd_to_7seg is signal not_bcd_s: std_logic_vector(3 downto 0) := X"0"; begin not_bcd_s <= not bcd; disp_out(7) <= (bcd(2) and not_bcd_s(1) and not_bcd_s(0)) or (not_bcd_s(3) and not_bcd_s(2) and not_bcd_s(1) and bcd(0)); disp_out(6) <= (bcd(2) and not_bcd_s(1) and bcd(0)) or (bcd(2) and bcd(1) and not_bcd_s(0)); disp_out(5) <= not_bcd_s(2) and bcd(1) and not_bcd_s(0); disp_out(4) <= (not_bcd_s(3) and not_bcd_s(2) and not_bcd_s(1) and bcd(0)) or (bcd(2) and not_bcd_s(1) and not_bcd_s(0)) or (bcd(2) and bcd(1) and bcd(0)); disp_out(3) <= (bcd(2) and not_bcd_s(1)) or bcd(0); disp_out(2) <= (not_bcd_s(3) and not_bcd_s(2) and bcd(0)) or (not_bcd_s(3) and not_bcd_s(2) and bcd(1)) or (bcd(1) and bcd(0)); disp_out(1) <= (not_bcd_s(3) and not_bcd_s(2) and not_bcd_s(1)) or (bcd(2) and bcd(1) and bcd(0)); disp_out(0) <= '1'; end bcd_to_7seg_arch;
Toute la logique de ce dispositif est exécutée en parallèle. J'ai expliqué comment obtenir des formules pour cet appareil dans l'une des vidéos de ma chaîne. Peu importe, voici un lien vers la
vidéo .
Dans le dispositif émetteur, je combine la logique série et parallèle entity transmitter is port(enable: in boolean; clk: in std_logic; digit_pos: in std_logic_vector(7 downto 0) := X"00"; digit: in std_logic_vector(7 downto 0) := X"00"; sclk, dio: out std_logic := '0'; ready: buffer boolean := true); end entity transmitter; architecture transmitter_arch of transmitter is constant max_int: integer := 16; begin sclk <= clk when not ready else '0'; send_proc: process(clk, enable, ready) variable dio_cnt_v: integer range 0 to max_int := 0; variable data_v: std_logic_vector((max_int - 1) downto 0); begin
Au signal sclk, je redirige la valeur du signal clk entrant dans l'émetteur, mais uniquement si l'appareil transmet actuellement des données (signal prêt = faux). Sinon, la valeur du signal sclk sera 0. Au début du transfert de données (enable = true signal), je combine les données de deux vecteurs 8 bits (digit_pos et digit) entrant dans l'appareil dans un vecteur 16 bits (data_v) et transmets les données de ce vecteur sont d'un bit par horloge, définissant la valeur du bit transmis dans le signal sortant dio. Parmi les choses intéressantes à propos de cet appareil, je tiens à noter que les données dans dio sont définies sur le bord arrière du signal clk, et les données de la broche dio seront écrites dans le registre à décalage de l'affichage lorsque le bord avant du signal sclk arrivera. Une fois la transmission terminée, en définissant le signal prêt <= vrai, je signale aux autres appareils que la transmission est terminée.
Voici à quoi ressemble l'entité et l'architecture d'un périphérique d'affichage entity display is port(clk: in std_logic; sclk, rclk, dio: out std_logic := '0'); end entity display; architecture display_arch of display is component delay is generic (delay_cnt: integer); port(clk: in std_logic; out_s: out std_logic := '0'); end component; component bcd_counter is port(clk: in std_logic; bcd: out std_logic_vector(3 downto 0)); end component; component bcd_to_7seg is port(bcd: in std_logic_vector(3 downto 0); disp_out: out std_logic_vector(7 downto 0)); end component; component transmitter is port(enable: in boolean; clk: in std_logic; digit_pos: in std_logic_vector(7 downto 0); digit: in std_logic_vector(7 downto 0); sclk, dio: out std_logic; ready: buffer boolean); end component; signal sec_s: std_logic := '0'; signal bcd_counter_s: std_logic_vector(3 downto 0) := X"0"; signal disp_out_s: std_logic_vector(7 downto 0) := X"00"; signal tr_enable_s: boolean; signal tr_ready_s: boolean; signal tr_data_s: std_logic_vector(7 downto 0) := X"00";
Cet appareil contrôle d'autres appareils. Ici, avant de déclarer des signaux auxiliaires, je déclare les composants que j'utiliserai. Dans l'architecture elle-même (après le mot-clé begin), je crée des instances de périphériques:
- sec_delay - une instance du composant de retard. Le signal sortant est acheminé vers sec_s.
- transfer_delay - une instance du composant de retard. Le signal sortant est envoyé au signal transfer_clk.
- bcd_counter1 - une instance du composant bcd_counter. Le signal sortant est acheminé vers bcd_counter_s.
- bcd_to_7seg1 - une instance du composant bcd_to_7seg. Le signal sortant est acheminé vers disp_out_s.
- émetteur1 est une instance du composant émetteur. Les signaux sortants sont envoyés aux signaux sclk, dio, tr_ready_s.
Après les instances de composant, un processus est déclaré. Ce processus résout plusieurs problèmes:
Si l'émetteur n'est pas occupé, le processus initialise le début du transfert de données. if(tr_ready_s) then if(not (prev_disp = disp_out_s)) then prev_disp := disp_out_s;
- Si l'émetteur est occupé (tr_ready_s = false), le processus définit alors la valeur du signal disp_refresh_s <= true (ce signal indique qu'une fois le transfert terminé, les données affichées doivent être mises à jour). La valeur du signal tr_enable_s <= false est également définie, si cela n'est pas fait avant la fin de la transmission, les données téléchargées vers l'émetteur seront transmises
Définit et réinitialise le signal rclk une fois le transfert de données terminé if(rclk_v = '1') then disp_refresh_s <= false; end if; if(tr_ready_s and disp_refresh_s) then rclk_v := '1'; else rclk_v := '0'; end if; rclk <= rclk_v;
Chronogramme
Voici le chronogramme de transfert du numéro 1 à la première position de l'affichage Tout d'abord, les données "10011111" sont transmises. Ensuite, la position du nombre sur l'affichage est "00010000" (ce paramètre arrive à l'émetteur comme constante X "10"). Dans les deux cas, le bit le plus à droite (lsb) est transmis en premier.
Tout le code peut être consulté sur
github . Les fichiers avec un indice * _tb.vhd sont des fichiers de débogage pour les composants correspondants (par exemple, transmetteur_tb.vhd est un fichier de débogage pour un transmetteur). Au cas où, je les ai également téléchargés sur github. Ce code a été téléchargé et travaillé sur un vrai tableau. Peu importe, vous pouvez voir une illustration du code
ici (Ă partir de 15h30). Merci de votre attention.