01signal.com

Live-Ansicht und Videoerfassung mit einem OV7670-Kamerasensor und Smart Zynq

Diese Webseite gehört zu einer Gruppe kleiner Projekte, die die Funktionen des Smart-Zynq-Boards untersuchen.

Dieses Projekt ist auch bei HelloFPGA veröffentlicht, das chinesischen Lesern empfohlen wird.

Einführung

In diesem Tutorial wird erklärt, wie man ein OV7670-Kameramodul mit dem Smart Zynq verbindet und das Live-Videosignal über den eigenen HDMI-Ausgang des Boards betrachtet. Außerdem zeige ich, wie der rohe Videostream mithilfe eines einfachen Linux-Befehls bequem in eine Datei gespeichert werden kann.

Die Informationen in diesem Tutorial sind auch für andere Arten der Datenerfassung (data acquisition) relevant: Die unten gezeigten Techniken können sowohl mit anderen Bilddatenquellen als auch mit anderen Quellen digitaler Daten verwendet werden.

Das Kameramodul ist mit dem PL-Teil (FPGA) des Zynq-Chips verbunden. Dadurch lässt sich problemlos Logik hinzufügen, die Bildverarbeitung durchführt, bevor die Bilddaten an den ARM-Prozessor gesendet werden. Da dieses Tutorial auf Xillinux basiert, besteht der Prozessorteil des Systems aus einer vollständigen Linux-Distribution.

Das OV7670-Modul wurde für diese Demonstration gewählt, weil diese Hardware preisgünstig, verbreitet und leicht erhältlich ist. Außerdem sind die digitalen Signale, die dieser Baustein erzeugt, einfach zu verstehen.

Allerdings hat der OV7670 einen unangenehmen Fehler: Standardmäßig sind die Farben des Videostreams nicht korrekt. Das ist ein bekanntes Problem dieses Kamerasensors. Dieser Fehler lässt sich beheben, indem man eine kleine Anzahl von Registern der Kamera ändert. Dieses Tutorial ist deshalb in zwei Teile gegliedert:

Beachten Sie, dass ein großer Teil dieses Tutorials die Funktionsweise der Implementierung erläutert. Um die Kamera zu benutzen, muss man diese Erläuterungen nicht verstehen.

Das OV7670-Modul

Dieses Tutorial basiert auf dem unten abgebildeten Kameramodul:

The OV7670 camera sensor module used in this project

Bei diesem Modul sind die meisten Pins der Stiftleiste direkt mit dem OV7670-Baustein verbunden. Nur 3,3 V und GND hängen an Spannungsreglern. Demzufolge sind alle Verbindungen zwischen dem FPGA und dem Modul direkte Verbindungen zwischen dem FPGA und dem OV7670-Baustein.

Es gibt weitere Module mit gleicher Funktion auf dem Markt. Es ist wahrscheinlich in Ordnung, auch diese anderen Module zu verwenden. Beispielsweise gibt es ein anderes Modul, auf dessen Platine „2017/3/15“ aufgedruckt ist. Auch dieses Modul funktioniert einwandfrei. Andererseits gibt es ein Modul, das nicht richtig funktioniert. Das nicht funktionierende Modul trägt auf der Platine die Aufschrift „QYF-OV7670 V3.0“.

Eine weitere Sache, die man beachten sollte: Vom Baustein OV7670 gibt es verschiedene Revisionen. Es ist möglich zu überprüfen, ob auf dem Modul die richtige Revision verwendet wird. Wie das geht, wird im zweiten Teil dieses Tutorials erklärt.

Es gibt zwei wichtige Informationsquellen zum OV7670. Diese beiden Dokumente sind im Internet zu finden:

Vorbereitung des Vivado-Projekts

