Atmende LED-Effekt mit Sinuswelle aus Block RAM erzeugen
Ich habe bemerkt, dass viele der Gadgets, die ich in den letzten Jahren gekauft habe, vom LED-Blinken zum LED-Atmen übergegangen sind. Die meisten elektronischen Geräte enthalten eine Status-LED, deren Verhalten Aufschluss über den internen Zustand gibt.
Meine elektrische Zahnbürste blinkt eine LED, wenn sie aufgeladen werden muss, und mein Handy nutzt die LED, um mich aus verschiedensten Gründen auf sich aufmerksam zu machen. Doch die LEDs blinken nicht mehr wie in den alten Tagen. Es ist eher ein analoger Puls-Effekt mit kontinuierlich variierender Intensität.
Die folgende GIF-Animation zeigt meine Logitech-Maus, die diesen Effekt nutzt, um anzuzeigen, dass sie den Akku lädt. Ich nenne diesen Effekt atmende LED, weil das Lichtintensitätsmuster in Geschwindigkeit und Beschleunigung dem menschlichen Atemzyklus ähnelt. Es wirkt so natürlich, weil der Beleuchtungszyklus einem Sinuswellenmuster folgt.

Dieser Artikel setzt letzten Wochen Blogpost über Pulsweitenmodulation (PWM) fort. Heute nutzen wir das PWM-Modul, den Sägezahnzähler und das Reset-Modul, die wir im vorherigen Tutorial erstellt haben. Klicken Sie auf den Link unten, um den Artikel über PWM zu lesen!
PWM-Controller in VHDL erstellen
Sinuswellenwerte im Block RAM speichern
Während es möglich ist, eine Sinuswelle mittels Digital Signal Processing (DSP)-Primitiven im FPGA zu generieren, ist ein simplerer Weg das Speichern der Abtastwerte im Block RAM. Statt die Werte zur Laufzeit zu berechnen, berechnen wir während der Synthese eine Reihe von Sinuswerten und erstellen ein Read-Only Memory (ROM), um sie zu speichern.
Betrachten Sie das folgende Minimalbeispiel, das zeigt, wie man eine volle Sinuswelle in einem 3×3-Bit-ROM speichert. Sowohl der Adresseingang als auch der Datenausgang sind drei Bit breit, was bedeutet, dass sie einen Integer-Wert im Bereich 0 bis 7 darstellen können. Wir können acht Sinuswerte im ROM speichern, und die Datenauflösung beträgt ebenfalls 0 bis 7.

