Da ises weg, das Weihnachtsgeld.
Draufgegangen ist das unter anderem für dieses hübsche, aber leider teure FPGA-Board mit einem Xilinx Spartan 3 FPGA:

Wie man sieht, sieht man ein A auf dem LCD Display. Viel mehr habe ich damit noch nicht zustande gebracht. Besonders einfach wirds einem aber sowieso nicht gemacht. Das Ding ist halt kein Prozessor, mit dem man sequentielle Abläufe durch das Ausführen eines Programms abbilden kann, sondern pure Digitaltechnik.
Durch VHDL wird einem das Beschreiben etwas einfacher gemacht, die Vorgänge laufen aber alle parallel in diesem Chip ab. Signale warten nicht einfach so auf ein Ereignis.
Signalabläufe wie sie von einem Displaycontroller erwartet werden können auch nur über eine Statemachine modelliert werden. Man hüpft also von einem Zustand in den anderen und legt die passenden Signale an die Leitungen. Danach springt man meistens wieder in eine Warteschleife, weil der LCD-Controller üblicherweise langsamer ist als so ein FPGA.
Da man das ganze äußerst schlecht debuggen kann, simuliert man im Vorfeld die Abläufe. Man schreibt also erstmal sein Modul in VHDL und darf sich dann auch noch darum kümmern einen Sogenannten Testbench zu schreiben. Dieser Testbench ist dann dafür zuständig die passenden Signale, die normalerweise durch externe Quellen erzeugt werden, an das Modul zu legen, um das alles in Software abbilden zu können.
Nachdem man dann die Simulation für einige Milisekunden ausgeführt hat, sieht das so aus:

Auf diesem Bild sieht man die Simulation eines RS232 Moduls. Die Länge eines Bits ist hier 8,6µs, was eine Baudrate von 115200 pro Sekunde ergibt.
Als erstes wird in tx_data das Byte abgelegt, welches üertragen werden soll. Dann zieht man tx_start kurz auf High, was man hier nicht sieht, da es bereits 40ns nach dem Start passiert, und die Übertragung startet. Erst kommt das Startbit auf der TXD-Leitung, dann eine 0, dann eine 1 usw. bis das Stopbit kommt und die Leitung wieder auf “Standby” geht und somit 0xAA übertragen wurde. RS232 überträgt das niederwertigste Bit zuerst.
Analog läuft das mit dem Empfangen ab.
In VHDL sieht das ganze dann so aus:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity RS232 is
Generic ( Clockrate : integer := 50000000; -- Hertz
Baudrate : integer := 9600 -- Bits/Sec
);
Port ( RXD : in STD_LOGIC;
RX_Data : out STD_LOGIC_VECTOR (7 downto 0);
RX_Busy : out STD_LOGIC;
TXD : out STD_LOGIC;
TX_Data : in STD_LOGIC_VECTOR (7 downto 0);
TX_Start : in STD_LOGIC;
TX_Busy : out STD_LOGIC;
CLK : in STD_LOGIC
);
end RS232;
architecture Behavioral of RS232 is
signal txstart : std_logic := '0';
signal txsr : std_logic_vector (9 downto 0) := "1111111111";
signal txbitcnt : integer range 0 to 10 := 10;
signal txcnt : integer range 0 to (Clockrate/Baudrate)-1;
signal rxd_sr : std_logic_vector (3 downto 0) := "1111";
signal rxsr : std_logic_vector (7 downto 0) := "00000000";
signal rxbitcnt : integer range 0 to 9 := 9;
signal rxcnt : integer range 0 to (Clockrate/Baudrate)-1;
begin
send: process(CLK)
begin
if rising_edge(CLK) then
txstart <= TX_Start;
if (TX_Start = '1' and txstart = '0') then
txcnt <= 0;
txbitcnt <= 0;
txsr <= '1' & TX_Data & '0';
else
if(txcnt<(Clockrate/Baudrate)-1) then
txcnt <= txcnt + 1;
else
if (txbitcnt < 10) then
txcnt <= 0;
txbitcnt <= txbitcnt + 1;
txsr <= '1' & txsr(txsr'left downto 1);
end if;
end if;
end if;
end if;
end process;
TXD <= txsr(0);
TX_Busy <= '1' when (txbitcnt < 10) else '0';
receive: process(CLK)
begin
if rising_edge(CLK) then
rxd_sr <= rxd_sr(rxd_sr'left - 1 downto 0) & RXD;
if (rxbitcnt < 9) then
if(rxcnt < (Clockrate / Baudrate) - 1) then
rxcnt <= rxcnt + 1;
else
rxcnt <= 0;
rxbitcnt <= rxbitcnt + 1;
rxsr <= rxd_sr(rxd_sr'left - 1) & rxsr(rxsr'left downto 1);
end if;
else
if (rxd_sr(3 downto 2) = "10") then
rxcnt <= ((Clockrate / Baudrate) - 1) / 2;
rxbitcnt <= 0;
end if;
end if;
end if;
end process;
RX_Data <= rxsr;
RX_Busy <= '1' when (rxbitcnt < 9) else '0';
end Behavioral;
Mit einer “entity” beschreibt man, dass es irgendein Modul mit entsprechendem Namen gibt und welche Leitungen da rein und rausgehen.
Im “Behaviour” beschreibt man dann, wie sich das Modul verhält und welche internen Signale oder Variablen es gibt. Wir haben in diesem Fall 2 Prozesse. Einen zum Senden und einen zum Emmpfangen. Jeder dieser Prozesse kann gleichzeitig ausgeführt werden.
Der passende Testbench zu diesem RS232 Modul sieht dann so aus:
LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
USE ieee.std_logic_unsigned.all;
USE ieee.numeric_std.ALL;
ENTITY rs232_tb IS
END rs232_tb;
ARCHITECTURE behavior OF rs232_tb IS
-- Component Declaration for the Unit Under Test (UUT)
COMPONENT RS232
GENERIC ( Clockrate : integer := 50000000;
Baudrate : integer := 115200);
PORT(
RXD : IN std_logic;
RX_Data : OUT std_logic_vector(7 downto 0);
RX_Busy : OUT std_logic;
TXD : OUT std_logic;
TX_Data : IN std_logic_vector(7 downto 0);
TX_Start : IN std_logic;
TX_Busy : OUT std_logic;
CLK : IN std_logic
);
END COMPONENT;
--Inputs
signal RXD : std_logic := '0';
signal TX_Data : std_logic_vector(7 downto 0) := (others => '0');
signal TX_Start : std_logic := '0';
signal CLK : std_logic := '0';
--Outputs
signal RX_Data : std_logic_vector(7 downto 0);
signal RX_Busy : std_logic;
signal TXD : std_logic;
signal TX_Busy : std_logic;
constant clk_period : time := 20 ns;
BEGIN
-- Instantiate the Unit Under Test (UUT)
uut: RS232 PORT MAP (
RXD => RXD,
RX_Data => RX_Data,
RX_Busy => RX_Busy,
TXD => TXD,
TX_Data => TX_Data,
TX_Start => TX_Start,
TX_Busy => TX_Busy,
CLK => CLK
);
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
RXD <= '1'; --idle
wait for clk_period*2;
TX_Data <= "10101010";
TX_Start <= '1';
wait for clk_period*5;
TX_Start <= '0';
wait for clk_period*5000;
--startbit
RXD <= '0';
--435 clock cycles by 20ns each = 8,7us = length of bit with 115200 baud
wait for clk_period*435;
RXD <= '0';
wait for clk_period*435;
RXD <= '1';
wait for clk_period*435;
RXD <= '0';
wait for clk_period*435;
RXD <= '1';
wait for clk_period*435;
RXD <= '0';
wait for clk_period*435;
RXD <= '1';
wait for clk_period*435;
RXD <= '0';
wait for clk_period*435;
RXD <= '1';
wait for clk_period*435;
--stopbit and idle
RXD <= '1';
wait;
end process;
end;
Der Aufbau ist ähnlich wie vorhin. Die Entity hier enthält jedoch keine Ein- oder Ausgabesignale, da hier später nur die internen Signale dieses Testbenches im Simulator betrachtet werden. Natürlich kann man später in der Waveform noch weitere Signale hinzufügen. Zum Beispiel interne Signale aus dem getesteten Modul.
Im Behaviour wird dann als erses die Komponente RS232 definiert. Es gibt also irgendwo ein solches Modul, das später hier genutzt werden kann. Darauf folgend kommen wieder einige Signale, denen man später in Prozessen Werte zuweisen kann oder von denen man Werte auf den Leitungen abgreifen kann.
Danach wird ein RS232 Modul mit dem Namen uut (“unit under test”) instanziert und die Ports des Moduls werden mit den internen Signalen verknüpft.
Dann folgen wieder Prozesse. Der erste generiert das Clocksignal und der zweite ist für den eigentlichen Ablauf des Test verantwortlich. Das Senden ist hier relativ einfach: Wert in tx_data schreiben, für kurze Zeit an tx_start ziehen und dann beginnt die Übertragung.
Der Empfangstest ist dahingehend schwieriger, dass jedes Bit für eine bestimmte Zeit anliegen muss, wie es in Wirklichkeit auf einem Kabel auch der Fall wäre.
clk_period*435 entspricht hier exakt 8,7µs, was wiederrum ungefähr 115200 Baud/s entspricht: 20ns*435 = 8700ns = 8,7µs
Als nächstes werde ich mich mal an den VGA Ausgang setzen und probieren ein Bild zu zaubern. Danach ist allerdings auch schon der DDR2-Speichercontroller dran: Für ein 12 Bit VGA-Bild bei einer Auflösung von 640×480 Pixeln sind schon 450KB Video RAM nötig. Mit double buffering sind wir dann bei 900KB.
Update: Das RS232 Modul stammt im übrigen von hier. Lediglich den Testbench habe ich mir dann noch dazugeschrieben.