Erstellen Sie ein neues Vivado-Projekt aus der Zip-Datei des Demopakets (demo bundle), des Boot-Partition-Kits. Öffnen Sie verilog/src/xillydemo.v in einem Texteditor. Löschen Sie den Teil des Codes, der mit „PART 2“ gekennzeichnet ist. Fügen Sie anstelle dieses Teils den folgenden Codeschnipsel ein:

   /*
    * PART 2
    * ======
    *
    * This code demonstrates a frame grabber (data acquisition) from
    * an OV7670 camera module.
    *
    */

   reg [1:0]  clkdiv;

   always @(posedge bus_clk)
     clkdiv <= clkdiv + 1;

   assign J6[10] = clkdiv[1]; // MCLK / XCLK

   assign J6[0] = 0; // PWDN, the camera is always on
   assign J6[1] = !user_w_write_32_open; // RESET#, active low

   wire [7:0] D_in;
   wire       pclk_in, hsync_in, vsync_in;

   assign D_in = J6[9:2];
   assign pclk_in = J6[11];
   assign hsync_in = J6[12];
   assign vsync_in = J6[13];

   (* IOB = "TRUE" *) reg [7:0] D_guard;
   (* IOB = "TRUE" *) reg       pclk_guard, hsync_guard, vsync_guard;

   reg [7:0]  D;
   reg 	      pclk, hsync, vsync;

   always @(posedge bus_clk)
     begin
	// Metastability guards on asynchronous inputs
	D_guard <= D_in;
	pclk_guard <= pclk_in;
	hsync_guard <= hsync_in;
	vsync_guard <= vsync_in;

	D <= D_guard;
	pclk <= pclk_guard;
	hsync <= hsync_guard;
	vsync <= vsync_guard;
     end

   wire       sample_valid;
   reg 	      previous_pclk;

   always @(posedge bus_clk)
     previous_pclk <= pclk;

   assign sample_valid = pclk && !previous_pclk;

   // wait_for_frame's purpose is to start getting data from the camera
   // at the beginning of a frame.
   reg 	      wait_for_frame;

   always @(posedge bus_clk)
     if (!user_r_read_32_open)
       wait_for_frame <= 1;
     else if (sample_valid && vsync)
       wait_for_frame <= 0;

   // fifo_has_been_full changes to '1' when the FIFO becomes full, so
   // that the data acquisition stops and an EOF is sent to the host.
   // This ensures that the data that arrives to the host is contiguous.

   reg 	      fifo_has_been_nonfull, fifo_has_been_full;
   wire       fifo_full;

   always @(posedge bus_clk)
     begin
	if (!fifo_full)
	  fifo_has_been_nonfull <= 1;
	else if (!user_r_read_32_open)
	  fifo_has_been_nonfull <= 0;

	if (fifo_full && fifo_has_been_nonfull)
	  fifo_has_been_full <= 1;
	else if (!user_r_read_32_open)
	  fifo_has_been_full <= 0;
     end

   assign user_r_read_32_eof = fifo_has_been_full && user_r_read_32_empty;

   // This part writes pixels from the camera to the FIFO

   reg 	      fifo_wr_en;
   reg [1:0]  byte_position;
   reg [31:0] dataword;

   always @(posedge bus_clk)
     if (wait_for_frame)
       begin
	  byte_position <= 0;
	  fifo_wr_en <= 0;
       end
     else if (sample_valid && hsync)
       begin
	  case (byte_position)
	    0: dataword[7:0] <= D;
	    1: dataword[15:8] <= D;
	    2: dataword[23:16] <= D;
	    3: dataword[31:24] <= D;
	  endcase

	  if (byte_position == 3)
	    fifo_wr_en <= !fifo_has_been_full;
	  else
	    fifo_wr_en <= 0;

	  byte_position <= byte_position + 1;
       end
     else
       fifo_wr_en <= 0;

   fifo_32x512 fifo_32
     (
      .clk(bus_clk),
      .srst(!user_r_read_32_open),

      .din(dataword),
      .wr_en(fifo_wr_en),
      .full(fifo_full),

      .rd_en(user_r_read_32_rden),
      .dout(user_r_read_32_data),
      .empty(user_r_read_32_empty)
      );

Alternativ können Sie xillydemo.v nach dieser Änderung von hier herunterladen.

Erzeugen Sie nach dieser Änderung wie üblich eine Bitstromdatei (bitstream). Wie dieser Verilog-Code funktioniert, wird weiter unten auf dieser Seite ausführlich erklärt.

Anschluss des Kameramoduls

Zum Verbinden des Kameramoduls mit dem Smart-Zynq-Board können kurze Dupont-Verbindungskabel verwendet werden. Die Länge der Kabel sollte 10 cm oder weniger betragen. Optimal sind 5 cm. Bei längeren Kabeln kann die Qualität der digitalen Signale durch Übersprechen (Crosstalk) beeinträchtigt werden. Übermäßiges Rauschen auf der horizontalen Synchronisation führt zu einem springenden Videobild mit grünen und violetten Streifen.

Bei Kabellängen von 10 cm kann es erforderlich sein, ein Register zu ändern, um den I/O-Treiberstrom des OV7670 zu verringern. Der zweite Teil dieses Tutorials zeigt, wie diese Änderung vorgenommen wird.

Hier ist ein Bild des OV7670-Moduls, angeschlossen an ein Smart-Zynq-SP-Board:

OV7670 module connected to the Smart Zynq board

Unten ist ein Bild aus der entgegengesetzten Richtung zu sehen. Das kleinere Bild oben links hebt hervor, dass der letzte Pin der Stiftleiste mit nichts verbunden ist.

OV7670 module connected to the Smart Zynq board

