esp32-cameraライブラリを読み解く ~OV2640, SCCB, DMA, I2S 編~

esp32,と,OV2640,でDMA,,I2S転送をやってみた ESP32 ( ESP-WROOM-32 )

esp32-cameraライブラリを使わずに、Arduino IDE でプログラミングしてみる

ようやくここまで来ました!

以上を全て踏まえて、最初に紹介した動画のように動作させるための Arduino IDE プログラミングを紹介します。

ですが、今回はesp32-cameraライブラリを読み解くと言いながら、esp32-cameraライブラリは使いません。
esp32-cameraライブラリを解体して、必要な部分を抜き出して改変します。
それ以外の Arduino core for the ESP32 の標準ライブラリを主に使っていきます。
OLED SSD1331 表示部分だけは私の自作ライブラリを使いますので、予めインストールしておいてください。
インストール方法は前回の記事を参照してください。
各バージョンは以下で動作確認しています。

Arduino IDE 1.8.9
Arduino core for the ESP32 ver 1.0.2
OLED SSD1331 用自作ライブラリ beta ver 2.0.0

ソースコードは以下の感じです。
これはあくまで esp32-cameraライブラリを読み解くために、私自身が理解し易くなるように改変したソースコードです。
全部で900行近くあるので、ちょっと大変かもしれません。

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

/* This was modified by mgo-tec with the following esp32-camera library files:
 * The MIT License (MIT)
 * License URL: https://opensource.org/licenses/mit-license.php
 * Copyright (c) 2019 Mgo-tec. All rights reserved.
 * 
 * Use Arduino core for the ESP32 v1.0.2
 * Use esp32_ssd1331_bv2 library beta ver 2.0.0 by mgo-tec.
 * 
 * esp32-camera library ( Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD)
 *  Licensed under the Apache License, Version 2.0 (the "License").
 *  URL:https://github.com/espressif/esp32-camera
 * 
 * sccb.c file is part of the OpenMV project. 
 *  Copyright (c) 2013/2014 Ibrahim Abdelkader.
 *  This work is licensed under the MIT license. 
 */
#include <rom/lldesc.h>
#include <driver/rtc_io.h>
#include <driver/i2s.h>
#include <driver/gpio.h>
#include <driver/i2c.h>
#include <esp_err.h>
#include <driver/ledc.h>

#include <esp32_ssd1331_bv2.h> //beta ver 2.0.0

const uint8_t ov2640_i2c_addrs = 0x30;

const int8_t cam_pin_PWDN = -1; //power down is not used
const int8_t cam_pin_RESET = 17; //software reset will be performed
const int8_t cam_pin_XVCLK = 27;
const int8_t cam_pin_SIOD = 21;
const int8_t cam_pin_SIOC = 22;

const int8_t cam_pin_D7 = 19;
const int8_t cam_pin_D6 = 36;
const int8_t cam_pin_D5 = 18;
const int8_t cam_pin_D4 = 39;
const int8_t cam_pin_D3 = 5;
const int8_t cam_pin_D2 = 34;
const int8_t cam_pin_D1 = 35;
const int8_t cam_pin_D0 = 32;
const int8_t cam_pin_VSYNC = 25;
const int8_t cam_pin_HREF = 26;
const int8_t cam_pin_PCLK = 23;

const uint8_t oled_SCLK =  14; //SCLK
const uint8_t oled_MOSI =  13; //MOSI (Master Output Slave Input)
const uint8_t oled_MISO =  -1; //未使用。MISO (Master Input Slave Output)
const uint8_t oled_DC =  16; //OLED DC(Data/Command)
const uint8_t oled_RST =  4; //OLED Reset
const uint8_t oled_CS = 15; //CS (Chip Select ピン)
 
Esp32Ssd1331Bv2 ssd1331(oled_SCLK, oled_MISO, oled_MOSI, oled_CS, oled_DC, oled_RST);

uint8_t camera_pid = 0;
const uint16_t sensor_resolution_h = 400, sensor_resolution_v = 296; //CIF mode
const uint16_t oled_width_pix = 96, oled_height_pix = 64;
const uint16_t out_camera_w = sensor_resolution_h/4;
const uint16_t out_camera_h = sensor_resolution_v/4;
const uint16_t max_w_pix_buf = out_camera_w * 2;
const uint16_t max_w_disp_buf = oled_width_pix * 2;
uint8_t oled_pix_buf[oled_height_pix][max_w_pix_buf] = {};
boolean goDrawing_disp = false;

const uint8_t ledc_duty = 1; //1bit value:1 = duty 50%
const double ledc_base_freq = 20000000.0;

const uint32_t sccb_freq = 200000; // I2C master frequency
const uint8_t i2c_write_bit = 0; // I2C master write
const uint8_t i2c_read_bit = 1;  // I2C master read
const uint8_t ack_check_en = 1; // I2C master will check ack from slave
//const uint8_t ack_check_dis = 0;
//const uint8_t ack_val = 0; // I2C ack value
const uint8_t nack_val = 1; // I2C nack value
const int sccb_i2c_port = 1;
uint8_t scan_i2c_addrs = 0;

typedef enum {
    SM_0A0B_0B0C = 0,
    SM_0A0B_0C0D = 1,
    SM_0A00_0B00 = 3,
} i2s_sampling_mode_t;

uint16_t dma_buf_size = out_camera_w * 2 * 4;
lldesc_t *dma_desc;

intr_handle_t i2s_intr_handle;
uint8_t y_pix_count = 0;

esp_err_t err = ESP_OK;
TaskHandle_t task_handl;

