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

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

こんばんは。

今回は前回記事に引き続き、esp32-cameraライブラリを読み解く第三弾です。
いよいよイメージセンサ OV2640 の詳細と、SCCBインターフェース、DMA, I2S インターフェースを扱ってみたいと思います。
約半年間くらい独学で追求し続けた、自分にとっての超大作記事です。
情報量が半端なく多いので、覚悟しておいてください。
それと、あくまで独学なので、勘違いや誤りがあるかもしれませんので、それも覚悟しておいてください。

スポンサーリンク

今までとっても気になっていて、先送りにしていた DMA や I2S インターフェースを今回初めて扱ってみました。

ご存知の方は多いと思いますが、Twitterで「らびやん」さんが M5Stack を使ってDMAで凄いものを作っていますね。
私も興味はあるものの、ほとんど知識が無いので敬遠していました。
でもって、今回使ってみたら、何と、ドえらい難しい!!!
理解するまで、何度も心が折れそうになりました。
アマチュアにとってはハードルが高すぎると思いました。
ネットにあまり情報が無いわけですね。
あったとしても、プロ用の超難解記事だけです。

ですが、DMAおよびI2Sは一度解明してしまうと、多量の画像データが割と手軽に高速転送できてしまうので、とっても便利で、もう画像のやり取りはそれ無しでは考えられなくなってきますね。
今までArduino IDEでちまちまプログラミングしてデータ転送していたのがアホらしくなるほどです。

ESP32には嬉しいことに、DMA や I2S をサポートしていて、Arduino core for the ESP32 にレジスタ設定もあって、これを使わない手はありません。

実は、イメージセンサやLCDディスプレイなどの画像データの送受信は、DMA, I2Sで行うのが定番みたいですね。
いやぁ~・・・、知りませんでした。

とりあえず、以下の動画をご覧ください。
esp32-cameraライブラリを使わずに、それを解体して抜粋したものでプログラミングしています。
そして、フレームサイズを100×72 pixel にして、OLED SSD1331 (96×64 pixel)内の画面に収めてカラーバーを表示させたり、デジタルズームしています。

デジタルズームはボタンで操作したかったのですが、今回は都合上省いて、いちいちコンパイルしました。

前回記事と比べるとあまり新しさは感じないかもしれませんが、DMAやI2Sを知らない人は今回の記事を全部読むとその凄さが分かって来ると思います。
(たぶん・・・)
私的には今回のハイライトは、DMA, I2Sの理解と、「割り込み」ですね。
割り込みがこんなに便利なもんだとは知りませんでしたよ!

というわけで、これからイメージセンサOV2640の詳細説明と、SCCBインターフェース、DMA, I2Sインターフェースについての自分なりに解明したことを備忘録的に紹介していきます。
とにかく嫌になるくらい情報量が多いです。
というか、自分で書いていて嫌になりましたね。

    【目次】

  1. OV2640データシートのテキストはコピペできないので翻訳大変!
  2. 使う物
  3. ESP32-DevKitC と OV2640 モジュールの接続
  4. OV2640 カメラモジュール ( Arducam B0011 ) 入出力端子について
  5. OV2640 の起動には、まず最初に XVCLK ( XCLK )端子にクロック信号を供給すべし
  6. OV2640 の制御は SCCBインターフェース
  7. OV2640のイメージセンサの画素およびRAWデータ、RGBデータについて
  8. OV2640の Sub-Sampling Mode ( SVGA, CIF )について
  9. OV2640 のフレームサイズ(画像サイズ)およびWindow設定
  10. OV2640 起動後の各端子の電圧波形
  11. ズームした場合のフレームサイズをロジアナで確認
  12. DMA、I2Sによるデータ転送
  13. OV2640からテスト用カラーバーを出力させる
  14. ズーム設定
  15. DMA, I2S 割り込みみついて
  16. esp32-cameraライブラリを使わずに、Arduino IDE でプログラミングしてみる
  17. OV2640 カメラモジュール ( Arducam B0011 )のピント調節
  18. 編集後記

OV2640データシートのテキストはコピペできないので翻訳大変!

これからOmni Visoin製のイメージセンサ OV2640 のデータシートを参照して読み解いていくわけですが、一つ文句を言わせてください。

OV2640 英語データシートは、テキストのコピープロテクトがかかっていて、コピー&ペーストできません。
翻訳が本当に大変でーす!!!

コピペできないようにする意味あるんですかね?
私はアマチュアなので良く分かりませんが、データシートくらいはコピペOKにしてもらいたいですね。
英語の苦手で無能な私にとっては、時間ばかり食って大変でした・・・。
以上、自己中の泣き言です。

ということで、データシートから手打ちでGoogle翻訳に打ち込んで、ほんの一部を和訳してみましたので、そちらも合わせて参照してみてください。

OV2640 データシートを一部独自和訳してみた

使う物

前回記事と全く同じです。

以下のリンクを参照してください。

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

ESP32-DevKitC と OV2640 モジュールの接続

前回記事と全く同じですが、念のため回路図を載せておきます。

OV2640 カメラモジュール ( Arducam B0011 ) 入出力端子について

まず、OV2640 カメラモジュール ( Arducam B0011 )の入出力端子について、自分なりの解釈で解説してみたいと思います。

VCC —— 3.3V 電源に接続
GND —— GND に接続
SCL ——- SCCB インターフェースの SCL 端子に接続
SDA ——- SCCB インターフェースの SDA 端子に接続
SYNC ( VSYNC ) ——- VSYNC出力 ( 垂直同期信号出力)
HREF ——- HREF出力(水平同期信号出力)
PCLK ——- Pixel Clock出力
XCLK ( XVCLK ) —— 外部システムクロック入力
Y0, Y1, D0~D7 —— pixelデータ出力(パラレル)
RST —— リセット信号入力
PWDN —— パワーダウン信号入力

 

【解説】

前回の記事でも述べたように、この Arducam B0011 の VCC は 3.3V で良いようです。たぶん。

SCL, SDA は後で紹介している SCCB インターフェースの端子です。
ほとんど I2C と同じ用途です。
この端子で、OV2640 にコマンドを送り、OV2640 を設定します。

SYNC 端子は、OV2640データシートによると、VSYNC 端子のことで垂直同期信号です。恐らく基板のシルク印刷スペースが無いため、’V’文字が無いものと思われます。
(VSYNC信号の詳細は後述)

HREF 端子は、水平同期信号です。
(HREF信号の詳細は後述)

PCLK 端子は、Pixel Clock 信号です。
(PCLK信号の詳細は後述)

XCLK 端子は、OV2640 の XVCLK 端子で、基板のシルク印刷のスペースが無いため、短い名前にしているものと思われます。
これは、後で紹介している LEDC を使って、ESP32 から精度の高いクロック信号を供給してやります。
(XCLK信号の詳細は後述)

Y0, Y1, D0~D7 の端子からは、イメージセンサのpixel ( 画素 )データが出力されます。
10bit 出力に設定した場合、Y0 ~ D7 まで10個の端子を使いますが、今回は 8bit 出力設定にしているので、Y0, Y1 端子は使いません。
(D0~D7信号の詳細は後述)

RST 端子は、ESP32 からGPIO の HIGH-LOWレベル切り替えを使って、OV2640をリセットさせるために使います。

PWDN 端子は OV2640 を省電力モードで使うためのものです。今回はこの端子は使いませんが、前回の記事でも述べたようにGND に接続しておかないと、まともに動作しないので要注意です。

OV2640 の起動には、まず最初に XVCLK ( XCLK )端子にクロック信号を供給すべし

OV2640 を動かすには、電源の他にまず、XVCLK ( XCLK ) の端子にクロック信号を供給しなければ動きません。

OV2640 のデータシートを見ると書いてありますが、XVCLK 端子はシステムクロック入力端子です。
ですから、クロック信号を供給してやらないと OV2640 が動作しないわけです。
(OV2640 自信の内部クロックで動作させるモードもあるようなのですが、それについてはまだ勉強していません。)

この XVCLK 信号を基準として、OV2640チップで VSYNC (垂直同期)、HREF (水平同期)、PCLK (Pixel Clock)信号を生成しているようです。

そして、この後紹介する SCCBインターフェースも XVCLK信号が無いと動作しません。
(Arducam B0011の場合の基板印刷の端子名は、XCLK となっていますので注意してください。)

XVCLK 信号は、ESP32 の LEDC ( PWM ) を使って、精度の高い20MHz のパルス(矩形波)を生成して供給してやります。
ESP32 の LEDC ( PWM ) については以前扱った以下の記事を参照してください。

Arduino – ESP32 の PWM ( LEDC )で 40MHzまでの安定した高周波パルスを思い通りに出せたぞ

XVCLK 信号は、マスタークロックのような、いわゆる心臓部のようなもんだと思います。

OV2640 の制御は SCCBインターフェース

さて、これからSCCBインターフェースについてお話しますが、私はアマチュア素人ですので、間違えていたらコメント投稿等でご連絡いただけると助かります。

イメージセンサ OV2640 とマイコンとの通信は、SCCBインターフェースを使って制御します。

そもそもSCCBインターフェースって何ぞや?

アマチュア電子工作家の方々には聞きなれない言葉だと思います。
私も M5Camera を使って初めて知りました。

SCCB は Serial Camera Control Bus というらしいです。
正確なことは、OmniVision社の
APPLICATION NOTE, Serial Camera Control Bus Functional Specification
を参照ください。
ネット上のどこかにPDFで配信されています。

ネット上の情報では、I2Cインターフェースと互換性があるとのことです。
I2Cについては、Arduino や ESP32, M5Stack を使っている方々にはWireライブラリでお馴染みですね。

I2Cと同じように、SCCBではESP32からOV2640へ8bitの制御コマンドアドレスを送信して書き込むと、そのレジスタに予め設定されている動作が稼働し始めるというわけです。
それなら、Wireライブラリを使えば良いのではないか・・・と思いますが、実はそうはうまく行きません。
一部のレジスタは動作するけど、他の特定のレジスタが通信できなかったり、コマンドを連続送信できなかったりします。

この原因を個人的な想像で考えると、たぶん、Wire ライブラリではACKチェックを必須としている場合があり、逆にSCCBインターフェースの場合はACKチェックは不要な場合があったりすることがあるからかもしれません。
その他、I2Cのstartコンディションとstopコンディションの入れ方の違いかもしれません。
(でも、実は Arduino core for the ESP32 の Wire ライブラリには、ACKチェックをスルーできたり、自分の好きな位置にstopコンディションを入れることは可能なのですが、いろいろ試してみても Wireライブラリではなぜかうまく行きませんでした。)

その他の原因として、Wireライブラリ中の Wire.h ファイルには、

#define I2C_BUFFER_LENGTH 128

と定義(2019/08/13時点)されていて、128byte以上の連続通信ができないのです。
ただ、I2C_BUFFER_LENGTHの値は変更可能で、それを変更してもダメで、128byte以内でstopコンディションを入れて通信して試しても何故かうまく行かないんです。

これって、SCCBインターフェースは I2Cインターフェースと互換性があるといっても、似て非なる物?かもしれません。

ロジックアナライザーで解析すれば原因究明できるかもしれませんが、今回はSCCBインターフェースですんなり動いたのと、ロジアナでSCCB解析する時間が無かったので今後の課題としたいと思います。

そんなこんなで、今回は、esp32-camera ライブラリにあるSCCB用関数を流用しました。
(実はそれは Arduino core for the ESP32 の i2cライブラリを使って作られていますが・・・。)

SCCB用関数を使うと、あっさりスムースに OV2640 が動作してくれます。
もしかしたら、Wireライブラリでトラブってどうしようもない時、SCCB用関数を使うと良いかもしれませんね。

因みに、先にも述べたように、OV2640のSCCBインターフェースは、XVCLK 端子にクロックを供給してやる必要があるので注意して下さい。

ESP32とOV2640をSCCBで制御できるようにする設定

では、Arduino core for the ESP32 を使って、ESP32 と OV2640 を SCCBインターフェースを使って通信できるようにするプログラミングを紹介します。

これは、esp32-cameraライブラリにある、sccb.h および sccb.c というファイルに記述されているものを私が改変しました。
元々それは OpenMV project の Ibrahim Abdelkader さんが MIT ライセンスで公開されているものを使っているそうです。

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

/*  This was modified by mgo-tec with sccb.c in esp32-camera library.
 *  sccb.c file is part of the OpenMV project. 
 *  Copyright (c) 2013/2014 Ibrahim Abdelkader.
 *  This work is licensed under the MIT license.
 *  URL: https://opensource.org/licenses/mit-license.php
 */
#include <driver/ledc.h>
#include <driver/i2c.h>

const int8_t cam_pin_RESET = 17;
const int8_t cam_pin_XVCLK = 27;
const int8_t cam_pin_SIOD = 21;
const int8_t cam_pin_SIOC = 22;

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

const uint8_t ov2640_i2c_addrs = 0x30;
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;

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

void loop() {
}

void 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{
    Serial.println("Can't detected camera OV2640");
  }
}

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 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 = 200000;

  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 sccbRead(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("sccbRead Failed addr:0x%02x, reg:0x%02x, data:0x%02x, ret:%d", scan_i2c_addrs, reg, data, ret);
  }
  return data;
}

uint8_t sccbWrite(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("sccbWrite ret=%d\r\n", ret);
  if (ret != ESP_OK) {
    Serial.printf("sccbWrite 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;
}

先にも述べたように、OV2640のSCCBを動作させるためには、39行目でESP32 の LEDC で20MHz のクロック信号をOV2640 の XVCLK 端子に供給してやります。

見て分かる通り、Wireライブラリを使うよりもかなり複雑で難解に見えますね。
でも、一旦関数化してしまえば、あとは sccbWrite や sccbRead 関数で制御できるため、それほど気にしないで使えば意外と難しくないと思います。
詳細によく見ると、ACKチェックの有無やstart,stopコンディションが自由に入れられるので、Wireライブラリよりもひょっとしたら使い勝手が良いかも知れません。

Arduino core for the ESP32 の Wire ライブラリの場合は、シンプルに関数化されているので、逆に自由が効かなくて、トラブった時には原因追及に時間がかかってしまうかも知れませんね。

84-101行目のsccbProbe関数は、デバイスのI2Cアドレスを0x00から順に調べて行って、デバイスから返信があったものをI2Cアドレスとして検知するという動作です。

これを Arduino IDE でコンパイル書き込みして、シリアルモニターに下図の様に表示されればOKです。

これで、ESP32 と OV2640 が SCCBインターフェースで通信できるようになりました。
でも、これだけではちゃんと動作しません。
次で紹介する OV2640 のカメラ設定をせねばなりません。

OV2640のカメラ初期化レジスタ設定

SCCBインターフェースでOV2640の制御が可能になったら、今度はOV2640からまともな画像データを送信してもらうようにコマンドを送らねばなりません。
たとえば、今回は画質をCIFモード(OV2640の最小サイズモード)にして、RGB565フォーマットのデータを送る場合を考えます。
RGB565フォーマットについては、前回の記事でも述べましたが、おさらいすると下図の様な感じです。

要するに、赤色5bit、緑色6bit、青色5bit で、緑色だけがバイトをまたいでいるデータフォーマットのことです。
これは、RGB888フォーマットよりもデータ量が少なく、フルカラーの高速表示に適しています。

では、初期化設定で、CIFモード、RGB565フォーマットにしてSCCBでコマンドを送る場合は以下のようにします。
sccbWrite関数の第1引数が制御コマンドレジスタアドレスで、第2引数がそのレジスタの値です。
OV2640データシートと合わせてOmni Vision社発行のOV2640 Camera Module Software Application Notes のRGB565 Reference Setting を参照しながら見てみてください。

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

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

どうですか?
メチャメチャ沢山のコマンドあり過ぎですよね!
どうしてこんなに多量にコマンドを送らねばならないかと言うと、OV2640 の製造元 Omni Vision社発行の
OV2640 Camera Module Software Application Notes
というPDFファイルの
13.2 RGB 565 Reference Setting
に書いてあることに習ったものです。
この通りコマンドを送信すると、OV2640から正常な画像がちゃんと送られてきます。

でも、これ、疑問に思いませんか?

OV2640のデータシートの制御コマンドレジスタ表には無いレジスタアドレスが沢山あります。
例えば、レジスタ 0x90~0x97 は、データシートには何の動作をするのか記載がありません。

ただ、よく見ると、0x8D-0xBF は Reserved と書かれているのがわかります。
つまり、8D-BF はメーカー側がOV2640チップに既に予約して書き込んである制御コマンドだと思われます。

ということは、ユーザーにはあまり操作されたくない、隠蔽した専用コマンドなのでしょう。

いずれにしても、OV2640にはこのように不明な多量のコマンドを送らなければ、まともに動作しないので、この通りにする必要があるということのようです。

さて、sccbWrite関数の第1引数に 0xff という16進数があると思います。
これは、OV2640データシートにあるように、レジスタには2つのbankがあって、そのbankを切り替えるためのコマンドです。
それぞれのbankについては

bank=0x00 : OV2640のDSP設定
bank=0x01 : OV2640のカメラセッティング等(ホワイトバランスや露出等)の設定

となっています。
このbankを切り替えたら、それ以降のコマンドは全てそのbankで動作し続けます。

このように、OV2640 のレジスタ初期化設定は、コマンドを連続して多量に送るために、Wire ライブラリではなかなか難しいことが分かると思います。

コメント

  1. Gustavo Murta より:

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

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

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