前回行った、OV7670カメラモジュールを使ったQVGA画像の表示を拡張して、VGA画像を表示できるようにしてみました。まだ不完全な部分がありますが、やったことを書いてみます。
全体構想
VGA(640 x 480) x 16bitカラーの画像を扱うためには、600KBのVRAM容量が必要となり、QVGAのようにFPGAのBRAMには格納できないため、ZYBOのPS側に搭載されているDDR3 RAMをVRAMとして使用する必要があります。今回はPL(FPGA部)からDDR3 RAMへのアクセスを行うことがテーマでした。
やり方は以下の3通りが考えられますが、③のAXI-HPを使ったメモリアクセスを使いました。
- AXIGPポート経由でアクセス(32bit幅)
- ZYNQ DMAエンジンを使ってDMA転送
- AXIHPポート経由でアクセス(32/64bit幅)
AXIHPポートを使ったDDR3メモリアクセスのイメージを下記に示します。データー転送側がAXI Masterデバイスとなります。
DDR3アクセスのために、AXI Masterプロトコルを喋るモジュールを作る必要がありますが、HLSで作るのが一番楽だと考え、以下のような構成としました。(インタフェースポートの属性にm_axiを指定するだけであとはHLSがAXI Masterの処理を行なってくれるため)
Cameraからのデータ受信モジュールとVGA IFはクロックに従ったデーターの送受信が必要なため、HLSではなくVerilogで作成しています(前回作ったモジュールを拡張)。Verilog - HLS間のインタフェースはAXIS(Stream)を使うことも考えたのですが、どのタイミングでAXISのデーターの切れ目(tvalid)をHLSに伝えればよいかがよく分からなかったため、ピクセル座標をap_hs(ハンドシェーク)で渡すインタフェースとしました。当初はap_hsもそれほど苦労せずに作れるだろうと思ったのですが、ap_hsを動かすのに思いっきりハマりました。
今回の構成では、カメラからのデータの取り込みはOV7670のPCLK(24MHz)に同期する必要がありますが、メモリアクセス部分はFPGA PLに供給されている100MHzのシステムクロック(FCLK)で動作するため、クロックドメインまたがり(Clock Domain Crossing: CDC)の問題に直面し、このせいで(だと思うのですが)当初Cameraデーター受信モジュールが期待通りに動いてくれずかなり悩みました。あまりスマートでないように思うのですが、カメラ側の24MHzクロックをFCLKに同期化することで対処しています。
各モジュールのコード
HLSで書いたMemory Write / Memory Readジュールのコードは以下の通りです。
<Memory Write>
#include <ap_int.h> #include <stdint.h> #define MAXLINE 480 #define MAXCOL 640 void MemWrite(ap_uint line, ap_uint col, uint16_t data, uint64_t *VRAM) { // depth 1280byte of lineBuff size is too big (got SIGSEGV fault in C/RTL co-sim), // but 160 words(64bit) is too small #pragma HLS INTERFACE m_axi depth=640 port=VRAM offset=direct bundle=VRAMW #pragma HLS INTERFACE ap_hs port=col #pragma HLS INTERFACE ap_hs port=line #pragma HLS INTERFACE ap_hs port=data #pragma HLS INTERFACE ap_ctrl_hs port=return static uint64_t lineBuff[2][MAXCOL/4]; // Force to make 2 line buffers in different BRAM #pragma HLS ARRAY_PARTITION variable=lineBuff factor=2 dim=1 #pragma HLS RESOURCE variable=lineBuff core=RAM_1P_BRAM static uint64_t strData = 0; static ap_uint<2> mod = 0; ap_uint<1> cur; uint64_t inData; // Data reception block cur = line & 0x0001; inData = data; switch(mod) { case 0: strData = data; break; case 1: strData = strData | inData << 16; break; case 2: strData = strData | inData << 32; break; case 3: strData = strData | inData << 48; lineBuff[cur][col/4] = strData; break; } mod++; if (col == (MAXCOL - 1)) { // Data transfer block uint64_t* destPtr = &VRAM[line*MAXCOL/4]; memcpy(destPtr, lineBuff[cur], MAXCOL*2); mod = 0; return; } return; }
<Memory Read>
#include <ap_int.h> #include <stdint.h> #define MAXLINE 480 #define MAXCOL 640 uint16_t MemRead(ap_uint vga_line, ap_uint vga_col, uint64_t* VRAM) { #pragma HLS INTERFACE m_axi depth=640 port=VRAM offset=direct bundle=VRAMR #pragma HLS INTERFACE ap_hs port=vga_line #pragma HLS INTERFACE ap_hs port=vga_col #pragma HLS INTERFACE ap_ctrl_hs port=return static uint64_t lineBuff[2][MAXCOL/4]; // Force to make 2 line buffers in different BRAM #pragma HLS ARRAY_PARTITION variable=lineBuff factor=2 dim=1 #pragma HLS RESOURCE variable=lineBuff core=RAM_1P_BRAM static uint64_t rdData = 0; static ap_uint<2> mod = 0; ap_uint<1> cur; uint16_t pixcelData; cur = vga_line & 0x0001; // Transfer data during blanking period if (vga_col == 0) { uint64_t* srcPtr = &VRAM[vga_line*MAXCOL/4]; memcpy(lineBuff[cur], srcPtr, MAXCOL*2); mod = 0; } switch(mod) { case 0: rdData = lineBuff[cur][vga_col/4]; pixcelData = (uint16_t)rdData; break; case 1: pixcelData = (uint16_t)(rdData >> 16); break; case 2: pixcelData = (uint16_t)(rdData >> 32); break; case 3: pixcelData = (uint16_t)(rdData >> 48); break; } mod++; return pixcelData; }
内容的には単純な処理で、MemoryWriteはCameraモジュールからCol/Lineの座標データと16bitカラーのデーターを受信し、4ピクセル分のデータを64bit長の配列に格納します。1 Line分(640 pixel)のデータを受信すると水平同期のブランキング期間を使ってデータをDDR3メモリーにバースト転送します。データー転送の時間を短くするためAXIバスは64bit長で動かしています(そのため、ピクセルデーターを64bit単位にパッキングしています)。VRAM portに64bit符号なし整数に対するポインターを(uint64_t *)指定して、インタフェースモードにm_axiを指定すると64bitモードで動いてくれました。
VRAMメモリのオフセット値はdirectを使ってBlock Designのconstantで指定しています(今回は、80_0000Hを使用)。
MemoryReadも同様で、水平同期のブランキング期間を最初に持ってきており、1 Line分のデータをブランキング期間にDDRメモリから読み込んで、VGA IFが要求するpixcel座標に対応したデーターを渡すようにしています。
HLSの合成結果を以下に示します。MemWriteは最小2クロックサイクルで動作し、DDRメモリーにデーター転送を行う際は169クロックかかることが分かります。MemReadは同様な処理を行なっているのですがLatencyが1クロックサイクル多く、1処理に3クロックサイクルを要しています。
次に、OV7670カメラモジュールIF、VGA IFのVerilogコードを示します。以前のHDLコードは参考にしたブログのコードそのままなのですが、複数のレジスターを一つのalwaysブロックの中で操作していました。参考書として参照した「FPGAボードで学ぶ組込みシステム開発入門[Altera編]」ではレジスタ毎に独立したalwaysブロックを使うスタイルの記述になっていたため、参考書のスタイルに改めました。生成される回路は異なると思いますが、alwaysブロックで複数レジスタを操作しないのがベストプラクティスな書き方なのかがまだよく分かっていません。
<OV7670カメラモジュールIF>
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Company: // Engineer: Kenshi Kamiya // // Create Date: 2016/10/11 15:00:13 // Design Name: ov7670_camera // Module Name: ov7670_camera // Project Name: ov7670 camera data capture and output data wht ap_hs // Target Devices: Zybo // Tool Versions: Vivado 2016.3 // // Revision: 01 // Revision 0.01 - File Created // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// /* 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 col col_next cycle -1 x | xx xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxx x xxxx xxx cycle 0 1 | 00 xxxxxxxxRRRRRGGG xxxxxxxxxxxxxxxx x xxxx col cycle 1 0 | 01 RRRRRGGGGGBBBBB xxxxxxxxRRRRRGGG x col col cycle 2 x | 10 GGGBBBBBxxxxxxxx RRRRRGGGGGGBBBBB 1 col col+1 */ module ov7670_camera_hs( input wire clk, input wire resetN, input wire pclk, input wire vsync, input wire href, input wire [7:0] data, output reg [9:0] line, output reg [9:0] col, output reg [15:0] data_out, output wire ap_start, input wire ap_idle, input wire ap_done, output wire col_vld, output wire line_vld, output wire data_vld, input wire col_ack, input wire line_ack, input wire data_ack ); reg [9:0] col_next; reg [9:0] line_next; reg [7:0] data_s; reg [15:0] data_in; reg [1:0] wr_hold; reg we; reg [2:0] state; reg [3:0] control; // Registers for synchronizer reg pclk_d, pclk_s; reg href_d, href_s; parameter IDLE=0, AP_START=1, OUT_VLD=2, OUT_ACK=3, AP_DONE=4, WAITE_NEXT=5; parameter MAXCOL=640, MAXLINE=480; // Synchronizer for pclk always @(posedge clk) begin pclk_d <= pclk; pclk_s <= pclk_d; end // Synchronizer for href always @(posedge clk) begin href_d <= href; href_s <= href_d; end // Synchronizer for input data always @(posedge clk) begin data_s <= data; end // FSM for ap_hs protocol always @(posedge clk) if (resetN == 0) state <= IDLE; else begin case(state) IDLE: begin if (we == 1) begin state <= AP_START; end else begin state <= IDLE; end end AP_START: begin state <= OUT_VLD; end OUT_VLD: begin state <= OUT_ACK; end OUT_ACK: begin // ap_ack retun immediatry, so no chcke ap_ack // To catch when ap_done return one clclk after ap_ack if (ap_done == 1) begin state <= WAITE_NEXT; end else begin state <= AP_DONE; end end AP_DONE: begin if (ap_done == 1) begin state <= WAITE_NEXT; end else begin state <= AP_DONE; end end WAITE_NEXT: begin // Wait to end current WE cycle if(we == 1) begin state <= WAITE_NEXT; end else begin state <= IDLE; end end default: begin state <= IDLE; end endcase end // Generate control signal always @(*) case(state) IDLE: control = 4'b0000; AP_START: control = 4'b1000; OUT_VLD: control = 4'b1111; OUT_ACK: control = 4'b0000; AP_DONE: control = 4'b0000; WAITE_NEXT: control = 4'b0000; default: control = 4'b0000; endcase assign {ap_start, col_vld, line_vld, data_vld} = control; // Camera data capture FSM always @(posedge pclk_s) begin if (vsync) wr_hold <= 2'd0; else begin we <= wr_hold[1]; wr_hold <= {wr_hold[0], (href_s & ~wr_hold[0])}; data_out <= data_in; data_in <= {data_in[7:0], data_s}; end end always @(posedge pclk_s) begin if (vsync) begin col <= 10'd0; col_next <= 10'd0; end else begin col <= col_next; if (wr_hold[1] == 1) begin col_next <= col_next + 1; if (col_next == MAXCOL - 1) col_next <= 10'd0; end end end always @(posedge pclk_s) begin if (vsync) begin line <= 10'd0; line_next<= 10'd0; end else begin line <= line_next; if (wr_hold[1] == 1 && col_next == MAXCOL - 1) begin line_next <= line_next + 1; if (line_next == MAXLINE - 1) line_next <= 10'd0; end end end endmodule
<VGA IF>
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Company: // Engineer: Kenshi Kamiya // // Create Date: 2016/11/06 16:33:59 // Design Name: VGA Driver with ap_hs protocol // Module Name: vga_hs // Project Name: OV7670_VGA // Target Devices: Zybo // Tool Versions: Vivado 2016.3 // Description: // // Dependencies: // // Revision: // Revision 0.01 - File Created // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// module vga_hs( input wire clk, 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 reg vo_hsync, output reg vo_vsync, output reg [9:0] vga_line, output reg [9:0] vga_col, input wire [15:0] frame_pixel, output wire ap_start, input wire ap_idle, input wire ap_done, output wire line_vld, output wire col_vld, input wire line_ack, input wire col_ack ); parameter hRez = 640, hFrontPorch = 16, hSyncPluse = 96, hBackPorch = 48, hMaxCount = 800; parameter vRez = 480, vFrontPorch = 10, vSyncPluse = 2, vBackPorch = 33, vMaxCount = 525; parameter hDispStart = hFrontPorch + hSyncPluse + hBackPorch; // 160 parameter vDispStart = vFrontPorch + vSyncPluse + vBackPorch; // 45 parameter IDLE=0, AP_START=1, OUT_VLD=2, AP_DONE=3, WAITE=4; reg [9:0] hCounter; reg [9:0] vCounter; reg [9:0] cur_hCounter; reg blank; reg [2:0] state; reg [15:0] pixelData; reg [2:0] control; wire cke; assign cke = ~blank & clk25; // hCounter always @(posedge clk25) begin if (resetN == 0) hCounter <= 10'd0; else if (hCounter == (hMaxCount - 1)) hCounter <= 10'd0; else hCounter <= hCounter + 1; end //vCounter always @(posedge clk25) begin if (resetN == 0) vCounter <= 10'd0; else if (hCounter == (hMaxCount - 1)) if (vCounter == (vMaxCount - 1)) vCounter <= 10'd0; else vCounter <= vCounter + 1; end // FSM for ap_hs protocol always @(posedge clk) if (resetN == 0) state <= 0; else case(state) IDLE: begin // State 0 if ( (cke && vga_col >= 1) || (vCounter >= vDispStart && hCounter == 11'd10)) state = AP_START; else state = IDLE; end AP_START: begin // State 1 state = OUT_VLD; end OUT_VLD: begin // State 2 state = AP_DONE; end AP_DONE: begin // State 3 // ap_ack retun immediately, so no chcke ap_ack if (ap_done && cke) state <= AP_START; // Sthort cut to execute next cycle else if (ap_done ) state = IDLE; else if (ap_done && hCounter < hDispStart) state = WAITE; else state= AP_DONE; end WAITE: begin // State 4 // Waite to finish hBlanking in case read first block at line start if (hCounter < hDispStart) state = WAITE; else state = IDLE; end default: begin state = IDLE; end endcase always @(*) case(state) IDLE: control = 3'b000; AP_START: control = 3'b100; OUT_VLD: control = 3'b111; AP_DONE: control = 3'b000; WAITE: control = 3'b000; default control = 4'b000; endcase assign {ap_start, line_vld, col_vld} = control; // Generate HSYNC always @(posedge clk25) begin if (resetN == 0) vo_hsync <= 1; else if ((hCounter > hFrontPorch) && (hCounter <= hFrontPorch + hSyncPluse)) vo_hsync <= 0; else vo_hsync <= 1; end // Generate VSYNC always @(posedge clk25) begin if (resetN == 0) vo_vsync <= 1; else if ((vCounter > vFrontPorch) && (vCounter <= vFrontPorch + vSyncPluse)) vo_vsync <= 0; else vo_vsync <= 1; end // Generate blank signal always @(posedge clk25) begin if (resetN == 0) blank <= 1; else if ((vCounter >= vDispStart) && (vCounter < vMaxCount ) && (hCounter >= hDispStart) && (hCounter < hMaxCount) ) blank <= 0; else blank <= 1; end // VGA column address always @(posedge clk25) begin if (resetN == 0) vga_col <= 10'd0; else if (blank == 0) if (vga_col == hRez - 1) vga_col <= 10'd0; else vga_col <= vga_col + 1; end // VGA line address always @(posedge clk25) begin if (resetN == 0) vga_line <= 10'd0; else if (blank == 0) if (vga_col == (hRez - 1)) if (vga_line == (vRez -1)) vga_line <= 10'd0; else vga_line <= vga_line +1; end // Generate color signal always @(posedge clk25) begin if (blank == 0) begin vo_r_data <= pixelData[15:11]; vo_g_data <= pixelData[10:5]; vo_b_data <= pixelData[4:0]; end else begin vo_r_data <= 5'd0; vo_g_data <= 6'd0; vo_b_data <= 5'd0; end end always @(posedge ap_done) begin pixelData <= frame_pixel; end endmodule
動作確認(シミュレーション)
Verilogで作成したCamera IFとHLSで作成したMemWriteが正しく動作するかの検証を行いました。特にap_hsプロトコルが期待通りに動くかの検証が必要でした。ただ、ov7670_camera_hsとMemWriteをBlock Designで繋いだだけではダメで、MemWriteをAXI-Slaveに繋いでやらないと動作してくれません。AXIのBFMが有償なのでシミュレーションができないとTwitterでぼやいていたら、@marsee101 さんからご自身で作成されたAXI Slave BFMのありかを教えていたき、そのBFMを使って以下のようなシミュレーション環境を作りました。
axi_slave_bfmが @marsee さんから拝借したAXI-SlaveのBFMです。おかげで、VerilogとHLSモジュールの連携をシミュレーションすることができて大変助かりました。Block DesignでMemWrite (HLS) とaxi_bfm_slaveを作成してauto connectを実行すると、自動的にAXI-InterconnectやProsessor System Resetモジュールもインスタンス化され接続してくれました。
当初のコードでは、ov7670_camera_hsの164行目に相当する、OV7670からのデーター受信処理で、OV7670からのpclkをそのまま使ってpclkに同期したwe信号(ov7670_camera_hs 170行目)がHighになることをトリガーにして、ap_hsをスタートして(ov7670_camera_hs 94行目)MemWrite (HLS)にデーター転送を行おうとしました。このコードでは、シミュレーションではBehavioral Simulation、Post-Synthesis Simulation共に正常に動作しているように見えたのですが、実機ではどうしてもweがHighになった際にap_startパルスが出ず、MemWriteを起動できない問題に遭遇しました。
この問題の解決にかなり時間を要したのですが、we信号はpclkに同期して生成しているが、ap_hsプロトコルはpclkとは非同期の100MHz FCLKに同期して動かしているため、クロックドメインの違いによって問題が発生しているのではないかと考え、pclkを67行目に示すシンクロナイザーを通してFCLKに同期化してやると正常に動作するようになりました。
クロックドメインまたがり(CDC: Clock Domain Crossing)の処理として、入力クロックをシステムクロックで打ち直すのが正しい処理なのか自信がないのですが、まずは動いているのでよしとしています。Post-Synthesis Timing Simulationを行なった際の波形を以下に示します。
href信号が有効になると画像データーの取り込み処理を始めて、weがHighになった次のシステムクロック(clk)の立ち上がりエッジでap_startを出し、3クロックサイクル後にap_doneが返っています。ap_startの信号を組み合わせ回路で生成しているため、信号にグリッチが出ています。レジスターを通すことでグリッチをなくすことができるのですが、ap_startの立ち上がりが1クロック遅延します。Camera IFではタイミングに余裕があるためレジスターを入れてもよいのですが、後で説明するVGA IF回路ではタイミングの余裕がなく、制御信号生成にレジスターを入れることができなかったため、処理ロジックを共通化する意味で、信号のグリッチには目をつぶっています(動作には問題なさそうですので)。
また、これまでFSMを書く際は、状態遷移(next_stateに遷移する条件)を組み合わせ回路(always @*)で記述して、状態を進める処理を以下のような順序回路で記述していました:
always @(posedge clk) begin if (resetN == 0) cur_state <= IDLE; else cur_state <= next_state; end
このような組み合わせ回路と状態を進める順序回路の組み合わせでFSMを記述すると、weを検出した際のap_start起動に1クロックの遅延が発生します。VGA IF回路ではap_startの遅延を最小限にしたかったため、今回は状態遷移自体を順序回路のスタイルで記述しています。
640 column分のデータを受信すると(colアドレス = 0x27Fになると)MemWrite (HLS)がDDRメモリーにデーター転送処理を始めるためap_doneが直ぐに返らずに、169クロック後にap_doneが返ってきます。
VGA IFに関しても同様の試験環境を作ってシミュレーションを行なっています。
OV7670 Camera IFの場合は24MHzサイクルの2クロック毎にMemWriteモジュールにデーターを転送できればよいため比較的タイミングに余裕があったのですが、VGA IFでは25MHzのVGA pixcelクロック毎にMemReadモジュールから画像データーをもらう必要があるため、タイミングマージンがギリギリになりました。以下にPost-Synthesis Timing Simulationを行なった際の波形を示します。VGA IFではvga_hsの59行目でvga pixcelクロック(25MHz)と有効画素範囲(ブランキング期間以外)のANDを取ってこのcke信号がHighになることをトリガーにしてMemReadモジュールに対してap_startを出しています。
ap_startを最短で検出できるよう、VGA clockをMMCMで生成する際に-30度位相シフトして、VGA clk(clk25)がHighになった直後のシステムクロック(clk)の立ち上がりでap_startが出せるようにしています。シミュレーションでは次のVGAクロックサイクルが始まる直前ギリギリにap_doneが返っていますが、やはりpixelデーター毎にハンドシェークを行うap_hsを使うのはタイミング的に厳しいので、このようなケースではAXIS(ストリーミング)を使った方がよいように思われます。
全体の構成
全モジュールを結合した最終的なBlock Designを以下に示します。
Cameraモジュールでap_startが出ない問題にかなり悩まされましたが、なんとか動くようになりました。
残課題
以下の課題が残っており、まだ完全とは言えません。
1) 画像の左側にノイズが出る
OV7670カメラの水平方向の有効画素範囲の設定の問題だと思うのですが、該当レジスタ値(HSTART, HSTOP, HREF)を変更して表示ウインドウを右にシフトさせようとしたのですが、デフォルト値以外の値に設定すると、href信号の出力パターン(パルス幅)が大きく変わってしまい画像が取り込めなくなるため、現状未解決です。
2) タイミング制約の問題
ov7670_camera_hsモジュールで、システムクロックに同期化したpclk_sを使って生成している信号やレジスタが、合成後unconstrainedになっています。unconstrainedの警告を消すために、試しにpclk-sにクロック制約を入れると、今度は合成時にhold/setupマージン不足(negative slack)の状態になるクロックパスが多数発生してしまうため、クロック制約は外しています。タイミング制約の使い方やタイミング条件の正しい設計はもっと勉強をしないといかんです。
クロック制約:create_clock -period 41.666 -name pclk_s -waveform {0.000 20.833} [get_pins ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s_reg/Q]
3) Unknown 1-bit CDC circuitry
クロック載せ替えで問題が出ていたので、Report CDCを行うと、1-bit unknown CDC circuitryのパスが多数検出されます。
Unsafeではないのですが、1-bit unknown CDCが出ている回路を表示してみると、以下のように、システムクロックに同期したリセット信号と、同じくシステムクロックに同期したVGAクロックが同じFFに入力されている2つの信号の関係性で、unknown 1-bit CDCが発生しているように見えます。試しに、destination側のレジスタに”ASYNC_REG”プロパティーを設定したりしたのですが変化がありませんでした。クロック載せ替えの最適化もまだよく分かっていない部分です。
参考資料
- AXI4 Slave Bus Functional Model のVerilog HDL版4
- FPGAマガジンNo.143章「初めての高位合成ツール Vivado HLSの使い方」
- FPGAボードで学ぶ組込みシステム開発入門[Altera編] 7-2章「VGA表示回路」