static inline void resetI2Sconf();
static void IRAM_ATTR i2s_isr(void* arg);

//*********************************************
void setup() {
  ssd1331.init65kColor();
  ssd1331.clearDisplay();
  ssd1331.drawLine(0, 0, 95, 63, 31, 63, 31);
  ssd1331.drawLine(95, 0, 0, 63, 31, 63, 31);
  ssd1331.drawRectangleLine(0, 0, 95, 63, 31, 0, 0);
  delay(1000);
  Serial.begin(115200);
  delay(1000);
  Serial.println();

  esp_err_t err = initCamera();
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }
  Serial.printf("Camera:%d x %d pix, OLED:%d x %d pix\r\n", out_camera_w, out_camera_h, oled_width_pix, oled_height_pix);

  dispOnColorBar();
  uint32_t time_out = millis();
  while(1){
    if(millis() - time_out > 5000) break;
    drawDisplay();
    delay(1);
  }
  dispOffColorBar();
}

void loop() {
  drawDisplay();
}
//****************************************
void taskDMA(void *pvParameters){
  Serial.println("taskDMA Start!!");
  disableCore0WDT(); //ウォッチドッグ無効

  while (true) {
    while(goDrawing_disp){
      delayMicroseconds(10); //※これが無いとディスプレイに表示されない
    }

    uint32_t st_t = millis();
    while (getGpioLevel((gpio_num_t)cam_pin_VSYNC) == 0) {
      if ((millis() - st_t) > 1000000LL) {
        Serial.println("Timeout waiting for VSYNC");
        goto timeout;
      }
    }

    while (getGpioLevel((gpio_num_t)cam_pin_VSYNC) == 1) {
      ;
    }
    I2S0.conf.rx_start = 0;
    I2S0.in_link.start = 0;
    esp_intr_disable(i2s_intr_handle);

    while (getGpioLevel((gpio_num_t)cam_pin_VSYNC) == 0) {
      ;
    }

    I2S0.rx_eof_num = dma_buf_size;
    I2S0.in_link.addr = (uint32_t)dma_desc;
    I2S0.in_link.start = 1;
    I2S0.conf.rx_start = 1;
    I2S0.int_clr.val = I2S0.int_raw.val;
    I2S0.int_ena.val = 0;
    I2S0.int_ena.in_done = 1;

    esp_intr_enable(i2s_intr_handle);
   
    while(getGpioLevel((gpio_num_t)cam_pin_VSYNC) != 0){
      ; //この間、OV2640からの画素データはFIFOメモリにDMAで書き込まれる
    }

    I2S0.conf.rx_start = 0;
    I2S0.in_link.start = 0;
    esp_intr_disable(i2s_intr_handle);
    resetI2Sconf();
  }
timeout:
  vTaskDelete(task_handl);
}

void drawDisplay(){
  if(goDrawing_disp){
    for(int i = 0; i < oled_height_pix; i++){
      ssd1331.drawPixel65kColorBytes(0, i, &oled_pix_buf[i][0], max_w_disp_buf);
    }
    goDrawing_disp = false;
  }
}

int setFramesize(uint16_t out_width, uint16_t out_height){
  int ret = 0;
  ret = writeSCCB(0xFF, 0x00);//bank dsp
  if (!ret) {
    ret = writeSCCB(0x05, 0x00); //R_BYPASS:0x00 DSP use. 
    if(ret) return ret;
  }
  delay(5);

  uint8_t pclk_div2 = 0x01; //PCLK clock divider 2
  uint16_t window_h_start = 137;
  uint16_t window_h_end = window_h_start + sensor_resolution_h;
  uint16_t window_v_start = 2;
  uint16_t window_v_end = window_v_start + sensor_resolution_v;
  uint8_t win_h_st_bit10_3 = (uint8_t)((window_h_start >> 3) & 0x00ff);
  uint8_t win_h_end_bit10_3 = (uint8_t)((window_h_end >> 3) & 0x00ff);
  uint8_t win_v_st_bit9_2 = (uint8_t)((window_v_start >> 2) & 0x00ff);
  uint8_t win_v_end_bit9_2 = (uint8_t)((window_v_end >> 2) & 0x00ff);
  uint8_t win_h_st_bit2_0 = (uint8_t)(window_h_start & 0x0007);
  uint8_t win_h_end_bit2_0 = (uint8_t)(window_h_end & 0x0007);
  uint8_t win_v_st_bit1_0 = (uint8_t)(window_v_start & 0x0003);
  uint8_t win_v_end_bit1_0 = (uint8_t)(window_v_end & 0x0003);

  writeSCCB(0xFF, 0x01); //BANK:sensor
  delay(5);
  writeSCCB(0x12, 0b00100000); //COM7 [6:4]CIF mode
  writeSCCB(0x17, win_h_st_bit10_3); //HREFST(default:0x11)
  writeSCCB(0x18, win_h_end_bit10_3); //HREFEND(SVGA,CIF default:0x43)
  writeSCCB(0x19, win_v_st_bit9_2); //VSTRT
  writeSCCB(0x1A, win_v_end_bit9_2); //VEND
  writeSCCB(0x32, 0x00 | (pclk_div2 << 7) | (win_h_end_bit2_0 << 3) | win_h_st_bit2_0); //REG32,[7:6]10:PCLK frequency devide by 2, [5:0]0x09:CIF
  writeSCCB(0x03, 0x00 | (win_v_end_bit1_0 << 2) | win_v_st_bit1_0); //COM1 0x0A:CIF?

  writeSCCB(0x11, pclk_div2); //CLKRC 1/2 clock divider
  delay(5);

  uint16_t HSIZE = sensor_resolution_h; //Image Horizontal Size
  uint16_t VSIZE = sensor_resolution_v; //Image Vertical Size
  uint16_t H_SIZE = HSIZE / 4;
  uint16_t V_SIZE = VSIZE / 4;

  uint8_t H_SIZE_bit7_0 = (uint8_t)(H_SIZE & 0x00ff);
  uint8_t V_SIZE_bit7_0 = (uint8_t)(V_SIZE & 0x00ff);
  uint8_t H_SIZE_bit8 = (uint8_t)((H_SIZE >> 8) & 0x0001);
  uint8_t H_SIZE_bit9 = (uint8_t)((H_SIZE >> 9) & 0x0001);
  uint8_t V_SIZE_bit8 = (uint8_t)((V_SIZE >> 8) & 0x0001);

  uint8_t zoom_speed = 0;
  uint16_t OUTW = (uint16_t)floor((double)out_width / 4.0);
  uint16_t OUTH = (uint16_t)floor((double)out_height / 4.0);
  uint8_t OUTW_bit7_0 = (uint8_t)(OUTW & 0x00ff);
  uint8_t OUTH_bit7_0 = (uint8_t)(OUTH & 0x00ff);
  uint8_t OUTW_bit9_8 = (uint8_t)((OUTW >> 8) & 0x0003);
  uint8_t OUTH_bit8 = (uint8_t)((OUTH >> 8) & 0x0001);

  uint8_t HSIZE_bit11 = (uint8_t)((HSIZE >> 11) & 0x0001);
  uint8_t HSIZE_bit2_0 = (uint8_t)(HSIZE & 0x0003);
  uint8_t VSIZE_bit2_0 = (uint8_t)(VSIZE & 0x0003);
  uint8_t HSIZE_bit10_3 = (uint8_t)((HSIZE >> 3) & 0x00ff);
  uint8_t VSIZE_bit10_3 = (uint8_t)((VSIZE >> 3) & 0x00ff);
  Serial.printf("HSIZE=%d, VSIZE=%d, H_SIZE=%d, V_SIZE=%d, OUTW=%d, OUTH=%d\r\n", HSIZE, VSIZE, H_SIZE, V_SIZE, OUTW, OUTH);

  writeSCCB(0xFF, 0x00); //BANK:DSP
  delay(5);
  writeSCCB(0xE0, 0b00000100); //RESET bit[2]:DVP ※DVPはパラレルデジタルフォーマット
  delay(5);
  writeSCCB(0xC0, HSIZE_bit10_3); //HSIZE8は11bitのうちの上位8bit。これはh_pixel 400
  writeSCCB(0xC1, VSIZE_bit10_3); //VSIZE8は11bitのうちの上位8bit。これはv_pixel 296
  writeSCCB(0x8C, 0x00); //SIZEL 使い方は不明

  writeSCCB(0x86, 0b00111101); //CTRL2
  writeSCCB(0x50, 0b10000000); //CTRLl LP_DP EN (※CTRL1と混同し易いので注意)

  writeSCCB(0x51, H_SIZE_bit7_0); //H_SIZE
  writeSCCB(0x52, V_SIZE_bit7_0); //V_SIZE
  writeSCCB(0x53, 0x00); //XOFFL
  writeSCCB(0x54, 0x00); //YOFFL
  writeSCCB(0x55, 0x00 | (V_SIZE_bit8 << 7) | (H_SIZE_bit8 << 3)); //VHYX
  writeSCCB(0x57, (H_SIZE_bit9 << 7) | 0x00); //TEST
  writeSCCB(0x5A, OUTW_bit7_0); //ZMOW[7:0] (real/4)
  writeSCCB(0x5B, OUTH_bit7_0); //ZMOH[7:0] (real/4)
  writeSCCB(0x5C, 0x00 | (zoom_speed << 4) | (OUTH_bit8 << 2) | (OUTW_bit9_8)); //ZMHH

  writeSCCB(0xD3, 0b10000010); //R_DVP_SP

  writeSCCB(0xE0, 0x00); //RESET
  writeSCCB(0x05, 0x00); //R_BYPASS:0x00 DSP use.

  delay(10); //解像度を変更する場合、これは必要らしい
  return ret;
}

