Hirarchisches FPGA-Design und Schleifen in VHDL

Im ersten Schritt wird ein VHDL-Modul erstellt welches:

  • Einen Systemtakt ausgibt
  • Einen synchronen Reset erzeugt
  • Eine Systemzeit (Zeitstempel) erzeugt
  • Eine Zeitbasis erzeugt 

Der Grund, diese Funktionen in ein Modul zu verlagern ist, dass es so einfacher zu testen ist und nach noch folgenden Erweiterungen in vielen FPGAs eingesetzt werden kann.

Oft hat man Prozesse in FPGAs abzubilden, welche sich auf die reale Zeit beziehen, z.B. eine Entprellung von Taster-Eingängen für die Glitches von weniger als z.B. 5 ms ausgeblendet werden sollen. Man kann natürlich jedes mal einen Zähler auf 100 MHz loslaufen lassen und wenn er 500000 erreicht hat reagieren. Oder man benutzt die Zeitbasis und zählt in ms Schritten bis 5.

Spezifikation

Nr Name Spezifikation
1 Reset NachPowerOn und nach nach dem asynchronen Reset wird Init aktiviert
TimeStamp und TimeBase werden durch den Reset zurück gesetzt
2 Clk Der Takteingang ist mit dem Taktausgang verbunden
3 TimeStamp Ist ein std_logic_vector, der die Taktzyklen seit Reset zählt
4 TimeBase Dieser Vektor enthält Trigger Signale die in 10er-Potenzen von 10 ns bis 1 Sekunde synchron aktiviert werden
--! FPGA reset and time handling
entity SysService is
	port (
		ExtClk		: in  std_logic; --! External clock
		Clk 		: out std_logic; --! Processing clock
		Reset		: in  std_logic; --! High acitve asynchronous reset input
		Init		: out std_logic; --! High acitve synchronous reset output
		TimeBase	: out std_logic_vector(8 downto 0); --! CE for 1 Hz .. 100 MHz
		TimeStamp	: out std_logic_vector(54 downto 0) --! Timestamp (> 10 years @ 100 MHz)
	);
end SysService;

Unser neues Modul hat natürlich eine normale Schnittstelle entity nach außen. Wer zuvor schon die Tests angeschaut hat, konnte ja sehen, dass man VHDL-Module in VHDL-Module einbetten kann.

Die ersten vier Signale kümmern sich um Takt und Reset, die beiden letzten Signale um die Services TimeBase und TimeStamp.

-- ============================================================================
-- constant declarations
-- ===================
constant TimeIdx10ns	   	: integer := 0;
constant TimeIdx100ns	   	: integer := 1;
constant TimeIdx1us 	   	: integer := 2;
constant TimeIdx10us	   	: integer := 3;
constant TimeIdx100us	   	: integer := 4;
constant TimeIdx1ms 	   	: integer := 5;
constant TimeIdx10ms	   	: integer := 6;
constant TimeIdx100ms 	   	: integer := 7;
constant TimeIdx1s		   	: integer := 8;

-- ============================================================================
-- signal declarations
-- ===================

-- Clock and reset
signal ProcClk			: std_logic;					--! Internal processing clock
signal GenInitR 		: std_logic := '1'; 			--! Causes an init pulse after power on
signal ResetR			: std_logic;					--! Synchronized reset input
signal InitC			: std_logic;					--! Synchronous reset condition
signal InitR			: std_logic;					--! Synchronous reset

-- Time stamp
signal	TimeStampR		: std_logic_vector(54 downto 0);	--! Timestamp counter

-- Time base
type	tDecadeChain is array(7 downto 0) of integer range 0 to 9;
signal	DecadeChainR 	: tDecadeChain;					--! one counter for each decade
signal	IncDecadeC	 	: std_logic_vector(8 downto 0);	--! increment decade counter
signal	IncDecadeR	 	: std_logic_vector(8 downto 0);	--! increment decade counter (registered)
signal	ResDecadeC	 	: std_logic_vector(7 downto 0);	--! reset decade counter
signal	ResDecadeR	 	: std_logic_vector(7 downto 0);	--! reset decade counter (registered)

