前回のポストでZYBOのZYNQ PS (ARMコア)を使ってOV7670カメラモジュールをI2C(SCCB)経由初期化できるようになりましたが、ようやく次のステップとしていた画像の取り込みとVGAモニターへの出力ができました。使ったカメラモジュールはaitendoさんから購入したOV7670です。
色々紆余曲折(最初の構想からの見直し)やFPGAを使った同期回路設計のスキル不足でかなり苦戦して、たかだか100行ちょっとのRTLコードを動かすのに1ヶ月近くかかってしまいました。まだまだ未熟ですが、(自己流ですが)少しノウハウがたまったので、顛末を書いてみます。
使用した開発環境は、最初はVivado 2016.2でしたが、最終的にVivado 2016.3にアップグレードしています。
当初の構想
最初は、既存のXILINX Video-OutとVTC IPを使って、画像の取り込み部分だけを作るのが近道かと思い(自分で記述する部分を最小にした方が確実に動かせると思っていた)この方向で情報収集を行いました。
Video Out IPに画像を流し込むためには、カメラ入力側にAXI4-Stream Masterインタフェースを作りこむ必要があります。今にして思うと、いきなりAIX4を作るというのも無謀な試みでした。You TubeにAXI-Stram Masterを作るチュートリアルビデオがあり、(英語ですが平易な語りなのでなんとか理解できますが、量は結構多い)これを参考にカメラデータの取り込みとAXI4-Stram出力のIPを作ってみました。単体のテストベンチではAIX4の制御信号を出力できているように見えていたのですが、Video OutやVTCと結合していきなり実機で動かそうとしても全く画像が表示されず。
当初、OV7670の解像度がVGA(640 x 480)なのでVGAモニターにスルーでデーターを流せると思っていたのですが、オシロで出力波形を観測すると垂直同期(リフレッシュ周期)が24Hz〜30Hz, 水平同期が12.5KHz位で、VGAのリフレッシュレート60Hz, 水平同期31KHzに対して速度が大きく異なるため、そもそもスルーで出力できないということが判明(考えてみれば、3000円以下のカメラモジュールで60Hzのリフレッシュレートを期待するのが間違っている)。Video OutやVTC IPもブラックボックスで中身が分からないため、問題の切り分けができず、表示部分も自分で作った方がよいと方針転換。
AXI4-Stramの実装は結局使い物にはなりませんでしたが、チュートリアルで学んだことはVivadoを使った開発フローを含めて結構有意義でした。
VGA画面表示も自分で作る
カメラモジュールとVGAの動作速度が異なるため、速度差を吸収するために、VRAM(Dual Port RAM)を間に入れて、カメラ入力に同期して画像データをVRAMに書き込み、VGAのPixelレートで画像データーを読み出せるようにする必要があります。
VGAのフル解像度を使うためには、必要なメモリー容量的にZYBOのDDR3-SDRAMを使う必要がありますが、いきなりDDR3-SDRAMを使うのはハードルが高いので、先ずはFPGA内に作れるBRAM(Block RAM)を使うことにしました。ZYBOに搭載されているZYNQ XC7Z010-1CLG400CのBRAM容量の制約から、VGAのフル解像度を使うのは諦めて、QVGA(320 x 240)で画像を取り込んで、VGA画面(640 x 480)の中央に320 x 240の画像を表示できるようにすることを目標に再設定。
このblogにあるVHDLの実装を参考に(というかほとんどパクっていますが…)Verilogに焼き直して、画像取り込みとVGA出力のIPを作成。参考にしたBlogはRGB444(12bitカラー)の画像データーをBRAMに取り込む際に、下位1bitを抜いた形でメモリーに格納していますが(BRAMの容量制約のために、19bit中の上位18bitをアドレスに使っている)、このようなデーターの間引きを行うとどういう形で画像データーが表示できるのかイメージができなかったのと、全部パクリでなく、多少オリジナルにしたかったため、RGB 565(16 bitカラー)のQVGAデーターを扱えるように改造しました。
再度カメラ画像取り込みとVGA表示のIPを作って単体でテストベンチを動かすと一応動いているように見えるので、BRAMやカメラモジュールと結合して実機で動かすとやはり画像が出力されず(画面が真っ暗)。オシロでVGA出力の信号を観測すると、VSYNCとHSYNCは正常に出力されているので、部分的には動作しているがどこが悪のか切り分けができず(後で、BRAMの読み出し側クロックを配線していなかったという、初歩的なチョンボだったことが分かるのですが..)。
モジュール単位のテストベンチだけでなく、全体を一気通貫で確認できるシミュレーション環境が必要と思い、以下のようにCPUブロックを除いた、カメラ画像取り込み→ BRAM → VGA出力のIPをつないだBlock Designを作ってシミュレーション環境にしました。
Test Benchを作る際は、Create HDL Wrapperが生成するBlock Designのインスタンスを雛形にして、試験信号生成の部分を追加記述しています。まず、以下の手順でWrapperファイルを生成。
生成されたWrapperファイルをベースに以下のテストベンチを記述
//Copyright 1986-2016 Xilinx, Inc. All Rights Reserved. //-------------------------------------------------------------------------------- //Tool Version: Vivado v.2016.3 (win64) Build 1682563 Mon Oct 10 19:07:27 MDT 2016 //Date : Tue Oct 18 21:28:14 2016 //Host : iMac2-Win running 64-bit major release (build 9200) //Command : generate_target ov7670_tb_wrapper.bd //Design : ov7670_tb_wrapper //Purpose : IP block netlist //-------------------------------------------------------------------------------- `timescale 1 ns / 1 ps module ov7670_tb_wrapper(); reg clka; reg clk25; reg href; reg vsync; reg pclk; reg resetN; wire [4:0]vo_b_data; wire [5:0]vo_g_data; wire [4:0]vo_r_data; wire vo_hsync; wire vo_vsync; reg [17:0] count; reg [7:0] test_data; reg [7:0] test_vector[307200:0]; // Instantiate device to be tested ov7670_tb ov7670_tb_i( // Instantiate ov7670_camera .pclk(pclk), .href(href), .data(test_data), // Instantiate VGA .clk25(clk25), .resetN(resetN), .vo_b_data(vo_b_data), .vo_g_data(vo_g_data), .vo_hsync(vo_hsync), .vo_r_data(vo_r_data), .vo_vsync(vo_vsync), .vsync(vsync) ); // Initialize test initial begin resetN <= 0; count <= 17'd0; vsync <= 0; href <= 0; #100; resetN <= 1; end // Generae vsync and href // 1t_LINE = (320 + 144) x pclk(84ns) initial begin $readmemh("testdata.txt", test_vector); #400; vsync <= 1; #400; vsync <= 0; #960; repeat (240) begin @(negedge pclk); href <= 1; #26880; // 320 pclk per line href <= 0; #1096; // 144 pclk end #2000; $display("Vsync"); #1000; vsync <= 1; #400; vsync <= 0; #960; repeat (240) begin @(negedge pclk); href <= 1; #26880; // 320 pclk per line href <= 0; #1096; // 144 pclk end #400; $finish; end // Generate clcok 12MHz camera pclk in => 84ns always begin pclk <= 1; if (href) begin test_data <= test_vector[count]; count <= count + 1; end #42; pclk <= 0; #42; end always begin clk25 <= 1; #20; clk25 <= 0; #20; // 25MHz VGA pixel clock end endmodule
試験用のダミーデータを読み込んで、2画面分の出力を行なっています。シミュレーション波形を見ると、hrefが有効になった後、2バイトのデーターを取り込んで、最初のweでBRAMのアドレス0x00000にデーターが書き込めています。タイミング的には動いている模様。
BRAMの設定は以下のように、Stand Alone/ Simple Dual Port RAMにしています。
BRAMのOperating ModeはWrite Firstを選択。BRAMへの書き込みと読み出しクロックは非同期ですが、書き込みと読み出しが競合する可能性がある場合は、Write Fisrtを推奨するとマニュアルに書いてあったのでそうしています。確かにテストベンチのシミュレーションでもcollisonの警告が出ていたりしますが、書き込みと読み出しの競合は避けられないので仕方ありません。
実機で動作させる
以下のようにPSコアとClock生成を組み込んだBlock Designを作成して実機で動作確認。OV7670の初期化はARMコアのソフト処理で行なっています。
またもや、画面が真っ黒、VSYNCとHSYNCは出ている、、
しばらく悩みましたが、VGA出力側のクロックをBRAMに配線し忘れていたという、超初歩的なミスが発覚。
配線を修正して動かしてみたら、やっと画像が表示されたが、こんな感じでえらく不鮮明。カメラの設定はちゃんとRGBにしているし(YUVとかになっているわけではない)。しばらく、カメラの設定が間違っていないかデーターシートとにらめっこをしながら確認しても、特におかしな箇所は見つからず。
しばらく悩んだのですが、カメラモジュールを壊したのではないかと結論づけて、2個目を購入。2個目が届いて入れ替えたら、やっと動きました!!
今回作ったVerilogコード
<カメラ画像取り込み>
/* This is a bit tricky href starts a pixel transfer that takes 3 cycles for first pixel then 2 cycles after 2nd pixcel Input | state after clock tick href | wr_hold data_in data_out we address address_next cycle -1 x | xx xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxx x xxxx xxx cycle 0 1 | 00 xxxxxxxxRRRRRGGG xxxxxxxxxxxxxxxx x xxxx addr cycle 1 1 | 01 RRRRRGGGGGBBBBB xxxxxxxxRRRRRGGG x addr addr cycle 2 1 | 10 GGGBBBBBxxxxxxxx RRRRRGGGGGGBBBBB 1 addr addr+1 */ module ov7670_camera( input wire pclk, input wire vsync, input wire href, input wire [7:0] data, output wire [16:0] bram_addr, output reg [15:0] data_out, output reg we ); reg [16:0] address; // address with one clcok cyle delayed reg [16:0] address_next; reg [15:0] data_in; reg [1:0] wr_hold; assign bram_addr = address; always @(posedge pclk) begin if (vsync) begin address <= 17'd0; address_next <= 17'd0; wr_hold <= 2'd0; end else begin data_out <= data_in; address <= address_next; we <= wr_hold[1]; wr_hold <= {wr_hold[0], (href & ~wr_hold[0])}; data_in <= {data_in[7:0], data}; if (wr_hold[1] == 1) begin address_next <= address_next + 1; end end end endmodule
<VGA画面表示>
module qvga_to_vga( input wire clk25, input wire resetN, output reg [4:0] vo_r_data, output reg [5:0] vo_g_data, output reg [4:0] vo_b_data, output wire vo_hsync, output wire vo_vsync, output reg [16:0] frame_addr, input wire [15:0] frame_pixel ); parameter hRez = 640, hStartSync = 640+16, hEndSync = 640+16+96, hMaxCount = 800; parameter vRez = 480, vStartSync = 480+10, vEndSync = 480+10+2, vMaxCount = 525; parameter hDispStart = 160, hDispEnd = 160+320; parameter vDispStart = 120, vDispEnd = 120+240; reg [9:0] hCounter; reg [9:0] vCounter; reg blank; assign vo_hsync = ((hCounter > hStartSync) && (hCounter <= hEndSync))? 0: 1; assign vo_vsync = ((vCounter >= vStartSync) && (vCounter < vEndSync))? 0: 1; always @(posedge clk25) begin if (resetN == 0) begin hCounter <= 10'd0; vCounter <= 10'd0; frame_addr <= 17'd0; blank <= 1; end else if (hCounter == (hMaxCount - 1) ) begin hCounter <= 10'd0; if (vCounter == (vMaxCount - 1)) begin vCounter <= 10'd0; end else begin vCounter <= vCounter + 1; end end else begin hCounter = hCounter + 1; end if (vCounter >= vDispEnd) begin frame_addr <= 0; end else if ( ((vCounter >= vDispStart) && (vCounter < vDispEnd)) && ((hCounter >= hDispStart) && (hCounter < hDispEnd)) ) begin blank <= 0; end else begin blank <= 1; end if (blank == 0) begin vo_r_data <= frame_pixel[15:11]; vo_g_data <= frame_pixel[10:5]; vo_b_data <= frame_pixel[4:0]; frame_addr <= frame_addr + 1; end else begin vo_r_data <= 5'd0; vo_g_data <= 6'd0; vo_b_data <= 5'd0; end end endmodule
ZYNQ PSを使ったカメラモジュールの初期化部分はコードが長いのでGitHubに置いておきました(初期化パラメーターはmbedの作例を流用)。
その他
実は、画像は表示できているのですが、画面が鏡に映った形で左右逆になるのと画面が倒立してしいます(aitendoのシルク印刷部分を上にすると画像が倒立する)。そのため、MVEPレジスタのMirror ImageをOn及びVFlipをOnにして反転を回避しています。この点はちょっと謎です。
カメラから読み出したデーターは、カメラのPCLK(Pixel Clock)をBRAMのクロックとして使っています。BRAMのWE信号もPCLKに同期して生成しているため、BRAMクロックの立ち上がりとWEの立ち上がりが同じタイミングになり、WEのSetup Timeマージンが取れていなのではないかと思い、BRAMのクロックを100MHzのFPGAクロックにしてみました(BRAMクロックの立ち上がりエッジでWEを確実に補足するため)。そうすると、逆に若干画像にノイズが乗るような現象が見えたので、BRAMの書き込み側はPCLKに戻しています。
Next Step
ZYBO + OV7670でI2Cを動かすのにもえらく時間がかかりましたが、最初AIX4で遠回りをしたことや、カメラモジュールの不調もありましたが、画像が出せるまで1ヶ月近くかかってしまいました。まだまだ、未熟で、先人の方には数週遅れで同じことをやっていますが、次はVGAのフル解像度データーをDDR3-SDRAMをVRAMとして取り込めるようにしたいと思っています。
その次は、HLSを使って画像処理などのフィルターを挿入できるようにしてみたいと思っています。