Cette page web fait partie d’un ensemble de petits projets qui explorent les possibilités de la carte Smart Zynq.
Ce projet est également publié sur HelloFPGA, un site recommandé pour les lecteurs chinois.
Introduction
Ce tutoriel explique comment connecter un module de caméra OV7670 à la carte Smart Zynq et afficher le signal vidéo en direct sur la sortie HDMI de la carte. Je montrerai aussi comment le flux vidéo brut peut être facilement enregistré dans un fichier à l’aide d’une simple commande Linux.
Les informations de ce tutoriel concernent aussi d’autres types d’acquisition de données (data acquisition) : les techniques présentées ci-dessous peuvent s’appliquer à d’autres sources de données d’image ainsi qu’à d’autres sources de données numériques.
Le module de caméra est connecté à la partie PL (FPGA) de la puce Zynq. Il est donc facile d’ajouter de la logique qui effectue un traitement d’image avant d’envoyer les données au processeur ARM. Comme ce tutoriel repose sur Xillinux, la partie processeur du système se compose d’une distribution Linux complète.
Le module OV7670 a été choisi pour cette démonstration parce que ce matériel est bon marché, courant et facile à acheter. De plus, les signaux numériques générés par ce composant sont simples à comprendre.
Cependant, l’OV7670 a un défaut gênant : par défaut, les couleurs du flux vidéo sont incorrectes. C’est un problème connu de ce capteur de caméra. Il est possible de corriger ce défaut en modifiant un petit nombre de registres de la caméra. Ce tutoriel est donc divisé en deux parties :
- Comment connecter le capteur de caméra à la carte Smart Zynq et lire le flux vidéo avec de simples outils Linux.
- Comment obtenir un flux vidéo avec des couleurs correctes. Cette partie montre comment utiliser Xillybus pour modifier les registres du capteur de caméra à l’aide du bus I2C.
Remarque : une grande partie de ce tutoriel explique le fonctionnement interne du montage. Ces explications ne sont pas nécessaires pour utiliser la caméra.
Le module OV7670
Ce tutoriel est basé sur le module de caméra représenté ci-dessous :
Sur ce module, la plupart des broches du connecteur sont reliées directement au composant OV7670. Seuls le 3,3 V et la masse (GND) passent par des régulateurs de tension. Par conséquent, toutes les connexions entre le FPGA et le module sont des connexions directes entre le FPGA et le composant OV7670.
Il existe d’autres modules équivalents sur le marché. Il est probablement possible d’utiliser ces autres modules également. Par exemple, un module sur lequel figure la mention « 2017/3/15 » sur le circuit imprimé fonctionne lui aussi correctement. En revanche, un module ne fonctionne pas correctement : il s’agit de celui qui porte la mention « QYF-OV7670 V3.0 » sur le circuit imprimé (PCB).
Il faut aussi savoir qu’il existe différentes révisions du composant OV7670. Il est possible de vérifier que la bonne révision est montée sur le module. La manière de procéder est expliquée dans la deuxième partie de ce tutoriel.
Il existe deux sources d’information principales sur l’OV7670. On peut trouver ces deux documents sur Internet :
- La fiche technique (datasheet) : OV7670/OV7171 CMOS VGA (640x480) CAMERA CHIP Sensor with OmniPixel Technology, Version 1.4, August 21, 2006. Il est important d’obtenir la version 1.4 de ce document.
- Le guide de mise en œuvre : OV7670/OV7171 CMOS VGA (640x480) CameraChip Implementation Guide. Apparemment, seule la version 1.0 du 2 septembre 2005 est disponible sur Internet. Cette version est malheureusement périmée et se réfère à une version antérieure de la puce de la caméra.
Préparation du projet Vivado
Créez un nouveau projet Vivado à partir de l’archive du pack de démonstration (demo bundle), c’est-à-dire le kit de partition de démarrage (boot partition kit). Ouvrez verilog/src/xillydemo.v dans un éditeur de texte. Supprimez la partie du code appelée « PART 2 ». Insérez à la place l’extrait de code suivant :
/*
* 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)
);
Vous pouvez aussi télécharger xillydemo.v après cette modification ici.
Générez ensuite un flux de bits (bitstream) comme d’habitude après cette modification. Le fonctionnement de ce code Verilog est expliqué en détail plus bas sur cette page.
Connexion du module de caméra
On peut utiliser des fils Dupont courts pour relier le module de caméra à la carte Smart Zynq. La longueur des fils doit être de 10 cm ou moins ; la longueur idéale est de 5 cm. Si les fils sont plus longs, la qualité des signaux numériques peut être dégradée par la diaphonie. Un excès de bruit sur la synchronisation horizontale produit une image vidéo qui saute, avec des rayures vertes et violettes.
Avec des fils de 10 cm, il peut être nécessaire de modifier un registre afin de réduire le courant des drivers d’E/S de l’OV7670. La deuxième partie de ce tutoriel montre comment procéder.
Voici une photo du module OV7670 connecté à une carte Smart Zynq SP :
La photo ci-dessous a été prise depuis la direction opposée. La petite image en haut à gauche fait ressortir le fait que la dernière broche du connecteur n’est connectée à rien.
Les photos ci-dessus montrent comment brancher les fils : cherchez d’abord l’endroit où est écrit « Bank 33 VCCIO Vadj » au dos de la carte Smart Zynq. La rangée de broches proche de cette inscription est le connecteur à broches sur lequel nous allons travailler. C’est le connecteur situé près du connecteur HDMI.
Seize fils relient en parallèle le capteur de caméra et la carte Smart Zynq. Seuls le 3,3 V et GND sont connectés à un autre endroit du connecteur. Ces deux fils n’ont pas besoin d’être courts.
Remarque : la dernière broche du connecteur est en 5 V. N’y connectez aucun fil.
Voici la correspondance de câblage entre le module de caméra et le connecteur de la Smart Zynq. On peut aussi la déduire des photos ci-dessus :
| Broches du connecteur | 1 | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 35 |
| Broches du module | PWDN | D0 | D2 | D4 | D6 | MCLK | HS | SDA | GND |
| Broches du module | RST | D1 | D3 | D5 | D7 | PCLK | VS | SCL | 3.3V |
| Broches du connecteur | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 37 |
Encore une fois, faites très attention au branchement du 3,3 V et de GND. Une erreur de connexion sur ces deux fils peut détruire le module de caméra.
Capture d’images
Démarrez Xillinux avec un flux de bits (bitstream) issu du xillydemo.v modifié (comme ci-dessus).
La commande suivante (à saisir dans un terminal) crée un court extrait vidéo à partir de la sortie de la caméra :
# cat /dev/xillybus_read_32 > clip.raw
Cette commande s’exécute pendant quelques secondes, puis s’arrête. La raison est que le débit du flux vidéo est supérieur à la vitesse d’écriture de la carte SD. Il en résulte un débordement (overflow) qui arrête le flux de données. Le mécanisme derrière ce comportement est expliqué plus bas.
Vous pouvez lire cet extrait vidéo avec la commande suivante :
# mplayer -demuxer rawvideo -rawvideo w=640:h=480:format=uyvy:size=614400:fps=31.25 clip.raw
Si vous lancez cette commande dans une fenêtre de terminal ouverte sur le bureau graphique de Xillinux, la vidéo s’affiche dans l’interface graphique de Xillinux.
Vous pouvez aussi afficher la vidéo sur l’écran d’un autre ordinateur en utilisant la technique décrite sur une page séparée. Par exemple, si l’adresse IP de l’autre ordinateur est 192.168.1.11, modifiez la commande pour qu’elle commence par :
# DISPLAY=192.168.1.11:0 mplayer -demuxer rawvideo ...
Comme déjà mentionné, les couleurs de l’extrait vidéo sont incorrectes. La page suivante explique comment corriger ce problème.
Cette commande lit une image vidéo complète dans un fichier nommé frame.raw :
# dd if=/dev/xillybus_read_32 of=frame.raw bs=614400 iflag=fullblock count=1
Le format de l’image brute est UYVY 4:2:2. En d’autres termes, chaque pixel tient sur 16 bits. Le premier octet est la composante U du premier pixel (ou Cb). L’octet suivant est la composante Y du même pixel. Les troisième et quatrième octets sont les composantes V et Y du deuxième pixel (respectivement Cr et Y).
Ce fichier peut être converti en PNG avec :
# convert -size 640x480 pal:frame.raw frame.png
Un petit outil permet d’afficher l’image :
# display frame.png &
Vue en direct
Pour obtenir une vue en direct de la caméra, créez un fichier nommé liveview.sh contenant ceci :
#!/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 -
Exécutez ce script avec :
# bash liveview.sh
Pourquoi ce script est-il nécessaire ? Il est impossible de lire les données directement depuis le fichier de périphérique, car mplayer est trop lent. En d’autres termes, le processeur ARM du Zynq n’est pas assez puissant pour lire la vidéo à la bonne cadence (31,25 images par seconde). Si vous essayez, la vidéo n’est lue que brièvement. Le flux de données s’arrête dès qu’un débordement se produit dans le FPGA.
Ce script repose sur une boucle sans fin qui lit une image brute depuis /dev/xillybus_read_32 à chaque passage. C’est la même commande que celle utilisée plus haut pour lire une image dans frame.raw. Mais cette fois, aucun fichier de sortie n’est donné à dd. dd écrit donc les données sur sa sortie standard.
Le résultat de cette boucle sans fin est redirigé vers l’entrée standard de mplayer grâce à un tube (pipe) — remarquez le « | » à la fin de la boucle. mplayer lit les données vidéo qui arrivent sur son entrée standard.
Le script résout le problème du débordement parce que dd lit toujours une image vidéo complète. Quand mplayer n’est pas prêt à accepter de nouvelles données, le flux dans le tube est momentanément arrêté. Par conséquent, dd saute des images produites par le capteur. La cadence affichée à l’écran est donc inférieure à celle du capteur. Plus précisément, la cadence affichée est la cadence maximale que mplayer peut afficher.
L’image vidéo affichée à l’écran est légèrement retardée à cause de la mise en mémoire tampon propre à mplayer. Pour obtenir une faible latence, il faut écrire un petit programme qui affiche l’image à l’écran sans ajouter de tampon.
mplayer est un lecteur multimédia puissant. Par exemple, si les couleurs incorrectes vous gênent, vous pouvez regarder l’extrait vidéo en noir et blanc. Ajoutez la partie suivante à la commande pour réduire la saturation à zéro :
# mplayer -saturation -100 -demuxer rawvideo ...
Fin de la partie pratique de cette page
La suite de cette page explique la mise en œuvre de la logique qui réalise l’acquisition de données. Si seuls les aspects pratiques vous intéressent, passez à la partie suivante de ce tutoriel.
Communication entre le FPGA et l’hôte
La logique de cet exemple repose sur le cœur IP (IP core) Xillybus. Ce cœur IP est chargé de la communication avec l’hôte.
Rappelons que la partie remplacée dans le code Verilog se terminait par ceci :
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)
);
C’est une instanciation (instantiation) d’une FIFO standard. Cette FIFO comporte trois ports destinés à la lecture : rd_en, dout et empty (vide). Ces ports sont connectés au cœur IP Xillybus. Le cœur IP peut ainsi lire les données dans la FIFO et les envoyer à l’hôte. Le résultat est que tout ce qui est écrit dans la FIFO aboutit dans le fichier de périphérique /dev/xillybus_read_32. Autrement dit, un programme ordinaire sur l’hôte peut ouvrir /dev/xillybus_read_32 comme un fichier normal. Quand le programme lit ce fichier, il reçoit les données que la logique applicative du FPGA a écrites dans la FIFO.
La FIFO comporte aussi trois ports destinés à l’écriture : wr_en, din et full (pleine). Ces ports sont reliés à la logique qui collecte les pixels du capteur. Les sections suivantes expliquent le fonctionnement de cette logique. Pour l’instant, notons simplement que la logique écrit les pixels dans la FIFO à l’aide de @dataword et @fifo_wr_en. À partir de là, c’est au cœur IP Xillybus d’acheminer ces données vers le programme qui s’exécute sur l’hôte. C’est pourquoi la commande suivante, déjà mentionnée plus haut, écrit ces données dans un fichier :
# cat /dev/xillybus_read_32 > clip.raw
Pour une explication générale du fonctionnement d’une FIFO, reportez-vous à cette page.
Le flux de données peut être résumé par ce schéma :
Il y a une section sur ce site web consacrée à Xillybus, avec en particulier une page sur l’acquisition de données. Il peut être utile de la lire.
Notez que user_r_read_32_open est relié au port srst de la FIFO. Quand /dev/xillybus_read_32 est ouvert sur l’hôte, ce signal passe à l’état haut. Ce signal est inversé ; par conséquent, quand le fichier de périphérique est fermé, la FIFO est maintenue en réinitialisation. Cela garantit qu’à chaque fermeture du fichier, toutes les données de la FIFO sont effacées.
Interface avec le capteur de caméra
Regardons maintenant le début du code Verilog ci-dessus :
reg [1:0] clkdiv;
always @(posedge bus_clk)
clkdiv <= clkdiv + 1;
assign J6[10] = clkdiv[1]; // MCLK / XCLK
La fréquence de @bus_clk est de 100 MHz. Cette horloge est divisée par quatre à l’aide de @clkdiv. Le module de caméra reçoit donc une horloge de référence de 25 MHz. D’après la fiche technique du capteur, c’est une fréquence autorisée. Cependant, le capteur est conçu pour produire une vidéo à 30 images par seconde quand la fréquence de l’horloge de référence est de 24 MHz. La cadence vidéo réelle est donc légèrement supérieure : 31,25 images par seconde.
Je devrais préciser que cette méthode n’est généralement pas correcte pour créer une horloge. La méthode correcte consiste à utiliser une PLL ou une ressource similaire. Cela ne pose aucun problème dans ce cas précis, car @clkdiv ne sert qu’à créer un signal de sortie : la logique du FPGA n’utilise pas ce signal.
La partie suivante du code Verilog est celle-ci :
assign J6[0] = 0; // PWDN, the camera is always on
assign J6[1] = !user_w_write_32_open; // RESET#, active low
J6[0] est relié à la broche PWDN du module de caméra. La caméra n’est jamais mise hors tension.
J6[1] est relié à RESET# de la caméra. Quand cette broche est à l’état bas, la caméra est réinitialisée. @user_w_write_32_open est à l’état haut quand /dev/xillybus_write_32 est ouvert par un programme sur l’hôte. Normalement, la caméra n’est donc pas réinitialisée, car @user_w_write_32_open est à l’état bas et, par conséquent, J6[1] est à l’état haut. Ce montage permet de réinitialiser la caméra avec la commande suivante :
# echo 1 > /dev/xillybus_write_32
Cette commande ouvre le fichier de périphérique pendant un court instant, ce qui produit l’effet recherché.
Jusqu’ici, j’ai montré comment créer les signaux qui vont du FPGA vers le capteur. Intéressons-nous maintenant aux signaux qui vont du capteur vers le FPGA.
L’OV7670 produit une horloge de pixels dont la fréquence est la même que celle de l’horloge de référence fournie par le FPGA. Autrement dit, PCLK est à 25 MHz. Dans le code Verilog, ce signal est connecté à @pclk_in.
Le capteur produit également trois signaux qui portent les données vidéo. Dans le code Verilog, ils s’appellent @D_in, @hsync_in et @vsync_in. Le capteur modifie ces signaux au moment où @pclk_in passe de l’état haut à l’état bas (front descendant). Plus précisément, les changements de @D_in, @hsync_in et @vsync_in sont alignés sur le front descendant de @pclk_in. Du point de vue du FPGA, c’est ce que l’on appelle une entrée source synchrone (source synchronous input).
Examinons maintenant la partie correspondante du code Verilog :
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
Remarquez que tous les signaux provenant du capteur sont échantillonnés à l’aide de @bus_clk. Même PCLK est échantillonnée comme les autres signaux. En d’autres termes, le signal PCLK n’est pas traité comme une horloge, mais comme un signal de données. J’explique brièvement cette technique ci-dessous.
Remarquez aussi que la relation temporelle entre @bus_clk et les signaux du capteur est inconnue. Les sorties des bascules qui reçoivent ces signaux ne sont donc pas fiables : il est impossible de garantir les contraintes de temporisation (timing constraints) de ces bascules, si bien qu’elles peuvent devenir instables pendant de brefs instants. C’est un problème classique lors d’un changement de domaine d’horloge (clock domain crossing).
La solution à ce problème est présentée sur une page séparée : les garde-fous contre la métastabilité (metastability guards). Concrètement, deux bascules sont connectées en série. La première bascule (par exemple @pclk_guard) reçoit le signal externe ; la seconde est connectée à la première. Ainsi, même si la première bascule devient instable pendant un court instant, les exigences de temporisation de la seconde sont garanties. La sortie de la seconde est donc fiable.
En résumé : @D, @pclk, @hsync et @vsync sont des registres fiables, synchrones avec @bus_clk.
Rappelons que @bus_clk est à 100 MHz, alors que PCLK est à 25 MHz. Les valeurs de @D, @hsync et @vsync ne doivent donc être consommées qu’une fois tous les quatre cycles d’horloge. Mais auquel de ces quatre cycles ?
La réponse se trouve dans ces lignes du code Verilog :
wire sample_valid;
reg previous_pclk;
always @(posedge bus_clk)
previous_pclk <= pclk;
assign sample_valid = pclk && !previous_pclk;
Cet extrait signifie simplement : si @pclk est à l’état haut maintenant et était à l’état bas au cycle d’horloge précédent, alors utilisez les valeurs de @D, @hsync et @vsync. Rappelons que les signaux du capteur changent quand PCLK passe de l’état haut à l’état bas. Par conséquent, quand PCLK passe de l’état bas à l’état haut, les autres signaux sont stables.
Mais @pclk, @D, @hsync et @vsync sont des registres. Synchrones avec @bus_clk, ils représentent un instantané des signaux du capteur à un instant donné. Plutôt que de détecter le front montant de PCLK lui-même, la logique fait quelque chose d’équivalent avec @pclk : quand la valeur de @pclk passe de l’état bas à l’état haut, c’est le bon moment pour utiliser les valeurs des autres registres.
Cette technique s’appelle l’échantillonnage 01 (01-signal sampling). Son principe est expliqué en détail sur une page consacrée à l’échantillonnage 01. Cette page explique aussi comment cette méthode garantit les contraintes de temporisation du FPGA. La logique assure ainsi que les valeurs de @D, @hsync et @vsync sont correctes.
Démarrer et arrêter le flux de données
Intéressons-nous maintenant à deux registres destinés à empêcher toute transmission de données vers l’hôte :
- @wait_for_frame : ce registre a pour rôle de garantir que les données du fichier de périphérique commencent au début d’une image.
- @fifo_has_been_full : ce registre participe à un mécanisme qui arrête le flux de données si la FIFO passe à « full » (pleine).
Je vais maintenant détailler chacun de ces deux registres. D’abord, @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 est à l’état haut quand le fichier de périphérique n’est pas ouvert. Ce registre passe à l’état bas en réponse au signal vsync du capteur. Ce signal reste à l’état haut pendant la période entre deux images. Autrement dit, quand @vsync est à l’état haut, le capteur n’émet aucune donnée de pixel.
En clair, @wait_for_frame est à l’état haut quand les données de pixels de la caméra doivent être ignorées : quand le fichier de périphérique est fermé, ou quand il vient d’être ouvert alors que la caméra est encore au milieu d’une image.
Passons maintenant à @fifo_has_been_full : il est important de garantir que les données qui arrivent à l’hôte sont identiques à celles que produit le capteur. Or, un débordement peut se produire dans la FIFO si le programme ne lit pas les données assez rapidement : les tampons DMA finissent par être pleins, et il n’y a plus d’endroit où copier le contenu de la FIFO. Le cœur IP Xillybus ne peut alors plus lire de données dans la FIFO. Le cas échéant, la FIFO passe à « full » (pleine), ce qui rend impossible l’écriture de nouvelles données.
La logique ne peut rien faire pour empêcher cette situation. En revanche, elle peut garantir la contiguïté des données qui arrivent à l’hôte : si la FIFO passe à « full » (pleine), la logique arrête d’y écrire. Ensuite, lorsque la FIFO repasse à « empty » (vide) après avoir été « full » (pleine), la logique demande l’envoi d’un EOF à l’hôte. Le programme reçoit donc toutes les données écrites dans la FIFO avant qu’elle ne devienne « full » (pleine), puis il reçoit un EOF. C’est exactement ce qui se produit quand on atteint la fin d’un fichier ordinaire.
Ce mécanisme permet au programme d’être certain que les données reçues sont correctes et contiguës. Si la contiguïté est perdue, l’EOF oblige le programme à fermer le fichier de périphérique. S’il ouvre à nouveau le fichier, les données repartent du début d’une nouvelle image, grâce à @wait_for_frame.
Voici la partie correspondante du code Verilog :
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 est à l’état haut quand la FIFO a été « full » (pleine). Ce registre repasse à l’état bas quand le fichier de périphérique n’est pas ouvert. @fifo_has_been_full passe à l’état haut quand @fifo_full et @fifo_has_been_nonfull sont tous les deux à l’état haut.
@fifo_full est relié à la sortie « full » (pleine) de la FIFO. Mais pourquoi @fifo_has_been_nonfull est-il nécessaire ? Parce qu’une FIFO maintient souvent sa sortie « full » (pleine) à l’état haut tant qu’elle est en cours de réinitialisation. Le but de cette particularité est de signaler à la logique applicative que la FIFO n’est pas encore prête à recevoir des données. @fifo_has_been_nonfull sert à empêcher que @fifo_has_been_full ne passe par erreur à l’état haut dans ce scénario.
@user_r_read_32_eof passe à l’état haut quand @fifo_has_been_full et @user_r_read_32_empty (vide) sont tous les deux à l’état haut. Autrement dit, un EOF est envoyé à l’hôte quand la FIFO a été « full » (pleine) par le passé et qu’elle est maintenant « empty » (vide). Remarquez que, dans cette situation, plus aucune nouvelle donnée ne sera écrite dans la FIFO de toute façon.
Il existe une page séparée qui présente une solution analogue pour garantir la contiguïté des données. La solution présentée sur cette page est nécessaire quand les deux côtés de la FIFO appartiennent à des domaines d’horloge (clock domains) différents. Dans le code présenté sur cette page, la FIFO est synchrone avec une seule horloge. L’implémentation de @fifo_has_been_full est donc plus simple ici.
Écriture des données dans la FIFO
La partie suivante du code Verilog écrit les données de pixels dans la 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;
Les pixels du capteur arrivent sous forme d’éléments de 8 bits. Cette partie de la logique regroupe ces éléments en mots de 32 bits afin de pouvoir les écrire dans la FIFO. Le flux Xillybus à 8 bits (/dev/xillybus_write_8) n’est pas utilisé pour cela, pour deux raisons :
- Pour les applications d’acquisition de données, un flux 32 bits est plus adapté. Le cœur IP Xillybus ne transporte pas les données aussi efficacement quand la largeur du mot de données n’est que de 8 ou 16 bits.
- /dev/xillybus_write_8 est utilisé pour la communication I2C avec le capteur, comme décrit dans la partie suivante de ce tutoriel.
Quand @wait_for_frame est à l’état haut, rien n’est écrit dans la FIFO, pour l’une de ces deux raisons : soit le fichier de périphérique n’est pas ouvert, soit il est ouvert mais le début d’une nouvelle image n’a pas encore été atteint.
Quand HSYNC du capteur est à l’état haut, les signaux de données contiennent des pixels valides. L’expression « sample_valid && hsync » combine deux critères : quand @sample_valid est à l’état haut, @hsync et @D contiennent des valeurs valides. Ainsi, si @hsync est à l’état haut, la valeur de @D est copiée dans une partie de @dataword. Et si @D est copié dans la dernière partie de @dataword (c’est-à-dire quand @byte_position vaut 3), @fifo_wr_en passe à l’état haut. @dataword est alors écrit dans la FIFO. Plus précisément, l’expression pour @fifo_wr_en est :
fifo_wr_en <= !fifo_has_been_full;
Ainsi, si @fifo_has_been_full est à l’état haut, rien n’est écrit dans la FIFO, comme déjà mentionné plus haut.
Relation entre le code Verilog et les vraies broches
Le code Verilog ci-dessus utilise le port bidirectionnel (inout) nommé J6. Mais comment les connexions de ce port aboutissent-elles sur le connecteur à broches ? La réponse se trouve dans xillydemo.xdc. Ce fichier fait partie du projet Vivado qui génère le flux de bits (dans le répertoire « vivado-essentials »).
xillydemo.xdc contient diverses informations nécessaires au bon fonctionnement du FPGA en tant que composant électronique. Ce fichier contient notamment les lignes suivantes :
[ ... ]
## 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
[ ... ]
La première ligne indique que le signal J6[0] doit être connecté à U22. C’est une position sur le boîtier physique du FPGA. D’après les schémas de la Smart Zynq, cette broche du FPGA est reliée à la première broche du connecteur. Les positions des autres ports sont définies de la même manière.
Conclusion
Cette page a montré comment obtenir les données de pixels de l’OV7670 et les envoyer à l’hôte à l’aide du cœur IP Xillybus.
La partie suivante de ce tutoriel explique comment utiliser le cœur IP Xillybus pour modifier les registres du capteur avec le protocole SCCB (c’est-à-dire I2C). Cela permet de changer les paramètres de la caméra. En particulier, c’est nécessaire pour obtenir une image avec des couleurs correctes.



