Kategória: Arduino.
OV7670
Reklámként dobta fel nekem a pár dolláros, OV7670 típusú Arduino kamerát, melynek nem tudtam ellenállni. Végül ez lett a legbonyolultabb Arduino eszköz, mellyel valaha találkoztam. A kód megértéséhez az Arduino chipjének legmélyebb bugyraiba kellett beleásnom magamat, és egy igen jelentős részét mind a mai napig nem értem.
A másik, amit érdemes rögtön az elején tisztázni: ezzel nem lehet minőségi fotókat készíteni. Élesben szinte biztosan nem tudjuk használni (pl. arra, hogy ha a mozgásérzékelő mozgást érzékel, akkor készítsen automatikusan pár fotót, amit mondjuk egy kártyára ment), és az összetettsége miatt oktatási célra is alkalmatlan. Igazából egy dologra jó csak: ha valaki szereti a bütykölős kihívásokat, annak megfelelő elfoglaltságot nyújt elég hosszú időre.
A következő leírás alapján készítettem el a projektet: https://create.arduino.cc/projecthub/techmirtz/visual-capturing-with-ov7670-on-arduino-069ebb
Bekötés
Maga az eszköz 18 lábat tartalmaz, és a bekötéshez a rengeteg kábelen kívül szükségünk van két 4,7kΩ és két 10 kΩ ellenállásra. Célszerű külön áramforrást biztosítani az eszköznek, és a külön áramforrás - pólusát összekötni az Arduino GND-vel. Bekötés:
- OV7670 3v3 → 3,3 V
- OV7670 GND → GND
- OV7670 SIOC → Arduno A5
- OV7670 SIOC → 10 kΩ → 3,3 V
- OV7670 SIOD → Arduno A4
- OV7670 VSYNC → Arduino D3
- OV7670 HREF → Arduino D8
- OV7670 PCLK → Arduino D2
- OV7670 XCLK → 4,7 kΩ → Arduino D11
- OV7670 XCLK → 4,7 kΩ (ez egy másik ellenállás) → GND
- OV7670 D7 → Arduino D7
- OV7670 D6 → Arduino D6
- OV7670 D5 → Arduino D5
- OV7670 D4 → Arduino D4
- OV7670 D3 → Arduino D3
- OV7670 D2 → Arduino D2
- OV7670 D1 → Arduino D1
- OV7670 D0 → Arduino D0
- OV7670 RESET → 3,3 V
- OV7670 PWDN → GND
Az alábbi ábra (mely mindkét fent hivatkozott oldalon megtalálható) a bekötést illusztrálja:
Az említett oldalakon azt is megtaláljuk, hogy melyik láb micsoda.
Arduino kód
Az Arduino kódja lényegesen hosszabb, és nagyságrendekkel bonyolultabb, mint amihez hozzászoktunk, íme:
#include <stdint.h> #include <util/twi.h> #include <util/delay.h> #include <avr/io.h> #include <avr/pgmspace.h> #define F_CPU 16000000UL #define COM7_RESET 0x80 #define COM8_AEC 0x01 #define COM8_AECSTEP 0x40 #define COM8_AGC 0x04 #define COM8_AWB 0x02 #define COM8_BFILT 0x20 #define COM8_FASTAEC 0x80 #define COM11_EXP 0x02 #define COM11_HZAUTO 0x10 #define COM13_UVSAT 0x40 #define COM15_R00FF 0xc0 #define COM16_AWBGAIN 0x08 #define REG_AEB 0x25 #define REG_AECH 0x10 #define REG_AEW 0x24 #define REG_BD50MAX 0xa5 #define REG_BD60MAX 0xab #define REG_BLUE 0x01 #define REG_COM1 0x04 #define REG_COM2 0x09 #define REG_COM3 0x0c #define REG_COM4 0x0d #define REG_COM5 0x0e #define REG_COM6 0x0f #define REG_COM7 0x12 #define REG_COM8 0x13 #define REG_COM9 0x14 #define REG_COM10 0x15 #define REG_COM11 0x3b #define REG_COM12 0x3c #define REG_COM13 0x3d #define REG_COM14 0x3e #define REG_COM15 0x40 #define REG_COM16 0x41 #define REG_COM17 0x42 #define REG_EDGE 0x3f #define REG_GAIN 0x00 #define REG_GFIX 0x69 #define REG_HAECC1 0x9f #define REG_HAECC2 0xa0 #define REG_HAECC3 0xa6 #define REG_HAECC4 0xa7 #define REG_HAECC5 0xa8 #define REG_HAECC6 0xa9 #define REG_HAECC7 0xaa #define REG_HREF 0x32 #define REG_HSTART 0x17 #define REG_HSTOP 0x18 #define REG_HSYEN 0x31 #define REG_HSYST 0x30 #define REG_MVFP 0x1e #define REG_RED 0x02 #define REG_REG76 0x76 #define REG_RGB444 0x8c #define REG_TSLB 0x3a #define REG_VPT 0x26 #define REG_VREF 0x03 #define REG_VSTART 0x19 #define REG_VSTOP 0x1a #define camAddr_RD 0x43 #define camAddr_WR 0x42 struct Regval { uint8_t reg_num; uint16_t value; }; const struct Regval qvga_ov7670[] PROGMEM = { { REG_COM14, 0x19 }, { 0x72, 0x11 }, { 0x73, 0xf1 }, { REG_HSTART, 0x16 }, { REG_HSTOP, 0x04 }, { REG_HREF, 0xa4 }, { REG_VSTART, 0x02 }, { REG_VSTOP, 0x7a }, { REG_VREF, 0x0a }, { 0xff, 0xff } }; const struct Regval yuv422_ov7670[] PROGMEM = { { REG_COM7, 0x00 }, { REG_RGB444, 0x00 }, { REG_COM1, 0x00 }, { REG_COM15, COM15_R00FF }, { REG_COM9, 0x6A }, { 0x4f, 0x80 }, { 0x50, 0x80 }, { 0x51, 0x00 }, { 0x52, 0x22 }, { 0x53, 0x5e }, { 0x54, 0x80 }, { REG_COM13, COM13_UVSAT }, { 0xff, 0xff } }; const struct Regval ov7670_default_regs[] PROGMEM = { { REG_COM7, COM7_RESET }, { REG_TSLB, 0x04 }, { REG_COM7, 0x00 }, { REG_HSTART, 0x13 }, { REG_HSTOP, 0x01 }, { REG_HREF, 0xb6 }, { REG_VSTART, 0x02 }, { REG_VSTOP, 0x7a }, { REG_VREF, 0x0a }, { REG_COM3, 0x00 }, { REG_COM14, 0x00 }, { 0x70, 0x3a }, { 0x71, 0x35 }, { 0x72, 0x11 }, { 0x73, 0xf0 }, { 0xa2, 0x01 }, { REG_COM10, 0x0 }, { 0x7a, 0x20 }, { 0x7b, 0x10 }, { 0x7c, 0x1e }, { 0x7d, 0x35 }, { 0x7e, 0x5a }, { 0x7f, 0x69 }, { 0x80, 0x76 }, { 0x81, 0x80 }, { 0x82, 0x88 }, { 0x83, 0x8f }, { 0x84, 0x96 }, { 0x85, 0xa3 }, { 0x86, 0xaf }, { 0x87, 0xc4 }, { 0x88, 0xd7 }, { 0x89, 0xe8 }, { REG_COM8, COM8_FASTAEC | COM8_AECSTEP }, { REG_GAIN, 0x00 }, { REG_AECH, 0x00 }, { REG_COM4, 0x40 }, { REG_COM9, 0x18 }, { REG_BD50MAX, 0x05 }, { REG_BD60MAX, 0x07 }, { REG_AEW, 0x95 }, { REG_AEB, 0x33 }, { REG_VPT, 0xe3 }, { REG_HAECC1, 0x78 }, { REG_HAECC2, 0x68 }, { 0xa1, 0x03 }, { REG_HAECC3, 0xd8 }, { REG_HAECC4, 0xd8 }, { REG_HAECC5, 0xf0 }, { REG_HAECC6, 0x90 }, { REG_HAECC7, 0x94 }, { REG_COM8, COM8_FASTAEC | COM8_AECSTEP | COM8_AGC | COM8_AEC }, { 0x30, 0x00 }, { 0x31, 0x00 }, { REG_COM5, 0x61 }, { REG_COM6, 0x4b }, { 0x16, 0x02 }, { REG_MVFP, 0x07 }, { 0x21, 0x02 }, { 0x22, 0x91 }, { 0x29, 0x07 }, { 0x33, 0x0b }, { 0x35, 0x0b }, { 0x37, 0x1d }, { 0x38, 0x71 }, { 0x39, 0x2a }, { REG_COM12, 0x78 }, { 0x4d, 0x40 }, { 0x4e, 0x20 }, { REG_GFIX, 0x00 }, { 0x74, 0x10 }, { 0x8d, 0x4f }, { 0x8e, 0x00 }, { 0x8f, 0x00 }, { 0x90, 0x00 }, { 0x91, 0x00 }, { 0x96, 0x00 }, { 0x9a, 0x00 }, { 0xb0, 0x84 }, { 0xb1, 0x0c }, { 0xb2, 0x0e }, { 0xb3, 0x82 }, { 0xb8, 0x0a }, { 0x43, 0x0a }, { 0x44, 0xf0 }, { 0x45, 0x34 }, { 0x46, 0x58 }, { 0x47, 0x28 }, { 0x48, 0x3a }, { 0x59, 0x88 }, { 0x5a, 0x88 }, { 0x5b, 0x44 }, { 0x5c, 0x67 }, { 0x5d, 0x49 }, { 0x5e, 0x0e }, { 0x6c, 0x0a }, { 0x6d, 0x55 }, { 0x6e, 0x11 }, { 0x6f, 0x9e }, { 0x6a, 0x40 }, { REG_BLUE, 0x40 }, { REG_RED, 0x60 }, { REG_COM8, COM8_FASTAEC | COM8_AECSTEP | COM8_AGC | COM8_AEC | COM8_AWB }, { 0x4f, 0x80 }, { 0x50, 0x80 }, { 0x51, 0x00 }, { 0x52, 0x22 }, { 0x53, 0x5e }, { 0x54, 0x80 }, { 0x58, 0x9e }, { REG_COM16, COM16_AWBGAIN }, { REG_EDGE, 0x00 }, { 0x75, 0x05 }, { REG_REG76, 0xe1 }, { 0x4c, 0x00 }, { 0x77, 0x01 }, { REG_COM13, 0x48 }, { 0x4b, 0x09 }, { 0xc9, 0x60 }, { 0x56, 0x40 }, { 0x34, 0x11 }, { REG_COM11, COM11_EXP | COM11_HZAUTO }, { 0xa4, 0x82 }, { 0x96, 0x00 }, { 0x97, 0x30 }, { 0x98, 0x20 }, { 0x99, 0x30 }, { 0x9a, 0x84 }, { 0x9b, 0x29 }, { 0x9c, 0x03 }, { 0x9d, 0x4c }, { 0x9e, 0x3f }, { 0x78, 0x04 }, { 0x79, 0x01 }, { 0xc8, 0xf0 }, { 0x79, 0x0f }, { 0xc8, 0x00 }, { 0x79, 0x10 }, { 0xc8, 0x7e }, { 0x79, 0x0a }, { 0xc8, 0x80 }, { 0x79, 0x0b }, { 0xc8, 0x01 }, { 0x79, 0x0c }, { 0xc8, 0x0f }, { 0x79, 0x0d }, { 0xc8, 0x20 }, { 0x79, 0x09 }, { 0xc8, 0x80 }, { 0x79, 0x02 }, { 0xc8, 0xc0 }, { 0x79, 0x03 }, { 0xc8, 0x40 }, { 0x79, 0x05 }, { 0xc8, 0x30 }, { 0x79, 0x26 }, { 0xff, 0xff } }; void error_led(){ DDRB |= 32; // make sure led is output while (true) { //wait for reset PORTB ^= 32; // toggle led _delay_ms(100); } } void twiStart() { TWCR = _BV(TWINT) | _BV(TWSTA) | _BV(TWEN); // send start while (!(TWCR & (1 << TWINT))); // wait for start to be transmitted if ((TWSR & 0xF8) != TW_START) { error_led(); } } void twiWriteByte(uint8_t data, uint8_t type) { TWDR = data; TWCR = _BV(TWINT) | _BV(TWEN); while (!(TWCR & (1 << TWINT))) {} if ((TWSR & 0xF8) != type) { error_led(); } } void twiAddr(uint8_t addr, uint8_t typeTWI){ TWDR = addr; // send address TWCR = _BV(TWINT) | _BV(TWEN); // clear interrupt to start transmission while ((TWCR & _BV(TWINT)) == 0) {} // wait for transmission if ((TWSR & 0xF8) != typeTWI) { error_led(); } } void wrReg(uint8_t reg, uint8_t dat){ twiStart(); twiAddr(camAddr_WR, TW_MT_SLA_ACK); twiWriteByte(reg, TW_MT_DATA_ACK); twiWriteByte(dat, TW_MT_DATA_ACK); TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO); _delay_ms(1); } static uint8_t twiRd(uint8_t nack){ if (nack) { TWCR = _BV(TWINT) | _BV(TWEN); while ((TWCR & _BV(TWINT)) == 0); if ((TWSR & 0xF8) != TW_MR_DATA_NACK) { error_led(); } return TWDR; } else { TWCR = _BV(TWINT) | _BV(TWEN) | _BV(TWEA); while ((TWCR & _BV(TWINT)) == 0); if ((TWSR & 0xF8) != TW_MR_DATA_ACK) { error_led(); } return TWDR; } } uint8_t rdReg(uint8_t reg) { uint8_t dat; twiStart(); twiAddr(camAddr_WR, TW_MT_SLA_ACK); twiWriteByte(reg, TW_MT_DATA_ACK); TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO); _delay_ms(1); twiStart(); twiAddr(camAddr_RD, TW_MR_SLA_ACK); dat = twiRd(1); TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO); _delay_ms(1); return dat; } void wrSensorRegs8_8(const struct Regval reglist[]) { uint8_t reg_addr, reg_val; const struct Regval *next = reglist; while ((reg_addr != 0xff) | (reg_val != 0xff)) { reg_addr = pgm_read_byte(&next->reg_num); reg_val = pgm_read_byte(&next->value); wrReg(reg_addr, reg_val); next++; } } void setColor(){ wrSensorRegs8_8(yuv422_ov7670); } void setRes(){ wrReg(REG_COM3, 4); // REG_COM3 enable scaling wrSensorRegs8_8(qvga_ov7670); } void camInit(){ wrReg(0x12, 0x80); _delay_ms(100); wrSensorRegs8_8(ov7670_default_regs); wrReg(REG_COM10, 32); //PCLK does not toggle on HBLANK. } void arduinoUnoInut() { cli(); //disable interrupts DDRB |= (1 << 3); // make pin 11 output ASSR &= ~(_BV(EXCLK) | _BV(AS2)); TCCR2A = (1 << COM2A0) | (1 << WGM21) | (1 << WGM20); TCCR2B = (1 << WGM22) | (1 << CS20); OCR2A = 0; DDRC &= ~15; //low d0-d3 camera DDRD &= ~252; //d7-d4 and interrupt pins _delay_ms(3000); //set up twi for 100khz TWSR &= ~3; //disable prescaler for TWI TWBR = 72; //set to 100khz UBRR0H = 0; //enable serial UBRR0L = 1; //0 = 2M baud rate. 1 = 1M baud. 3 = 0.5M. 7 = 250k 207 is 9600 baud rate. UCSR0A |= 2; //double speed aysnc UCSR0B = (1 << RXEN0) | (1 << TXEN0); //Enable receiver and transmitter UCSR0C = 6; //async 1 stop bit 8bit char no parity bits } void setup() { arduinoUnoInut(); camInit(); setRes(); setColor(); wrReg(0x11, 10); } void loop() { char *str = PSTR("*RDY*"); do { while (!(UCSR0A & (1 << UDRE0))); UDR0 = pgm_read_byte_near(str); while (!(UCSR0A & (1 << UDRE0))); } while (pgm_read_byte_near(++str)); while (!(PIND & 8)); while ((PIND & 8)); int y = 240; while (y > 0) { y--; int x = 320; while (x > 0) { x--; while (PIND & 4); UDR0 = (PINC & 15) | (PIND & 240); while (!(UCSR0A & (1 << UDRE0))); while (!(PIND & 4)); while (PIND & 4); while (!(PIND & 4)); } } _delay_ms(100); }
A kódban nagyon sok hardver szintű kódelem található: a megszokott Arduino függvényhívások helyett közvetlenül állítja a regisztereket. Ezt a kódot én magam sem értem teljes egészében: nem tudom, a program elején található hexadecimális kódok mit jelentenek. A kód többi részét komoly nehézségek árán valamelyest sikerült megértenem, melyről a Hardverközeli Arduino programozás oldalon olvashatunk részletesen.
Az eredeti forrás ennek több mint kétszerese; ennyire sikerült leredukálnom a hosszát. Bizonyos részeket megpróbáltam átírni a szokásos Arduino függvényhívásokra, viszont azt tapasztaltam, hogy az eredmény fekete foltokat tartalmazott. Ebből arra következtetek, hogy mindenképpen a legalacsonyabb szintű hozzáféréssel tudjuk csak programozni, mert különben túl lassú lesz.
Számítógépen futó kód
A tesztelés során az Arduino-nak össze kell lennie kötve a számítógéppel, és a számítógépen egy Java kódot kell futtatnunk. Persze ez sem olyan egyszerű…
- Ellenőrizzük, hogy van-e a számítógépünkön Java fordító (javac -version) és Java futtató (java -version). Ha nincs, töltsük le és telepítsük fel a JDK-t. Fontos, hogy 32 bites legyen, és a leírásban 1.8-as szerepel, így a letöltő oldal a következő: https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html. Letöltés és feltelepítés után a Java bin könyvtárát adjuk hozzá a PATH környezeti változóhoz: Edit the system environment variables -> Environment Variables … -> alul Path -> Edit … -> adjuk hozzá a megfelelő könyvtárat, pl. c:\Program Files (x86)\Java\jdk1.8.0_211\bin.
- Töltsük le ezt a fájlt: [[extra.rar]].
- A benne található win32com.dll fájlt másoljuk a JDK bin könyvtárába, pl. c:\Program Files (x86)\Java\jdk1.8.0_211\bin\.
- A lib\comm.jar fájt másoljuk a JRE lib\ext könyvtárába, pl. c:\Program Files (x86)\Java\jre1.8.0_211\lib\ext\.
- A lib\javax.comm.properties fájlt másoljuk a JRE lib könyvtárába, pl. c:\Program Files (x86)\Java\jre1.8.0_211\lib\.
A tömörített fájlt tartalmaz Java kódot is, ezt viszont a következőre egyszerűsítettem. A fájl neve legyen SimpleRead.java.
import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Enumeration; import javax.comm.CommPortIdentifier; import javax.comm.SerialPort; public class SimpleRead { private static final int WIDTH = 320; private static final int HEIGHT = 240; public static void main(String[] args) { if (args.length != 2) { System.out.println("Usage: SimpleRead port outputdir"); System.out.println("E.g.: SimpleRead COM3 c:/out/"); return; } Enumeration portList = CommPortIdentifier.getPortIdentifiers(); CommPortIdentifier portId = null; while (portList.hasMoreElements()) { portId = (CommPortIdentifier) portList.nextElement(); if (portId.getName().equals(args[0])) { break; } } if (portId != null) { int[][] rgb = new int[HEIGHT][WIDTH]; int[][] rgbTranspose = new int[WIDTH][HEIGHT]; try { SerialPort serialPort = (SerialPort) portId.open("SimpleReadApp", 1000); InputStream inputStream = serialPort.getInputStream(); serialPort.setSerialPortParams(1000000, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE); int counter = 0; while (true) { System.out.println("Looking for image"); boolean isImageStart = false; while (!isImageStart) { isImageStart = true; char[] command = {'*', 'R', 'D', 'Y', '*'}; for (int index = 0; index < command.length; index++) { if (!(command[index] == (char)inputStream.read())) { isImageStart = false; break; } } } System.out.println("Found image: " + counter); for (int y = 0; y < HEIGHT; y++) { for (int x = 0; x < WIDTH; x++) { int result = (char)inputStream.read(); rgb[y][x] = ((result & 0xFF) << 16) | ((result & 0xFF) << 8) | (result & 0xFF); } } for (int y = 0; y < HEIGHT; y++) { for (int x = 0; x < WIDTH; x++) { rgbTranspose[x][y] = rgb[y][x]; } } saveBMP(args[1] + (counter++) + ".bmp", rgbTranspose); System.out.println("Saved image: " + counter); } } catch (Exception e) { e.printStackTrace(); } } else { System.out.println("Port " + args[0] + " not found."); } } public static void saveBMP(String filename, int[][] rgbValues) throws IOException { byte offset = 54; FileOutputStream fos = new FileOutputStream(new File(filename)); byte[] bytes = new byte[54 + 3 * rgbValues.length * rgbValues[0].length]; bytes[0] = 'B'; bytes[1] = 'M'; bytes[5] = (byte)bytes.length; bytes[4] = (byte)(bytes.length >> 8); bytes[3] = (byte)(bytes.length >> 16); bytes[2] = (byte)(bytes.length >> 24); bytes[10] = offset; bytes[14] = 40; int width = rgbValues[0].length; bytes[18] = (byte)width; bytes[19] = (byte)(width >> 8); bytes[20] = (byte)(width >> 16); bytes[21] = (byte)(width >> 24); int height = rgbValues.length; bytes[22] = (byte)height; bytes[23] = (byte)(height >> 8); bytes[24] = (byte)(height >> 16); bytes[25] = (byte)(height >> 24); bytes[26] = 1; bytes[28] = 24; for (int row = 0; row < height; row++) { for (int column = 0; column < width; column++) { int rgb = rgbValues[row][column]; int index = offset + 3 * (column + width * row); bytes[index + 2] = (byte)(rgb >> 16); bytes[index + 1] = (byte)(rgb >> 8); bytes[index] = (byte)rgb; } } fos.write(bytes); fos.close(); } }
Ez - azon túl, hogy egy fájlból áll - paraméterezhető: megadható neki egyrészt, hogy melyik portról olvassa az adatokat (a fenti példákban 5 lehetséges portra van "bedrótozva"), másrészt a kimeneti könyvtár is beállítható. Fordítás:
javac SimpleRead.java
Teszt
Hozzuk létre a megfelelő könyvtárat. Majd indítsuk el az olvasó programot a következőképpen:
java SimpleRead COM3 c:/out/
A portot állítsuk be a megfelelőre, amit az Arduino IDE-ből is ki tudunk olvasni, és a kimeneti könyvtárat is állítsuk be megfelelően. Az Arduino legyen rákapcsolva a számítógép soros portjára.
Ha mindent jól csináltunk, akkor a kimeneti könyvtárban megjelennek az elkészült képek. Ezek várhatóan először igen elmosódottak lesznek. Próbáljuk meg az objektív elcsavarásával és/vagy a távolság megváltoztatásával módosítani a fókuszt. A megvilágítás legyen megfelelő. Valamint türelmesnek kell lennünk: idővel adott beállításokkal is javulhat az eredmény. Túl nagy csodára sajnos nem számíthatunk; az alábbi képet elég sok próbálkozást követő tudtam elkészíteni: