OV2640のJPEG出力でM5CameraとM5StackのWiFi動画ストリーミング実験

M5CameraのOV2640からJPEG出力させ、M5StackへWiFi動画ストリーミングする実験。高フレームレートです。 M5Stack

送信側 M5Camera のスケッチ(プログラムソースコード)

では、まず送信側 M5Camera のスケッチを紹介します。
とにかく素人コードですから、無駄が多いですし、コーディングスタイルもバラバラだし、誤りがあるかもしれません。
あまり参考にしない方が良いですよ!
ただ、何かお気づきの点があればコメント投稿でご連絡いただけると助かります。
大まかな解説は後で述べています。

因みに、56行~72行のコメントアウトしているGPIOピンアサインは、ESP32-DevKitC と Arducam B0011 を接続した場合のピンアサインです。

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

/* This is a program modified by mgo-tec from the esp32-camera library and CameraWebServer sketch.
 * 
 * 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>
#include <img_converters.h>

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

const uint8_t ov2640_i2c_addrs = 0x30;

//use M5Camera
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;

/*
//use ESP32-DevKitC and Arducam BOO11
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;
*/
uint8_t camera_pid = 0;
const uint16_t sensor_resolution_h = 400, sensor_resolution_v = 296; //CIF mode

uint16_t out_camera_w = 240;
uint16_t out_camera_h = 176;
uint16_t jpg_buf_size = out_camera_w * 2 * out_camera_h;
size_t jpg_buf_len = 0;
uint8_t *jpg_buf = NULL;

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;

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

const uint8_t dma_desc_count = 4;
uint8_t dma_desc_cur = 0;
uint8_t read_desc_cur = 0;
uint32_t jpg_buf_cnt = 0;
uint8_t dma_filtered_count = 0;

intr_handle_t i2s_intr_handle;
esp_err_t err = ESP_OK;

static inline void IRAM_ATTR i2s_conf_reset();
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);
static void IRAM_ATTR test_digitalWrite(uint8_t pin, uint8_t val);

httpd_handle_t stream_httpd = NULL;
httpd_handle_t camera_httpd = NULL;

bool isI2Sisr = false;
bool canStartStream = false;
bool canSendImage = false;
bool isWiFiConnected = false;
bool isCloseConnection = false;
bool shouldStartBus = false;
bool isStartedBus = false;
bool shouldStopBus = false;
bool isChangeFramesize = false;
bool isPassIncDescCur = false;
bool canResetDmaDesc = false;

uint32_t fps_timer = 0;
uint8_t fps_count = 0;
uint8_t frame_size_num = 5;
uint8_t quality = 10;
uint8_t pclk_div2 = 32; // 48MHz/pclk_div2

const char *stream_content_type = "multipart/x-mixed-replace;boundary=--myboundary";
const char *stream_boundary = "\r\n--myboundary\r\n";
const char *stream_part = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";