Die obigen Bilder zeigen, wie die Kabel anzuschließen sind: Suchen Sie zuerst auf der Rückseite des Smart-Zynq-Boards nach der Aufschrift „Bank 33 VCCIO Vadj“. Die Pinreihe in der Nähe dieser Markierung ist die Stiftleiste, mit der wir arbeiten werden. Es handelt sich um die Stiftleiste in der Nähe des HDMI-Steckers.

Zwischen Kamerasensor und Smart-Zynq-Board verlaufen 16 Kabel parallel. Nur 3,3 V und GND werden an einer anderen Stelle der Stiftleiste angeschlossen. Diese beiden Kabel müssen nicht kurz sein.

Beachten Sie, dass der letzte Pin der Stiftleiste 5 V führt. Schließen Sie also kein Kabel an diesen Pin an.

Im Folgenden ist die Verdrahtung zwischen dem Kameramodul und der Stiftleiste des Smart Zynq angegeben. Diese Angaben lassen sich auch aus dem obigen Bild ableiten:

Stiftleiste 1 3 5 7 9 11 13 15 35
Modul-Pin PWDN D0 D2 D4 D6 MCLK HS SDA GND
Modul-Pin RST D1 D3 D5 D7 PCLK VS SCL 3.3V
Stiftleiste 2 4 6 8 10 12 14 16 37

Noch einmal: Achten Sie besonders auf den Anschluss von 3,3 V und GND. Ein falscher Anschluss dieser beiden Kabel kann das Kameramodul zerstören.

Einzelbilderfassung

Starten Sie Xillinux mit einer Bitstromdatei, die auf dem aktualisierten xillydemo.v basiert (wie oben gezeigt).

Der folgende Befehl (an der Shell-Eingabeaufforderung) erzeugt einen kurzen Videoclip aus der Ausgabe der Kamera:

# cat /dev/xillybus_read_32 > clip.raw

Dieser Befehl läuft einige Sekunden lang und stoppt dann. Der Grund dafür ist, dass die Datenrate des Videostreams höher ist als die Schreibgeschwindigkeit der SD-Karte. Das führt zu einem Überlauf (overflow), der den Datenfluss stoppt. Der Mechanismus hinter diesem Verhalten wird weiter unten erklärt.

Mit dem folgenden Befehl lässt sich dieser Videoclip abspielen:

# mplayer -demuxer rawvideo -rawvideo w=640:h=480:format=uyvy:size=614400:fps=31.25 clip.raw

Wenn dieser Befehl in einem Terminalfenster innerhalb der grafischen Oberfläche von Xillinux ausgeführt wird, wird das Video in der grafischen Oberfläche von Xillinux abgespielt.

Es ist auch möglich, das Video auf dem Bildschirm eines anderen Rechners abzuspielen. Die dafür verwendete Technik wird auf einer separaten Seite beschrieben. Wenn die IP-Adresse des anderen Rechners beispielsweise 192.168.1.11 lautet, ändern Sie den Befehl so, dass er wie folgt beginnt:

# DISPLAY=192.168.1.11:0 mplayer -demuxer rawvideo ...

Wie bereits erwähnt, sind die Farben des Videoclips nicht korrekt. Die nächste Seite erklärt, wie sich dieses Problem beheben lässt.

Der folgende Befehl liest einen einzelnen Video-Frame in eine Datei namens frame.raw:

# dd if=/dev/xillybus_read_32 of=frame.raw bs=614400 iflag=fullblock count=1

Das Format des rohen Frames ist UYVY 4:2:2. Mit anderen Worten: Jedes Pixel besteht aus 16 Bit. Das erste Byte ist die U-Komponente des ersten Pixels (auch Cb genannt). Das nächste Byte ist die Y-Komponente desselben Pixels. Das dritte und vierte Byte sind die V- und Y-Komponenten des zweiten Pixels (Cr und Y).

Mit dem folgenden Befehl lässt sich diese Datei in eine PNG-Datei umwandeln:

# convert -size 640x480 pal:frame.raw frame.png

Es gibt ein einfaches Werkzeug zum Anzeigen des Bildes:

# display frame.png &

Live-Ansicht

Um eine Live-Ansicht der Kamera zu erhalten, erstellen Sie eine Datei mit dem Namen liveview.sh, die Folgendes enthält:

#!/bin/bash

while [ 1 ] ; do
  dd if=/dev/xillybus_read_32 bs=614400 iflag=fullblock count=1 2>/dev/null
done | mplayer -demuxer rawvideo -rawvideo w=640:h=480:format=uyvy:size=614400 -

Führen Sie dieses Skript (script) mit folgendem Befehl aus:

# bash liveview.sh

Warum wird dieses Skript benötigt? Es ist unmöglich, die Daten direkt aus der Gerätedatei zu lesen, weil mplayer zu langsam ist. Mit anderen Worten: Der ARM-Prozessor des Zynq ist nicht leistungsfähig genug, um das Video mit der korrekten Bildrate (31,25 fps) abzuspielen. Wenn man das versucht, wird das Video nur kurz abgespielt. Der Datenfluss stoppt, sobald ein Überlauf im FPGA auftritt.

Dieses Skript basiert auf einer Endlosschleife, die jedes Mal einen rohen Frame aus /dev/xillybus_read_32 liest. Es ist derselbe Befehl, der zuvor verwendet wurde, um einen Frame in frame.raw zu lesen. Diesmal ist für dd jedoch keine Ausgabedatei angegeben. Deshalb schreibt dd die Daten stattdessen in die Standardausgabe.

Die Ausgabe dieser Endlosschleife wird über eine Pipe (Pipeline, achten Sie auf das „|“ am Ende der Schleife) in die Standardeingabe von mplayer umgeleitet. mplayer spielt die Videodaten ab, die aus der Standardeingabe ankommen.

Das Skript löst das Überlaufproblem, weil dd immer einen vollständigen Video-Frame liest. Wenn mplayer nicht bereit ist, weitere Daten anzunehmen, wird der Datenfluss der Pipe kurzzeitig angehalten. Dadurch überspringt dd Video-Frames des Kamerasensors. Die auf dem Bildschirm angezeigte Bildrate ist daher niedriger als die Bildrate der Kamera. Genauer gesagt ist die angezeigte Bildrate die maximale Bildrate, die mplayer darzustellen in der Lage ist.

Das auf dem Bildschirm erscheinende Videobild ist durch die Pufferung von mplayer leicht verzögert. Für eine geringe Latenz wäre es nötig, ein einfaches Programm zu schreiben, das das Bild ohne zusätzlichen Puffer anzeigt.

mplayer ist ein leistungsfähiger Mediaplayer. Wenn die falschen Farben zum Beispiel stören, kann man den Videoclip auch als Schwarzweiß-Video abspielen. Fügen Sie dem Befehl den folgenden Teil hinzu, um die Sättigung auf null zu reduzieren:

# mplayer -saturation -100 -demuxer rawvideo ...

Ende des praktischen Teils dieser Seite

Der Rest dieser Seite erklärt die Implementierung der Logik, die die Datenerfassung durchführt. Wenn Sie sich nur für die praktischen Themen interessieren, lesen Sie weiter im nächsten Teil dieses Tutorials.

Kommunikation zwischen FPGA und Host

Die Logik in diesem Beispiel basiert auf dem IP-Kern (IP core) von Xillybus. Dieser IP-Kern ist für die Kommunikation mit dem Host verantwortlich.

Erinnern Sie sich, dass der oben ersetzte Teil des Verilog-Codes mit Folgendem endet:

   fifo_32x512 fifo_32
     (
      .clk(bus_clk),
      .srst(!user_r_read_32_open),

      .din(dataword),
      .wr_en(fifo_wr_en),
      .full(fifo_full),

      .rd_en(user_r_read_32_rden),
      .dout(user_r_read_32_data),
      .empty(user_r_read_32_empty)
      );

Dabei handelt es sich um eine Instanziierung (instantiation) eines Standard-FIFOs. Dieses FIFO besitzt drei Ports zum Lesen von Daten aus dem FIFO: rd_en, dout und empty (leer). Diese Ports sind mit dem Xillybus-IP-Kern verbunden. Dadurch kann der IP-Kern Daten aus dem FIFO lesen und an den Host senden. Das Ergebnis ist, dass alles, was in das FIFO geschrieben wird, in der Gerätedatei /dev/xillybus_read_32 ankommt. Mit anderen Worten: Ein normales Computerprogramm auf dem Host kann /dev/xillybus_read_32 wie eine reguläre Datei öffnen. Wenn das Programm aus dieser Datei liest, erhält es die Daten, die von der Anwendungslogik im FPGA in das FIFO geschrieben wurden.

Das FIFO besitzt drei Ports zum Schreiben von Daten: wr_en, din und full (voll). Diese Ports sind mit der Logik verbunden, die die Pixeldaten vom Kamerasensor einsammelt. Die nächsten Abschnitte dieser Seite erklären, wie diese Logik funktioniert. Vorerst weise ich nur darauf hin, dass die Logik die Pixeldaten mit Hilfe von @dataword und @fifo_wr_en in das FIFO schreibt. Ab hier ist es die Aufgabe des Xillybus-IP-Kerns, diese Daten zu dem Computerprogramm zu bringen, das auf dem Host läuft. Deshalb schreibt der folgende (bereits oben erwähnte) Befehl diese Daten in eine Datei:

# cat /dev/xillybus_read_32 > clip.raw

Eine allgemeine Erklärung, wie ein FIFO funktioniert, finden Sie auf dieser Seite.

Der Datenfluss lässt sich mit diesem Diagramm zusammenfassen:

Simplified diagram of data acquisition with Xillybus

