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

ESP32-DevKitC,と,OV2640カメラモジュールとOLED,SSD1331,モジュールを繋げて、ほぼデジカメ ESP32 ( ESP-WROOM-32 )

まずはesp32-cameraライブラリを使って、OV2640のカメラ映像をOLED SSD1331ディスプレイに表示させる簡単なサンプルスケッチで動作チェック

では、まずは先に紹介した回路で動作チェックです。

Arduino core for the ESP32 に標準で入っている ESP32 Camera ライブラリを使って、簡単なサンプルスケッチをテストしてみます。
Arduino core for the ESP32 に入っているサンプルスケッチに CameraWebServer というものがありますが、これはあまりにも使い方が難し過ぎるので、私なりに読み解いて簡略化してみました。

実は、このライブラリを自由に使いこなすためには、ライブラリの構造をよく理解していないとサッパリわからないと思います。
私自身、ここ数か月間、自分なりに読み解いたことによって、ここまで簡単なスケッチ(ソースコード)が実現できました。
最初はサッパリ分からなかったので、今は自分的には少しは進歩したのかなと思います。

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

/*  mgo-tec modified CameraWebServer.ino and app_httpd.cpp.
 *  Copyright 2015-2016 Espressif Systems (Shanghai)
 *  Licensed under the Apache License, Version 2.0
 */
#include <esp_camera.h>
#include <esp32_ssd1331_bv2.h> //mgo-tec自作ライブラリ beta ver 2.0.0

#define PWDN_GPIO_NUM     -1
#define RESET_GPIO_NUM    17
#define XCLK_GPIO_NUM     27
#define SIOD_GPIO_NUM     21
#define SIOC_GPIO_NUM     22

#define Y9_GPIO_NUM       19
#define Y8_GPIO_NUM       36
#define Y7_GPIO_NUM       18
#define Y6_GPIO_NUM       39
#define Y5_GPIO_NUM        5
#define Y4_GPIO_NUM       34
#define Y3_GPIO_NUM       35
#define Y2_GPIO_NUM       32
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     26
#define PCLK_GPIO_NUM     23

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

Esp32Ssd1331Bv2 ssd1331(oled_SCLK, oled_MISO, oled_MOSI, oled_CS, oled_DC, oled_RST);

const uint8_t oled_width_pix = 96, oled_height_pix = 64;
const uint8_t camera_width = 160;
const uint16_t max_h_buf_size = camera_width * 2;
uint32_t last_time = 0;
sensor_t *sensor = NULL;

void setup() {
  Serial.begin(115200);
  Serial.println();

  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_RGB565;
  config.frame_size = FRAMESIZE_QQVGA;
  config.fb_count = 1;

  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }

  ssd1331.init65kColor();
  ssd1331.clearDisplay();

  sensor = esp_camera_sensor_get();
  sensor->set_colorbar(sensor, 1);  //Color Bar display ON
  camera_setting();
  last_time = millis();
}

void loop() {
  camera_fb_t * fb = NULL;
  fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Camera capture failed");
    return;
  }

  if(millis() - last_time > 3000){
    sensor->set_colorbar(sensor, 0); //Color Bar display OFF
  }

  for(int i = 0; i < oled_height_pix; i++){
    ssd1331.drawPixel65kColorBytes(0, i, (fb->buf + i * max_h_buf_size), oled_width_pix * 2);
  }
}

void camera_setting(){
  sensor->set_exposure_ctrl(sensor, 1);
  sensor->set_aec2(sensor, 1);
  //sensor->set_aec_value(sensor, 100);
  //sensor->set_special_effect(sensor, 5);
  //sensor->set_agc_gain(sensor, 30);
  //sensor->set_hmirror(sensor, 1);
  //sensor->set_vflip(sensor, 1);
  //sensor->set_raw_gma(sensor, 1);
  sensor->set_whitebal(sensor, 1);
  //sensor->set_awb_gain(sensor, 1);
  //sensor->set_gain_ctrl(sensor, 1);
  //sensor->set_lenc(sensor, 0);
  //sensor->set_dcw(sensor, 1);
  //sensor->set_bpc(sensor, 0);
  //sensor->set_wpc(sensor, 1);
}

【解説】

●5行目:
Arduino core for the ESP32 に標準で入っている esp_camera ライブラリのインクルードです。

●6行目:
私の自作の SSD1331 ライブラリのインクルードです。

●8-24行:
OV2640 カメラモジュールと、ESP32-DevKitC のGPIOピンアサイン定義です。
PWDN (Power Down Mode)は今回使いませんので、-1にしておきます。

●26-31行:
OLED SSD1331 と ESP32-DevKitC のGPIOピンアサイン定義です。

●35-39行:
OLED SSD1331 は 96×64 pixel です。
しかし、esp_camera ライブラリの最小画面サイズは、QQVGA( 160×120 pixel )です。
esp_cameraライブラリを使う時、それ以下のサイズにしたい場合はちょっと複雑になるので、ここでは、camera_widthを160pixel としています。
max_h_buf_size というのは、pixel format を RGB565 とした場合、1pixel のデータは赤5bit、緑6bit、青5bit で表現した2byte データなので、水平1行の画素のデータサイズは、
160 x 2 = 320 byte
となるわけです。
sensor_t 型はイメージセンサの設定値を格納しておく構造体ですが、ここでは詳細は省きます。esp_camera ライブラリに予め定義されているものです。これは74行で OV2640 から値を取得して代入します。

●45-67行:
camera_config_t型というのは、esp_camera.h に定義されている、カメラモジュールの GPIOピンアサイン等を保存しておく構造体です。
42-43行では、Arduino core for the ESP32 の LEDCライブラリを使って20MHzのクロックパルスを出力して、XCLKに送ります。
PCKL (Pixel Clock)というのは、XCLKのクロック信号を基に信号が生成され、それを基に画素データが作られます。
VSYNC は垂直同期信号。
HREF は水平同期信号。

sccb_sda はSCCBインターフェースと言う、I2Cと互換性があるインターフェースのSDA信号です。

sccb_scl はSCCBインターフェースのSCL信号です。

fb_count は良く分からないのですが、とりあえず1としておきます。
JPEGを受信する時にスキップするフレーム数を設定したりするようです。

●69行:
OV2640カメラモジュールを初期化します。

●75行:
OLED SSD1331 モジュールを初期化しますが、今回は約65000色フルカラーを使います。
init65kColor という関数は今回から追加しました。

●78行:
ここで、SCCBインターフェースを使って、OV2640の設定値を読み込みます。

●79行:
set_colorbar 関数で、OV2640からカラーバー信号を出力させます。
1という値をゼロにすると、信号をOFFにできます。

●80行:
97-113行に実体のあるカメラ設定をする関数です。

set_aec2 という関数は、AEC ( Automatic Exposure Control )という、自動露出制御関数です。
今回はONにしてみました。
OV2640 CMOS イメージセンサは、そのチップの中でかなり多種のカメラ設定が可能です。
これについては、後述します。

●85-86行:
OV2640 から送られてきた画素データは、DMA ( Direct Memory Access )転送されて、ESP32 の DMA専用のメモリに自動的に格納されています。
それを、esp_camera_fb_get 関数で読み取り、そのデータのアドレスをcamera_fb_t 型のfbという構造体に格納しています。
画素データfb構造体の中の fb->buf に格納されています。

●92-94行:
カラーバーを3秒後にOFFにして、カメラ映像に切り替えます。

●96-98行:
ここで、OLED SSD1331 に画素データを出力しています。
今回から、drawPixel65kColorBytes という関数を新たに作りました。
これは、RGB565の2byteデータをそのまま複数のバイトを一気にSSD1331にSPI通信で送信します。
RGB565の2byte画素データは fb->buf に下図の様に格納されています。

要するに、RGB565とは、赤色(red)5bit、緑色(green)6bit、青色(blue)5bit をそれぞれ8bit(1byte)づつに分けた2byteデータです。
RGB888の場合は、赤8bit、緑8bit、青8bit となって、1pixel(画素)データを送るのに 3byte 必要になり、その分、転送速度が遅くなってしまいます。
でも、RGB565 ならば2byteで済むため、転送速度が速いです。
人間の目では、RGB888とRGB565では見分けがつかないらしい? です。
そう考えると、RGB565というデータフォーマットはよく考えられた効率の良い構造ですね。
OLED SSD1331 には都合の良いことに、RGB565データをそのまま扱えて、画素データが格納されている fb->buf をある程度まとめて、そのまま送信すれば、ディスプレイに表示されるという寸法です。

ただ、注意していただきたいのは、OV2640 で取得した画素データは 160 x 120 pixel が最小のため、OLED SSD1331 へ出力する場合は 96 x 63 pixel までにしています。
さらに画像サイズを小さくしたい場合は後日改めて記事にする予定です。

●102-117行:
ここで、カメラ設定ができる関数を集めています。
正直言って、使い方が良く分かりませんので、みなさんいろいろ試してみて下さい。
この関数の意味はすぐ後で分る範囲で紹介します。
コメントアウトしているところは、必要に応じてコメントを解除して使ってみてください。

カメラ設定

では、サンプルスケッチの 102-116行のカメラ設定用関数を自分なりに読み解いてみたものを紹介します。
これは、GitHub の esp32-camera ライブラリの ov2640.c に定義されています。
ライセンス:MIT

作者:Copyright (c) 2013/2014 Ibrahim Abdelkader

何分、カメラに関してはド素人なので、使い方が良く分かりません。
正直、ちゃんと動いているのかどうかも良く分かりません。

因みに、以下の関数群は static 定義ですが、関数名の前にsensor_t構造体のポインタを指示すれば、スケッチ上で自由に使えるようになります。
例えば、自動露出制御の場合、サンプルスケッチにあるように、

sensor->set_exposure_ctrl(sensor, 1);

という感じです。
この関数にはエラー検知のための返り値がありますが、ここでは無視しています。

以下のような露出や明るさ、色、ガンマ補正など、OV2640 に入っているレジスタに指令を出すだけで、実現できてしまうんです。
最近のイメージセンサってスゴイっすね。
と、言ってもこのデバイスは10年以上前のデバイスですけど・・・。

int set_framesize(sensor_t *sensor, framesize_t framesize);

フレームサイズ設定。
framesize = FRAMESIZE_QQVGA ~ FRAMESIZE_UXGA

int set_contrast(sensor_t *sensor, int level);

コントラスト設定

int set_brightness(sensor_t *sensor, int level);

明るさ設定

int set_saturation(sensor_t *sensor, int level);

彩度設定

int set_special_effect(sensor_t *sensor, int effect);

特殊効果設定
effect = 0~6

int set_wb_mode(sensor_t *sensor, int mode);

ホワイトバランスモード
mode = 0: sunny
mode = 1: cloudy
mode = 2: office
mode = 3: home

int set_quality(sensor_t *sensor, int quality);

量子化スケール設定。
たぶん、画質に直結する設定だと思います。
quality = 0 ~63

int set_agc_gain(sensor_t *sensor, int gain);

AGC ( Automatic Gain Control ) 自動ゲイン制御のゲイン設定。
たぶん、アナログアンプでゲイン値を自動??で決めて、イメージセンサのpixel電圧を取得するための設定らしい。

int set_gainceiling(sensor_t *sensor, gainceiling_t gainceiling);

たぶん、イメージセンサのアナログアンプのGain上限を決める設定。
使い方は良く分かりません。
gainceiling = GAINCEILING_2X ~ GAINCEILING_128X

int set_gain_ctrl(sensor_t *sensor, int enable);

AGC ( Automatic Gain Control ) 自動ゲイン制御のON, OFF設定。

int set_exposure_ctrl(sensor_t *sensor, int enable);

OV2640のセンサによるAEC ( Automatic Exposure Control ) 自動露出制御のON, OFF設定。
デフォルトでONになります。
このON状態だけでは露出値が足りない場合、set_aec2もONにすると良い。

int set_aec2(sensor_t *sensor, int enable);

OV2640のDSPによるAEC ( Automatic Exposure Control ) 自動露出制御のON, OFF設定。
set_exposure_ctrlをONにしないと有効にならないと思われます。
set_exposure_ctrlとset_aec2の両方をONにすると、自動露出制御としては納得のいく露出が得られると思います。

int set_ae_level(sensor_t *sensor, int level);

set_exposure_ctrlがONの時に有効。
levelの値は、-2~+2 の範囲。

int set_aec_value(sensor_t *sensor, int value);

AEC ( Automatic Exposure Control ) 自動露出制御の露出値設定。
set_exposure_ctrをOFFにすると、マニュアルの露出設定が可能です。
value = 0~1200 ですが、実質使えるのは0~255くらいだと思います。

int set_colorbar(sensor_t *sensor, int enable);

カラーバーフレーム出力 ON, OFF 設定。

int set_hmirror(sensor_t *sensor, int enable);

Horizontal Mirror 水平鏡面反転表示 ON, OFF 設定

int set_vflip(sensor_t *sensor, int enable);

Vertical Flip 垂直反転表示 ON, OFF 設定

int set_raw_gma(sensor_t *sensor, int enable);

たぶん、生データのガンマ補正の ON, OFF 設定

int set_whitebal(sensor_t *sensor, int enable);

AWB ( Automatic White Balance ) 自動ホワイトバランスの ON, OFF 設定

int set_awb_gain(sensor_t *sensor, int enable);

AWB ( Automatic White Balance ) 自動ホワイトバランスゲインの ON, OFF 設定

int set_lenc(sensor_t *sensor, int enable);

たぶん、LENC ( Lens Correction ) レンズ補正の ON, OFF 設定

int set_dcw(sensor_t *sensor, int enable);

良く分かりません。DCW の意味わかりません。
だれか教えてくださーい!
たぶん、ダウンサンプリングだと思います。
デフォルトでONになっていて、OFFにするとまともに表示されません。

int set_bpc(sensor_t *sensor, int enable);

これも良く分かりません。
たぶん、個人的には BPC ( Black Point Corrections ) かな?
Twitter で not logical さんから頂いた情報では、SPRESENCEカメラでは、
Bad Pixel Corrections
らしいです。
どなたか分る方は教えて下さーい!!

int set_wpc(sensor_t *sensor, int enable);

これも良く分かりません。
たぶん、個人的には WPC ( White Pixel Canceling ) かな?

コンパイル書き込み実行

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

たぶん、あっさり動いてしまうので、カメラデバイスの通信の仕組みが良く分からないと思います。

これにGPIOピンにオシロスコープを繋いで波形を見たりすると、いろいろ複雑な動作をしていることが分ります。

次回の記事で、さらにライブラリを解体して、動作のしくみを検証していきます。

(追記:2019/08/15)初回起動時にリセットが数回繰り返される問題について

この記事をアップしてしばらく動作させていると、初回起動直後に画面が表示された後、短い時間で3回くらいリセットを繰り返すという挙動が出てきました。
リセットを繰り返した後、通常起動してその後は安定動作します。

この挙動は、おそらく ESP32-DevKitC の電源レギュレーターのキャパを超えている可能性があります。
キャパを超えるといっても、初回起動時です。

以前、ESP32 の瞬時大電流による電圧降下について以下の記事で取り上げたことがあります。

●ESP-WROOM-32 ( ESP32 )の消費電流を電流プローブ無しで測定してみました
●ESP-WROOM-32 (ESP32) の 電流 測定 その2
●ESP-WROOM-32 ( ESP32 ) のUSB電源突入電流(インラッシュカレント)を考える
●ESP-WROOM-32 ( ESP32 ) の保護機能付き電源強化対策の実験

これによると、ESP32 の起動時の突入電流と、それ以外に Wi-Fi 起動時に瞬間最大電流が 600mA 流れます。
そうすると、今回は起動して画面表示されてからリセットを繰り返す症状のため、突入電流というよりも、ESP32 のWi-Fi起動によるものと考えられます。
ESP32-DevKitC の電源レギュレーターは、通常使用では問題ありませんが、OV2640 カメラモジュールと、OLED SSD1331 モジュールが接続されているため、結構多めの電流が流れます。
そして、ESP32 のWi-Fi起動時の瞬時大電流によって、電源ラインの電圧降下が起きて、ESP32 の最小動作電圧を瞬間的に下回ってしまったのだと思われます。
ESP32-DevKitC の電源レギュレーターは ADP3338 よりも瞬時電圧降下に弱く、予想外にESP32の動作電圧より下がってしまう可能性があります。

このことは、Arduino IDE スケッチ書き込みのリセット時にも問題になる可能性があるため、高速応答大容量レギュレーター回路を追加しなければいけないかも知れません。
次回以降の記事でその回路を追加してみたいと思います。

