[lwptoc]
Einleitung
Die Entwicklung von Funktionen für einen FPGA sind einerseits vergleichbar mit der Entwicklung von Software, andererseits auch vergleichbar mit der Entwicklung von Hardware. Es ist eine Mischung aus beiden Welten, so dass eine Spezialisierung erforderlich ist.
Nachfolgend stelle ich verschiedene Themen aus dem Bereich VHDL und FPGA vor, die mich schon begleitet haben.
Rund um VHDL
Es folgen diverse Einsteiger- und Fortgeschrittenen Themen rund um VHDL.
VHDL Grundlagen
Dieser Abschnitt vermittelt ein paar Grundlagen zu VHDL. VHDL ist eine Unterform der HDL (Hardware Description Language = Hardware Beschreibungssprache) und steht als Abkürzung für Very High Speed Integrated Circuit Hardware Description Language. Diese ist im IEEE 1076-Standard definiert.
Codeschnipsel
---------------------------------------------------------------- -- Firma: Ingenieurbüro David C. Kirchner -- Entwickler: David Kirchner -- Erstellt am: 12:34 01.04.2016 -- Erstellt für: Beispielkunde -- Design Name: Beispiel-Design -- Modul Name: Top_Level - Behavioral -- Projekt Name: Beispiel-Projekt -- Zielbaustein: Spartan 6 - XC6SLX45-2FGG484 -- Programmversion: Xilinx ISE 14.7 -- Beschreibung: Implementierung des Top-Level-Moduls -- Abhängigkeiten: keine -- Revision: 1.00 - Erste freigegebene Version -- Weitere Kommentare: Keine Kommentar -- SVN-Version: $Id:$ ---------------------------------------------------------------- library IEEE; use IEEE.STD_LOGIC_1164.ALL; use IEEE.STD_LOGIC_ARITH.ALL; library UNISIM; use UNISIM.VComponents.all; entity Top_Level is Port ( -- Takteingang Clk : in std_logic := '0'; CE : in std_logic := '0'; -- Signaleingänge A : IN std_logic := '0'; B : IN std_logic := '0'; C : IN std_logic := '0'; -- Signalausgänge X : out std_logic; Y : out std_logic; Z : out std_logic ); end Top_Level; architecture Behavioral of Top_Level is -- Interne Signale signal X_int :std_logic := '0'; signal Y_int :std_logic := '0'; signal Z_int :std_logic := '0'; begin -- Eingangssignale verarbeiten X_int <= A or B; y_int <= x_int xor C; -- D-Flip-Flop process ( Clk ) begin if rising_edge( Clk ) then if CE = '1' then z_int <= y_int; endif; endif; end process; -- Signalausgabe X <= x_int; Y <= y_int; Z <= z_int; end Behavioral;
Erklärung
Das Beispiel zeigt den typischen Aufbau einer einfachen VHDL-Datei, die eine einfachen Logik mit D-Flip-Flop, einer Verundung und einer Veroderung darstellt. Alle notwendigen Signale kommen von außen. Die Signale A und B werden Verundet x_int und das Ergebnis mit C Verodert y_int. Das Ergebnis y_int wird noch durch ein D-Flip-Flop abgetaktet und liefert das Ergebnis z_int. Das D-Flip-Flop wird im Abschnitt Eingangsprozess genauer beschrieben.
Schauen wir uns nun aber noch die Datei im Detail an:
Im ersten Teil des Codeschnipsels befindet sich der Dateikommentarkopf. Im nächsten Abschnitt gehe ich hier im Detail darauf ein.
Anschließend werden die Bibliotheken IEEE und UNISIM geladen. Die Bibliothek IEEE enthält verschiedene Definitionen und Funktionen, die innerhalb der VHDL-Datei benötigt werden. Dazu gehören z.B. die Funktion rising_edge oder auch der Typ std_logic. Die Bibliothek UNISIM enthält Definitionen und Funktionen, um den Code simulieren zu können.
Der nächste Abschnitt beschreibt die Ports am FPGA, also alle Signale, die rein und raus gehen und was es für Signale sind. Eine Definition der Pins am physischen Baustein könnte hier auch erfolgen, sollte aber aus Gründen der Übersichtlichkeit und Portierbarkeit lieber in einer separaten Datei erfolgen (bei Xilinx wäre dies die UCF-Datei, dazu später mehr in einem anderen Blogpost).
Nach der Port-Definition folgt die Beschreibung des Verhaltens des FPGAs. Dazu gehören Signalzuweisungen und Erstellung von Prozessen, die auf bestimmte Signale – in diesem Fall das Taktsignal – reagieren.
Ersatzschaltbild
Zusammenfassung
HDL und VHDL sind Sprachen, die – wie der Name schon sagt – die Hardware und deren Verhalten beschreiben. Dieser Beitrag soll nur einen kurzen Einblick in die Struktur von VHDL liefern. Es gibt im Internet diverse Quellen, die VHDL-Grundlagen und die Sprache VHDL bis ins kleinste Detail beschreiben.
Dateikommentarblock
Auch wenn es eine Selbstverständlichkeit sein sollte, dass eine Quelltextdatei einen Dateikommentarblock besitzt, möchte ich dennoch an dieser Stelle darauf eingehen. Der Dateikommentar hat gleich mehrere Funktionen, doch vorab ein Beispiel, das ich seit langer Zeit einsetze (angelehnt an das Template von Xilinx).
Codeschnipsel
----------------------------------------------------------------
-- Firma: Ingenieurbüro David C. Kirchner
-- Entwickler: David Kirchner
--
-- Erstellt am: 12:34 01.04.2016
-- Erstellt für: Beispielkunde
-- Design Name: Beispiel-Design
-- Modul Name: Top_Level - structural
-- Projekt Name: Beispiel-Projekt
-- Zielbaustein: Spartan 6 - XC6SLX45-2FGG484
-- Programmversion: Xilinx ISE 14.7
-- Beschreibung: Implementierung des Top-Level-Moduls
-- Einbindung aller Pins und Ports
--
-- Abhängigkeiten: keine
--
-- Revision:
-- 0.01 - Datei erstellt
-- 0.10 - Erste lauffähige Version
-- 1.00 - Erste freigegebene Version
-- 1.10 - Eine kleine Änderung (Minor Release)
-- 2.00 - Eine große Änderung (Major Release)
--
-- Weitere Kommentare: Keine Kommentar
--
-- SVN-Version: $Id:$
--
----------------------------------------------------------------
Erklärung
Neben dem Ersteller (Firma und Person) dieser Datei, sind Projekt bezogene Informationen mit abgelegt. Es ist der Zielbaustein (also das FPGA) und die Programmversion, mit der dieses Projekt bearbeitet wird, dokumentiert. Der Name und die Art des Moduls ist beschrieben und man kann (und in meinen Augen sollte) Informationen niederschreiben, worum es in diesem Modul überhaupt geht.
Weitere Informationen in diesem Kommentarblock sind die Historie der Datei mit Dokumentation der Revisionen. Und sofern ein SVN-System benutzt wird, kann an dieser Stelle auch die SVN-ID (inklusive der Information über Zeitpunkt und Person, die diese Datei eingecheckt hat) eingetragen werden. Diese Information wird dann von SVN eigenständig aktualisiert.
Der Fokus liegt hierbei auf der Dokumentation der Quelltextdatei (der Datei und nicht der einzelnen Programmzeilen- oder abschnitten). Es empfiehlt sich, diese Informationen zu hinterlegen, um auch später noch nachvollziehen zu können, was diese Datei macht bzw. bewirken soll.
Es sollte nicht davon abhalten, einzelne Abschnitte oder auch einzelne Zeilen mit Kommentaren zu hinterlegen. Auch wenn dieses Thema kontrovers diskutiert wird, ist meine Meinung dazu, dass ein Kommentar dem eigenen und auch fremden Verständnis weiterhilft. Denn wer weiß schon genau, was er vor einigen Monaten oder Jahren damit bewirken wollte?
Aufbau eines Top-Level-Modules
Das Top-Level Modul ist – wie der Name schon sagt – das oberste Modul innerhalb der Hierarchie der VHDL-Dateien. Hier werden die Verbindungen in die Außenwelt definiert und die in der Hierarchie weiter unten liegenden Module eingebunden.
Codeschnipsel
---------------------------------------------------------------- -- Firma: Ingenieurbüro David C. Kirchner -- Entwickler: David Kirchner -- Erstellt am: 12:34 01.04.2016 -- Erstellt für: Beispielkunde -- Design Name: Beispiel-Design -- Modul Name: Top_Level - Behavioral -- Projekt Name: Beispiel-Projekt -- Zielbaustein: Spartan 6 - XC6SLX45-2FGG484 -- Programmversion: Xilinx ISE 14.7 -- Beschreibung: Implementierung des Top-Level-Moduls -- Abhängigkeiten: keine -- Revision: 1.00 - Erste freigegebene Verstion -- Weitere Kommentare: Keine Kommentar -- SVN-Version: $Id:$ ---------------------------------------------------------------- library IEEE ; use IEEE.STD_LOGIC_1164.ALL ; use IEEE.STD_LOGIC_ARITH.ALL ; library UNISIM ; use UNISIM.VComponents.all ; entity Top_Level is Port ( -- Takteingang Clk : in std_logic := '0' ; CE : in std_logic := '0' ; -- Signaleingänge A : IN std_logic ; B : IN std_logic ; C : IN std_logic ; -- Signalausgänge X : out std_logic ; Y : out std_logic ; Z : out std_logic ; ) ; end Top_Level ; architecture Behavioral of Top_Level is -- Interne Signale signal X_int : std_logic := '0' ; signal Y_int : std_logic := '0' ; signal Z_int : std_logic := '0' ; begin -- Eingangssignale verarbeiten X_int <= A and B ; y_int <= x_int or C ; -- D-Flip-Flop process( Clk ) begin if rising_edge( Clk ) then if CE_In = '1' then z_int <= y_int ; end if ; end if ; end process ; -- Signalausgabe X <= x_int ; Y <= y_int ; Z <= z_int ; end Behavioral ;
Erklärung
Das Top-Level Modul beginnt – wie andere Quelltext Dateien auch – mit einem Kommentarblock. Anschließend werden die physischen Pins des FPGAs definiert. Hier entsteht also die Verbindung zur Außenwelt. An dieser stelle kann zusätzlich auch der I/O-Standard und die Position des Pins definiert werden. Meistens macht man diese Definition aber – da übersichtlicher – in einer eigenen Datei. Bei Xilinx macht das User-Constraint-File diese Definition.
Als nächstes werden lokale Signale im Top-Level Modul beschrieben und vordefiniert. Hier sollten nach Möglichkeit eindeutige Namen verwendet werden. Weiterhin werden nun die Module definiert, die zusätzlich eingebunden werden sollen. Die stellt nur die Deklaration dar. Die Instanziierung erfolgt später. Sind alle Signale definiert, startet die eigentliche Beschreibung des Systems. Jetzt werden alle Module instanziiert und mit den internen oder auch externen Signalen verbunden. Natürlich kann auch im Top-Level Modul schon eine Signalverarbeitung statt finden. Dazu werden entsprechende Verknüpfungen oder auch Prozesse gebildet.
Noch einmal zusammengefasst: das Top-Level Modul bildet den Anfang eines jeden FPGA Projekts. Eine saubere Hierarchie hilft später bei der Fehlersuche und beim Wiederverwenden von Programmteilen.
Eingangsprozess
Wenn Signale in eine neue Taktdomäne gelangen, ist es notwendig, dass diese mit dem in der Domäne vorherrschenden Takt abgetastet werden und somit einen Eingangsprozess durchlaufen. Dies gilt sowohl für Signale, die von außen in das FPGA kommen, als auch für Signale, die innerhalb des FPGAs von der einen in die andere Taktdomäne geleitet werden.
Codeschnipsel
-- Eingangsprozess process( Clk ) begin if rising_edge( Clk ) then if CE_In = '1' then Internes_Signal <= Externes_Signal ; end if ; end if ; end process ;
Erklärung
Schauen wir uns nun den Quelltext genauer an. Er besteht aus einem Prozess, der mit dem Takt Clk der neuen Taktdomäne läuft. In diesem Prozess wird mit rising_edge auf die steigende Flanke dieses Taktsignals gewartet. Wenn die Steigende Flanke des Taktsignals ankommt, wird noch ein Takt-Enable (CE_In) Signal abgefragt.
Erst wenn dieses Signal auch high ist, wird dem internen Signal das Externe Signal zugewiesen. Hierbei spielt es auch keine Rolle, ob es sich um ein einzelnes Signal oder einen Vektor handelt. Es können natürlich auch diverse andere Signale innerhalb dieses Prozesses übergeben werden.
Ersatzschaltbild
Zusammenfassung
Zusammenfassend lässt sich sagen, dass jedes Signal, das in eine Taktdomäne kommt abgetaktet werden sollte. Da die heutigen FPGAs über genug Flip-Flops verfügen, besteht kein Grund, an dieser Stelle zu sparen. Sollte es jedoch dazu kommen, dass das Signal evtl. nicht sauber anliegt, müssen mehrere Flip-Flop Stufen benutzt werden. In einem späteren Beitrag werde ich dazu mehr bringen. Das Thema wird dann die Flankenerkennung eines Signals sein.
Wie erkenne ich eine Flanke?
Wenn in einem Eingangssignal die Flanke erkannt werden soll, gibt es genau für diesen Zweck verschiedene Techniken. Eine dieser Techniken wird hier vorgestellt. Mit einer kleinen Abwandlung kann auch der Wert des Signals sauber ermittelt werden.
Codeschnipsel
-- Detektierung der steigenden Flanke process( clk ) begin if rising_edge( clk ) and ce='1' then input_s <= input & input_s( 5 downto 1 ); end if ; end process ; input_det_edge <= '1' when input_s( 3 downto 0 ) = "1100" else '0' ;
Ersatzschaltbild
Erklärung
Vorab ein paar Definitionen: input ist ein Eingangssignal vom Typ STD_LOGIC. Das Signal input_s hingegen ist ein STD_LOGIC_VECTOR, also ein Block aus mehreren Signalen. Das Ende _s soll andeuten, dass es sich um ein synchronisiertes Signal handelt. Am Ende wird das Signal input_det_edge erzeugt, das genau dann high ist, wenn eine steigende Flanke erkannt wurde.
Doch wie genau erfolgt nun die Erkennung und wofür kann dies eingesetzt werden?
Der zweite Teil der Frage lässt sich schnell beantworten: Immer wenn ein (im Vergleich zum Systemtakt gesehen) langsameres Signal eingelesen werden soll und es auf die Erkennung der Flanke ankommt, sollte diese Art der Erkennung genutzt werden. Zum Beispiel soll auf die steigende Flanke eines externen Taktsignals reagiert und genau in diesem Moment dann ein Signal eingelesen werden. Dann lässt sich mit der Flankenerkennung ein Speicher-Flipflop ansprechen.
Kommen wir nun zum ersten Teil der Frage.
Das Prinzip, dass sich dahinter verbirgt, ist eine Kette aus Flipflops, in die das Signal eingelesen wird. Anschließend werden alle Ausgänge der Flipflops so miteinander in Verbindung gesetzt (verundet), dass ein spezielles Pattern im Signal erkannt werden kann. Im Fall einer positiven Flanke sind die Flipflops 2 und 3 mit ‚1‘ und die Flipflops 0 und 1 mit ‚0‘ geladen. Für eine negative Flange müssen die Werte getauscht werden.
Eine positive oder negative Flanke zu erkennen ist verhältnismäßig einfach. Durch diese paar Zeilen Code wird es dem System ermöglicht, sauber eine Flanke zu erkennen. Mit einer kleinen Erweiterung (Veroderung) können auch beide Flanken erkannt werden.
Und zum Schluss noch der Tipp: Wenn man sauber erkennen möchte, ob ein Signal high ist, dann muss man nur prüfen, ob die Werte in den letzten vier Flipflops alle high sind.
Sinn und Unsinn von Reset Signalen
In der Hardware werden gerne Resetsignale verwendet, um ein Gerät bzw. ein System in einen vordefinierten Zustand zu bringen. In der Software wird nach einem Reset auch wieder von Vorne definiert gestartet. Und bei FPGAs? Was passiert hier?
Codeschnipsel
-- Prozess mit synchronem Reset proc_sync_reset: process( Clk ) begin if rising_edge( Clk ) then if Reset='1' then -- Code für den Reset Zustand else -- Code für den normalen Zustand end if ; end if ; end process ; -- Prozess mit asynchronem Reset proc_async_reset: process( Clk, Reset ) begin if Reset='1' then -- Code für den Reset Zustand elsif rising_edge( Clk ) then -- Code für den normalen Zustand end if ; end process ;
Erklärung
Reset Signale können in FPGAs genutzt werden, um Flip-Flops zurück zu setzen. Dadurch werden die angeschlossenen Flip-Flops auf einen definierten Zustand gesetzt. Es wird dabei zwischen synchronen und asynchronen Resets unterschieden. Synchrone Resets werden nur mit der steigenden (manchmal fallenden) Flanke ausgewertet, asynchrone Resets wirken umgehen, sobald diese anliegen, ohne auf die Flanken zu achten.
Erste Problem
Und genau hier gibt es Probleme, wenn diese Typen gemischt werden. Bei Xilinx FPGAs lässt sich diese Funktion in den Flip-Flops nur global konfigurieren. Also entweder sind alle Resets bei den Flip-Flops synchron oder asynchron. Fängt man nun also an, zu mischen, wird zusätzliche Logik benötigt, um die Funktion des synchronen Resets nachzubilden. Das kostet unnötig Aufwand.
Zweite Problem
Das zweite Problem kann speziell es bei asynchronen Resets geben; Wenn bei komplexen Designs die Flips-Flops zu unterschiedlichen Zeitpunkten aus dem Resetzustand kommen, können fehlerhafte Zustände entstehen. Die Resetsignale werden – im Gegensatz zu den Taktsignalen – nicht über spezielle Leitungen im FPGA verteilt. Es entstehen also Laufzeiten, die das genannte Problem auslösen.
Die Erklärung zu den Codeschnipsel
Die beiden obigen Codeschnipsel zeigen die Unterschiede im Code. Es ist quasi nur ein kleines Detail, aber mit großer Wirkung. Beim synchronen Reset wird das Signal innerhalb des rising_edge Teils ausgewertet, beim asynchronen Reset davor.
Zusammenfassung
Doch was soll man nun besser benutzen, als globale synchrone oder asynchrone Resets? Bei heutigen FPGAs werden alle Flip-Flops beim Starten mit einem definierten Wert geladen (dieser lässt sich auch im Quellcode vordefinieren). Aus diesem Grund kann oftmals auf ein globales Resetsignal verzichtet werden. Selbst Statemachines haben hier wenig Probleme, da sie als Startwert ebenfalls den Resetstate erhalten.
Zusammengefasst lässt sich sagen: Man kann meistens auf ein Reset verzichten und wenn es einmal erforderlich sein sollte, dann nur lokal und synchron.
Reset Generator
Ein FPGA bzw. Routinen im FPGA benötigt ab und zu intern einen Reset. Dieser Reset sollte die ersten paar Takte vom Systemtakt aktiv bleiben und erst dann inaktiv werden. Und genau dafür gibt es einen kleinen, aber praktischen Tipp in diesem Abschnitt.
Es ist eine Routine, die ich schon in diversen Projekten eingesetzt habe und die bisher immer funktioniert hat.
Codeschnipsel
SRL16E_inst : SRL16E generic map ( INIT => X"FFFF" ) port map ( Q => reset, -- SRL data output A0 => '1', -- Select[0] input A1 => '1', -- Select[1] input A2 => '1', -- Select[2] input A3 => '1', -- Select[3] input CE => '1', -- Clock enable input CLK => clk, -- Clock input D => '0' -- SRL data input ) ;
Ersatzschaltbild
Erklärung
Im Codeschnipsel ist die direkte Instanziierung eines Schieberegisters dargestellt. Es wird durch SRL16E benannt und mit den nachfolgenden Ports beschrieben. Angeschlossen ist hier allerdings nur der Takt und der Ausgang. Alle anderen Signale sind mit festen Werten vorbelegt.
Eine weitere Besonderheit ist die Initialisierung des Flip-Flops in diesem Schieberegister. Es werden alle Flip-Flops mit dem Wert 1 vorgeladen. Was nun passiert, zeigt die Simulation am Besten. Im Bild unten sind zwei Signale dargestellt. Der Systemtakt und das Resetsignal. Der Systemtakt kommt von außen und das Resetsignal wird durch das Schieberegister erzeugt. Wenn der Takt anfängt zu arbeiten, so schiebt das Schieberegister mit jedem Takt eine 1 raus und lädt eine 0 nach (da der Eingang des Schieberegisters auf 0 liegt). Sind alle Register dieses 16-Bit Schieberegisters raus geschoben, wechselt das Ausgangssignal reset von 1 auf 0.
Zusammenfassung
Und das war es auch schon. Dieser Code funktioniert natürlich nicht nur in Xilinx FPGAs, aber genau bei diesen FPGAs von Xilinx (im Spartan 3, im Spartan 6 und im Kintex 7) läuft dieser Code problemlos. Falls also ein Code benötigt wird, der ein Resetsignal etwas länger aktiv hält, dann ist dieser kleiner Codeschnipsel genau das richtige.
Die Statemachine
Es gibt diverse Möglichkeiten, auf Signale zu reagieren. In VHDL können zum Beispiel über IF Anfragen auf ein Signal reagiert werden. Wenn ein Ablauf programmiert werden soll, werden Statemachines eingesetzt (bzw. sollten Statemachines eingesetzt werden.) Ich gehe hier auf die Grundlagen einer Statemachine ein.
Für Statemachines im Allgemeinen und auf anderen Plattformen hier ein paar andere Seiten:
Grundlegende Fragen
Am Anfang stehen ein paar Fragen, die beantwortet werden möchten, bevor in den Quelltext eingestiegen wird.
Was ist eigentlich eine Statemachine?
Eine Statemachine übernimmt Steueraufgaben innerhalb des FPGAs. Sie arbeitet fest getaktet einzelne Arbeitsschritte (die so genannten States) ab. Teilweise wird das Fortschalten zum nächsten Schritt durch externe Einflüsse (z.B Signale oder Zähler) gesteuert. Teilweise gibt es jedoch auch Statemachines, die einmal getriggert werden und dann definiert durchlaufen.
Welche Arten von Statemachines gibt es?
In VHDL gibt es verschiedene Möglichkeiten, eine Statemachine aufzubauen. Zwei Varianten sind mir persönlich am geläufigsten:
1) Ein kompletter Prozess, der neben den States auch die Signalein- und -ausgabe regelt.
2) Mehrere Prozesse, die die Aufgaben Einlesen, Ausgeben, Weiterschalten und Entscheiden verteilt bearbeiten.
Welche Vor- und Nachteile gibt es hier?
Bei kleinen Statemachines ist der Vorteil von Variante 1), dass diese sehr kompakt ist, während Variante 2) umfangreicher ist. Allerdings überwiegt meiner Meinung nach der Vorteil von Variante 2): Die einzelnen Aufgabenbereiche werden klarer von einander getrennt und mögliche Abhängigkeiten sind schneller zu erfassen.
Was ist der häufigste Fehler im Design einer Statemachine?
Ein Fehler, der mir schon mehrfach beim Überarbeiten von VHDL-Code von Kunden aufgefallen ist, entsteht dann, wenn eine Statemachine nach Variante 1) erstellt wird, diese aber auf kein Taktsignal synchronisiert wird. Zur Vermeidung von Fehler, die durch nicht synchronisierte Statemachines entstehen können, sollte die Statemachine entweder synchronisiert werden, oder besser nach Variante 2) umgeschrieben werden. Dann ist es meist auch leichter zu erkennen, wo eine Synchronisierung erforderlich ist.
Ablaufdiagramm
Um einen ersten Eindruck einer einfachen Statemachine zu erhalten, folgt ein Ablaufdiagramm:
Jetzt möchte ich konkret einen Typen vorstellen und zeigen, wie so eine Statemachine aufgebaut werden kann. Es handelt sich um eine vollständig integrierte Statemachine. Die also sowohl den Prozess zum Weiterschalten, als auch die Ein- und Ausgangsprozesse vereint. Schauen wir nun zunächst in den Code:
Codeschnipsel
process( clk ) begin -- Auf steigende Taktflanke warten if rising_edge( clk ) then -- Auf Clock-Enable warten if ce = '1' then -- State bearbeiten case state is when RESET => -- Reset-State LED1 <= '0' ; LED2 <= '0' ; next_state <= STATE_1 ; when STATE_1 => -- State 1 LED1 <= '1' ; LED2 <= '0' ; if x = '1' then next_state <= STATE_2 ; end if ; when STATE_2 => -- State 2 LED1 <= '0' ; LED2 <= '1' ; if y = '1' then next_state <= STATE_1 ; end if ; when others => -- Fehlerfall LED1 <= '1' ; LED2 <= '1' ; next_state <= RESET ; end case ; -- Weiterschalten state <= next_state ; end if ; end if ; end process ;
Erklärung
Der Codeschnipsel zeigt aus der gesamten Datei den Ausschnitt mit der Statemachine. Deren drei States lauten: RESET, STATE_1 und STATE_2. Eingangssignale sind das Taktsignal clk, mit dem Takt-Enable-Signal ce und die beiden Signale X und Y. Diese letzten beiden Signale steuern die Zustände. Als Ausgänge sind zwei Signale für LEDs herausgeführt.
Ziel ist die Umschaltung der States in Abhängigkeit der Eingangssignale. Beim Starten des FPGAs ist die Statemachine im State RESET und geht automatisch in den State STATE_1. Wenn das Signal x high wird, geht die Statemachine in den State STATE_2. Wird das Signal y dann high, geht die Statemachine in den State STATE_1.
Die LEDs schalten entsprechend: Im STATE_1 ist LED1 und im STATE_2 ist LED2 angeschaltet.
Am Ende wird dann auf den nächsten State weitergeschaltet.
Da es sich um eine synchrone Statemachine handelt, reagiert diese immer nur auf die steigende Flanke des Taktsignals clk und wenn das Signal ce high ist.
Die komplette Datei kann hier heruntergeladen werden. Sie ist mit der Xilinx ISE 14.7 übersetzbar. Mit Sicherheit kann dieser Code auch in anderen Umgebungen eingesetzt werden.
Technology Schematic
Ein Ersatzschaltbild mit Flip-Flops usw. ist an dieser Stelle wenig zielführend. Die Software Xilinx ISE bietet die Möglichkeit, aus dem VHDL-Quelltext die technische Umsetzung im FPGA wiederzugeben. Das nachfolgende Bild zeigt das „Technology Schematic“:
Diese Statemachine ist nur ein einfaches Beispiel, zeigt jedoch die mögliche Funktionalität. Der Code oben zeigt jedoch auch, dass eine integrierte Statemachine schnell unübersichtlich werden kann. Im nächsten Abschnitt zeige ich das selbe Bespiel in einer anderen Form.
Codeschnipsel
-- Prozess zur Signaleingabe process( clk ) begin -- Auf steigende Taktflanke warten if rising_edge( clk ) then -- Auf Clock-Enable warten if ce = '1' then x_int <= x ; y_int <= y ; end if ; end if ; end process ; -- Prozess zum Weiterschalten der Statemachine process( clk ) begin -- Auf steigende Taktflanke warten if rising_edge( clk ) then -- Auf Clock-Enable warten if ce = '1' then state <= next_state ; end if ; end if ; end process ; -- Kern der Statemachine process( state, x_int, y_int ) begin -- State bearbeiten case state is when RESET => -- Reset-State next_state <= STATE_1 ; when STATE_1 => -- State 1 if x_int = '1' then next_state <= STATE_2 ; end if ; when STATE_2 => -- State 2 if y_int = '1' then next_state <= STATE_1 ; end if ; when others => -- Fehlerfall next_state <= RESET ; end case ; end process ; -- Prozess zur Signalausgabe process( clk ) begin -- Auf steigende Taktflanke warten if rising_edge( clk ) then -- Auf Clock-Enable warten if ce = '1' then -- State bearbeiten case state is when RESET => -- Reset-State LED1 <= '0' ; LED2 <= '0' ; when STATE_1 => -- State 1 LED1 <= '1' ; LED2 <= '0' ; when STATE_2 => -- State 2 LED1 <= '0' ; LED2 <= '1' ; when others => -- Fehlerfall LED1 <= '1' ; LED2 <= '1' ; end case ; end if ; end if ; end process ;
Erklärung
Diesmal ist der Codeschnipsel ein wenig länger geraten. Das liegt jedoch daran, dass ich nun die Statemachine in mehrere Teile zerlegt habe: Eingangsprozess, Ausgangsprozess, Prozess zum Weiterschalten und die eigentliche Statemachine. Durch diese Aufteilung lassen sich die entsprechenden Aufgaben leichter erstellen. Im Kern ist die Statmachine ist nun kompakter und überschaubarer. Man kann besser verfolgen, welcher State wann und wie verarbeitet wird. Der Prozess der Statemachine ist nun auch nicht mehr vom Takt abhängig, sondern von dem State und von den Eingangssignalen
Die komplette Datei kann hier heruntergeladen werden. Sie ist mit der Xilinx ISE 14.7 übersetzbar. Mit Sicherheit kann dieser Code auch in anderen Umgebungen eingesetzt werden.
Wie oben schon beschrieben, macht dieser Statemachine das selbe wie im letzten Teil. Nur die Darstellung hat sich erheblich verändert. Diese Form eignet sich besonders dann, wenn viele State verwendet werden und es zu vielen Abhängigkeiten mit Steuersignalen kommt. Denn ohne diese Übersichtlichkeit verliert man sonst schnell den Überblick.
Simulation einer Statemachine
Heute stelle ich als Beispiel die Simulation der gerade behandelten Statemachine vor. In dieser Art und Weise lassen sich auch andere VHDL Programme simulieren.
Codeschnipsel
-- Instantiate the Unit Under Test ( UUT ) uut: Top_Level PORT MAP ( clk => clk, ce => ce, x => x, y => y, LED1 => LED1, LED2 => LED2 ) ; -- Clock process definitions clk_process :process begin clk <= '0' ; wait for clk_period/2 ; clk <= '1' ; wait for clk_period/2 ; end process ; -- Stimulus process stim_proc: process begin wait for 10 us ; -- nach 10us Clockenable auf high ce <= '1' ; -- kurzer Impuls ( 5 Takte lang ) auf x wait for 100 us ; x <= '1' ; wait for 5us ; x <= '0' ; -- kurzer Impuls ( 5 Takte lang ) auf y wait for 100 us ; y <= '1' ; wait for 5us ; y <= '0' ; -- kurzer Impuls ( 5 Takte lang ) auf x wait for 100 us ; x <= '1' ; wait for 5us ; x <= '0' ; -- kurzer Impuls ( 5 Takte lang ) auf y wait for 100 us ; y <= '1' ; wait for 5us ; y <= '0' ; -- Ende der Simulation wait ; end process ;
Erklärung
Die Simulation besteht in diesem Fall aus drei Teilen. Im ersten Teil wird die „Unit Under Test“, also unsere Statemachine, instanziiert. Der zweite Schritt ist ein Prozess zur Taktgenerierung. Die eigentliche Simulation erfolgt dann im dritten Teil. Hier wird nun in einer zeitlichen Reihenfolge eingetragen, was für Signale zur Stimulation genutzt werden sollen. Konkret ist das hier:
- Nach 10µs das Signal CE auf high setzen.
- Nach 100µs das Signal x für 5µs high setzen.
- Nach 100µs das Signal y für 5µs high setzen.
- Nach 100µs das Signal x für 5µs high setzen.
- Nach 100µs das Signal y für 5µs high setzen.
Wie sich die UUT verhält, zeigt anschließend das Simulationsergebnis.
Die komplette Simulationsdatei kannst du hier herunterladen. Diese wird in die Xilinx ISE hinzugefügt. Aber nicht die Datei oben angegebene Quelltextdatei der Statemachine vergessen 😉 Sonst kann die UUT nicht simuliert werden.
Nachfolgend ein Ausschnitt aus dem Simulationsprogramm Xilinx ISim zu sehen. Hier sind die Eingangs- und Ausgangssignale dargestellt. Zusätzlich können auch die internen Signale (in diesem Fall nur der aktuelle und zukünftige State) angezeigt werden.
Die Simulation von VHDL-Programmen hilft, die Qualität und Funktionalität zu überprüfen, noch bevor Hardware zur Verfügung stehen muss. Gerade dadurch ist man in der Lage, auch Teile der Software (z.B. bei größeren Projekten) auf ihre Funktion zu überprüfen.
Es lassen sich auch Signalströme einlesen und ausgeben. Dadurch ist z.B. eine Überprüfung von Routinen der Signalverarbeitung möglich.
Filtern von Signalen
Je nach Aufgabe kann es erforderlich sein, ein Signal zu filtern. Sei es um über einen Tiefpass höhere Frequenzen zu unterdrücken oder mit einem Hochpass den Gleichspannungsanteil zu entfernen.
Die erste Grundlage wird in diesem Abschnitt gelegt.
Codeschnipsel
-- Komponente einbinden component TP_Eingang port ( -- Eingangstakt aclk : in STD_LOGIC ; -- Eingangsdaten s_axis_data_tvalid : in STD_LOGIC ; s_axis_data_tready : out STD_LOGIC ; s_axis_data_tuser : in STD_LOGIC_VECTOR( 1 downto 0 ) ; s_axis_data_tdata : in STD_LOGIC_VECTOR( 23 downto 0 ) ; -- Ausgangsdaten m_axis_data_tvalid : out STD_LOGIC ; m_axis_data_tuser : out STD_LOGIC_VECTOR( 1 downto 0 ) ; m_axis_data_tdata : out STD_LOGIC_VECTOR( 47 downto 0 ) ; event_s_data_chanid_incorrect : OUT STD_LOGIC ) ; end component ;
-- Eingangsfilter einbinden Inst_TP_Eingang : TP_Eingang port map ( aclk => Clk, s_axis_data_tvalid => s_axis_data_tvalid, s_axis_data_tready => s_axis_data_tready, s_axis_data_tuser => Kanal_cnt, s_axis_data_tdata => Filter_In, m_axis_data_tvalid => m_axis_data_tvalid, m_axis_data_tuser => Kanal_out, m_axis_data_tdata => Filter_Out, event_s_data_chanid_incorrect => event_s_data_chanid_incorrect ) ;
Erklärung
Das im Codeschnippsel gezeigte Beispiel ist eine Instanziierung eines FIR Filter IP-Cores von Xilinx. Dieser wird über verschiedene Parameter vorkonfiguriert. Dazu gehören u.a. Breite der Eingangs- und Ausgangssignale, welche Steuerleitung verwendet werden sollen, usw. Ein wichtiger Parameter sind die Filterkoeffizienten. Diese geben dem FIR-Filterblock erst seine Filterfunktion.
So kann hier ein Tiefpass Filter erzeugt werden, oder ein Hochpass Filter, oder – sofern man das in dieser Art machen möchte – ein Bandpass Filter.
Das oben gezeigte Code-Beispiel bezieht sich auf den LogiCORE IP FIR von Xilinx. Im Product Guide ist das unten gezeigte Ersatzschaltbild zu finden. Der Trick im FPGA ist hier, dass die einzelnen Taps eines FIR-Filter nicht gleichzeitig, sondern nacheinander berechnet werden und die Coeffizienten und die Eingangsdaten zwischengespeichert werden. Da das FPGA mit sehr hohen Taktfrequenzen arbeiten kann, werden so die Ressourcen effizient ausgenutzt.
Den Product Guide zum LogiCORE IP FIR von Xilinx findest du hier.
Ersatzschaltbild
Zusammenfassung
Theoretisch gibt es auch die Möglichkeit, direkt einen Bandpass Filter in eine FIR-Struktur zu bekommen. Doch da dies recht aufwändig ist, gibt es eine andere Methode. Darauf werde ich jedoch in einem späteren Blogpost eingehen.
Zum Thema FIR-Filter wurden schon diverse Artikel veröffentlicht, so dass ich an dieser Stelle gerne auf zwei verweisen möchte
Hier werden die Eigenschaften und auch die Theorie hinter den FIR-Filtern beschrieben.
Umsetzung von FIR Filtern
Im letzten Abschnitt ging es generell um das Filtern von Signalen. Wie genau diese Filter nun umgesetzt werden, erklärt der nachfolgende Abschnitt.
Erklärung
FIR Filter bestehen aus drei Hauptkomponenten:
- Verzögerung Zwischenspeicher (Flip-Flops)
- Skalierung über Koeffizienten
- Addierung über Summierer
In den Verzögerungen (die Taps) werden die zeitdiskreten Signalwerte zwischengespeichert. Die Anzahl dieser hintereinander geschalteten Verzögerungen bestimmt die Länge des FIR-Filters. Jede Verzögerung hat eine eigene Skalierung, der Wert des Taps. Das Ergebnis aller skalierten und verzögerten Werte ergibt dann durch Addition den entsprechenden Ausgangswert. Zur Verdeutlichung hier eine exemplarische Darstellung:
Nun ist es bei sehr schnellen Eingangssignalen manchmal notwendig, ein Signal mit seiner Taktrate weiterzuverarbeiten. Diese Fähigkeit besitzt ein FPGA, da er massiv parallel arbeiten kann. Die im Prinzipbild dargestellten Teile lassen sich 1:1 in eine VHDL-Struktur bringen. Das bedeutet jedoch einen sehr großen Aufwand an Ressourcen.
Ist die Abtastrate des Eingangssignals jedoch langsamer, als der Systemtakt, können im entsprechenden Verhältnis mehrere Berechnungen pro Abtasttakt durchgeführt werden. Ist das Verhältnis Systemtakt: Eingangstakt zum Beispiel 1:10, so kann das Signal 10 mal pro Eingangstakt verarbeitet werden. Also würde es für ein FIR Filter mit 11 Koeffizienten reichen.
Zusammenfassung
Eine geschickte Wahl von Eingangstakt zu Systemtakt ermöglicht es also ein FIR-Filter mit weniger Ressourcen aufzubauen. Manchmal ist das Nutzsignal im Verhältnis zur Abtastrate klein, dann lohnt es sich, mehrstufig von einem hohen Abtastwert zu einem niedrigen Abtastwert zu kommen. In jeder Stufe kann dann die Anzahl der Koeffizienten erhöht werden.
In einem Projekt diese Eigenschaft genau ausgenutzt und kann es nur weiterempfehlen. Ich hatte dort mit Zehntelband Filtern gearbeitet. Das bedeutet, es die Abtastrate wurde von Stufe zu Stufe um Faktor 10 reduziert, so dass die eingesetzt Filter immer länger und damit besser werden konnten.
Hier kommt es allerdings auf eine gute Auswahl der Koeffizienten an, zumal in diesem Projekt das auf der Gleichspannungsanteil des Signals nicht verändert werden durfte, und das ohne Gleitkommarechnung.
Nachladbare FIR Filter
Moderne Systeme können es erforderlich machen, nachladbare FIR Filter einzusetzen. Wie diese Filter neuen Koeffizienten erhalten und was dafür gemacht werden muss, beschreibt diesen Abschnitt.
Warum werden Filter nachgeladen?
Werden Signale gefiltert, kann eine Anwendung es erfordern, dass die Koeffizienten auch nachträglich geändert werden können. Die Software kann dann die Bandbreite verändern oder aus einem Tiefpass einen Bandpass oder auch einen Hochpass bauen.
Somit ist eine Verbindung zwischen der Flexibilität eines Mikroprozessors (oder auch eines DSPs) und der Leistungsfähigkeit eines FPGAs geschaffen.
Wie können nachladbare FIR Filter erstellt werden?
In Xilinx FPGAs steht mit dem FIR Filter Compiler (hier wird der IP LogiCORE FIR Compiler v5.0 z. B. für Xilinx Spartan 6 FPGAs besprochen) die Möglichkeit bereit, ein Filter nachladbar zu gestalten. Im FIR Filter Compiler werden die Signalbreite, die Samplerate, die Kanalanzahl usw. festgelegt. Weiterhin steht hier der initiale Koeffizientensatz bereit.
Die Ports im VHDL Quelltext bei diesem Filter sehen im Beispiel so aus:
Inst_FIR_Filter : FIR_Filter port map ( clk => clk, coef_ld => FIR_coef_ld, coef_we => FIR_coef_we, coef_din => FIR_coef_din, din_1 => ADC_signal, din_2 => ADC_signal, dout_1 => ADC_FIR1, dout_2 => ADC_FIR2 ) ;
Dabei gibt es die Signaleingänge din_x und Signalausgänge dout_x. In diesem Fall sind es zwei Kanäle, die parallel verarbeitet werden.
Weiterhin sind die Steuereingänge für das Nachladen der FIR Koeffizienten coef_ld, coef_we und coef_din vorhanden. Diese werden mit einer Statemachine benutzt, um die Koeffizienten nachzuladen. Die Reihenfolge der Koeffizienten, wie sie nachzuladen sind, wird vom FIR Compiler als Textdatei bereitgestellt.
Wie geschieht das Nachladen?
Entscheidend beim Nachladen eines FIR Filters sind die drei oben genannten Leitungen:
coef_ld
Dieser Eingang kündigt dem FIR Filter den Start des Nachladens an. Die Länge beträgt einen Taktzyklus. In diesem Moment wird auch der interne Adresszähler, mit dem die ankommenden Werte zugeordnet werden, zurückgesetzt.
coef_we
Ist dieses Signal positiv, wir das Datum am Port coef_din in das Filter übernommen und anschließend der Adresszähler um eins hochgezählt.
coef_din
An diesen Port werden die neuen Koeffizienten angelegt und verarbeitet. An welche Position im FIR Filter das Datum geschrieben wird, wird durch den Algorithmus des Filters entschieden. Die Reihenfolge ist vorab schon festgelegt.
Timingdiagramm
Fazit
Mit einem recht simplen Ablauf (z.B. in einer kleinen Statemachine) kann ein FIR Filter mit neuen Koeffizienten versorgt werden. Natürlich lassen sich auch Filter nachladen, die schon von Hause aus mehrere Koeffizientensätze haben. Dann kommt eine weitere Steuerleitung hinzu, mit der dann der entsprechende Koeffizientensatz ausgewählt wird.
DDS Generator
Zunächst gibt es einen Einblick, wie ein DDS Generator in einem FPGAs aufbaut wird. Ein DDS Generator wird in der Signalverarbeitung für verschiedene Zwecke benötigt.
Anschließend kommt ein Beispiel zur Umsetzung dieses DDS Generators. Vorab: Es hat etwas mit Radio zu tun 🙂
Grundlagen
Was bedeutet DDS nun genau? DDS bedeutet direkte digitale Synthese. Damit ist gemeint, dass man ein periodisches Signal mit theoretisch beliebig feinen Schritten und der dazu gehörigen Amplitude erzeugen kann. Und da dieses Signal vollständig im digitalen erzeugt wird, hat man auch keine „Probleme“ der analogen Welt, also Temperaturdrift, Toleranzen usw.
Das ganze Verfahren ist mathematisch berechenbar und somit auch sehr gut simulierbar.
Details
Gehen wir nun etwas genauer auf das eigentliche Verfahren ein. Es setzt sich im wesentlichen aus … Komponenten zusammen:
- Steuerwert für die Frequenz
- Addierer für das Phasenwort
- Phasenregister
- Tabelle zur Wandlung von der Phase zur Amplitude
Soll das Signal nun noch analog ausgegeben werden, dann ist ein Digital-Analog-Wandler erforderlich. Das Ersatzschaltbild zeigt den praktischen Aufbau dieses Systems.
Nimmt man zum Beispiel nun ein Sinussignal, dann liegt eine Sinustabelle in der Tabelle für die Wandlung von Phase zur Amplitude. Um den Speicherplatz zu optimieren, reicht es auch oftmals aus, nur eine viertel Sinuswelle abzuspeichern. Wenn der Steuerwert klein ist, dann ändert sich die Phase nur langsam und damit wird eine tiefe Frequenz erzeugt. Ist der Steuerwert groß, dann ändert sich die Phase schnell (da in jedem Takt ein hoher Wert hinzuaddiert wird) und es wir eine hohe Frequenz erzeugt.
Theoretisch kann man bis zur halben Taktfrequenz gehen, dann erzeugt man im digitalen sozusagen nur noch ein Rechtecksignal.
Ersatzschaltbild
Beispiel für einen DDS Generator
Nachfolgend wird ein Bespiel für einen FM Modulator gezeigt, der mit Hilfe der DDS umgesetzt wurde. Dabei gibt es folgende Eingangsgrößen:
takt = Systemtakt vom FPGA
ce = Clock Enable Signal
out24576 = Clock Enable Signal mit 40,69µs
HubScale = Hub der FM
audio_24576 = Audio-Eingang mit Samplerate 24,576MHz
Der Ausgang kann direkt an einen 16Bit DA-Wandler angeschlossen werden. Dort wird dann das FM modulierte Audiosignal ausgegeben. Die Trägerfrequenz wird mit DPhi vorgegeben
Codeschnipsel
-- Hub-Skalierer einbauen (Xilinx IP) Inst_hubscale_multiplier: hubscale_multiplier PORT MAP( clk => takt, ce => out24576, a => audio_24576, -- Signal Eingang b => HubScale, -- Skalierungs Eingang p => audio_scale ) ; -- DDS-Akku Inst_MyDDSAccu: MyDDSAccu PORT MAP( clk => takt, ce => ce, ce_low => out24576, traeger_in => DPhi, audio_in => audio_scale( 64 downto 33 ), phi => Phi_Out ) ; -- Sin/Cos Tabelle einbauen Inst_cossin_tab: cossin_tab PORT MAP ( THETA => Phi_Out( 31 downto 18 ), CLK => takt, CE => ce, SINE => Tab_Sin_Out ) ; -- Ausgangs-FF ( OFD16 sollen das werden ) process( sys_takt ) begin if rising_edge( sys_takt ) then DAC_P1 <= Tab_Sin_Out ; end if ; end process ;
Der DDSAccu hat folgenden Quelltext:
architecture Behavioral of DDSAccu is signal cnt : STD_LOGIC_VECTOR ( 31 downto 0 ) := x"00000000" ; signal sum : STD_LOGIC_VECTOR ( 31 downto 0 ) := x"00000000" ; signal traeger_in_int : STD_LOGIC_VECTOR ( 31 downto 0 ) := x"00000000" ; signal audio_in_int : STD_LOGIC_VECTOR ( 31 downto 0 ) := x"00000000" ; begin -- Eingangsprozess process( clk ) begin if rising_edge( clk ) then if ce_low = '1' then traeger_in_int <= traeger_in ; audio_in_int <= audio_in ; end if ; end if ; end process ; -- Zählprozess process( clk ) begin if rising_edge( clk ) then if ce = '1' then sum <= traeger_in_int + audio_in_int ; cnt <= cnt + sum ; end if ; end if ; end process ; -- Ausgabeprozess process( clk ) begin if rising_edge( clk ) then phi <= cnt ; end if ; end process ; end Behavioral ;
Erklärung
Im DDSAccu sieht man den eigentlichen DDS. Es gibt einen Eingangs- und einen Ausgangsprozess und in der Mitte die eigentliche Verarbeitung. Hier werden zuerst das Audiosignal zum Träger hinzuaddiert und dieser dann als Phaseninkrement an den Zähler übergeben.
Diese Phase wird dann über eine Sinustabelle in ein Sinussignal übersetzt und abschließend an den DA-Wandler ausgegeben.
Fazit
Ein FM-Modulator ist sehr einfach zu programmieren. Spannend wird es bei der Sauberkeit des Spektrums am Ausgang. Hier muss auf eine entsprechend hohe Samplerate geachtet werden, da ansonsten die Spiegelfrequenzen des Signals im Spektrum zu sehen sind.
Signalmischer
Bei manchen Arten von Signalverarbeitungen werden Signalmischer benötigt. Dazu gehören zum Beispiel Basisbandmischer. Wie diese Signalmischer gebaut werden, erkläre ich im folgenden Abschnitt.
Starten wir zunächst wieder mit einem Codeschnippsel und der nachfolgenden Erklärung dazu.
Codeschnipsel
Inst_Signal_Multiplizierer : Signal_Multiplizierer port map ( clk => Clk, ce => CE, a => Signaleingang, b => Mischsignal, p => Ausgang ) ;
Erklärung
Was ist ein Signalmischer eigentlich? Nun, ein Signalmischer hat zwei Eingangssignale und ein Ausgangssignal. Auch wenn das Titelbild es vermuten lässt, hat dieser Mischer nichts mit dem additiven Zusammenmsichen von Signalen in einem Mischpult zu tun.
Der Signalmischer multipliziert zwei Signale im Zeitbereich miteinander und wird zur Frequenzumsetzung verwendet.
Wird ein niederfrequentes Signal f1 mit einem hochfrequenten Signal f2 multipliziert, entstehen im Frequenzbereich Mischprodukte. Dazu gehört das Signal f1 und das Signalpaar f2-f1 und f2+f1, sowie die Vielfachen n*f2-f1 und n*f2+f1. Mit Hilfe von nachgeschalteten Filtern lassen sich so die gewünschten Frequenzanteile herausfiltern.
Ersatzschaltbild
Zusammenfassung
Ein multiplikativer Mischer wird also für das herauf oder auch herunter Mischen von Signalen im Frequenzbereich benötigt. Die Durchführung erfolgt wie im obigen Codeschnipsel mit einem Multiplizierer. Zu beachten ist hier, dass beide Signal und der Multiplizierer mit der selben Taktfrequenz laufen müssen.
Was kann man noch mit einem Signalmischer machen? Der Einsatzzweck ist zum Beispiel ein Basisbandmischer, der Signale von einer Trägerfrequenz in das komplexe Basisband (bei 0Hz) heruntermischt. Dabei werden die Signale in einen I und einen Q Anteil aufgeteilt. Genutzt wird dieses Verfahren z.B. in Bandpässen oder bei der Bestimmung von Phasen.
Die Mitte der Signal
Direkt im Anschluss an den Signalmischer, geht es nun zu den Filtern für die Mitte der Signale. Gemeint ist hier nicht die Zeit-, sondern die Frequenzebene. Nachfolgend behandle ich das FIR Bandpassfilter bzw. welche elegante Methode es gibt, diese einfach im FPGA umzusetzen.
Starten wir zunächst wieder mit einem Codeschnippsel und der nachfolgenden Erklärung dazu.
Erklärung
Es ist eine Kombination aus den beiden schon bekannten Elementen: Tiefpassfilter, Mischer und ein Sinus- und Cosinusgenerator. Dieser Generator findet sich etwas oberhalb von hier und nennt sich DDS-Generator.
In der Signaltheorie wird beschrieben, dass man einen Bandpass auch als Tiefpass darstellen kann, sofern man die Mittenfrequenz 0 annimmt. Man kann nun durch Heruntermischen des Signals in das Basisband das Signal auf die Mittenfrequenz 0 bekommen und es hier nun Tiefpassfiltern. Dies geschieht wie schon beschrieben über FIR-Filter.
Zu beachten ist hier allerdings, dass es sich um das komplexe Basisband handelt (I-Q-Signal). Das Eingangssignal wird also mit Sinus und Cosinus gemischt und damit in zwei Stränge aufgeteilt. Anschließend werden diese beide Stränge durch ein FIR Tiefpassfilter geschickt.
Möchte man das Signal anschließend wieder in seiner ursprünglichen Frequenzlage weiternutzen, ist es wieder hochzumischen und muss abschließend zusammenaddiert werden
Fazit
Natürlich könnte man auch einen Bandpass als FIR Filter designen. Doch ist es effektiver, diese Filterung im Basisband durchzuführen. Man kann so auf schon bestehende Strukturen zurückgreifen und die Berechnung der Koeffizienten gestaltet sich ebenfalls einfacher.
Temperaturmessung mit dem FPGA
Im ersten Moment scheint es übertrieben, eine Temperaturmessung mit dem FPGA durchzuführen, jedoch kann es eine praktische Nebenbeschäftigung für ein FPGA sein.
Im ersten Abschnitt schauen wir uns die Hardware-Seite an und im Anschluss die Software-Seite.
Einführung
Viele Systeme mit FPGAs haben eine Schnittstelle zu übergeordneten Systemen und dazu noch den einen oder anderen Pin frei. Jetzt lässt sich natürlich die Temperatur auch mit speziellen ICs und Schnittstellen messen, doch das FPGA kann mit Hilfe eines externen Komparators die Temperatur auch recht einfach messen.
Als Temperatursensor kann ein einfacher NTC-Widerstand benutzt werden, dazu ein passender Komparator und ein paar passive Komponenten. Das ist alles an Hardware (neben dem FPGA natürlich), was für die Messung benötigt wird.
Der Analog-Digital-Wandler wird mit Hilfe des Komparators gebildet. Das FPGA generiert ein PWM-Signal (ein Puls-Weiten-Moduliertes Signal) und registriert den Wechsel des Komparatorausgangs. Der Wert des PWM-Signals entspricht dem Signalpegel am Eingang des Komparators.
Natürlich hat ein NTC-Widerstand eine nichtlineare Kurve, diese kann im FPGA jedoch recht einfach korrigiert werden. Im einfachsten Fall über eine Tabelle.
Abschließend für diesen Teil ein Beispielbild:
Die Hardware-Seite ist beschrieben, so dass nun noch die passende Software fehlt.
Codeschnipsel
-- Rampensignal erzeugen process( clk ) begin if rising_edge( clk ) then if sub_cnt = 0 then PWM_val <= PWM_val + 1 ; end if ; sub_cnt <= sub_cnt + 1 ; end if ; end process ; -- PWM Erzeugung process( clk ) begin if rising_edge( clk ) then PWM_cnt <= ( "0" & PWM_cnt( 11 downto 0 ) ) + ( "0" & PWM_val ); end if ; end process ; -- PWM Ausgabe process( clk ) begin if rising_edge( clk ) then PWMout <= PWM_cnt( 12 ) ; end if ; end process ; -- Komparatoreingänge synchronisieren process( clk ) begin if rising_edge( clk ) then Comp_int <= Comp_int( 2 downto 0 ) & Comp_In; end if ; end process ; -- Komparatorwert ausgeben process( clk ) begin if rising_edge( clk ) then if Comp_int( 3 downto 1 ) = "110" then NTC_val <= PWM_val( 11 downto 4 ) ; end if ; end if ; end process ; NTC : NTC_Kennlinie port map ( clka => clk, addra => NTC_val, douta => TempWertOut ) ;
Erklärung
Bevor die Erklärung startet, noch eine kurze Anmerkung zu den Begrifflichkeiten. Der Begriff PWM ist nur teilweise korrekt, hier geht es mehr um eine PDM (Pulsdichtemodulation) mit Hilfe eines Sigma-Delta Modulators. Da jedoch der Begriff PWM bekannter ist und man sich eher etwas darunter vorstellen kann, benutze ich diesen Begriff.
Der obige Prozess läuft mit 10MHz, wobei der Takt mit Hilfe eine Zählers und dem Clock-Enable Signal um den Faktor 16 reduziert wird. Der PWM-Counter und damit auch das Abtastwort des Analog-Digital-Wandlers ist 12 Bit breit. Damit ergibt sich eine PWM-Signal-Frequenz von ca. 152Hz (10MHz / 16 / 2^12).
Im ersten Schritt wird ein Rampensignal erzeugt, das mit 1/16 der Prozessfrequenz inkrementiert wird. Die Periode ist wie oben erwähnt bei 152Hz.
Im zweiten Schritt wird im PWM Akkumulator, der auf der Systemfrequenz läuft, das aktuelle Inkrement zum Zähler hinzugezählt.
Im dritten Schritt wird das höchste Bit als PWM-Signal ausgegeben.
Je höher nun das Inkrement ist, desto mehr Einsen werden als PWM-Signal ausgegeben.
Parallel zu diesem Prozess wird nun der Komparator eingelesen und auf den Systemtakt synchronisiert. Wenn nun eine positive Flanke erkannt wird, dann speichert ein Prozess den Wert des Rampensignals, denn dann hat die Rampe den selben Wert wie der zu messende analoge Wert. Dieser Wert wird dann für die weitere Verarbeitung bereit gestellt. Für die Temperaturmessung erfolgt nun noch eine Anpassung der Kennlinie des NTC.
In der Simulation sieht das ganze dann wie folgt aus:
Zu erkennen ist hier sehr gut die Änderung in den Ausgangspulsen der PWM und der Sprung in der Rampe.
Fazit
Natürlich lässt sich dieses Verfahren statt für eine Temperaturmessung auch auf andere analoge Signale anwenden. Es stellt einen sehr einfach nachzubauenden AD-Wandler dar.
Wie schon beschrieben, liegt der Vorteil hier in der Einfachheit dieses Messprinzips, denn wenn eine Systemkomponente nur aus einem FPGA besteht und dieser über einen Bus mit dem Hauptsystem kommuniziert, lassen sich auch mit geringem Aufwand (also ohne extra ADCs mit speziellen Schnittstellen) eine Diagnosemöglichkeit einbauen.
8b/10b Kodierung in VHDL
In vielen seriellen Übertragungssystemen wird eine 8b/10b Kodierung eingesetzt, um zusätzlich zum Datenstrom weitere Informationen übertragen zu können. Teilweise werden diese Kodierungen eingesetzt, um ein Signal gleichspannungsfrei zu machen, damit es problemlos über Transformatoren übertragen werden kann.
Als erstes zeige ich in einem längeren Codeschnippsel, wie eine 8b/10b Kodierung im HDMI Datenstrom eingesetzt wird:
Codeschnipsel
xored( 0 ) <= data( 0 ) ; xored( 1 ) <= data( 1 ) xor xored( 0 ) ; xored( 2 ) <= data( 2 ) xor xored( 1 ) ; xored( 3 ) <= data( 3 ) xor xored( 2 ) ; xored( 4 ) <= data( 4 ) xor xored( 3 ) ; xored( 5 ) <= data( 5 ) xor xored( 4 ) ; xored( 6 ) <= data( 6 ) xor xored( 5 ) ; xored( 7 ) <= data( 7 ) xor xored( 6 ) ; xored( 8 ) <= '1' ; xnored( 0 ) <= data( 0 ) ; xnored( 1 ) <= data( 1 ) xnor xnored( 0 ) ; xnored( 2 ) <= data( 2 ) xnor xnored( 1 ) ; xnored( 3 ) <= data( 3 ) xnor xnored( 2 ) ; xnored( 4 ) <= data( 4 ) xnor xnored( 3 ) ; xnored( 5 ) <= data( 5 ) xnor xnored( 4 ) ; xnored( 6 ) <= data( 6 ) xnor xnored( 5 ) ; xnored( 7 ) <= data( 7 ) xnor xnored( 6 ) ; xnored( 8 ) <= '0' ; -- Count how many ones are set in data ones <= "0000" + data( 0 ) + data( 1 ) + data( 2 ) + data( 3 ) + data( 4 ) + data( 5 ) + data( 6 ) + data( 7 ) ; -- Decide which encoding to use process( ones, data( 0 ), xnored, xored ) begin if ones > 4 or ( ones = 4 and data( 0 ) = '0' ) then data_word <= xnored ; data_word_inv <= NOT( xnored ) ; else data_word <= xored ; data_word_inv <= NOT( xored ) ; end if ; end process ; -- Work out the DC bias of the dataword ; data_word_disparity <= "1100" + data_word( 0 ) + data_word( 1 ) + data_word( 2 ) + data_word( 3 ) + data_word( 4 ) + data_word( 5 ) + data_word( 6 ) + data_word( 7 ) ; -- Now work out what the output should be process( clk ) begin if rising_edge( clk ) then if blank = '1' then -- In the control periods, all values have and have balanced bit count case c is when "00" => encoded <= "1101010100" ; when "01" => encoded <= "0010101011" ; when "10" => encoded <= "0101010100" ; when others => encoded <= "1010101011" ; end case ; dc_bias <= ( others => '0' ) ; else if dc_bias = "00000" or data_word_disparity = 0 then -- dataword has no disparity if data_word( 8 ) = '1' then encoded <= "01" & ; data_word( 7 downto 0 ); dc_bias <= dc_bias + data_word_disparity ; else encoded <= "10" & ; data_word_inv( 7 downto 0 ); dc_bias <= dc_bias - data_word_disparity ; end if ; elsif ( dc_bias( 3 ) = '0' and data_word_disparity( 3 ) = '0' ) or ( dc_bias( 3 ) = '1' and data_word_disparity( 3 ) = '1' ) then encoded <= '1' & ; data_word( 8 ) & data_word_inv( 7 downto 0 ); dc_bias <= dc_bias + data_word( 8 ) - data_word_disparity ; else encoded <= '0' & ; data_word; dc_bias <= dc_bias - data_word_inv( 8 ) + data_word_disparity ; end if ; end if ; end if ; end process ;
Erklärung
Im obigen Codeschnippsel werden die Datensignale kodiert, dazu werden die Werte entweder XOR oder XNOR kodiert und die Information, wie kodiert wird, im 9. Bit abgelegt. Zusätzlich kann das Signal noch invertiert werden. Diese Information liegt im 10. Bit. Die Entscheidung, ob XOR oder XNOR eingesetzt wird, hängt von der Verteilung der Einsen und Nullen im Signal ab.
Damit wird insgesamt der DC-Anteil im Signal minimiert.
Der obige Codeschnippsel ist als Diagramm in der HDMI-Spezifikation zu finden. Hier wird der Ablauf beschrieben, wie aus 8Bit Daten ein 10Bit Symbol wird.
Aus der HDMI-Spezifikation ist folgendes Diagramm bekannt:
Fazit
Dieses Beispiel zeigt die Anwendung in HDMI. Es gibt auch in anderen seriellen Übertragungen diese Art der Kodierung, so gibt es zum Beispielt noch die 64b/66b bei Ethernet.