Auf dieser Website gibt es einen Abschnitt über Xillybus, und in diesem Abschnitt gibt es eine Seite zur Datenerfassung. Es kann hilfreich sein, diese Seite zu lesen.

Beachten Sie, dass user_r_read_32_open mit dem srst-Port des FIFO verbunden ist. Wenn /dev/xillybus_read_32 auf dem Host geöffnet wird, wechselt dieses Signal auf high. Das Signal ist mit einem NICHT verknüpft. Wenn also die Gerätedatei geschlossen wird, wird das FIFO im Reset-Zustand gehalten. Dadurch wird sichergestellt, dass bei jedem Schließen der Datei alle Daten im FIFO gelöscht werden.

Schnittstelle zum Kamerasensor

Wir sehen uns nun den Anfang des obigen Verilog-Codes an:

   reg [1:0]  clkdiv;

   always @(posedge bus_clk)
     clkdiv <= clkdiv + 1;

   assign J6[10] = clkdiv[1]; // MCLK / XCLK

@bus_clk hat eine Frequenz von 100 MHz. Dieser Takt wird mit Hilfe von @clkdiv durch vier geteilt. Dementsprechend erhält das Kameramodul einen Referenztakt von 25 MHz. Laut Datenblatt der Kamera ist diese Frequenz zulässig. Die Kamera ist jedoch dafür ausgelegt, bei einer Referenztaktfrequenz von 24 MHz einen Videostream mit 30 fps zu erzeugen. Die tatsächliche Bildfrequenz liegt daher geringfügig höher: 31,25 fps.

Anmerken möchte ich, dass dies normalerweise keine korrekte Methode zur Takterzeugung ist. Die korrekte Methode ist die Verwendung einer PLL oder einer ähnlichen Ressource. In diesem speziellen Fall ist diese Methode unproblematisch, weil @clkdiv nur zur Erzeugung eines Ausgangssignals dient: Die Logik des FPGA selbst verwendet dieses Signal nicht.

Der nächste Teil des Verilog-Codes lautet:

   assign J6[0] = 0; // PWDN, the camera is always on
   assign J6[1] = !user_w_write_32_open; // RESET#, active low

J6[0] ist mit dem PWDN-Pin des Kameramoduls verbunden. Die Kamera wird nie ausgeschaltet (nicht in den Power-Down versetzt).

J6[1] ist mit dem RESET#-Eingang der Kamera verbunden. Wenn dieser Pin low ist, wird die Kamera zurückgesetzt. @user_w_write_32_open ist high, wenn /dev/xillybus_write_32 von einem Programm auf dem Host geöffnet wird. Normalerweise wird die Kamera also nicht zurückgesetzt, denn @user_w_write_32_open ist low, und folglich ist J6[1] high. Diese Anordnung ermöglicht es, die Kamera mit dem folgenden Befehl zurückzusetzen:

# echo 1 > /dev/xillybus_write_32

Dieser Befehl öffnet die Gerätedatei für kurze Zeit, und das führt zum gewünschten Ergebnis.

Bisher habe ich gezeigt, wie Signale vom FPGA zum Kamerasensor erzeugt werden. Nun zu den Signalen vom Kamerasensor zum FPGA.

Der OV7670 erzeugt einen Pixeltakt, dessen Frequenz genauso hoch ist wie die des Referenztakts vom FPGA. Mit anderen Worten: PCLK hat eine Frequenz von 25 MHz. Dieses Signal ist im Verilog-Code mit @pclk_in verbunden.

Der Kamerasensor erzeugt außerdem drei Signale, die die Videodaten enthalten. Im Verilog-Code heißen diese Signale @D_in, @hsync_in und @vsync_in. Der Kamerasensor ändert die Werte dieser Signale gleichzeitig mit dem Wechsel von @pclk_in von high auf low (fallende Flanke). Genauer gesagt sind die Änderungen von @D_in, @hsync_in und @vsync_in an der fallenden Flanke von @pclk_in ausgerichtet. Aus Sicht des FPGA handelt es sich dabei um einen quellensynchronen Eingang (source synchronous).

Nun sehen wir uns den relevanten Teil des Verilog-Codes an:

   wire [7:0] D_in;
   wire       pclk_in, hsync_in, vsync_in;

   assign D_in = J6[9:2];
   assign pclk_in = J6[11];
   assign hsync_in = J6[12];
   assign vsync_in = J6[13];

   (* IOB = "TRUE" *) reg [7:0] D_guard;
   (* IOB = "TRUE" *) reg       pclk_guard, hsync_guard, vsync_guard;

   reg [7:0]  D;
   reg 	      pclk, hsync, vsync;

   always @(posedge bus_clk)
     begin
	// Metastability guards on asynchronous inputs
	D_guard <= D_in;
	pclk_guard <= pclk_in;
	hsync_guard <= hsync_in;
	vsync_guard <= vsync_in;

	D <= D_guard;
	pclk <= pclk_guard;
	hsync <= hsync_guard;
	vsync <= vsync_guard;
     end