void initI2S(){
  gpio_num_t pins[] = {
    (gpio_num_t)cam_pin_D7,
    (gpio_num_t)cam_pin_D6,
    (gpio_num_t)cam_pin_D5,
    (gpio_num_t)cam_pin_D4,
    (gpio_num_t)cam_pin_D3,
    (gpio_num_t)cam_pin_D2,
    (gpio_num_t)cam_pin_D1,
    (gpio_num_t)cam_pin_D0,
    (gpio_num_t)cam_pin_VSYNC,
    (gpio_num_t)cam_pin_HREF,
    (gpio_num_t)cam_pin_PCLK
  };
  gpio_config_t conf;
  conf.mode = GPIO_MODE_INPUT;
  conf.pull_up_en = GPIO_PULLUP_ENABLE;
  conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
  conf.intr_type = GPIO_INTR_DISABLE;

  for (int i = 0; i < sizeof(pins) / sizeof(gpio_num_t); ++i) {
    if (rtc_gpio_is_valid_gpio(pins[i])) {
      rtc_gpio_deinit(pins[i]);
    }
    conf.pin_bit_mask = 1LL << pins[i];
    gpio_config(&conf);
  }

  gpio_matrix_in(cam_pin_D0, I2S0I_DATA_IN0_IDX, false);
  gpio_matrix_in(cam_pin_D1, I2S0I_DATA_IN1_IDX, false);
  gpio_matrix_in(cam_pin_D2, I2S0I_DATA_IN2_IDX, false);
  gpio_matrix_in(cam_pin_D3, I2S0I_DATA_IN3_IDX, false);
  gpio_matrix_in(cam_pin_D4, I2S0I_DATA_IN4_IDX, false);
  gpio_matrix_in(cam_pin_D5, I2S0I_DATA_IN5_IDX, false);
  gpio_matrix_in(cam_pin_D6, I2S0I_DATA_IN6_IDX, false);
  gpio_matrix_in(cam_pin_D7, I2S0I_DATA_IN7_IDX, false);
  gpio_matrix_in(cam_pin_VSYNC, I2S0I_V_SYNC_IDX, false);
  gpio_matrix_in(0x38, I2S0I_H_SYNC_IDX, false);
  gpio_matrix_in(cam_pin_HREF, I2S0I_H_ENABLE_IDX, false);
  gpio_matrix_in(cam_pin_PCLK, I2S0I_WS_IN_IDX, false);

  // Enable and configure I2S peripheral
  periph_module_enable(PERIPH_I2S0_MODULE);
  // Toggle some reset bits in LC_CONF register
  // Toggle some reset bits in CONF register
  resetI2Sconf();
  // Enable slave mode (sampling clock is external)
  I2S0.conf.rx_slave_mod = 1;
  // Enable parallel mode
  I2S0.conf2.lcd_en = 1;
  // Use HSYNC/VSYNC/HREF to control sampling
  I2S0.conf2.camera_en = 1;
  // Configure clock divider
  I2S0.clkm_conf.clkm_div_a = 1;
  I2S0.clkm_conf.clkm_div_b = 0;
  I2S0.clkm_conf.clkm_div_num = 2;
  // FIFO will sink data to DMA
  I2S0.fifo_conf.dscr_en = 1;
  // FIFO configuration
  I2S0.fifo_conf.rx_fifo_mod = SM_0A00_0B00; //fifo mode = 3
  I2S0.fifo_conf.rx_fifo_mod_force_en = 1;
  I2S0.conf_chan.rx_chan_mod = 1;
  // Clear flags which are used in I2S serial mode
  I2S0.sample_rate_conf.rx_bits_mod = 0;
  I2S0.conf.rx_right_first = 0;
  I2S0.conf.rx_msb_right = 0;
  I2S0.conf.rx_msb_shift = 0;
  I2S0.conf.rx_mono = 0;
  I2S0.conf.rx_short_sync = 0;
  I2S0.timing.val = 0;
  I2S0.timing.rx_dsync_sw = 1;

  // Allocate I2S interrupt, keep it disabled
  esp_intr_alloc(ETS_I2S0_INTR_SOURCE,
                 ESP_INTR_FLAG_INTRDISABLED | ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_IRAM,
                 &i2s_isr, NULL, &i2s_intr_handle);
}

static void IRAM_ATTR i2s_isr(void* arg){
  I2S0.int_clr.val = I2S0.int_raw.val;
  bool need_yield = false;
  lldesc_t *d = dma_desc;
  int i, pix_cnt = 0;

  for (i = 2; i < dma_buf_size; i += 4) {
    oled_pix_buf[y_pix_count][pix_cnt++] = (uint8_t) * (d->buf + i);
  }

  if(y_pix_count == (oled_height_pix - 1)){
    I2S0.conf.rx_start = 0;
    I2S0.in_link.start = 0;
    y_pix_count = 0;
    goDrawing_disp = true;
    return;
  }
  y_pix_count = (y_pix_count + 1) % oled_height_pix;

  if (need_yield) {
      portYIELD_FROM_ISR();
  }
}

int getGpioLevel(gpio_num_t gpio_num){
  if (gpio_num < 32) {
    return (GPIO.in >> gpio_num) & 0x1;
  } else {
    return (GPIO.in1.data >> (gpio_num - 32)) & 0x1;
  }
}

static inline void resetI2Sconf(){
  const uint32_t lc_conf_reset_flags = I2S_IN_RST_M | I2S_AHBM_RST_M
                                       | I2S_AHBM_FIFO_RST_M;
  I2S0.lc_conf.val |= lc_conf_reset_flags;
  I2S0.lc_conf.val &= ~lc_conf_reset_flags;

  const uint32_t conf_reset_flags = I2S_RX_RESET_M | I2S_RX_FIFO_RESET_M
                                    | I2S_TX_RESET_M | I2S_TX_FIFO_RESET_M;
  I2S0.conf.val |= conf_reset_flags;
  I2S0.conf.val &= ~conf_reset_flags;
  while (I2S0.state.rx_fifo_reset_back) {
    ;
  }
}

esp_err_t initDMAdesc(){
  Serial.println("initDMAdesc");
  assert(out_camera_w % 4 == 0); //幅pixelが4で割り切れるかどうかを事前判定

  Serial.printf("DMA buffer size: %d\r\n", dma_buf_size);

  dma_desc = (lldesc_t*) malloc(sizeof(lldesc_t));
  if (dma_desc == NULL) {
    return ESP_ERR_NO_MEM;
  }

  Serial.printf("Allocating DMA buffer size=%d\r\n", dma_buf_size);

  lldesc_t* pd = dma_desc;
  pd->length = dma_buf_size;
  pd->size = pd->length;
  pd->owner = 1;
  pd->sosf = 1;
  pd->buf = (uint8_t*) malloc(dma_buf_size);
  if (pd->buf == NULL) {
    Serial.println("pd->buf NULL");
    return ESP_ERR_NO_MEM;
  }
  pd->offset = 0;
  pd->empty = 0;
  pd->eof = 1;
  pd->qe.stqe_next = dma_desc;

  return ESP_OK;
}

