ハードが用意できたので、続いてソフト編です。OV2640のSCCB制御にはI2C3を割り当てました。カメラに供給するクロックはTIM12を使ってSystemClockを分周して15MHz (90MHz / 6)を作っています。OV2640の初期化ルーチンは以前作ったものを使うつもりでしたが、STM32CubeF4の中のサンプルプロジェクトでカメラを使うものがあり、そこで使用するためにDrivers/BSP/Componets/ov2640というディレクトリがあり、ドライバーが入っていることに気がついたので、今回はこれを流用することにしました。ただし、このドライバではカメラから取り込んだ画像データをそのままLCDに送って表示することを想定しているようで、カメラの出力データ形式がRGB565になっていましたので、これをYUV422に変更して使っています。
画像データの取り込みにはDCMIを使い、そのデータはDMAでメモリに転送します。STM32CubeMXでの設定は次のようにしました。VsyncとHsyncの極性指定は、映像データの出力信号が無効な期間の極性を指定することに注意。
OV2640にはJPEGで画像データを取り込む機能がありますが、今回はその機能は使わずに無圧縮でデータを取り出し、UVCでホストに送信することにします。
STM32CubeF4が提供するHALのAPIとしては、HAL_DCMI_Start_DMA()が用意されています。引数のDCMI_MODEとしてDCMI_MODE_CONTINUOUSを指定してやると画像データを連続してDMA受信してくれます。DCMIの使い方を示すサンプルプロジェクトを見ると、1フレーム分のバッファを用意しておいてフレーム単位で受信する使い方をしているのですが、そのためにはSDRAMのような外部メモリが必要となってしまいます。今回の実験であるUVCカメラでは、受信したデータはすぐUSBから送信してしまえば良いので、フレーム分のバッファは不要です。そこで、2ラスタ分のバッファを用意しておき、これをダブルバッファリングで使うことにしました。つまり、1ラスタの受信が終わったら、そのデータをすぐにUSBから送信し、その間にもう一つのバッファにカメラからのデータを受信しておきます。
ラスタ単位での受信完了はDCMIのIT_LINE割り込みで検出することができ、HALのAPIではHAL_DCMI_LIneEventCallback()が呼ばれますので、この時点で受信できているデータをUSB UVCで送ってやります。 またUSB UVCではフレームの区切りをパケットのヘッダー情報として示してやる必要がありますが、この区切りはIT_VSYNCとHAL_DCMI_VsyncEventCallback()で検出してやります。
このようにしてなんとかOV640からのVGA画像をMac上のQuick Cameraアプリで表示できるようになりました。
室内で実験していますが、XPCLK=15MHzにておよそ9fpsとなりました。XPCK=22.5MHzにすると13.7fpsくらい出るのですが、PCに画像が表示できません。そこでカメラが出力している信号を確認してみると...
Hsyncの間隔が短い箇所では111.4usecしかありません。受信した1ラスタ分の画像信号を1マイクロフレームに入れて送っていたのですが、USB High speedのマイクロフレームの間隔は125usecですので、画像の取り込み速度に間に合っていません。Hsync信号自体は、1msに最大8回しか出ていないので、USBに出力するパケットデータをキューに溜めてから出力するようにしたところ13.7fpsの画像も表示できるようになったのですが、時折画像が乱れます。
画像が乱れる原因としては次のような事項が挙げられます。
- DCMIで受信した画像データを一旦USB送信用のバッファーにコピーしているため、その処理に時間がかかっている。DCMIとUSB送信処理でバッファを共有してコピー不要にすれば、改善できるはず。
- USB送信用バッファからSTM32F446のPDCの送信FIFOにデータを送る際にソフトでコピー処理を行っているために時間がかかっている。DMAを使えば改善できる。
無駄なデータコピーが2度も行われており、そのためにCPUサイクルが消費されてしまっているのが原因です。DMAを使うだけでも13.7fpsでの画像が安定することはわかっているのですが、 そうするとどういうわけか UVC のインターフェース切り替えを行なった際にUSBのスタックの動作がおかしくなってしまいます。USBスタックのDMA動作に問題が残っているようです。
今回の実装では、1ラスタ分のデータを1マイクロフレームに入れて送信しています。1ラスタ分のUVCデータ量は、YUVデータ(640*2) + ヘッダ(12) = 1292バイトになりますが、アイソクロナス転送での1トランザクションの最大データ量は1024バイトなので、1マイクロフレームの期間に2トランザクションを使ってデータを送っています。カメラが出力するHsync信号が1msあたり8回ですので、このような簡単な処理でも表示ができています。画像データサイズが大きくなったり、取得できる秒間フレーム数が増えた場合には、できるだけ多くのデータを1マイクロフレームで運ぶ必要が生じます。そのためには、2トランザクション分の容量2048バイトに画像データを詰め込んで送信したり、それでも足りない場合には1マイクロフレームあたり、3トランザクションを送るようにする必要があります。本当はそのあたりも挑戦してみたかったのですが、DMAで安定して1トランザクションで3パケットが送れるようにならないと無理ですね。DMAについては
STM32 Forumでもバグ報告されているので、今後のリリースに反映されることを期待したいです。