M5StackとM5Camera でWiFi TCP/IP 動画ストリーミングする実験

M5StackとM5CameraをWiFi,TCP/IP,でMJPEG動画ストリーミングする実験 M5Stack

M5Camera (送信側)イメージセンサ動画ストリーミングのスケッチ入力

では、今度は、送信側のM5Cameraのイメージセンサ(OV2640)を使って、M5Stackへ動画ストリーミング送信するスケッチをArduino IDEに入力していきます。

概要

setup関数とloop関数はCPU core 1タスクです。
httpdライブラリによるサーバー構築は、CPU core 0タスクです。
ザッとこんな感じです。

void setup() {
  TaskHandle_t taskClientCtrl_handl, taskClientStrm_handl;
  //CPU core 0 でWiFi通信サーバー用タスク生成
  TaskHandle_t taskServer_handl;
  xTaskCreatePinnedToCore(&taskServer, "taskServer", 8192, NULL, 20, &taskServer_handl, 0);

  //イメージセンサ(Camera) OV2640 初期化
  //ESP32 DMA制御初期化
}

void loop() {
  //メインループはCPU core 1
  //画像データ配列に描画
}

void taskServer(void *pvParameters){
  connectToWiFi(); //WiFiアクセスポイント接続
  startHttpd(); //httpd開始
  while(true){
    delay(1);
  }
}

void startHttpd(){
  //port 80 httpd_start  制御コマンド受信開始
  //port 81 httpd_start  ストリーミング送信開始
}

void connectToWiFi(){
  //WiFi アクセスポイント接続
  isWiFiConnected = true;
}

loop関数では、200 x 148画素配列に描画する処理にしました。

httpdライブラリによるサーバー構築は、ライブラリ任せなので、マルチタスクのどのタスクで実行されているのかは良く分かりません。
とりあえず、taskServerタスクで実行させれば良いと思います。

スケッチ(プログラムソースコード)

スケッチは1200行を超えてしまいました。
分割してライブラリ化しようかと思ったのですが、手が付けられなかったので、このままです。

httpdライブラリ関連は、先ほど紹介したテスト用動画ストリーミングスケッチとほぼ同じです。

あまりにも長いので、GitHubにも置こうと思ったのですが、受信側のスケッチとセットにしなければいけないのと、受信側は自前のライブラリインストールやフォントの問題があり、ドキュメント作成が面倒だったので、この記事上での公開とさせていただきます。

とにかく独学素人コードなので、無駄や誤りが多いと思います。

(※SSIDやパスワードは、ESP32が第三者の手に渡った場合、ソフトウェアによって情報を抜き取られる場合があります。
このスケッチによるいかなるトラブルも当方では責任を負えませんのでご了承ください。)

【ソースコード】 (※無保証 ※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) 2020 Mgo-tec. All rights reserved.
 * 
 * Use Arduino core for the ESP32 stable v1.0.4
 *  
 * Modify app_httpd.cpp(Arduino core for the ESP32 v1.0.4).
 * Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD
 * app_httpd.cpp - Licensed under the Apache License, Version 2.0
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * 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/i2c.h>
#include <esp_err.h>
#include <driver/ledc.h>
#include <WiFi.h>
#include <esp_http_server.h>

const char* ssid = "xxxxxxxxx"; //ご自分のルーターのSSIDに書き換えてください
const char* password = "xxxxxxxxx"; //ご自分のルーターのパスワードに書き換えてください

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 = 15; //software reset will be performed
const int8_t cam_pin_XVCLK = 27;
const int8_t cam_pin_SIOD = 22;
const int8_t cam_pin_SIOC = 23;
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 = 21;

uint8_t camera_pid = 0;
const uint16_t sensor_resolution_h = 400, sensor_resolution_v = 296; //CIF mode
const uint16_t disp_width_pix = 200, disp_height_pix = 148;
const uint16_t out_camera_w = sensor_resolution_h/2;
const uint16_t out_camera_h = sensor_resolution_v/2;
const uint16_t max_w_pix_buf = out_camera_w * 2;
const uint16_t max_w_disp_buf = disp_width_pix * 2;

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;
uint8_t block_count = 0;
esp_err_t err = ESP_OK;

static inline void IRAM_ATTR resetI2Sconf();
static void IRAM_ATTR i2s_isr(void* arg);
static void IRAM_ATTR vsync_intr_enable();
static void IRAM_ATTR vsync_intr_disable();
static void IRAM_ATTR vsync_isr(void* arg);
boolean isI2Sisr = false;
boolean isVsyncIsr = false;
//------Initialize bitmap data------
const uint16_t data_size = disp_width_pix * 2 * disp_height_pix;
const uint8_t data_size_lsb = (uint8_t)(0x00ff & data_size);
const uint8_t data_size_msb = (uint8_t)(data_size >> 8); 
const uint8_t bmp_head_size = 66;
const uint16_t file_size = bmp_head_size + data_size;
const uint8_t file_size_lsb = (uint8_t)(0x00ff & file_size);
const uint8_t file_size_msb = (uint8_t)(file_size >> 8);
const uint8_t info_header_size = 0x28; //情報ヘッダサイズは常に40byte = 0x28byte
const uint8_t bits_per_pixel = 16; //色ビット数=16bit(0x10)
const uint8_t compression = 3; //色ビット数が16bitの場合、マスクを設定するので3にする。
const uint8_t red_mask[2] =   {0b11111000, 0b00000000};
const uint8_t green_mask[2] = {0b00000111, 0b11100000};
const uint8_t blue_mask[2] =  {0b00000000, 0b00011111}; 
//※Bitmap file headerは全てリトルエンディアン
const uint8_t bmp_header[bmp_head_size]=
    {0x42, 0x4D,
     file_size_lsb, file_size_msb, 0, 0,
     0, 0, 0, 0,
     bmp_head_size, 0, 0, 0,
     info_header_size, 0, 0, 0,
     disp_width_pix, 0, 0, 0,
     disp_height_pix, 0, 0, 0,
     1, 0, bits_per_pixel, 0,
     compression, 0, 0, 0,
     data_size_lsb, data_size_msb, 0, 0,
     0,0,0,0,
     0,0,0,0,
     0,0,0,0,
     0,0,0,0,
     red_mask[1], red_mask[0], 0, 0,
     green_mask[1], green_mask[0], 0, 0,
     blue_mask[1], blue_mask[0], 0, 0};
//-------------------------------------
uint8_t bmp_file_buf[file_size] = {};
uint16_t pix_cnt = bmp_head_size;
httpd_handle_t stream_httpd = NULL;
httpd_handle_t camera_httpd = NULL;
bool isFirstI2Sisr = true;
bool canStartStream = false;
bool canSendImage = false;
bool isWiFiConnected = false;
bool isCloseConnection = false;
uint32_t fps_timer = 0;
uint8_t fps_count = 0;
uint32_t interval[3] = {1, 15, 60};
uint8_t chg_intvl = 0;

//********CPU core 1 task********************
void setup() {
  Serial.begin(115200);
  Serial.println();
  delay(1000);
  memcpy(bmp_file_buf, bmp_header, bmp_head_size);
  TaskHandle_t taskServer_handl;
  xTaskCreatePinnedToCore(&taskServer, "taskServer", 8192, NULL, 20, &taskServer_handl, 0);
  while(!isWiFiConnected){
    Serial.print('.');
    delay(500);
  }
  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, Display:%d y %d pix\r\n", out_camera_w, out_camera_h, disp_width_pix, disp_height_pix);
  initCameraDMA();
  changeAutoWhiteBalance(0);
  changeExposureControl(0);
}

void loop() {
  getCameraBuf();
  if(canStartStream && (millis() - fps_timer > 1000)){
    Serial.printf("%d (fps)\r\n", fps_count);
    fps_count = 0;
    fps_timer = millis();
  }
}
//********CPU core 0 task********************
void taskServer(void *pvParameters){
  connectToWiFi();
  while(!isWiFiConnected){
    delay(1);
  }
  startHttpd();
  while(true){
    delay(1);
  }
}
//****************************************
void initCameraDMA(){
  Serial.println("taskDMA Start!!");
  initI2S();
  err = initDMAdesc();
  if (err != ESP_OK) {
    Serial.println("Failed to initialize I2S and DMA");
  }

  vsync_intr_disable();
  gpio_install_isr_service(ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_IRAM);
  err = gpio_isr_handler_add((gpio_num_t)cam_pin_VSYNC, &vsync_isr, NULL);
  if (err != ESP_OK) {
      Serial.printf("vsync_isr_handler_add failed (%x)\r\n", err);
  }

  I2S0.rx_eof_num = dma_buf_size;
  I2S0.in_link.addr = (uint32_t)dma_desc;
  I2S0.in_link.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);
  I2S0.conf.rx_start = 1;
  vsync_intr_enable();
}
//****************************************
void getCameraBuf(){
  if(!canSendImage){
    if(isVsyncIsr){
      if(isI2Sisr){
        if(isFirstI2Sisr){
          isFirstI2Sisr = false;
          isI2Sisr = false;
          return;
        }
        for(int i = 2; i < dma_buf_size; i += 4) {
          bmp_file_buf[pix_cnt++] = (uint8_t) * (dma_desc->buf + i);
        }
        if(pix_cnt >= file_size){
          pix_cnt = bmp_head_size;
          canSendImage = true;
          isVsyncIsr = false;
        }
        isI2Sisr = false;
        return;
      }
    }
  }
}
//****************************************
static esp_err_t stream_handler(httpd_req_t *req){
  esp_err_t res = ESP_OK;

  String res_http = "HTTP/1.1 200 OK\r\n";
    res_http += "Access-Control-Allow-Origin: *\r\n";
    res_http += "Content-type: multipart/x-mixed-replace;boundary=--myboundary\r\n";
    res_http += "\r\n";

  httpd_send(req, res_http.c_str(), res_http.length());
  res_http = "";

  String boundary_header = "--myboundary\r\n";
  boundary_header += "Access-Control-Allow-Origin: *\r\n";
  boundary_header += "Content-type: image/bmp\r\n";
  boundary_header += "Content-Length: " + String(file_size);
  boundary_header += "\r\n\r\n";
  Serial.printf("boundary_header size=%d\r\n", boundary_header.length());

  uint32_t last_time = 0;
  uint8_t send_order = 0;
  while(true){
    if(canStartStream){
      if(canSendImage){
        if(millis() - last_time > interval[send_order]){
          if(send_order == 0){
            res = httpd_send(req, boundary_header.c_str(), boundary_header.length());
            send_order = 1;
          }else if(send_order == 1){
            res = httpd_send(req, (const char *)bmp_file_buf, file_size);
            send_order = 2;
          }else if(send_order == 2){
            res = httpd_send(req, "\r\n", 2);
            canSendImage = false;
            isVsyncIsr = false;
            fps_count++;
            send_order = 0;
          }
          last_time = millis();
        }
      }
    }
    if(isCloseConnection){
      Serial.println("Loop Out Stream!");
      break;
    }
    delay(1);
  }
  return res;
}
//****************************************
static esp_err_t cmd_handler(httpd_req_t *req){
  char*  buf;
  size_t buf_len;
  char id_txt[32] = {0,};
  char value_txt[32] = {0,};

  buf_len = httpd_req_get_url_query_len(req) + 1;
  if (buf_len > 1) {
    buf = (char*)malloc(buf_len);
    if(!buf){
      httpd_resp_send_500(req);
      return ESP_FAIL;
    }
    if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
      Serial.println("-----Receive Control Command");
      Serial.println(buf);
      if (httpd_query_key_value(buf, "id", id_txt, sizeof(id_txt)) == ESP_OK &&
        httpd_query_key_value(buf, "value", value_txt, sizeof(value_txt)) == ESP_OK) {
      } else {
        free(buf);
        httpd_resp_send_404(req);
        return ESP_FAIL;
      }
    } else {
      Serial.println(buf);
      free(buf);
      httpd_resp_send_404(req);
      return ESP_FAIL;
    }
    free(buf);
  } else {
    httpd_resp_send_404(req);
    return ESP_FAIL;
  }

  uint8_t val = atoi(value_txt);
  int res = 0;
  if(!strcmp(id_txt, "aec")) {
    changeExposureControl(val);
    Serial.printf("%s = %d\r\n", id_txt, val);
  }else if(!strcmp(id_txt, "awb")) {
    changeAutoWhiteBalance(val);
    Serial.printf("%s = %d\r\n", id_txt, val);
  }else if(!strcmp(id_txt, "change_endian")){
    changeEndian();
  }else if(!strcmp(id_txt, "change_interval")){
    if(chg_intvl == 0){
      interval[0] = 0, interval[1] = 0, interval[2] = 0; 
      chg_intvl = 1;
    }else{
      interval[0] = 1, interval[1] = 15, interval[2] = 60; 
      chg_intvl = 0;
    }
  }else if(!strcmp(id_txt, "start_stream")){
    canStartStream = true;
    isCloseConnection = false;
  }else if(!strcmp(id_txt, "stop_stream")){
    canStartStream = false;
    isCloseConnection = true;
  }else if(!strcmp(id_txt, "reset")){
    ESP.restart(); //ESP32強制リセット
  }else if(!strcmp(id_txt, "ping80")){
    Serial.println("---------ping receive");
  }else{
    res = -1;
  }

  if(res){
      return httpd_resp_send_500(req);
  }

  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  return httpd_resp_send(req, NULL, 0);
}
//****************************************
static esp_err_t index_handler(httpd_req_t *req){
  String html_body = "<!DOCTYPE html>\r\n";
          html_body += "<html><head></head><body>\r\n";
          html_body += "<img id='pic_place' width='200' height='148' style='border-style:solid; transform:scale(1, -1);'>\r\n";
          html_body += "<div>\r\n";
          html_body += "<p><button style='border-radius:25px;' onclick='startStream()'>Start Stream</button>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='changeCtrlCam(\"stop_stream\",0)'>Stop Stream</button></p>\r\n";
          html_body += "<p><button style='border-radius:25px;' onclick='changeCtrlCam(\"awb\",1)'>AWB ON</button>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='changeCtrlCam(\"awb\",0)'>AWB OFF</button></p>\r\n";
          html_body += "<p><button style='border-radius:25px;' onclick='changeCtrlCam(\"aec\",0)'>AEC auto</button>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='changeCtrlCam(\"aec\",2)'>AEC OFF</button></p>\r\n";
          html_body += "<p><button style='border-radius:25px;' onclick='changeCtrlCam(\"change_endian\",0)'>Change Endian</button>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='changeCtrlCam(\"change_interval\",0)'>Change Interval</button></p>\r\n";
          html_body += "</div>\r\n";
          html_body += "<script>\r\n";
          html_body += "var base_url = document.location.origin;\r\n";
          html_body += "var url_stream = base_url + ':81';\r\n";
          html_body += "function startStream() {\r\n";
          html_body += "var pic = document.getElementById('pic_place');\r\n";
          html_body += "pic.src = url_stream+'/stream';\r\n";
          html_body += "changeCtrlCam('start_stream',0);};\r\n";
          html_body += "function changeCtrlCam(id_txt, value_txt){\r\n";
          html_body += "var new_url = base_url+'/control?id=';\r\n";
          html_body += "new_url += id_txt + '&';\r\n";
          html_body += "new_url += 'value=' + value_txt;\r\n";
          html_body += "fetch(new_url)\r\n";
          html_body += ".then((response) => {\r\n";
          html_body += "if(response.ok){return response.text();} \r\n";
          html_body += "else {throw new Error();}})\r\n";
          html_body += ".then((text) => console.log(text))\r\n";
          html_body += ".catch((error) => console.log(error));};\r\n";
          html_body += "</script></body></html>\r\n\r\n";

    httpd_resp_set_type(req, "text/html");
    httpd_resp_set_hdr(req, "Accept-Charset", "UTF-8");
    return httpd_resp_send(req, html_body.c_str(), html_body.length());
}
//****************************************
void startHttpd(){
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();

  httpd_uri_t index_uri = {
        .uri       = "/",
        .method    = HTTP_GET,
        .handler   = index_handler,
        .user_ctx  = NULL
    };

  httpd_uri_t cmd_uri = {
        .uri       = "/control",
        .method    = HTTP_GET,
        .handler   = cmd_handler,
        .user_ctx  = NULL
    };

  httpd_uri_t stream_uri = {
      .uri       = "/stream",
      .method    = HTTP_GET,
      .handler   = stream_handler,
      .user_ctx  = NULL
  };

  if (httpd_start(&camera_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(camera_httpd, &index_uri);
    httpd_register_uri_handler(camera_httpd, &cmd_uri);
  }

  config.server_port += 1;
  config.ctrl_port += 1;

  if (httpd_start(&stream_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(stream_httpd, &stream_uri);
  }
}
//****************************************
static void IRAM_ATTR vsync_isr(void* arg){
  GPIO.status1_w1tc.val = GPIO.status1.val;
  GPIO.status_w1tc = GPIO.status;
  //esp_intr_disable(i2s_intr_handle);
  I2S0.conf.rx_start = 0;
  I2S0.in_link.start = 0;
  I2S0.int_clr.val = I2S0.int_raw.val;
  resetI2Sconf();
  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;
  isVsyncIsr = true;
  isFirstI2Sisr = true;
  //esp_intr_enable(i2s_intr_handle);
}

static inline void IRAM_ATTR 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) {
    ;
  }
}

static void IRAM_ATTR vsync_intr_disable(){
  esp_intr_disable(i2s_intr_handle);
  gpio_set_intr_type((gpio_num_t)cam_pin_VSYNC, GPIO_INTR_DISABLE);
}

static void IRAM_ATTR vsync_intr_enable(){
  gpio_set_intr_type((gpio_num_t)cam_pin_VSYNC, GPIO_INTR_NEGEDGE);
}

static void IRAM_ATTR i2s_isr(void* arg){
  I2S0.int_clr.val = I2S0.int_raw.val;
  isI2Sisr = true;
}
//****************************************
void connectToWiFi(){
  Serial.println("Connecting to WiFi network: " + String(ssid));
  WiFi.disconnect(true, true);
  delay(1000);
  WiFi.onEvent(WiFiEvent);
  WiFi.begin(ssid, password);
  Serial.println("Waiting for WIFI connection...");
}

void WiFiEvent(WiFiEvent_t event){
  switch(event) {
    case SYSTEM_EVENT_STA_GOT_IP:
      Serial.println("WiFi connected!");
      Serial.print("My IP address: ");
      Serial.println(WiFi.localIP());
      delay(1000);
      isWiFiConnected = true;
      break;
    case SYSTEM_EVENT_STA_DISCONNECTED:
      Serial.println("WiFi lost connection");
      isWiFiConnected = false;
      break;
    default:
      break;
  }
}
//****************************************
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_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);
}

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, 0b00000000 | 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);
  }

  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, 0b00101000); //REG04: bit[7]Horizontal Mirror, bit[6]Vertical Flip
  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, 0b11100001); //CTRL1
  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, 0b11100001); //CTRL1
  writeSCCB(0x7f, 0x00);
  //
  writeSCCB(0xda, 0b00001000); //IMAGE_MODE RGB565. bit[0]:0=big endian
  //
  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, 0b00001000); //IMAGE_MODE, RGB565. bit[0]:0=big endian
  writeSCCB(0xE0, 0x00); //RESET
}

void changeEndian(){
  writeSCCB(0xff, 0x00);
  uint8_t old_IMAGE_MODE = readSCCB(0xDA); //IMAGE_MODE, RGB565. bit[0]:endian
  Serial.print("Old IMAGE_MODE(change endian)=");
  Serial.println(old_IMAGE_MODE, BIN);
  uint8_t tmp_IMAGE_MODE = old_IMAGE_MODE & 0x01;
  if(tmp_IMAGE_MODE == 0){
    tmp_IMAGE_MODE = 0x01;
  }else{
    tmp_IMAGE_MODE = 0;
  }
  uint8_t new_IMAGE_MODE = (old_IMAGE_MODE & 0b11111110) | tmp_IMAGE_MODE;
  writeSCCB(0xDA, new_IMAGE_MODE);
  Serial.print("New IMAGE_MODE(change endian)=");
  Serial.println(new_IMAGE_MODE, BIN);
}

void changeAutoWhiteBalance(uint8_t val){
  Serial.printf("AWB change!");
  uint8_t CTRL1;
  writeSCCB(0xff, 0x00);
  CTRL1 = readSCCB(0xC3);
  Serial.print("Old CTRL1=");
  Serial.println(CTRL1, BIN);
  uint8_t CTRL1_bit3_2 = 0, awb_bit = 0, awb_gain_bit = 0;
  awb_bit = val << 3;
  awb_gain_bit = val << 2;
  CTRL1_bit3_2 = awb_bit | awb_gain_bit;
  uint8_t msb, lsb;
  msb = CTRL1 & 0b11110000;
  lsb = CTRL1 & 0b00000011;
  CTRL1 = (lsb | CTRL1_bit3_2) | msb;
  writeSCCB(0xff, 0x00);
  writeSCCB(0xC3, CTRL1);
  Serial.print("New CTRL1=");
  Serial.println(CTRL1, BIN);
}

void changeExposureControl(uint8_t val){
  writeSCCB(0xff, 0x01);
  uint8_t COM8 = readSCCB(0x13);
  Serial.print("Old COM8=");
  Serial.println(COM8, BIN);
  uint8_t tmp_com8 = 0;
  if(val == 0) {
    tmp_com8 = 0b11100111; //Bit[0] = 1: auto, Bit[5]=1:ON set min exp time 1/120s
  }else{
    tmp_com8 = 0b11000110; //Bit[5]=0:OFF set min exp time 1/120s
    uint16_t exposure[5] = 
    {0, 8, 64, 192, 8192};
    exposureResister(exposure[val - 1]);
  }
  if(COM8 != tmp_com8){
    COM8 = tmp_com8;
    writeSCCB(0xff, 0x01);
    writeSCCB(0x13, COM8);
    Serial.print("New COM8=");
    Serial.println(COM8, BIN);
  }
}

void exposureResister(uint16_t exp16){
  /* AEC[15:10] = REG45[5:0](0x45)
   * AEC[9:2] = AEC[9:2](0x10)
   * AEC[1:0] = REG04[1:0](0x04)
   */
  uint8_t exposure[2] = {(uint8_t)(exp16 >> 8), (uint8_t)(exp16 & 0x00ff)};
  writeSCCB(0xff, 0x01);
  uint8_t old_REG45 = readSCCB(0x45);
  uint8_t old_AEC = readSCCB(0x10);
  uint8_t old_REG04 = readSCCB(0x04);
  uint16_t old_ex[3] = {};
  old_ex[0] = (((uint16_t)old_REG45 & 0x003f)) << 10;
  old_ex[1] = ((uint16_t)old_AEC) << 2;
  old_ex[2] = ((uint16_t)old_REG04) & 0x0003;
  uint16_t old_exposure = old_ex[0] | old_ex[1] | old_ex[2];
  Serial.printf("old_exposure=%d\r\n", old_exposure);
  uint8_t new_REG45 = (old_REG45 & 0b11000000);
  new_REG45 = new_REG45 | (exposure[0] >> 2);
  uint8_t new_AEC = (exposure[0] << 6) | (exposure[1] >> 2);
  uint8_t new_REG04 = (old_REG04 & 0b11111100);
  new_REG04 = new_REG04 | (exposure[1] & 0b00000011);
  writeSCCB(0xff, 0x01);
  writeSCCB(0x45, new_REG45);
  writeSCCB(0x10, new_AEC);
  writeSCCB(0x04, new_REG04);
  Serial.printf("new_exposure=%d\r\n", exp16);
}

ザッとスケッチ解説

ビットマップ(BMP)ファイルやMJPEG、httpdライブラリについては以下の記事を参照してください。

