Kampis Elektroecke

VGA im FPGA – Ausgabe eines Bildes

Da der VGA-Controller nun einsatzbereit ist, kann mit seiner Hilfe ein Bild ausgegeben werden kann. In diesem Beispiel wird der VGA-Controller genutzt um ein Bild aus einem Display-RAM auf dem VGA-Monitor darzustellen. Da das Zybo nicht genug RAM besitzt um 640×480 Pixel zu speichern, habe ich den Bildschirm in 8×8 große Felder unterteilt, auf denen dann ein Text geschrieben werden soll. Am Ende ist der VGA-Controller somit in der Lage 80×60 Zeichen auf dem Bildschirm darzustellen. 

Einbinden des VGA-Controllers:

Im ersten Schritt soll die vorhandene Schaltung des VGA-Controllers als Modul eingefügt werden. Dazu wird eine neue VHDL-Datei namens VGA_Top erstellt und der Clocking Wizard muss aus dem VGA-Controller in die Top-Datei verschoben werden, da die Erzeugung des Taktes nicht im VGA-Controller stattfinden soll.

entity VGA is
    Port ( Color : out STD_LOGIC_VECTOR(15 downto 0);
           HSync : out STD_LOGIC;
           VSync : out STD_LOGIC;
           Output : out STD_LOGIC_VECTOR(2 downto 0);
           Reset : in STD_LOGIC;
           Clock : in STD_LOGIC 
           );
end VGA;

architecture VGA_Arch of VGA is
 
    signal Lock : std_logic;
    signal Clock_VGA : std_logic;
 
    component System is
        Port (  Clock_In : in STD_LOGIC;
                Clock_Reset : in STD_LOGIC;
                Clock_Locked : out STD_LOGIC;
                Clock_Out : out STD_LOGIC
                );
    end component System;
    
begin
 
    Clock_25MHz : System port map (Clock_In => Clock, 
                                   Clock_Reset => '1', 
                                   Clock_Locked => Lock, 
                                   Clock_Out => Clock_VGA
                                   );
    
end VGA_Arch;

Die Eingangssignale des Top-Moduls unterscheiden sich geringfügig von denen des VGA-Controllers. Im Top-Modul sind die Signale für die x- und y-Koordinaten nicht mehr vorhanden, da diese Signale jetzt direkt in die entsprechende Farbe am Monitor umgewandelt werden. Um diese Signale nach außen zu geben, ist ein Anschluss mit dem Namen Color hinzugekommen, welcher wie folgt aufgebaut ist:

  • Rot – Bit 0 bis Bit 4
  • Blau – Bit 5 bis Bit 9
  • Grün – Bit 10 bis Bit 15

Durch die Zusammenfassung der Farben in ein 16-Bit Signal habe ich mir das Speichern der Farbinformationen erleichtert, indem die Farbe eines jeden Pixels des Bildes nun durch einen 16-Bit Wert beschrieben wird. Im nächsten Schritt wird dann der VGA-Controller instantiiert:

architecture VGA_Arch of VGA is

    signal Lock : std_logic;
    signal Clock_VGA : std_logic;

    component Clock is
        Port (  Clock_In : in STD_LOGIC;
                Clock_Reset : in STD_LOGIC;
                Clock_Locked : out STD_LOGIC;
                Clock_Out : out STD_LOGIC
                );
    end component Clock;
    
    component VGA_Controller is
        Port (  HSync : out STD_LOGIC;
                VSync : out STD_LOGIC;
                Clock_VGA : in STD_LOGIC;
                Reset : in STD_LOGIC;
                x_out : out STD_LOGIC_VECTOR(9 downto 0);                
                y_out : out STD_LOGIC_VECTOR(9 downto 0) 
                );
    end component VGA_Controller;  

begin

     Clock_25MHz : Clock port map (Clock_In => Clock_In, 
                                   Clock_Reset => '1', 
                                   Clock_Locked => Lock, 
                                   Clock_Out => Clock_VGA
                                   );

    VGA_Core     : VGA_Controller port map (HSync => HSync, 
                                            VSync => VSync, 
                                            Clock_VGA => Clock_VGA, 
                                            Reset => Reset,
                                            x_out => Pixel_x_delay_0, 
                                            y_out => Pixel_y_delay_0
                                            );
    
end VGA;

Da in dieser Schaltung Komponenten mit unterschiedlichen Verzögerungen zusammenarbeiten (VGA-Controller, RAM und ROM) ist es unerlässlich, dass die Signale zueinander synchron gehalten werden müssen. Dies ist im Code durch einen separaten Prozess realisiert, den ich hier nicht genauer erklären möchte.

