ZYBO (Zynq) でHDMI出力をする

# この記事について
ZYBO (Z7-20) でDDR上の画像データをHDMI出力するための方法をまとめます。初代ZYBOでVGA出力する方法は色々と見つかったのですが、HDMI出力に関してはあまり見当たらなかったので、まとめることにしました。

基本的には、「FPGAパソコンZYBOで作る Linux I/Oコンピュータ」の第2部 第1章で記載されているVGA出力方法と、Digilent社提供のrgb2dvi IPを組み合わせているだけで、特に新規性はありません。このIPはMITライセンスなので、遠慮なく使用することにします。一方VGA出力方法の手順は、ここで記載してしまうと本と全く同じ内容（パクり）になってしまうので、ある程度省略します(詳細を知りたい方は、本をご購入ください)。完成形の回路(ブロックデザイン)自体は、ネットでも同様のものがいくつかあったので、イメージをつかむためにも全体図だけは載せます。

![09.jpg](https://qiita-image-store.s3.amazonaws.com/0/214268/709e68e6-ec95-2a10-a8a7-9d44331d93a2.jpeg)

## 「FPGAパソコンZYBOで作る Linux I/Oコンピュータ」の第2部 第1章 との差分
- 出力がHDMIである
- ピクセルクロックのソースには、PL側で外部(K17)から入力されるクロック(125MHz)を使用する
	- 本だとaclk(PSのFCLK_CLK0)を使用していたが、分けることにした。
- Verilog実装不要。IP INTEGRATOR上のGUI操作で完結させる
	- VGA出力だと一部の信号をVerilogで加工していたが、HDMIだと不要
- 制御コード(C アプリケーション)でVDMAを制御する際に、ちゃんとドライバAPIを使用する
	- 本のサンプルコードだと、レジスタ直叩きだったが、ドライバAPIを使用する
	- (でも、将来的にLinuxからUIOで制御することを考えたら、レジスタ直叩きの方が逆に移植性が高いのかも。。。)

## 環境
- 開発用PC: Windows 10 64-bit 
	- Vivado 2017.4 WebPACKライセンス
	- Xilinx SDK 2017.4
- ターゲットボード: ZYBO (Z7-20) 

## 事前知識
VivadoとXilinx SDKの簡単な使い方は分かっているものとします。下記記事を参考にしてください。

- ZYBO (Zynq) 初心者ガイド
	- [1回目: 開発環境の準備](https://qiita.com/take-iwiw/items/966f252f6ca954aff08b)
	- [2回目: Hello Worldプロジェクト](https://qiita.com/take-iwiw/items/24a8a94741fdbb80f62a)
	- [3回目: PSのGPIOでLチカ](https://qiita.com/take-iwiw/items/39d2ed00fa88a1204fce)
	- [4回目: PLのAXI GPIOでPSからLチカ](https://qiita.com/take-iwiw/items/a3b935a4e4dbd41593e1)

# ハードウェアの作成
## プロジェクトの作成
Vivado上で、適当なプロジェクト(例. project_hdmi_out)を作成します。

## Digilentライブラリの追加
Digilent社提供のIPライブラリを使用できるようにします。
https://github.com/Digilent/vivado-library をダウンロードして、適当なディレクトリにコピーしてください。Vivadoのワークフォルダや、先ほど作成したプロジェクトのフォルダと同じ階層がよろしいと思います。

Flow Navigator -> PROJECT MANAGER -> Settings、IP -> Repositoryで、上記フォルダを指定して追加します。

![01.jpg](https://qiita-image-store.s3.amazonaws.com/0/214268/79fcbbc5-226e-d9c5-cf49-db135310c5d3.jpeg)

## IP Integratorでブロックデザインを作る
IP INTEGRATOR -> Create Block Designで、適当なブロックデザイン (例. design_1)を作ります。「FPGAパソコンZYBOで作る Linux I/Oコンピュータ」の第2部 第1章に記載されている通りに、各IPを配置、設定、配線していきます。
その後、rgb2dvi(RGB to DVI Video Encoder (Source))を追加します。

v_axi4s_vid_out_0 (AXI4-Stream to Video Out)の出力 (vid_io_out)は、本では出力ポートになっていますが、以下のようにrgb2dviに接続します。そして、rgb2dviの出力(TMDS)を出力ポートにします。また、ピクセルクロックのソースクロックも、外部から入力することにします。(本だとaclkに接続)

![02.jpg](https://qiita-image-store.s3.amazonaws.com/0/214268/07f4c929-7e3e-b1fa-20ad-96517f214cae.jpeg)

全体は以下のようになります。見やすくするために、HDMI出力に関する部分は階層化(hdmi_ddr_out)しています。

![03.jpg](https://qiita-image-store.s3.amazonaws.com/0/214268/ea71eb21-78ca-3e8b-db34-c90808fdc549.jpeg)

hdmi_ddr_outの部分
![04.jpg](https://qiita-image-store.s3.amazonaws.com/0/214268/85336d67-2a41-c84b-927e-85051497d2e4.jpeg)


### 重要な設定: rgb2dvi
使用するピクセルクロックに応じて、rgb2dviのプロパティを変更する必要があります。後述しますが、ピクセルクロックは使用する画サイズとフレームレートによって変わります。今回は本の通りに720pにしたので、ピクセルクロックは74.25MHzになります。そのため、rgb2dviをダブルクリックして、以下のようにTMDS clock rangeを設定します。

![05.jpg](https://qiita-image-store.s3.amazonaws.com/0/214268/1481391a-cb6d-4a73-4a95-83cb629293b0.jpeg)

これをやらないと、以下のようなエラーが出ます。

```txt:エラー内容
[DRC PDRC-34] MMCM_adv_ClkFrequency_div_no_dclk: The computed value 250.000 MHz (CLKIN1_PERIOD, net FCLK_CLK0) for the VCO operating frequency of the MMCME2_ADV site MMCME2_ADV_X1Y0 (cell video_sys_i/rgb2dvi_0/U0/ClockGenInternal.ClockGenX/GenMMCM.DVI_ClkGenerator) falls outside the operating range of the MMCM VCO frequency for this device (600.000 - 1200.000 MHz). The computed value is (CLKFBOUT_MULT_F * 1000 / (CLKINx_PERIOD * DIVCLK_DIVIDE)). Please run update_timing to update the MMCM settings. If that does not work, adjust either the input period CLKINx_PERIOD (20.000000), multiplication factor CLKFBOUT_MULT_F (5.000000) or the division factor DIVCLK_DIVIDE (1), in order to achieve a VCO frequency within the rated operating range for this device.

[DRC AVAL-46] v7v8_mmcm_fvco_rule1: The current computed target frequency, FVCO, is out of range for cell video_sys_i/rgb2dvi_0/U0/ClockGenInternal.ClockGenX/GenMMCM.DVI_ClkGenerator. The computed FVCO is 371.250 MHz. The valid FVCO range for speed grade -1 is 600MHz to 1200MHz. The cell attribute values used to compute FVCO are CLKFBOUT_MULT_F = 5.000, CLKIN1_PERIOD = 13.46801, and DIVCLK_DIVIDE = 1 (FVCO = 1000 * CLKFBOUT_MULT_F/(CLKIN1_PERIOD * DIVCLK_DIVIDE)).
This violation may be corrected by:
  1. The timer uses timing constraints for clock period or clock frequency that affect CLKIN1 to set cell attribute CLKIN1_PERIOD, over-riding any previous value. This may already be in place and, if so this violation will be resolved once Timing is run.  Otherwise, consider modifying timing constraints to adjust the CLKIN1_PERIOD and bring FVCO into the allowed range.
  2. In the absence of timing constraints that affect CLKIN1, consider modifying the cell CLKIN1_PERIOD to bring FVCO into the allowed range.
  3. If CLKIN1_PERIOD is satisfactory, modify the CLKFBOUT_MULT_F or DIVCLK_DIVIDE cell attributes to bring FVCO into the allowed range.
  4. The MMCM configuration may be dynamically modified by use of DRP which is recognized by an ACTIVE signal on DCLK pin.
```

### 重要な設定: ブロックデザインを階層化した場合
今回僕は、HDMI出力に関する部分は階層化(hdmi_ddr_out)しました。そのため、自動接続機能がうまく動きませんでした。不安な方はフラットな状態でIPを配置していった方がいいと思います。また、階層化していると、axi_vdma(AXI Video Direct Memory Access)がマスターであるHP0のアドレスが自動的に設定されませんでした。自分でAddress Editorから以下のように設定してあげる必要がありました。これも、フラットなデザインで設計している場合は不要です。

![06.jpg](https://qiita-image-store.s3.amazonaws.com/0/214268/ad322c51-7d5e-1e92-7648-21604a832fd7.jpeg)

これをやらないと、以下のようなエラーが発生しました。

```txt:エラー内容
[BD 41-703] Peripheral </processing_system7_0/S_AXI_HP0/HP0_DDR_LOWOCM> is mapped into master segment </processing_system7_0/Data/SEG_processing_system7_0_HP0_DDR_LOWOCM>, but there is no path between them. This is usually because an interconnect between the master and the peripheral has become misconfigured. Check and reconfigure the interconnect, or delete the master segment.
```

## ハードウェアを出力する
ブロックデザインが出来たら、Generate Output ProductsとCreate HDL Wrapperします。Create HDL Wrapperする際に、本では、"Copy generated wrapper to allow user edits"を選んで、Topモジュールは自分でverilogコードで編集していました。が、今回は不要なので、"Let Vivado manage wrapper and auto-update"を選んで、自動生成されたdesign_1_wrapper.vをそのまま使います。

![07.jpg](https://qiita-image-store.s3.amazonaws.com/0/214268/d28d6845-26b5-ff87-695a-086690587793.jpeg)


その後、RTL_ANALYSISのI/O Portsタブ上で、出力ピンを割り当てていきます。HDMIポートには、TMDS_33を使用します。また、設定するのはp側だけで、n側は自動的に割り当てられます。

![08.jpg](https://qiita-image-store.s3.amazonaws.com/0/214268/ddc2fee6-7f5d-bb7a-2cb7-c633c3beff65.jpeg)

最後に、Generate Bitstreamと、Export Hardwareをして、ビットストリーム付きのhdfを作成します。


# 制御ソフトウェアの作成
## プロジェクトの作成
Vivado上でそのまま、Launch SDKして、Xilinx SDKを起動します。
standaloneプラットフォームのCアプリケーションプロジェクトを作成します。(例. hdmi_out)
ソースコード全文は以下になります。ターミナルから、`a`、`b`を入力するたびに2つの画像面(単色とカラーバー)が切り替わります。また、HDMI制御(Video DMAやVideo Timing Control)に関するところは別途videoOut.c/hにまとめています。

```c:helloworld.c
#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"
#include "sleep.h"
#include "xparameters.h"
#include "xil_cache.h"
#include "common.h"
#include "videoOut.h"

#define IMAGE_WIDTH  1280
#define IMAGE_HEIGHT 720
#define IMAGE_STRIDE (IMAGE_WIDTH * 3)
//#define IMAGE_STRIDE 0x1000
#define IMAGE_BUFFER_A  0x08000000
#define IMAGE_BUFFER_B  0x09000000

void createTestDrawBuffers()
{
	// todo: Pixel format seems GBR, for some reasons...
	Xil_DCacheDisable();
	/* Create buffer A as solid RED color image */
	for (int v = 0; v < IMAGE_HEIGHT; v++) {
		volatile uint8_t *ptr = (volatile uint8_t*)(IMAGE_BUFFER_A + (IMAGE_STRIDE * v));
		for (int h = 0; h < IMAGE_WIDTH; h++) {
			*ptr++ = 0xFF;  *ptr++ = 0x00; *ptr++ = 0x00;
		}
	}
	/* Create buffer B as color bar */
	for (int v = 0; v < IMAGE_HEIGHT / 2; v++) {
		volatile uint8_t *ptr = (volatile uint8_t*)(IMAGE_BUFFER_B + (IMAGE_STRIDE * v));
		for (int h = 0 * IMAGE_WIDTH / 3; h < 1 * IMAGE_WIDTH / 3; h++) { *ptr++ = 0xFF;  *ptr++ = 0x00; *ptr++ = 0x00; }
		for (int h = 1 * IMAGE_WIDTH / 3; h < 2 * IMAGE_WIDTH / 3; h++) { *ptr++ = 0x00;  *ptr++ = 0xFF; *ptr++ = 0x00; }
		for (int h = 2 * IMAGE_WIDTH / 3; h < 3 * IMAGE_WIDTH / 3; h++) { *ptr++ = 0x00;  *ptr++ = 0x00; *ptr++ = 0xFF; }
	}
	for (int v = IMAGE_HEIGHT / 2; v < IMAGE_HEIGHT; v++) {
		volatile uint8_t *ptr = (volatile uint8_t*)(IMAGE_BUFFER_B + (IMAGE_STRIDE * v));
		for (int h = 0 * IMAGE_WIDTH / 3; h < 1 * IMAGE_WIDTH / 3; h++) { *ptr++ = 0xFF;  *ptr++ = 0xFF; *ptr++ = 0xFF; }
		for (int h = 1 * IMAGE_WIDTH / 3; h < 2 * IMAGE_WIDTH / 3; h++) { *ptr++ = 0x88;  *ptr++ = 0x88; *ptr++ = 0x88; }
		for (int h = 2 * IMAGE_WIDTH / 3; h < 3 * IMAGE_WIDTH / 3; h++) { *ptr++ = 0x00;  *ptr++ = 0x00; *ptr++ = 0x00; }
	}
	Xil_DCacheEnable();
}

int main()
{
	init_platform();
	LOG("Hello World\n\r");

	createTestDrawBuffers();

	/* Initialize HDMI OUT */
	videoOut_init(XPAR_AXIVDMA_0_DEVICE_ID, XPAR_VTC_0_DEVICE_ID, IMAGE_WIDTH, IMAGE_HEIGHT, 3, IMAGE_STRIDE);
	videoOut_setSrcAddress(IMAGE_BUFFER_A);
	videoOut_start();

	/* control to switch buffer */
	while(1) {
		char c = getchar();
		switch (c) {
		case 'a':
			videoOut_stop();
			videoOut_setSrcAddress(IMAGE_BUFFER_A);
			videoOut_start();
			break;
		case 'b':
			videoOut_stop();
			videoOut_setSrcAddress(IMAGE_BUFFER_B);
			videoOut_start();
			break;
		default:
			LOG("none\n");
		}
	}

	cleanup_platform();
	return 0;
}
```

```c:common.h
#ifndef SRC_COMMON_H_
#define SRC_COMMON_H_

#include <stdint.h>

/* common return code */
#define RET_OK       0x00000000
#define RET_NO_DATA  0x00000001
#define RET_TIMEOUT  0x00000002
#define RET_ERR      0x80000001
#define RET_ERR_XDRV 0x80000002	// Error related to Xilinx Driver

/* LOG macros */
#define LOG(str, ...)   {xil_printf("\x1b[39m"); xil_printf("[%s:%d] " str, __FILE__, __LINE__, ##__VA_ARGS__);}
#define LOG_W(str, ...) {xil_printf("\x1b[33m"); xil_printf("[WARNING %s:%d] " str, __FILE__, __LINE__, ##__VA_ARGS__);}
#define LOG_E(str, ...) {xil_printf("\x1b[31m"); xil_printf("[ERROR %s:%d] " str, __FILE__, __LINE__, ##__VA_ARGS__);}

#endif /* SRC_COMMON_H_ */
```

```c:videoOut.h
#ifndef SRC_VIDEOOUT_H_
#define SRC_VIDEOOUT_H_

int videoOut_init(uint16_t vtcDeviceId, uint16_t vdmaDeviceId, int width, int height, int bytePerPixel, int stride);
int videoOut_setSrcAddress(uint32_t address);
int videoOut_start();
void videoOut_stop();

#endif /* SRC_VIDEOOUT_H_ */
```

```c:videoOut.c
#include "common.h"
#include "xparameters.h"
#include "xvtc.h"
#include "xaxivdma.h"
#include "videoOut.h"

static XVtc	s_vtc;
static XAxiVdma s_axiVdma;

int videoOut_init(uint16_t vtcDeviceId, uint16_t vdmaDeviceId, int width, int height, int bytePerPixel, int stride)
{
	int status;
	XVtc_Config *configVtc;
	XAxiVdma_Config *configVdma;

	/*** Initialize VTC ***/
	configVtc = XVtc_LookupConfig(vtcDeviceId);
	if (!configVtc){
		LOG_E("XVtc_LookupConfig: %d\n", vtcDeviceId);
		return RET_ERR_XDRV;
	}
	status = XVtc_CfgInitialize(&s_vtc, configVtc, configVtc->BaseAddress);
	if (status != (XST_SUCCESS)) {
		LOG_E("XVtc_CfgInitialize: %d\n", status);
		return RET_ERR_XDRV;
	}


	/*** Initialize VDMA ***/
	configVdma = XAxiVdma_LookupConfig(vdmaDeviceId);
	if (!configVdma){
		LOG_E("XAxiVdma_LookupConfig: %d\n", vdmaDeviceId);
		return RET_ERR_XDRV;
	}

	status = XAxiVdma_CfgInitialize(&s_axiVdma, configVdma, configVdma->BaseAddress);
	if (status != XST_SUCCESS) {
		LOG_E("XAxiVdma_CfgInitialize: %d\n", status);
		return RET_ERR_XDRV;
	}

	XAxiVdma_DmaSetup ReadCfg;
	ReadCfg.VertSizeInput = height;
	ReadCfg.HoriSizeInput = width * bytePerPixel;
	ReadCfg.Stride = stride;
	ReadCfg.FrameDelay = 1;		/* 0 or 1 */
	ReadCfg.EnableCircularBuf = 1;
	ReadCfg.EnableSync = 1;  /* Gen-Lock */
	ReadCfg.PointNum = 0;
	ReadCfg.EnableFrameCounter = 0; /* Endless transfers */
	ReadCfg.FixedFrameStoreAddr = 0; /* We are not doing parking */
	status = XAxiVdma_DmaConfig(&s_axiVdma, XAXIVDMA_READ, &ReadCfg);
	if (status != XST_SUCCESS) {
		LOG_E("XAxiVdma_DmaConfig: %d\n", status);
		return RET_ERR_XDRV;
	}
}

int videoOut_setSrcAddress(uint32_t address)
{
	UINTPTR FrameStoreStartAddr[1];
	FrameStoreStartAddr[0] = address;
	int status = XAxiVdma_DmaSetBufferAddr(&s_axiVdma, XAXIVDMA_READ, FrameStoreStartAddr);
	if (status != XST_SUCCESS) {
		LOG_E("XAxiVdma_DmaSetBufferAddr: %d\n", status);
		return RET_ERR_XDRV;
	}
}

int videoOut_start()
{
	XVtc_Enable(&s_vtc);

	/* Start the Read channel of VDMA */
	int status = XAxiVdma_DmaStart(&s_axiVdma, XAXIVDMA_READ);
	if (status != XST_SUCCESS) {
		LOG_E("XAxiVdma_DmaStart: %d\n", status);
		return RET_ERR_XDRV;
	}
}

void videoOut_stop()
{
	XAxiVdma_DmaStop(&s_axiVdma, XAXIVDMA_READ);
	XVtc_Disable(&s_vtc);
}


void videoOut_dumpStatus()
{
	XAxiVdma_DmaRegisterDump(&s_axiVdma, XAXIVDMA_READ);
}
```


## 実行する
ビットストリームファイルを書き込んだ後に、上記プログラムをビルド、Runします。すると、以下のようにHDMIディスプレイに指定したバッファー面の画像データが出力されます。

![09.jpg](https://qiita-image-store.s3.amazonaws.com/0/214268/c980a535-9674-e42f-9b28-69c8dbec13b7.jpeg)


今回は、1280x720@60pフォーマット固定にしました。PC用のちゃんとしたディスプレイだと、それに合わせて良い感じに引き延ばしたりして全体を表示してくれます。上の写真のディスプレイは、以前ラズパイ用に買った7inch (1080 x 600)のしょぼいディスプレイなので、左上だけが切り取られて表示されています。

### メモ
- 1ピクセル当たり3色で、24bit(3Byte)になります。色の並びはRGB(またはBGR?)のはずなのに、なぜかGBRになっていました。
	- 24-bit RGBがワード単位にパッキングされたため?
	- 斜線を引いたらちゃんとまっすぐな線になったので、ピクセルの位置がずれているとかでは無さそう。単に色が違うだけ。配線間違えてるだけ?
- Cアプリケーションを再度Runするときには、ボード全体をリセットして、再度ビットストリームの書き込みからやる必要がある。
	- どこかのバッファが詰まっているとか、IPがエラーになっているのが理由だと思われます。リセットする方法があるはずだけど。。。要調査

# 補足説明
## 画像フォーマットとピクセルクロックの関係
今回は本に倣い、デフォルト設定のままの720pを使用しました。本だと、当然のようにピクセルクロック=74.25MHzを使用していましたが、その関係について説明します。

ビデオ出力のHsyncやVsyncといったタイミングを決めるのは、VTC (Video Timing Controller)になります。こいつに「720p」とか設定するだけで、良い感じに同期信号を生成してくれます。また、ブランキングも適切に含めてくれます。

![10.jpg](https://qiita-image-store.s3.amazonaws.com/0/214268/cc6f18e5-a958-44e4-36c0-5e6d6fcc489e.jpeg)

上図のように、720pのときには、Active Size = 1280 x 720。 Frame Size = 1650 x 750 と設定されます。また、HBlanking、VBlanking内に適切にHSync、VSync信号を生成してくれます。これを図にしたものが以下になります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214268/9baaff46-6ee2-1e9b-2b2d-4ed0155fb868.png)

例えば、これを60pで出力したい場合には、1650x750ピクセルを1秒間に60回出力する必要があります。結果として、

__ピクセルクロック = 1650 x 750 x 60 = 74.25 [MHz]__

となります。


# 成果物
https://github.com/take-iwiw/ZYBO_HDMI_OUT
↑Vivadoプロジェクトと、XSDK用のCアプリケーションが入っています。
~~(IP化がうまくできなかったので、プロジェクトそのまま)~~ ⇒ IP化じゃないけど、File -> Export -> Export Block Design (Automatically create top design)でブロックデザインを出力できた。

Vivado 2017.4なら、以下手順でブロックデザインを取り込めます。

- 新規Vivadoプロジェクトを作る (または、既存プロジェクトを開く)
- DigilentのIPライブラリ (https://github.com/Digilent/vivado-library) をダウンロードして、IP Repositoryに追加する (追加するのはip/rgb2divと、if/tmdsだけでもOKです。)
- https://github.com/take-iwiw/ZYBO_HDMI_OUT/blob/master/design_mem2hdmi.tcl をダウンロードして、適当なフォルダ (例. c:\work) にコピー
- Vivado画面下部のTcl Consoleで以下コマンドを実行
    - `cd c:/work`
    - `source design_mem2hdmi.tcl`

__Vivado 2017.4以外のバージョンだとうまく取り込めない可能性があります__
__Tcl Consoleでのパス指定では、円マーク(`\`)ではなくスラッシュ(`/`)を使います__

# 資料
- 「FPGAパソコンZYBOで作る Linux I/Oコンピュータ」 岩田 利王/横溝 憲治 ／ 共著
- http://www.hdmi-navi.com/tmds/
- https://www.xilinx.com/support/documentation/ip_documentation/axi_vdma/v6_2/pg020_axi_vdma.pdf

## IPの組み合わせではなく、自分でHDMI出力したい場合は、以下が参考になるかと思います。
- http://www.fpga4fun.com/HDMI.html
- https://qiita.com/cyebu1103/items/c8682b3799d959a14b38