« 2016年9月 | トップページ | 2016年11月 »

2016年10月の記事

ZYBOでOV7670カメラモジュールの画像を表示する

前回のポストで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 Blcok Design

Test Benchを作る際は、Create HDL Wrapperが生成するBlock Designのインスタンスを雛形にして、試験信号生成の部分を追加記述しています。まず、以下の手順でWrapperファイルを生成。

Create HDL 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にデーターが書き込めています。タイミング的には動いている模様。

Simulation

BRAMの設定は以下のように、Stand Alone/ Simple Dual Port RAMにしています。

BRAM Setting-1

BRAMのOperating ModeはWrite Firstを選択。BRAMへの書き込みと読み出しクロックは非同期ですが、書き込みと読み出しが競合する可能性がある場合は、Write Fisrtを推奨するとマニュアルに書いてあったのでそうしています。確かにテストベンチのシミュレーションでもcollisonの警告が出ていたりしますが、書き込みと読み出しの競合は避けられないので仕方ありません。

BRAM Setting-2

実機で動作させる

以下のようにPSコアとClock生成を組み込んだBlock Designを作成して実機で動作確認。OV7670の初期化はARMコアのソフト処理で行なっています。

Final Desing

またもや、画面が真っ黒、VSYNCとHSYNCは出ている、、
しばらく悩みましたが、VGA出力側のクロックをBRAMに配線し忘れていたという、超初歩的なミスが発覚。

配線を修正して動かしてみたら、やっと画像が表示されたが、こんな感じでえらく不鮮明。カメラの設定はちゃんとRGBにしているし(YUVとかになっているわけではない)。しばらく、カメラの設定が間違っていないかデーターシートとにらめっこをしながら確認しても、特におかしな箇所は見つからず。 

Camera Image NG

しばらく悩んだのですが、カメラモジュールを壊したのではないかと結論づけて、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

2016/12/11追記:ピン配置の制約ファイルを追記します。

set_property PACKAGE_PIN V20 [get_ports iic_0_scl_io]
set_property PACKAGE_PIN W19 [get_ports iic_0_sda_io]
set_property PACKAGE_PIN W18 [get_ports xclk]
set_property PACKAGE_PIN T20 [get_ports href]
set_property PACKAGE_PIN U20 [get_ports pclk]
set_property PACKAGE_PIN Y19 [get_ports vsync]
set_property PACKAGE_PIN M14 [get_ports led0]
set_property PACKAGE_PIN V15 [get_ports {data[0]}]
set_property PACKAGE_PIN W15 [get_ports {data[2]}]
set_property PACKAGE_PIN T11 [get_ports {data[4]}]
set_property PACKAGE_PIN T10 [get_ports {data[6]}]
set_property PACKAGE_PIN W14 [get_ports {data[1]}]
set_property PACKAGE_PIN Y14 [get_ports {data[3]}]
set_property PACKAGE_PIN T12 [get_ports {data[5]}]
set_property PACKAGE_PIN U12 [get_ports {data[7]}]
set_property PACKAGE_PIN M19 [get_ports {vo_r_data[0]}]
set_property PACKAGE_PIN L20 [get_ports {vo_r_data[1]}]
set_property PACKAGE_PIN J20 [get_ports {vo_r_data[2]}]
set_property PACKAGE_PIN G20 [get_ports {vo_r_data[3]}]
set_property PACKAGE_PIN F19 [get_ports {vo_r_data[4]}]
set_property PACKAGE_PIN H18 [get_ports {vo_g_data[0]}]
set_property PACKAGE_PIN N20 [get_ports {vo_g_data[1]}]
set_property PACKAGE_PIN L19 [get_ports {vo_g_data[2]}]
set_property PACKAGE_PIN J19 [get_ports {vo_g_data[3]}]
set_property PACKAGE_PIN H20 [get_ports {vo_g_data[4]}]
set_property PACKAGE_PIN F20 [get_ports {vo_g_data[5]}]
set_property PACKAGE_PIN P20 [get_ports {vo_b_data[0]}]
set_property PACKAGE_PIN M20 [get_ports {vo_b_data[1]}]
set_property PACKAGE_PIN K19 [get_ports {vo_b_data[2]}]
set_property PACKAGE_PIN J18 [get_ports {vo_b_data[3]}]
set_property PACKAGE_PIN G19 [get_ports {vo_b_data[4]}]
set_property PACKAGE_PIN P19 [get_ports vo_hsync]
set_property PACKAGE_PIN R19 [get_ports vo_vsync]

set_property IOSTANDARD LVCMOS33 [get_ports iic_0_scl_io]
set_property IOSTANDARD LVCMOS33 [get_ports iic_0_sda_io]
set_property IOSTANDARD LVCMOS33 [get_ports xclk]
set_property IOSTANDARD LVCMOS33 [get_ports led0]
set_property IOSTANDARD LVCMOS33 [get_ports vsync]
set_property IOSTANDARD LVCMOS33 [get_ports href]
set_property IOSTANDARD LVCMOS33 [get_ports pclk]
set_property IOSTANDARD LVCMOS33 [get_ports {data[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports {data[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {data[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {data[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {data[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {data[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {data[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {data[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_b_data[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_b_data[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_b_data[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_b_data[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_b_data[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_g_data[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_g_data[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_g_data[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_g_data[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_g_data[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_g_data[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_r_data[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_r_data[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_r_data[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_r_data[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_r_data[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports vo_hsync]
set_property IOSTANDARD LVCMOS33 [get_ports vo_vsync]

set_property SLEW FAST [get_ports {vo_b_data[4]}]
set_property SLEW FAST [get_ports {vo_b_data[3]}]
set_property SLEW FAST [get_ports {vo_b_data[2]}]
set_property SLEW FAST [get_ports {vo_b_data[1]}]
set_property SLEW FAST [get_ports {vo_b_data[0]}]
set_property SLEW FAST [get_ports {vo_g_data[5]}]
set_property SLEW FAST [get_ports {vo_g_data[4]}]
set_property SLEW FAST [get_ports {vo_g_data[3]}]
set_property SLEW FAST [get_ports {vo_g_data[2]}]
set_property SLEW FAST [get_ports {vo_g_data[1]}]
set_property SLEW FAST [get_ports {vo_g_data[0]}]
set_property SLEW FAST [get_ports {vo_r_data[4]}]
set_property SLEW FAST [get_ports {vo_r_data[3]}]
set_property SLEW FAST [get_ports {vo_r_data[2]}]
set_property SLEW FAST [get_ports {vo_r_data[1]}]
set_property SLEW FAST [get_ports {vo_r_data[0]}]
set_property SLEW FAST [get_ports vo_hsync]
set_property SLEW FAST [get_ports vo_vsync]
set_property SLEW FAST [get_ports xclk]

set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets pclk_IBUF]
set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets pclk_IBUF_BUFG]

ZYNQ PSを使ったカメラモジュールの初期化部分はコードが長いのでGitHubに置いておきました(初期化パラメーターはmbedの作例を流用)。

ZYBO OV7670

その他

実は、画像は表示できているのですが、画面が鏡に映った形で左右逆になるのと画面が倒立してしいます(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を使って画像処理などのフィルターを挿入できるようにしてみたいと思っています。

参考資料

オシロ(RIGOL DS1054Z)を購入しました

最近FPGA(特にZYBO)をいじるようになって、ちょっとした信号波形の観測がしたいケースが増えてきました。そこで、とうとうオシロスコープを買ってしまいました。以前は日曜大工の電子工作にオシロスコープはもったいないと思って購入を躊躇していたのですが、いざ買って使ってみるとやはり便利です。

購入した機種はRIGOL DS1054Z(中国製)でAmazonから購入しました。同価格帯(5万円台)で買える、Tektronix TBS1052Bとどちらにしようかと少し悩んだのですが、4ch vs 2chでRIGOLの方がch数が多いこと、その他測定機能もRIGOLの方が充実しているように見えたのと、Amazonや他のBlogのレビューを見ても、RIGOLの評価は概ね良かったので、ブランド的にはTextronixですがコスパを取ってRIGOLにしました。

IMG 1175

オシロスコープは学生時代にアナログオシロをちょっと触ったことがある程度で、ディジタルオシロは初めてだったので、まだ使いこなせていませんが、個人用の電子工作用途にはこれで十分だと思います。やっぱり実際の信号波形が見えると動作確認の効率が飛躍的に上がるので、買って良かったと思っています。

マニュアル・表示ともに日本語にも対応しており、マニュアルの日本語が不自然なこともありません(ただ、英語版に比べて更新がやや遅いようですが)。DS1054ZはオプションでI2Cのプロトコル解析やデコードにも対応しているのですが(買った状態では使用時間限定で動かすことができます)、マニュアルを見ただけではさっぱり使いたかが分かりませんでした。Webで検索するとYou-Tubeに実際に操作している画像がアップされており、それを見て使い方が分かりました。英語が主体になりますが、サポート情報も比較的充実しているのでその点も良いと思います。

OV7670 25V PU

今の所気になる点は、ファンの動作音がややうるさいこと(You Tubeには自分でファンを静音型のもの交換している画像もありました)、画面表示をUSBメモリーに保存できるのですが本体にRTCがないらしく、ファイルのタイムスタンプが2015年の固定の日付になってしまい、母艦のMacにコピーした際に時間順にソートできないことです(コンソールから毎回touchコマンドでタイムスタンプを更新する必要があり面倒)。

とは言え、機能的には十分満足しています。まだまだ使いこなせていないので、色々触ってみたいと思っています。

« 2016年9月 | トップページ | 2016年11月 »

2017年2月
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28        
無料ブログはココログ