Display-RAM:

Zusätzlich wird ein Display-RAM benötigt, welches in jeder Speicherzelle einen Buchstaben des Bildschirminhaltes speichern soll. Da jedes Zeichen zudem über eine 16-Bit Farbinformation benötigt, muss das Display-RAM eine Kapazität von 4800×24 Bit besitzen. Dabei sind die untersten 8 Bits die Adresse des ASCII-Zeichens und die übrigen 16 Bits sind die Farbinformationen.

entity Display_RAM is
    Port (  Clock       : in STD_LOGIC;
            Reset       : in STD_LOGIC;
            Read_Write  : in STD_LOGIC;
            Address     : in UNSIGNED(12 downto 0);
            Data_Out    : out STD_LOGIC_VECTOR(23 downto 0);
            Data_In     : in STD_LOGIC_VECTOR(23 downto 0)
            );  
end Display_RAM;

architecture Display_RAM_Arch of Display_RAM is

    type Display_RAM_Type is array (0 to (2**Address'length) - 1) of std_logic_vector(Data_In'range);
    signal Read_Address : unsigned(Address'range);
    signal RAM : Display_RAM_Type := (
    x"000000",
    x"000000",
    x"000000",
    ...
    x"F80032", 
    x"F80034", 
    x"F8002E", 
    x"F80030", 
    x"F80033", 
    x"F8002E", 
    x"F80032", 
    x"F80030", 
    x"F80031",
    x"F80035",     
    others=> "00000000"
    );
begin

    process(Clock)
    begin
        if(rising_edge(Clock)) then
            if(Read_Write = '0') then
                RAM(to_integer(Read_Address)) <= Data_In;
             end if;
    
             Read_Address <= Address;
        end if;
    end process;

    Data_Out <= RAM(to_integer(Read_Address));   

end Display_RAM_Arch;

Damit der VGA-Controller was anzeigt, habe ich das RAM bereits mit ein paar Zeichen gefüllt. Der Zeichencode x“FFFF76″ beinhaltet so z. B. ein weißes Zeichen (0xFFFF) mit dem Wert 0x76 (der Buchstabe v). Für zukünftige Erweiterungen habe ich das Display-RAM mit Schreibleitungen ausgestattet. Damit kann eine nachgeschaltete Logik auch Inhalte in das RAM schreiben (wird in dieser Beschreibung aber nicht berücksichtigt).

Font-ROM:

In dem Display-RAM sind lediglich einzelne Zeicheninformationen und keine Pixelmuster gespeichert. Um sich das Leben nicht all zu schwer zu machen, empfiehlt es sich, dass die gespeicherten Zeichen ASCII-codiert sind. Dies hat den netten Nebeneffekt, dass man den VGA-Controller später auch an das Processing-System anschließen und mittels Software ohne größere Umwege direkt den Text in das RAM schreiben kann. Es wird also noch so etwas wie ein Font-ROM benötigt, welches das jeweilige ASCII-Zeichen aus dem Display-RAM in ein 8×8 Pixel großes Symbol umwandelt. Dieses Font-ROM habe ich über ein BRAM-ROM realisiert und dann im Code instantiiert:

Die einzelnen Zeichen habe ich mittels coe-Datei während der Synthese ins ROM geladen, wodurch das ROM direkt beschrieben wird.

Die komplette Schaltung:

Diese einzelnen Komponenten werden nun alle instantiiert.

    
VGA_Ctrl        : VGA_Timing port map (HSync => HSync_delay_0, 
                                       VSync => VSync_delay_0, 
                                       Clock_VGA => Clock_VGA, 
                                       Reset => Reset, 
                                       x_out => Pixel_x_delay_0, 
                                       y_out => Pixel_y_delay_0
                                       );
                                            
Char_Font       : Font_ROM port map (ROM_Address => ROM_Address, 
                                     ROM_Clock => Clock_VGA, 
                                     ROM_DataOut => ROM_Data, 
                                     ROM_Reset => '0'
                                     );
                                           
Display         : Display_RAM port map (Clock => Clock_VGA, 
                                        Reset => Reset, 
                                        Read_Write => '1', 
                                        Address => Display_Address, 
                                        Data_Out => Display_Buffer, 
                                        Data_In => (others => '0')
                                        ); 
                                            
Clock_25MHz : Clock port map (Clock_In => Clock_In, 
                              Clock_Reset => '1', 
                              Clock_Locked => Lock, 
                              Clock_Out => Clock_VGA
                              );

Aus dem VGA-Controller kommen x- und y-Koordinaten des sichtbaren Bereichs. Mit diesen Koordinaten kann die aktuelle Adresse im Display-RAM berechnet werden:

Zeichen_Address <= Pixel_x_delay_0(9 downto 3);   
Display_Address <= Offset + Zeichen_Address; 

process(Clock_VGA)
begin 
   if(rising_edge(Clock_VGA)) then  
       if(Pixel_x_delay_0(2 downto 0) = "000") then
           if((Pixel_x_delay_0 = 0) and (Pixel_y_delay_0 = 0)) then
               Offset <= to_unsigned(0, Offset'length);
           elsif((Pixel_x_delay_0 = 0) and (Pixel_y_delay_0(2 downto 0) = 0)) then 
               Offset <= Offset + 80; 
           end if;        
        end if;  
     end if;
end process;

Da jedes Zeichen 8 Pixel breit ist, kann durch die Betrachtung der oberen 7 Bits des Pixelzählers kann das aktuelle Zeichen bestimmt werden:

Zeichen_Address <= Pixel_x_delay_0(9 downto 3);

Die Adresse im Display-RAM wird dann durch die Addition der Zeichenadresse und eines Offsets, welcher von der aktuellen Zeile abhängig ist, bestimmt werden:

Display_Address <= Offset + Zeichen_Address;

Der Offset wird bei jedem Zeilenwechsel um eine Zeile, also 80 Zeichen, erhöht:

process(Clock_VGA)
begin 
   if(rising_edge(Clock_VGA)) then  
       if(Pixel_x_delay_0(2 downto 0) = "000") then
           if((Pixel_x_delay_0 = 0) and (Pixel_y_delay_0 = 0)) then
               Offset <= to_unsigned(0, Offset'length);
           elsif((Pixel_x_delay_0 = 0) and (Pixel_y_delay_0(2 downto 0) = 0)) then 
               Offset <= Offset + 80; 
           end if;        
        end if;  
     end if;
end process;

Einen Taktzyklus später gibt das Display-RAM dann das Zeichen, welches unter Display_Address gespeichert war aus. Das Zeichen im Display-RAM enthält als Informationen die Farben und den ASCII-Code. Beides wird jetzt voneinander getrennt:

Character   <= Display_Buffer((Char_Size - 1) downto 0);
Color       <= Display_Buffer((Color_Width + Char_Size - 1) downto Char_Size);

Nun liegen die Farbinformationen und das aktuelle Zeichen vor. Im Font-ROM ist jedes Zeichen an der Stelle seines jeweiligen ASCII-Codes gespeichert. Das Zeichen v mit dem ASCII-Code 0x76 ist also an der Adresse 0x76 gespeichert. Das Zeichen ist an der Speicherstelle als 8×8 Matrix gespeichert. Die untersten 8 Bits der x-Koordinate maskieren das Bit der jeweiligen Zeile des Zeichens:

Col_Addr    <= std_logic_vector(Pixel_x_delay_2(2 downto 0));
ROM_Bit     <= ROM_Data(to_integer(unsigned(Col_Addr)));

Dabei ist die aktuelle Zeile (also der Wert von Adresse 0x76 für die erste Zeile, der Wert der Adresse 0x77 für die zweite, usw.) des Zeichens in ROM_Data gespeichert. Die ROM-Adresse für die Zeile des Zeichens setzt sich aus der Startadresse des Zeichens, also der ASCII-Wert – hier 0x76, und einem Zeilenoffset zusammen:

Row_Addr    <= std_logic_vector(Pixel_y_delay_2(2 downto 0));
ROM_Address <= Character & Row_Addr;

Damit ist die Schaltung komplett und kann synthetisiert und implementiert werden. Das Resultat sieht dann folgendermaßen aus:

Das komplette Projekt steht in meinem GitHub-Repository zum Download bereit.

4 Kommentare

  1. Guten Tag Herr Kampert,

    ich finde deine Schritt für Schritt Beschreibung des VGA-Controllers total klasse. Gerne würde ich mir mal das Gesamtprojekt auf GitLab anschauen, jedoch ist dies nicht mehr vorhanden. Wäre es möglich, dass Sie mir das anderweitig zukommen zu lassen?
    Vielen Dank schonmal im Voraus und einen schönen Tag Ihnen noch.

    Felix Rauterberg

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert