カテゴリー「ZYBO」の記事

Xilinx ZYNQ FPGAボード

Vivado Constraints Wizardによるクロック・入出力制約の作成

前回のポストで、ZYBOを使って、OV7670カメラのVGA画像が取り込めるようになりましたが、制約条件が未設定でした。VivadoのConstraints Wizardを使うことによってクロックと入出力の制約条件を設定して、Unconstraintsの箇所を消すことができました。制約条件を設定することによって画質が向上したため、回路を正しく動作させるためには制約条件の設定は必須なんだと分かりました。

以下に、Constraints Wizardを使った制約条件の設定方法を記載します。

操作手順

Constraints WizardはVivadoのFlow Navigatorから起動でき、SynthesisとImplementationの両方から起動できます。一旦合成・配置を行なった後だと、どちらから起動しても作成される制約ファイルの内容は同じになるようです。

Constraints Wizard

Wizardを起動します。カメラのPCLKをFCLKで同期化したpclk_sと、HLSとのインタフェースに使っているap_doneが、Generated Clock(ユーザー定義のクロック)の候補として表示されました。ソースクロック(FCLK)の分周率を指定する形式となっており、実際のクロック周波数とは同じになりませんが、ここでは4を指定しました。pclk_sは24MHzなので、100MHz ÷ 4がそれに近い値ということで。

当初はpclk_sの制約条件を”create_clock”コマンドで作成しようとしたのですが、create_clockはプライマリークロックの定義となっているため、create_clockを使うとImplematation時にタイミング違反が発生してしまいます。このようなケースでは、”create_generated_clock”を使うのが正しいのだということが分かりました。

Constraints Wizard-2

Constraints Wizard-3

次に入力遅延を設定します。入力遅延はリファレンスクロック(ここではFCLK)の立ち上がりに対する遅延ということなので、これも実態(PCLKの立ち上がりに対して、実際のデーターはセットアップ時間前に有効になっている)とは異なりますが、1〜2 nsの遅延を指定。

Constraints Wizard input delay

続けて、出力遅延を設定。こちらは、25MHzのVGAクロックの立ち上がりに対するセットアップ・ホールド遅延の設定になります。

Constraints Wizard output delay

次に、非同期クロックドメインを指定する画面が表示されます。下段に3つのエントリが表示されていますが、Non-recommendedということなのでチェックはしていません。この項目にチェックを入れると、"set_clock_groups -asynchronous “の制約が生成されるのですが、オブジェクトに指定されるクロック名が認識できない旨のCritical Waringが合成時に出てしまいました。

20161211 Constraints Wizard Async Clock

最後にWizardを完了。

Constraints Wizard done

以下のエントリが制約ファイルに追加されました。 

create_generated_clock -name ov7670_vram_sys_i/MemRead_0/inst/ap_done -source [get_pins {ov7670_vram_sys_i/processing_system7_0/inst/PS7_i/FCLKCLK[0]}] -divide_by 4 [get_pins {ov7670_vram_sys_i/MemRead_0/inst/ap_CS_fsm_reg[10]/Q}]
create_generated_clock -name ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s -source [get_pins {ov7670_vram_sys_i/processing_system7_0/inst/PS7_i/FCLKCLK[0]}] -divide_by 4 [get_pins ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s_reg/Q]
create_clock -period 40.000 -name VIRTUAL_ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s -waveform {0.000 20.000}
create_clock -period 40.000 -name VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0 -waveform {-3.333 16.667}
set_input_delay -clock [get_clocks clk_fpga_0] -min -add_delay 2.000 [get_ports {data[*]}]
set_input_delay -clock [get_clocks clk_fpga_0] -max -add_delay 3.000 [get_ports {data[*]}]
set_input_delay -clock [get_clocks clk_fpga_0] -min -add_delay 2.000 [get_ports href]
set_input_delay -clock [get_clocks clk_fpga_0] -max -add_delay 3.000 [get_ports href]
set_input_delay -clock [get_clocks clk_fpga_0] -min -add_delay 2.000 [get_ports pclk]
set_input_delay -clock [get_clocks clk_fpga_0] -max -add_delay 3.000 [get_ports pclk]
set_input_delay -clock [get_clocks VIRTUAL_ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s] -min -add_delay 2.000 [get_ports vsync]
set_input_delay -clock [get_clocks VIRTUAL_ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s] -max -add_delay 3.000 [get_ports vsync]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -min -add_delay -1.000 [get_ports {vo_b_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -max -add_delay 3.000 [get_ports {vo_b_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -min -add_delay -1.000 [get_ports {vo_g_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -max -add_delay 3.000 [get_ports {vo_g_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -min -add_delay -1.000 [get_ports {vo_r_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -max -add_delay 3.000 [get_ports {vo_r_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -min -add_delay -1.000 [get_ports vo_hsync]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -max -add_delay 3.000 [get_ports vo_hsync]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -min -add_delay -1.000 [get_ports vo_vsync]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -max -add_delay 3.000 [get_ports vo_vsync]

合成・インプリメンテーションを実行すると、Unconstainted Pathがなくなりました。

Timing Summary

Clock Interactionを表示するとまだUnsafeなクロック乗せ変えパスが表示されています。WizardのAsynchronous CDCをチェックして赤がついているクロック間を非同期にするとUnsafeの表示を消すことができるのですが、先に示したように、Wizardが生成する制約エントリがうまく認識されないのでこのままとしています。 

Constraints Wizard clock interaction

制約条件を設定することによって、タイミング条件はかなり改善されたようです。入力側の信号がFPGAのFCLKに同期しているわけではないため(実態はカメラ側のPCLKに同期)、本当にこれでよいのかはまだよくわかっていないのですが。

結果

制約条件を加えて作成したBitstreamを使ってカメラを立ち上げると、以前に比べて画質がかなり向上しました。制約条件を入れる前は画像にジャギーが目立っていたのですが、制約条件を設定した後はジャギーがなくなりました。

<制約条件設定前の画像>

Constraints Wizard before

<制約条件設定後の画像>

Constraints Wizard after

制約条件が未設定の状態では回路が正しく動いていなかったことが分かりました。FPGAを使った回路の設計では、制約条件の設定やタイミング・クロージャーは必須事項なのだとよく分かりました。

参考資料

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

前回行った、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を使ったメモリアクセスを使いました。

  1. AXIGPポート経由でアクセス(32bit幅)
  2. ZYNQ DMAエンジンを使ってDMA転送
  3. AXIHPポート経由でアクセス(32/64bit幅)

AXIHPポートを使ったDDR3メモリアクセスのイメージを下記に示します。データー転送側がAXI Masterデバイスとなります。

AXIHP

DDR3アクセスのために、AXI Masterプロトコルを喋るモジュールを作る必要がありますが、HLSで作るのが一番楽だと考え、以下のような構成としました。(インタフェースポートの属性にm_axiを指定するだけであとはHLSがAXI Masterの処理を行なってくれるため)

System_Design

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クロックサイクルを要しています。

MemWrite

MemRead

次に、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を使って以下のようなシミュレーション環境を作りました。

MemWrite_TsetBed

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を行なった際の波形を以下に示します。

MemWrite Simulation-1

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が返ってきます。

MemWrite Simulation-2

MemWrite Simulation 3

VGA IFに関しても同様の試験環境を作ってシミュレーションを行なっています。

MemRead TestBed

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を出しています。

MemRead Simulation

ap_startを最短で検出できるよう、VGA clockをMMCMで生成する際に-30度位相シフトして、VGA clk(clk25)がHighになった直後のシステムクロック(clk)の立ち上がりでap_startが出せるようにしています。シミュレーションでは次のVGAクロックサイクルが始まる直前ギリギリにap_doneが返っていますが、やはりpixelデーター毎にハンドシェークを行うap_hsを使うのはタイミング的に厳しいので、このようなケースではAXIS(ストリーミング)を使った方がよいように思われます。

全体の構成

全モジュールを結合した最終的なBlock Designを以下に示します。

System Block Design

Cameraモジュールでap_startが出ない問題にかなり悩まされましたが、なんとか動くようになりました。

残課題

以下の課題が残っており、まだ完全とは言えません。

1) 画像の左側にノイズが出る

OV7670カメラの水平方向の有効画素範囲の設定の問題だと思うのですが、該当レジスタ値(HSTART, HSTOP, HREF)を変更して表示ウインドウを右にシフトさせようとしたのですが、デフォルト値以外の値に設定すると、href信号の出力パターン(パルス幅)が大きく変わってしまい画像が取り込めなくなるため、現状未解決です。

Lift side noize

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]

Unconstrained

3) Unknown 1-bit CDC  circuitry

クロック載せ替えで問題が出ていたので、Report CDCを行うと、1-bit unknown CDC circuitryのパスが多数検出されます。

Unkonown 1bit CDC

Unsafeではないのですが、1-bit unknown CDCが出ている回路を表示してみると、以下のように、システムクロックに同期したリセット信号と、同じくシステムクロックに同期したVGAクロックが同じFFに入力されている2つの信号の関係性で、unknown 1-bit CDCが発生しているように見えます。試しに、destination側のレジスタに”ASYNC_REG”プロパティーを設定したりしたのですが変化がありませんでした。クロック載せ替えの最適化もまだよく分かっていない部分です。

Unkonown CDC 2

参考資料

Vivado Integrated Logic Analyzer(ILA)の使い方

ちょっと小ネタですが、VivadoのIntegrated Logic Analyzer(ILA)を使ったFPGA内の信号観測の方法について記載します。

ILAの使い方は、以下のWebにも詳しい使い方が書いてあるのですが、最新版のVivadoではもっと簡単に設定ができることが分かったのでその内容を記載します(執筆時点ではVivado 2016.3で動作確認しています)。

1. Debug対象信号の指定

マウスの右クリックからMark Debugを選択します。選択した信号にDebugマークがつきます。信号を選択し終わったら、Run connection automationでILAを接続します。以下のように、Debugマークがついた信号がILAに接続されます。

Block Design with ILA

2. 論理合成

Mark DebugをつけたBlock Designで”Run Synthesis”を実行

3. Implementation実行とBitstream生成

通常の手順で”Run Implementation”と”Generate Bitstream”を実行します。

4. デバッグ画面の表示

Hardware Managerを開き、”Open target”をクリック。

Hardware Mnager

Auto Connectをクリック。ターゲットが認識されるので、”Program device”でbitstreamをダウンロードします

Bitstream download ˇ

ZynqデバイスのFCLK_CLK0など、PSからのクロックをロジックアナライザに使用している場合やPSのプログラムで周辺デバイスの初期化などを行なっている場合はは、Xilinx SDKでプログラムを実行してPSを起動します。

その後、”Refresh device”を実行します。デバッグ画面が表示されます。 Waveformウインドウにデバッグ対象にした信号の一覧が表示されています。

Debug Screen

5. デバッグ

トリガーにしたい信号をWaveformウインドウからTrigger Setupウインドウにドラッグアンドドロップし、トリガー条件を設定します。

Trigger Setup

”Run Trigger for this ILA core”ボタンを押すとトリガー条件を検出時に信号が波形表示されます。

Wave From Display

以前のように制約ファイル(xdcファイル)にデバッグ用のエントリを追加する必要もありません。デバッグが終わったらデバッグ対象の信号を右クリックして”Clear Debug”を実行し、ILAを削除すれば終わりです。

非常にお手軽に信号観測ができます。

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を使って画像処理などのフィルターを挿入できるようにしてみたいと思っています。

参考資料

ZYBOのPSでI2Cを動かしてみた

ZYBOのPS(ARM Core部分)のI2Cを動かしてみました。本当は、OV7670カメラモジュールを使って画像の取り込みをやってみたかったのですが、Amazonで買ったOV7670モジュールがどうも不良品のようで、SCCB(I2Cのサブセットのカメラモジュール制御プロトコル)を使ってカメラモジュールとどうしても通信ができず、その過程で分かったIC2の使い方を書いています。

カメラモージュールの実験は、代替え品をaitendoさんに注文したので、商品が届いたら出直しです。Amazonのもaitendoさんのもカメラモジュール自体は同じものを使っていると思いますが、回路構成が若干異なり、aitendoの製品の方が使いやすと思います。理由は、Amazonで販売しているモジュールはパワーオンリセットやI2C信号線のプルアップがないためです。ペリフェラル側にプルアップがあると、ブレッドボードなどを経由してプルアップの配線をする必要がないので構成がスッキリします。

プルアップはFPGA内蔵のプルアップ機能を使う手もありますが、外付け抵抗を使ったプルアップの方が安定して動くと思います。ちなみに、今回の実験で使ったTMP102温度センサーは外付けプルアップ抵抗が必要で、FPGAのプルアップではクロックを10KHzに落としても正常に動作しませんでした。FPGAの内蔵プルアップはweak pull-upなので高速動作には使えないというようなフォーラムの書き込みがありました。

Update:FPGA内蔵のプルアップで動作しなかったのは、200Ωの保護抵抗が直列に入っているStandard Pmod (JE)にI2Cセンサーをつないだ時の場合でした。試しに保護抵抗が入っていない、Hi-Speed Pmod (JD)にFPGA内蔵のプルアップでつないでみたたらクロック100KHzでも動作しました。条件がよければFPGA内蔵のプルアップでも動作しますが、10KΩ程度の外部抵抗を使った方がより安定していると思われます。

ZYNQ PSのI2Cを使う方法

ZYBOでZYNQ PSのI2Cを使う方法は、以下の2通りのやり方があります:

  • PL(FPGAブロック)を介して、ZYBOのPmodコネクタ(JB〜JE)に接続する
  • PS(CPUブロック)直結のPmod MIO(JF)に接続する

ここでは、それぞれの場合について試してみます。開発環境は執筆時点で最新の、Vivado 2016.2を使用しています。

PL(FPGAブロック)を介して、ZYBOのPmodコネクタ(JB〜JE)に接続する方法

Vivadoで新規プロジェクトを作成して、IP Integrator(IPI)で新規のデザインを作成します(今回はsystemというデザイン名にしています)。図のようにZYNQ 7 Processing Sysemのみをインスタンス化してクロックの接続を手動で行います。

Block Design

ZYNQ 7 Processing Systemのアイコンを右クリックして、”Customize Block..”メニューを開きます。

Customize Block

MIO Configurationをクリックして、IO Peripheralsのプルダウンを開き、I2C0にチェックを入れます。また、IOに「EMIO」を指定します。EMIOを指定することによって、CPUコアのI2C信号がFPGAのPL部分を通って外部に接続できるようになります。

I2C EMIO

ブロックデザインに戻って、ZYNQ 7のIIC_0を右クリックして”Make External”メニューを選択します。この操作によって、I2Cの信号をFPGAから外部に出力できるようになります。外部出力を作る方法には、”Create Interface Port.."などオプション指定ができる方法もありますが、今回はMake Externalで問題ありませんでした。

I2C Make External

次に、Block DesignのSource画面に移って、デザイン名(今回の場合はsystem)を右クリックし、”Create HDL Wrapper..”を選択します。

CreateHDL Wrapper

以下のダイアログボックスが表示されるので、”Let Vivado manage wrapper and auto-update”を選択してOKをクリック。

Let Vivado Manage Wrapper

Flow Navigatorから「Run Implementation」を実行。Implementationが終了すると以下のダイアログボックスが表示されるので、”Open Implemented Design”を選択。

Implementation Complete

画面下に表示される「IO Ports」タブを開くとIIC_0ポートのピンアサイン画面が表示されます。ここに信号を接続したいFPGAのピン番号を入力します。

IO Port

ZYBOのReference Manualを参照して、今回は信号をPmod JE(Standard Pmod)のJE1とJE2に接続します。それぞれに対応するFPGAのピン番号V12とW16を入力、出力電圧をLVCMOS33(3.3V)に指定します。

Assign PL Pin

CTL-Sキーを押すと、Constraintsを保存するダイアログボックスが表示されるのでOKをクリック。

Save Constraints

ファイル名を指定して保存すると、SourcesにConstraints(制約)ファイルが追加されています。

Constraints Added

続けて、Flow Navigatorから「Generate Bitstream」を実行。実行が完了すると以下のダイアログボックスが表示されるので、”Open Implementation Design”を指定してOKをクリック。

Genarate BitStream

File Menu → Export → Export Hardware..を選択。Include bitstreamをチェックしてOKをクリック。

Expot Hardware

File Menu → Launch SDKを選択。SDKが立ち上がります。

SDK Launched

SDKのFile Menu → New → Application Projectを選択。New Projectの設定画面にプロジェクト名(今回はI2C Test)を入力し、Nextをクリック。

New Project

Hello World Templateを選択します(このテンプレートにはprint文を使ってUARTにデバッグ情報を出力するために必要なファイルが含まれているため)。

Select Hello World

プロジェクトが生成されたら、helloworld.cをリネーム。今回は、i2c_test.cにしています(これは好みですが)。

Rename helloworld c

テンプレートが自動生成したソースを全部削除して、以下のコードを入力。 

 
#include "platform.h"
#include "xparameters.h"
#include "sleep.h"
#include "xiicps.h"
#include "stdio.h"


// I2C parameters
#define IIC_SCLK_RATE		100000	// clock 100KHz
#define TMP102_ADDRESS		0x48	// 7bit address
#define IIC_DEVICE_ID		XPAR_XIICPS_0_DEVICE_ID

XIicPs Iic;

int Init()
{
	int Status;
	XIicPs_Config *Config;	/**< configuration information for the device */

	Config = XIicPs_LookupConfig(IIC_DEVICE_ID);
	if(Config == NULL){
		printf("Error: XIicPs_LookupConfig()\n");
		return XST_FAILURE;
	}

	Status = XIicPs_CfgInitialize(&Iic, Config, Config->BaseAddress);
	if(Status != XST_SUCCESS){
		printf("Error: XIicPs_CfgInitialize()\n");
		return XST_FAILURE;
	}

	Status = XIicPs_SelfTest(&Iic);
	if(Status != XST_SUCCESS){
		printf("Error: XIicPs_SelfTest()\n");
		return XST_FAILURE;
	}

	XIicPs_SetSClk(&Iic, IIC_SCLK_RATE);
	printf("I2C configuration done.\n");

	return XST_SUCCESS;
}

int i2c_write(XIicPs *Iic, u8 command, u16 i2c_adder)
{
	int Status;
	u8 buffer[4];
	buffer[0] = command;

	Status = XIicPs_MasterSendPolled(Iic, buffer, 1, i2c_adder);

	if(Status != XST_SUCCESS){
		return XST_FAILURE;
	}

	// Wait until bus is idle to start another transfer.
	while(XIicPs_BusIsBusy(Iic)){
		/* NOP */
	}

	return XST_SUCCESS;
}


int i2c_read(XIicPs *Iic, u8* buff, u32 len, u16 i2c_adder)
{
	int Status;

	Status = XIicPs_MasterRecvPolled(Iic, buff, len, i2c_adder);

	if (Status == XST_SUCCESS)
		return XST_SUCCESS;
	else
		return -1;
}


int main()
{
	init_platform();
	Init();

	u8    buff[4];
	u16   rawdata;
	float temp;

	while(1) {
		i2c_write(&Iic, 0, TMP102_ADDRESS);
		i2c_read(&Iic, buff, 2, TMP102_ADDRESS);

		rawdata = ((int8_t)buff[0] << 4) | ((u8)buff[1] >> 4);
		temp = (float) ((float)rawdata * 0.0625);
		printf("Tmep: %2.1f\n", temp);
		usleep(1000*1000);		// sleep 1sec (1000 x 1000us)
	}

	cleanup_platform();
	return 0;
}

ファイルをセーブすると自動的にビルドが実行されます。次に、ツールバーのProgram FPGAボタンをクリックしてFPGAのコンフィグデーター(Bitstream)をJTAGインタフェース経由で転送します(ZYBOのJP5ジャンパーピンをJTAGに設定しておくこと)。

Program FPGA

続いて、デバッガーを起動してPS(ARM CPUコア)のプログラムを転送しますが、ちょっとコツがあります。私の環境では、Debgug Configurationをいきなり作って起動しようとするとエラーが出てプログラムの起動に失敗することが多いです。そのため、Project ExplorerのI2C_Testプロジェクトを右クリックし、Debug As → Launc on Hardware (GDB)を選択してまずプログラムを起動します。無事プログラムが起動するとmainの最初の行でプログラムがブレークします。

Debug Started

この段階ではDebug Configurationをしていないため、"STDIO not connected”の警告がConsoleに出力され、print文の実行結果は表示されません。ここで一旦、ツルーバーのTerminateボタンを押してプログラムを終了します。デバッグPerspectiveから一旦C/C++ Perspectiveに戻ってProject Explorer → I2C_Testプロジェクトを右クリック → Debug As → Debug Configurations..を選択。STDIO ConnectionにCOMポート番号と通信速度(115200)を設定してDebugをクリック。ツールバーのResumeボタンをクリックするとプログラムが動き出します。

Debug Configurations

TMP102から読み取った温度がConsole画面に表示されています。

Program Run

今回使っているI2CドラバーライブラリのサンプルやドキュメントはSDKのインストールフォルダーの中(C:\Xilinx\SDK\2016.2\data\embeddedsw\XilinxProcessorIPLib\drivers)にあるので、コードの中身の説明は割愛します。

以上が、I2Cの信号をFPGA(PL)経由で取り出す方法です。FPGA内の配線を見ると、I2CのSDAなどI/Oの信号はinとoutの2本の独立した信号としてCPUから出ており、Tri-state Bufferを介してI/Oの信号として外部ピンに接続されています。なんだが回りくどいことをやってるんですね。

SDA iobuf

このIOBUFはVivadoのIPをWrapperの中でインスタンス化して接続していることが以下のHDLコードから分かります。

Wrapper IOBUF

PS(CPUブロック)直結のPmod MIO(JF)に接続する方法

SDKを終了し、VivadoのBlock Designを開き、IIC_0ポートを削除します。

DeleteI 2C Port

Customize Block..を開き、I2C0の接続先を「MIO 10..11」に変更します。

Assign I2C MIO

OKをクリックし、Run Implementationを実行します。実行後のI/O Portsタブを見ると、I2Cの信号が消えていることが分かります。これはI2Cの信号がFPGAブロック(PL)を経由しなくなったことを意味します。

No I2C In PL

Generate Bitstreamを実行し、終了後、Export Hardwareを実行して再度SDKを立ち上げます。SDKのProject ExplorerからI2C_Test_bspを右クリックして、Re-generate BSP Sourcesを実行。さらに、Project Menu → Clean..を実行してビルド環境をクリーンアップします。

Clean Build

センサーのI2CのピンをJFコネクタのMIO-10 (JF2: SCL)、MIO-11 (JF3: SDA)に繋ぎ変えてデバックを実行するとプログラムが起動します。今回はCPUから直接I2Cに出力しています。デバッグでプログラムの起動に失敗する場合は、Debgug Configurationに入って既存のデバッグエントリを一旦削除してから、最初の手順ででデバッグを際実行する(先ずはDebug Configを作らずにDebug Asから起動する)とうまくいくと思います。

Delete Debug Target

ということで、PL経由・PS直結のどちらでもI2Cが動くことが確認できました。

参考情報

ZYBOでPmodCLP(Parallel Interface LCD)を動かす

FPGAハマりの勢いで、Alteraに加えて、Xilinxにも手を出してしまいました。

今回買ったのは、XilinxのSoC (ARMコア)付きFPGA Zynqを搭載したZYBOを秋月さんで購入しました。SoC付きのFPGAを試すのなら、Altera系のDE1-SoCが自然な流れですが、HDLでなくC/C++を使った高位合成(HLS)が無償で使えるXilixの開発環境(Vivado)も試したくなって、Xilinxに手を出してしまいました。下の写真の上段がDE0-CVで、下段が今回買ったZYBOです。

IMG_1084.jpg

高位合成のお試しは、下の写真にある「FPGAマガジンNo.14」を参考してやろうと思っているのですが、その前にZYBOにLCDを繋いでみました。

IMG_1147.jpg

ZYBOは写真で見て分かる通り、DE0-CVに比べて7SEG-LEDがないなど、表示系デバイスが少ないです。その代わりに、ZYBOにはPmodと呼ばれる拡張コネクターが6個付いており、ZYBOの発売元であるDigilent社から各種のPmod拡張モジュールが販売されています。回路のデバッグを行う際など表示系があると便利だと思い、LEDが8個搭載されたPmod8LD(写真右側)とParallel接続のLCD PmodCLP(写真左側)を購入しました。(FedExを使った送料込みで、$50超となり、ちょっと高かったですが)。

IMG_1148.jpg

Pmodの種別

ZYBOのPmodコネクタにはPS(ARMコア)につながっているPmod MIOと、PL(FPGA部)につながっているStandard Pmod, Hi-Speed Pmod, XADC Pmodがあります。PmodCLPはPmodコネクタ2つを占有するのですが、基板下側に4つ並んでいるPmodは左側1つがStandard Pmodで残りの3つはHi-Speed Pmodです。マニュアルによると、Hi-Speed Pmodは隣接する2つの信号ピンをペアで差動出力として使用することによって高速伝送ができるコネクターで、信号ペアを別々に使うとクロストークが発生すると記載があります。

しかしながら、ZYBOの構成ではどうしても1つはHi-Speed Pmodを使う必要があるので、買ってからこの制約に気がついて、果たしてLCDが動くのか気になっていたのでまずはこちらを試してみました。結果はちゃんと動いています。

今回は、制御信号4pinはStandard-Pmodコネクター(JE)を使用して、データーバス8pinをHi-Speed Pmodコネクター(JD)につなぎました。PmodCLPの回路図を見ると、データーバスには200Ωの抵抗が直列で入っていること(これは短絡対策の保護も兼ねていると思いますが)、差動出力で使う信号ペア(例えば、J1のpin-1とpin-2)をダイオードで2.5V電源にクランプしており、この辺りがHi-speed Pmodコネクターに繋いだ時のクロストーク対策になっているのかしらと思ったりしています(データーバスはFPGAの3.3V出力に接続するのでダイオードを使った過電圧保護は不要だと思うのですが、何のために入っているのか)。

PmodCLP.png

ZynqのIP作成

Zynq(SoC付きFPGA)では まずARMコアありきで、ARMコアに回路ブロックを接続します。Vivadoではライブラリ化された回路ブロック(IP)をZinqに接続していきます。このあたりは、AlteraのQsysと同様の考え方だと思うのですが、QsysよりVivadoのIP Integratorの方が配線やアドレス設定を自動化してくれる度合いが高く使い易い感じがしました。またQsysではTopレベルのHDLは手で書く必要がありますが、Vivadoでは自動生成してくれます。

IPを作成する手順ですが、Digilentが提供している、Vivado用のBorad Fileをインストールしておくと、オンボードのLED/SWはGPIOのメニューに表示されるようになります。ZYBOボードを指定することでZYBOが使用しているZynq FPGAのタイプも自動的に選択してくれるためこのBorad Fileはインストールしておくと便利です。また、Digilentが提供しているPmod-Libraryがあるのですが、PmodCLPとPmod8LDにはライブラリがありません。

そのため、Zynq FPGAにARMコアとGPIOをVivadoのIP Integratorを使って配置して、GPIOにPmodCLPのデータバスと制御信号を接続します。

  • IP IntegratorのCreate Block Designを選択、新規Designを作成。Design名はsystemとしました(IP Integratorの使い方はGetting Started with Zynqに詳細な解説があります)
  • Add IPを使って、Zynq-7 Processing SystemとAXI GPIOを配置して接続します
  • GPIOは「Enable Dual Channel」をチェックして、GPIO2も有効にし、GPIOをデーター用の8-bit入出力(All input/ All output双方をチェックしない)、GPIO2を制御用の4-bitの出力ポートに設定します。
  • GPIOポートの名称を「LCD-DB」、GPIO2のポート名を「LCD-CTL」と設定します。

Vivado_IP.PNG


Vovado-GPIO.PNG

制約ファイルの作成

次に、FPGAのピンとPmodのピン(GPIOの端子)を関連付ける制約ファイルを作成します。この手順は、このブログを参考にさせていただきました。概要は以下の通りです。

  • IP作成ができたら、HDL Wrappperを作成。SourcesウインドウのIP SourcesタブからBlock Design名(今回の例ではsystem)を右クリック、「Create HDL Wrapper...」を選択します。
  • 次に制約ファイルを作成。SourcesウィンドウのHierarchyタブにおいて、Constraints > constrs_1上にて右クリック「Add Source」を選択、Add or create constraints選択して、PmodLCD.xdcの名称で制約ファイルを作成します。
  • Run Implementationを実行した後、Open Implementation Designを選択してOKをクリック、画面下のI/O Portsタブを選択して、GPIOポートのPackage Pin列にFPGAのpin番号を割り付けます。今回は、LCD-DBポート(lcd_db_tri_io[0]〜[7])にPmod JDコネクターに対応したFPGAの端子を割り当て、LDC-CTLポート(lcd_ctl_tri_o[0]〜[3])にはPmod-JEコネクターの下段pin(JE7〜JE10に対応するFPGAのpin)を以下のように割り当てました。
  • 加えて、I/O StdをLVCMOS33に変更します。
  • 割り当てができたら、CTRL-Sでファイルを保存します。

Pmod-xdc.PNG

一度割り当てを作ってしまえば、次からは制約ファイル(PmodLCD.xdc)インポートすることでFPGAのPin割り当てを自動的に行うことができます。

Bitstream(FPGA configデータ)の生成とSDKへのExport

  • Generate Btstramを実行後、File > Export > Export Hardware…から、Include bitstreamチェックしてOKクリック。
  • File > Launch SDKを選択して、EclipseベースのSDKでARMコア用のLCD制御ソフトを作成します。

LCD制御ソフトの作成

DigilentのPmodCLP用「Library and MPLAB Example」にサンプルコードがあるのですが、中身を見るとArduino用のLCDライブラリが入っています。このままでは使えないので、空のC++プロジェクトを作って、コードを以下のようにZynqのベアメタルARM用に書き換えました。

/************************************************************************/
/*                                                                                                                                              */
/*      LCDP.h  --      Declaration for LCDP library                                            */
/*                                                                                                                                              */
/************************************************************************/
/*      Author:         Cristian Fatu                                                                                   */
/*      Copyright 2011, Digilent Inc.                                                                           */
/************************************************************************/
/*  File Description:                                                                                                   */
/*              This file declares LCDP library functions and constants involved*/
/*                                                                                                                                              */
/************************************************************************/
/*  Revision History:                                                                                                   */
/*                                                                                                                                              */
/*      12/10/2011(CristianF): created                                                                          */
/*  21/08/2016(Todotani): adapted fro ZYBO PmodCLP                      */
/*                                                                                                                                              */
/************************************************************************/
#if !defined(LCDP_H)
#define LCDP_H


/* ------------------------------------------------------------ */
/*                              Include File Definitions                                                */
/* ------------------------------------------------------------ */
#include <inttypes.h>
#include "xparameters.h"
#include "xgpio.h"
#include "sleep.h"


/* ------------------------------------------------------------ */
/*                                      Definitions                                                                     */
/* ------------------------------------------------------------ */

/* ------------------------------------------------------------ */
/*                                      Errors Definitions                                                      */
/* ------------------------------------------------------------ */

/* ------------------------------------------------------------ */
/*              Command codes Definitions                                                               */
/* ------------------------------------------------------------ */
#define LCDP_CMD_LcdFcnInit     0x38    // function set command, (8-bit interface, 2 lines, and 5x8 dots)
#define LCDP_CMD_LcdCtlInit     0x08    // display control set command
#define LCDP_CMD_LcdClear               0x01    // clear display command
#define LCDP_CMD_LcdRetHome             0x02    // return home command
#define LCDP_CMD_LcdDisplayShift 0x18   // shift display command
#define LCDP_CMD_LcdCursorShift  0x10   // shift cursor command
#define LCDP_CMD_LcdSetDdramPos 0x80    // set DDRAM position command
#define LCDP_CMD_LcdSetCgramPos 0x40    // set CGRAM position command

#define LCDP_MSK_BStatus        0x80            // bit busy
#define LCDP_MSK_ShiftRL        0x04            // shift direction mask
#define LCDP_OPT_DisplayOn      0x4             // Set Display On option
#define LCDP_OPT_CursorOn       0x2             // Set Cursor On option
#define LCDP_OPT_BlinkOn        0x1             // Set Blink On option


/* ------------------------------------------------------------ */
/*                              Parameters Definitions                                                  */
/* ------------------------------------------------------------ */
#define LCDP_DISP_SetOptionDisplayOn    0x4 // Set Display On option
#define LCDP_DISP_SetOptionCursorOn     0x2 // Set Cursor On option
#define LCDP_DISP_SetBlinkOn                    0x1 // Set Blink On option
/* ------------------------------------------------------------ */
/*                                      Class Declarations                                                      */
/* ------------------------------------------------------------ */
#define mskLCDPDat07            0x000000FF

#define LCDP_ERR_SUCCESS                                0               // The action completed successfully
#define LCDP_ERR_UCHAR_POSITION_INVALID 0x20    // The user character position is not correct
#define LCDP_ERR_ARG_ROW_RANGE                  0x80    // The row index is not valid
#define LCDP_ERR_ARG_COL_RANGE                  0x40    // The column index is not valid

#define LCDP_NO_ROWS    2
#define LCDP_NO_COLS    40
#define LCDP_NO_UCHARS  8

/* ------------------------------------------------------------ */
/*                                      Control Singnals                                                        */
/* ------------------------------------------------------------ */
#define RS                              0b0001          // Register Select: High for Data Transfer, Low for Instruction Transfer
#define RW                              0b0010          // Read/Write signal: High for Read mode, Low for Write mode
#define EN                              0b0100          // Read/Write Enable: High for Read, falling edge writes data
#define BL                              0b1000

class LCDP {
private:
        XGpio PmodLCD;
        uint8_t control;
        
        uint8_t m_bDisplayMode;

        uint8_t ReadByte();
        void WriteByte(uint8_t bData);

        void WaitUntilNotBusy();
        void WriteCommand(uint8_t bCmd);
        void WriteDataByte(uint8_t bData);

        void SetWriteCgramPosition(uint8_t bAdr);
        void SetWriteDdramPosition(uint8_t bAdr);
        uint8_t ReadStatus();
public:

        LCDP();

        void begin();

        void DisplayClear();
        void ReturnHome();
        void SetDisplay(bool fDisplayOn);
        void SetCursor(bool fCursorOn);
        void SetBlink(bool fBlinkOn);
        void SetBacklight(bool fBacklightOn);
        uint8_t SetPos(uint8_t idxLine, uint8_t idxCol);
        uint8_t WriteStringAtPos(char *szLn, uint8_t idxLine, uint8_t idxCol);
        void DisplayShift(bool fRight);
        void CursorShift(bool fRight);
        uint8_t DefineUserChar(uint8_t *pBytes, uint8_t bCharNo);
        uint8_t WriteUserCharsAtPos(uint8_t* rgCharPos, uint8_t bNoChars, uint8_t idxLine, uint8_t idxCol) ;
        };

#endif


/************************************************************************/
/*                                                                                                                                              */
/*      LCDP.cpp                --      Definition for LCDP library                             */
/*                                                                                                                                              */
/************************************************************************/
/*      Author:         Cristian Fatu                                                                                   */
/*      Copyright 2011, Digilent Inc.                                                                           */
/************************************************************************/
/*  File Description:                                                                                                   */
/*              This file defines functions for LCDP                                                    */
/*                                                                                                                                              */
/************************************************************************/
/*  Revision History:                                                                                                   */
/*                                                                                                                                              */
/*      12/10/2011(CristianF): created                                                                          */
/*  21/08/2016(Todotani): adapted fro ZYBO PmodCLP                      */
/*                                                                                                                                              */
/************************************************************************/


/* ------------------------------------------------------------ */
/*                              Include File Definitions                                                */
/* ------------------------------------------------------------ */

#include "LCDP.h"


/* ------------------------------------------------------------ */
/*                              Procedure Definitions                                                   */
/* ------------------------------------------------------------ */


/* ------------------------------------------------------------ */
/**        Description:
**                      Class constructor. Performs variables initialization tasks
**
**
*/
LCDP::LCDP()
{
        m_bDisplayMode = 0;
}

/* ------------------------------------------------------------ */
/*        LCDP::begin
**
**        Synopsis:
**
**        Description:
**                              This function saves the pins corresponding to the CLP and performs the required LCDP initialization tasks.
**
*/
void LCDP::begin()
{
        // Initialize Zynq GPIO
        XGpio_Initialize(&PmodLCD, XPAR_AXI_GPIO_0_DEVICE_ID);
        XGpio_SetDataDirection(&PmodLCD, 2, 0x0);           // Set control port output

        // set control flag 0
        control = 0;

        // perform initialization sequence, according to datasheet
        //      wait 20 ms
        usleep(20*000);
        // Set function
        WriteCommand(LCDP_CMD_LcdFcnInit);
        // Wait 37 us
        usleep(37);

        // display on, no cursor, no blinking
        SetDisplay(true);
        SetBacklight(true);

        // Wait 37 us
        usleep(37);
        // Display Clear
        DisplayClear();
        // Wait 1.52 ms
        usleep(1520);
}


/* ------------------------------------------------------------ */
/*        ReadByte
**
**        Return Values:
**                uint8_t - the byte that was read
**
**        Description:
**                              The function implements a CLP read sequence.
**                              The function is used to read data (RS set before calling this function) or to read status (RS cleared before calling this function).
**                              The function implements two approaches of handling data pins.
**
*/
uint8_t LCDP::ReadByte()
{
        uint8_t bData = 0;

        // Set data port input
        XGpio_SetDataDirection(&PmodLCD, 1, 0xFF);

        // Set RW
        control |=  RW;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
        // Set EN
        control |=  EN;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);

        bData = XGpio_DiscreteRead(&PmodLCD, 1);

        // Clear EN
        control &= ~EN;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
        // Clear  RW
        control &=  ~RW;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);

        return bData;
}


/* ------------------------------------------------------------ */
/*        WriteByte
**
**        Parameters:
**                              uint8_t bData - data to be written to display
**
**        Return Values:
**                void
**
**        Description:
**                              The function implements a CLP write sequence.
**                              The function is used to write data (RS set before calling this function) or to write commands (RS cleared before calling this function).
**                              When writing data it writes in DDRAM or CGRAM according to the last set write position.
*/
void LCDP::WriteByte(uint8_t bData)
{
        // Set data port output
        XGpio_SetDataDirection(&PmodLCD, 1, 0x00);

        // Clear RW
        control &= ~RW;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
        // Set Enalbe
        control |= EN;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);

        XGpio_DiscreteWrite(&PmodLCD, 1, bData);
        
        // Clear Enable
        control &= ~EN;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
        // Set Read
        control |= RW;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
}

/* ------------------------------------------------------------ */
/*        ReadStatus
**
**        Return Values:
**                uint8_t - the byte that was read.
**
**        Description:
**                              Reads the status of the CLP. It clears the RS and calls ReadByte() function.
**
*/
uint8_t LCDP::ReadStatus()
{
        uint8_t bStatus;
        // clear RS
        control &= ~RS;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
        
        // read status byte
        bStatus = ReadByte();
        return bStatus;
}

/* ------------------------------------------------------------ */
/*        WaitUntilNotBusy
**
**        Synopsis:
**                              WaitUntilNotBusy()
**
**        Return Values:
**                void
**
**        Description:
**                              Waits until the status of the CLP is not busy. This function relies on ReadStatus().
**
*/
void LCDP::WaitUntilNotBusy()
{
        uint8_t bStatus;
        bStatus = ReadStatus();
        while (bStatus & LCDP_MSK_BStatus)
        {
                usleep(10);
                bStatus = ReadStatus();
        }
}

/* ------------------------------------------------------------ */
/*        WriteCommand
**
**        Synopsis:
**                              WriteCommand(cmdLcdClear);
**        Parameters:
**                              uint8_t bCmd    - the command code byte
**
**        Description:
**                              Writes the specified byte as command. When the device is ready it clears the RS and writes byte.
**
*/
void LCDP::WriteCommand(uint8_t bCmd)
{
        // wait until LCD is not busy
        WaitUntilNotBusy();

        // Clear RS
        control &= ~RS;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);

        // Write command byte
        WriteByte(bCmd);
}


/* ------------------------------------------------------------ */
/*        WriteDataByte
**
**        Synopsis:
**                              WriteDataByte(pBytes[idx]);
**        Parameters:
**                              uint8_t bData           - the data byte
**
**        Description:
**                              Writes the specified byte as data. When the device is ready it sets the RS and writes byte.
**
*/
void LCDP::WriteDataByte(uint8_t bData)
{
        // wait until LCD is not busy
        WaitUntilNotBusy();

        // Set RS
        control |= RS;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);

        // Write command byte
        WriteByte(bData);
}

/* ------------------------------------------------------------ */
/*        SetWriteCgramPosition
**
**        Synopsis:
**                              SetWriteCgramPosition(bAdr);
**        Parameters:
**                              uint8_t bAdr    - the write location. The position in CGRAM where the next data writes will put bytes.
**
**        Description:
**                              Sets the CGRAM write position. This is the location where the next data write will be performed.
**                              Be aware that writing to a location auto-increments the write location.
**
*/
void LCDP::SetWriteCgramPosition(uint8_t bAdr)
{
        uint8_t bCmd = LCDP_CMD_LcdSetCgramPos | bAdr;
        WriteCommand(bCmd);
}

/* ------------------------------------------------------------ */
/*        SetWriteDdramPosition
**
**        Synopsis:
**                              SetWriteDdramPosition(bAddrOffset);
**        Parameters:
**                              uint8_t bAdr - the write location. The position in DDRAM where the next data writes will put bytes.
**                                      0x00-0x27 refer to the first row
**                                      0x40-0x67 refer to the second row
**
**        Description:
**                              Sets the DDRAM write position. This is the location where the next data write will be performed.
**                              Be aware that writing to a location auto-increments the write location.
**
*/
void LCDP::SetWriteDdramPosition(uint8_t bAdr)
{
        uint8_t bCmd = LCDP_CMD_LcdSetDdramPos | bAdr;
        WriteCommand(bCmd);
}


/* ------------------------------------------------------------ */
/*        DisplayClear
**
**        Synopsis:
**                              DisplayClear();
**
**        Description:
**                              Clears the display and returns the cursor home (upper left corner).
**
*/
void LCDP::DisplayClear()
{
        WriteCommand(LCDP_CMD_LcdClear);
}


/* ------------------------------------------------------------ */
/*        ReturnHome
**
**        Description:
**                              Returns the cursor home (upper left corner).
**
*/
void LCDP::ReturnHome()
{
        WriteCommand(LCDP_CMD_LcdRetHome);
}

/* ------------------------------------------------------------ */
/*        SetDisplay
**
**        Synopsis:
**                              SetDisplay(true);
**        Parameters:
**                              bool fDisplayOn - Display option
**                                              - true in order to set the display ON
**                                              - false in order to set the display OFF
**
**        Description:
**                              Sets the display option. If true, display is on, if false, the display is off.
**
*/
void LCDP::SetDisplay(bool fDisplayOn)
{
        if(fDisplayOn)
        {
                m_bDisplayMode |= LCDP_OPT_DisplayOn;
        }
        else
        {
                m_bDisplayMode &= ~LCDP_OPT_DisplayOn;
        }
        WriteCommand(LCDP_CMD_LcdCtlInit | m_bDisplayMode);
}

/* ------------------------------------------------------------ */
/*        SetCursor
**
**        Synopsis:
**                              SetCursor(true);
**        Parameters:
**                              bool fCursorOn - Cursor option
**                                              - true in order to set the Cursor ON
**                                              - false in order to set the Cursor OFF
**
**        Description:
**                              Sets the cursor option. If true, Cursor is on, if false, the Cursor is off.
**
*/
void LCDP::SetCursor(bool fCursorOn)
{
        if(fCursorOn)
        {
                m_bDisplayMode |= LCDP_OPT_CursorOn;
        }
        else
        {
                m_bDisplayMode &= ~LCDP_OPT_CursorOn;
        }
                
        WriteCommand(LCDP_CMD_LcdCtlInit | m_bDisplayMode);
}

/* ------------------------------------------------------------ */
/*        SetBlink
**
**        Synopsis:
**                              SetBlink(true);
**        Parameters:
**                              bool fBlinkOn - Blink option
**                                              - true in order to set the Blink ON
**                                              - false in order to set the Blink OFF
**
**        Description:
**                              Sets the Blink option. If true, Blink is on, if false, the Blink is off.
**
*/
void LCDP::SetBlink(bool fBlinkOn)
{
        if(fBlinkOn)
        {
                m_bDisplayMode |= LCDP_OPT_BlinkOn;
        }
        else
        {
                m_bDisplayMode &= ~LCDP_OPT_BlinkOn;
        }
                
        WriteCommand(LCDP_CMD_LcdCtlInit | m_bDisplayMode);
}

/* ------------------------------------------------------------ */
/*        SetBacklight
**
**        Synopsis:
**                              SetBacklight(fBackLight);
**        Parameters:
**                              bool fBl - Backlight option
**                                              - true in order to set the backlight ON
**                                              - false in order to set the backlight OFF
**
**        Description:
**                              This function turns the backlight on or off, according to the user's selection.
**                              Note that there are CLP Pmods that do not have backlight functionality. Using this function for this type of modules will have no effect.
**
*/
void LCDP::SetBacklight(bool fBacklightOn)
{
        if (fBacklightOn)
                control |= BL;
        else
                control &= ~BL;
        XGpio_DiscreteWrite(&PmodLCD, 2, control);
}

/* ------------------------------------------------------------ */
/*        SetPos
**
**        Synopsis:
**                              SetPos(0, 3);
**        Parameters:
**                              uint8_t idxLine - the line where the position will be set
**                              uint8_t idxCol  - the column where the position will be set
**
**
**        Return Value:
**                uint8_t
**                                      - LCDP_ERR_SUCCESS (0)  - The action completed successfully
**                                      - a combination (ORed) of the following errors:
**                                              - LCDP_ERR_ARG_ROW_RANGE (0x80) - The row index is not valid
**                                              - LCDP_ERR_ARG_COL_RANGE (0x40) - The column index is not valid
**        Description:
**                              This function sets the corresponding LCD position. This is used for write position and cursor position.
**                              If position set is invalid (outside the display), errors are returned.
**
**
*/
uint8_t LCDP::SetPos(uint8_t idxLine, uint8_t idxCol)
{
        uint8_t bResult = LCDP_ERR_SUCCESS;

        if (idxLine < 0 || idxLine >= LCDP_NO_ROWS)
        {
                bResult |= LCDP_ERR_ARG_ROW_RANGE;
        }
        if (idxCol < 0 || idxCol >= LCDP_NO_COLS)
        {
                bResult |= LCDP_ERR_ARG_COL_RANGE;
        }
        if (bResult == LCDP_ERR_SUCCESS)
        {
                // Set write position
                uint8_t bAddrOffset = (idxLine == 0 ? 0: 0x40) + idxCol;
                SetWriteDdramPosition(bAddrOffset);
        }
        return bResult;
        
}

/* ------------------------------------------------------------ */
/*        WriteStringAtPos
**
**        Synopsis:
**                              WriteStringAtPos(szInfo1, 0, 0);
**        Parameters:
**                              char *szLn      - string to be written to LCD
**                              uint8_t idxLine - the line where the string will be displayed
**                              uint8_t idxCol  - the column line where the string will be displayed
**
**
**        Return Value:
**                uint8_t
**                                      - LCDP_ERR_SUCCESS (0)  - The action completed successfully
**                                      - a combination (ORed) of the following errors:
**                                              - LCDP_ERR_ARG_ROW_RANGE (0x80) - The row index is not valid
**                                              - LCDP_ERR_ARG_COL_RANGE (0x40) - The column index is not valid
**
**
**        Errors:
**                              See Return Value
**
**        Description:
**                              The function writes the specified string at the specified position (line and column).
**                              It sets the corresponding write position and then writes data bytes when the device is ready.
**                              Strings that span over the end of line are trimmed so they fit in the line.
**                              If position is invalid (outside the display), errors are returned.
**
*/
uint8_t LCDP::WriteStringAtPos(char *szLn, uint8_t idxLine, uint8_t idxCol)
{
        uint8_t bResult = SetPos(idxLine, idxCol);
        if (bResult == LCDP_ERR_SUCCESS)
        {
                // Strings that span over the end of line are trimmed so they fit in the line.
                int len = strlen(szLn);
                if(len + idxCol > LCDP_NO_COLS)
                {
                        len = LCDP_NO_COLS - idxCol;
                        szLn[len] = 0; // crop the string at this position
                }

                uint8_t bIdx = 0;
                while(bIdx < len)
                {
                        WriteDataByte(szLn[bIdx]);
                        bIdx++;
                }
        }
        return bResult;
        
}


/* ------------------------------------------------------------ */
/*        DisplayShift
**
**        Synopsis:
**                              DisplayShift(fBtn1Process);
**        Parameters:
**                              bool fRight - parameter indicating the direction of the display shift
**                                              - true in order to shift the display right
**                                              - false in order to shift the display left
**
**        Description:
**                              This function shifts the display one position right or left, depending on the fRight parameter.
**
**
*/
void LCDP::DisplayShift(bool fRight)
{
        uint8_t bCmd = LCDP_CMD_LcdDisplayShift | (fRight != false ? LCDP_MSK_ShiftRL: 0);
        WriteCommand(bCmd);
}


/* ------------------------------------------------------------ */
/*        CursorShift
**
**        Synopsis:
**                              CursorShift(fBtn1Process);
**        Parameters:
**                              bool fRight - parameter indicating the direction of the cursor shift
**                                              - true in order to shift the cursor right
**                                              - false in order to shift the cursor left
**
**        Description:
**                              This function shifts the cursor one position right or left, depending on the fRight parameter.
**
*/
void LCDP::CursorShift(bool fRight)
{
        uint8_t bCmd = LCDP_CMD_LcdCursorShift | (fRight != false ? LCDP_MSK_ShiftRL: 0);
        WriteCommand(bCmd);
}

/* ------------------------------------------------------------ */
/*        DefineUserChar
**
**        Synopsis:
**                              MyLCDP.DefineUserChar(defChar2, 2);
**        Parameters:
**                              uint8_t *pBytes - pointer to the string that contains the 8 bytes definition of the character.
**                              uint8_t bCharNo - the position of the user character to be saved in the memory
**
**
**        Return Values:
**                uint8_t
**                                      - LCDP_ERR_SUCCESS (0)                          - The action completed successfully
**                                      - LCDP_ERR_UCHAR_POSITION_INVALID       (0x20) - The user character position is not within 0 - 7
**
**        Description:
**                              This function writes the specified number of bytes to CGRAM starting at the specified position.
**                              It sets the corresponding write position and then writes data bytes when the device is ready.
**                              If the user character position is not within 0 - 7 range, error is returned.
**
*/
uint8_t LCDP::DefineUserChar(uint8_t *pBytes, uint8_t bCharNo)
{
        uint8_t bResult = LCDP_ERR_SUCCESS;

        if (bCharNo >=0 || bCharNo < LCDP_NO_UCHARS)
        {
                uint8_t bAdr = bCharNo << 3; // multiply by 8
                // Set write position to CGRAM
                SetWriteCgramPosition(bAdr);
                uint8_t len = 8;
                // Write the string of bytes that define the character to CGRAM
                uint8_t bIdx = 0;
                while(bIdx < len)
                {
                        WriteDataByte(pBytes[bIdx]);
                        bIdx++;
                }
                bResult = LCDP_ERR_SUCCESS;
        }
        else
        {
                bResult = LCDP_ERR_UCHAR_POSITION_INVALID;
        }
        return bResult;
}

/* ------------------------------------------------------------ */
/*        WriteUserCharsAtPos
**
**        Synopsis:
**                              WriteUserCharsAtPos(szInfo1, 0, 0);
**        Parameters:
**                              rgCharPos - an array containing the index (position) of the user characters to be displayed
**                              bNoChars - an array containing the index (position) of the user characters to be displayed
**                              uint8_t idxLine - line where the string will be displayed
**                              uint8_t idxCol  - the starting position of the string within the line

**
**        Return Value:
**                uint8_t
**                                      - LCDP_ERR_SUCCESS (0)  - The action completed successfully
**                                      - a combination (ORed) of the following errors:
**                                              - LCDP_ERR_ARG_ROW_RANGE (0x80) - The row index is not valid
**                                              - LCDP_ERR_ARG_COL_RANGE (0x40) - The column index is not valid
**                                              - LCDP_ERR_UCHAR_POSITION_INVALID       (0x20) - The user character position is not within the accepted range (0 ? 7)
**
**      Description:
**              This function displays one or more user defined characters at the specified positions on the LCD.
**              If the position set or the user character position is not correct, errors are returned.
**
**
**
-----------------------------------------------------------------------*/
uint8_t LCDP::WriteUserCharsAtPos(uint8_t* rgCharPos, uint8_t bNoChars, uint8_t idxLine, uint8_t idxCol)
{
        uint8_t bResult = SetPos(idxLine, idxCol);
        if (bResult == LCDP_ERR_SUCCESS)
        {
                // validate the user character positions to be between 0 and 7
                uint8_t bIdx = 0;
                while(bIdx < bNoChars)
                {
                        if (rgCharPos[bIdx] < 0 || rgCharPos[bIdx] >= LCDP_NO_UCHARS)
                        {
                                bResult = LCDP_ERR_UCHAR_POSITION_INVALID;
                                bIdx = bNoChars; // force out, no need to continue
                        }
                        bIdx++;
                }
                if (bResult == LCDP_ERR_SUCCESS)
                {
                        //set the write position of the cursor to the wanted line/column for displaying custom chars
                        uint8_t bAddrOffset = (idxLine == 0 ? 0: 0x40) + idxCol;
                        SetWriteDdramPosition(bAddrOffset);
                        //send the position of the user character to be displayed
                        uint8_t bIdx = 0;
                        while(bIdx < bNoChars)
                        {
                                WriteDataByte(rgCharPos[bIdx]);
                                bIdx++;
                        }
                }
        }
        return bResult;
}

LCDに表示を行う簡単なmain.cppを以下のように作成

#include "LCDP.h"
#include "sleep.h"
#include <stdio.h>

int main()
{
        LCDP lcd;
        char buff[16];
        char message[16] = "Hello ZYBO";

        lcd.begin();
        int i = 0;

        while(1) {
                lcd.WriteStringAtPos(message, 0, 0);
                sprintf(buff, "%d", i);
                lcd.WriteStringAtPos(buff, 1, 0);
                i++;
                usleep(50*1000);
        }

        return 0;
}

プログラムの実行

ビルドを行い以下の手順でFPGAのconfigとプログラムの実行を行います。

  • ZYBOのProgramming Mode JumperをJTAGにセット
  • Xilix Tools → Program FPGAを実行。LD10のグリーンLED(DONE)が点灯すればFPGAのconfigが完了です
  • Run → Run As → Launch on Hardware (GDB)

以下のとおりプログラムが起動します。


JTAGインターフェース経由で起動した場合、電源を切ったりリセットを行うと、FPGAのconfig・PSのプログラムとも消えてしまいます。configやプログラムを永続化したい場合は、QSPIメモリーにbootイメージを書き込む必要があります。この辺りは別途書きます。
参考情報

 

2023年6月
        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 29 30  
無料ブログはココログ