lynxeyedの電音鍵盤

MBDとFPGAと車載で使うデバイスの備忘録

QuickLogic EOS S3でCMOSカメラOV5642を駆動する

小規模FPGA+小規模マイコンならではの使い方としてイメージセンサの駆動は最適かもしれません。
マイコンだけだと、駆動ロジック自体は単純な割に処理速度がギリギリになってしまうからです。外部メモリーIFを持っているマイコンであれば駆動は簡単ですが、そのような品種は比較的高価で多ピンの場合が多いので、EOS S3はそれらに対するアドバンテージと言える、かも。知らんけど。

※ QuickLogic EOS S3評価ボードはスイッチサイエンスさんでも発売中。
www.switch-science.com

この記事は、イメージセンサの選定から、ロジックの検討、HDL記述、CPUからFPGAへのアクセス、とりだした画像データのMATLABによるデコードまで行います。

イメージセンサ検討

扱いやすく安価なイメージセンサがあります。
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. 外部大容量RAMを接続する
  2. CPU側はトータル512kbyteのSRAMを持っているのでFPGA/CPU協調動作と気合で頑張る
  3. 時代はミニマリスト。潔く画像全部を取り込むのは諦める

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に記録する事にしました。下図に示します。
f:id:Lynx-EyED:20211014125523p:plain

こうすると、ピクセルクロック(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ソースコード)から見た時の状態です。
f:id:Lynx-EyED:20211014161318p:plain
以上を踏まえ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シリアルを接続しています。
f:id:Lynx-EyED:20211014133256j:plain
結線図を書く気力がないので、カメラと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);

動作風景

できました!
f:id:Lynx-EyED:20211014162932p:plain
埼玉県新座市イメージキャラクタ 「ゾウキリン」 (C)2010 新座市

MATLABのプロジェクトはこちらです(上記「プロジェクト」に含まれています)
https://github.com/panda5mt/qf_wbfpga_pio/tree/v0.0.5/matlab

おしまい。

*1:Region of Interest:関心領域