Kampis Elektroecke

VGA im FPGA – Der VGA-Controller

In diesem Artikel stelle ich eine Möglichkeit vor, wie ein alter VGA Monitor an das FPGA angebunden und angesteuert wird. Da diese Aufgabe etwas länger wird unterteile ich den Artikel in zwei Sektionen:

  1. Die Erstellung eines eigenen VGA-Controllers zur Generierung aller Signale (dieser Teil)
  2. Der VGA-Controller wird zur Ausgabe eines Bildes genutzt (der nächste Teil)

In diesem Artikel zeige ich, wie ein VGA-Signal aufgebaut ist und wie ein einfacher VGA-Controller für ein FPGA aufgebaut ist.

Ein paar Grundlagen zum VGA:

Bevor ich damit beginne einen VGA-Controller zu entwerfen, müssen ein paar Grundlagen zum VGA bekannt sein. Beim VGA handelt es sich um einen analog/digitalen Übertragungsstandard für Bildsignale. Das Bild wird Zeilenweise übertragen und dabei hat jedes Pixel eine fest definierte Dauer.

Die Farbinformationen eines Pixels liegen als analoge Spannungen an und werden bei jedem Pixel neu eingelesen. Sobald eine Zeile zu Ende ist, wird ein Horizontal-Synchronisationssignal ausgelöst, wodurch der Monitor die Zeile wechselt und wieder an den Anfang einer Zeile springt. Wenn alle Zeilen durchlaufen sind, löst der VGA-Controller ein Horizontal-Synchronisationssignal aus, wodurch der Monitor wieder an die Anfangsposition springt. Neben dem sichtbaren Teil besitzt jedes Bild noch eine rechte und eine linke, sowie eine obere und untere Schwarzschuler. Diese Schwarzschultern dienen dazu die Zeit zu überbrücken bis der Elektronenstrahl an den Anfang der nächsten Zeile, bzw. an den Bildanfang gesprungen ist. Zusammengefasst sieht ein komplettes Bild also folgendermaßen aus:

Der VGA-Standard kommt aus der Zeit der Röhrenmonitore und da benötigten die Elektronenstrahlen eine gewisse Zeit um an eine Position zu springen. Hätte man die Schwarzschultern nicht eingeführt, würde bei jedem Zeilenwechsel ein Teil des Bildes fehlen und es wäre zudem noch verschoben, da sich der Elektronenstrahl noch nicht an der Startposition befunden hätte. Die Belegung der VGA-Stecker hat folgenden, standardisierten Aufbau:

Für die Umsetzung werden folgende Pins benötigt:

  • 1 – 3 für die Farben
  • 5 – 8, 10 als Masse
  • 13 Horizontales Synchronisationssignal (Low Aktiv)
  • 14 Vertikales Synchronisationssignal (Low Aktiv)

Mit diesem Wissen kann man nun mit dem Entwurf des VGA-Controllers beginnen.

Entwurf des VGA-Controllers:

Da die Farben des Bildes durch analoge Spannungen generiert werden, ein FPGA aber in der Regel nur über digitale Ausgänge verfügt, muss man an den Ausgängen Spannungsteiler platzieren um die Spannungen zu erzeugen. Dies ist beim ZYBO bereits implementiert:

Für den Anfang soll der Controller für ein 640×480 Pixel Display ausgelegt sein. Daraus ergeben sich nun folgende Daten, die für das Projekt wichtig sind:

  • Pixeltakt 25,175 MHz

Für eine einzelne Zeile:

  • Sichtbarer Bereich 25,422 µs oder 640 Pixel
  • Linke Schwarzschulter (Back porch) 1,9 µs oder 48 Pixel
  • Rechte Schwarzschulter (Front porch) 0,635 µs oder 16 Pixel
  • Sync Puls 3,81 µs oder 96 Pixel
  • Gesamte Zeile 31,777 µs oder 800 Pixel

Für ein komplettes Bild

  • Sichtbarer Bereich 15,253 ms oder 480 Reihen
  • Linke Schwarzschulter (Back porch) 1,04 ms oder 33 Reihen
  • Rechte Schwarzschulter (Front porch) 0,3177 ms oder 10 Reihen
  • Sync Puls 0,063 ms oder 2 Reihen
  • Gesamtes Bild 16,683 ms oder 480 Reihen

Der VGA-Controller muss also alle 31,77 µs den Horizontalsync für 3,81 µs und alle 16,68 ms den Vertikalsync für 0,063 ms auf Low ziehen, eben immer genau dann, wenn eine Zeile bzw. ein komplettes Bild abgeschlossen wurde.

Da das ZYBO einen 125 MHz Takt besitzt, ich aber 25,175 MHz brauche, kann kein einfacher Taktteiler verwendet werden (theoretisch würde es gehen, aber dann müssten alle Pixelwerte neu berechnet werden). Daher verwende ich ein Clocking Wizard Modul um den Takt zu erzeugen. Dieses Modul ist in Vivado als IP-Core hinterlegt. Es wird also erst ein Blockschaltbild erzeugt und dann dort der IP-Core eingefügt:

Jetzt wird der IP-Core noch konfiguriert:

Praktischerweise können die IP-Cores aus dem Blockschaltbild im eigenen VHDL-Code instantiiert werden. Vivado stellt dafür sogar ein Beispiel bereit:

Mit diesem Beispiel kann der Clocking Wizard direkt im eigenen Code eingefügt werden:

entity VGA_Timing 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 VGA_Timing ;

architecture VGA_Timing_Arch of VGA_Timing is

    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;

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

end VGA_Timing_Arch;

Im Moment befindet sich der Clocking Wizard noch direkt im Code für den VGA-Controller. Später wird er aber daraus entfernt, da dem VGA-Controller dann der richtige Takt einfach nur zur Verfügung gestellt wird und die Takterzeugung nichts mit dem Controller an sich zu tun hat. Zusätzlich habe ich dem VGA-Controller noch einige Anschlüsse verpasst:

  • HSync – Der Ausgang für den Horizontal Sync
  • VSync – Der Ausgang für den Vertikal Sync
  • Clock_VGA – Der Eingang für den Takt (später der 25, 175 MHz Takt)
  • Reset – Zum Deaktivieren des Controllers
  • x_out – Die aktuelle x-Koordinate des Bildes vom Controller
  • y_out – Die aktuelle y-Koordinate des Bildes vom Controller

Der eigentliche VGA-Controller besteht aus drei Prozessen, die bei jedem Taktimpuls bearbeitet werden. Im ersten Prozess kümmert sich die Schaltung um die Erzeugung des H-Sync und des V-Sync.

process(Clock_VGA, Reset, Pixel_Counter) 
begin
    if(rising_edge(Clock_VGA)) then
        if(Reset = '1') then
            HSync <= '1';
            VSync <= '1';
            Pixel_Counter <= 0;
            Line_Counter <= 0;
        else
            Pixel_Counter <= Pixel_Counter + 1;

            if(Pixel_Counter = 799) then
                Pixel_Counter <= 0;
                Line_Counter <= Line_Counter + 1;
            end if;   

            if(Line_Counter = 525) then
                Line_Counter <= 0; 
             end if;        

            if(Pixel_Counter = 703) then
                HSync <= '0';
            end if; 

            if(Pixel_Counter = 791) then 
                HSync <= '1'; 
            end if;

            if(Line_Counter = 523) then
                VSync <= '0';
            end if;

            if(Line_Counter = 525) then 
                VSync <= '1'; 
            end if;                
        end if;
    end if;
end process;

Sobald der Pixelcounter den 703 erreicht hat wird der Pin für den H-Sync auf Low gezogen und bleibt so lange Low bis der Counter den Wert 799 erreicht hat. Das selbe Verfahren verwende ich für den V-Sync. Es müssen nur die Werte angepasst werden.

Der zweite Prozess zählt die sichtbaren Pixel einer Zeile der Reihe nach durch, sobald sic der Elektronenstrahl im sichtbaren Bereich befindet:

process(Clock_VGA)
begin
    if(rising_edge(Clock_VGA)) then
        if((Pixel_Counter > 47) and (Pixel_Counter < 688)) then    
            x <= x + 1;  
        elsif(Pixel_Counter = 47)) then
            x <= 0;
        end if;
    end if;        
end process;

Sobald der „Elektronenstrahl“ den nicht sichtbaren Bereich verlassen hat werden die Pixel gezählt, aber nur solange wie der Wert kleiner als 640 ist. Analog geschieht das für die y-Koordinate, nur das dort mit dem Zeilenzähler gearbeitet wird und als zusätzliche Bedingung nur gezählt wird, wenn eine Zeile komplett abgelaufen wurde:

process(Clock_VGA)
begin
    if rising_edge(Clock_VGA) then
        if(Pixel_Counter = 799) then
            if((Line_Counter > 32) and (Line_Counter < 513)  then    
                y <= y + 1; 
            elsif(Line_Counter = 32) then
                y <= 0;
            end if;
        end if;
    end if;        
end process;

Für die y-Koordinate übernehme ich das Prinzip, nur das ich nebenbei noch warten muss bis der Pixelcounter den Wert 799 erreicht hat (erst dann ist eine Zeile zu Ende).

Damit wären alle notwendigen Signale generiert. Die x- und y-Koordinaten werden nun noch auf den Ausgang des Controllers gemappt:

x_out <= to_unsigned(x, x_out'length);
y_out <= to_unsigned(y, y_out'length);

Damit ist der VGA-Controller einsatzbereit. Im nächsten Schritt zeige ich, wie ein Bild aus dem Speicher auf dem Monitor dargestellt werden kann.

3 Kommentare

  1. Hallo,

    ich hätte eine kleine Anmerkung bzw. Frage zu der VHDL-Beschreibung zur Erzeugung von H-Sync und V-Sync.

    if(Pixel_Counter = 800) then
    HSync <= '1';
    end if;

    Dieser Bereich kann meiner Meinung nach gar nicht erreicht werden, da vorher der Pixelcounter schon wieder bei 799 zurück gesetzt wird:

    if(Pixel_Counter = 799) then
    Pixel_Counter <= 0;
    Line_Counter <= Line_Counter + 1;
    end if;

    Oder habe ich hier einen Denkfehler.
    Danke und Gruß

    1. Hallo paul,

      du hast Recht. In dem Artikel stehen noch alte Codeschnipsel drin. Ich habe in letzter Zeit etwas an dem Core weiter gearbeitet und bissl debugt. Allerdings habe ich vergessen den Artikel zu aktualisieren.
      Die Zeile muss lauten:

      if(Pixel_Counter = 799) then
      HSync <= '1'; end if; Ich korrigiere das heute Abend mal. Mit was machst du den VGA? Auch mit einem Zybo? Weil dann würde ich dich quasi als "Testperson" missbrauchen wollen um meinen IP-Core zu testen :) Gruß Daniel

  2. Ich mache gerade die ersten Versuche mit dem Zybo und bin auf der Suche nach Einstieg und Projekten auf deine Seite aufmerksam geworden. Ein VGA-Projekt kommt eventuell in Frage.

Schreibe einen Kommentar

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