Arduino kamera alkalmazás

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:

bekotes.jpg

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:

fenykep.bmp

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:

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:

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() {}
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License