int reset(){
  int ret = 0;

  ret = writeSCCB(0xFF, 0x01);//bank sensor
  if (!ret) {
    Serial.println("OV2640 System Resister Reset (COM7)");
    ret = writeSCCB(0x12, 0b10100000); //COM7:SRST System Reset & CIF mode
    if(ret) return ret;
  }
  delay(10);
  
  writeRegisterCIF();
  delay(100);

  return ret;
}

esp_err_t probeCamera(){
  enableOutClockToCamera();

  sccbInit(cam_pin_SIOD, cam_pin_SIOC);

  Serial.println("Resetting camera");
  gpio_config_t conf = { 0 };
  conf.pin_bit_mask = 1LL << cam_pin_RESET;
  conf.mode = GPIO_MODE_OUTPUT;
  gpio_config(&conf);

  gpio_set_level((gpio_num_t)cam_pin_RESET, 0);
  delay(10);
  gpio_set_level((gpio_num_t)cam_pin_RESET, 1);
  delay(10);

  Serial.println("Searching for camera address");
  delay(10);
  uint8_t slv_addr = sccbProbe();

  if (slv_addr == ov2640_i2c_addrs) {
    Serial.println("Detected camera OV2640");
  }else{
    disableOutClockToCamera();
    return ESP_FAIL;
  }

  uint8_t reg_PIDH = 0x0A; //Product ID Number MSB
  uint8_t reg_PIDL = 0x0B; //Product ID Number LSB
  uint8_t reg_MIDH = 0x1C; //Manufacture ID Byte MSB
  uint8_t reg_MIDL = 0x1D; //Manufacture ID Byte LSB

  writeSCCB(0xFF, 0x01);//bank sensor

  camera_pid = readSCCB(reg_PIDH);
  uint8_t ver = readSCCB(reg_PIDL);
  uint8_t midh = readSCCB(reg_MIDH);
  uint8_t midl = readSCCB(reg_MIDL);
  delay(10);

  Serial.printf("Product ID=0x%02X\r\n", camera_pid);
  Serial.printf("Product Ver=0x%02X\r\n", ver);
  Serial.printf("Manufacture ID High=0x%02X\r\n", midh);
  Serial.printf("Manufacture ID Low=0x%02X\r\n", midl);

  if(camera_pid == 0x26){
      Serial.println("camera_model = CAMERA_OV2640");
  }else{
      disableOutClockToCamera();
      Serial.println("Detected camera not supported.");
      return ESP_FAIL;
  }

  reset();
  return ESP_OK;
}

void enableOutClockToCamera(){
  ledcSetup(LEDC_CHANNEL_0, ledc_base_freq, LEDC_TIMER_1_BIT); //40MHzにする場合、bit=1
  ledcAttachPin(cam_pin_XVCLK, LEDC_CHANNEL_0);
  ledcWrite(LEDC_CHANNEL_0, ledc_duty); //duty:50%
}

void disableOutClockToCamera(){
  periph_module_disable(PERIPH_LEDC_MODULE);
}

esp_err_t initCamera()
{
  esp_err_t err = probeCamera();
  if (err != ESP_OK) {
    Serial.printf("Camera probe failed with error 0x%x", err);
    goto fail;
  }

  if (setFramesize(out_camera_w, out_camera_h) != 0) {
    Serial.println("Failed to set frame size");
    err = ESP_FAIL;
    goto fail;
  }

  writeRegisterRGB565();

  writeSCCB(0xFF, 0x01);//bank sensor
  Serial.printf("Now Gain ceiling=%d\r\n", readSCCB(0x14));

  if (camera_pid == 0x26) {
    uint8_t s_value1, s_value2;
    //set AGC(Auto Gain Ceiling)
    writeSCCB(0xFF, 0x01); //Bank Sensor
    s_value1 = readSCCB(0x14); //COM9
    delay(5);
    writeSCCB(0xFF, 0x01); //readの後のwriteは必ずbank設定必要
    writeSCCB(0x14, s_value1 | 0b01000000); //COM9 AGC(Auto Gain Ceiling) 8x
    delay(5);
    //set BPC(Black Point Corrections?),WPC(White Point Corrections?), LENC(Lens Corrections?)
    writeSCCB(0xFF, 0x00); //Bank DSP
    s_value1 = readSCCB(0x87); //CTRL3
    s_value2 = readSCCB(0xC3); //CTRL1
    delay(5);
    writeSCCB(0xFF, 0x00); //Bank DSP
    writeSCCB(0x87, 0b10010000 | s_value1); //CTRL3 [7]BPC:1, [6]WPC:1
    writeSCCB(0xC3, 0b00001000 | s_value2); //CTRL1 [3]AWB, [1]LENC
    writeSCCB(0xC2, 0b10001100); //CTRL0 [7]AEC_EN
    delay(10);

    writeSCCB(0xFF, 0x01);
    Serial.printf("After set Gain ceiling=%d\r\n", readSCCB(0x14));
    writeSCCB(0xFF, 0x00); //Bank DSP
    Serial.print("After set CTRL3= ");
    Serial.println(readSCCB(0x87), BIN);
  }

  initI2S();

  err = initDMAdesc();
  if (err != ESP_OK) {
    Serial.println("Failed to initialize I2S and DMA");
    goto fail;
  }
  
  if (!xTaskCreatePinnedToCore(&taskDMA, "taskDMA", 8192, NULL, 10, &task_handl, 0)) {
    Serial.println("Failed to create DMA filter task");
    err = ESP_ERR_NO_MEM;
    goto fail;
  }

  return ESP_OK;

fail:
  disableOutClockToCamera();
  Serial.println("ERROR camera init");
  return err;
}

