lyricalmagicalのアイコン画像
lyricalmagical 2021年02月05日作成 (2022年05月19日更新)
製作品 製作品 閲覧数 3117
lyricalmagical 2021年02月05日作成 (2022年05月19日更新) 製作品 製作品 閲覧数 3117

[FPGA]液晶モニター遅延測定器を作る[ARTY Z7-20]

[FPGA]液晶モニター遅延測定器を作る[ARTY Z7-20]

ゲーマーなら気になる、液晶モニターの遅延を測定する装置を作ってみました。

やりたいこと

具体的に何をやりたいのかというと、PCやゲーム機が出力した画像が実際に液晶モニターに表示されるまでに、どのくらいの時間がかかっているか、というのを測定したいです。
そのためには、

  • 「今出力した画像」が「いつ表示されるか」

を、測定すればいいことになります。

PCでも簡単にできそうですが、

  • プログラムで表示した画像が本当にその瞬間にHDMI出力から出力されているのか?OSやデバイスドライバがフレームバッファを持ってたりしないのか?
  • 「今」出力した、という情報をどうやって外部に渡すのか?USBやシリアル経由で出力するとしたら、デバイスドライバや変換回路がFIFOなどで遅延させていないか?

というあたりが障害になって、高度なOS上で動くプログラムでは指示した出力がいろいろな要因で遅延されるので、なかなか正確な測定は難しいです。
Raspberry piなどでも、Linuxで動かしたりしていると、同じ問題を抱えてます。

というわけで、このあたりのHDMI出力系をすべてハードウェアロジックで作って、正確に測定してみようという試みです。

仕組み

では実際にどういう動作をするものを作ればこの測定ができるのでしょうか。
まず単純に、出力する映像としては、数パターンの映像を用意して、それを1フレームごと順番に出力します。ターゲットは1080/60pとして、1/60秒ごとに映像を切り替えていきます。
次に、「今どのパターンの映像を出力しているか」という情報もLEDで表示するようにします。LEDの反応速度は非常に高速なので、測定対象を考えると無視できる反応速度です。

ここで、LEDの表示が「パターン4出力中」となっていて、実際に液晶に表示されている画像が「パターン1」であれば、3フレームの遅延があることになります。
もちろん目視確認できないので、液晶モニターとLEDを同時に写真撮影して、結果を確認することになります。

実際には遅延は1フレーム単位とは限らないので、もう少し細かく測定する必要がありますが、そちらについては後述します。

使用したもの

  • DIGILENT ARTY Z7-20 FPGA評価ボード

ARTY Z7-20
FPGAボードです。
XILINXのZynq-7000シリーズのFPGAが搭載されています。
このFPGAは、ARM Cortex-A9も搭載されていて、ARM部+PLD部という構成になっています。ただ、今回は、ARM自体は未使用で、単純にHDMI出力ができるFPGAとして使用しています。
そのほかにもこのボードにはいろいろな機能が搭載されていますが、今回はほとんどの機能を使っていません。
多機能なのでちょっとお高め(といってもFPGA評価ボードとしては格安のほう)ですが、TMDS出力さえあればHDMIとして使えるので、もっと安価なボードでも可能と思います。

  • LED
    LED
    8個 x 2色使用します。
    今回は接続するLEDの数が多くなるので、抵抗内蔵LEDを使用してみました。
    そのため、直接FPGAの出力で発光させることができ、部品点数を減らすことができます。

  • その他
    ブレッドボードやジャンパーワイヤーなどはいつも通りです。

HDMIの伝送方法概要

さて、正しい測定を行うためには、まず、HDMIはどうやって画像を送信しているのかという説明が必要になります。
できる限り、今回作成するのに必要な部分だけに抜粋して、細かいところは省略します。

まず、HDMIは実際の画像データのほかにも、いろいろなデータを送信しています。そのタイミングを図にすると下記のようになります。
キャプションを入力できます
黄色い部分が実際に表示されるデータ、それ以外の部分は制御信号です。
HDMIは画像情報をシリアル伝送しますが、図の左上から右方向へ、そして、上から下へと順に送信します。
この図では、1920x1080 60pを前提としたタイミングでclock数が記載されています。実画像の1pixelが1clockとしています(ピクセルクロックという)。
ピクセルクロックは148.5 MHzです。
このあたりの数値は規格として決められていますので、詳細は参考資料を参照してください。
図に書かれている通り、横1ラインは88+44+148+1920=2200ピクセルクロック、それを4+5+36+1080=1125ライン送信することで1画面分のデータの送信ができます。
つまり、2200 x 1125=2475000ピクセルクロックで1画面を送信できます。ピクセルクロックが148.5MHzなので、148.5M÷2475000=60となり、フレームレートはちょうど60Hzとなります。

実際にはHDMIでは3組のシリアル伝送路があり、RGBが別々に送信されています。また、1pixelのRGB各要素を各10bitで送信するため、実際の送信clockはピクセルクロックの10倍(1.485GHz x 3経路)になりますが、ここはDIGILENTの無料IP(Intellectual Property=ソフトウェアでいうライブラリのようなもの、後述)がいい感じに変換してくれています。こちらも単純なパラレル→シリアル変換ではないのですが、IPが全部処理してくれるので詳細は省略します。
ちなみに、IP自体にも遅延がありますが、数ピクセルクロック程度なので、そこは今回は無視します。

