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 accéder aux registres du capteur de caméra OV7670 avec une carte Smart Zynq. Il fait suite à la page précédente, qui décrit comment recevoir les données vidéo du capteur.
L’OV7670 dispose d’une interface Serial Camera Control Bus (SCCB) permettant de configurer les paramètres du capteur. Le protocole SCCB est défini dans le document d’Omnivision intitulé « OmniVision Serial Camera Control Bus (SCCB) Functional Specification ». Ce protocole est compatible avec le protocole I2C bien connu.
La principale motivation pour accéder aux registres du capteur est d’obtenir une image aux couleurs correctes. Il y a cependant d’autres avantages à contrôler la caméra par ses registres : contrôler le courant électrique des signaux numériques, contrôler et éventuellement désactiver les réglages automatiques de luminosité et de couleur, demander une mire de test, et bien plus encore.
Le processeur du Zynq possède deux unités intégrées qui implémentent chacune un maître de bus I2C (bus master). On pourrait utiliser l’une de ces unités pour communiquer avec le capteur. Malheureusement, un essai avec ces unités I2C intégrées a montré qu’elles ne fonctionnent pas bien avec l’OV7670. La raison est probablement le niveau de bruit important sur les fils. L’absence de résistances de rappel dédiées (pull-up) est une autre cause possible (les résistances de rappel internes du FPGA ont été utilisées à cette fin).
Comme les unités I2C intégrées ne peuvent pas être utilisées, on ajoute plutôt un module Verilog au projet. Cette logique est conçue pour mieux supporter les signaux bruités.
Modifications du projet Vivado
Les instructions ci-dessous se basent sur le projet Vivado déjà créé conformément à la page précédente.
Téléchargez l’implémentation Verilog du maître de bus I2C (bus master) depuis ce lien. Copiez ce fichier dans le répertoire verilog/src/. Ajoutez ensuite le fichier au projet Vivado : cliquez sur File > Add Sources… et choisissez « Add or create design sources ». Cliquez ensuite sur Next. Cliquez sur le bouton « Add Files » et choisissez le fichier nommé « i2c_if.v » dans le répertoire verilog/src/. Cliquez enfin sur le bouton « Finish ».
Le processeur pilote ce module à l’aide de deux flux Xillybus. Ouvrez verilog/src/xillydemo.v dans un éditeur de texte. Supprimez la partie du code intitulée « PART 3 ». Insérez à la place l’extrait de code suivant :
/*
* PART 3
* ======
*
* The instantiation of i2c_if demonstrates how to use two Xillybus
* streams to implement an I2C interface with the camera sensor module.
*
*/
i2c_if i2c_if_ins
(
.bus_clk(bus_clk),
.quiesce(quiesce),
.i2c_clk(J6[15]),
.i2c_data(J6[14]),
.user_w_write_8_open(user_w_write_8_open),
.user_w_write_8_wren(user_w_write_8_wren),
.user_w_write_8_data(user_w_write_8_data),
.user_w_write_8_full(user_w_write_8_full),
.user_r_read_8_open(user_r_read_8_open),
.user_r_read_8_rden(user_r_read_8_rden),
.user_r_read_8_data(user_r_read_8_data),
.user_r_read_8_empty(user_r_read_8_empty),
.user_r_read_8_eof(user_r_read_8_eof)
);
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.
Notez que le module i2c_if se connecte à J6[15] et J6[14]. Ces ports sont reliés, via le connecteur à broches, à SCL et SDA du module de caméra.
Modification des registres du capteur de caméra
Le programme qui envoie les commandes I2C à la caméra peut être téléchargé depuis ce lien. Copiez ce programme dans le système de fichiers de Xillinux (par exemple avec scp, ou en copiant directement le fichier sur la carte TF).
Placez-vous dans le répertoire où se trouve le fichier. Tapez la commande suivante dans un terminal pour compiler :
# gcc -Wall -O3 -o i2c i2c.c
Cette commande doit se terminer sans afficher de message.
Voici comment exécuter le programme. La sortie qu’il produit normalement est également affichée :
# ./i2c
Camera sensor's product ID is 0x7673
Reg 0x3d = 0x88 (to be altered)
Reg 0xb0 = 0x00 (to be altered)
Reg 0x6f = 0x9a (to be altered)
Wrote 0x3d = 0x81
Wrote 0xb0 = 0x84
Wrote 0x6f = 0x9f
Le programme commence par lire les registres qui contiennent l’identifiant produit (product ID) du capteur. Il existe différentes versions du capteur OV7670. Ce tutoriel se base sur le capteur qui s’identifie avec le numéro 0x7673 comme identifiant produit. Il est peu probable de rencontrer un capteur OV7670 avec un identifiant produit différent. Si l’identifiant est différent, il est possible qu’un autre modèle de capteur soit monté sur le module de caméra.
Le programme effectue les modifications minimales nécessaires pour obtenir des couleurs correctes sur l’image du capteur. Pour cela, trois registres sont modifiés.
Avant de modifier quoi que ce soit, le programme lit la valeur actuelle des registres. Ce sont les trois lignes suivantes dans la sortie du programme. Il écrit ensuite les valeurs correctes dans ces registres.
Signification de ces trois registres
Malheureusement, la signification des registres de ce capteur n’est que partiellement documentée. Beaucoup de registres de l’OV7670 sont définis comme « réservés ». Il n’existe donc aucune explication sur la nécessité de certaines de ces modifications. On trouve de nombreuses sources d’information sur les registres de l’OV7670. Le meilleur endroit pour chercher des indices est le pilote Linux du capteur : ov7670.c. En particulier, ov7670_default_regs[] (une variable définie dans le pilote) contient une foule d’indications précieuses.
Voici les explications disponibles sur les raisons pour lesquelles le programme i2c.c modifie ces trois registres. Malheureusement, les raisons de certaines de ces modifications ne sont pas connues, même s’il est clair qu’elles sont nécessaires.
- COM13 (0x3d) : d’abord, le bit 0 de ce registre passe à '1'. En conséquence, les positions de U et V sont échangées dans la sortie du capteur. Cela est nécessaire pour que le format de sortie soit UYVY, le format attendu par mplayer et d’autres logiciels. En outre, le bit 3 de ce registre passe à '0'. La signification de ce bit n’est pas documentée dans la documentation du capteur.
- Registre réservé (0xb0) : aucun document ne décrit ce registre.
- AWBCTR0 (0x6f) : ce registre est lié à la balance des blancs du capteur. Selon le guide d’implémentation (Implementation Guide) de l’OV7670, écrire la valeur 0x9f dans ce registre provoque deux changements : le mode AWB avancé est activé et le gain de couleur maximal passe de 2x à 4x. Si ce registre n’est pas modifié, la balance des blancs ne fonctionne pas correctement.
Écriture dans d’autres registres
Voici la définition de @writelist, que l’on trouve près du début du programme i2c.c :
static const struct {
int addr;
int value;
} writelist [] = {
{ 0x3d, 0x81 }, // COM13, swap UV, turn off reserved bit 3
{ 0xb0, 0x84 },
{ 0x6f, 0x9f }, // AWBCTR0, crucial for white balance
{ -1, -1 }, // Terminate
};
Les éléments du tableau @writelist sont composés de deux nombres : le premier est l’adresse du registre, le second est la valeur à écrire dans ce registre.
Par exemple, le premier élément est { 0x3d, 0x81 }. Cela signifie que la valeur 0x81 est écrite dans COM13, dont l’adresse est 0x3d.
Le dernier élément du tableau doit être { -1, -1 }.
Réduction du courant de pilotage du capteur
Lorsque les fils entre le capteur de caméra et la carte Smart Zynq sont trop longs, il est possible que l’image vidéo soit instable : l’image saute et des rayures vertes et violettes traversent l’écran. Cela arrive souvent à cause de la diaphonie (crosstalk) entre les fils.
On peut peut-être résoudre ce problème en réduisant le courant électrique que le capteur applique sur les fils. Pour cela, écrivez la valeur 0x00 dans COM2. L’adresse de ce registre est 0x09.
Autrement dit, modifiez la définition de @writelist comme suit :
static const struct {
int addr;
int value;
} writelist [] = {
{ 0x09, 0x00 }, // Drive current to 1x level
{ -1, -1 }, // Terminate
};
Compilez ensuite ce programme et exécutez-le comme précédemment.
Autres possibilités
La documentation du capteur (en particulier l’OV7670/OV7171 CMOS VGA (640x480) CameraChip Implementation Guide) donne des informations sur plusieurs autres registres. Comme déjà mentionné, le pilote du noyau Linux peut aussi fournir des indications importantes.
Rappelons que, comme expliqué dans la première partie de ce tutoriel, on peut réinitialiser le capteur de caméra avec la commande suivante :
# echo 1 > /dev/xillybus_write_32
Tous les registres retrouvent leurs valeurs par défaut après cette commande.
Affichage des valeurs de tous les registres
Voici un extrait de la fonction main() du programme i2c.c :
if (0) { // Change this in order to print out registers instead
for (i=0; i<=0xc9; i++) {
i2c_read(i, &value);
printf("Reg 0x%02x = 0x%02x\n", i, value);
}
return 0;
}
Cette partie a pour but d’afficher les valeurs de tous les registres. Elle n’est normalement jamais atteinte à cause de la condition « if (0) ». Remplacez-la par « if (1) » pour obtenir la liste des valeurs de tous les registres.
L’affichage de tous les registres devrait prendre moins d’une seconde. Si l’exécution du programme marque une pause ou se bloque, c’est qu’il y a des erreurs de communication sur le bus I2C. Dans ce cas, la sortie du programme peut être incorrecte ou incomplète. Relancez le programme jusqu’à ce qu’il s’exécute rapidement et sans accroc.
Un relevé de tous les registres peut être téléchargé via ce lien. Ce relevé reflète l’état des registres lorsque le capteur produit une image aux couleurs correctes. Le relevé des valeurs par défaut (juste après la réinitialisation du capteur) peut être téléchargé ici. Notez que la caméra modifie en permanence certains registres en raison du contrôle automatique de la luminosité, de la balance des blancs, etc.
Déroulement d’une opération d’écriture I2C
Cette section suppose que vous connaissez les bases du protocole I2C.
Le programme i2c.c communique avec le module i2c_if.v dans le FPGA par l’intermédiaire de deux flux Xillybus : /dev/xillybus_write_8 et /dev/xillybus_read_8.
Une opération d’écriture I2C se déroule comme suit :
- Lorsque l’hôte ouvre /dev/xillybus_write_8, le FPGA génère une condition de start (start condition) sur le bus I2C.
- Les octets écrits dans ce fichier de périphérique apparaissent sur les fils I2C (sans aucune modification).
- Lorsque l’hôte ferme /dev/xillybus_write_8, le FPGA génère une condition de stop (stop condition).
Ces étapes sont implémentées par la fonction i2c_write() :
static void i2c_write(int addr, unsigned char data) {
unsigned char sendbuf[3] = { i2c_addr << 1, addr, data };
allwrite(sendbuf, sizeof(sendbuf));
}
Cette fonction prépare un tampon de 3 octets :
- L’adresse I2C, qui vaut 0x42 pour une opération d’écriture.
- L’adresse du registre.
- La valeur à écrire dans le registre.
Le FPGA envoie ces trois octets au capteur via le bus I2C. La fonction i2c_write() ouvre /dev/xillybus_write_8, écrit les données du tampon puis ferme le fichier.
Selon le protocole I2C, le destinataire doit acquitter chaque octet envoyé sur le bus : pour chaque octet (qui se compose de 8 bits), un neuvième bit est prévu à cet effet. Ce neuvième bit occupe un intervalle de temps spécial pendant la transmission. Le côté qui a reçu l’octet doit mettre la ligne SDA à '0' pendant cet intervalle afin de confirmer la bonne réception de l’octet.
Si le capteur de caméra ne répond pas ainsi, le module i2c_if dans le FPGA refuse d’accepter d’autres octets via le fichier de périphérique. Cela ne provoque pas d’erreur, mais l’appel à close() ne se termine qu’après un délai de 1000 ms. La raison est que le pilote Xillybus attend que toutes les données restantes aient atteint le FPGA avant de fermer le fichier. Mais si l’esclave (slave) I2C n’a pas acquitté un octet, le FPGA refuse d’accepter l’octet suivant. Dans ce cas, le pilote attend 1000 ms, puis ferme quand même le fichier et ajoute ce message au journal du noyau (kernel log) :
Timed out while flushing. Output data may be lost.
Les messages du journal du noyau peuvent être consultés avec la commande « dmesg ».
En résumé, si un appel à i2c_write() met une seconde à se terminer, c’est probablement parce que le capteur n’a pas répondu correctement aux opérations sur le bus I2C. Il est possible que le capteur soit mal connecté au FPGA, ou même pas connecté du tout.
Déroulement d’une opération de lecture I2C
L’opération de lecture est plus compliquée, car elle se compose de deux opérations distinctes :
- Une opération d’écriture, mais sans octet de données. Le but de cette action est de transmettre l’adresse du registre à l’esclave (slave) I2C.
- Une opération de lecture, au cours de laquelle la valeur du registre est envoyée de l’esclave vers le maître.
La fonction i2c_read() est présentée ci-dessous :
static void i2c_read(int addr, unsigned char *data) {
int fdr;
unsigned char cmdbuf[2] = { i2c_addr << 1, addr };
unsigned char dummybuf[2] = { (i2c_addr << 1) | 1, 0 };
allwrite(cmdbuf, sizeof(cmdbuf));
// We open xillybus_read_8 only now. Had it been open during the first
// operation, there would have been a restart condition rather than a
// stop condition after the first command.
fdr = open("/dev/xillybus_read_8", O_RDONLY);
if (fdr < 0) {
perror("Failed to open /dev/xillybus_read_8 read-only");
exit(1);
}
allwrite(dummybuf, sizeof(dummybuf));
allread(fdr, data, sizeof(*data));
close(fdr);
}
Cette fonction commence par envoyer deux octets (@cmdbuf) à l’esclave :
- L’adresse I2C, c’est-à-dire 0x42, comme pour une opération d’écriture.
- L’adresse du registre que l’on souhaite lire.
i2c_read() ouvre ensuite /dev/xillybus_read_8. Notez que ce n’est pas allread() qui fait cette ouverture, contrairement à allwrite().
Ensuite, i2c_read() écrit deux octets (@dummybuf) sur le bus à l’aide d’allwrite() :
- L’adresse I2C, qui vaut 0x43 et indique à l’esclave qu’il s’agit d’une opération de lecture sur le bus.
- Un octet contenant zéro. Le module i2c_if ignore le contenu de cet octet.
Le module i2c_if dans le FPGA examine le bit 0 du premier octet qu’il reçoit. Il en déduit si une opération d’écriture ou de lecture doit avoir lieu sur le bus. Si une lecture est demandée, le contenu des autres octets est ignoré. Ces octets servent uniquement à indiquer au FPGA combien d’octets doivent être reçus.
Le module i2c_if lit le nombre d’octets demandé sur le bus et les envoie à l’hôte via /dev/xillybus_read_8. Ce fichier de périphérique doit être ouvert avant que la lecture sur le bus ne soit déclenchée par l’écriture de @dummybuf. Ensuite, allread() lit la valeur du registre. allread() n’ouvre ni ne ferme le fichier, car celui-ci doit être ouvert dès le début.
Restart sur le bus
Cette section ne concerne pas le capteur OV7670. Ces informations peuvent toutefois être utiles si i2c_if est utilisé avec un autre esclave (slave).
Notez que i2c_read() appelle deux fois allwrite(). À chaque appel, /dev/xillybus_write_8 est ouvert puis fermé. Il en résulte une condition de start (start condition) avant l’envoi des données et une condition de stop (stop condition) après.
Autrement dit, une condition de stop apparaît après l’envoi de l’adresse du registre à l’esclave, puis une condition de start apparaît avant que le maître ne commence l’opération de lecture.
Le capteur de caméra attend cette séquence. En revanche, d’autres composants électroniques qui jouent le rôle d’esclaves I2C ne fonctionnent pas correctement si l’on tente une lecture de cette façon : ils oublient l’adresse du registre lorsqu’ils reçoivent une condition de stop. Il est donc nécessaire de générer une condition de redémarrage (restart condition) entre la première et la deuxième action sur le bus.
Le module i2c_if prend en charge cette possibilité : si /dev/xillybus_write_8 est fermé puis rouvert alors que /dev/xillybus_read_8 reste ouvert en continu, une condition de redémarrage (restart condition) est générée sur le bus à la place. Autrement dit, si l’esclave exige une condition de redémarrage, l’appel à allwrite() doit être déplacé. i2c_read() se présentera alors ainsi :
fdr = open("/dev/xillybus_read_8", O_RDONLY);
if (fdr < 0) {
[ ... ]
}
allwrite(cmdbuf, sizeof(cmdbuf));
allwrite(dummybuf, sizeof(dummybuf));
allread(fdr, data, sizeof(*data));
close(fdr);
Encore une fois, ce code ne convient pas pour l’OV7670.
Conclusion
Il est possible d’utiliser le cœur IP (IP core) Xillybus pour accéder aux registres du capteur de caméra. Un module supplémentaire est nécessaire pour s’interfacer avec le bus I2C : i2c_if. Ce module est également utile pour communiquer avec d’autres esclaves I2C.
Les informations disponibles sur les registres du module de caméra OV7670 sont malheureusement insuffisantes. Il peut donc être nécessaire de chercher des solutions sur Internet ou de s’aider du pilote Linux du capteur.