Im Interface war ja schon der Vektor (8..0) TimeBase zu sehen. Mit den Konstanten TimeIdx... kann man darauf zugreifen. TimeBase liefert zyklische Triggersignale. Über den TimeIdx kann man die Zykluszeit wählen.

Unten kommt dann der Abschnitt Time base, den ich erläutern möchte.

Neu ist das Array of integer. Genauer gesagt integer von 0 .. 9 (damit weiß die Synthese wie viele Bits maximal benötigt werden). An den TimeIdx... konnte man ja schon erkennen, dass die TimeBase Signale von bit zu bit um den Faktor 10 langsamer werden. Dazu muss man jeweils von 0..9 zählen. Das wird mit der DecadeChain erreicht. Für jeden der Teiler gibt es zudem ein Reset und ein Inkrement. Damit hat man die Signale beieinander, mit denen man die Zeitbasis erzeugen kann.

	-- Clock generation
	ProcClk 	<= ExtClk;
	Clk 		<= ProcClk;

	--! Reset generation
	p_reset : process (ProcClk)
	begin
		if rising_edge(ProcClk) then
			GenInitR	<= GenInitR and not InitR;	-- Release InitR after it was activated
			InitR		<= InitC;
			ResetR		<= Reset;
		end if;
	end process;

	InitC				<=	ResetR or GenInitR; 		-- Reset on button and power on
	Init				<=	InitR;

Die Takterzeugung ist trivial aber schaut unnötig Kompliziert aus: ExtClk, ProcClk und Clk sind ja wie die einige Dreifaltigkeit. Wozu drei Signale, wenn es auch ein Clk-Eingangssignal wie sonst tun würde.

Das Modul soll ja universell sein. Man könnte sich in einer anderen Anwendung vorstellen, dass man einen externen 33 MHz Takt hat, aber intern mit 100 MHz arbeiten möchte. Dann würde man ExtClk mit einem PLL-Eingang verbinden, in der PLL die 100 MHz erzeugen und diese dann auf ProcClk ausgeben.

Clk ist als 'out', also Ausgangssignal definiert, es kann also in diesem Modul nicht als Eingang verwendet werden. Deswegen wird Clk ausgegeben und ist synchron zu ProcClk.

Der Rest dieses Snippets ist die Reset-Generierung wie wir sie schon so ähnlich hatten. Wichtig ist hier nur, dass der asynchrone Reset von außen in ResetR einsynchronisiert wird und es dann synchron bis zu InitR/Init weiter geht.

	--! Time stamp
	p_timestamp : process (ProcClk)
	begin
		if rising_edge(ProcClk) then
			if (InitR = '1') then
				TimeStampR        <= (others => '0');
			else
				TimeStampR        <= TimeStampR + 1;
			end if;
		end if;
	end process;
	TimeStamp        <= TimeStampR;

p_timestamp kümmert sich um einen Zähler, der die Clock-Ticks sein Anfang des Universums zählt. In diesem Universum also ab dem Reset.

TimeStampR ist als 55 Bit Zahl definiert, was bei 100 MHz erst nach mehr als 10 Jahren überläuft. Warum 55 Bit? Ich denke, als ich dieses Modul geschrieben habe, konnte ein 55 Bit Zähler in dem Verwendeten FPGA bei 10 ns gerade noch realisiert werden. Heute geht wohl mehr. Du kannst es ja ausprobieren.

	--! Time base
	p_timebase : process (ProcClk)
	begin
		if rising_edge(ProcClk) then
			-- Divider chain handling
			for i in 0 to 7 loop
				if ((ResDecadeR(i) = '1') or (InitR = '1')) then
					DecadeChainR(i)	<= 0;
				else
					if (IncDecadeR(i) = '1') then
						DecadeChainR(i)	<= DecadeChainR(i) + 1;
					end if;
				end if;
			end loop;

			-- Register stages for divider contr
			if (InitR = '1') then
				IncDecadeR		<= "000000001";
				ResDecadeR		<= (others => '0');
			else
				IncDecadeR		<= IncDecadeC;
				ResDecadeR		<= ResDecadeC;
			end if;

		end if;
	end process;
	TimeBase		<= IncDecadeR;

	IncDecadeC(0)	<= '1';
	ResDecadeC(0)	<= '1' when	(DecadeChainR(0) = 8)							else '0';
	IncDecadeC(1)	<= '1' when (ResDecadeC(0) = '1')							else '0';

	g_inc_dec : for i in 1 to 7 generate
		ResDecadeC(i)	<= '1' when (DecadeChainR(i) = 9) and (IncDecadeC(i) = '1')	else '0';
		IncDecadeC(i+1)	<= '1' when (ResDecadeC(i) = '1')							else '0';
	end generate;

