Generator sygnałów VGA w VHDL
W dzisiejszym wpisie zaprezentuję jak wykonać generator sygnałów VGA dla trybu 640×480@60Hz. Jest to element umożliwiający wyświetlanie czegokolwiek na matrycy/kineskopie monitora, także, o ile zajmujemy się projektowaniem GPU, warto poświęcić mu trochę uwagi (:
Każdy monitor ze złączem w standardzie VGA wymaga podawania pięciu sygnałów:
- R,G,B – składowe kolorów (“treść” obrazu)
- v_sync – synchronizacja pionowa
- h_sync – synchronizacja pozioma
Poszczególne sygnały muszą spełniać pewne normy czasowe, które zostały opisane (również dla innych rozdzielczości) na tej stronie. Obraz jest wyświetlany z częstotliwością 25MHz (jest to wartość przybliżona, rzeczywista jest nieco większa, ale nie ma to znaczenia) na pojedynczy piksel, przy czym należy zauważyć, iż całkowity obraz ma wymiary 800×525px, z czego obszar aktywny zajmuje oczywiście 640×480px. Czas spędzony w pozostałym obszarze jest używany do cofania plamki do pozycji początkowej oraz przechodzenia do nowej linii. Jest to “pamiątka” po erze monitorów CRT które wyświetlały obraz z użyciem działa elektronowego, o czym więcej tutaj.
Wyróżniamy cztery podstawowe etapy synchronizacji poziomej:
- 640 px – obszar aktywny (Horizontal Display, HD)
- 16 px – obszar wygaszania plamki i “przygotowania do powrotu” (Horizontal Front Porch, HF)
- 96 px – obszar powrotu plamki (Horizontal Return, HR)
- 48 px – obszar zapalania plamki i “przygotowania do rysowania” (Horizontal Back Porch, HB)
Sumarycznie etapy te trwają 800px. Sygnał synchronizacji poziomej powiniem mieć wartość ‘0′ podczas trwania etapu nr 3 oraz wartość ‘1′ we wszystkich pozostałych.
Analogicznie, dla sychrnonizacji pionowej, mamy:
- 480 li – obszar aktywny (Vertical Display, VD)
- 10 li – (Vertical Front Porch, VF)
- 33 li – (Vertical Back Porch, VB)
- 2 li – powrót plamki (Vertical Return, VR)
Sumarycznie 525 linii. Sygnał synchronizacji pionowej powinien mieć wartość ‘0′ podczas etapu nr 3, w pozostałych wypadkach powinien mieć wartość ‘1′. Posileni takim ładunkiem teorii możnemy przystąpić do wcielania naszego generatora w życie.
Pierwszym krokiem jest dobór sygnałów wejściowych i wyjściowych naszego generatora. Ja rozpisałem je następująco:
port ( – common signals
clk, rst : in std_logic;
– vga signals
pix_x, pix_y : out std_logic_vector(9 downto 0);
hs, vs, blank, pix_clk : out std_logic);
end vga_640_gen;
Sygnały clk, i rst to nic innego jak wejście zegara, który na płycie Nexys 2 ma częstotliwość 50MHz, tak więc po podzieleniu na 2 idealnie nadaje się na zegar pikseli 25MHz oraz sygnał resetu całego generatora. 10-bitowe sygnały pix_x oraz pix_y zawierają informację odnośnie aktualnie wyświetlanego piksela. Linie hs i vs to linie synchronizacji odpowiednio: poziomej i pionowej. Sygnał blank informuje nas o tym czy generator znajduje się w obszarze aktywnym czy też nie, zaś pix_clk to wyjście zegara pikseli 25Mhz.
Przystępujemy do implementacji architektury generatora:
– signals for pixel clock generation
signal pix_clk_reg, pix_clk_next : std_logic;
signal blank_reg, blank_next : std_logic;
– internal signals
signal h_tick, v_tick : std_logic;
signal h_sync_reg, h_sync_next : std_logic;
signal v_sync_reg, v_sync_next : std_logic;
– counters for horizontal and vertical sync
signal v_count_reg, v_count_next : std_logic_vector(9 downto 0);
signal h_count_reg, h_count_next : std_logic_vector(9 downto 0);
– horizontal and vertical syncs generating constants
constant HD : integer := 640;
constant HF : integer := 16;
constant HB : integer := 48;
constant HR : integer := 96;
constant VD : integer := 480;
constant VF : integer := 10;
constant VB : integer := 33;
constant VR : integer := 2;
begin
– update process
update : process (clk, rst)
begin
if (rst = ‘1′) then
h_count_reg <= (others => ‘0′);
v_count_reg <= (others => ‘0′);
pix_clk_reg <= ‘0′;
h_sync_reg <= ‘0′;
v_sync_reg <= ‘0′;
blank_reg <= ‘0′;
elsif rising_edge(clk) then
pix_clk_reg <= pix_clk_next;
h_count_reg <= h_count_next;
v_count_reg <= v_count_next;
h_sync_reg <= h_sync_next;
v_sync_reg <= v_sync_next;
blank_reg <= blank_next;
end if;
end process update;
– clock generation
pix_clk_next <= not pix_clk_reg;
– blank generation
blank_next <= ‘0′ when (h_count_reg < HD) and (v_count_reg < VD) else ‘1′;
– horizontal and vertical tick signal generation
h_tick <= ‘1′ when h_count_reg = (HD+HF+HB+HR-1) else ‘0′;
v_tick <= ‘1′ when v_count_reg = (VD+VF+VB+VR-1) else ‘0′;
– sync signals generation
h_sync_next <= ‘1′ when (h_count_reg >= (HD+HF)) and
(h_count_reg <= (HD+HF+HR-1)) else ‘0′;
v_sync_next <= ‘1′ when (v_count_reg >= (VD+VF)) and
(v_count_reg <= (VD+VF+VR-1)) else ‘0′;
– horizontal counter process
h_counter : process (pix_clk_reg, h_tick, h_count_reg)
begin
if pix_clk_reg = ‘1′ then
if (h_tick = ‘1′) then
h_count_next <= (others => ‘0′);
else
h_count_next <= h_count_reg + 1;
end if;
else
h_count_next <= h_count_reg;
end if;
end process h_counter;
– vertical counter process
v_counter : process (pix_clk_reg, h_tick, v_tick, v_count_reg)
begin
if pix_clk_reg = ‘1′ and h_tick = ‘1′ then
if (v_tick = ‘1′) then
v_count_next <= (others => ‘0′);
else
v_count_next <= v_count_reg + 1;
end if;
else
v_count_next <= v_count_reg;
end if;
end process v_counter;
– pixel clock signal
pix_clk <= pix_clk_reg;
– blank signal
blank <= blank_reg;
– generate horizontal sync signal
hs <= h_sync_reg;
– generate vertical sync signal
vs <= v_sync_reg;
– output pixel counter signals
pix_x <= h_count_reg;
pix_y <= v_count_reg;
end Behavioral;
Proces update odpowiada za przypisywanie nowych wartości do wszystkich rejestrów, jakie zostały użyte w implementacji. Dzięki temu mamy pełną synchronizację wewnątrz bloku generatora VGA i możliwość pracy całego FPGA z wysoką częstotliwością taktowania. Zegar jest uzyskany poprzez negowanie “samego siebie” (linia 56) z częstotliwością zegara, co w rezultacie owocuje przebiegiem o częstotliwości 25Mhz. Proces h_count odpowiada za aktualizację wartości licznika synchronizacji poziomej, dla wartości mniejszych niż 800 następuje zwiększenie wartości licznika o 1, dla wartości 800 następuje wyzerowanie licznika. Podobnie funkcjonuje proces v_count tyle że on jest synchronizowany przejściem do nowej linii. Sygnał blank ma wartość ‘0′ gdy aktualnie wyświetlany piksel należy do obszaru aktywnego (linia 59). Sygnały hs i vs są generowane zgodnie z “przepisem” podanym wcześniej (linie 66 i 68).
Test generatora
Aby przetestować generator należałoby spróbować wyświetlić jakikolwiek obraz na monitorze. Najprostszym sposobem jest wygenerowanie tzw. xor pattern, tzn takiego obrazu, gdzie kolor każdego piksela ma wartość obliczoną jako xor jego współrzędnej x oraz y. Przykładowy plik testowy wygląda więc następująco:
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity test is
port( clk : in std_logic;
vga_hs, vga_vs : out std_logic;
vga_rgb : out std_logic_vector(7 downto 0));
end test;
architecture Behavioral of test is
signal pix_x, pix_y : std_logic_vector(9 downto 0);
signal blank : std_logic;
begin
vga_gen : entity vga_640_gen
port map(clk => clk, rst => ‘0′,
pix_x => pix_x, pix_y => pix_y,
hs => vga_hs, vs => vga_vs, blank => blank, pix_clk => open);
vga_rgb <= pix_x(7 downto 0) xor pix_y(7 downto 0) when blank = ‘0′ else x"00";
end Behavioral;
Warto zauważyć, że sygnały wyjściowe vga_rgb powinny mieć wartość zera logicznego w momencie, gdy plamka znajduje się poza aktywnym obszarem wyświetlania (w tym celu z generatora wyprowadzono sygnał blank). Jeśli tego nie zrobimy, to niektóre monitory zaczynają wariować, obraz jest przesunięty i słychać syczenie i gwizdy (:. Kod wgrany do FPGA, znajdującego się na płytce Nexys 2, daje takie oto efekty:
