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

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

OV2640のJPEG出力は、esp32-cameraライブラリやサンプルスケッチCameraWebServerを読み解くしかなかった

今までM5Cameraで私がプログラミングして、このブログで紹介してきたものは全てRGB出力のビットマップ(BMP)画像でした。
WiFi転送もビットマップ画像でした。

ビットマップ画像はそのままLCD(液晶ディスプレイ)に表示できるので、プログラミング的には便利でしたが、WiFiで動画ストリーミングしようとすると、残念ながら実用的ではありませんでした。
そこで、今回は思い切ってJPEG画像を扱ってみようと思ったわけです。

M5Cameraのイメージセンサ OV2640には、RGBやYUV出力の他にJPEG出力が装備されているのは以前から分かっていて、Arduino core for the ESP32のサンプルスケッチ CameraWebServer もJPEG画像を扱っていることは分かっていました。
だったら、それほど苦労せずとも出来そうな気でいました。

でも、いざ、OV2640のJPEG出力を自分でプログラミングして抽出してみようとすると、どうやって良いのかサッパリ見当も付きませんでした。
イメージセンサからのJPEG出力方法や、その画像の取り出し方についてはネット上や情報誌に殆ど情報がありませんでした。
あったとしても、専門家用のものばかりで、私には到底理解できないものでした。

じゃぁ、esp32-cameraライブラリを読み解いていくしかありません。
でも、これが本当に難しくて、長い長ーい時間と労力が必要でした。

まず、一番に疑問だったことは、OV2640のパラレルデータのJPEG出力です。

RGBビットマップ出力の場合は、以前のこちらの記事で紹介したように、垂直同期(VSYNC)と水平同期信号(HREF)とピクセルクロック(PCLK)で構成されていて、だいたい理解できていたのですが、JPEG出力の場合は画像が圧縮されているので、水平同期(HREF)やピクセルクロック(PCLK)は一体どうなるの? ということです。

次に疑問だったのは、JPEG出力されたデータをどの様にしてマイコンへ取り込むのかということです。
この具体的な方法がどこにも情報がありません。
頼みのデータシートには、JPEGのレジスタ設定のみの記載だけで、他は全く情報は無いですし、OV2640アプリケーションノートにもありませんでした。

JPEGデータさえマイコンに取り込むことができれば、前回の記事で紹介したライブラリを使ってJPEGデコードしてRGBビットマップに変換できるので、そこが肝でした。

そこで、GitHub にあるesp32-cameraライブラリを読み解いていくわけですが、ソースコードを眺めているだけでは全然わかりません。

ならば、重い腰を上げて、長い間引き出しにしまっておいたロジックアナライザやオシロスコープを取り出して実験してみたんです。
そしたら、なんと!
一目瞭然ではないですか!
esp32-cameraライブラリのやっていることが、ようやく理解できるようになってきたというわけです。

あともう一つ、このブログを書きながら気づいたことがありました。

ソースコードを眺めていても、オシロスコープやロジックアナライザで解析しても、イマイチ掴めなかったことが沢山ありました。
でも、このブログを書いて、実験したことを言葉にして、図を描いたりしていると、ふと閃いたり、急に理解できるようになったことが多々ありました。
ブログを書くことは正直苦痛ですが、役に立つこともあるんだなと改めて思いました。

ということで、以下、今回の実験を自分流の言葉で説明していきます。
想像で書いていることも有りますのでご了承ください。

VSYNC(垂直同期)信号の割り込み初期化はネガティブエッジに設定

以前のこちらの記事では、I2S信号の割り込みについて触れましたが、今回、JPEGデータを扱うに当たって、VSYNC信号の割り込みが非常に重要な役割を担っています。

イメージセンサ OV2640のVSYNC(垂直同期)信号は、JPEG出力の場合、HIGHレベルの間に画像1枚(1フレーム)分のデータが送られてきます。
要するに、前回記事で述べたように、JPEG開始マーカー FFD8 ~ 終了マーカー FFD9 まで送られてきます。
よって、VSYNC信号の割り込み発生を起点としてJPEGデータの検出処理をすれば良いわけです。

VSYNC信号の割り込み設定方法は、M5Cameraスケッチの initCameraDMA関数内で、GPIOのVSYNC信号割り込み許可設定をします。

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);

ESP_INTR_FLAG_LEVEL1は、Arduino core ESP32 の esp_intr_alloc.h に定義されていて、割り込み優先順位が最も低いレベルという意味です。
ESP_INTR_FLAG_IRAM は、おそらくIRAM領域からの呼び出しと関係があると思います。

そして、次のvsync_intr_enable関数のように 、IRAM_ATTR属性を付けて、IRAMとう特別なメモリに配置させて、割り込み検知で素早い読み込みができるようにしておきます。

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

GPIO_INTR_NEGEDGE は、VSYNC信号がHIGHレベルからLOWレベルへ遷移する、つまり立ち下がり(ネガティブエッジ)で割り込み発生という設定です。
VSYNC信号がHIGHレベルの間にJPEG信号が送られてくるはずなのに、なぜ立ち下がり(ネガティブエッジ)で割り込み設定しなければならないのでしょうか?

これについては長い間疑問だったのですが、今回、ロジックアナライザやオシロスコープで解析してみて、ようやくわかってきたのです。
次の節で説明します。

レジスタ直叩きdigitalWrite関数にIRAM_ATTR属性を付け、VSYNCやDMA,I2S割り込みのタイミングをロジアナやオシロで視覚化してみた

ESP32のVSYNC(垂直同期)信号割り込みや、DMA (Direct Memory Access)によるI2S割り込みのタイミングって、プログラムソースコードを眺めていても、イマイチ良く解らないんですよね。
シリアルモニターに出力するようにしても、シリアルモニターの出力速度が遅くて、正確な割り込み発生位置を捉えることは困難でした。

そこで、Arduino core for the ESP32 のdigitalWrite関数を割り込み発生位置に置いて、GPIOのHIGHとLOWを切り替えて、ロジックアナライザやオシロスコープで確認しようと考えました。

でも、M5Cameraスケッチにある割り込み関数の vsync_isr()関数や、i2s_isr()関数は、以前のこちらの記事でも触れたように、IRAM_ATTR属性が付いていて、digitalWrite関数には付いていません。
IRAM_ATTR属性を付けると、IRAMという特別なメモリに関数を格納して、割込み信号を検知したら素早く読み出せるようになる属性らしく、vsync_isr()関数や、i2s_isr()関数内にIRAM_ATTR属性の無いdigitalWrite関数を置いてしまうと、コンパイルエラーになってしまいます。

そこで、digitalWrite関数内で使用されているGPIOレジスタ直叩き指令を拝借して、新たにIRAM_ATTR属性を付与したテスト用diritalWrite関数を作りました。
以下の感じです。

const uint8_t test_pin = 4;

setup(){
  pinMode(test_pin, OUTPUT);
}

static void IRAM_ATTR test_digitalWrite(uint8_t pin, uint8_t val);

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));
    }
  }
}

これがあれば、割り込み関数だろうがそうでなかろうが、どこへでも好きな位置にテスト用digitalWrite関数を置けます。
因みに、setup関数内で事前に pinMode関数を使ってOUTPUTに設定しておくことを忘れないようにします。

例えばGPIO #4をアサインして、vsync_isr関数やi2s_isr関数内に置いて、HIGH と LOW を切り替えれば、オシロスコープやロジックアナライザで可視化できるわけです。

では、先ほど紹介した ESP32-DevKitCとOV2640モジュールの測定用回路を使って、GPIO #4からちゃんと信号が出ているか、ロジックアナライザ LAP-C で 測定してみます。

vsync_isr()関数に
test_digitalWrite(test_pin, HIGH);
と置いて、i2s_isr()関数に
test_digitalWrite(test_pin, LOW);
として置きます。

VSYNC(垂直同期)信号の立ち下がり(ネガティブエッジ)付近をロジックアナライザで見ると下図のようになりました。

やっぱり、ロジックアナライザって便利ですね。
一発で信号のタイミングが把握できて、スバラシイです。

見て分かる通り、JPEGデータの終了マーカー FFD9 を受信した後、VSYNC(垂直同期)信号が立ち下がり、その後にGPIO #4 が立ち上がっています。
概ね予想通りでした。
VSYNCのネガティブエッジによる割り込みがしっかり実行できていることが分かりました。

では、今度は、VSYNC(垂直同期)信号の立ち上がり(ポジティブエッジ)付近を見てみます。

なるほど!!!
っていう感じです。

VSYNCが立ち上がってから、ほんのわずかな時間でI2S信号のJPEGデータが送信されています。

そして、JPEGデータを数バイト受信した後、GPIO #4 が立ち上がっていて、そこでi2s_isr関数の割り込みが発生していることが分かります。
これはちょっと意外でしたね。

