Differenze tra le versioni di "OV7670"

Da raspibo.
Jump to navigation Jump to search
(OV7670)
 
 
Riga 84: Riga 84:
 
== Lato Arduino Due ==
 
== Lato Arduino Due ==
  
Lo sketch dell'Instructable fondamentalmente fa le seguenti cose
+
Lo sketch dell'Instructable fa le seguenti cose:
Imposta i registri di OV7670 in modo che sia QVGA in formato YCbCr422
+
* Imposta i registri di OV7670 in modo che sia QVGA in formato YCbCr422
Manda un segnale "*RDY*" e memorizza su una matrice un byte no e uno sì dei pin D0-D7
+
* Manda un segnale "*RDY*" e memorizza su una matrice un byte no e uno sì dei pin D0-D7
Quando l'immagine è completa, manda via seriale ogni singolo byte della matrice
+
* Quando l'immagine è completa, manda via seriale ogni singolo byte della matrice
  
 
La modifica più importante è stata quella di mandare alternati il valore di Y e quello di CbCr. Nel frattempo avrebbe anche aspettato dallo script lato computer un segnale per dare il via alla lettura oppure un comando per leggere o scrivere su uno dei registri. Successivamente ho anche eliminato tutte le configurazioni che non cambiavano significativamente l'immagine e che probabilmente hanno a che fare con cose come il bilanciamento del bianco.
 
La modifica più importante è stata quella di mandare alternati il valore di Y e quello di CbCr. Nel frattempo avrebbe anche aspettato dallo script lato computer un segnale per dare il via alla lettura oppure un comando per leggere o scrivere su uno dei registri. Successivamente ho anche eliminato tutte le configurazioni che non cambiavano significativamente l'immagine e che probabilmente hanno a che fare con cose come il bilanciamento del bianco.

Versione attuale delle 14:50, 20 feb 2021

Diversi anni fa comprai questa OV7670 con l'illusione che sarebbe stato divertente farla funzionare con un Arduino. Il mio obiettivo era una cosa molto semplice, come un sensore che scatta foto quando triggerato e che salvasse l'immagine su una SD. Purtroppo non si è rivelato possibile per diverse limitazioni delle schede Arduino più economiche: una scheda come l'Uno non ha abbastanza RAM per memorizzare un'immagine intera alle risoluzioni più alte, ma anche la più dotata Due ha bisogno di una gabola per riuscire ad avere i colori.

OV7670.JPG

Per ovviare a questo occorre una memoria FIFO, normalmente integrata nella scheda assieme a OV7670, ma la mia non l'aveva e acquistarla a parte ha solo comportato avere due problemi (come far funzionare la cam E come far funzionare la FIFO) al posto di uno.

Fra tutte le schede ho scelto l'Arduino Due perché ha abbastanza memoria per tenere un'intera immagine in QVGA (320x240) in bianco e nero e perché ha livelli logici a 3,3V, il che permette di collegarlo a OV7670 senza bisogno di un partitore di tensione.

Le Guide

Instructables

Il primo passo per far funzionare una qualsiasi cosa è cercare guide e datasheet. La prima guida in cui ci si imbatte è questa: https://www.instructables.com/How-to-Connect-OV7670-to-Arduino-Due/

Questo Instructable è stato il mio punto di partenza. Inizia con un semplice schema per il wiring:

DUE		OV7670
3.3V		VDD
GND		GND
SCL(21)	SDIOC
SDA(20)	SDIOD
52		VSYNC
32		PCLK
7		XCLK
44		D7
45		D6
46		D5
47		D4
48		D3
49		D2
50		D1
51		D0
33		RESET
GND		PWDN

A questo schema -senza nessun commento- manca il collegamento di HREF. Non verrà utilizzato e non è necessario collegarlo, ma nella guida non è specificato. Nel resto della guida si spiega come il Due debba generare un clock e come scrivere nei registri di OV7670 tramite I²C. Viene fornito anche uno sketch che sembrerebbe funzionare e che in teoria leggerebbe l'immagine in bianco e nero per poi mandare l'output su seriale. Putroppo i programmi per decodificare questo segnale sono su un sito irraggiungibile. Ero quindi a un punto morto.