//********CPU core 1 task********************
void setup() {
  Serial.begin(115200);
  Serial.println();
  delay(1000);
  pinMode(4, OUTPUT);
  //jpg_buf = (uint8_t *)ps_malloc(sizeof(uint8_t) * jpg_buf_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, out_camera_w, out_camera_h);
  initCameraDMA();
  changeAutoWhiteBalance(1);
  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 getCameraBuf(){
  if(shouldStartBus){
    isStartedBus = i2s_start_bus();
    shouldStartBus = false;
  }
  if(shouldStopBus){
    i2s_stop_bus();
    shouldStopBus = false;
  }

  if(isStartedBus){
    if(isI2Sisr){
      if(dma_filtered_count == 0){
        jpg_buf_cnt = 0;
      }
      //Serial.printf("desc=%d,read=%d\r\n",dma_desc_cur,read_desc_cur);
      //FIFOから読み出し、jpg_bufへ格納
      lldesc_t* test_desc = &dma_desc[read_desc_cur];
      uint32_t cnt = 0;
      uint8_t dummy = 0;
      for(int i = 2; i < dma_desc_buf_size; i+=4) {
        if(jpg_buf_cnt >= jpg_buf_size) break;
        if(canSendImage){
          dummy = (uint8_t) * (test_desc->buf + i);
        }else{
          jpg_buf[jpg_buf_cnt++] = (uint8_t) * (test_desc->buf + i);
          cnt++;
        }
      }
      isI2Sisr = false;

      if(!canSendImage){
        if(dma_filtered_count == 0){
          //JPEG開始マーカーFFD8の検出
          uint32_t head = *((uint32_t *)jpg_buf);
          if(head == 0xE0FFD8FF){
            //FF,E0:marker JFIF形式, FF,E1:EXIF形式
            dma_filtered_count++;
          }
        }

        if(dma_filtered_count){
          //JPEG終了マーカーFFD9の検出
          int32_t cd = jpg_buf_cnt - 1;
          uint8_t *bf = &jpg_buf[jpg_buf_cnt - 1];
          uint32_t now_ptr = jpg_buf_cnt - cnt;
          while(bf >= (jpg_buf + now_ptr)){
            if(bf[0] == 0xff && bf[1] == 0xd9){
              jpg_buf_len = cd + 2;
              canSendImage = true;
              if(isChangeFramesize == true){
                setFramesize(frame_size_num);
                isChangeFramesize = false;
              }
              return;
            }
            bf--;
            cd--;
          }
          dma_filtered_count++;
        }

        if(jpg_buf_cnt >= jpg_buf_size){
          dma_filtered_count = 0;
          canResetDmaDesc = true;
          Serial.println("bad frame!");
          if(isChangeFramesize == true){
            setFramesize(frame_size_num);
            isChangeFramesize = false;
          }
          return;
        }
      }
    }
  }
}
//****************************************
static esp_err_t stream_handler(httpd_req_t *req){
  esp_err_t res = ESP_OK;
  char part_buf[64];

  res = httpd_resp_set_type(req, stream_content_type);
  if(res != ESP_OK){
    return res;
  }
  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");

  esp_err_t res1 = ESP_FAIL, res2 = ESP_FAIL, res3 = ESP_OK;

  uint32_t time_out = millis();
  while(true){
    if(canStartStream){
      if(canSendImage){
        if(res3 == ESP_OK){
          res1 = httpd_resp_send_chunk(req, stream_boundary, strlen(stream_boundary));
          time_out = millis();
          res3 = ESP_FAIL;
        }
        if(res1 == ESP_OK){
          size_t hlen = snprintf((char *)part_buf, 64, stream_part, jpg_buf_len);
          res2 = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
          time_out = millis();
          res1 = ESP_FAIL;
        }
        if(res2 == ESP_OK){
          res3 = httpd_resp_send_chunk(req, (const char *)&jpg_buf[0], jpg_buf_len);
          time_out = millis();
          canSendImage = false;
          dma_filtered_count = 0;
          canResetDmaDesc = true;
          fps_count++;
          res2 = ESP_FAIL;
        }
      }
    }
    if(millis() - time_out > 5000){
      canSendImage = false;
      canResetDmaDesc = true;
      time_out = 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, "var", id_txt, sizeof(id_txt)) == ESP_OK &&
        httpd_query_key_value(buf, "val", 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, "quality")) {
    quality = val;
    changeQuality(val);
  }else if(!strcmp(id_txt, "framesize")){
    frame_size_num = val;
    isChangeFramesize = true;
  }else if(!strcmp(id_txt, "pclk_div")){
    pclk_div2 = val;
    changePclkDivider(val);
  }else if(!strcmp(id_txt, "start_stream")){
    canSendImage = false;
    canStartStream = true;
    shouldStartBus = true;
    isCloseConnection = false;
  }else if(!strcmp(id_txt, "stop_stream")){
    canStartStream = false;
    shouldStopBus = true;
    shouldStartBus = 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>\r\n";
          html_body += "<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'>\r\n";
          html_body += "</head><body>\r\n";
          html_body += "<img id='pic_place' 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>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='stopStream()'>Window Stop</button></p>\r\n";
          html_body += "<p><button style='border-radius:25px;' onclick='changeCtrlCam(\"framesize\",0)'>96 x96</button>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='changeCtrlCam(\"framesize\",3)'>160 x 120</button>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='changeCtrlCam(\"framesize\",5)'>240 x 176</button></p>\r\n";
          html_body += "<p><button style='border-radius:25px;' onclick='changeCtrlCam(\"quality\",10)'>Qs 10</button>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='changeCtrlCam(\"quality\",60)'>Qs 60</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 += "</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 stopStream(){\r\n";
          html_body += "window.stop();};\r\n";
          html_body += "function changeCtrlCam(id_txt, value_txt){\r\n";
          html_body += "var new_url = base_url+'/control?var=';\r\n";
          html_body += "new_url += id_txt + '&';\r\n";
          html_body += "new_url += 'val=' + 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 inline void IRAM_ATTR i2s_conf_reset(){
  //これを実行すると、FIFOメモリがリセットされるらしい
  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_isr(void* arg){
  GPIO.status1_w1tc.val = GPIO.status1.val;
  GPIO.status_w1tc = GPIO.status;
  if(canResetDmaDesc){
    dma_desc_cur = 0;
    I2S0.conf.rx_start = 0;
    I2S0.in_link.start = 0;
    I2S0.int_clr.val = I2S0.int_raw.val;
    i2s_conf_reset();
    I2S0.rx_eof_num = dma_desc_buf_size;
    I2S0.in_link.addr = (uint32_t)&dma_desc[dma_desc_cur];
    I2S0.in_link.start = 1;
    I2S0.conf.rx_start = 1;
    isPassIncDescCur = true;
    canResetDmaDesc = false;
  }
}
//**********************************************
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){
  test_digitalWrite(4, HIGH);
  I2S0.int_clr.val = I2S0.int_raw.val;
  //vsync_isr直後のdesc_curカウントアップはスルーする
  if(isPassIncDescCur){
    isPassIncDescCur = false;
  }else{
    read_desc_cur = dma_desc_cur;
    dma_desc_cur = (dma_desc_cur + 1) % dma_desc_count;
    isI2Sisr = true;
  }
  test_digitalWrite(4, LOW);
}

static void IRAM_ATTR test_digitalWrite(uint8_t pin, uint8_t val){
  if(val) {
    if(pin < 32) {
      GPIO.out_w1ts = ((uint32_t)1 << pin);
    } else if(pin < 34) {
      GPIO.out1_w1ts.val = ((uint32_t)1 << (pin - 32));
    }
  } else {
    if(pin < 32) {
      GPIO.out_w1tc = ((uint32_t)1 << pin);
    } else if(pin < 34) {
      GPIO.out1_w1tc.val = ((uint32_t)1 << (pin - 32));
    }
  }
}
//****************************************
bool i2s_start_bus(){
  dma_desc_cur = 0;
  read_desc_cur = 0;
  dma_filtered_count = 0;
  canResetDmaDesc = true;

  esp_intr_disable(i2s_intr_handle);
  i2s_conf_reset();

  I2S0.rx_eof_num = dma_desc_buf_size;
  I2S0.in_link.addr = (uint32_t) &dma_desc[0];
  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();
  return true;
}
//****************************************
void i2s_stop_bus(){
  dma_filtered_count = 0;
  read_desc_cur = 0;
  esp_intr_disable(i2s_intr_handle);
  vsync_intr_disable();
  i2s_conf_reset();
  I2S0.conf.rx_start = 0;
}
//****************************************
int setFramesize(uint8_t frame_size_num){
  if(jpg_buf){
    free(jpg_buf);
    jpg_buf = NULL;
  }
  switch(frame_size_num){
    case 0:
      out_camera_w = 96;
      out_camera_h = 96;
      break;
    case 1:
      out_camera_w = 80;
      out_camera_h = 160;
      break;
    case 2:
      out_camera_w = 160;
      out_camera_h = 80;
      break;
    case 3:
      out_camera_w = 160;
      out_camera_h = 120;
      break;
    case 4:
      out_camera_w = 192;
      out_camera_h = 144;
      break;
    case 5:
      out_camera_w = 240;
      out_camera_h = 176;
      break;
    default:
      break;
  }
  jpg_buf_size = out_camera_w * 2 * out_camera_h;
  jpg_buf = (uint8_t *)malloc(jpg_buf_size);

  Serial.println("my setFramesize() IN");
  int ret = 0;
  writeSCCB(0xFF, 0x00); //BANK:DSP
  writeSCCB(0x05, 0x01); //R_BYPASS: bit[0]:0x01 Bypass DSP, sensor out directly
  ov2640_settings_to_cif();

  //set_window-------------------------------
  ret = writeSCCB(0xFF, 0x00);//bank dsp
  if (!ret) {
    ret = writeSCCB(0x05, 0x00); //R_BYPASS:0x00 DSP use. 
    if(ret) return ret;
  }
  delay(5);

  //これは必ず0x02でなければならない
  uint8_t pclk_div = 0x02; //REG32 Common Control. PCLK frequency divide by 2(0x02=1/2, 0x03=1/4)

  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_div << 6) | (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?
  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_camera_w / 4.0);
  uint16_t OUTH = (uint16_t)floor((double)out_camera_h / 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, 0x20 | 0x1d); //CTRL2 ,(CTRL2_DCW_EN | 0x1D), 0b00111101
  writeSCCB(0x50, 0x80 | 0x00); //CTRLl CTRLI_LP_DP=0x80 | 0x00, 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(0xFF, 0x01); //BANK:sensor
  writeSCCB(0x11, 0x00); //CLKRC: bit[7]:Internal frequency doublers OFF, clk=XVCLK/(CLKRC[5:0] + 1)
  writeSCCB(0xFF, 0x00); //BANK:DSP
  //--------PCLK clock divider setting-----------------
  //※ブレットボード上では0b00100000~0b00001000が限界
  //M5Cameraの場合、4(PCLK 10MHz)以上。16(0x10)以上が安定
  writeSCCB(0xD3, pclk_div2); //R_DVP_SP: bit[7]:auto mode, bit[6:0] PCLK system_clock 48MHz/bit[6:0](YUV)
  //---------------------------------------------------
  writeSCCB(0x05, 0x00); //R_BYPASS:0x00 DSP use.
  writeSCCB(0xE0, 0x00); //RESET
  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
  i2s_conf_reset();
  // 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_ERROR_CHECK(esp_intr_alloc(ETS_I2S0_INTR_SOURCE,
                   ESP_INTR_FLAG_INTRDISABLED | ESP_INTR_FLAG_LOWMED | 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_desc_buf_size);
  //dma_desc_count = 4; DMAディスクプリタ用メモリ4つ分の領域確保
  dma_desc = (lldesc_t*) malloc(sizeof(lldesc_t) * dma_desc_count);
  if (dma_desc == NULL) {
    return ESP_ERR_NO_MEM;
  }
  Serial.printf("Allocating DMA buffer size=%d\r\n", dma_desc_buf_size);

  for(int i = 0; i < dma_desc_count; ++i){
    lldesc_t* pd = &dma_desc[i];
    pd->length = dma_desc_buf_size;
    pd->size = pd->length;
    pd->owner = 1;
    pd->sosf = 1;
    pd->buf = (uint8_t*) malloc(dma_desc_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[(i + 1) % dma_desc_count];
  }
  Serial.printf("System Free Heap Size = %d\r\n", esp_get_free_heap_size());

  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, 0b10000000); //COM7:SRST System Reset & CIF mode
    if(ret) return ret;
  }
  delay(10);
  writeRegisterCIF();
  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(frame_size_num) != 0) {
    Serial.println("Failed to set frame size");
    err = ESP_FAIL;
    goto fail;
  }

  ov2640_settings_jpeg3();
  changeQuality(quality);

  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 initCameraDMA(){
  Serial.println("taskDMA Start!!");
  initI2S();
  err = initDMAdesc();
  if (err != ESP_OK) {
    Serial.println("Failed to initialize I2S and DMA");
  }
  vsync_intr_disable();
  //ESP_INTR_FLAG_LEVEL1: Accept a Level 1 interrupt vector (lowest priority)
  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);
  }
}
//***************************************
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(){
  //esp32-camera library. ov2640_settings_cif
  Serial.println("writeRegisterCIF() esp32-camera library.");
  writeSCCB(0xff, 0x00);
  delay(5);
  writeSCCB(0x2c, 0xff);
  writeSCCB(0x2e, 0xdf);
  delay(5);
  writeSCCB(0xff, 0x01);
  delay(5);
  writeSCCB(0x3c, 0x32);
  //
  writeSCCB(0x11, 0x01); //CLKRC 1/2 clock divider
  writeSCCB(0x09, 0x02); //COM2, COM2_OUT_DRIVE_3x
  writeSCCB(0x04, 0b00101000); //REG04: bit[7]Horizontal Mirror, bit[6]Vertical Flip
  writeSCCB(0x13, 0b11100101); //COM8:default C7, [2]AGC 0:manual 1:auto [0]AEC(自動露出) 0:manual 1:auto
  writeSCCB(0x14, 0b01001000); //COM9(AGC gain ceiling),COM9_AGC_GAIN_8x
  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(0x06, 0x88);
  writeSCCB(0x07, 0xc0);
  writeSCCB(0x0d, 0x87); //COM4
  writeSCCB(0x0e, 0x41);
  writeSCCB(0x4c, 0x00);
  writeSCCB(0x4a, 0x81);
  writeSCCB(0x21, 0x99);
  writeSCCB(0x24, 0x40); //AEW
  writeSCCB(0x25, 0x38); //AEB
  writeSCCB(0x26, 0b10000010); //VV Fast Mode Large Step Threshold. [7:4]High threshold, [3:0]Low threshold, (8<<4)|(2&0x0f)
  writeSCCB(0x5c, 0x00);
  writeSCCB(0x63, 0x00);
  writeSCCB(0x61, 0x70); //HISTO_LOW
  writeSCCB(0x62, 0x80); //HISTO_HIGH
  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(0x3d, 0x34);
  writeSCCB(0x5a, 0x57);
  writeSCCB(0x4f, 0xbb); //BD50
  writeSCCB(0x50, 0x9c); //BD60
  writeSCCB(0x12, 0x20); //COM7, COM7_RES_CIF
  writeSCCB(0x17, 0x11); //HSTART
  writeSCCB(0x18, 0x43); //HSTOP
  writeSCCB(0x19, 0x00); //VSTART
  writeSCCB(0x1A, 0x25); //VSTOP
  writeSCCB(0x32, 0x89); //REG32
  writeSCCB(0x37, 0xc0);
  writeSCCB(0x4F, 0xca); //BD50
  writeSCCB(0x50, 0xa8); //BD60
  writeSCCB(0x6d, 0x00);
  writeSCCB(0x3d, 0x38);

  writeSCCB(0xff, 0x00); //BANK_SEL, BANK_DSP
  writeSCCB(0xe5, 0x7f);
  writeSCCB(0xF9, 0x80 | 0x40); //MC_BIST, MC_BIST_RESET | MC_BIST_BOOT_ROM_SEL
  writeSCCB(0x41, 0x24);
  writeSCCB(0xE0, 0x10 | 0x04); //RESET, RESET_JPEG | RESET_DVP
  writeSCCB(0x76, 0xff);
  writeSCCB(0x33, 0xa0);
  writeSCCB(0x42, 0x20);
  writeSCCB(0x43, 0x18);
  writeSCCB(0x4c, 0x00);
  writeSCCB(0x87, 0x40 | 0x10 ); //CTRL3, CTRL3_WPC_EN | 0x10
  writeSCCB(0x88, 0x3f);
  writeSCCB(0xd7, 0x03);
  writeSCCB(0xd9, 0x10);
  writeSCCB(0xD3, 0x80 | 0x02); //R_DVP_SP, R_DVP_SP_AUTO_MODE | 0x02
  writeSCCB(0xc8, 0x08);
  writeSCCB(0xc9, 0x80);
  writeSCCB(0x7C, 0x00); //BPADDR
  writeSCCB(0x7D, 0x00); //BPDATA
  writeSCCB(0x7C, 0x03); //BPADDR
  writeSCCB(0x7D, 0x48); //BPDATA
  writeSCCB(0x7D, 0x48); //BPDATA
  writeSCCB(0x7C, 0x08); //BPADDR
  writeSCCB(0x7D, 0x20); //BPDATA
  writeSCCB(0x7D, 0x10); //BPDATA
  writeSCCB(0x7D, 0x0e); //BPDATA
  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(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(0xC3, 0xfd); //CTRL1
  writeSCCB(0x7f, 0x00);
  writeSCCB(0xe5, 0x1f);
  writeSCCB(0xe1, 0x67);
  writeSCCB(0xdd, 0x7f);
  writeSCCB(0xDA, 0x00); //IMAGE_MODE
  writeSCCB(0xE0, 0x00); //RESET
  writeSCCB(0x05, 0x00); //R_BYPASS, R_BYPASS_DSP_EN
  writeSCCB(0, 0);
}

void ov2640_settings_to_cif() {
  Serial.println("ov2640_settings_to_cif() IN");
  writeSCCB(0xff, 0x01);
  writeSCCB(0x12, 0x20); //COM7=0x12,COM7_RES_CIF=0x20

  //Set the sensor output window
  writeSCCB(0x03, 0x0A); //COM1

  writeSCCB(0x32, 0x89); //REG32=0x32,REG32_CIF=0x89
  writeSCCB(0x17, 0x11); //HSTART=0x17
  writeSCCB(0x18, 0x43); //HSTOP=0x18
  writeSCCB(0x19, 0x00); //VSTART=0x19
  writeSCCB(0x1A, 0x25); //VSTOP=0x1A

  //{CLKRC, 0x00);
  writeSCCB(0x4f, 0xca); //BD50=0x4f
  writeSCCB(0x50, 0xa8); //BD60=0x50

  writeSCCB(0x5a, 0x23);
  writeSCCB(0x6d, 0x00);
  writeSCCB(0x3d, 0x38);
  writeSCCB(0x39, 0x92);
  writeSCCB(0x35, 0xda);
  writeSCCB(0x22, 0x1a);
  writeSCCB(0x37, 0xc3);
  writeSCCB(0x23, 0x00);
  writeSCCB(0x34, 0xc0); //ARCOM2=0x34
  writeSCCB(0x06, 0x88);
  writeSCCB(0x07, 0xc0);
  writeSCCB(0x0d, 0x87); //COM4=0x0d
  writeSCCB(0x0e, 0x41);
  writeSCCB(0x4c, 0x00);

  writeSCCB(0xff, 0x00);
  writeSCCB(0xE0, 0x04); //RESET=0xE0, RESET_DVP=0x04

  //Set the sensor resolution (UXGA, SVGA, CIF)
  writeSCCB(0xc0, 0x32); //HSIZE8=0xc0
  writeSCCB(0xc1, 0x25); //VSIZE8=0xc1
  writeSCCB(0x8c, 0x00); //SIZEL=0x8c

  //Set the image window size >= output size
  writeSCCB(0x51, 0x64); //HSIZE=0x51
  writeSCCB(0x52, 0x4a); //VSIZE=0x52
  writeSCCB(0x53, 0x00); //XOFFL=0x53
  writeSCCB(0x54, 0x00); //YOFFL=0x54
  writeSCCB(0x55, 0x00); //VHYX=0x55
  writeSCCB(0x57, 0x00); //TEST=0x57

  writeSCCB(0x86, 0x20 | 0x1D); //CTRL2=0x86, CTRL2_DCW_EN=0x20
  writeSCCB(0x50, 0x80 | 0x00); //CTRLI=0x50, CTRLI_LP_DP=0x80
  //{R_DVP_SP, 0x08},
  writeSCCB(0, 0);
}

void ov2640_settings_jpeg3() {
  Serial.println("writeRegisterJPEG");
  writeSCCB(0xff, 0x00); //bank dsp
  writeSCCB(0xE0, 0b00010100); //RESET JPEG DVP
  writeSCCB(0xDA, 0b00010010); //IMAGE_MODEBit[4]:jpeg output, Bit[1]:HREF=VSYNC

  writeSCCB(0xD7, 0x03);
  writeSCCB(0xE1, 0x77);
  writeSCCB(0xE5, 0x1F);
  writeSCCB(0xD9, 0x10);
  writeSCCB(0xDF, 0x80);
  writeSCCB(0x33, 0x80);
  writeSCCB(0x3C, 0x10);
  writeSCCB(0xEB, 0x30);
  writeSCCB(0xDD, 0x7F);

  writeSCCB(0xE0, 0b00000000); //RESET
  writeSCCB(0, 0);
  delay(10);
}
//****************************************
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;
  }
}
//****************************************
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);
}

void changeQuality(uint8_t val){
  writeSCCB(0xff, 0x00);
  uint8_t Qs = readSCCB(0x44); //0x44 Quantization Scale Factor
  Serial.printf("Old Qs=%d\r\n", Qs);
  Serial.println(Qs);
  Qs = val;
  if(Qs > 63){
    Qs = 63;
  }else if(Qs < 10){
    Qs = 10;
  }
  writeSCCB(0x44, Qs);
  Serial.printf("New Qs=%d\r\n", Qs);
}

void changePclkDivider(uint8_t val){
  val = val + 1;
  pclk_div2 = pow(2, val);
  Serial.printf("pclk_div2 = %d\r\n", pclk_div2);
  setFramesize(frame_size_num);
}

受信側 M5Stack のスケッチ(プログラムソースコード)

次に、受信側 M5Stack のスケッチを紹介します。
同じく素人コードですから、無駄が多いですし、コーディングスタイルもバラバラだし、誤りがあるかもしれません。
あまり参考にしない方が良いですよ!
ただ、何かお気づきの点があればコメント投稿でご連絡いただけると助かります。
大まかな解説は後で述べています。

因みに、19行目のローカルIPアドレスは、先の送信側M5Cameraをコンパイル書き込み実行させて、シリアルモニターを起動すると、M5Camera自体のローカルIPアドレスが表示されますので、それに書き換えてください。

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

/* 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  
 */
#define MGO_TEC_BV1_M5STACK_SD_SKETCH
#include <mgo_tec_bv1_m5stack_sd_simple1.h> //ESP32_mgo_tec library. beta ver 1.0.71
#include <WiFi.h>
#include <rom/tjpgd.h> //TJpgDec – Tiny JPEG Decompressor. (C)ChaN, 2011

const char* utf8sjis_file = "/font/Utf8Sjis.tbl"; //UTF8 Shift_JIS 変換テーブルファイル名を記載しておく
const char* shino_half_font_file = "/font/shnm8x16.bdf"; //半角フォントファイル名を定義
const char* shino_full_font_file = "/font/shnmk16.bdf"; //全角東雲フォントファイル

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

const char* host = "192.168.0.22"; //M5Camera(相手先サーバー)のアドレス

enum SelCamCtrl {
  PING80 = 0,
  CHANGE_PCLK = 10,
  CHANGE_AEC = 20,
  CHANGE_FRAMESIZE = 100,
  CHANGE_JPG_QUALITY = 110,
  STOP_STREAM = 200, START_STREAM = 201,
  RESET_CAM = 255,
} SelectCamCtrl = PING80;

enum StateStream{
  OFF_STREAM = 0, ON_STREAM = 1, CLOSE_CONNECTION = 2
} StatusStream = OFF_STREAM;

uint8_t pclk_div_cnt = 4;
int8_t exposure_count = 0;
const char pclk_div_c[6][6] = {"div 2", "div 4", "div 8", "div16", "div32", "div64"};
const char aec_c[6][6] = {"Auto "," - 2 "," - 1 "," +-0 "," + 1 "," + 2 "};
const char stream_c[3][6] = {"Stop ", "Start", "Intrv"};
char framesize_c[6][10] = {" 96 x 96 ", "80 x 160", "160 x 80", "160 x 120", "192 x 144", "240 x 176"};
uint8_t frame_size_num = 5; //default 240 x 176

uint32_t fps_timer = 0;
uint8_t fps_count = 0;
uint32_t ping80_lasttime = 0;
bool isWiFiConnected = false;
bool isPort80Handshake = false;
bool isPort81Handshake = false;
bool canSendCamCtrl = false;
bool canDisplayLCD = false;
//-------------------------
uint8_t offset = 1;
uint16_t btnA_x0 = 0, btnA_x1 = 107;
uint16_t btnB_x0 = 107, btnB_x1 = 212;
uint16_t btnC_x0 = 212, btnC_x1 = 319;
uint16_t btn_y0 = 197, btn_y1 = 233;
//----------------------
typedef struct {
    const void *fp;          /* File pointer for input function */
    BYTE *fbuf = NULL;     /* Pointer to the frame buffer for output function */
    size_t fbuf_size = 240 * 2 * 176;
    UINT wfbuf = 240;    /* Width of the frame buffer [pix] */
    UINT hfbuf = 176;
} IODEV;

void *work = NULL;       /* Pointer to the decompressor work area */
JDEC jdec;        /* Decompression object */
JRESULT res;      /* Result code of TJpgDec API */
IODEV devid;      /* User defined device identifier */

uint8_t *jpg_buf = NULL;
uint32_t jpg_len = 0;
uint32_t jpg_buf_seek = 0;
uint8_t quality = 10;
uint16_t old_w = 240;
uint16_t old_h = 176;

//********CPU core 1 task********************
void setup(){
  Serial.begin(115200);
  delay(1000);

  initDisplay();
  TaskHandle_t taskClientCtrl_handl, taskClientStrm_handl;
  xTaskCreatePinnedToCore(&taskClientControl, "taskClientControl", 4096, NULL, 5, &taskClientCtrl_handl, 0);
  xTaskCreatePinnedToCore(&taskClientStream, "taskClientStream", 8192, NULL, 20, &taskClientStrm_handl, 0);

  while(!isWiFiConnected){
    Serial.print('.');
    delay(500);
  }
  connectedWifiDisp();
  while(!isPort81Handshake){ //ESP32サーバーとのMJPEGハンドシェイクが終わるまで待つ
    actionButton();
    delay(1);
  }
}

void loop(){
  if(canDisplayLCD){
    if(old_w == jdec.width && old_h == jdec.height){
      log_v("w=%d, h=%d, size=%d\r\n", jdec.width, jdec.height, devid.fbuf_size);
      log_v("befor LCD heap=%d\r\n", esp_get_free_heap_size());
      LCD.drawPixel65kColRGB565Bytes(offset, offset, jdec.width, jdec.height, (uint8_t*)devid.fbuf, devid.fbuf_size);
      if(devid.fbuf){
        free(devid.fbuf);
        devid.fbuf = NULL;
      }
      log_v("after LCD heap=%d\r\n", esp_get_free_heap_size());
      canDisplayLCD = false;
    }else{
      displayFrameSize(jdec.width, jdec.height);
      LCD.displayClear(0, 0, 240 + offset, 176 + offset);
      LCD.drawRectangleLine(0, 0, devid.wfbuf + offset, devid.hfbuf + offset, 31, 63, 31);
      old_w = jdec.width, old_h = jdec.height;
    }
  }
  if((StatusStream == ON_STREAM) && (millis() - fps_timer > 1000)){
    Serial.printf("%d (fps)\r\n", fps_count);
    char fps_c[3], jpglen_c[7];
    sprintf(fps_c, "%2d", fps_count);
    if(jpg_len > 0 && jpg_len < 10000){
      sprintf(jpglen_c, "%6d", jpg_len);
    }
    mM5.disp_fnt[5].dispText( mM5.font[5], String(jpglen_c) );
    mM5.disp_fnt[6].dispText( mM5.font[6], String(fps_c) );
    fps_count = 0;
    fps_timer = millis();
  }

  actionButton(); //ボタン操作
}
//********CPU core 0 task********************
void taskClientControl(void *pvParameters){
  connectToWiFi();
  while(!isWiFiConnected){
    delay(1);
  }
  while(true){
    WiFiClient client80;
    connectClient80(client80);
    if (client80) {
      Serial.println("new client80");
      String get_request = "GET / HTTP/1.1\r\n";
      get_request += "Host: " + String(host);
      get_request += "\r\n";
      get_request += "Connection: keep-alive\r\n\r\n";

      while (client80.connected()){
        client80.print(get_request);
        get_request = "";
        String req_str;
        while(true){
          if(client80.available()){
            req_str =client80.readStringUntil('\n');
            if(req_str.indexOf("200 OK") > 0){
              Serial.println(req_str);
              req_str = "";
              if(!receiveToBlankLine(client80)){
                delay(1); continue;
              }
              Serial.println("complete server responce!");
              Serial.println("Go stream!");
              isPort80Handshake = true;
              ping80_lasttime = millis();
              while(true){
                changeCamControl(client80);
                sendPing80(client80, 60000);
                while(client80.available()){
                  Serial.write(client80.read());
                  delay(1);
                }
                if(!isPort80Handshake) break;
                delay(1);
              }
              goto exit_1;
            }
          }
          delay(1);
        }
        delay(1);
      }
    }
exit_1:
    while(client80.available()){
      Serial.write(client80.read());
      delay(1);
    }
    Serial.println("!!!!!!!!!!!!!!!!!!!! client80.stop");
    delay(10);
    client80.stop();
    client80.flush();
    delay(10);
  }
}
//********CPU core 0 task********************
void taskClientStream(void *pvParameters){
  while(true){
    if(isPort80Handshake){
      WiFiClient client81;
      if(!connectStreamClient(client81)){
        canSendCamCtrl = true;
        Serial.println("Loop Out... wait client81 available");
        delay(10);
        client81.stop();
        client81.flush();
        Serial.println("!!!!!!!!!!!!!!!!!!!!! client81.stop");
        delay(10);
        SelectCamCtrl = PING80;
        isPort80Handshake = false;
        StatusStream = CLOSE_CONNECTION;
      }
    }
    delay(1);
  }
}
//********************************************
bool connectStreamClient(WiFiClient &client81){
  int stream_port = 81;
  while(true){
    if (!client81.connect(host, stream_port)) {
      Serial.print(',');
    }else{
      Serial.printf("client81.connected(2) %s\r\n", host);
      break;
    }
    delay(500);
  }

  String get_request = "GET /stream HTTP/1.1\r\n";
  get_request += "Host: " + String(host);
  get_request += ":81\r\n";
  get_request += "Connection: keep-alive\r\n\r\n";

  client81.print(get_request);
  get_request = "";
  if(!receiveToBlankLine(client81)){
    Serial.println("connectStreamClient FALSE");
    return false;
  }
  Serial.println("stream header receive OK!");
  isPort81Handshake = true;
  StatusStream = ON_STREAM;
  return receiveStream(client81);
}
//*********************************************
bool receiveStream(WiFiClient &client81){
  uint32_t time_out;
  while(StatusStream == ON_STREAM){
jpg_rcv0:
    if(!receiveBoundary(client81)){
      delay(1); continue;
    }
    if(client81.available()){
      time_out = millis();
      while(StatusStream == ON_STREAM){
        if((char)client81.read() == '\n') {
          if((char)client81.read() == '\r'){
            if((char)client81.read() == '\n') {
              if((char)client81.read() == '\r'){
                if((char)client81.read() == '\n') {
                  String res_str = client81.readStringUntil('\n');
                  jpg_len = strtol(res_str.c_str(), NULL, 16);
                  log_v("jpg receive LEN:=%d\r\n", jpg_len);
                  if(jpg_len) {
                    jpg_buf = (uint8_t *)malloc(jpg_len);
                  }else{
                    delay(1);
                    goto jpg_rcv0;
                  }
                  break;
                }
              }
            }
          }
        }
        if(millis() - time_out > 1000){
          receiveBoundary(client81);
          time_out = millis();
          delay(1);
        }
      }
      receiveStreamJPG(client81);
      while(StatusStream == ON_STREAM){
        if(!canDisplayLCD) break;
        delay(1);
      }
    }
    delay(1);
  }
  return false;
}
//********************************************
bool receiveBoundary(WiFiClient &client81){
  char cstr[16] = {};
  while(StatusStream == ON_STREAM){
    if(client81.available()){
      if((char)client81.read() == '-'){
        if(client81.read((uint8_t*)cstr, 15)){
          //Serial.write(cstr);
          //Serial.println();
          if(strcmp(cstr, "-myboundary\r\n\r\n") == 0){
            return true;
          }
        }else{
          delay(1); continue;
        }
      }else{
        delay(1); continue;
      }
    }
    delay(1);
  }
  return false;
}
//********************************************
bool receiveStreamJPG(WiFiClient &client81){
  bool ret = false;
  if(!canDisplayLCD){
    if(isPort81Handshake){
      while(StatusStream == ON_STREAM){
        if(client81.available()) {   
          uint32_t ptr_addrs = 0;          
          uint32_t remain_bytes = jpg_len;
          int tmp = 0;
          log_v("before receive jpg heap=%d\r\n", esp_get_free_heap_size());
          while(true){
            if(StatusStream != ON_STREAM) return false;
            if(client81.available() > 0) {
              tmp = client81.read(jpg_buf + ptr_addrs, remain_bytes);
              //Serial.printf("tmp=%d\r\n",tmp);
              if(tmp < 0){
                delay(1); continue;
              }
              ptr_addrs += tmp;
              remain_bytes = remain_bytes - tmp;
              if(remain_bytes <= 0) break;
            }
            delay(1);
          }
          log_v("after receive jpg heap=%d\r\n", esp_get_free_heap_size());
          log_v("%02x, %02x\r\n", jpg_buf[0], jpg_buf[1]);
          ret = decodeJPG();
          log_v("after free(jpg_buf) heap=%d\r\n", esp_get_free_heap_size());
          return ret;
        }
        delay(1);
      }
    }
  }
  return ret;
}
//********************************************
void connectClient80(WiFiClient &client80){
  int httpPort = 80;
  while(true){
    if(SelectCamCtrl == START_STREAM){
      if (client80.connect(host, httpPort)) {
        Serial.printf("client80.connected! %s\r\n", host);
        canDisplayLCD = false;
        break;
      }else{
        Serial.print(',');
      }
    }
    delay(1);
  }
}
//********************************************
void sendPing80(WiFiClient &client80, uint32_t interval_time){
  if((millis() - ping80_lasttime) > interval_time){
    sendCamCtrlRequest(client80, "ping80", "0");
    ping80_lasttime = millis();
  }
}
//********************************************
void changeCamControl(WiFiClient &client80){
  if(canSendCamCtrl){
    String id_str = "", val_str = "0";
    switch(SelectCamCtrl){
      case CHANGE_PCLK:
        id_str = "pclk_div", val_str = String(pclk_div_cnt);
        break;
      case CHANGE_AEC:
        id_str = "aec", val_str = String(exposure_count);
        break;
      case CHANGE_FRAMESIZE:
        id_str = "framesize";
        val_str = String(frame_size_num);
        break;
      case CHANGE_JPG_QUALITY:
        id_str = "quality", val_str = String(quality);
        break;
      case STOP_STREAM:
        id_str = "stop_stream";
        StatusStream = OFF_STREAM;
        isPort80Handshake = false;
        break;
      case START_STREAM:
        id_str = "start_stream";
        break;
      case RESET_CAM:
        id_str = "reset";
        break;
      default:
        id_str = "ping80";
        break;
    }
    sendCamCtrlRequest(client80, id_str, val_str);
    SelectCamCtrl = PING80;
  }
}
//********************************************
void sendCamCtrlRequest(WiFiClient &client80, String id_str, String val_str){
  String get_request = "GET /control?var=" + id_str;
  get_request += "&val=" + val_str;
  get_request += " HTTP/1.1\r\n";
  get_request += "Host: " + String(host);
  get_request += "\r\nConnection: keep-alive\r\n\r\n";
  client80.print(get_request);
  Serial.print(get_request);
  get_request = "";
  id_str = "";
  val_str = "";
  receiveToBlankLine(client80);
  canSendCamCtrl = false;
}
//*********************************************
bool receiveToBlankLine(WiFiClient &client){
  String req_str = "";
  uint32_t time_out = millis();
  while(StatusStream == ON_STREAM){
    if(client.available()){
      req_str =client.readStringUntil('\n');
      Serial.println(req_str);
      if(req_str.indexOf("\r") == 0) return true;
    }
    if(millis() - time_out > 10000) {
      Serial.println("--------error Time OUT receiveToBlankLine");
      break;
    }
    delay(1);
  }
  return false;
}
//********************************************
void initDisplay(){
  mM5.init( utf8sjis_file, shino_half_font_file, shino_full_font_file );
  LCD.ILI9341init();
  LCD.brightness(255);  
  LCD.displayClear();
  LCD.drawRectangleLine(0, 0, devid.wfbuf + offset, devid.hfbuf + offset, 31, 63, 31);
  LCD.drawRectangleLine(btnA_x0, btn_y0, btnA_x1, btn_y1, "#ffffff");
  LCD.drawRectangleLine(btnB_x0, btn_y0, btnB_x1, btn_y1, "#ffffff");
  LCD.drawRectangleLine(btnC_x0, btn_y0, btnC_x1, btn_y1, "#ffffff");

  mM5.font[0].x0 = 250; mM5.font[0].y0 = 1;
  mM5.font[0].htmlColorCode( "#FFFFFF" );
  mM5.font[0].Xsize = 1, mM5.font[0].Ysize = 2;
  mM5.disp_fnt[0].dispText( mM5.font[0], " Wait." );

  mM5.font[1].x0 = 10; mM5.font[1].y0 = 200;
  mM5.font[1].htmlColorCode( "red" );
  mM5.font[1].Xsize = 2, mM5.font[1].Ysize = 2;
  mM5.disp_fnt[1].dispText( mM5.font[1], String(stream_c[0]) );
  mM5.font[2].x0 = 127; mM5.font[2].y0 = 200;
  mM5.font[2].htmlColorCode( "white" );
  mM5.font[2].Xsize = 2, mM5.font[2].Ysize = 2;
  mM5.disp_fnt[2].dispText( mM5.font[2], "Qs10" );
  mM5.font[3].x0 = 230; mM5.font[3].y0 = 200;
  mM5.font[3].htmlColorCode( "#ff00ff" );
  mM5.font[3].Xsize = 2, mM5.font[3].Ysize = 2;
  mM5.disp_fnt[3].dispText( mM5.font[3], String(aec_c[0]) );
}
//********************************************
void connectedWifiDisp(){
  mM5.font[0].x0 = 250; mM5.font[0].y0 = 1;
  mM5.font[0].htmlColorCode( "#FFFFFF" );
  mM5.font[0].htmlBgColorCode( "#008800" );
  mM5.font[0].Xsize = 1, mM5.font[0].Ysize = 2;
  LCD.drawRectangleFill(242, 0, 319, 63, "#008800");
  mM5.disp_fnt[0].dispText( mM5.font[0], " Wi-Fi" );
  mM5.font[0].x0 = 250; mM5.font[0].y0 = 33;
  mM5.disp_fnt[0].dispText( mM5.font[0], "TCP OK!" );
  LCD.drawRectangleLine(242, 0, 319, 63, "#FFFFFF");

  IODEV dev;
  displayFrameSize(dev.wfbuf, dev.hfbuf);

  mM5.font[5].x0 = 250; mM5.font[5].y0 = 96;
  mM5.font[5].htmlColorCode( "#ffffff" );
  mM5.font[5].Xsize = 1, mM5.font[5].Ysize = 2;
  mM5.disp_fnt[5].dispText( mM5.font[5], "       B" );

  mM5.font[6].x0 = 250; mM5.font[6].y0 = 132;
  mM5.font[6].htmlColorCode( "#00ff00" );
  mM5.font[6].Xsize = 1, mM5.font[6].Ysize = 2;
  String cam_fps_str = "    FPS";
  mM5.disp_fnt[6].dispText( mM5.font[6], cam_fps_str );
  mM5.font[6].x0 = 250;
}
//********************************************
void displayFrameSize(uint16_t w, uint16_t h){
  char w_c[4], h_c[4];
  sprintf(w_c, "%3d", w);
  sprintf(h_c, "%3d", h);
  mM5.font[4].x0 = 243; mM5.font[4].y0 = 65;
  mM5.font[4].htmlColorCode( "#FFFFFF" );
  mM5.font[4].Xsize = 1, mM5.font[4].Ysize = 2;
  String str = String(w_c)+" x ";
  str += String(h_c);
  mM5.disp_fnt[4].dispText( mM5.font[4], str);
}
//********************************************
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;
  }
}
//********************************************
void changeStateStreamBtnDisp(){
  mM5.font[1].htmlBgColorCode( "black" );
  mM5.font[1].Xsize = 2, mM5.font[1].Ysize = 2;
  LCD.drawRectangleFill(btnA_x0+1, btn_y0+1, btnA_x1-1, btn_y1-1, "black");
  if(StatusStream == OFF_STREAM || StatusStream == CLOSE_CONNECTION){
    SelectCamCtrl = START_STREAM;
    StatusStream = ON_STREAM;
    mM5.font[1].htmlColorCode( "green" );
    mM5.disp_fnt[1].dispText( mM5.font[1], String(stream_c[1]) );
  }else if(StatusStream == ON_STREAM){
    SelectCamCtrl = STOP_STREAM;
    StatusStream = CLOSE_CONNECTION;
    mM5.font[1].htmlColorCode( "red" );
    mM5.disp_fnt[1].dispText( mM5.font[1], String(stream_c[0]) );
  }
  canSendCamCtrl = true;
  Serial.printf("SelCamCtrl=%d\r\n", (uint8_t)SelectCamCtrl);
}

void changeFrameSizeBtnDisp(){
  frame_size_num++;
  if(frame_size_num > 5) frame_size_num = 0;
  mM5.font[1].htmlColorCode( "white" );
  mM5.font[1].htmlBgColorCode( "blue" );
  mM5.font[1].Xsize = 1, mM5.font[1].Ysize = 2;
  LCD.drawRectangleFill(btnA_x0+1, btn_y0+1, btnA_x1-1, btn_y1-1, "blue");
  mM5.disp_fnt[1].dispText( mM5.font[1], String(framesize_c[frame_size_num]) );
  SelectCamCtrl = CHANGE_FRAMESIZE;
  canSendCamCtrl = true;
  Serial.println("Send CHANGE_FRAMESIZE");
}

void changeQualityBtnDisp(){
  quality += 10;
  if(quality > 63) quality = 10;
  LCD.drawRectangleFill(btnB_x0+1, btn_y0+1, btnB_x1-1, btn_y1-1, "black");
  mM5.font[2].htmlColorCode( "white" );
  mM5.font[2].htmlBgColorCode( "black" );
  mM5.font[2].x0 = 127;
  mM5.disp_fnt[2].dispText( mM5.font[2], "Qs" );
  mM5.font[2].x0 = 159; 
  mM5.disp_fnt[2].dispText( mM5.font[2], String(quality) );
  Serial.printf("Qs=%d\r\n", quality);
  SelectCamCtrl = CHANGE_JPG_QUALITY;
  canSendCamCtrl = true;
}

void changeAEC(){
  exposure_count++;
  if(exposure_count > 5) exposure_count = 0;
  if(exposure_count == 0) {
    mM5.font[3].htmlColorCode( "#ff00ff" );
    Serial.println("exposure=AUTO");
  }else{
    mM5.font[3].htmlColorCode( "yellow" );
    Serial.printf("exposure=%d\r\n", exposure_count);
  }
  mM5.disp_fnt[3].dispText( mM5.font[3], String(aec_c[exposure_count]) );
  SelectCamCtrl = CHANGE_AEC;
  canSendCamCtrl = true;
}
//********************************************
bool decodeJPG(){
  bool ret = false;
  work = malloc(3100);
  res = jd_prepare(&jdec, in_func, work, 3100, &devid);
  log_v("after jd_prepare heap=%d\r\n", esp_get_free_heap_size());
  if (res == JDR_OK) {
    log_v("Image dimensions: %u by %u. %u bytes used.\r\n", jdec.width, jdec.height, 3100 - jdec.sz_pool);
    devid.wfbuf = jdec.width;
    devid.hfbuf = jdec.height;
    devid.fbuf_size = jdec.width * 2 * jdec.height; //RGB565
    devid.fbuf = (BYTE *)malloc(devid.fbuf_size);
    log_v("after devid.fbuf malloc heap=%d\r\n", esp_get_free_heap_size());
    if(devid.fbuf){
      res = jd_decomp(&jdec, out_func, 0);   /* Start to decompress with 1/1 scaling */
      log_v("after jd_decomp heap=%d\r\n", esp_get_free_heap_size());
      if (res == JDR_OK) {
        canDisplayLCD = true;
        fps_count++; //フレームレート表示カウントアップ
        ret = true;
      } else {
        Serial.printf("Failed to decompress: rc=%d\n", res);
        if(devid.fbuf){
          free(devid.fbuf);
          devid.fbuf = NULL;
        }
        ret = false;
      }
    }else{
      Serial.println("devid.fbuf malloc FAILED.");
      ret = false;
    }
  } else {
    Serial.printf("%02x, %02x\r\n", jpg_buf[0], jpg_buf[1]);
    Serial.printf("Failed to prepare: rc=%d\n", res);
    ret = false;
  }
  jpg_buf_seek = 0;
  free(work);
  work = NULL;
  if(jpg_buf) {
    free(jpg_buf);
    jpg_buf = NULL;
  }
  jpg_len = 0;
  return ret;
}