私の予想では、JPEGデータを受信した直後にI2S割り込みが発生すると思っていましたが、もうちょっと間があったみたいです。
(実はこれ、FIFOメモリの読み出しが初回のI2S割り込みから可能ということを意味していますが、それについては後で述べます。)

このVSYNCが立ち上がってからI2S割り込みの発生する短い時間間隔では、CPUではろくな処理ができません。
よって、VSYNC立ち上がりを合図としてFIFOメモリのリセット処理をすると、JPEGの先頭データを取りこぼしてしまいます。
実際にVSYNCをポジティブエッジ割り込みでプログラミングしてみたら、JPEGデータを受信できませんでした。
よって、VSYNCの立下り(ネガティブエッジ)でメモリのリセット処理をすれば良いわけですね。

ということで、IRAM属性を付けたテスト用digitalWrite関数を使ったおかげで、VSYNC割り込みとI2S割り込みのタイミングが明白に分かりました。
これで、DMAによるFIFOメモリ書込み順を意識してプログラミングする道筋ができました。

ところで、組み込み開発の世界ではJTAGというデバック手法があるらしいですね。
ESP32はそれを備えているようです。
私はJTAGを使ったこと無いのですが、たぶんこれに近い方法なのかな? と想像しています。
いつか試してみたいと思っています。

イメージセンサOV2640からのJPEG出力を調べる

では、OmniVision製イメージセンサ OV2640 のJPEG出力を調べていきます。

まず、OV2640のデータシートを眺めると、初期化の時に、ESP32からSCCBインターフェースで以下のレジスタコマンド
0xE0 (RESET)
0xDA (IMAGE_MODE)
を設定すれば、JPEG出力モードに切り替わると書いてあります。

そのレジスタコマンド0xDAの後のデータのBit[1] を1に設定すれば、
HREF timing select in DVP JPEG output mode.
HREF = VSYNC
となるとデータシートに書いてあります。

???
何だとぉ???

JPEG output モードに切り替わって、水平同期(HREF)と垂直同期(VSYNC)が同じになるのかな???
意味不明です。

以前のこちらの記事でビットマップ(BMP)データ出力を扱った時には、HREF信号が重要な役割を担っていました。

ならば、JPEG出力モードにしたらどうなるかを、ロジックアナライザ ZEROPLUS LAP-C で解析してみます。

OV2640からのJPEGデータ開始マーカーFFD8と終了マーカーFFD9をロジックアナライザで見る

では、OV2640のVSYNCがLOWレベルからHIGHレベルへ遷移するところ、つまり、立ち上がり(ポジティブエッジ)付近をロジックアナライザ LAP-C で見てみると、こうなりました。

バッチリです!
JPEG開始マーカー FFD8 が検出できました!
ロジアナって改めてスゲーなと思いましたね。
OV2640からの出力データが一目瞭然です。
PCLKやXVCLKの様子も良く解ります。

では、VSYNC立ち下がり(ネガティブエッジ)付近を見てみます。
以下のようになりました。

これもバッチリです!
JPEG終了マーカーFFD9がしっかり検知できています。

ただ、その後すぐにはVSYNC立ち下がっているというわけではなく、ずっと後にLOWレベルになっています。

JPEGの画質設定

イメージセンサ OV2640のJPEG出力画質を変えるには、SCCBインターフェースで、レジスタコマンドを送ってやります。

OV2640のデータシートによると、bank(0xFF)設定のデータを0x00のDSP設定に切り替えて、レジスタコマンド 0x44 で設定します。
レジスタ名は Qs です。

これは、Quantization Scale Factor というらしく、JPEG圧縮のパラメーターの一つらしいです。
10~63 の範囲で設定します。
10が最高画質、63が最低画質です。

注意するところは、一般的に数値が大きい方が高画質というイメージだと思いますが、このQsの場合は逆です。
今回は、10~60までの設定にしています。

例えば、最低画質のQs = 60 にしたい場合は、
writeSCCB(0xFF, 0x00);
writeSCCB(0x44, 60);
とSCCB送信すれば良いわけです。

HREFとPCLKを調べる

では、今度はHREF信号とPCLK信号に注目して見てみます。

JPEGを最低画質(Qs=60) にした波形は以下です。

JPEG画質を落とすと、間がスカスカになるんですね。
そして、HREFがHIGHレベルになっている間だけD0~D7にデータが出力されている様子が分かります。
それと、PCLK(Pixel Clock)信号に注目してみると、単なるクロック信号のようにデータ出力と関係なく一定のパルスが出ているように見えます。