Ok ok non del tutto, nei commenti (ahimè l'ho scoperto quando ormai avevo già riscritto parte del codice) c'è una soluzione fatta in Processing che funziona bene: https://pastebin.com/weAEcrQG

Come funziona OV7670 e come non aver paura di YCbCr

http://embeddedprogrammer.blogspot.com/2012/07/hacking-ov7670-camera-module-sccb-cheat.html

Trovandomi un po' spiazzato, ho pensato che forse avrei dovuto approfittarne per riscrivere da capo tutto quanto, così da far funzionare finalmente la cam e allo stesso tempo imparare qualcosa di nuovo. Lo sketch dell'instructable precedente era per me un po' ostico da leggere, con molti riferimenti a me sconosciuti ai registri interni dell'Arduino Due e della cam. Punto di partenza è stato questo sito che spiega diverse cose sul funzionamento di OV7670.

Qui scopriamo che:

VSYNC è il segnale di sincronia verticale (HREF quello orizzontale, ma non lo useremo)
PCLK è il clock del pixel (in realtà -mezzo- pixel)
XCLK è il clock esterno, OV7670 ha bisogno di un clock esterno per funzionare: se volete scansionare gli indirizzi I²C per scoprire quello della cam, dovrete prima collegare un clock a XCLK

Un pixel su un monitor è composto da due informazioni: la posizione e il colore. La posizione è espressa in coordinate X e Y mentre il colore dalla luminosità dei colori primari R=Red/Rosso, G=Green/Verde e B=Blue/Blu. Normalmente ogni colore è espresso con 8 bit, da cui il nome RGB888. Purtroppo OV7670 invece comunica i pixel sequenzialmente a partire da 0,0 a 320,240 riga per riga e come formati di colore ha RGB565, RGB555 e RGB444 oppure YCbCr422. I primi tre semplicemente mandavano 5 bit di R, 6 di G e 5 di B oppure rispettivamente 5 oppure 4 per ogni colore. L'ultimo formato invece era completamente diverso e necessita di un approfondimento.

Nel format YCbCr l'immagine è separata in tre canali: la luma (Y) ossia la luminosità (se prendessimo solo Y come valore per RGB allora vedremmo l'immagine in bianco e nero) e la croma, ovvero i livelli di differenza da Y del blu (Cb) e del rosso (Cr). Ognuno di questi occupa un byte di informazione.

Ogni falling del PCLK la cam invia un byte di informazione in modo parallelo sui pin da D0 a D7. Come fa quindi a comunicare un pixel ogni colpo PCLK? Semplice: non lo fa. Manda i valori alternati in questo modo: Cb0,Y0,Cr0,Y1,Cb2,Y2,Cr2,Y3... La definizione di "Pixel Clock" è infatti fuorviante. Sarebbe "Half Pixel Clock" semmai.

Se bastasse l'immagine in bianco e nero allora basterà memorizzare un PCLK no e uno sì e avremo la luma. Questo è quello che faceva in teoria lo sketch del primo instructable. Ovviamente a me non bastava (a parte il fatto che comunque non funzionava), volevo anche i colori. Cb e Cr sono comuni per due pixel alla volta. Sempre secondo il link alla guida sopra:

pixel 0 = Y0 Cb0 Cr0
pixel 1 = Y1 Cb0 Cr0
pixel 2 = Y2 Cb2 Cr2
pixel 3 = Y3 Cb2 Cr2

Questo semplificava molto le cose. Invece di mandare la luma via seriale e scartare la croma, avrei mandato alternativamente prima la luma del frame e successivamente la croma. Avrebbe causato degli strani effetti nelle immagini in movimento ma sarebbe stato meglio di prima. Perché non mandare contemporaneamente l'informazione? L'Arduino Due ha solo 96KB di SRAM, la luma o le crome da sole sono 320*240*1 byte = 76.8KB l'una. Mandare i dati via seriale alla fine della riga poteva essere una soluzione ma causava numerosi glitch sull'immagine risultante.

La guida è chiara, ma purtroppo manca uno sketch per il Due e relativo programma per processare l'immagine da seriale.

Il datasheet

http://web.mit.edu/6.111/www/f2015/tools/OV7670app.pdf

