NSLで回路を作ろう#05: SDRAMを使って画像表示 


[【改訂2版】FPGAボードで学ぶ 組込みシステム開発入門［Intel FPGA編］][text](以下、テキスト)の第8章に載っているSDRAMを使用したグラフィック表示回路をハードウェア記述言語のNSLで作ってみよう。

# 動かした様子

VGAケーブルでFPGAボードとディスプレイを接続して画像を表示した。

![DE1-SoCtoVGA.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/86c8aeb6-a1be-c6b8-4e0b-bc6943225bfe.jpeg)


使用したもの
- Quartus Prime Lite 18.1
- FPGAボード: DE1-SoC
- [[フリー写真] 色とりどりの四角形の色 - パブリックドメインQ：著作権フリー画像素材集](https://publicdomainq.net/colorful-square-0055362/)
	- この画像を `640`x`480` に縮小し、NiosIIのシステムで扱えるようにGIMPで変換(参考:[テキストPP304-307][text])
 

# 1. 回路について

[前記事][prev]ではVGA出力を扱った。
- FPGAボード上のメモリの容量が足りないので、表示は文字のみ。

今回使っているボード(DE1-SoC)にはSDRAMが搭載されている。
- SDRAMを扱うためのコントローラ回路を載せれば使用できる。
- 増えた容量を使い、VGA出力に画像を表示することを目標として回路を作る。

SDRAMコントローラ回路はPlatform Desiner上のIP Catalogを用いて実装する
- Avalon-MMで操作できる。

コントローラのクロック入力はセットアップやホールドの時間を考え、位相差を設ける必要がある。
- そのためPLLを追加する。
- 今までの回路ではFPGAボード上から生成されていた50MHzで動作させていたクロックから、PLLから出される100MHzクロックに変更する。
  - DE1-SoCは100MHzで駆動しても問題ない性能があるようだ。

今回の回路も[前記事][prev]のVGA文字表示回路と同様にハードウェアとソフトウェア、そしてハードウェアではモジュール作成とNiosIIのシステム構築にわかれる。

## ハードウェア

仕様
- 解像度はVGA(`640 x 480`px)
- 各ピクセルは`RGB888`形式で保存し、1ピクセルにつき4Byte使う。
- 64MBのSDRAMの前半をプログラム/データ用、後半を表示する画像のデータ格納用に割り当てる。
- 表示アドレスの設定、表示のオンオフ制御はPIOで行

イメージとしては、SDRAMはこのように使用する、

![SDRAM-memmap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/b6b226f2-7683-3714-3214-d09709bcd9f3.png)


そして、PIOの取り扱いは以下のレジスタマップとする。

![disp_ip-regmap00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/b8a79a6e-87e0-82fa-f95d-b9ff44992594.png)


それぞれのbitの動作を以下にまとめる
- DISPADDR
  - 表示開始アドレスを示すレジスタ
  - 回路は、読込み開始時に、この値を読込んで、SDRAMにアクセスするアドレスを決定する
- DISPON
  - 画面表示が有効になっていることを示すbit
  - 回路は同期信号のタイミングと、ここのbitが1であることを確認してVRAMから画像データを読込む。
- CLRVBLNK
  - 回路はこのbitが1であった場合VBLANKを0にさせる。
  - 検知前にフラグを0クリアするためのbit。
- VBLANK
  - 垂直ブランキング、垂直同期信号が立ち下がった瞬間に１になる。
  - プログラムでは、このbitで非表示領域であるか否かを判断し、VRAMに書込んだり、DISPADDRを切り替えたり等の処理を行う。

モジュール作成

disp_ip

- この回路のトップモジュール。以下の5モジュールの接続や、NiosIIのシステムやFPGAボードの入出力をやり取りする。
- 今回作成するdisp_ipモジュールはAvalon-MMのマスターを実装し、SDRAMからピクセルのRGBを読み取り、VGA端子に出力するので、[前記事の文字表示回路][prev]よりも規模は大きい。

disp_ipモジュールは以下のモジュールを使う
- disp_ctrl
- disp_fifo
- syncgen
- disp_out
- disp_flag

disp_ctrl
- Avalon-MM マスターの読出し制御を行い、RAMから画素値の情報をFIFOに格納する。

disp_fifo
- 24bitの入出力、1024段をもつFIFO。IP Catalog から作成する。

syncgen
- [前記事][prev]でも作成した、水平・垂直のカウンタから同期信号を生成する回路。
- VGAに出力するクロック(PCK)を分周して作り出すことも担う。
- [前記事]では50MHｚのクロック入力から25MHzのPCKを生成するために2分周だったが、今回の回路は100MHzのクロック入力がはいるのでPCKは4分周で生成する。

disp_out
- VGAに出力する画素の信号をFIFOから読込み、出力する回路。
- [テキスト][text]ではこのモジュールをPCKで動作させていたが、PLLを使っていない論理信号をクロック入力にして使うのお作法としてはよろしくないという意見を頂いたので、元のクロックに変更して4クロック毎にくるイネーブル信号で制御する形に変更する。

disp_flag
- NiosIIのシステムに垂直同期の開始を伝える回路。
	- 画像を切り替えるタイミングをチェックするのに必要。

NiosII システム
- 以下のコンポーネントを使用する(注:On Chip Memoryは使用しない)
	- NiosII CPU
	- JTAG UART
	- System ID
	- disp_ip
	- **SDRAM コントローラ**
	- **Altera PLL**
	- PIO x 3(後述)

レジスタマップ
- PIOをNiosIIシステムに組込み、プログラム側でdisp_ipに設定を書き込んだり、フラグを受け取ったりする。


最上位階層
 - NiosIIシステムとFPGAボードのVGA端子とSDRAMに接続する。


**ソフトウェア**
- ヘッダファイル化したbitmapデータをSDRAMにコピーするプログラム
- 複数枚のbitmapを一定時間で切り替えるプログラム



# 2.作ってみよう
1.でまとめたものをそれぞれ作成していく。

- disp_ctrl
- disp_fifo
- syncgen
- disp_out
- disp_flag
- disp_ip
- NiosII システム
- 最上位階層
- ソフトウェア

## 2.1. disp_ctrlの作成

disp_ctrlではAvalon-MMのマスターを実装し、SDRAMから画像のピクセルの値を読込む。
連続データを効率よく読み取る為にバースト転送を用いる。

バースト転送のタイミング
- 最初にマスターが転送開始のアドレスとバースト長を設定し、 `read` 信号を `1` にする。
-  `waitrequest` を `0` にして読み出しを受け付けられるまでアドレスとバースト長の設定値を保持する。
- 読み出したデータ(`readdata`)は、`readdatavalid` と共に出力される。

バースト転送では常にデータが連続的に用意されるとは限らないのでFIFOを経由する。
 - 回路を簡潔にするために、`readdata`、`readdatavalid`をそれぞれFIFOのデータ入力、書き込み信号に直結させる。

FIFOに格納されたデータは、disp_outが読込み、VGA信号を作成・出力する。
- disp_outの出力よりdisp_ctrlの読込みの方がはやいのでFIFOの空き容量が一定以下の時は待機する。

上記の仕様を実装するために、ステートマシン使った制御を行う。
状態遷移図は以下のような感じ。
```mermaid
stateDiagram-v2

	state if_state <<choice>>
	[*]     --> HALT
	HALT    --> SETADDR  : dispst
	SETADDR --> READING  : waitrequest==0
	READING --> if_state : rdcnt == (BURSTSIZE-1) \n&& readdatavalid
	if_state --> SETADDR : (3)else
	if_state --> WAITING : (2)FIFOREADY == 0
	WAITING --> SETADDR  : FIFOREADY == 1
	if_state --> HALT    : (1)dispend

```


各状態では以下のように処理を行う。 
- HALT
  - 初期状態、1画面が始まるのまで待機
- SETADDR
  - 設定状態、waitrequestが0になるまでread信号と読込アドレスの値を保持する
- READING
  - 読込み状態、VRAMから送られてきたデータをFIFOに書き込む
  - バースト転送終了時に3状態に分岐する
	- 1画面分読込終了: HALT
	- FIFOの容量が少ない: WAITING
	- 上記以外: SETADDR
- WAITING
  - 待機状態、FIFOの容量が空くまで待機


<details>
<summary>NSLの記述</summary>

ヘッダファイル
```
#ifndef _DISP_CTRL_NSH
#define _DISP_CTRL_NSH


#define BURSTSIZE (16)
#define def_VGA_MAX (640 * 480 * 4)

declare disp_ctrl {
	input     waitrequest;
	output    address[26];
	output    read;
	input     readdatavalid;
	input     DISPSTART;
	input     DISPON;
	input     DISPADDR[26];
	input     FIFOREADY;
}

#endif // _DISP_CTRL_NSH
```

モジュール
```
#include "disp_ctrl.nsh"

module disp_ctrl {
	state_name HALT, SETADDR, READING,WAITING;
	
	/* 内部信号の宣言 */
	reg addrcnt[26] = 0;
	func_self dispend();
	
	/* VRAM読み出し開始（DISPSTARTをCLKで同期化し立ち上がりを検出） */
	reg dispstart_ff[3] = 0;
	func_self dispst();

	/* バーストカウンタ */
	reg rdcnt[4] = 0;

	if(addrcnt == def_VGA_MAX) dispend();
	/* VRAM読み出し開始（DISPSTARTをCLKで同期化し立ち上がりを検出） */
	dispstart_ff := {dispstart_ff[1], dispstart_ff[0], DISPSTART};
	if(DISPON & (dispstart_ff[2:1] == 2'b01)) dispst();


	/* バーストカウンタ */
	/* アドレスおよび読み出し信号 */
	address = addrcnt + DISPADDR;
	// read    = SETADDR;

	state HALT {
		if(dispst) {
			addrcnt := 26'd0;
			goto SETADDR;
		}
	}
	state SETADDR {
		rdcnt := 0;
		read = 1;
		if ( waitrequest==1'b0 ){
			addrcnt := addrcnt + 26'h0040;
			goto READING;
		}else{
		}
	}
	state READING {
		if(readdatavalid){
			rdcnt++;
		}
		if (rdcnt == (BURSTSIZE-1) && readdatavalid){
			alt {
				(dispend):{
					goto HALT;
				}
				(!FIFOREADY):{
					goto WAITING;
				}
				else:{
					goto SETADDR;
				}
			}
		}
	}
	state WAITING {
		if ( FIFOREADY ) goto SETADDR;
	}

}
```

</details>


## 2.2. disp_fifoの作成
準備としてプロジェクトフォルダ内に`FIFO`フォルダを作成し、その中に作成する

IP Catalog で内蔵メモリを作成する場合、FPGAボードによって作成手順が異なる。
- 今回は Cyclone V 準拠のものを作成する。
- その他、IP Catalogの詳しい説明は[テキスト][text]参照

Quartus Prime の `IP Catalog > Library > Basic Functions > On Chip Memory > FIFO` を選ぶ。

出てくるウィンドウに従って以下の設定を行う
- 名前を`disp_fifo`にし、場所は`FIFO`フォルダ内直下にする。
- file typeは`Verilog`にする。
- bit幅`24`,ワード数`1024`と設定する
- クロックは読み書きで別系統にするため(`No, synchronize reading and writing ...`)を選ぶ。
- `Optimization`の設定は、デフォルトの中間条件を設定する。
- フラグ類の設定は`Write-side`の`usedw[]`と、`Asynchronous clear`、`Add circuit to synchronize 'aclr' input with 'rdclk'`にチェックをする。
- 読み出しの設定は`Normal synchronous FIFO mode. ...`を選ぶ
- その他の設定はデフォルト。

`finish`を選択すると、qipファイルを追加するかどうか確認されるので`Yes`を選択

## 2.3. syncgenの作成
syncgenは分周回路が2分周から4分周に変わった所以外は
[前記事][prev]の回路と同様の回路である。
- 詳しい説明はそちらの記事を見てほしい。

PCKの生成は以下のようになっている。
- 1bitのregだったのを2bitに拡張して上位ビットをPCKとして扱うようになった。

```verilog
reg pck_cnt[2] = 0;
pck_cnt++;

PCK = pck_cnt[1];

if (pck_cnt == 2'b01) {
	PCK_posedge();
}
```

また、前回と同様に定数をまとめた`vga_param.nsh`を使用する
- このファイルを別途`#include`しておく。

<details>
<summary>vga_param.nsh</summary>

```nsl
#ifndef _VGA_PARAM_NSH
#define _VGA_PARAM_NSH

#define HPERIOD 800
#define HFRONT  16
#define HWIDTH  96
#define HBACK   48

#define VPERIOD 525
#define VFRONT  10
#define VWIDTH  2
#define VBACK   33

#endif // _VGA_PARAM_NSH

```
</details>

<details>
<summary>NSLの記述</summary>

ヘッダファイル
```
#ifndef _SYNCGEN_NSH
#define _SYNCGEN_NSH

#include "vga_param.nsh"

declare syncgen {
	output   PCK;
	output   VGA_HS;
	output   VGA_VS;
	output   HCNT[10];
	output   VCNT[10];
	func_out PCK_posedge();
}

#endif // _SYNCGEN_NSH
```

モジュール
```
#include "syncgen.nsh"

module syncgen {
	reg VGA_HS_r = 1;
	reg VGA_VS_r = 1;
	reg HCNT_r[10] = 0;
	reg VCNT_r[10] = 0;

	/* 水平カウンタ */
	wire hcntend;

	/* 同期信号 */
	wire  hsstart[10];
	wire  hsend[10];
	wire  vsstart[10];
	wire  vsend[10];
	
	/* システムクロックを4分周してPCKを生成 */

	reg pck_cnt[2] = 0;
	// reg PCK_r = 0;
	pck_cnt++;

	PCK = pck_cnt[1];

	/* 水平カウンタ */
	hcntend = if(HCNT_r==HPERIOD-1)1 else 0;
	/* 同期信号 */
	hsstart = (HFRONT - 1);
	hsend = (HFRONT + HWIDTH - 1);
	vsstart = (VFRONT);
	vsend = (VFRONT + VWIDTH);
	
	if (pck_cnt == 2'b01) {
		PCK_posedge();
		if ( hcntend ){
			HCNT_r := 10'h000;
			if ( VCNT_r == (VPERIOD - 1) ){
				VCNT_r := 10'h000;
			}else{
				VCNT_r++;
			}    
		}else{
			HCNT_r++;
		}
		
		any {
			( HCNT_r == hsstart ):{
				VGA_HS_r := 1'b0;
				if ( VCNT_r==vsstart )
					VGA_VS_r := 1'b0;
				else if ( VCNT_r==vsend )
					VGA_VS_r := 1'b1;
			}
			( HCNT_r==hsend ):{
				VGA_HS_r := 1'b1;
			}
		}
	}

	VGA_HS = VGA_HS_r;
	VGA_VS = VGA_VS_r;
	HCNT = HCNT_r;
	VCNT = VCNT_r;
}
```

</details>


## 2.4. disp_outの作成

disp_outでは同期信号のタイミングに合わせてFIFOからデータを読み込んで
RGBの信号を作成し、VGA端子に出力する。

syncgenからのHCNT,VCNTの値に従って読み出し制御をおこなう、
FIFOの読込とRGBの出力作成にそれぞれクロックを要するので、
表示領域が開始する2PCK前(HCNT=158)から、FIFOの読み出しを開始する。



<details>
<summary>NSLの記述</summary>

ヘッダファイル
```
#ifndef _DISP_OUT_NSH
#define _DISP_OUT_NSH

#include "vga_param.nsh"

declare disp_out {
	input  DISPON;
	output FIFORD;
	
	input  FIFOOUT[24];
	input  HCNT[10];
	input  VCNT[10];
	output DISPSTART;

	output VGA_R[8];
	output VGA_G[8];
	output VGA_B[8];
	output VGA_BLANK_N;

	func_in PCK_posedge();
}

#endif // _DISP_OUT_NSH

```

モジュール
```
#include "disp_out.nsh"

module disp_out {


	reg FIFORD_r = 0;
	reg DISPSTART_r = 0;
	reg VGA_R_r[8] = 0;
	reg VGA_G_r[8] = 0;
	reg VGA_B_r[8] = 0;
	reg VGA_BLANK_N_r = 0;
	
	/* FIFO読み出し信号 */
	wire rdstart[10];
	wire rdend[10];
	
	/* FIFORDを1クロック遅らせて表示の最終イネーブルを作る */
	/* さらに１クロック遅らせてVGA_BLANK_Nを作成           */
	reg disp_enable = 0;



	/* FIFO読み出し信号 */
	rdstart = (HFRONT + HWIDTH + HBACK - 3);
	rdend = HPERIOD - 3;

	func PCK_posedge {

		/* FIFO読み出し信号 */
		// any {
		alt {
			(VCNT < (VFRONT + VWIDTH + VBACK)):{
				FIFORD_r := 0;
			}
			((HCNT == rdstart) & DISPON):{
				FIFORD_r := 1;
			}
			( HCNT == rdend):{
				FIFORD_r := 0;
			}
		}

		/* FIFORDを1クロック遅らせて表示の最終イネーブルを作る */
		/* さらに１クロック遅らせてVGA_BLANK_Nを作成           */
		VGA_BLANK_N_r := disp_enable;
		disp_enable := FIFORD_r;

		/* VGA_R～VGA_B出力 */
		.{VGA_R_r, VGA_G_r, VGA_B_r} := if(disp_enable) FIFOOUT
		else 24'h0; 
		
		/* VRAM読み出し開始 */
		DISPSTART_r := if(
			VCNT == ( VFRONT + VWIDTH + VBACK - 1)
		) 1 else 0;
	}

	FIFORD      = FIFORD_r;
	DISPSTART   = DISPSTART_r;
	VGA_R       = VGA_R_r;
	VGA_G       = VGA_G_r;
	VGA_B       = VGA_B_r;
	VGA_BLANK_N = VGA_BLANK_N_r;
}
```

</details>


## 2.5. disp_flagの作成
垂直同期信号の開始を伝える`VBLANK`信号を生成するモジュール。
- VGA_VSの立ち下がりをもとに信号を生成
- `CLRVBLNK`を受け取ったときにクリアする。


<details>
<summary>NSLの記述</summary>

ヘッダファイル
```nsl
#ifndef _DISP_FLAG_NSH
#define _DISP_FLAG_NSH

declare disp_flag {
		input   VGA_VS;
		input   CLRVBLNK;
		output  VBLANK; 
}

#endif // _DISP_FLAG_NSH
```

モジュール
```
#include "disp_flag.nsh"

module disp_flag {
	
	/* VBLANKセット信号・・・VGA_VSをCLKで同期化 */
	reg vblank_ff[3];
	reg VBLANK_r;

	wire set_vblank;

	vblank_ff := { vblank_ff[1:0], VGA_VS };
	set_vblank = (vblank_ff[2:1] == 2'b10);
	alt {
		(CLRVBLNK):{
			VBLANK_r := 0;
		}
		(set_vblank):{
			VBLANK_r := 1;
		}
	}
	VBLANK = VBLANK_r;
}
```

</details>


## 2.6. disp_ipの作成
Avalonバスの入出力や、レジスタ部分のに相当するPIOに接続する端子、FIFO、syncgen、disp_ctrl、disp_out、disp_flagを配線する。
また、FIFOに十分な空き(1/4以上)があるか判断する信号を作成している。

```nsl
wire wrcnt[10];
wire FIFOREADY;
wrcnt = u0_disp_fifo.wrusedw; // FIFOの使用量
FIFOREADY = (wrcnt<10'd768);  // 1/4の空きがある <--> 使用量が3/4未満である
```


<details>
<summary>NSLの記述</summary>

ヘッダファイル
```
#ifndef _DISP_IP_NSH
#define _DISP_IP_NSH

#include "vga_param.nsh"
#include "disp_ctrl.nsh"
#include "disp_out.nsh"
#include "disp_flag.nsh"
#include "syncgen.nsh"

declare disp_ip interface {
	
    // Avalon MM Master
    input  clk, reset;
    input  avm_waitrequest;
    input  avm_readdata[32];
    input  avm_readdatavalid;
    output avm_address[26];
    output avm_read;
    output avm_burstcount[5];

    /* 画像出力 */
    output coe_VGA_CLK;
    output coe_VGA_R[8];
    output coe_VGA_G[8];
    output coe_VGA_B[8];
    output coe_VGA_HS, coe_VGA_VS;
    output coe_VGA_BLANK_N;

    /* GPIOに接続 */
    input  coe_DISPADDR[26];
    input  coe_DISPON;
    input  coe_CLRVBLNK;
    output coe_VBLANK;

}


declare disp_fifo interface {
    input aclr;
	input data[24];
	input rdclk;
	input rdreq;
	input wrclk;
	input wrreq;
	output q[24];
	output wrusedw[10];
}

#endif // _DISP_IP_NSH

```

モジュール
```
#include "disp_ip.nsh"

module disp_ip {

    disp_ctrl u0_disp_ctrl;
    disp_fifo u0_disp_fifo;
    disp_out u0_disp_out;
    disp_flag u0_disp_flag;
    syncgen u0_syncgen;

    /* ブロック間接続信号 */
    wire PCK;
    wire DISPSTART;
    wire FIFORD;
    wire FIFOOUT[24];
    wire HCNT[10];
    wire VCNT[10];
    wire wrcnt[10];
    wire VGA_VS;

    wire FIFOREADY;
    FIFOREADY = (wrcnt<10'd768);

    coe_VGA_CLK = PCK;

    /* 固定信号 */
    avm_burstcount = 5'd16;  /* バースト長：16 */

    u0_disp_ctrl.m_clock = clk;
    u0_disp_ctrl.p_reset = reset;

    u0_disp_ctrl.waitrequest   = avm_waitrequest;
    u0_disp_ctrl.readdatavalid = avm_readdatavalid;
    u0_disp_ctrl.DISPSTART     = DISPSTART;
    u0_disp_ctrl.DISPON        = coe_DISPON;
    u0_disp_ctrl.DISPADDR      = coe_DISPADDR;
    u0_disp_ctrl.FIFOREADY     = FIFOREADY;
    avm_address                = u0_disp_ctrl.address;
    avm_read                   = u0_disp_ctrl.read;

    u0_disp_fifo.aclr    = ~VGA_VS;
    u0_disp_fifo.data    = avm_readdata[23:0];
    u0_disp_fifo.rdclk   = PCK;
    u0_disp_fifo.rdreq   = FIFORD;
    u0_disp_fifo.wrclk   = clk;
    u0_disp_fifo.wrreq   = avm_readdatavalid;
    FIFOOUT = u0_disp_fifo.q;
    wrcnt = u0_disp_fifo.wrusedw;

    u0_disp_out.m_clock = clk;
    u0_disp_out.p_reset = reset;
    u0_disp_out.DISPON      = coe_DISPON;
    u0_disp_out.FIFOOUT     = FIFOOUT;
    u0_disp_out.HCNT        = HCNT;
    u0_disp_out.VCNT        = VCNT;
    FIFORD = u0_disp_out.FIFORD;
    DISPSTART = u0_disp_out.DISPSTART;
    coe_VGA_R = u0_disp_out.VGA_R;
    coe_VGA_G = u0_disp_out.VGA_G;
    coe_VGA_B = u0_disp_out.VGA_B;
    coe_VGA_BLANK_N = u0_disp_out.VGA_BLANK_N;

    u0_disp_flag.m_clock = clk;
    u0_disp_flag.p_reset = reset;
    u0_disp_flag.VGA_VS  = VGA_VS;
    u0_disp_flag.CLRVBLNK= coe_CLRVBLNK;
    coe_VBLANK = u0_disp_flag.VBLANK;

    u0_syncgen.m_clock = clk;
    u0_syncgen.p_reset = reset;
    PCK = u0_syncgen.PCK;
    coe_VGA_HS = u0_syncgen.VGA_HS;
    VGA_VS = u0_syncgen.VGA_VS;
    HCNT = u0_syncgen.HCNT;
    VCNT = u0_syncgen.VCNT;
    coe_VGA_VS = VGA_VS;

    func u0_syncgen.PCK_posedge {
        u0_disp_out.PCK_posedge();
    }

}
```

</details>


## 2.7. NiosII システムの作成

以下のファイルを用意しdisp_ipフォルダを作成。その中にNSLをVerilogにコンパイルしたファイルと一緒に入れておく。


<details>
<summary>disp_ip_hw</summary>

```tcl
package require -exact qsys 16.1


# 
# module disp_ip
# 
set_module_property DESCRIPTION ""
set_module_property NAME disp_ip
set_module_property VERSION 1.0
set_module_property INTERNAL false
set_module_property OPAQUE_ADDRESS_MAP true
set_module_property AUTHOR ""
set_module_property DISPLAY_NAME disp_ip
set_module_property INSTANTIATE_IN_SYSTEM_MODULE true
set_module_property EDITABLE true
set_module_property REPORT_TO_TALKBACK false
set_module_property ALLOW_GREYBOX_GENERATION false
set_module_property REPORT_HIERARCHY false


# 
# file sets
# 
add_fileset QUARTUS_SYNTH QUARTUS_SYNTH "" ""
set_fileset_property QUARTUS_SYNTH TOP_LEVEL disp_ip
set_fileset_property QUARTUS_SYNTH ENABLE_RELATIVE_INCLUDE_PATHS false
set_fileset_property QUARTUS_SYNTH ENABLE_FILE_OVERWRITE_MODE false
add_fileset_file disp_ip.v VERILOG PATH disp_ip.v TOP_LEVEL_FILE
add_fileset_file disp_ctrl.v VERILOG PATH disp_ctrl.v
add_fileset_file disp_flag.v VERILOG PATH disp_flag.v
add_fileset_file disp_out.v VERILOG PATH disp_out.v
add_fileset_file syncgen.v VERILOG PATH syncgen.v


# 
# parameters
# 


# 
# display items
# 


# 
# connection point clock
# 
add_interface clock clock end
set_interface_property clock clockRate 0
set_interface_property clock ENABLED true
set_interface_property clock EXPORT_OF ""
set_interface_property clock PORT_NAME_MAP ""
set_interface_property clock CMSIS_SVD_VARIABLES ""
set_interface_property clock SVD_ADDRESS_GROUP ""

add_interface_port clock clk clk Input 1


# 
# connection point reset
# 
add_interface reset reset end
set_interface_property reset associatedClock clock
set_interface_property reset synchronousEdges DEASSERT
set_interface_property reset ENABLED true
set_interface_property reset EXPORT_OF ""
set_interface_property reset PORT_NAME_MAP ""
set_interface_property reset CMSIS_SVD_VARIABLES ""
set_interface_property reset SVD_ADDRESS_GROUP ""

add_interface_port reset reset reset Input 1


# 
# connection point avalon_master_0
# 
add_interface avalon_master_0 avalon start
set_interface_property avalon_master_0 addressUnits SYMBOLS
set_interface_property avalon_master_0 associatedClock clock
set_interface_property avalon_master_0 associatedReset reset
set_interface_property avalon_master_0 bitsPerSymbol 8
set_interface_property avalon_master_0 burstOnBurstBoundariesOnly false
set_interface_property avalon_master_0 burstcountUnits WORDS
set_interface_property avalon_master_0 doStreamReads false
set_interface_property avalon_master_0 doStreamWrites false
set_interface_property avalon_master_0 holdTime 0
set_interface_property avalon_master_0 linewrapBursts false
set_interface_property avalon_master_0 maximumPendingReadTransactions 0
set_interface_property avalon_master_0 maximumPendingWriteTransactions 0
set_interface_property avalon_master_0 readLatency 0
set_interface_property avalon_master_0 readWaitTime 1
set_interface_property avalon_master_0 setupTime 0
set_interface_property avalon_master_0 timingUnits Cycles
set_interface_property avalon_master_0 writeWaitTime 0
set_interface_property avalon_master_0 ENABLED true
set_interface_property avalon_master_0 EXPORT_OF ""
set_interface_property avalon_master_0 PORT_NAME_MAP ""
set_interface_property avalon_master_0 CMSIS_SVD_VARIABLES ""
set_interface_property avalon_master_0 SVD_ADDRESS_GROUP ""

add_interface_port avalon_master_0 avm_waitrequest waitrequest Input 1
add_interface_port avalon_master_0 avm_readdata readdata Input 32
add_interface_port avalon_master_0 avm_readdatavalid readdatavalid Input 1
add_interface_port avalon_master_0 avm_address address Output 26
add_interface_port avalon_master_0 avm_read read Output 1
add_interface_port avalon_master_0 avm_burstcount burstcount Output 5


# 
# connection point conduit_end_0
# 
add_interface conduit_end_0 conduit end
set_interface_property conduit_end_0 associatedClock clock
set_interface_property conduit_end_0 associatedReset reset
set_interface_property conduit_end_0 ENABLED true
set_interface_property conduit_end_0 EXPORT_OF ""
set_interface_property conduit_end_0 PORT_NAME_MAP ""
set_interface_property conduit_end_0 CMSIS_SVD_VARIABLES ""
set_interface_property conduit_end_0 SVD_ADDRESS_GROUP ""

add_interface_port conduit_end_0 coe_VGA_R vga_r Output 8
add_interface_port conduit_end_0 coe_VGA_G vga_g Output 8
add_interface_port conduit_end_0 coe_VGA_B vga_b Output 8
add_interface_port conduit_end_0 coe_VGA_HS vga_hs Output 1
add_interface_port conduit_end_0 coe_VGA_VS vga_vs Output 1
add_interface_port conduit_end_0 coe_DISPADDR dispaddr Input 26
add_interface_port conduit_end_0 coe_DISPON dispon Input 1
add_interface_port conduit_end_0 coe_CLRVBLNK clrvblnk Input 1
add_interface_port conduit_end_0 coe_VBLANK vblank Output 1
add_interface_port conduit_end_0 coe_VGA_BLANK_N vga_blank_n Output 1
add_interface_port conduit_end_0 coe_VGA_CLK vga_clk Output 1
```

</details>

Platform Designerで以下のコンポーネントを追加してNiosIIシステム構築をする。詳しい手順は[前記事][prev]や[テキスト][text]参照
- NiosII CPU
- JTAG UART
- System ID
- disp_ip_hw

### SDRAMコントローラとPLLの設定

上記のコンポーネントを追加したら今回の目玉であるSDRAMを扱えるように設定する。
`[Memory Interfaces and Controllers] > [SDRAM] > [SDRAM Controller]`を選択し、
以下の画像(1)(2)のように設定する

![SDRAM_ctrl_setting01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/469fcc17-453a-50e7-561a-0eee7a383457.png)
![SDRAM_ctrl_setting02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/6c8c3e27-c643-5651-f0aa-f23897282d50.png)

### PIOの設定

PIOを3つ追加し、PIO_0, PIO_1, PIO_2の設定を下図のように行う。

![PIO_seting00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/f68609f6-c300-8205-68fb-4b569f1a8e35.png)

### 配線

下図を参考にクロックとリセット、外部端子、IRQ等の配線をする。詳しい手順は[前々回の記事][prev2]や[テキスト][text]参照

![nios2_components00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/31a62214-bff2-93f7-9037-0af6171ff938.png)
![nios2_components01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/94cf5b80-d2cc-69a6-5dad-fd833e06a0fc.png)


前と違う点としてNiosIIのベクタをon chip memoryにしていたのを、SDRAMコントローラを選択する。

![nios2_vector_setting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/c1ed83b1-7d0d-4061-43fa-d5d9dad1ae83.png)


## 2.8. 最上位階層の作成

Verilogで作成されたものを使用
- NSLだと入出力端子の配線がうまくいかない
- [テキスト][text]の最上位層を改変(端子名を変更)
  - CLK -> m_clock
  - RST -> p_reset 
- ライセンスが不明なので記述ファイルの内容についてはは省略
  - サイトのページからダウンロードできる`display.v`を使用してほしい

## 2.9. ソフトウェアの作成
今回作成するソフトウェアプログラムはこんな感じ
- ヘッダファイル化したbitmapデータをSDRAMにコピーする
- 複数枚のbitmapを一定時間で切り替える

### 2.9.0. 準備

- プログラムで画像データを扱えるようにデータを加工する。

まずはフリー素材のサイトでダウンロードしてきた適当な画像ファイルを用意して、640x480のサイズに変換する。
リサイズしたファイルを[GIMP][gimp]で読込み、C言語のヘッダファイル形式(.h)にエクスポートすると、以下のようになっている。

```c
/*  GIMP header image file format (RGB): C:\********\colorpallet.h  */

static unsigned int width = 640;
static unsigned int height = 480;

/*  Call this macro repeatedly.  After each use, the pixel data can be extracted  */

#define HEADER_PIXEL(data,pixel) {\
pixel[0] = (((data[0] - 33) << 2) | ((data[1] - 33) >> 4)); \
pixel[1] = ((((data[1] - 33) & 0xF) << 4) | ((data[2] - 33) >> 2)); \
pixel[2] = ((((data[2] - 33) & 0x3) << 6) | ((data[3] - 33))); \
data += 4; \
}
static char *header_data =
	"^`@X^`@X^`@X^`@X^`@X^`@X^`@X^`@X^`@X^`@X^`@X^`@X^`@X^`@X^`@X^`@X"
	"..." // 文字列が延々と続く
	"";
```
- 中身を推測するに文字列データにbitmapの画素値を変換した値を変数に格納し、マクロで元に戻すようにしているのだろうか。
	- ``^`@X``を整数で読込むと`[94, 96, 64, 88]`になり、その値をマクロの式に代入すると`[247, 247, 247]`となり、冒頭の写真で表示させた画像の左上の画素値と一致する。

複数枚使用するときのために、マクロや`width,height`の定義はメインプログラムで記述するようにし、`header_data`の名前部分を被らないように変えておく
- 名前は無難に`colorpallet_data`にしといた

---

- リンカスクリプトの設定

1.の仕様の項で出したSDRAMの扱いを実現するためにリンカスクリプトの修正を行う。
- BSPプロジェクトを選択して右クリック`Nios II > BSP Editor`をえらぶ(下図左)
- `Linker Script` タブを選択して`Linker Memory Regions` の `new_sdram_controller_0` 行 `Size (bytes)`列をダブルクリックして編集し、値を`33554400`にする(下図右)
  - Address Range が　0x00000020 - 0x01FFFFFF であればOK

![Linker_Script_setting_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/d1b1212c-11cf-83bb-19da-53707a4e0aa3.png)


### 2.9.1. ヘッダファイル化したbitmapデータをSDRAMにコピーする
実際にプログラムを作成していく。
まずは扱う定数をdefineしておく。

```c
#define DISPADDR PIO_0_BASE
#define CLRV_DON PIO_1_BASE
#define VBLANK   PIO_2_BASE
#define DISPON_BIT 0x01
#define CLRVB_BIT  0x02
#define VRAM ((volatile unsigned int *) 0x02000000)
```

また、垂直同期信号のタイミングにあわせて値を設定するために、
待機する関数を追加する。

```c
void wait_vblank(void) {
	IOWR_ALTERA_AVALON_PIO_SET_BITS(CLRV_DON, CLRVB_BIT);
	IOWR_ALTERA_AVALON_PIO_CLEAR_BITS(CLRV_DON, CLRVB_BIT);
	while (IORD_ALTERA_AVALON_PIO_DATA(VBLANK)==0);
}
```

メイン関数ではまずヘッダファイル化されている画像データを
VRAMに書き込む。

```c
char *data = colorpallet_data;
unsigned char pic[3];
int i;

// ヘッダファイルからVRAMに読み込み
for ( i=0; i<colorpallet_width*colorpallet_height; i++ ) {
	HEADER_PIXEL(data, pic);
	VRAM[i] = (pic[0] << 16) | (pic[1] << 8) | pic[2];
}
alt_dcache_flush_all();
```

そして垂直ブランキングを待って、表示開始アドレスを設定し表示を有効化。
```c
// 表示をON
wait_vblank();
IOWR_ALTERA_AVALON_PIO_DATA(DISPADDR, 0x02000000);
IOWR_ALTERA_AVALON_PIO_SET_BITS(CLRV_DON, DISPON_BIT);
```

<details>
<summary>プログラム全体</summary>

```c
#include "system.h"
#include "sys/alt_cache.h"
#include "altera_avalon_pio_regs.h"


#include "colorpallet.h"

#define DISPADDR PIO_0_BASE
#define CLRV_DON PIO_1_BASE
#define VBLANK   PIO_2_BASE
#define DISPON_BIT 0x01
#define CLRVB_BIT  0x02
#define VRAM ((volatile unsigned int *) 0x02000000)

static unsigned int width = 640;
static unsigned int height = 480;

/*  Call this macro repeatedly.  After each use, the pixel data can be extracted  */

#define HEADER_PIXEL(data,pixel) {\
pixel[0] = (((data[0] - 33) << 2) | ((data[1] - 33) >> 4)); \
pixel[1] = ((((data[1] - 33) & 0xF) << 4) | ((data[2] - 33) >> 2)); \
pixel[2] = ((((data[2] - 33) & 0x3) << 6) | ((data[3] - 33))); \
data += 4; \
}


// VBLANK待ち
void wait_vblank(void) {
    IOWR_ALTERA_AVALON_PIO_SET_BITS(CLRV_DON, CLRVB_BIT);
    IOWR_ALTERA_AVALON_PIO_CLEAR_BITS(CLRV_DON, CLRVB_BIT);
    while (IORD_ALTERA_AVALON_PIO_DATA(VBLANK)==0);
}

int main()
{
    char *data = colorpallet_data;
    unsigned char pic[3];
    int i;

    // ヘッダファイルからVRAMに読み込み
   for ( i=0; i<colorpallet_width*colorpallet_height; i++ ) {
        HEADER_PIXEL(data, pic);
        VRAM[i] = (pic[0] << 16) | (pic[1] << 8) | pic[2];
    }
    alt_dcache_flush_all();

    // 表示をON
    wait_vblank();
    IOWR_ALTERA_AVALON_PIO_DATA(DISPADDR, 0x02000000);
    IOWR_ALTERA_AVALON_PIO_SET_BITS(CLRV_DON, DISPON_BIT);

  return 0;
}

```

</details>


### 2.9.2. 複数枚のbitmapを一定時間で切り替える

複数枚画像を使用するため。新たな画像を用意する。


用意した画像のリンク
- [[フリー写真] カラフルな曲線のグラデーション - パブリックドメインQ：著作権フリー画像素材集](https://publicdomainq.net/colorful-rainbow-background-0055363/)
- [[フリー写真] コップに入ったカラフルな液体 - パブリックドメインQ：著作権フリー画像素材集](https://publicdomainq.net/liquid-colorful-0059171/)
- [[フリー写真] 星空と船 - パブリックドメインQ：著作権フリー画像素材集](https://publicdomainq.net/stars-ship-night-0061123/)

それぞれできたファイルを2.9.0.の手順を踏んで、プログラムで扱えるようにする。

基本的なプログラムは2.9.1のものと同じ、
ヘッダファイル化されている画像データをVRAMに書き込む。
- 書き込む画像が複数になった以外は同じ
```c
char *data[4] = {colorpallet_data, colorglasses_data, rainbow_data, starryskyship_data};
unsigned char pic[3];
int i, j;