///////////////////////////////////////////////
void sccbInit(int pin_sda, int pin_scl){
  i2c_config_t conf;
  conf.mode = I2C_MODE_MASTER;
  conf.sda_io_num = (gpio_num_t)pin_sda;
  conf.sda_pullup_en = GPIO_PULLUP_ENABLE;
  conf.scl_io_num = (gpio_num_t)pin_scl;
  conf.scl_pullup_en = GPIO_PULLUP_ENABLE;
  conf.master.clk_speed = sccb_freq;

  i2c_param_config((i2c_port_t)sccb_i2c_port, &conf);
  i2c_driver_install((i2c_port_t)sccb_i2c_port, conf.mode, 0, 0, 0);
}

uint8_t sccbProbe(){
  uint8_t slave_addr = 0x0;
  while (slave_addr < 0x7f) {
    i2c_cmd_handle_t cmd = i2c_cmd_link_create();
    i2c_master_start(cmd);
    i2c_master_write_byte(cmd, ( slave_addr << 1 ) | i2c_write_bit, ack_check_en);
    i2c_master_stop(cmd);
    esp_err_t ret = i2c_master_cmd_begin((i2c_port_t)sccb_i2c_port, cmd, 1000 / portTICK_RATE_MS);
    i2c_cmd_link_delete(cmd);
    if ( ret == ESP_OK) {
      scan_i2c_addrs = slave_addr;
      Serial.printf("Detected Slave Address=%02X\r\n", scan_i2c_addrs);
      return scan_i2c_addrs;
    }
    slave_addr++;
  }
  return scan_i2c_addrs;
}

uint8_t readSCCB(uint8_t reg){
  uint8_t data = 0;
  esp_err_t ret = ESP_FAIL;
  i2c_cmd_handle_t cmd = i2c_cmd_link_create();
  i2c_master_start(cmd);
  i2c_master_write_byte(cmd, ( scan_i2c_addrs << 1 ) | i2c_write_bit, ack_check_en);
  i2c_master_write_byte(cmd, reg, ack_check_en);
  i2c_master_stop(cmd);
  ret = i2c_master_cmd_begin((i2c_port_t)sccb_i2c_port, cmd, 1000 / portTICK_RATE_MS);
  i2c_cmd_link_delete(cmd);

  if (ret != ESP_OK) {
    Serial.println(ret);
    Serial.println("fail");
    return -1;
  }
  cmd = i2c_cmd_link_create();
  i2c_master_start(cmd);
  i2c_master_write_byte(cmd, ( scan_i2c_addrs << 1 ) | i2c_read_bit, ack_check_en);
  i2c_master_read_byte(cmd, &data, (i2c_ack_type_t)nack_val);
  i2c_master_stop(cmd);
  ret = i2c_master_cmd_begin((i2c_port_t)sccb_i2c_port, cmd, 1000 / portTICK_RATE_MS);
  i2c_cmd_link_delete(cmd);
  if (ret != ESP_OK) {
    Serial.printf("readSCCB Failed addr:0x%02x, reg:0x%02x, data:0x%02x, ret:%d", scan_i2c_addrs, reg, data, ret);
  }
  return data;
}

uint8_t writeSCCB(uint8_t reg, uint8_t data){
  esp_err_t ret = ESP_FAIL;
  i2c_cmd_handle_t cmd = i2c_cmd_link_create();
  i2c_master_start(cmd);
  i2c_master_write_byte(cmd, ( scan_i2c_addrs << 1 ) | i2c_write_bit, ack_check_en);
  i2c_master_write_byte(cmd, reg, ack_check_en);
  i2c_master_write_byte(cmd, data, ack_check_en);
  i2c_master_stop(cmd);
  ret = i2c_master_cmd_begin((i2c_port_t)sccb_i2c_port, cmd, 1000 / portTICK_RATE_MS);
  i2c_cmd_link_delete(cmd);
  //Serial.printf("writeSCCB ret=%d\r\n", ret);
  if (ret != ESP_OK) {
    Serial.printf("writeSCCB Failed addr:0x%02x, reg:0x%02x, data:0x%02x, ret:%d\r\n", scan_i2c_addrs, reg, data, ret);
  }
  return ret == ESP_OK ? 0 : -1;
}

