Table of Contents
|
Bevezető
Ez az oldal az Arduino leírás kiterjesztése.
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.
Szoftverek
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 lap alján található összefoglaló.
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:

Hardverközeli Arduino programozás
A hardverközeli programozás során nem (ill. nem csak) a magasabb szintű függvényeket használjuk, hanem a chip regisztereit közvetlenül állítjuk, ill. hardverközeli függvényeket hívunk. Ennek számos előnye és számos hátránya van. Előnyei:
- Bizonyos funkciók csak ilyen módon érhetőek el. Pl. csak így tudjuk elérni az EEPROM memóriát, vagy azt, hogy adat kerüljön a programmemóriába.
- A magasabb szintű függvények használatának szinte mindig van valamekkora "ára": többnyire lehet jobban optimalizálni, mint ahogyan azt egy általános fordító teszi. Erre ritkán, de szükség lehet, pl. fotó készítésekor a beépített függvények sebessége nem elegendő, de közvetlen hardver szintű programozással el tudjuk érni a kívánt sebességet.
- Segítségével jobban meg tudjuk érteni azt, hogy mi történik legalul. Pl. megtudhatjuk, hogy a várakozás során sohasem pontosan annyi ideig függeszti fel a futást a processzor, amennyit megadunk neki, mivel az csak az órajel többszöröse lehet.
Ugyanakkor ne feledkezzünk meg a hátrányokról sem:
- A kód hordozhatósága lényegében megszűnik, vagy legalábbis rendkívüli módon leszűkül. Ha nagyon picit eltérő lapkára szeretnénk feltölteni a kódot, lehetséges, hogy módosítanunk kell rajta.
- A kód olvashatósága is jelentősen romlik.
Ezeket figyelembe véve igyekezzünk csak akkor használni a lent felsorolt technikákat, ha feltétlenül muszáj.
Konstansok tárolása a programmemóriában
Amint arról már volt szó, az Arduino külön memóriahelyen tárolja a programot és az adatot. A program memória Arduino UNO esetén 32 kB, az adta memória viszont csak 2 kB. A konstans jellegű adatokat érdemes tehát a program memóriában tárolni. Ezt a PROGMEM és F() makrók segítségével tudjuk megtenni:
const int dataInProgmem[] PROGMEM = {1, 2, 3, 4, 5}; void setup() { Serial.begin(9600); Serial.println(F("Reading data from PROGMEM.")); for (int i = 0; i < 5; i++) { int dataFromProgmem = pgm_read_word(dataInProgmem + i); Serial.println(dataFromProgmem); } } void loop() {}
Hasonlítsuk össze a következővel, ahol külön nem jelezzük, és az adat automatikusan az SRAM-ba kerül, nem a program memóriába:
int dataInSRAM[] = {1, 2, 3, 4, 5}; void setup() { Serial.begin(9600); Serial.println("Reading data from SRAM."); for (int i = 0; i < 5; i++) { int dataFromSRAM = dataInSRAM[i]; Serial.println(dataFromSRAM); } } void loop() {}
Ez utóbbi fordítási eredménye az alábbi:
Sketch uses 1790 bytes (5%) of program storage space. Maximum is 32256 bytes.
Global variables use 222 bytes (10%) of dynamic memory, leaving 1826 bytes for local variables. Maximum is 2048 bytes.
Az előbbi, PROGMEM-es változat viszont ez:
Sketch uses 1840 bytes (5%) of program storage space. Maximum is 32256 bytes.
Global variables use 188 bytes (9%) of dynamic memory, leaving 1860 bytes for local variables. Maximum is 2048 bytes.
Látható, hogy a PROGMEM-es esetben több adat került a program memóriába, melynek mérete 32256 bájt, a másikban viszont a dinamikus memóriába, melynek a mérete mindössze 2048 bájt.
A PSTR("…") szintén olyan makró, melynek segítségével szöveget tudunk tárolni a program memóriában.
A program memóriáról részletesen olvashatunk itt: https://www.arduino.cc/reference/en/language/variables/utilities/progmem/. A makrók és függvények a avr/pgmspace.h fejlécben vannak deklarálva, viszont az újabb fejlesztőeszközök esetén már nem kell explicit importálni. A specifikációja egyébként itt található: https://www.nongnu.org/avr-libc/user-manual/group__avr__pgmspace.html.
Adat tárolása EEPROM-ban
Az EEPROM-ban tárolt adat kikapcsoláskor is megmarad. Az EEPROM könyvtár (https://www.arduino.cc/en/Reference/EEPROM) függvényeit tudjuk felhasználni adatok olvasására ill. írására.
A következő példában a lapkára szerelt 13-as LED villan először egyet, majd kettőt, hármat, és így tovább, és 10 után ismét egyet. Érdemes lekapcsolni mondjuk ötnél, és újraindítást követően ellenőrizni, hogy hatot villan-e.
#include <EEPROM.h> const int address = 0; int data; void setup() { pinMode(13, OUTPUT); digitalWrite(13, LOW); } void loop() { EEPROM.get(address, data); if (data > 10 || data < 1) { data = 1; EEPROM.put(address, 1); } for (int i = 0; i < data; i++) { digitalWrite(13, HIGH); delay(100); digitalWrite(13, LOW); delay(100); } data++; EEPROM.put(address, data); delay(1000); }
A példában a 0 című memóriaterületről olvasunk ill. írunk. A kód kicsit defenzív, mivel a feltöltés törli az EEPROM memóriát, ott elvileg nem lehet egy korábbi program által hátrahagyott "szemét", mindenesetre egy negatív vagy túl nagy kezdeti értékre is fel van vértezve a program.
Az EEPROM-ban tárolt adat ki- és beolvasása lassú, ráadásul korlátos, hogy összesen hányszor lehet ezt megtenni (kb. százezer ciklus), így indokolatlanul nem érdemes használni.
Hardverközeli portelérés
Az Arduino által nyújtott felület elfedi a hardverbeli eltéréseket a különböző lapkák között. Annak érdekében, hogy ugyanaz a kód többféle lapkával is használható legyen, továbbá amiatt, hogy a kód olvasható maradjon, célszerű a magasabb szintű függvényeket használni, pl. pinMode(), digitalWrite() stb. Azonban jó, ha ismerjük a saját eszközünk processzorát is, ugyanis előfordulhat, hogy bizonyos eszközöknél már nem megengedett a közvetítésből adódó lassulás, közvetlenül, hardver szinten kell programoznunk. Ill. ami valószínűbb, belefutunk ilyen kódba, pl. bizonyos könyvtárakba, és hogy meg tudjuk azt is érteni.
A port kezelés dokumentációja itt található: https://www.arduino.cc/en/Reference/PortManipulation. Az Arduino UNO ATmega328P chip-et tartalmaz, mely hardver szinten 3*3 regisztert definiál port elérésre:
- B: a 8-13 lábak elérésére
- C: az A0-A5 lábak elérésére
- D: a 0-7 lábak elérésére
Mindhárom port esetén 3 műveletet tudunk végrehajtani (x jelzi a regisztert, tehát B, C vagy D):
- DDR[x]: adat irány regiszter. Ezzel tudjuk beállítani, hogy az adott láb bemenet vagy kimenet legyen, tehát a pinMode() regiszter szintű párja. 1 az output, 0 az input. Írni és olvasni is tudjuk.
- PORT[x]: adat regiszter. Ezzel tudunk írni az adott lábra.
- PIN[x]: bemeneti regiszter. Ezzel tudunk beolvasni.
Példák:
- DDRB |= 32;: a B regiszterről van szó, tehát a 8-13 közötti portokról. DDR, tehát az adat irányáról beszélünk. A 32 binárisan kódolva 00100000, a | művelet pedig a bitenkénti logikai vagy. Ennek eredményeként a DDRB regiszter 6. bitje 1 lesz, a többi változatlan. A 6. bit pedig a 13-as lábat jelenti. Így ez az utasítás egyenértékű ezzel: pinMode(13, OUTPUT);.
- PORTB ^= 32;: a fentihez hasonló logika mentén végiggondolva, a 13-as lábon levő értéket változtatja meg (a ^ a bitenkénti logikai kizáró vagy, így a 6. bit fog változni, a többi marad). Ez tehát nagyjából megegyezik a state = HIGH - state; digitalWrite(13, state); utasításokkal.
Még egy gyakori utasítás, egészen pontosan makró: _BV(x). Ez egyenértékű egy bit balra léptetésével x lépést. Ezzel lehet adott bitet kiválasztani. Pl. a _BV(0) eredménye bináris 00000001 lesz, a _BV(5) eredménye pedig bináris 00100000, azaz decimális 32. Jelenleg pont erre van szükségünk.
A delay() hardverszintű megfelelője _delay_ms(), így a kezdeti LED villogtató program regiszterekkel val megvalósítása az alábbi:
void setup() { DDRB |= _BV(5); } void loop() { PORTB ^= _BV(5); _delay_ms(1000); }
Hardverközeli soros kommunikáció
A TX-RX kommunikációt a Serial osztály függvényeivel célszerű megvalósítani. Ezt közvetlenül a státusz regiszterek, azon belül a státusz bitek beállításával is meg tudjuk tenni. Az érintett regiszterek: UCSR0A , UCSR0B és UCSR0C. Ezzel kapcsolatban viszonylag kevés dokumentációt találunk; egy blogbejegyzést olvashatunk a témáról itt: https://appelsiini.net/2011/simple-usart-with-avr-libc/.
Hardverközeli PWM kezelés
A PWM hardver szintű programozásához a TCCRnA és TCCRnB regisztereket kell megfelelően beállítani, melynek részletesebb leírása megtalálható a következő oldalakon:
- https://playground.arduino.cc/Main/TimerPWMCheatsheet/
- https://www.arduino.cc/en/Tutorial/SecretsOfArduinoPWM
Hardverközeli TWI
A TWI (más néven I2C) interfészt tipikusan közvetetten érjük el, az adott hardver könyvtárán keresztül. Mivel a tipikus felhasználása az, hogy hardver szinten küldünk adatokat a célhardvernek, tehát nem is assembly, hanem közvetlen gépi kódú programozásról van itt szó, ez a téma kívül esik e tanuló jellegű leírás keretein. A téma mélyét nem említve, az érintett regiszterek az alábbiak:
- TWBR (Two Wire Interface Bit Rate Register) - ezzel tudjuk a bitrátát beállítani
- TWCR (Two Wire Interface Control Register) - a regiszter bitjeit beállítva (a 8 bitből 7-et) tudjuk vezérelni az adatforgalmat
- TWSR (Two Wire Interface Status Register) - segítségével a státuszt tudjuk kiolvasni; a kódok jelentését a processzor adatlapja tartalmazza
- TWDR (Two Wire Interface Data Register) - a küldendő adatot tartalmazza
A következő függvény adott I2C címre adott regiszternek adott értéket ad:
void twiWrite(uint8_t addr, uint8_t reg, uint8_t dat){ TWCR = _BV(TWINT) | _BV(TWEN) | _BV(TWSTA); while (!(TWCR & _BV(TWINT))) {} TWDR = addr; TWCR = _BV(TWINT) | _BV(TWEN); while (!(TWCR & _BV((TWINT))) {} TWDR = reg; TWCR = _BV(TWINT) | _BV(TWEN); while (!(TWCR & _BV(TWINT))) {} TWDR = dat; TWCR = _BV(TWINT) | _BV(TWEN); while (!(TWCR & _BV(TWINT))) {} TWCR = _BV(TWINT) | _BV(TWEN) | _BV(TWSTO); }
További adatforrások:
- https://playground.arduino.cc/Main/WireLibraryDetailedReference - a TWI általános leírása
- https://playground.arduino.cc/Code/ATMELTWI - a TWI mély bugyrai
- A processzor specifikáció 22. fejezete szól a TWI-ről.
Hardverközeli időzítés
A regiszterek beállításával elért időzítésről egy kiváló összefoglalót találhatunk itt: https://www.instructables.com/id/Arduino-Timer-Interrupts/. Lássuk, hogyan is néz ki a már bemutatott fél másodperces megszakításokkal LED-et villogtató példa:
void setup(){ pinMode(13, OUTPUT); cli(); TCCR1A = 0; TCCR1B = 0; TCNT1 = 0; OCR1A = 7882; // = 16,000,000 / (2 * 1024) - 1 (must be < 65536) TCCR1B |= (1 << WGM12); TCCR1B |= (1 << CS12) | (1 << CS10); TIMSK1 |= (1 << OCIE1A); sei(); } ISR(TIMER1_COMPA_vect) { digitalWrite(13, 1 - digitalRead(13)); } void loop() {}