液晶モニター側も、受信した信号に合わせて、順次液晶画面を書き換えていきます。なので、左上から右下にかけて、順次書き換えられていきます。一瞬で画像が切り替わるわけではありません。

余談ですが、front+sync+backを合わせて、blanking期間といいます。今回は使用しませんが、HDMIの規格ではblanking期間に音声データを送信します。
※blanking期間と実画像の比率がおかしいですが、図解のしやすさでこうしています。

参考資料:
High-Definition Multimedia Interface Specification V1.3a
VESA DMT v1.13

作るものの仕様

では、冒頭で「仕組み」に書いたものをもう少し具体的に詳しく考えてみます。
まず、「数パターンの画像を繰り返し表示する」というものが必要です。
何パターンが必要かわかりませんが、少なくとも液晶モニターの遅延フレーム数以上のパターンが必要です。3フレームくらいは遅れるテレビがあるらしいので、余裕をもって8パターンにしました。
きれいな写真とかである必要もないので、単純に「0」~「7」を画面いっぱいに表示するだけとしました。
また、最終的にカメラで液晶モニターとLEDを撮って判定するという都合上、V blanking期間に写真を撮らない限りは、画像の書き換え中に写真を撮ることになるので、画面の上下で異なるパターンが表示されてしまいます。
なので、大きな文字1文字だけを表示すると、ちぎれてしまって何が表示されているかわからない(こともなさそうだが念の)ため、小さい文字を画面いっぱい並べることにしました。
あとは、今現在どのパターンを表示しているかを示すLEDが必要です。
これらがあれば、「今どのパターンを出力中か」と「液晶モニターがどのパターンを描画中か」がわかるので、何フレーム遅延しているかを測定することができます。

しかし、これだけでは1フレーム単位の遅延しか測定できないので、もう少し細かく測定したいところです。
なので、「画像のどのあたりを送信中か」という進行度もある程度わかるように、こちらもLEDで表示するようにしました。こちらは全体を9分割して、8個のLEDで進行度を表示するようにしました。
これを先ほどのタイミング説明の画像にあわせて図示してみます。
LEDタイミング
※先ほどのタイミング説明図より、blanking期間と実画像期間の比率を実際に近づけています

このようにすることで、「今どのパターンの画像を表示しているのか」と「だいたいどのあたりまで画像情報を送信したか」と「液晶モニターがどのパターンを表示しているか」が写真を撮るとわかるようになります。

実際に作ってみる

Vivado編

ソースなどの記述やコンパイル、FPGAボードへの書き込みは、XILINX Vivadoというソフトで行います。
ここでは、その手順を説明します。
VivadoはXILINXのwebページからダウンロードできます。高度な機能は有償版しか使えませんが、今回の内容であれば無償版(Web pack)で問題ありません。
手順はVivado2020.2で確認しています。Vivado自体のインストールは完了させておいてください(これが長い…)。

事前準備

まずは、ARTY Z7-20のボード情報をダウンロードします。https://github.com/Digilent/vivado-boardsの[↓Code]からDownload ZIPで一式ダウンロードできますので、ダウンロードが完了したら、どこかテンポラリに展開します。

展開したら、vivado-boards-master.zip\vivado-boards-master\new\board_files以下のフォルダ全てを、VivadoのインストールフォルダのVivado\2020.2\data\boards\board_filesに置きます。
arty-z7-20だけでもいいのかもしれませんが、念のためすべてコピーしておきましょう。

次に、必要となるDIGILENTのIPをhttps://github.com/Digilent/vivado-libraryからダウンロードします。こちらは恒久的に使うファイルになりますので、ダウンロードが完了したらどこかにわかりやすいところに展開しておきます。

実際にVivadoを使う

プロジェクトの作成

ボードファイルとIPの準備ができましたら、Vivadoを起動します。
Vivadoが起動したら、Create Projectを選択します。
キャプションを入力できます
その後、projectファイルを作成する場所やproject名(なんでもいい)を入力して「Next>」で進めて、RTL Projectを作成します。
キャプションを入力できます
Add sourceとADD Constraintsは何も入力せず「Next>」を押します。
Default partでは「Boards」を選択して「Arty Z7-20」を選択し、「Next>」を押します。
キャプションを入力できます
最後に確認が表示されるので「Finish」で進めると、少し時間がかかりますがメインウィンドウが表示されます。

追加IPの設定

まず必要となるのは、先ほどダウンロードしたIPをプロジェクトに追加することです。
左上の「Setting」を選択し、表示されるウィンドウの「IP」の中の「Repository」を選択、その後、「+」を押して、先ほどダウンロードしたIPを展開したフォルダを設定します。
設定するのは「vivado-library-master」階層で、その下の「ip」などではありません。
キャプションを入力できます
追加しましたら、「OK」を押してウィンドウを閉じていきます。

ソースを書く

次に、ソースファイルを書きましょう。今回は2つのソースに分けました。
「Add Sources」から「Add or create design sources」を選択し、「Next>」を押します。
キャプションを入力できます

次に「Create File」を押し、「File name」を入力して「OK」を押します。今回は「bmp.v」と「fontdata.v」の2ファイルを作成します。
キャプションを入力できます
2つのファイルを作成したら、「Finish」を押すと、端子定義などを設定する「Define Module」ウィンドウが開きますが、後で丸々コピペするので今は何も入力せずに「OK」で進めてください。