Die trigonometrische Sinusfunktion liefert für jeden Winkel x einen Wert im Bereich [-1, 1]. Zudem wiederholt sich der Zyklus, wenn x ≥ 2π. Daher reicht es, nur die Sinuswerte von null bis einschließlich, aber nicht einschließend 2π zu speichern. Der Sinuswert für 2π ist derselbe wie für null. Die obige Illustration zeigt dieses Konzept. Wir speichern Sinuswerte von null bis \frac{7\pi}{4}, dem letzten gleichmäßig verteilten Schritt vor dem vollen 2π-Kreis.
Digitale Logik kann reelle Werte wie Winkel oder Sinuswerte nicht mit unendlicher Präzision darstellen. Das gilt für jedes Computersystem. Selbst bei Double-Precision-Gleitkommazahlen handelt es sich nur um eine Approximation. So funktionieren Binärzahlen, und unser Sinus-ROM ist da keine Ausnahme.
Um das Maximum aus den verfügbaren Datenbits herauszuholen, addieren wir beim Berechnen des ROM-Inhalts einen Offset und einen Skalierungsfaktor zu den Sinuswerten. Der niedrigste mögliche Sinuswert von -1 wird auf den Datenwert 0 abgebildet, während der höchste mögliche Sinuswert von 1 in 2^{\mathit{data\_bits}-1} übersetzt wird, wie die folgende Formel zeigt.
\mathit{data} = \mathit{Round}\left(\frac{(1 + \sin \mathit{addr}) * (2^\mathit{data\_bits} - 1)}{2}\right)
Um eine ROM-Adresse in einen Winkel x zu übersetzen, können wir folgende Formel nutzen:
x = \frac{\mathit{addr} * 2\pi}{2^\mathit{addr\_bits}}
Natürlich liefert diese Methode keinen universellen Winkel-zu-Sinuswert-Konverter. Wenn das Ihr Ziel ist, benötigen Sie zusätzliche Logik oder DSP-Primitiven zur Skalierung von Adresseingang und Datenausgang. Für viele Anwendungen reicht jedoch eine Sinuswelle als vorzeichenlose Ganzzahl. Und wie wir im nächsten Abschnitt sehen werden, ist das genau das, was wir für unser LED-Pulsing-Projekt brauchen.
Sine-ROM-Modul
Das in diesem Artikel vorgestellte Sine-ROM-Modul wird auf den meisten FPGA-Architekturen Block RAM inferieren. Passen Sie die Breiten- und Tiefen-Generics an die Speicherprimitiven Ihres Ziel-FPGAs an. Das ergibt die beste Ressourcennutzung. Sie können sich jederzeit am Beispiel-Lattice-Projekt orientieren, falls Sie unsicher sind, wie Sie den Sine-ROM für ein echtes FPGA-Projekt einsetzen.
Hinterlassen Sie Ihre E-Mail im untenstehenden Formular, um die VHDL-Dateien und ModelSim- / iCEcube2-Projekte herunterzuladen!
Die Entity
Der folgende Code zeigt die Entity des Sine-ROM-Moduls. Sie enthält zwei Generic-Eingänge, mit denen Sie Breite und Tiefe des inferierten Block RAMs festlegen können. Ich nutze Range-Spezifizierer bei den Konstanten, um unbeabsichtigten Integer-Overflow zu verhindern. Mehr dazu später im Artikel.
entity sine_rom is generic ( addr_bits : integer range 1 to 30; data_bits : integer range 1 to 31 ); port ( clk : in std_logic; addr : in unsigned(addr_bits - 1 downto 0); data : out unsigned(data_bits - 1 downto 0) ); end sine_rom;
Die Port-Deklaration hat einen Takteingang, aber keinen Reset, da RAM-Primitiven keinen Reset haben können. Der addr-Eingang ist der Ort, an dem wir den skalierten Winkel (x) zuweisen, und der data-Ausgang ist der Ort, an dem der skalierte Sinuswert erscheint.
Typdeklarationen
Am Anfang der deklarativen Region der VHDL-Datei deklarieren wir einen Typ und einen Subtyp für unser ROM-Speicherobjekt. Der addr_range-Subtyp ist ein Integer-Bereich, der der Anzahl der Slots in unserem RAM entspricht, und der rom_type beschreibt ein 2D-Array, das alle Sinuswerte speichert.
subtype addr_range is integer range 0 to 2**addr_bits - 1; type rom_type is array (addr_range) of unsigned(data_bits - 1 downto 0);
Doch wir werden das Speichersignal noch nicht deklarieren. Zuerst müssen wir die Funktion definieren, die die Sinuswerte erzeugt, damit wir sie zur Initialisierung des ROM-Speichersignals nutzen können. Wir müssen sie vor der Signaldeklaration definieren, damit wir die Funktion für die Initialisierung verwenden können.
Beachten Sie, dass wir den addr_bits-Generic als Basis für die Definition von addr_range verwenden. Deshalb musste ich für addr_bits einen Maximalwert von 30 spezifizieren. Denn für größere Werte wird die Berechnung 2**addr_bits - 1 überlaufen. VHDL-Integers sind 32 Bit lang, obwohl sich das mit VHDL-2019 ändern soll, das 64-Bit-Integers verwendet. Vorerst müssen wir mit dieser Einschränkung leben, wenn wir Integers in VHDL nutzen, bis die Tools VHDL-2019 unterstützen.
Funktion zur Generierung von Sinuswerten
Der folgende Code zeigt die init_rom-Funktion, die die Sinuswerte generiert. Sie gibt ein rom_type-Objekt zurück, weshalb wir erst den Typ, dann die Funktion und schließlich die ROM-Konstante deklarieren müssen. Sie hängen in genau dieser Reihenfolge voneinander ab.
function init_rom return rom_type is variable rom_v : rom_type; variable angle : real; variable sin_scaled : real; begin for i in addr_range loop angle := real(i) * ((2.0 * MATH_PI) / 2.0**addr_bits); sin_scaled := (1.0 + sin(angle)) * (2.0**data_bits - 1.0) / 2.0; rom_v(i) := to_unsigned(integer(round(sin_scaled)), data_bits); end loop; return rom_v; end init_rom;
Die Funktion nutzt einige Hilfsvariablen, darunter rom_v, eine lokale Kopie des Arrays, die wir mit Sinuswerten füllen. Innerhalb des Subprogramms iterieren wir per For-Schleife über den Adressbereich und berechnen für jeden ROM-Slot den Sinuswert mit den zuvor beschriebenen Formeln. Am Ende geben wir die rom_v-Variable zurück, die nun alle Sinus-Samples enthält.
data_bits-Generic auf 31 Bit beschränken musste. Für größere Bitlängen würde es überlaufen.
constant rom : rom_type := init_rom;
Unterhalb der init_rom-Funktionsdefinition deklarieren wir das rom-Objekt als Konstante. Ein ROM ist einfach ein RAM, in den nie geschrieben wird, das ist also völlig in Ordnung. Jetzt rufen wir unsere Funktion auf. Wir rufen init_rom auf, um die Initialwerte zu generieren, wie im obigen Code gezeigt.
Der ROM-Prozess
Die einzige Logik in der Sine-ROM-Datei ist der unten aufgeführte, recht einfache Prozess. Er beschreibt einen Block RAM mit einem einzelnen Leseport.
ROM_PROC : process(clk) begin if rising_edge(clk) then data <= rom(to_integer(addr)); end if; end process;
Top-Modul
Dieses Design setzt das PWM-Projekt fort, das ich im vorherigen Blogpost vorgestellt habe. Es enthält ein Reset-Modul, einen PWM-Generator und einen freilaufenden Takzyklus-Zähler (Sägezahnzähler). Sehen Sie im letzten Wochenartikel nach, wie diese Module funktionieren.
Das folgende Diagramm zeigt die Verbindungen zwischen den Submodulen im Top-Modul.