end RTL;

Jetzt kommen wir zur Implementierung der Zeitbasis. Bei der Zeitbasis handelt es sich um einen Vektor von Signalen welche synchron zueinander in definierten Zeitabständen für jeweils einen Takt aktiv sind.

Im Programmgerüst wurden schon die TimeIdx... Konstanten definiert, um auf die einzelnen Signale dieses Vektors zugreifen zu können.

Der Prozess fängt mit einer Schleife an. Eine Schleife an dieser Stelle bedeutet nicht "Mache acht mal hintereinander" sondern "Erzeuge mir acht mal". Was soll erzeugt werden? Zähler, die jeweils ihren Reset als auch ihr Increment haben (Zudem können alle Zähler noch über den globalen Reset InitR zurückgesetzt werden.

Die Laufvariable i muss übrigens nicht extra deklariert werden.

Da wir ja einen Vektor von Zählern haben, haben wir auch einen Vektor von Reset und Init-Signalen, die auch noch im Prozess verwaltet werden. Der Reset Wert für IncDecadeR ist im untersten Bit '1'. Der erste Teiler soll ja die 100 MHz teilen, inkrementiert also andauernd, wenn er nicht am Ende der Dekade zurückgesetzt wird. Die Rest der Logik dafür befindet sich aber außerhalb des Prozesses.

Somit ist die Struktur der Teilerkette definiert. Jetzt folgt die eigentliche Denkarbeit. Diese begrenzt sich darauf wann ein Teiler inkrementiert, wann er zurückgesetzt und wann das Ausgangssignal aktiviert werden muss:

  • Da wir eine Kette von Teilern haben ist erst einmal klar, dass immer wenn ein Teiler auf 0 zurückgesetzt wird, der nächste Teiler inkrementiert werden muss.
  • Ein Teiler muss zurückgesetzt werden, wenn er auf 9 steht und inkrementiert werden soll.
  • Das Ausgangssignal der Zeitbasis ist identisch mit dem Inkrement des zugehörigen Teilers.

Also IncDecadeC(0) <=  '1' gilt immer, da der 100 ns Zähler andauernd arbeitet.
ResDecadeC(0) <= '1' when (DecadeChainR(0) = 8) else '0'; Die 8 weil das R-Signal dann mit der 9 aktiviert wird.
 IncDecadeC(1) <= '1' when (ResDecadeC(0) = '1') else '0';  Der eigene Reset ist der Inkrement vom nächsten

Danach kommt noch mal eine Schleife und zwar generate. Mit generate außerhalb der Prozesse generiert man den betroffenen Code mehrmals, genau so wie wenn man die Codezeilen mit der aufgelösten Laufvariablen mehrmals hintereinander schreiben würde.

Man muss aber auch aufpassen, was man hier macht. IncDecadeC(8)  hängt ja von ResDecadeC(7), das wiederum von Zählerstand und IncDecadeC(7) und so weiter ab. So dass recht viele Signale im FPGA zu vergleichen sind, obwohl der Code ja recht kompakt aussieht. In einer optimierten Version kann man das aber verbessern.

Weiter zu TestSysService oder Schritt 2.