すると、メインウィンドウの「Source」に今作ったファイルが追加されていますので、ツリーを展開して確認してください。ファイルの追加順によっては、表示される階層が変わりますが問題ありません。
キャプションを入力できます
ここで、ファイル名をダブルクリックするとテキストエディタが開いて編集できるようになるので、下記ファイルをコピペします。コピペした後は、保存(ティストエディタウィンドウのフロッピーディスクアイコンを押す)を忘れないでください。もともと何か記載があるかもしれませんが、すべて上書きでコピペで問題ありません。

module bmp(
    CLK,
    data_o,
    hsync_o,
    vsync_o,
    vde_o,
    led_page,
    led_progress
    );
 
input CLK;
output [23:0] data_o;
output hsync_o;
output vsync_o;
output vde_o;
output [7:0] led_page;
output [7:0] led_progress;

reg [23:0] data_o;
reg hsync_o;
reg vsync_o;
reg vde_o;

//Timing
reg [11:0] cnt_x;
always @(posedge CLK) cnt_x <= (cnt_x==2199) ? 0 : cnt_x+1;

reg [10:0] cnt_y;
always @(posedge CLK) if(cnt_x==2199) cnt_y <= (cnt_y==1124) ? 0 : cnt_y+1;

//page counter
reg [2:0] page;
always @(posedge CLK) if((cnt_x==2199)&&(cnt_y==1124)) page<=page+1;

//page led
reg [7:0] led_page;
always @(posedge CLK) 
 if ((cnt_x==0)&&(cnt_y==0)) begin
  if (page==0) led_page<=8'b00000001;
  else if (page==1) led_page<=8'b00000010;
  else if (page==2) led_page<=8'b00000100;
  else if (page==3) led_page<=8'b00001000;
  else if (page==4) led_page<=8'b00010000;
  else if (page==5) led_page<=8'b00100000;
  else if (page==6) led_page<=8'b01000000;
  else if (page==7) led_page<=8'b10000000;
 end

//progress led;
always @(posedge CLK) 
 if (cnt_y==0) led_progress<=8'b00000000;
 else if (cnt_y==125) led_progress<=8'b00000001;
 else if (cnt_y==250) led_progress<=8'b00000011;
 else if (cnt_y==375) led_progress<=8'b00000111;
 else if (cnt_y==500) led_progress<=8'b00001111;
 else if (cnt_y==625) led_progress<=8'b00011111;
 else if (cnt_y==750) led_progress<=8'b00111111;
 else if (cnt_y==875) led_progress<=8'b01111111;
 else if (cnt_y==1000) led_progress<=8'b11111111;

reg [7:0] led_progress;

//bitmap
wire [11:0] pos_x;
wire [10:0] pos_y;
wire pattern;
wire [7:0] pattern_byte;

assign pos_x = cnt_x-280;
assign pos_y = cnt_y-45;

fontdata fontdata(
    .chara(page),
    .index(pos_y[5:2]),
    .bit(pos_x[5:2]),
    .pattern(pattern)
    );

assign pattern_byte=(pattern==1'b1)? 8'hff : 8'h00;
    
//pixel data
always @(posedge CLK)begin
    data_o <= {pattern_byte,pattern_byte,pattern_byte};//rbg
end

//hsync
always @(posedge CLK)begin
    if((cnt_x>=88) && (cnt_x<132))
        hsync_o <= 1'b1;
    else
        hsync_o <= 1'b0;
end

//vsync
always @(posedge CLK)begin
    if((cnt_y>=4) && (cnt_y<9))
        vsync_o <= 1'b1;
    else
        vsync_o <= 1'b0;
end

//Video Data Enable
always @(posedge CLK)begin
    if((cnt_x>=280) && (cnt_y>=45))
        vde_o <= 1'b1;
    else
        vde_o <= 1'b0;
end
   
endmodule
module fontdata(
    chara,
    index,
    bit,
    pattern
    );
    
input [2:0] chara;
input [3:0] index;
input [3:0] bit;
output pattern;

wire [255:0] chara0;
wire [255:0] chara1;
wire [255:0] chara2;
wire [255:0] chara3;
wire [255:0] chara4;
wire [255:0] chara5;
wire [255:0] chara6;
wire [255:0] chara7;
wire [255:0] sel_chara;
wire [15:0] bitpattern;

assign chara0=256'h0000018002400420042008100810081008100810081004200420024001800000;
assign chara1=256'h0000010003000500010001000100010001000100010001000100010007c00000;
assign chara2=256'h000003c00420081008100010001000200040008001000200040008100ff00000;
assign chara3=256'h000003c00420081000100010002001c000200010001008100810042003c00000;
assign chara4=256'h000000c001c0014002400240044004400840084010401ff00040004000e00000;
assign chara5=256'h00000fe008000800080008000bc00c2008100010001000100810042003c00000;
assign chara6=256'h000003c004200810080008000bc00c2008100810081008100810042003c00000;
assign chara7=256'h00000ff008100810002000200040004000800080010001000100010001000000;

assign sel_chara= (chara==0)?chara0 :
                (chara==1)?chara1 :
                (chara==2)?chara2 :
                (chara==3)?chara3 :
                (chara==4)?chara4 :
                (chara==5)?chara5 :
                (chara==6)?chara6 : chara7;
                
assign bitpattern= (index==0)?sel_chara[255:240] :
                (index==1)?sel_chara[239:224] :
                (index==2)?sel_chara[223:208] :
                (index==3)?sel_chara[207:192] :
                (index==4)?sel_chara[191:176] :
                (index==5)?sel_chara[175:160] :
                (index==6)?sel_chara[159:144] :
                (index==7)?sel_chara[143:128] :
                (index==8)?sel_chara[127:112] :
                (index==9)?sel_chara[112:96] :
                (index==10)?sel_chara[95:80] :
                (index==11)?sel_chara[79:64] :
                (index==12)?sel_chara[63:48] :
                (index==13)?sel_chara[47:32] :
                (index==14)?sel_chara[31:16] :sel_chara[15:0];
assign pattern= (bit==0)?bitpattern[15]:
                (bit==1)?bitpattern[14]:
                (bit==2)?bitpattern[13]:
                (bit==3)?bitpattern[12]:
                (bit==4)?bitpattern[11]:
                (bit==5)?bitpattern[10]:
                (bit==6)?bitpattern[9]:
                (bit==7)?bitpattern[8]:
                (bit==8)?bitpattern[7]:
                (bit==9)?bitpattern[6]:
                (bit==10)?bitpattern[5]:
                (bit==11)?bitpattern[4]:
                (bit==12)?bitpattern[3]:
                (bit==13)?bitpattern[2]:
                (bit==14)?bitpattern[1]:bitpattern[0];
endmodule

もう少し綺麗に書けよって気がするソースではありますが、ひとまずこれだけでHDMI出力ができます。

制約ファイルを書く

次に、制約ファイルを書きます。制約ファイルとは、ソースファイルと物理的な回路を結びつけるためのものです。ソースファイル上のどの信号がFPGAのどのPINに出力されるか(すなわち、FPGAボードのどこに出力されるか)とかですね。
ソースファイル同様に、「Add Source」から、今度は「Add or create constraints」を選び、同様にファイルを作成します。
ファイル名はなんでもいいのですが「const.xdc」などにしましょう。
すると、ソースファイル同様に、メインウィンドウの「Source」の「Constraints」の下に追加されますので、ツリーを展開して確認してください。
また、ソースファイル同様に、ファイル名をダブルクリックするとテキストエディタが開きますので、以下コピペしてください。保存も忘れずに。

set_property -dict { PACKAGE_PIN H16    IOSTANDARD LVCMOS33 } [get_ports { clk }]; #IO_L13P_T2_MRCC_35 Sch=SYSCLK
create_clock -add -name sys_clk_pin -period 8.00 -waveform {0 4} [get_ports { clk }];#set

## HDMI TX Signals
#set_property -dict { PACKAGE_PIN G15   IOSTANDARD LVCMOS33 } [get_ports { hdmi_tx_cec }]; #IO_L19N_T3_VREF_35 Sch=HDMI_TX_CEC
set_property -dict { PACKAGE_PIN L17   IOSTANDARD TMDS_33  } [get_ports { hdmi_tx_clk_n }]; #IO_L11N_T1_SRCC_35 Sch=HDMI_TX_CLK_N
set_property -dict { PACKAGE_PIN L16   IOSTANDARD TMDS_33  } [get_ports { hdmi_tx_clk_p }]; #IO_L11P_T1_SRCC_35 Sch=HDMI_TX_CLK_P
set_property -dict { PACKAGE_PIN K18   IOSTANDARD TMDS_33  } [get_ports { hdmi_tx_data_n[0] }]; #IO_L12N_T1_MRCC_35 Sch=HDMI_TX_D0_N
set_property -dict { PACKAGE_PIN K17   IOSTANDARD TMDS_33  } [get_ports { hdmi_tx_data_p[0] }]; #IO_L12P_T1_MRCC_35 Sch=HDMI_TX_D0_P
set_property -dict { PACKAGE_PIN J19   IOSTANDARD TMDS_33  } [get_ports { hdmi_tx_data_n[1] }]; #IO_L10N_T1_AD11N_35 Sch=HDMI_TX_D1_N
set_property -dict { PACKAGE_PIN K19   IOSTANDARD TMDS_33  } [get_ports { hdmi_tx_data_p[1] }]; #IO_L10P_T1_AD11P_35 Sch=HDMI_TX_D1_P
set_property -dict { PACKAGE_PIN H18   IOSTANDARD TMDS_33  } [get_ports { hdmi_tx_data_n[2] }]; #IO_L14N_T2_AD4N_SRCC_35 Sch=HDMI_TX_D2_N
set_property -dict { PACKAGE_PIN J18   IOSTANDARD TMDS_33  } [get_ports { hdmi_tx_data_p[2] }]; #IO_L14P_T2_AD4P_SRCC_35 Sch=HDMI_TX_D2_P
#set_property -dict { PACKAGE_PIN R19   IOSTANDARD LVCMOS33 } [get_ports { hdmi_tx_hpdn }]; #IO_0_34 Sch=HDMI_TX_HDPN
#set_property -dict { PACKAGE_PIN M17   IOSTANDARD LVCMOS33 } [get_ports { hdmi_tx_scl }]; #IO_L8P_T1_AD10P_35 Sch=HDMI_TX_SCL
#set_property -dict { PACKAGE_PIN M18   IOSTANDARD LVCMOS33 } [get_ports { hdmi_tx_sda }]; #IO_L8N_T1_AD10N_35 Sch=HDMI_TX_SDA

## ChipKit Outer Digital Header
set_property -dict { PACKAGE_PIN T14   IOSTANDARD LVCMOS33 } [get_ports { led_page[0]  }]; #IO_L5P_T0_34            Sch=CK_IO0
set_property -dict { PACKAGE_PIN U12   IOSTANDARD LVCMOS33 } [get_ports { led_page[1]  }]; #IO_L2N_T0_34            Sch=CK_IO1
set_property -dict { PACKAGE_PIN U13   IOSTANDARD LVCMOS33 } [get_ports { led_page[2]  }]; #IO_L3P_T0_DQS_PUDC_B_34 Sch=CK_IO2
set_property -dict { PACKAGE_PIN V13   IOSTANDARD LVCMOS33 } [get_ports { led_page[3]  }]; #IO_L3N_T0_DQS_34        Sch=CK_IO3
set_property -dict { PACKAGE_PIN V15   IOSTANDARD LVCMOS33 } [get_ports { led_page[4]  }]; #IO_L10P_T1_34           Sch=CK_IO4
set_property -dict { PACKAGE_PIN T15   IOSTANDARD LVCMOS33 } [get_ports { led_page[5]  }]; #IO_L5N_T0_34            Sch=CK_IO5
set_property -dict { PACKAGE_PIN R16   IOSTANDARD LVCMOS33 } [get_ports { led_page[6]  }]; #IO_L19P_T3_34           Sch=CK_IO6
set_property -dict { PACKAGE_PIN U17   IOSTANDARD LVCMOS33 } [get_ports { led_page[7]  }]; #IO_L9N_T1_DQS_34        Sch=CK_IO7

## Pmod Header JA
set_property -dict { PACKAGE_PIN Y18   IOSTANDARD LVCMOS33 } [get_ports { led_progress[0] }]; #IO_L17P_T2_34 Sch=JA1_P
set_property -dict { PACKAGE_PIN Y19   IOSTANDARD LVCMOS33 } [get_ports { led_progress[1] }]; #IO_L17N_T2_34 Sch=JA1_N
set_property -dict { PACKAGE_PIN Y16   IOSTANDARD LVCMOS33 } [get_ports { led_progress[2] }]; #IO_L7P_T1_34 Sch=JA2_P
set_property -dict { PACKAGE_PIN Y17   IOSTANDARD LVCMOS33 } [get_ports { led_progress[3] }]; #IO_L7N_T1_34 Sch=JA2_N
set_property -dict { PACKAGE_PIN U18   IOSTANDARD LVCMOS33 } [get_ports { led_progress[4] }]; #IO_L12P_T1_MRCC_34 Sch=JA3_P
set_property -dict { PACKAGE_PIN U19   IOSTANDARD LVCMOS33 } [get_ports { led_progress[5] }]; #IO_L12N_T1_MRCC_34 Sch=JA3_N
set_property -dict { PACKAGE_PIN W18   IOSTANDARD LVCMOS33 } [get_ports { led_progress[6] }]; #IO_L22P_T3_34 Sch=JA4_P
set_property -dict { PACKAGE_PIN W19   IOSTANDARD LVCMOS33 } [get_ports { led_progress[7] }]; #IO_L22N_T3_34 Sch=JA4_N

ちなみに、このファイルの元ネタはArty Z7-20ボード用としてDIGILENTで公開されているもので、そこからHDMIとLED制御に必要な個所を抜き出して、信号名を変更したものです。

ブロックデザインを作成する

ブロックデザインというものを作成して、今作ったソースやIP同士を接続します。
「IP INTEGRATOR」の「Create Block Design」を選択します。「Create Block Design」ウィンドウが表示されたら、そのまま「OK」押して閉じます。
キャプションを入力できます

「Diagram」子ウィンドウが表示されたら、「+」を押すとIP選択ウィンドウが出るので、IPを選択して配置します。
今回必要なIPは下記の3つですので、3つ配置します。

  • RGB to DVI Video Encoder (Source) ←今回の本命。RGB(8bit x 3)値をいい感じにHDMIのシリアル形式に変換してくれるIP
  • Clocking Wizard ←基準クロックを作ってくれる
  • Constant ←単なる固定値

キャプションを入力できます

配置したものはドラッグで移動もできるので、適当に見やすいように配置します。

次に、入出力ポートを配置します。
「Diagram」子ウィンドウの何もないところをクリックしたら表示されるメニューから、「Create Port」を選択します。
ちなみに、何かの要素が選択されていると、右クリックメニューの中身が変わるので「Create Port」がない場合は何か選択されていないか確認してください。
表示されるウィンドウでPort nameは「clk」、Directionは「Input」、Typeは「Clock」、Frequencyは「125」を入力し、「OK」を押します。これでクロック入力ポートが配置できました。
キャプションを入力できます
同様に、

  • Port name「led_page」、Direction「Output」、Type「Other」、Create vectorにチェック、from「7」to「0」。ページ表示LED出力端子です
  • Port name「led_progress」、Direction「Output」、Type「Other」、Create vectorにチェック、from「7」to「0」。進捗表示LED出力端子です

を作成します。

そして、HDMI出力ポートも配置します。
ここでは、右クリックメニューの「Create Port」ではなく、「Create Interface Port」を選択します。
「Interface name」は「hdmi_tx」を設定してください。
また、tmdsで検索すると「RAW TMDS~」というのが見つかるので、それを選択してOKします。
キャプションを入力できます

あとは、最後のモジュールとして、先ほど書いたソースを配置します。
右クリックメニューの「Add Module」を選択します。
すると、ソースの一覧(正確にはverilog moduleの一覧)が表示されますので、bmp.vを選択して「OK」を押します(fontdata.vはbmp.vの配下なので、ここでは直接配置しません)。

これで、必要なブロックはすべて配置し終わりました。
次に、いくつかのブロックの設定を変更します。
まず、一番簡単な、「Constant」ブロックの設定を変更してみます。「Constant」ブロックをダブルクリックしてください。
設定変更ウィンドウが表示されますので、「Const Val」を0に変更し、「OK」を押します。これで、固定値「0」を出力するようになりました。
キャプションを入力できます

同様に「Clocking Wizard」ブロックの設定を変更します。
「Output Clocks」タブの中の「clk_out1」を148.5に、「reset」と「locked」のチェックを外して「OK」を押します。
キャプションを入力できます

これで、各ブロックに必要な設定は終わりましたので、ブロック同士を接続します。端子のあたりにマウスポインタを持っていくとペンのようなポインタに変わりますので、それで端子同士を下図のように接続します。
rgb2dviのRGB端子は、「+」を押すと展開します。
キャプションを入力できます

これで、ブロックデザインの作成は終わりました。あとは、こちらのブロックデザインの通りのWrapperを作成します。
「Source」の「Design Sources」の「design_1」を右クリックして出てくるメニューの「Create HDL Wrapper」を選択します。途中で選択肢が出てきますが「OK」で進めます。

完了すると、「design_1_wrapper」が作成されているはずです。
「design_1_wrapper」を右クリックして、「Set as Top」を選択してください。「design_1_wrapper」の左側に「∴」のような記号が付きます。
キャプションを入力できます

あとは、コンパイル~FPGAへの書き込みです。
「Generate Bitstream」をクリックして、出てきた選択肢をOKで進めてしばらく待ちます(結構時間かかります)。
キャプションを入力できます

「Bitstream Generation Completed」ウィンドウが出ると成功です。
「Open Hardware Manager」を選択して、「OK」を押してください。
Failした場合、どこか手順を間違っていると思いますので確認してください。

ちなみに、ARTY Z7-20に使用されているFPGAは本来はスペック上1080/60pのHDMI出力は対応できません。
そのため、タイミングがmetしないWarningが出力されます。
運が悪いと動かない場合もあるのかもしれませんが、DIGILENTのサンプルも同様に1080/60pの出力をしているようなので、まあ問題ないでしょう。
※ARTY Z7-20は1080/60p出力できることを確認して出荷してるとどこかで見たような気がします。

さて、ここまでくると、一旦動作確認が可能です。
LEDは接続していませんが、画像出力はされますので、FPGAボードとPCをUSBでつないで、あとは、FPGAボートと液晶モニターをHDMIケーブルで接続しましょう。
接続したら、「Open Target」を選択し、出てくるメニューから「Auto Connect」を選択します。
キャプションを入力できます

少し待たされた後、「Program Device」が選択可能になるので、選択します。ここで、デバイス名「xc7z020_1」が表示されるので、そのまま選択します。
すると、Program Deviceウィンドウが開き、Bitstream fileを選択できるようになりますが、デフォルトで選択されていますので、そのまま「Program」ボタンを押します。
すると、FPGAにプログラムされ、無事、FPGAボードにつないだ液晶モニターからテストパターンが表示されるはずです。
表示されなければ・・・どこか間違っているので、確認してみてください。念のため別の液晶モニターも試してみた方が良いかもしれません。
※こちらでいくつかの液晶モニターを試したところ問題なく表示されましたが、認識できないビデオキャプチャーがありました。おそらくRGB非対応(YUV専用)入力と思われます。

物理編

次は物理的に作る方です。いったん念のためFPGAボードはPCやHDMIケーブルと外してください。

作るとはいっても、FPGAボードにLEDをたくさんつなぐだけなので、ArduinoのLチカとなんら変わりません。数が多いだけです。
回路図には抵抗を記載していますが、今回は抵抗内蔵LEDを使用したので、実際には抵抗をLEDと別に用意する必要もなく、LEDをつなげるだけです。
LEDと抵抗を別々に用意するのであれば、数百Ωくらいのものでいいでしょう(適当)。
LEDの色も2色あればなんでもいいのですが、回路図通りに緑と黄にすると、この記事の写真と同じなります。
回路図

実際のボードで接続する端子は写真の通りです。
結線個所

完成した様子です。GNDの取り出し個所が上の写真と異なりますがGNDであればどこでも大丈夫ですので気にしないでください。
完成写真

ここまで完成しましたら、またPCとUSBで接続して動作確認してみましょう。ここではとりあえずHDMIはつながなくても大丈夫です。
ボードをいったん取り外した後に再接続した場合は、Vivadoでエラーウィンドウが表示されていると思いますが、エラーウィンドウを閉じた後、もう一度、「Open Target」~「Program Device」をやり直すことで、FPGAボードへ再書き込みができます。
書き込みが完了すると、黄色のLEDが高速で流れていく様子が確認できると思います。
緑のLEDも実際には点滅しているのですが、肉眼で認識できる速度ではないので、並んでいるLEDがLED0~LED7に向かって徐々に暗くなっているように見えるかと思います。

使ってみる

さて、では実際に使ってみましょう。
HDMIで液晶モニターに接続すると、高速で画面が切り替わる画像が表示されていると思います。
あとは、その液晶モニターと、今作った回路のLED部が一緒に写真に写るように配置します。
そして、写真を撮りまくるだけです。1枚でもいいのですが、判別しにくいタイミングで撮れてしまうことがあるので、10枚くらいは撮りましょう。
注意点としては、撮影時はなるべくシャッタースピードを速くしてください。仮にシャッタースピードが1/60秒だと、撮影している間に画面全体が書き換わってしまいますので計測できません。1/1000秒とか1/2000秒とかは必要と思います。
一眼レフとかが用意できなければ、できる限り液晶モニターと周辺を明るくするとシャッタースピードは速くなると思いますが、スマホのカメラなどでは難しいかもしれません。

計測:型番もわからない中華のモバイルモニターの場合

キャプションを入力できます
赤線を境に、画面上部に「5」、下部に「4」が表示されています(赤線よりすぐ上はかろうじて薄く見える程度ですが)。
綺麗に切り替わらず、文字が重なっているのは、シャッタースピードの関係と、液晶パネル自体の応答速度の関係です。
ボード側のパターン表示LED(黄)は、LED5が点灯していますので、ボードは「5」の画像を送信しています。実際、液晶画面も「5」の表示途中ですので、少なくとも遅延値は1フレーム未満ということはわかりますね。
進行度表示LED(緑)はLED0~LED4の5つ点灯しています。つまり「進捗度5/8」ですので、これを元にもう少し詳しく遅延値を求めてみましょう。

進行度表示は、1画面分の送信時間(1/60秒)を0/8~8/8の9分割していますので、1/(60x9)s≒0.00185s=1.85ms(ミリ秒)ごとに進むことになります。これはブランキング期間を含む値です。
図示すると、フレームの送信開始(ブランキング期間を含む全データの最初のデータ)を0秒として、
キャプションを入力できます
となります。
今、進行度は5/8なので、送信開始から9.26ms~11.11ms経っているということになります。これ以上細かい粒度はないので、中央値を取って、10.185±0.925msとしましょう。

続いて、液晶モニター側です。
横1ライン表示するのに必要な時間は、1/(60x1125)=0.0148msです。
文字サイズは縦64pixelですので、文字1段分を表示する時間は、上記を64倍して、約0.948msです。
とすると、同じく送信開始(ブランキング期間を含む全データの最初のデータ)を0秒として、
キャプションを入力できます
となります。
今、10段目まで「5」が表示されていますので、送信開始を基準として、10.15ms程度まで表示していることとなります。

とすると、送信側は10.185ms分送信していて、受信側は10.15ms分まで表示しているので、表示遅延は10.185-10.15=0.035msとなります。
ところで、先ほど送信側分解能の都合で、±0.925msの誤差が出ることはわかっているので、今求めた遅延値は±0.925msの誤差があることになります。
とすると、実際の遅延値は、0.035-0.925ms~0.035+0.925msなので、
-0.89ms ~ 0.96msとなりますが、遅延値が0未満になるわけはないので、0ms~0.96msの遅延値となります。
測定分解能の限界以下になってしまいましたが、このモニターはかなり低遅延のようです。
ただ、「かろうじて見える」ところを判定基準としているので、現実的には体感もう少し遅いでしょうね。

計測:LG 27MU67-Bの場合

27MU67
先ほどと同様に、かろうじて見える程度ですが、上8段に「7」が表示されて、それより下が「6」になっています。
ボード側のパターン表示LEDは、LED7が点灯していますので、ボードは「7」の画像を送信しています。実際、液晶画面も「7」の表示途中ですので、こちらも遅延値は1フレーム未満ということになりますね。
また、進行度表示はLED3まで点灯しています。
こちらも先ほどと同じく、最初のデータの送信開始からの経過時間を計算してみると、先ほどの図から、送信開始から7.41ms~9.26msとなります。こちらも中央値を取って8.335±0.925msとしましょう。
次に、液晶モニター側についても先ほどと同じく計算してみると、8段目まで7が表示されていますので、送信開始を基準として、8.25msまで表示していることとなります。

なので、先ほどと同じく、表示遅延は8.335-8.25±0.925=0.085±0.925msなので、マイナスを切り捨てると、
0ms~1.01msの遅延値となります。
先ほどの中華モニターより数値上は若干遅いですが、測定分解能の限界以下の違いですので、ほぼ同等の遅延と考えられます。
こちらも優秀ですね。

計測:LG 27MU67-Bの2画面モードの場合

というわけで、上記の2台の液晶モニターはどちらも遅延はかなり少ないという結果になり、優秀な液晶モニターであることはわかったのですが、せっかく測定器を作った以上、もっと遅い結果も見たいものです。
なので、おそらく液晶モニター内部でフレームバッファを持っているであろう、「2画面モード」を測定してみました。
2画面モード
今回は、上11段に「2」が表示されているのがわかります。
ボード側のパターン表示LEDは、LED3が点灯していますので、送信中の画像は「3」です。しかし、液晶に表示中の画像は「2」なので、この時点で1フレーム遅延があるのは確実です。予想通りです。
あとは、今までと同様に時間を計算してみます。
進行度表示はLED4まで点灯していますので、「パターン3」を送信し始めてから10.185±0.925ms経過しています。
液晶モニター側については、上11段まで「2」が表示されているので、「パターン2」を表示し始めてから11.10ms経過しています。
なので、表示遅延は10.185-11.10±0.925=ー0.915±0.925msです。ただ、これは1フレームの遅延を考慮していないので、実際には、
1フレーム(1/60s)ー0.915±0.925msです。
1フレームは16.667msですので、16.667ー0.915±0.925msとなり、
15.751±0.925msが、2画面モードの遅延値となります。ほぼ丸々1フレーム分遅延していることになりますね。

改善点

  • もう少し進行度表示の分解能を細かくした方がよかったかなと思いました。LEDを増やしていくだけなので、IOの数が足りれば設計自体は難しくはないですが、さすがにブレッドボードで配線するのは厳しいと思います。

  • 液晶モニターに出力するパターンは、目盛りのようなものもつければ、もっと細かく測定できるとおもいます。

  • 音声も一緒に送信することで、音声出力の遅延も測定することが可能と思います。LEDでの確認はできませんが、HDMIに送信した音声と同じものをFPGAボードの音声出力から出力して、モニターの音声出力をLch、FPGAボードからの音声出力をRchに接続するなどしてPCなどで録音して、LchとRchの遅延差を比較することで測定できるでしょう。
    ついでに、映像の遅延と音声の遅延値を比較することで、映像と音声が本当に同期しているかも確認できるはずです。
    しかし、HDMIに音声を乗せるためには今回使用したDIGILENTのIPではできません。XILINXのIP(有償)だとできそうです。もしくは、自分でserializerを作成する必要があるでしょう。

おわりに

ゲームをするときは低遅延モニターを、というのがゲーマーの一般的な見解ですが、2画面モードはともかくとして、普通にフルスクリーンで使うならこの程度なら気にしなくてもいいかな?と個人的には思いました。
PCモニターではなく、高画質化エンジンなどがてんこ盛りな高級テレビとかであれば、もっと遅いのかもしれませんね。
商品レビューが好きなゲーマーブロガーさんとか、ゲーミングに力を入れているPCショップさんとかはこういうネタ好きそうですよね。是非色々なモニターで試してもらいたいです。
4kや144Hzモニターなども試してみたいところではありますが、HDMI2対応のFPGAボードはまだ非常に高価なので手が出せません。基本的には同じ原理で可能とは思います。

使用上の注意

液晶は高速で数パターンの映像を切り替えて表示していると、画面焼けに似たような残像が残る場合があるようです(IPSだけかも?)。実験は短時間で終わらせましょう。
ちなみに、残像が残った場合でも、数時間くらい放置しておくと直るみたいです(最初は焦りました)。
※これはこのハードウェアに限ったことではなく、PCなどからでも同様の画像を表示しても発生します。

謝辞

以下のページを参考にさせていただきました。有益な情報を整理していただけたことを感謝申し上げます。
FPGAでHDMIから画像出力!フリーのIPを使ってみた
https://misoji-engineer.com/archives/fpga-hdmi.html

2
lyricalmagicalのアイコン画像
電子デバイスはとってもりりかるなの
  • lyricalmagical さんが 2021/02/05 に 編集 をしました。 (メッセージ: 初版)
  • Opening
    lyricalmagicalのアイコン画像 lyricalmagical 2021/03/16

    こんなめんどくさいことしなくても、フォトトランジスタで液晶画面拾えばよかったんじゃ・・・

    0 件の返信が折りたたまれています
  • lyricalmagical さんが 2021/12/29 に 編集 をしました。
  • lyricalmagical さんが 2022/05/19 に 編集 をしました。
  • Opening
    grafiのアイコン画像 grafi 2023/01/12

    楽しく拝読させていただきました。ローリングシャッターの影響はないでしょうか? 画像一枚をスキャンするのに 10ms 程度かかるようですので影響が出そうに思いました。カメラを横向きにして撮影すればいいかもしれないです。

    lyricalmagicalのアイコン画像 lyricalmagical 2023/01/13

    コメントありがとうございます。
    確かにご指摘通りセンサーの走査時間が十分かを考慮する必要がありますね。
    実験に使ったのはフォーカルプレーンシャッター機ですが、ストロボ連動が1/200秒までなので、おそらくシャッターが動ききるのに1/200秒程度はかかっていそうです。
    むしろ一眼やミラーレスより、最近のスーパースロー動画が撮影できるスマホとかのほうが、走査時間が十分に短い保証ができるかもしれませんね。

    まあ、ディスプレイにフォトトランジスタなどの光センサーを貼り付けるのが確実ではありますが。

    1 件の返信が折りたたまれています
ログインしてコメントを投稿する