ESP32でビットマップ画像ファイルを生成し、ブラウザに連続送信してMotion JPEGならぬMotion BMP動画ストリーミングする実験

ESP32でhttpdライブラリを使うとMotion JPEG(BMP)動画ストリーミングが速い!

M5CameraのイメージセンサOV2640制御に関しては、以下の記事を参照してください。

esp32-cameraライブラリを読み解く ~モジュール接続、動作チェック編~

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

255-281行が、MJPEG動画を送信しているループです。
boundary_headerを送信した後は、delay(1)以外は止めずにループ処理して、60ms経ったら画像1フレームを送信します。
このIntervalが小さいと、動画がカクカクし過ぎてしまいます。
逆に大きすぎても良くありませんでした。
Interval値は、330-337行にあるように、M5StackからGETリクエストでid=change_interval を検知したら変更できるようにしています。

341-343行では、GETリクエストでstop_streamを検知したら、255-281行の動画送信無限ループを脱出して、コネクション切断するようにしています。

イメージセンサ(カメラ)OV2640制御で今回新たに加えたところは、1134-1194行のRGB565(16bit)のエンディアン変換です。
イメージセンサOV2640のレジスタアドレス0xDAでエンディアンを変更できます。
SCCBインターフェースで0xDAを送った後、1byteデータを送るのですが、その0bit目を1か0に変更すればエンディアンを変更できます。

その他、1172-1221行にあるように、イメージセンサの露出調整をブラッシュアップしました。
自動露出調整に加えて、5段階調整できるようにしました。

また、先ほど述べたように、435-482行の割り込み制御関数全てにIRAM_ATTR属性をつけました。
これが無いと、アクセスポイントの電源を切った時にESP32が強制リセットしてしまうので注意です。

コンパイル書き込み実行

では、事前にWiFi環境を整えておき、コンパイル書き込み実行させます。
Arduino IDEのボード設定はテスト用スケッチと同様でOKです。

しばらく経って、M5Stackも再起動させます。

そして、M5Stackの「Stream」ボタンを押します。

ボタン操作はテスト用スケッチと同じで、以下の感じです。

 

ボタンを長押しすると、こんな感じになります。

冒頭の動画を見てもらえれば分かるのですが、左のボタンを長押しして、Intervalを切り替えられます。
パソコンやスマホのブラウザに表示させて、interval値を高速に変更したあと、M5Stack表示に切り替えると、動画処理が追い付かなくて、カクカクしてしまいます。
その時にこのボタンを押してinterval値を変えれば、動画が安定します。

中央のボタンを長押しすると、M5Cameraを強制リセットできます。

右のボタンを長押しすると、RGB565の2byte(16bit)データのエンディアンを変えられます。
MSBとLSBが逆になります。

M5Stackからスマホまたはパソコンブラウザへ表示を切り替える

冒頭の動画のように、M5Stackの動画ストリーミングからスマホまたはパソコンブラウザへ表示を切り替えられます。
もうほぼハックしているようなもんです。

M5Stack動画表示中に、スマホのブラウザからM5CameraのIPアドレスを入力して、画面が表示されたら、画面が小さいので拡大表示します。
そうしたら、一旦「Stop Stream」ボタンを押してから、「Start Stream」を押すとブラウザ表示されます。

すると、下図のように表示されます。

「Change Endian」ボタンを押すと、正常に表示されます。

「Change Interval」ボタンを押すと、フレームレートが上がります。
私の環境の場合6fps→8fpsまで上げられました。

あとは、動画にあるように色々なデバイスでカメラ表示を切り替えてみると面白いと思います。

WiFi長時間動画ストリーミング運転していると意味不明な動作出現

もう、何も分からんです!

ここ2か月以上、謎の動作に悩まされ続けました。
ESP32発売初期から何度も悩まされましたが、Arduino core ESP32でプログラミングして、完璧動作したと思っても、数時間連続稼働していると、いつも必ず謎の動作停止や意味不明な動作が出現します。
自分が勉強不足なので仕方ないけど、趣味用途ではこれ以上の追及は無理です。
Arduino core ESP32 のWiFi は短時間動作か、フリーズした時には強制リセットに限りますね。

さて、TCP/IP の規格によれば、長時間サーバーにアクセスが無ければ、タイムアウトする設定らしいです。
これは、Arduino core for the ESP32 でもタイムアウト設定は変更可能なようです。

今回の実験では、M5Stackのボタン操作で制御コマンドを送信する分には、1分毎にPingPongのやり取りがあるので、タイムアウトしません。

しかし、動画ストリーミングの場合は、1時間で停止してしまう時もあれば、4時間以上動き続ける時もありました。
起動当初は4fps出ていたのですが、40分経過すると、0~2fpsにまで落ち込んだりしました。

ただ、動画ストリーミングが停止した後、20分くらい放置していたら突然復活したなんてことも多々ありました。
ルーターのDHCP設定の影響ではありませんでした。

まったく意味不明です。
タイムアウトしているのかも不明でした。

そして、もう一つ不思議なのが、テスト用 MJPEG (BMP)動画ストリーミングプログラムでM5Stackに送信していた時、100分くらいは順調に12fpsくらいの速度で動画表示していたんですが、そこから動画が停止してしまいました。
データ送信も停止してしまいました。
しばらく20分くらい放置していると、ふと送信側のESP32からはデータ送信し始めました。
でも、M5StackのLCD動画が動きませんでした。

自分の単なる想像ですが、おそらく動画ストリーミング用のポート81番もPingPongの送受信をしないとダメなのかもしれません。
でも、不思議なのが、TCP規格のタイムアウトが原因ならば、なぜM5Cameraのイメージセンサ動画ストリーミングは4時間以上も稼働できたんでしょうか?
フレームレートが遅かったからでしょうか?
とにかく、何も分からんです。。。

ただ、port 80番のボタン操作で再スタートすると復旧するので、個人的にはヨシとします。
ドロ沼にハマりたくないので、深く追求しない事にします。
いずれにしても、これはあまり信頼性を求められる場所には使えないですね。
あくまでも、趣味用途ですね。
この謎が解る方がいらっしゃったら、教えて下さーーい!

通信距離について

私の環境、ホテルルーター使用の場合は、6~7m離れた部屋で扉を閉めていても、WiFi電波は届きました。
動画はカクカクして、スムースには表示されませんでしたが、そこそこ監視カメラとしては機能しそうでした。
UDPだと、パケットロスが激しくて、益々CPUが発熱してしまうので、やはり動画ストリーミングはTCPが良いようです。

WiFiルーターがもっと強力なものだったら、もっと遠い距離でも通信OKだと思われます。
ただ、冒頭の動画にあるように、他のデバイスで簡単にハックできてしまうので、セキュリティは脆弱です。
セキュリティを考えなければ、そこそこ使えそうです。
暗号化通信は今後の課題ですね。

編集後記

疲れました!

書くこと多すぎ! 反省!
5回分くらいの記事を書いた気がしています。
全然終わらなくて、途中で何度も投げだしそうになりました。

でも、TCP動画ストリーミングを自力でプログラミングすると、いろいろ発見があって、記事がダラダラ長くなってしまうんです。
それだけ、TCPはUDPの比ではない複雑さがあるということです。

このArduino core ESP32のTCPストリーミングは、UDPのようなパケットロス処理にそれほど神経を使いません。
ライブラリ任せのところが大きいのです。
パケットロスをライブラリで処理して、ある程度到達保証してくれるため、余分なCPU発熱が無く、なかなか良いと思います。
さすが、長い年月で先人が作り上げたシステムだと思いますね。

あと、欲を言えば、Arduino core ESP32 のWiFi通信が長時間でもガッツリ安定して、意図したとおりに動いてくれれば文句ないのですが、、、。
でも、自分自身のプログラミングミスかもしれないので、何も言えません。。。

ということで、このTCPストリーミング通信ができるようになると、SSL通信も夢ではないですね。
今後の課題です。

さて、いつディープラーニングを始められるのやら。。。

ではまた。。。

コメント

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