はじめに
今回は、友人の研究を手伝うついでに色々コードを書いたので備忘録がてら記録してこうと思います。ソースコード自体はGitHubにあげていますので自由に扱ってください。ちなみに、ソースコードに間違いがあるかもしれないので、この記事を見て、間違いに気づかれた方が指摘してくださることを願っています。
目的
友達の手伝いということもあり、Pynq-Z1を使ってHDMIを出力する。
よって準備物は、Pynq-Z1とHDMIケーブルくらいです。あと、実機にbitstreamを載せるためのVivadoくらいですかね。
あとは、digilent社が提供してくださっているIPコアの中にあるrgb2dviというもの。
これだけで一応HDMI出力はできます。
#参考サイト
今回は実装に際して、このサイトを参考にしました。このサイトでは、制御タイミングがわかりやすく説明してあるので、初めての僕でもわかりやすかった。どこかに日本語に訳されたサイトもあったと思うが、見つけるのがめんどくさかったため、英語で我慢してください。
#実装
まずは、Topモジュールから作成する今回はHDMIを使って映像だけの出力になるので、
TMDSデータと、クロックを作動で送り出すインターフェイスとなる。
入力は、Pynqで生成される125MHzのクロックのみで大丈夫だろう。
module HDMI_Top(
input wire sysclk,
input wire rst,
output wire hdmi_tx_clk_n,
output wire hdmi_tx_clk_p,
output wire [2:0] hdmi_tx_d_n,
output wire [2:0] hdmi_tx_d_p
);
一応Topモジュールはこんな感じでいいと思います。(Pynq)制約ファイルはこれに合わせて作ってください。Pynq用の制約ファイルもgithubにおいています。
次に出力データのタイミングを作るモジュールを作成します。これは、パラメータですぐに変更できるように実装します。
//Horizontal Timing
parameter H_ACTIVE_PIXEL =10'd 640;
parameter H_FRONT_PORCH =10'd 16;
parameter H_SYNC_WIDTH =10'd 96;
parameter H_BACK_PORCH =10'd 48;
parameter H_TOTAL =10'd 800;
parameter H_WIDTH =10'd 11;
//Vertical Timing
parameter V_ACTIVE_LINE =10'd 480;
parameter V_FRONT_PORCH =10'd 10;
parameter V_SYNC_WIDTH =10'd 2;
parameter V_BACK_PORCH =10'd 33;
parameter V_TOTAL =10'd 525;
parameter V_WIDTH =10'd 10;
bit幅は適宜変更してください。今回はめんどくさかったので固定にしています。
参考サイトを確認すると、パラメータが何の値を表しているのかわかると思います。
タイミング制御モジュールのインターフェースは以下のようになります。
module HDMI_Timing(
input wire clk,
input wire rst,
output wire hsync, //水平同期信号
output wire vsync, //垂直同期信号
output wire de //データイネーブル信号
);
タイミング制御はモジュール内でカウンタを用いることで実装しています。
reg [H_WIDTH-1:0] h_cnt;
reg [V_WIDTH-1:0] v_cnt;
output信号線に流すデータは以下のようになります。
assign hsync = !(h_cnt < H_ACTIVE_PIXEL+H_FRONT_PORCH+H_SYNC_WIDTH
&& h_cnt > H_ACTIVE_PIXEL+H_FRONT_PORCH-1);
assign vsync = !(v_cnt < V_ACTIVE_LINE+V_FRONT_PORCH+V_SYNC_WIDTH
&& v_cnt > V_ACTIVE_LINE+V_FRONT_PORCH-1);
assign de = h_cnt < H_ACTIVE_PIXEL && v_cnt < V_ACTIVE_LINE;
ここは参考にしたサイトを確認して作りました。
カウンタのはgithubに落ちているコード見てもらえばすぐにわかると思います。ので、めんどくさいのでここに書くのはやめときます。
次からはvivado内での処理に移ります。
先ほど作成した2つのモジュールと制約ファイルを用いて新規プロジェクトを作成します。
rgb2dviコアは内部でMMCMやPllを用いて、シリアルクロックを生成してくれますが、今回の友人の要望の640x480のサイズを出力するのに必要な、25MHzがデフォルトでは出力されない設計でした。IP内部の設定を変更して、25MHzをレンジに入れてあげることもできますが、(私個人では設定を変更した)簡単に作るために、シリアルクロックと、ピクセルクロックを外部モジュールで生成することがベストではないでしょうか。資源は食いますが、まぁ簡単なので。。。
外部クロックモジュールは、vivadoのIPカタログからClocking_Wizardを使います。
primitiveはMMCMを選択し、Primary clockはPynq-Z1の125MHZを入力してあげます。
OutputClockの設定では、ピクセルクロックの値と、シリアルクロックの値を入力してあげてください。
シリアルクロックはピクセルクロックの5倍の値を入力します。
そして、トップモジュールにインスタンス化してあげれば外部クロックモジュールの組み込みは終了です。
今回はlockedを内部モジュールのリセットに用いています。
clk_wiz_0 instance_name
(
// Clock out ports
.clk_out1(pixelclk), // output clk_out1
.clk_out2(serialclk), // output clk_out2
// Status and control signals
// .resetn(rst), // input resetn
.locked(sysrst), // output locked
// Clock in ports
.clk_in1(sysclk)
);
最後にrgb2dviモジュールのインスタンス化をする必要があります。これは差動信号に変換されたクロックとデータが出力されます。
rgb2dvi_0 #(
.KGenerateSerialClk(0). //外部でクロック信号を生成する場合
//.KGenerateSerialClk(1) //内部でクロック信号を生成する場合
)rgb2dvi_1(
.TMDS_Clk_p (hdmi_tx_clk_p),
.TMDS_Clk_n (hdmi_tx_clk_n),
.TMDS_Data_p(hdmi_tx_d_p),
.TMDS_Data_n(hdmi_tx_d_n),
.aRst(!sysrst),
.vid_pData(24'hff0000),//ここに表示したいRGB値データを入力
.vid_pVDE(de),
.vid_pHSync(hsync),
.vid_pVSync(vsync),
.PixelClk(pixelclk), //外部クロックモジュールで生成したピクセルクロック
.SerialClk(serialclk) //外部クロックモジュールで生成したシリアルクロック
);
一応これで誰でもできるはずです。最後はvivadoのぽちぽちゲーなので終わりです。
あくまで個人の備忘録としてみてください。
#まとめ
割と簡単にできた。
あんまり長いソースコードを書くのが嫌いなので、パラメータで使う解像度で変更するようにしました。基本的にはトップモジュールと、クロックモジュールを変更してあげるだけで対応はできるかと思います。
今回感じたのはdigilentのIPはRTLでの提供なのでとても親切ってことですかね。使い方はVivadoで
ブロックモジュールにするのもあり。要望があれば、追記します。
#最後に
今回は、いくつかのモニタサイズをPynqを用いて表示させてみたが、モニターによっては640x480を表示できなかったりとバグなのか、モニタの仕様なのかよくわからないが表示できなかったことがあった。モニタの仕様書を探せなかったので、今後はなぜ表示できなかったのかの解決をしつつ、他のプロジェクトを進めることをしようと思う。