Hallo Habr! Ich möchte meinen Beitrag zur Weiterentwicklung von FPGAs leisten. In diesem Artikel werde ich versuchen zu erklären, wie in VHDL ein Gerät beschrieben wird, das eine Sieben-Segment-Anzeige steuert. Aber bevor ich anfange, möchte ich kurz darüber sprechen, wie ich zu FPGA gekommen bin und warum ich mich für die VHDL-Sprache entschieden habe.
Vor ungefähr einem halben Jahr habe ich mich entschlossen, FPGAs zu programmieren. Vorher war ich noch nie auf Schaltkreise gestoßen. Es gab wenig Erfahrung mit Mikrocontrollern (Atmega328p, STM32). Unmittelbar nach der Entscheidung, sich mit FPGAs vertraut zu machen, stellte sich die Frage, welche Sprache ich verwenden würde. Die Wahl fiel auf VHDL aufgrund seiner strengen Typisierung. Als Anfänger wollte ich so viele mögliche Probleme wie möglich in der Synthesephase und nicht auf einem Arbeitsgerät erkennen.
Warum genau eine Sieben-Segment-Anzeige? Das Blinken der LED ist bereits müde, und die Logik des Blinkens ist nicht interessant. Die Logik der Steuerung der Anzeige ist einerseits komplizierter als das Blinken einer LED (d. H. Das Schreiben ist interessanter), und andererseits ist sie recht einfach zu implementieren.
Was ich beim Erstellen des Geräts verwendet habe:
- FPGA Altera Cyclone II (Ich weiß, dass es hoffnungslos veraltet ist, aber die Chinesen können es für einen Cent kaufen)
- Quartus II Version 13.0.0 (soweit ich weiß, ist dies die neueste Version, die Cyclone II unterstützt)
- Simulator ModelSim
- Sieben-Segment-Anzeige mit Schieberegister
Herausforderung
Erstellen Sie ein Gerät, das die Zahlen 0 - 9 in einem Zyklus anzeigt. Einmal pro Sekunde sollte der auf dem Display angezeigte Wert um 1 erhöht werden.
Es gibt viele Möglichkeiten, diese Logik zu implementieren. Ich werde dieses Gerät in Module unterteilen, von denen jedes eine Aktion ausführt und das Ergebnis dieser Aktion an das nächste Modul übertragen wird.
Module
- Dieses Gerät sollte in der Lage sein, die Zeit zu zählen. Um die Zeit zu zählen, habe ich ein Verzögerungsmodul erstellt. Dieses Modul hat 1 eingehendes und 1 ausgehendes Signal. Das Modul empfängt das FPGA-Frequenzsignal und ändert nach einer bestimmten Anzahl von Perioden des eingehenden Signals den Wert des ausgehenden Signals in die entgegengesetzte Richtung.
- Das Gerät sollte von 0 bis 9 lesen. Hierfür wird das Modul bcd_counter verwendet.
- Um ein Segment auf dem Display zu beleuchten, müssen Sie das dem Segment entsprechende Bit im Schieberegister des Displays auf 0 setzen und zum Löschen des Segments in Bits 1 schreiben (mein Display hat eine invertierte Logik). Der Decoder bcd_2_7seg übernimmt die Installation und das Zurücksetzen der gewünschten Bits.
- Das Sendermodul ist für die Datenübertragung verantwortlich.
Das Host-Gerät steuert die korrekte Übertragung von Signalen zwischen den Modulen und erzeugt nach Abschluss der Datenübertragung ein rclk-Signal.
Zur Verdeutlichung gebe ich ein Diagramm dieses Geräts Wie Sie dem Diagramm entnehmen können, verfügt das Gerät über 1 eingehendes Signal (clk) und 3 ausgehende Signale (sclk, dio, rclk). Das clk-Signal kommt in 2 Signalteilern (sec_delay und transfer_delay). Ein ausgehendes Signal mit einer Periode von 1s verlässt das Gerät sec_delay. Bei der ansteigenden Flanke dieses Signals beginnt der Zähler (bcd_counter1), die nächste Zahl zur Anzeige zu generieren. Nachdem die Nummer generiert wurde, konvertiert der Decoder (bcd_2_7seg1) die binäre Darstellung der Nummer in beleuchtete und nicht beleuchtete Segmente auf dem Display. Welche über den Sender (Sender1) an das Display übertragen werden. Der Sender wird mit dem Gerät transfer_delay getaktet.
Code
Um ein Gerät in VHDL zu erstellen, wird eine Konstruktion aus zwei Komponenten von Entität und Architektur verwendet. Die Entität deklariert eine Schnittstelle für die Arbeit mit dem Gerät. Die Architektur beschreibt die Logik des Geräts.
So sieht die Entität des Verzögerungsgeräts aus Über das generische Feld können wir das Gerät auf die gewünschte Verzögerung einstellen. Und im Feld Ports beschreiben wir die eingehenden und ausgehenden Signale des Geräts.
Die Verzögerungsvorrichtungsarchitektur ist wie folgt Der Code innerhalb des Prozessabschnitts wird sequentiell ausgeführt, jeder andere Code wird parallel ausgeführt. In Klammern werden nach dem Prozessschlüsselwort Signale angezeigt, indem geändert wird, welcher Prozess gestartet wird (Sensitivitätsliste).
Das bcd_counter-Gerät ist hinsichtlich der Ausführungslogik mit dem Verzögerungsgerät identisch. Daher werde ich nicht im Detail darauf eingehen.
Hier ist die Entität und Architektur des Decoders 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;
Die gesamte Logik dieses Geräts wird parallel ausgeführt. Ich habe in einem der Videos auf meinem Kanal darüber gesprochen, wie man Formeln für dieses Gerät erhält. Wen kümmert es, hier ist ein Link zum
Video .
Im Sendegerät kombiniere ich serielle und parallele Logik 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
Zum sclk-Signal leite ich den Wert des in den Sender eintretenden clk-Signals um, aber nur, wenn das Gerät gerade Daten sendet (Signal bereit = falsch). Andernfalls ist der Wert des sclk-Signals 0. Zu Beginn der Datenübertragung (enable = true signal) kombiniere ich die Daten von zwei 8-Bit-Vektoren (digit_pos und digit), die in das Gerät eingegeben werden, in einen 16-Bit-Vektor (data_v) und sende Daten von diesem Vektor sind ein Bit pro Takt, wodurch der Wert des übertragenen Bits im ausgehenden Signal dio gesetzt wird. Von den interessanten Dingen an diesem Gerät möchte ich beachten, dass die Daten in dio auf die Hinterflanke des clk-Signals gesetzt sind und die Daten von pin dio in das Schieberegister der Anzeige geschrieben werden, wenn die Vorderflanke des sclk-Signals eintrifft. Nach Abschluss der Übertragung signalisiere ich anderen Geräten durch Setzen des Signals ready <= true, dass die Übertragung abgeschlossen wurde.
So sieht die Entität und Architektur eines Anzeigegeräts aus 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";
Dieses Gerät steuert andere Geräte. Bevor ich Hilfssignale deklariere, deklariere ich hier die Komponenten, die ich verwenden werde. In der Architektur selbst (nach dem Schlüsselwort begin) erstelle ich Geräteinstanzen:
- sec_delay - eine Instanz der Verzögerungskomponente. Das ausgehende Signal wird an sec_s weitergeleitet.
- transfer_delay - eine Instanz der Verzögerungskomponente. Das ausgehende Signal wird an das Signal transfer_clk gesendet.
- bcd_counter1 - eine Instanz der Komponente bcd_counter. Das ausgehende Signal wird an bcd_counter_s weitergeleitet.
- bcd_to_7seg1 - eine Instanz der Komponente bcd_to_7seg. Das ausgehende Signal wird an disp_out_s weitergeleitet.
- Sender1 ist eine Instanz der Senderkomponente. Ausgehende Signale werden an die Signale sclk, dio, tr_ready_s gesendet.
Nach Komponenteninstanzen wird ein Prozess deklariert. Dieser Prozess löst mehrere Probleme:
Wenn der Sender nicht besetzt ist, initialisiert der Prozess den Start der Datenübertragung. if(tr_ready_s) then if(not (prev_disp = disp_out_s)) then prev_disp := disp_out_s;
- Wenn der Sender besetzt ist (tr_ready_s = false), setzt der Prozess den Wert des Signals disp_refresh_s <= true (dieses Signal bedeutet, dass nach Abschluss der Übertragung die Daten auf dem Display aktualisiert werden müssen). Der Signalwert tr_enable_s <= false wird ebenfalls gesetzt. Wenn dies nicht vor Abschluss der Übertragung erfolgt, werden die auf den Sender heruntergeladenen Daten übertragen
Setzt das rclk-Signal und setzt es zurück, nachdem die Datenübertragung abgeschlossen ist 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;
Zeitdiagramm
Hier ist das Zeitdiagramm zum Übertragen der Nummer 1 an die erste Position der Anzeige Zunächst werden die Daten „10011111“ übertragen. Dann lautet die Position der Nummer auf dem Display „00010000“ (dieser Parameter erreicht den Sender als Konstante X „10“). In beiden Fällen wird zuerst das Bit ganz rechts (lsb) übertragen.
Der gesamte Code kann auf
github angezeigt werden . Dateien mit einem Index * _tb.vhd sind Debug-Dateien für die entsprechenden Komponenten (beispielsweise ist Transmitter_tb.vhd eine Debug-Datei für einen Sender). Für alle Fälle habe ich sie auch auf github hochgeladen. Dieser Code wurde heruntergeladen und auf einem echten Board bearbeitet. Wen kümmert es, Sie können eine Illustration des Codes
hier sehen (ab 15:30 Uhr). Vielen Dank für Ihre Aufmerksamkeit.