void writeRegisterCIF(){
  Serial.println("writeRegisterCIF");
  writeSCCB(0xff, 0x00);
  delay(5);
  writeSCCB(0x2c, 0xff);
  writeSCCB(0x2e, 0xdf);
  delay(5);
  writeSCCB(0xff, 0x01);
  delay(5);
  writeSCCB(0x3c, 0x32);
  //
  writeSCCB(0x11, 0x00); //CLKRC: none clock divide
  writeSCCB(0x09, 0x02);
  writeSCCB(0x04, 0x28);
  writeSCCB(0x13, 0b11100111); //COM8:default C7, [2]AGC 0:manual 1:auto [0]AEC(自動露出) 0:manual 1:auto
  writeSCCB(0x14, 0x48);
  writeSCCB(0x2c, 0x0c);
  writeSCCB(0x33, 0x78);
  writeSCCB(0x3a, 0x33);
  writeSCCB(0x3b, 0xfB);
  //
  writeSCCB(0x3e, 0x00);
  writeSCCB(0x43, 0x11);
  writeSCCB(0x16, 0x10);
  //
  writeSCCB(0x39, 0x92);
  //
  writeSCCB(0x35, 0xda);
  writeSCCB(0x22, 0x1a);
  writeSCCB(0x37, 0xc3);
  writeSCCB(0x23, 0x00);
  writeSCCB(0x34, 0xc0); //ARCOM2
  writeSCCB(0x36, 0x1a);
  writeSCCB(0x06, 0x88);
  writeSCCB(0x07, 0xc0);
  writeSCCB(0x0d, 0x87);
  writeSCCB(0x0e, 0x41);
  writeSCCB(0x4c, 0x00);
  writeSCCB(0x48, 0x00);
  writeSCCB(0x5B, 0x00);
  writeSCCB(0x42, 0x03);
  //
  writeSCCB(0x4a, 0x81);
  writeSCCB(0x21, 0x99);
  //
  writeSCCB(0x24, 0x40);
  writeSCCB(0x25, 0x38);
  writeSCCB(0x26, 0b10000010); //VV Fast Mode Large Step Threshold. [7:4]High threshold, [3:0]Low threshold
  writeSCCB(0x5c, 0x00);
  writeSCCB(0x63, 0x00);
  writeSCCB(0x46, 0x22);
  writeSCCB(0x0c, 0x3c);
  //
  writeSCCB(0x61, 0x70);
  writeSCCB(0x62, 0x80);
  writeSCCB(0x7c, 0x05);
  //
  writeSCCB(0x20, 0x80);
  writeSCCB(0x28, 0x30);
  writeSCCB(0x6c, 0x00);
  writeSCCB(0x6d, 0x80);
  writeSCCB(0x6e, 0x00);
  writeSCCB(0x70, 0x02);
  writeSCCB(0x71, 0x94);
  writeSCCB(0x73, 0xc1);
  //
  writeSCCB(0x12, 0x40); //COM7
  writeSCCB(0x17, 0x11);
  writeSCCB(0x18, 0x43);
  writeSCCB(0x19, 0x00);
  writeSCCB(0x1a, 0x4b);
  writeSCCB(0x32, 0x09);
  writeSCCB(0x37, 0xc0);
  writeSCCB(0x4f, 0xca); //BD50
  writeSCCB(0x50, 0xa8); //BD60
  writeSCCB(0x5a, 0x23);
  writeSCCB(0x6d, 0x00);
  writeSCCB(0x3d, 0x38);
  //
  delay(5);
  writeSCCB(0xff, 0x00);
  delay(5);
  writeSCCB(0xe5, 0x7f);
  writeSCCB(0xf9, 0xc0);
  writeSCCB(0x41, 0x24);
  writeSCCB(0xe0, 0x14);
  writeSCCB(0x76, 0xff);
  writeSCCB(0x33, 0xa0);
  writeSCCB(0x42, 0x20);
  writeSCCB(0x43, 0x18);
  writeSCCB(0x4c, 0x00);
  writeSCCB(0x87, 0xd5);
  writeSCCB(0x88, 0x3f);
  writeSCCB(0xd7, 0x03);
  writeSCCB(0xd9, 0x10);
  writeSCCB(0xd3, 0x82);
  //
  writeSCCB(0xc8, 0x08);
  writeSCCB(0xc9, 0x80);
  //
  writeSCCB(0x7c, 0x00);
  writeSCCB(0x7d, 0x00);
  writeSCCB(0x7c, 0x03);
  writeSCCB(0x7d, 0x48);
  writeSCCB(0x7d, 0x48);
  writeSCCB(0x7c, 0x08);
  writeSCCB(0x7d, 0x20);
  writeSCCB(0x7d, 0x10);
  writeSCCB(0x7d, 0x0e);
  //
  writeSCCB(0x90, 0x00);
  writeSCCB(0x91, 0x0e);
  writeSCCB(0x91, 0x1a);
  writeSCCB(0x91, 0x31);
  writeSCCB(0x91, 0x5a);
  writeSCCB(0x91, 0x69);
  writeSCCB(0x91, 0x75);
  writeSCCB(0x91, 0x7e);
  writeSCCB(0x91, 0x88);
  writeSCCB(0x91, 0x8f);
  writeSCCB(0x91, 0x96);
  writeSCCB(0x91, 0xa3);
  writeSCCB(0x91, 0xaf);
  writeSCCB(0x91, 0xc4);
  writeSCCB(0x91, 0xd7);
  writeSCCB(0x91, 0xe8);
  writeSCCB(0x91, 0x20);
  //
  writeSCCB(0x92, 0x00);
  writeSCCB(0x93, 0x06);
  writeSCCB(0x93, 0xe3);
  writeSCCB(0x93, 0x05);
  writeSCCB(0x93, 0x05);
  writeSCCB(0x93, 0x00);
  writeSCCB(0x93, 0x04);
  writeSCCB(0x93, 0x00);
  writeSCCB(0x93, 0x00);
  writeSCCB(0x93, 0x00);
  writeSCCB(0x93, 0x00);
  writeSCCB(0x93, 0x00);
  writeSCCB(0x93, 0x00);
  writeSCCB(0x93, 0x00);
  //
  writeSCCB(0x96, 0x00);
  writeSCCB(0x97, 0x08);
  writeSCCB(0x97, 0x19);
  writeSCCB(0x97, 0x02);
  writeSCCB(0x97, 0x0c);
  writeSCCB(0x97, 0x24);
  writeSCCB(0x97, 0x30);
  writeSCCB(0x97, 0x28);
  writeSCCB(0x97, 0x26);
  writeSCCB(0x97, 0x02);
  writeSCCB(0x97, 0x98);
  writeSCCB(0x97, 0x80);
  writeSCCB(0x97, 0x00);
  writeSCCB(0x97, 0x00);
  //
  writeSCCB(0xc3, 0xed);
  writeSCCB(0xa4, 0x00);
  writeSCCB(0xa8, 0x00);
  writeSCCB(0xc5, 0x11);
  writeSCCB(0xc6, 0x51);
  writeSCCB(0xbf, 0x80);
  writeSCCB(0xc7, 0x10);
  writeSCCB(0xb6, 0x66);
  writeSCCB(0xb8, 0xA5);
  writeSCCB(0xb7, 0x64);
  writeSCCB(0xb9, 0x7C);
  writeSCCB(0xb3, 0xaf);
  writeSCCB(0xb4, 0x97);
  writeSCCB(0xb5, 0xFF);
  writeSCCB(0xb0, 0xC5);
  writeSCCB(0xb1, 0x94);
  writeSCCB(0xb2, 0x0f);
  writeSCCB(0xc4, 0x5c);
  //
  writeSCCB(0xc0, 0x64);
  writeSCCB(0xc1, 0x4B);
  writeSCCB(0x8c, 0x00);
  writeSCCB(0x86, 0x3D);
  writeSCCB(0x50, 0x00); //bank00, CTRl
  writeSCCB(0x51, 0xC8);
  writeSCCB(0x52, 0x96);
  writeSCCB(0x53, 0x00);
  writeSCCB(0x54, 0x00);
  writeSCCB(0x55, 0x00);
  writeSCCB(0x5a, 0xC8); //ZMOW(Zoom Output Width) 200px
  writeSCCB(0x5b, 0x96); //ZMOH(Zoom Output Height) 150px
  writeSCCB(0x5c, 0x00);
  writeSCCB(0xd3, 0x82);
  //
  writeSCCB(0xc3, 0xed);
  writeSCCB(0x7f, 0x00);
  //
  writeSCCB(0xda, 0x08); //IMAGE_MODE RGB565
  //
  writeSCCB(0xe5, 0x1f);
  writeSCCB(0xe1, 0x67);
  writeSCCB(0xe0, 0x00);
  writeSCCB(0xdd, 0x7f);
  writeSCCB(0x05, 0x00);
}