Beachten Sie, dass alle Signale des Kamerasensors mit Hilfe von @bus_clk abgetastet (sampling) werden. Selbst der PCLK der Kamera wird auf dieselbe Weise abgetastet wie die übrigen Signale. PCLK wird also nicht als Takt, sondern als Datensignal behandelt. Ich erkläre diese Technik weiter unten kurz.

Beachten Sie außerdem, dass die zeitliche Beziehung zwischen @bus_clk und den Signalen des Kamerasensors unbekannt ist. Die Ausgänge der Flip-Flops, die diese Signale empfangen, sind daher nicht zuverlässig: Man kann die Timing-Anforderungen dieser Flip-Flops nicht sicherstellen, sodass sie für kurze Zeit instabil werden können. Dies ist ein bekanntes Problem beim Wechsel zwischen Taktdomänen (clock domain crossing).

Die Lösung für dieses Problem wird auf einer separaten Seite beschrieben: Metastabilitätsschutz (metastability guards). Das bedeutet, dass zwei Flip-Flops in Reihe geschaltet sind. Das erste Flip-Flop (@pclk_guard zum Beispiel) ist mit dem externen Signal verbunden. Das zweite Flip-Flop ist mit dem ersten verbunden. Selbst wenn das erste Flip-Flop für kurze Zeit instabil wird, sind die Timing-Anforderungen des zweiten Flip-Flops garantiert. Der Ausgang des zweiten Flip-Flops ist daher zuverlässig.

Zusammenfassend gilt: @D, @pclk, @hsync und @vsync sind zuverlässige Register, die mit @bus_clk synchron sind.

Zur Erinnerung: @bus_clk hat eine Frequenz von 100 MHz, PCLK dagegen 25 MHz. Die Werte von @D, @hsync und @vsync sollten daher nur einmal in jedem vierten Taktzyklus verwendet werden. Aber welcher der vier Taktzyklen ist der richtige?

Die Antwort steht in diesen Zeilen des Verilog-Codes:

   wire       sample_valid;
   reg 	      previous_pclk;

   always @(posedge bus_clk)
     previous_pclk <= pclk;

   assign sample_valid = pclk && !previous_pclk;

Dieser Codeschnipsel bedeutet einfach: Wenn @pclk jetzt high ist und im vorherigen Taktzyklus low war, dann verwende die Werte in @D, @hsync und @vsync. Zur Erinnerung: Die Signale des Kamerasensors ändern ihre Werte, wenn der PCLK der Kamera von high auf low wechselt. Wenn also PCLK von low auf high wechselt, sind die anderen Signale stabil.

Aber @pclk, @D, @hsync und @vsync sind Register. Sie sind mit @bus_clk synchron und stellen eine Momentaufnahme der Signale des Kamerasensors zu einem bestimmten Zeitpunkt dar. Anstatt die steigende Flanke von PCLK selbst zu erkennen, macht die Logik etwas Ähnliches mit @pclk: Wenn sich der Wert von @pclk von low auf high ändert, ist das der richtige Zeitpunkt, die Werte der übrigen Register zu verwenden.

Diese Technik nennt man 01-Signal-Abtastung (01-signal sampling). Die Idee hinter dieser Technik wird auf einer separaten Seite zur 01-Signal-Abtastung ausführlich erklärt. Diese Seite erklärt auch, wie diese Methode die Timing-Anforderungen des FPGA garantiert. Auf diese Weise stellt die Logik sicher, dass die Werte in @D, @hsync und @vsync korrekt sind.

Starten und Stoppen des Datenflusses

Wir sehen uns nun zwei Register an, die verhindern sollen, dass Daten an den Host übertragen werden:

Ich werde diese beiden Register nun im Einzelnen besprechen. Zuerst @wait_for_frame:

   reg 	      wait_for_frame;

   always @(posedge bus_clk)
     if (!user_r_read_32_open)
       wait_for_frame <= 1;
     else if (sample_valid && vsync)
       wait_for_frame <= 0;

@wait_for_frame ist high, wenn die Gerätedatei nicht geöffnet ist. Der Wert dieses Registers wechselt als Reaktion auf das vsync-Signal des Kamerasensors auf low. Dieses Signal ist während eines Zeitraums zwischen zwei Frames high. Mit anderen Worten: Wenn @vsync high ist, werden vom Kamerasensor keine Pixeldaten übertragen.

Zusammenfassend ist @wait_for_frame high, wenn die Pixeldaten der Kamera ignoriert werden sollen: wenn die Gerätedatei geschlossen ist oder wenn die Gerätedatei vor kurzem geöffnet wurde, sich die Kamera aber noch mitten in einem Frame befindet.