for ( j=0; j<4; j++ ) {
	for ( i=0; i<width*height; i++ ) {
		HEADER_PIXEL(data[j], pic);
		VRAM[PICSIZE*j+i] = (pic[0] << 16) | (pic[1] << 8) | pic[2];
	}
}
alt_dcache_flush_all();
```

そして垂直ブランキングを待って、表示開始アドレスを設定し表示を有効化。
```c
wait_vblank();
IOWR_ALTERA_AVALON_PIO_DATA(DISPADDR, 0x02000000);
IOWR_ALTERA_AVALON_PIO_SET_BITS(CLRV_DON, DISPON_BIT);
```
そして3秒待機(180回垂直ブランキングをチェックする)して
次の画像を表示するようにする。
```c
j = 1;
while(1) {
	for( i=0; i<60*3; i++ ) wait_vblank();
	IOWR_ALTERA_AVALON_PIO_DATA(DISPADDR, 0x02000000 + PICSIZE*j*4);
	if ( j==3 ) j=0;
	else j++;
}
```

<details>
<summary>プログラム全体</summary>

```c
#include "system.h"
#include "sys/alt_cache.h"
#include "altera_avalon_pio_regs.h"

#include "colorpallet.h"
#include "colorglasses.h"
#include "rainbow.h"
#include "starryskyship.h"

#define DISPADDR PIO_0_BASE
#define CLRV_DON PIO_1_BASE
#define VBLANK   PIO_2_BASE
#define DISPON_BIT 0x01
#define CLRVB_BIT  0x02
#define VRAM ((volatile unsigned int *) 0x02000000)
#define PICSIZE 640*480

static unsigned int width = 640;
static unsigned int height = 480;

/*  Call this macro repeatedly.  After each use, the pixel data can be extracted  */

#define HEADER_PIXEL(data,pixel) {\
pixel[0] = (((data[0] - 33) << 2) | ((data[1] - 33) >> 4)); \
pixel[1] = ((((data[1] - 33) & 0xF) << 4) | ((data[2] - 33) >> 2)); \
pixel[2] = ((((data[2] - 33) & 0x3) << 6) | ((data[3] - 33))); \
data += 4; \
}

// VBLANK待ち
void wait_vblank(void) {
    IOWR_ALTERA_AVALON_PIO_SET_BITS(CLRV_DON, CLRVB_BIT);
    IOWR_ALTERA_AVALON_PIO_CLEAR_BITS(CLRV_DON, CLRVB_BIT);
    while (IORD_ALTERA_AVALON_PIO_DATA(VBLANK)==0);
}

int main()
{
    char *data[4] = {colorpallet_data, colorglasses_data, rainbow_data, starryskyship_data};
    unsigned char pic[3];
    int i, j;

    for ( j=0; j<4; j++ ) {
        for ( i=0; i<width*height; i++ ) {
            HEADER_PIXEL(data[j], pic);
            VRAM[PICSIZE*j+i] = (pic[0] << 16) | (pic[1] << 8) | pic[2];
        }
    }
    alt_dcache_flush_all();

    wait_vblank();
    IOWR_ALTERA_AVALON_PIO_DATA(DISPADDR, 0x02000000);
    IOWR_ALTERA_AVALON_PIO_SET_BITS(CLRV_DON, DISPON_BIT);

    j = 1;
    while(1) {
        for( i=0; i<60*3; i++ ) wait_vblank();
        IOWR_ALTERA_AVALON_PIO_DATA(DISPADDR, 0x02000000 + PICSIZE*j*4);
        j++;
        if ( j>=4 ) j=0;
    }
  return 0;
}
```

</details>


# 3. 動作チェック

以下は冒頭の写真の再掲だが画像の表示は問題なくできていそうだ。

![DE1-SoCtoVGA.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/86c8aeb6-a1be-c6b8-4e0b-bc6943225bfe.jpeg)


ModelSim使って波形を見てみる。
![check01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/444c15fc-0188-4f73-a3b4-de9d3befbf9f.png)

- 黄色の縦線の所は水平同期信号が立ち上ってからVGAの画素が表示される部分。
  - その部分の間隔は1920[ns]になっているが25MHzのPCK48周期分(48*40ns)なので、バックポーチ(40)＋ボーダー(8)のタイミングと対応している
- 表示開始時の画素値がF7F7F7
  - F7F7F7を実際に表示してみたものと左上を拡大した部分の比較(下図)
    - 見た感じでは同じに見える
   
![vgaout_check00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/f7b35861-6286-3675-7515-0be466458b2f.png)


# 4. まとめ

FPGAボード上のSDRAMを使用して画像をVGAに出力するシステムを作った。
- Avalon-MMのマスター回路で読込む
- 値をFIFOに入れて出力する
- 画像を用意してプログラムに組込めるように変換する。
- プログラムでSDRAMにコピーして実際に表示させる。

次回は[テキスト][text]の第8章のCMOSカメラのキャプチャ回路を作ってみます。


# 参考文献

- [【改訂2版】FPGAボードで学ぶ 組込みシステム開発入門［Intel FPGA編］][text]

[gimp]: https://www.gimp.org/
[text]: https://gihyo.jp/book/2018/978-4-7741-9388-5
[prev4]: https://qiita.com/mk_masa/items/2c4eb17dde97695cee4c
[prev3]: https://qiita.com/mk_masa/items/85d6e47b081195c277db
[prev2]: https://qiita.com/mk_masa/items/2088aac1ca16b49df952
[prev]: ../vol04_vgaif/vgaif_with_NSL.md

<!--
# 付録

作った回路のファイルはGitHubにupしています
- リポジトリ
	- 準備中
-->