Der folgende Code zeigt den deklarativen Bereich der Top-VHDL-Datei. In letzter Wochen PWM-Design war das duty_cycle-Objekt ein Alias für die MSBs des cnt-Zählers. Das würde jetzt nicht funktionieren, da der Ausgang des Sine-ROMs den Duty Cycle steuert, daher habe ich es durch ein echtes Signal ersetzt. Zudem habe ich einen neuen Alias namens addr erstellt, der die MSBs des Zählers sind. Wir werden ihn als Adresseingang für den ROM nutzen.
signal rst : std_logic; signal cnt : unsigned(cnt_bits - 1 downto 0); signal pwm_out : std_logic; signal duty_cycle : unsigned(pwm_bits - 1 downto 0); -- MSBs des Zählers für Sine-ROM-Adresseingang nutzen alias addr is cnt(cnt'high downto cnt'length - pwm_bits);
Unten sehen Sie, wie man unseren neuen Sine-ROM im Top-Modul instanziiert. Wir setzen Breite und Tiefe des RAMs so, dass sie der Länge des PWM-Moduls internen Zählers folgen. Der data-Ausgang des ROMs steuert den duty_cycle-Eingang des PWM-Moduls. Der Wert auf dem duty_cycle-Signal wird ein Sinuswellenmuster darstellen, wenn wir die RAM-Slots nacheinander auslesen.
SINE_ROM : entity work.sine_rom(rtl) generic map ( data_bits => pwm_bits, addr_bits => pwm_bits ) port map ( clk => clk, addr => addr, data => duty_cycle );
Sine-Wave-ROM simulieren
Das folgende Bild zeigt das Wellenform des Top-Modul-Simulations in ModelSim. Ich habe die Darstellung des unsigned duty_cycle-Signals in ein analoges Format geändert, damit wir die Sinuswelle beobachten können.

Es ist der led_5 Top-Level-Ausgang, der das PWM-Signal trägt, welches die externe LED steuert. Man sieht, dass der Ausgang sich schnell ändert, wenn der Duty Cycle ansteigt oder fällt. Wenn der Duty Cycle am Scheitelpunkt der Sinuswelle ist, ist led_5 ein stetiges ‘1’. Wenn die Welle am Tal der Steigung ist, bleibt der Ausgang kurz bei ‘0’.
Möchten Sie es auf Ihrem Heimcomputer ausprobieren? Tragen Sie Ihre E-Mail-Adresse im untenstehenden Formular ein, um die VHDL-Dateien und die ModelSim- und iCEcube2-Projekte zu erhalten!
LED-Atmung auf dem FPGA implementieren
Ich habe die Lattice iCEcube2-Software genutzt, um das Design auf dem iCEstick FPGA-Board zu implementieren. Nutzen Sie das obige Formular, um das Projekt herunterzuladen und auf Ihrem Board auszuprobieren, falls Sie einen iCEstick besitzen!
Die folgende Auflistung zeigt die Ressourcennutzung, wie vom Synplify Pro Software berichtet, die mit iCEcube2 geliefert wird. Es zeigt, dass das Design ein Block-RAM-Primitiv nutzt. Das ist unser Sine-ROM.
Resource Usage Report for led_breathing Mapping to part: ice40hx1ktq144 Cell usage: GND 4 uses SB_CARRY 31 uses SB_DFF 5 uses SB_DFFSR 39 uses SB_GB 1 use SB_RAM256x16 1 use VCC 4 uses SB_LUT4 65 uses I/O ports: 7 I/O primitives: 7 SB_GB_IO 1 use SB_IO 6 uses I/O Register bits: 0 Register bits not including I/Os: 44 (3%) RAM/ROM usage summary Block Rams : 1 of 16 (6%) Total load per clock: led_breathing|clk: 1 @S |Mapping Summary: Total LUTs: 65 (5%)
Nach dem Routen des Designs in iCEcube2 finden Sie die .bin-Datei im led_breathing_Implmnt\sbt\outputs\bitmap-Ordner innerhalb des Lattice_iCEcube2_proj-Projektverzeichnisses.
Sie können die Lattice Diamond Programmer Standalone Software nutzen, um das FPGA zu programmieren, wie im iCEstick-Benutzerhandbuch gezeigt. Das habe ich getan, und die folgende GIF-Animation zeigt das Ergebnis. Die Lichtintensität der LED oszilliert mit einem Sinuswellenmuster. Es wirkt sehr natürlich, und die LED scheint zu „atmen“, wenn man etwas Fantasie hineininvestiert.

Abschließende Gedanken
Die Nutzung von Block RAM für vorgegebene Sinuswerte ist ziemlich unkompliziert. Doch es gibt einige Einschränkungen, die diese Methode nur für Sinuswellen mit begrenzter X- und Y-Auflösung geeignet machen.
Der erste Grund, der mir einfällt, ist das 32-Bit-Limit für Integer-Werte, das ich zuvor besprochen habe. Ich bin sicher, man kann dieses Problem durch cleverere Berechnung des Sinuswerts umgehen, aber das ist nicht alles.
Für jedes Bit, das Sie die ROM-Adresse erweitern, verdoppeln Sie den RAM-Verbrauch. Wenn Sie hohe Präzision auf der X-Achse benötigen, reicht der RAM möglicherweise nicht einmal auf einem größeren FPGA aus.
Wenn die Anzahl der Bits für die Y-Achsen-Sinuswerte die native RAM-Tiefe übersteigt, wird das Synthese-Tool zusätzliche RAMs oder LUTs zur Implementierung des ROMs nutzen. Das frisst mehr von Ihrem Ressourcenbudget, je mehr Sie die Y-Präzision erhöhen.
Theoretisch müssen wir nur einen Quadranten der Sinuswelle speichern. Daher könnten Sie mit einem Viertel des RAM-Verbrauchs auskommen, wenn Sie eine Finite-State-Machine (FSM) zur Steuerung des ROM-Auslesens einsetzen. Sie müsste den Sinusquadranten für alle vier Permutationen der X- und Y-Achsen invertieren. Dann könnten Sie eine volle Sinuswelle aus dem einzelnen im Block RAM gespeicherten Quadranten aufbauen.
Leider ist es schwierig, alle vier Segmente nahtlos zu verbinden. Zwei gleiche Samples in den Übergängen am Scheitelpunkt und Tal der Sinuswelle verzerren die Daten, indem sie flache Stellen auf der Sinuswelle erzeugen. Das Einführen von Rauschen untergräbt den Zweck, nur den Quadranten zu speichern, um die Präzision der Sinuswelle zu verbessern.
VHDL
- Ihr erstes AWS CloudFormation Template erstellen – Ein praktischer Leitfaden
- Reibungslose Nutzererfahrungen schaffen: Ein UX-Design-Blaupause
- String-Listen in VHDL erstellen: Best Practices & Beispiel
- Tcl-gesteuerter Testbench für ein VHDL Code-Lock-Modul erstellen
- FPGA Block RAM aus Textdateien mit VHDLs TEXTIO-Bibliothek initialisieren
- Selbstprüfenden VHDL-Testbench erstellen – Praxisleitfaden
- Dynamische verkettete Liste in VHDL mit Protected Types und Access-Pointern
- Array von Objekten in Java erstellen: Schritt-für-Schritt-Anleitung
- Digitale Fertigung revolutioniert anatomische Modelle in der modernen Medizin
- PLC-Funktionsbausteine über OPC UA-Clients aufrufen: Praktischer Leitfaden