Nun zu @fifo_has_been_full: Es ist wichtig sicherzustellen, dass die Daten, die am Host ankommen, mit den Daten übereinstimmen, die der Kamerasensor erzeugt. Allerdings kann es im FIFO zu einem Überlauf (overflow) kommen, wenn das Computerprogramm die Daten nicht schnell genug aus der Gerätedatei liest: Die DMA-Puffer werden schließlich voll, sodass es keinen Platz mehr gibt, in den der Inhalt des FIFO kopiert werden kann. Folglich kann der Xillybus-IP-Kern keine Daten mehr aus dem FIFO lesen. Wenn das passiert, wird das FIFO full (voll), sodass es unmöglich ist, neue Daten hineinzuschreiben.

Die Logik kann diese Situation nicht verhindern. Sie kann jedoch sicherstellen, dass die Daten, die beim Host ankommen, zusammenhängend sind: Wenn das FIFO full (voll) wird, hört die Logik auf, Daten in das FIFO zu schreiben. Wenn das FIFO anschließend, nachdem es full (voll) gewesen ist, empty (leer) wird, veranlasst die Logik, dass ein EOF an den Host gesendet wird. Dadurch erhält das Computerprogramm alle Daten, die in das FIFO geschrieben wurden, bevor es full (voll) wurde. Nach diesen Daten empfängt das Programm ein EOF. Das ist dasselbe, was auch passiert, wenn das Ende einer regulären Datei erreicht wird.

Dieser Mechanismus stellt sicher, dass das Computerprogramm darauf vertrauen kann, dass die ankommenden Daten korrekt und zusammenhängend sind. Geht die Lückenlosigkeit verloren, zwingt das EOF das Programm, die Gerätedatei zu schließen. Öffnet das Programm die Gerätedatei erneut, beginnen die Daten dank @wait_for_frame mit einem neuen Frame.

Hier ist der zugehörige Teil des Verilog-Codes:

   reg 	      fifo_has_been_nonfull, fifo_has_been_full;
   wire       fifo_full;

   always @(posedge bus_clk)
     begin
	if (!fifo_full)
	  fifo_has_been_nonfull <= 1;
	else if (!user_r_read_32_open)
	  fifo_has_been_nonfull <= 0;

	if (fifo_full && fifo_has_been_nonfull)
	  fifo_has_been_full <= 1;
	else if (!user_r_read_32_open)
	  fifo_has_been_full <= 0;
     end

   assign user_r_read_32_eof = fifo_has_been_full && user_r_read_32_empty;

@fifo_has_been_full ist high, wenn das FIFO einmal full (voll) gewesen ist. Dieses Register wechselt auf low, wenn die Gerätedatei nicht geöffnet ist. @fifo_has_been_full wird high, wenn sowohl @fifo_full als auch @fifo_has_been_nonfull high sind.

@fifo_full ist mit dem Port „full“ (voll) des FIFO verbunden. Warum ist @fifo_has_been_nonfull nötig? Der Grund ist, dass ein FIFO seinen Ausgang „full“ (voll) oft so lange auf high hält, wie sich das FIFO im Reset-Zustand befindet. Der Zweck dieses Verhaltens ist es, der Anwendungslogik mitzuteilen, dass das FIFO noch nicht bereit ist, Daten zu empfangen. @fifo_has_been_nonfull verhindert, dass @fifo_has_been_full in diesem Szenario fälschlich high wird.

@user_r_read_32_eof wird high, wenn sowohl @fifo_has_been_full als auch @user_r_read_32_empty high sind. Mit anderen Worten: Ein EOF wird an den Host gesendet, wenn das FIFO früher full (voll) war und jetzt empty (leer) ist. Beachten Sie, dass in dieser Situation ohnehin keine neuen Daten mehr in das FIFO geschrieben werden.

Eine separate Seite behandelt eine ähnliche Lösung zur Sicherstellung der Lückenlosigkeit der Daten. Die dort vorgestellte Lösung ist erforderlich, wenn die beiden Seiten des FIFO zu verschiedenen Taktdomänen (clock domains) gehören. Bei dem auf dieser Seite gezeigten Code arbeitet das FIFO mit nur einem Takt synchron. Die Implementierung von @fifo_has_been_full ist auf dieser Seite deshalb einfacher.

Schreiben von Daten in das FIFO

Der nächste Teil des Verilog-Codes schreibt die Pixeldaten in das FIFO:

   reg 	      fifo_wr_en;
   reg [1:0]  byte_position;
   reg [31:0] dataword;

   always @(posedge bus_clk)
     if (wait_for_frame)
       begin
	  byte_position <= 0;
	  fifo_wr_en <= 0;
       end
     else if (sample_valid && hsync)
       begin
	  case (byte_position)
	    0: dataword[7:0] <= D;
	    1: dataword[15:8] <= D;
	    2: dataword[23:16] <= D;
	    3: dataword[31:24] <= D;
	  endcase

	  if (byte_position == 3)
	    fifo_wr_en <= !fifo_has_been_full;
	  else
	    fifo_wr_en <= 0;

	  byte_position <= byte_position + 1;
       end
     else
       fifo_wr_en <= 0;

Die Pixeldaten des Kamerasensors kommen als 8 Bit breite Datenelemente an. Dieser Logikteil gruppiert diese Datenelemente zu 32 Bit, damit die Daten in das FIFO geschrieben werden können. Der 8-Bit-Datenstrom von Xillybus (/dev/xillybus_write_8) wird dafür aus zwei Gründen nicht verwendet:

Wenn @wait_for_frame high ist, wird nichts in das FIFO geschrieben. Dafür gibt es zwei Möglichkeiten: Entweder ist die Gerätedatei nicht geöffnet, oder die Gerätedatei ist geöffnet, aber der Anfang eines neuen Frames ist noch nicht erreicht.

Wenn HSYNC des Kamerasensors high ist, bedeutet das, dass die Datensignale gültige Pixel enthalten. Der Ausdruck „sample_valid && hsync“ kombiniert zwei Kriterien: Wenn @sample_valid high ist, enthalten @hsync und @D gültige Werte. Ist also @hsync high, wird der Wert von @D in einen Teil von @dataword kopiert. Wird @D außerdem in den letzten Teil von @dataword kopiert (d. h. @byte_position ist gleich 3), wird @fifo_wr_en high. Dadurch wird @dataword in das FIFO geschrieben. Genauer gesagt lautet der Ausdruck für @fifo_wr_en folgendermaßen:

fifo_wr_en <= !fifo_has_been_full;

Ist @fifo_has_been_full also high, wird nichts in das FIFO geschrieben, wie bereits weiter oben erwähnt wurde.

Der Zusammenhang zwischen Verilog-Code und echten Pins

Der obige Verilog-Code verwendet den Inout-Port mit dem Namen J6. Aber wie gelangen die Verbindungen dieses Ports zur Stiftleiste? Die Antwort findet sich in xillydemo.xdc. Diese Datei gehört zu dem Vivado-Projekt, das den Bitstrom erzeugt (im Verzeichnis „vivado-essentials“).

xillydemo.xdc enthält verschiedene Informationen, die erforderlich sind, damit das FPGA als elektronisches Bauelement korrekt funktioniert. Unter anderem enthält diese Datei folgende Zeilen:

[ ... ]

## J6 on board (BANK33 VADJ)
set_property PACKAGE_PIN U22  [get_ports {J6[0]}];   #J6/1  = IO_B33_LN2
set_property PACKAGE_PIN T22  [get_ports {J6[1]}];   #J6/2  = IO_B33_LP2
set_property PACKAGE_PIN W22  [get_ports {J6[2]}];   #J6/3  = IO_B33_LN3
set_property PACKAGE_PIN V22  [get_ports {J6[3]}];   #J6/4  = IO_B33_LP3
set_property PACKAGE_PIN Y21  [get_ports {J6[4]}];   #J6/5  = IO_B33_LN9
set_property PACKAGE_PIN Y20  [get_ports {J6[5]}];   #J6/6  = IO_B33_LP9
set_property PACKAGE_PIN AB22 [get_ports {J6[6]}];   #J6/7  = IO_B33_LN7
set_property PACKAGE_PIN AA22 [get_ports {J6[7]}];   #J6/8  = IO_B33_LP7

[ ... ]

In der ersten Zeile steht, dass das Signal J6[0] mit U22 verbunden werden soll. Dabei handelt es sich um eine Position auf dem physischen Gehäuse des FPGA. Laut den Schaltplänen des Smart Zynq ist dieser FPGA-Pin mit dem ersten Pin der Stiftleiste verbunden. Die Positionen der übrigen Ports sind auf dieselbe Weise festgelegt.

Fazit

Diese Seite hat gezeigt, wie man die Pixeldaten des OV7670 erhält und diese Daten mithilfe des Xillybus-IP-Kerns an den Host sendet.

Der nächste Teil dieses Tutorials erklärt, wie der Xillybus-IP-Kern verwendet wird, um die Register des Kamerasensors mithilfe von SCCB (d. h. I2C) zu ändern. Das ist nützlich, um die Kameraparameter zu verändern. Insbesondere ist dies notwendig, um ein Bild mit korrekten Farben zu erhalten.

Diese Seite wurde maschinell aus dem Englischen übersetzt. Im Zweifelsfall siehe den Originaltext.
Copyright © 2021-2026. All rights reserved. (dcc38493)