UINT in_func (JDEC* jd, BYTE* buff, UINT nbyte){
  if (buff) {
    //Serial.printf("jpg_buf_seek=%d, nbyte=%d\r\n", jpg_buf_seek, nbyte);
    memcpy(buff, jpg_buf + jpg_buf_seek, nbyte);
    jpg_buf_seek += nbyte;
    //Serial.printf("buff0=%02x, buff1=%02x\r\n", buff[0], buff[1]);
    return nbyte;
  }else{
    jpg_buf_seek += nbyte;
    return nbyte;
  }
}

UINT out_func (JDEC* jd, void* bitmap, JRECT* rect){
  IODEV *dev = (IODEV*)jd->device;
  uint8_t *src, *dst;
  uint16_t x, y, bwd;

  src = (uint8_t*)bitmap;
  //use RGB565
  dst = dev->fbuf + 2 * (rect->top * dev->wfbuf + rect->left);;
  //bws = 2 * (rect->right - rect->left + 1);
  bwd = 2 * dev->wfbuf;
  uint8_t red, green, blue;
  int16_t xcnt;
  for (y = rect->top; y <= rect->bottom; y++) {
    xcnt = 0, red = 0, green = 0, blue = 0;
    for (x = rect->left; x <= rect->right; x++){
      red = *(src++) & 0xF8;
      green = *(src++) & 0xFC;
      blue = *(src++) & 0xF8;
      *(dst + xcnt++) = (red) | (green >> 5);
      *(dst + xcnt++) = (green << 3) | (blue >> 3);
    }
    dst += bwd;
  }
  return 1;    /* Continue to decompress */
}
//*************************************************
void actionButton(){
  mM5.btnA.buttonAction();
  switch( mM5.btnA.ButtonStatus ){
    case mM5.btnA.MomentPress:
      Serial.println("\r\nButton A Moment Press");
      changeFrameSizeBtnDisp();
      break;
    case mM5.btnA.ContPress:
      Serial.println("\r\n-------------Button A Cont Press");
      changeStateStreamBtnDisp();
      break;
    default:
      break;
  }

  mM5.btnB.buttonAction();
  switch( mM5.btnB.ButtonStatus ){
    case mM5.btnB.MomentPress:
      Serial.println("\r\nButton B Moment Press");
      changeQualityBtnDisp();
      break;
    case mM5.btnB.ContPress:
      Serial.println("\r\n-------------Button B Cont Press");
      SelectCamCtrl = RESET_CAM;
      canSendCamCtrl = true;
      mM5.font[2].x0 = 127;
      mM5.font[2].htmlColorCode( "white" );
      mM5.font[2].htmlBgColorCode( "red" );
      LCD.drawRectangleFill(btnB_x0+1, btn_y0+1, btnB_x1-1, btn_y1-1, "red");
      mM5.disp_fnt[2].dispText( mM5.font[2], "RESET" );
      Serial.println("Send [Reset]!!!");
      break;
    default:
      break;
  }

  mM5.btnC.buttonAction();
  switch( mM5.btnC.ButtonStatus ){
    case mM5.btnC.MomentPress:
      Serial.println("\r\nButton C Moment Press");
      changeAEC();
      break;
    case mM5.btnC.ContPress:
      Serial.println("\r\n-------------Button C Cont Press");
      mM5.font[3].x0 = 230;
      mM5.font[3].htmlColorCode( "#00ffff" );
      pclk_div_cnt++;
      if(pclk_div_cnt > 5){
        pclk_div_cnt = 0; 
      }
      SelectCamCtrl = CHANGE_PCLK;
      canSendCamCtrl = true;
      mM5.disp_fnt[3].dispText( mM5.font[3], String(pclk_div_c[pclk_div_cnt]) );
      break;
    default:
      break;
  }
}

操作方法

まず、コンパイル書き込みを行う前に、先ほど述べたようにWiFiルーター環境を整えておき、M5CameraやM5Stackが接続できるようにしておいてください。

その後、まず、M5Cameraをコンパイル書き込みし、シリアルモニターを起動すると、下図の様にローカルIPアドレスが表示されると思います。

このIPアドレスでM5Stack用スケッチの19行目のIPアドレスを書き換えます。

その後で、M5Stack用スケッチをコンパイル書き込みします。

あとは、最初に紹介した動画を見てもらえれば分かると思います。

M5StackがWiFiアクセスポイントに接続されると下図の様に表示されます。
操作方法はこんな感じです。

M5Stackのボタンは瞬時押しと長押しで操作が異なります。

最初に紹介した動画のように、M5Stackとスマホ等ブラウザで表示を切り替える場合は、必ずM5StackのボタンでストリーミングをStopさせておくことです。
そうしないと、ブラウザで接続できません。

以前のこちらの記事では、Stopボタンを押さなくても途中でハックして切り替えられたのですが、今回はできませんでした。

そして、ブラウザのURL欄にM5CameraのIPアドレスを入力して、「Start Stream」ボタンを押せば、ストリーミングが始まると思います。
ブラウザの操作方法は下図です。

冒頭で紹介した動画のように、フレームサイズ(画角)については、切り替わる時と切り替わらない時があります。
切り替わらない時は、「Stop Stream」ボタンを押して、その後「Window Stop」を押して、その後、「Start Stream」ボタンを押せば切り替わると思います。

また、「Stop Stream」ボタンを押した場合は、必ず「Window Stop」を押して、その後ブラウザの更新ボタンを押せば、HTML表示をリフレッシュできます。

ブラウザ表示についてはまだテスト段階なので、あまりちゃんと作り込んでいません。
ブラウザで使いたければ、サンプルスケッチのCameraWebServerを使った方が遙かに良いと思いますよ。

M5Stackの中央のボタンを瞬時押しすると、JPEG画質を変えられます。
デフォルトでは最高画質のQs10です。
Qs とは、OV2640のレジスタの Quality scale factor のことです。
これについては後で詳しく述べます。
気を付けて欲しいのは、数値が小さい方が高画質です。
10~60 の範囲で設定できるようにしました。

まず、Qs10の最高画質はこんな感じです。

Qs=60 の最低画質にすると、下図の様に粗い画像になります。
その分、JPEGデータサイズはかなり圧縮してくれます。

また、左のボタンを瞬時押しすると、フレームサイズを切り替えることができます。
下図は 96 x 96 pixel です。
JPEGデータサイズが小さいので、25fps出せました。

80 x 160 pixel で縦長の特殊なフレームサイズです。
M5StickCに使えるかもしれません。
ただ、画像自体縦に伸びちゃいます。

160 x 80 pixel で、画像が横伸びしています。
M5StickC で使えるかも知れません。

次は160 x 120 pixel です。
実際は24fps出ます。

次は192 x 144 pixel です。
これは21fps 出ました。
なかなか良い感じです。

あとはデフォルトの240 x 176 pixel ですが、そうなるとガクンと速度が落ちて、13~15fpsになってしまいますね。

とにかく、ボタン操作でカメラ画像のフレームサイズ(画角)を変えられるのは、なかなか新鮮ですね。

では、次は以上のスケッチの解説も含めた、OV2640のJPEG出力について説明します。

コメント

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