ではその付近をちょっとズームしてみると以下のようになります。

このように、HREF信号がHIGHの時だけD0~D7のデータは有効ということが分かりますね。

それと、PCLKについては、以前のこちらの記事で紹介したRGB出力の時のPCLKとは明らかに異なっていて、D0~D7のデータ有効性とは関係が無いようですね。
ただ、PCLK波形の1周期分相当がD0~D7のパラレルデータ1つ分のパルス幅と同じ幅になっていて、PCLKを分周して周波数を下げる場合、D0~D7のパルス幅も同様に変化するので、PCLKはD0~D7データのパルス幅を決定していると想像します。

このことから、PCLKの周波数を自分の意図通りに自由に変えることができれば、受信側のデバイスのCPU処理速度に合わせられそうですね。
PCLKの分周については後で詳しく述べます。

では、今度はJPEG画質を変えた場合、OV2640からのパラレルデータはどう変化するのかを見てみます。
VSYNCがHIGHレベルの間は17ms以上あって、ロジックアナライザ LAP-C では全部を見ることができませんので、オシロスコープでVSYNCとHREFの波形をザックリ見てみます。

まずは、JPEG出力のフレームサイズを240 x 176 pixel にします。
最高画質(Qs=10)の場合です。

この画像ではちょっと分かりにくいかも知れませんが、赤色のVSYNC波形の1波長は20msです。
つまり、1秒間に50回HIGHレベルになるので、イメージセンサ OV2640 からは 50fps という速度でJPEG画像を出力しているということになります。
そして、VSYNCがHIGHレベルの間にHREFが23回HIGHレベルになっていることがわかります。

RGBビットマップ出力の場合、HREFは水平同期信号ということを考慮すると、JPEG出力の場合のHREFが23回ということは、もはや水平同期信号とは一切関係が無いということですね。

では、JPEG画質を Qs=60として、最低画質にしてみます。
つまり、圧縮率が高くなり、JPEGデータ量は少なくなるはずです。

これを見ても、Qs=10 とあまり変化がありませんね。
実は、厳密にはHREFのHIGHレベル区間が変化しているのですが、VSYNCがHIGHの間のHREFの回数は同じです。
そして、VSYNCの1波長も20ms、つまり50fpsというのも同じです。
つまり、JPEG画質を変えてもフレームレートは変化しないということが分かりました。

では、今度はPCLKの周波数を変化させてみます。
まずは、最高速度 20MHzにした場合です。
見やすいように先ほどのグラフよりズームしてみます。

VSYNCがHIGHレベルの間のHREFは23回と変わりありませんし、VSYNCの波長も変化なく、50fpsのままです。

では、PCLKを1.25MHzとして速度をガクンと落としてみます。

VSYNC立ち上がり付近のHREFの2山の幅が明白に変わりましたね。
厳密には他のHREFの幅も大きくなっています。
でも、VSYNCのパルス幅も変化無しです。
ということは、PCLKを分周して速度を落としても、フレームレートは変化しないということです。

では、今度はフレームサイズを96 x 96 pixelにして、小さい画像にしてみるとこうなりました。

画角を96 x 96 pixelという小さいサイズにしても、フレームレートは50fpsのままでした。
ただ、VSYNCがHIGHレベルの間のHREFは13回と激減しましたね。

以上から、先ほど述べたOV2640データシートの、レジスタコマンド0xDAについて、データのBit[1] を1に設定した場合の、
HREF timing select in DVP JPEG output mode.
HREF = VSYNC
という文面を個人的に解釈すると、JPEG出力モードの場合のHREFは単なるD0~D7のパラレルデータが有効かどうかの判別に使うだけで、画質や画角を変化させた場合、VSYNCがHIGHレベルの間にHREFの回数を変化させて50fps内に収めているわけで、HREFがHIGHの間にJPEG画像1フレーム分を出力しているというわけです。

ならば、VSYNC=HREF というのも大体納得いきますね。
でも VSYNC=HREF という文面だけ見ると、勘違いしますね。
こうやって測定して初めて分かることですね。
ようやく腑に落ちました。

以上より、イメージセンサ OV2640 のJPEG出力モードにすると、VSYNCがHIGHレベルの間にJPEGデータ1フレーム分を出力していて、デフォルト設定では50fpsの速度で出力しているということが分かりました。

そして、フレームサイズ(画角)やJPEG画質によらず、フレームレートは変化しないということが分かりました。

では、次はFIFOメモリ初期化と自己参照構造体について説明します。

コメント

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