void writeRegisterRGB565(){
  Serial.println("writeRegisterRGB565");
  writeSCCB(0xff, 0x00); //bank dsp
  writeSCCB(0xE0, 0x04); //RESET DVP
  writeSCCB(0xDA, 0x08); //IMAGE_MODE, RGB565
  writeSCCB(0xE0, 0x00); //RESET
}

void dispOnColorBar(){
  writeSCCB(0xff, 0x01); //bank sensor
  writeSCCB(0x12, 0b00100010); //COM7 CIF mode, colorbar en
}

void dispOffColorBar(){
  writeSCCB(0xff, 0x01); //bank sensor
  writeSCCB(0x12, 0b00100000); //COM7 CIF mode, colorbar en
}

どうでしょうか?

これはやっぱりesp32-cameraライブラリをそのまま使った方が簡単ですね。
でも、Arduino core for the ESP32 のサンプルスケッチではCIFサイズより小さい画面サイズにする方法が良く分からないし、スマホ使用前提のプログラミングもあって難解です。
自分の自由にOV2640を制御するには、このようにesp32-cameraライブラリを解体して独自にプログラミングしていくしかありません。

最初は2000行越えでしたが、必要な物だけをピックアップして、自分なりにかなり改変して、何とか900行弱に抑えました。

先に紹介したことをすべて読んでいただければ分かるかと思われますので、詳細な解説は割愛させていただきます。

これをコンパイル書き込み実行させて、最初に紹介した動画のように動作すればOKです。

OV2640 カメラモジュール ( Arducam B0011 )のピント調節

実は、M5Camera も OV2640カメラモジュール (Arducam B0011)もピント調節が可能なんです。
これは、Twitterで紅樹 タカオさんによってタイムラインに流れて来て拝見した方法です。
これは、目から鱗でしたね。
自分も試してみて、下図の様に恐る恐る回してみたら、バッチリでした。

ただ、気を付けて欲しいのは、ラジオペンチを2つ使って、下の四角い黒い部分を押さえながら上の歯車っぽいパーツを慎重に回すことです。
最初は少々「バキっ」て音が鳴りますが、それ以降はスルスル回ると思います。
これは自己責任で回してくださいね。

編集後記

どうでしたでしょうか?

イメージセンサをマイコンで動かすには、こんなにも情報量が多いのかと途方にくれませんか?
私はかれこれ半年以上も費やしましたよ。

DMA や I2S、そして割り込みやSCCBなどなど、盛り沢山すぎますね。
SPIでLCDに表示させるだけの方が圧倒的に簡単ですね。

でも、実はLCD表示もDMA転送機能があるので、それを使うと表示が速くなると思います。

今回、初めてイメージセンサを扱ってDMAやI2Sを使ってみましたが、イメージセンサについてかなり知見が溜まりました。
これなら、OV2640の上位センサも使えそうな気がします。
手作りデジカメも、そう難しいものではないですね。

これでそろそろディープラーニングの勉強に入れそうな気がします。
長い道のりだったなぁ・・・。

ではまた・・・。

コメント

  1. Gustavo Murta より:

    Hi aMiGO,
    Excellent job! I appreciate it very much.
    Although you don’t know much about DMA, you have come a long way.
    Congratulations

    Thanks
    Gustavo Murta (from Brazil)
    amigo (portuguese) = friend

タイトルとURLをコピーしました