In questo datasheet sono elencati i registri interni di OV7670. Nello sketch dell'Instructable sono impostati nella maggior parte dei casi senza spiegare cosa servono. Gli stessi valori li ho trovati anche in quest'altra guida: http://embeddedprogrammer.blogspot.com/2012/07/hacking-ov7670-camera-module-sccb-cheat.html e dal relativo codice su github: https://github.com/indrekluuk/LiveOV7670 per il collegamento della cam a un display. Da quel che mi è parso di capire, sono i valori che il driver di Linux mette di default e usa per le varie impostazioni della cam (ad es. per passare da QVGA ad altri formati come il QCIF). Molto interessante ma a questo punto la mia missione era già diventata qualcos'altro, ovvero fare in modo che questi registri potessero essere modificati in modo interattivo.

La soluzione

L'obiettivo è quindi creare un programma che leggesse la seriale e restituisse l'immagine e allo stesso tempo fare in modo che si potessero leggere e scrivere i registri della cam.

Lato Arduino Due

Lo sketch dell'Instructable fa le seguenti cose:

  • Imposta i registri di OV7670 in modo che sia QVGA in formato YCbCr422
  • Manda un segnale "*RDY*" e memorizza su una matrice un byte no e uno sì dei pin D0-D7
  • Quando l'immagine è completa, manda via seriale ogni singolo byte della matrice

La modifica più importante è stata quella di mandare alternati il valore di Y e quello di CbCr. Nel frattempo avrebbe anche aspettato dallo script lato computer un segnale per dare il via alla lettura oppure un comando per leggere o scrivere su uno dei registri. Successivamente ho anche eliminato tutte le configurazioni che non cambiavano significativamente l'immagine e che probabilmente hanno a che fare con cose come il bilanciamento del bianco.

Vediamo alcuni dettagli dello sketch.

Generare un segnale di clock con Arduino

Arduino Due ha un generatore di PWM con i quali è possibile creare un segnale di clock di almeno 8MHz sufficienti per il XCLK di OV7670.

 int32_t PWM_pin = digitalPinToBitMask(XCLK);
 REG_PMC_PCER1 = 1 << 4;     // Enable peripheral ID 36 (PWM) in the peripheral clock enable register - see 28.15.23
 REG_PIOC_PDR |= PWM_pin;    // Allow peripheral control for PWM_pin
 REG_PIOC_ABSR |= PWM_pin;   // Select peripheral B
 REG_PWM_CPRD6 = 8;          // Period: 84 MHz / 8 = 10.5 MHz - see 38.6.2.2 of datasheet
 REG_PWM_CDTY6 = 4;          // Duty Cycle: 8 / 4  = 0.5
 REG_PWM_ENA = 1 << 6;       // Enable PWML6 (pin 7) - see 38.5.1 and 38.7.5 of datasheet for more info

Gran parte delle informazioni su come funziona questo codice è spiegato su questa guida: https://nhoffmanresearch.com/index.php/12-arduino-trap-control e relativo datasheet del SAM3X9E: http://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-11057-32-bit-Cortex-M3-Microcontroller-SAM3X-SAM3A_Datasheet.pdf

I registri

Tramite I²C è possibile leggere lo stato di un registro o scrivere un nuovo valore. La lista dei registri è elencata sul datasheet, tuttavia alcuni, descritti come "Reserved", non sono documentati. La prima cosa quando si accende la cam è settare i registri in modo che trasmetta le informazioni nel formato QVGA e YCbCr, inoltre sono abilitate alcune funzioni come l'autobilanciamento del bianco. Una volta impostate queste cose, si può cominciare a catturare le immagini.

Loop

Una delle cose che non riuscivo a far funzionare era centrare l'immagine. Essendo scollegato HREF il punto di inizio era arbitrario, e questo comportava che l'immagine fosse fuori quadro. Ho cambiato un po' questa filosofia e ho fatto in modo che l'Arduino Due aspettasse un segnale sulla seriale prima di iniziare a trasmettere. Il segnale è composto da due byte, se sono entrambi 0xD0 allora invia una nuova immagine, se invece il primo è un altro valore, viene inviato il registro corrispondente a quel valore, se sono entrambi diversi il registro corrispondente al primo valore viene aggiornato con il secondo valore. Non esiste un registro 0xD0, quindi possiamo usare tale byte per segnalare quando siamo pronti a ricevere una nuova immagine.

