QuickLogic EOS S3でCMOSカメラOV5642を駆動する
小規模FPGA+小規模マイコンならではの使い方としてイメージセンサの駆動は最適かもしれません。
マイコンだけだと、駆動ロジック自体は単純な割に処理速度がギリギリになってしまうからです。外部メモリーIFを持っているマイコンであれば駆動は簡単ですが、そのような品種は比較的高価で多ピンの場合が多いので、EOS S3はそれらに対するアドバンテージと言える、かも。知らんけど。
※ QuickLogic EOS S3評価ボードはスイッチサイエンスさんでも発売中。
www.switch-science.com
この記事は、イメージセンサの選定から、ロジックの検討、HDL記述、CPUからFPGAへのアクセス、とりだした画像データのMATLABによるデコードまで行います。
- イメージセンサ検討
- イメージセンサ初期化
- 画像データのストア方法検討
- FPGAロジック設計方針
- SRAMが足りない問題
- CPU側のプログラム
- ハードウェア
- プロジェクト
- 動作方法
- RGB565をプレビューしたい
- 動作風景
イメージセンサ検討
扱いやすく安価なイメージセンサがあります。
akizukidenshi.com
外部から24MHzでクロックを入れる必要があります。
MIPIインターフェースのみを持つカメラが多い昨今で、OmniVision OV5642はDVP(=8bitパラレルIF)を持ちVGAサイズ時60fpsまで駆動できるこのデバイスはかなり重宝します。EOS S3のようにカメラ用IFハードマクロを持たない古典的なCPLD/FPGAでも駆動できるからです。
イメージセンサ初期化
SCCB(I2C)インタフェースで設定をします。今回VGAサイズ、RGB565フォーマット出力に設定しています。この記事でSCCB設定方法について書くつもりはありません。OV5642はイメージセンサにしては結構「枯れた」デバイスですので、検索すると設定方法のブログや記事などが結構見つかります。実際に記述したコードはこちらをご覧ください。
画像データのストア方法検討
EOS S3のハードマクロSRAMは8kbyteあります。もちろんこれを画像データの蓄積に利用するのですが、VGAサイズでRGB565=16bit/ピクセルの場合、640x480x2[byte/pixel]=600kbyteとなるので、必要データ数の75分の1しかありません。対策は以下のいずれかを選ぶと思います。
1は真っ当な解決方法です。ほとんどの組み込みエンジニアは異論は無いでしょう。2もできるならしたいところです。やってみて出来そうだな、という手応えはありましたが、それ以外のこと(例えば画像処理など)を並行してさせると一気に破綻しそうなので、今回は採用しませんでした。3は一見すると意味不明ですが、事と次第においては最適解です。例えば画像処理において、取得できた画像のすべての要素がROI*1ということはまずあり得ません。画像中でROIとなるオブジェクトは多くて数点、画像中の占める割合で言うと25%~30%くらいでしょう。
加えて量産の場合、せっかく格安FPGAを採用したのにRAMを外付けするというと稟議で「PoCまではRAM付けていいけど、量産(前試作)までに外してね 」と返ってくることもあります。量産だと一個百円のデバイスでも苦い顔される。購買サイドで考えればわかんなくないけど。
というわけで、今回は3を試してみます。1でもQSPI IoT SRAMで実験したので、また後日ブログにしようかと思います。
FPGAロジック設計方針
eFPGAでOV5642からのピクセルクロック、VSYNC、HREF、8bit信号を取得するロジックを記述します。データはハードマクロ非同期SRAMに記録しAHB to WishboneでCPUメモリ空間からリードできるようにします。
問題はWishboneバスブリッジが最大10MHzであるということです。お世辞にも高速とは言えません。転送帯域を稼ぐため、8bitデータを4回分(=32bit)をFPGA側で受信後に整形し、RAMに記録する事にしました。下図に示します。
こうすると、ピクセルクロック(PCLK)=24MHzでデータが転送される場合、RAMへのアクセススピードは24/4=6MHz(<10MHz)となり帯域を十分確保できます。詳細は後述しますが、指定された分の記録が終わるとステートマシンは動作を止め、CPUへ完了した旨をステータスビットを1にして知らせます。CPUから解除信号を受信するとステートマシンはリセットされ動作を再開します。
ステートマシンを記述します。
localparam CRSET = 2'd0; // RESET localparam CB08F = 2'd1; // Camera buffer 8bit Full localparam CB16F = 2'd2; // Camera buffer 16bit full localparam CB24F = 2'd3; // Camera buffer 24bit full localparam RST32 = 32'hFFFF_FFFF; localparam RST11 = 11'h7ff; assign cam_data_valid = HREFI & VSYNCI ; always @( posedge PCLKI or posedge WBs_RST_i) begin if(WBs_RST_i) begin cam_reg1 <= RST32; cam_reg_out <= 32'h00; cam_status <= CRSET; cam_reg_rdy <= 1'b0; cam_ram_cnt <= RST11; cam_freerun <= RST32; end else begin // PCLK if(cam_data_valid & cam_go_flag)begin case(cam_status) CRSET: begin cam_reg1 <= {24'h00, CAM_DAT[7:0]}; cam_reg_rdy <= 1'b0; cam_status <= (cam_state_stop)? cam_status : CB08F; end CB08F: begin cam_reg1 <= {16'h00,cam_reg1[7:0],CAM_DAT[7:0]}; cam_freerun <= cam_freerun + 32'h01; cam_ram_cnt <= (cam_ram_cnt + 11'h01); cam_reg_rdy <= 1'b0; cam_status <= CB16F; end CB16F: begin cam_reg1 <= {8'h00,cam_reg1[15:0],CAM_DAT[7:0]}; cam_reg_rdy <= 1'b0; cam_status <= CB24F; end CB24F: begin cam_reg_out <= {cam_reg1[23:0],CAM_DAT[7:0]}; cam_reg1 <= 32'h0; cam_reg_rdy <= 1'b1; // 非同期SRAM書き込み信号 cam_status <= CRSET; end endcase end else if(~cam_go_flag)begin cam_reg1 <= RST32; cam_reg_out <= cam_reg_out; cam_status <= CRSET; cam_reg_rdy <= 1'b0; cam_ram_cnt <= RST11; cam_freerun <= RST32; end else begin //!cam_data_valid cam_reg_rdy <= 1'b0; cam_status <= cam_status; cam_reg_out <= cam_reg_out; end end // PCLK end
SRAMが足りない問題
この問いに一言で回答するなら「画像データの一部分だけをスキャンして残りは捨てる」です。前述したようにROIが判明している場合は他の要素が邪魔になるのでよくやる手法です。メモリが足りないという理由でやることはあまりしませんが…。
OV5642からは1秒あたり数フレームから数十フレーム相当のデータが絶えず流れてきます。今回は8kBしかRAMがないので、
- 1フレーム目は最初から8kB分を取得しCPUへ転送。フレーム中の残りのデータは捨てる
- 2フレーム目は8kB~16kBまでの8kB分を取得しCPUへ転送。フレーム中の残りのデータは捨てる
- 3フレーム目は16kB~24kBまでの8kB分を取得しCPUへ転送。フレーム中の残りの(以下略
.....
- 75フレームでVGAサイズ1枚分データがようやく取得完了
と、75回繰り返してデータを取得することになります。このやり方だと動く物体を撮像するのは少し無理があります。
今回はUSBシリアルで取得データをゆっくり転送する都合上、さらに時間を要するのでさらに遅くなります。しかたないね。
ROIを考慮する場合はデータは少なくて済むはずなのでここまで手間ではないと思います。
CPU側のプログラム
ハードマクロSRAMは32bit x 512ワードの非同期SRAMを4つ接続しています。4つのアドレスは決まっているため、C側では以下のように宣言しています。
volatile uint32_t **fb_ram0 = 0x40022000; volatile uint32_t **fb_ram1 = 0x40024000; volatile uint32_t **fb_ram2 = 0x40026000; volatile uint32_t **fb_ram3 = 0x40028000; volatile uint32_t **fb_status = 0x4002a000;
最後のfb_status
はステータスレジスタです。ステータスレジスタ読み書きによってFPGA側のモジュールの起動・停止などを行います。ステータスの詳細は下記の通りです。read only / write onlyはそれぞれCPU側(=Cソースコード)から見た時の状態です。
以上を踏まえVGAサイズのデータ1枚分を取得するコードを記述すると、以下のように記述できます。
uint32_t fboundary = 0; uint32_t a[512*4]; // 8kb分のデータ保管用 for(uint32_t ii = 1 ; ii < 76; ii++) { fboundary = ii;// *(volatile uint32_t *)fb_status = (0x00000 | fboundary); // リセット *(volatile uint32_t *)fb_status = (0x10000 | fboundary); // fboundary番目のデータリードスタート while( 0x08 != (*(volatile uint32_t *)fb_status & 0x08) ); //転送は完了したか *(volatile uint32_t *)fb_status = (0x00000 | fboundary); // ステートマシンリセット memcpy(&a[512*0], fb_ram0, (512 * sizeof(uint32_t))); // ram0 -> a memcpy(&a[512*1], fb_ram1, (512 * sizeof(uint32_t))); // ram1 -> a memcpy(&a[512*2], fb_ram2, (512 * sizeof(uint32_t))); // ram2 -> a memcpy(&a[512*3], fb_ram3, (512 * sizeof(uint32_t))); // ram3 -> a for(uint32_t i = 0 ; i < 512*4 ; i++) { dbg_hex32(a[i]); dbg_str("\n"); // シリアルデータ送信 } }
ハードウェア
QuickFeatherに直接はんだづけ。こんな雑な配線でもデータが化けないので感動します。ボードのリセットボタンとユーザーボタンがすり減って反応が鈍くなってきたので、そろそろ基板を起こそうかと考えています。
カメラデータ取得・通信用にUSBシリアルを接続しています。
結線図を書く気力がないので、カメラとQuickFeatherとの配線情報はpcfを参照ください。
プロジェクト
下記githubからcloneしてください
github.com
git clone -b v0.0.5 https://github.com/panda5mt/qf_wbfpga_pio/
動作方法
書き込み後、リセットするとスタートします。ボーレートはかなり早めの921600baud、8N1でデータがシリアルから取得できます。
ターミナルを使っているならば、パイプで一度テキストファイル等に保存した方が取り扱いが楽だと思われます。
下記例はpicture.txtにデータを保存しています。/dev/ttyUSB0
はUSBシリアルのポートです。各自の環境で変更してください。
例:
$ cu -l /dev/ttyUSB0 -s 921600 > picture.txt
RGB565をプレビューしたい
ビットマップのヘッダとかよくわからなかったのと、わかったところで真面目に実装する気力もないのでMATLABの力を借りました。先ほど取得できたpicture.txtを読み込みます。
数十行で書けてしまう。MATLABはいいぞ。
clc; format long; RGB_img = zeros(480,640,3,'uint8'); img = zeros(480,640,'uint32'); lower5 = hex2dec('1f') .* ones(480,640,'uint32'); % 0x1f 0x1f .... lower6 = hex2dec('3f') .* ones(480,640,'uint32'); % 0x3f 0x3f .... lower8 = hex2dec('ff') .* ones(480,640,'uint32'); % 0xff 0xff .... lower16 = 65535 .* ones(480,640,'uint32'); % 0xffff 0xffff .... fileID = fopen('picture.txt'); while (true) tline = fgetl(fileID); disp(tline); if contains(string(tline),'!st') disp("start signal received..."); break; end end for HGT = 1:480 for WID = 1:2:640 data = fgetl(fileID); if -1 ~= data img(HGT, WID) = hex2dec(data); img(HGT, WID+1) = bitshift(hex2dec(data),-16); end end end img = bitand(lower16, img); %img = RGB565 imgR = (255/63) .* bitand(lower5, bitshift(img,-11)); % Red component imgG = (255/127).* bitand(lower6, bitshift(img,-5)); % Green component imgB = (255/63) .* bitand(lower5, img); % Blue component RGB_img(:,:,1) = imgR; RGB_img(:,:,2) = imgG; RGB_img(:,:,3) = imgB; imshow(RGB_img);
動作風景
できました!
埼玉県新座市イメージキャラクタ 「ゾウキリン」 (C)2010 新座市
MATLABのプロジェクトはこちらです(上記「プロジェクト」に含まれています)
https://github.com/panda5mt/qf_wbfpga_pio/tree/v0.0.5/matlab
おしまい。
*1:Region of Interest:関心領域