編集後記

どうでしたか?
あまりにもあっさり動いてしまいますね。

でも、これでは、まだどのように画素データが ESP32 に転送されて、画面に表示されるのかが全く分からないと思います。

今回は、M5Camera でブラックボックスになってしまったものを、それぞれのデバイスを別途接続して、サンプルスケッチを利用して動作確認しただけでした。
これでは、OLED SSD1331 の 96 x 64 pixel に合っておらず、カメラの画素が大きすぎますね。
もっと、画素数を減らしたいのですが、ライブラリを使うとうまくできません。

ということで、次回以降アップする記事では、さらに踏み込んで SCCBインターフェースや DMA転送等もチェックしていきたいと思います。
もうすでに動作確認はしているのですが、情報量が多すぎて、なかなか記事にできません。

今後もしばらくお待ちいただければと思います。

ではまた・・・。

 

コメント

  1. juchang より:

    mgo-tec 様

    早速試そうと思いましたが、手元に OV2640 CMOSカメラモジュールが無く、秋月電子通商さんに注文し、現在入荷を待っているところです。
    M5Camera が使用できないのは残念ですが、今後のご検討を期待しております。

    • mgo-tec mgo-tec より:

      juchangさん

      ご無沙汰しております。
      いつも記事をご覧いただき、そして試していただき、ありがとうございます。

      この記事はM5Cameraを自在に使えるようにする目的でもあります。
      その前に、それぞれの配線と通信プロトコルを理解するためにも、OV2640モジュールは別途配線して確認する必要がありました。
      esp32-cameraライブラリが理解できるようになると、M5Cameraでディープラーニングができるかも知れません。
      今後ご期待いただければと思います。
      いつも本当にありがとうございます。
      m(_ _)m

  2. 匿名 より:

    ESP32 CAMでデジカメみたいなものが作れないかと考えて本ブログにたどり着きました。
    自分なりに試行錯誤してみたのですが、どうあがいてもフレームレートが出ません。以下のように記載しているのですが、
    fb = esp_camera_fb_get();
    if (!fb) {
    Serial.println(“Camera capture failed”);
    }

    シリアルモニタには、一秒に数回、”Camera capture failed”と表示されます。画像フォーマットはRGB565です。

    フレームレートを出すにあたってカメラのデータ取得の点で何か工夫はありますでしょうか。

    • mgo-tec mgo-tec より:

      匿名さん

      ブログをご覧いただき、ありがとうございます。

      この記事のプログラミングは1年以上前なので、今の環境でちゃんと動くかは不明です。

      まず、お聞きしたいのは、フレームレートはどのくらい出ているのでしょうか?
      esp32-cameraライブラリの場合は、この後のM5Cameraとの記事を読み進めていくと分かって来るのですが、最高で25fpsまでしか出ません。
      loop関数の処理が速いと、25fpsを上回ってしまい、OV2640から画像データを取得できない時が多々出てくるのかも知れません。
      LCD画面が正常に表示されていれば、シリアルモニタの表示は気にしなくて良いかも知れません。
      最初の方で紹介した動画くらいの速度が出ていれば問題無いかと思います。

      また、OV2640とESP32の配線はできるだけ短い方がエラーが少ないです。

      また、シリアルモニタを表示させるだけで遅くなりますので、シリアルモニタを起動しない方が速くなるかも知れません。

  3. nopnop2002 より:

    おはようございます。
    有益な情報の公開、ありがとうございます。

    GPIO#12が使えない理由は、フラッシュチップ(VDD_SDIO)に電力を供給する内部レギュレータの出力電圧を選択するためのブートストラップピンとして使用されているからだと思います。
    こちらにGPOIO12に関する注意点が記載されています。
    https://github.com/espressif/esp-idf/blob/master/examples/storage/sd_card/sdmmc/README.md

    • mgo-tec mgo-tec より:

      nopnop2002さん

      おひさしぶりですね。
      再びコメントいただき、ありがとうございまーす!

      GPIO #12 の謎はそういうことだったんですね。
      MMCは使ったこと無いので、そのドキュメントは全く知りませんでした。
      教えて頂き、ありがとうございます。
      トラブルシューティング記事の項目も修正しておこうと思います。
      重ねてありがとうございました~。
      m(_ _)m

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