In questo modo è possibile alterare in diretta l'immagine, cambiare luminosità, formato e anche scoprire a cosa servono i registri "Reserved".

Cattura dell'immagine

La cattura è fatta in due passate per questioni di spazio sulla RAM. Per prima cosa leggiamo Y e poi CbCr. Se non siamo interessati al colore, possiamo commentare la seconda chiamata alla funzione captureImg a cui ho aggiunto un terzo parametro, il bool chroma, per specificare se salvare i bit pari o quelli dispari. Ogni PCLK basso viene aggiunto un valore (Y oppure CbCr) alla matrice che rappresenta le righe e le colonne. Durante la lettura il tempo è critico, per cui occorrono alcuni accorgimenti per sveltire le operazioni.

noInterrupts();

Questo comando disabilita gli interrupt. Non mi sembra che abbia nessun effetto benefico, ma male non fa.

REG_PIOB_PDSR & (1 << 21)
REG_PIOD_PDSR & (1 << 10)

Purtroppo le tanto comode funzioni digitalRead() di Arduino sono troppo lente per l'output di OV7670. Leggere direttamente il Pin Data Status Register (PDSR) di SAM3X9E è molto più veloce. Il bit 21 del PortB corrisponde al pin 52, che è quello dove abbiamo attaccato VSYNC, il bit 10 del PortD è il pin 32, dove abbiamo attaccato PCLK. Non è immediato convertire il pin in registro, ma ci sono moltissimi schemi con il pinout del Due, con descritto per ogni pin il Port e il bit.

(REG_PIOC_PDSR & 0xFF000) >> 12

Invece il PortC sono tutti i pin dov0 abbiamo collegato da D0 a D7. Memorizzando lo stato del Port e applicando un filtro in modo che restituisca solo i valori che ci interessano (PortC dalla 12 alla 19), non dobbiamo subire la costrizione di leggere lo stato di un pin alla volta.

Allo stesso tempo non è possibile usare la seriale di programmazione per trasmettere la matrice, occorre usare la porta nativa (Native) e invece di Serial, la funzione SerialUSB.write. Non c'è bisogno di inizializzarla con un baud rate perché andrà sempre alla velocità massima possibile. Questo dualismo mi ha permesso anche di fare alcune cose interessanti, come usare la porta nativa per la comunicazione con il programma che elabora le immagini e la porta di programmazione per mandare dei messaggi di debug.

Lato Python

Sul computer ho scritto un piccolo script che pettinasse l'output dell'Arduino.

Per prima cosa manda un segnale di due byte. Se il segnale è 0xD0 0xD0 allora si mette in attesa di larghezza*altezza byte. Una volta ricevuti e messi in ordine (ricordate, un dato di croma vale per due pixel, mentre la luma c'è per ognuno) occorre usare alcune formule per trasformare i dati in RGB. Ho usato questa formula, che si può trovare nelle guide già citate sopra:

R = int(max(0, min(255,Y + 1.40200 * (Cr - 0x80))))
G = int(max(0, min(255,Y - 0.34414 * (Cb - 0x80) - 0.71414 * (Cr - 0x80))))
B = int(max(0, min(255,Y + 1.77200 * (Cb - 0x80))))

Lo script readYUV422.py restituisce quindi una bitmap. Gli altri script saveYUV422.py e showYUV422.py rispettivamente salvano l'immagine con PIL oppure la mostrano con pygame. L'altro script, tkinterface.py, invece è un'interfaccia fatta con tkinter con cui possiamo interagire direttamente con i registri di OV7670. Per risolvere il problema delle azioni concorrenti, ho usato un thread che facesse il refresh dell'immagine. La porta usata è /dev/ttyACM0, ma è possibile cambiarla aggiungendo un argomento quando si lancia lo script.

OVCAM.JPG

I risultati

Non è andato sempre tutto bene al primo colpo. Soprattutto all'inizio, quando non avevo ancora capito come leggere l'output, la cam ha dato degli output interessanti. Eccone alcuni. Glitches.jpg

Il repo

Tutta la roba, con tutte le istruzioni e quant'altro, lo trovate nel repository su: https://github.com/oloturia/ovcam