【改訂2版】FPGAボードで学ぶ 組込みシステム開発入門[Intel FPGA編](以下、テキスト)の第8章に載っているSDRAMを使用したグラフィック表示回路をハードウェア記述言語のNSLで作ってみよう。
動かした様子
VGAケーブルでFPGAボードとディスプレイを接続して画像を表示した。
使用したもの
- Quartus Prime Lite 18.1
- FPGAボード: DE1-SoC
-
[フリー写真] 色とりどりの四角形の色 - パブリックドメインQ:著作権フリー画像素材集
- この画像を
640
x480
に縮小し、NiosIIのシステムで扱えるようにGIMPで変換(参考:テキストPP304-307)
- この画像を
1. 回路について
前記事ではVGA出力を扱った。
- FPGAボード上のメモリの容量が足りないので、表示は文字のみ。
今回使っているボード(DE1-SoC)にはSDRAMが搭載されている。
- SDRAMを扱うためのコントローラ回路を載せれば使用できる。
- 増えた容量を使い、VGA出力に画像を表示することを目標として回路を作る。
SDRAMコントローラ回路はPlatform Desiner上のIP Catalogを用いて実装する
- Avalon-MMで操作できる。
コントローラのクロック入力はセットアップやホールドの時間を考え、位相差を設ける必要がある。
- そのためPLLを追加する。
- 今までの回路ではFPGAボード上から生成されていた50MHzで動作させていたクロックから、PLLから出される100MHzクロックに変更する。
- DE1-SoCは100MHzで駆動しても問題ない性能があるようだ。
今回の回路も前記事のVGA文字表示回路と同様にハードウェアとソフトウェア、そしてハードウェアではモジュール作成とNiosIIのシステム構築にわかれる。
ハードウェア
仕様
- 解像度はVGA(
640 x 480
px) - 各ピクセルは
RGB888
形式で保存し、1ピクセルにつき4Byte使う。 - 64MBのSDRAMの前半をプログラム/データ用、後半を表示する画像のデータ格納用に割り当てる。
- 表示アドレスの設定、表示のオンオフ制御はPIOで行
イメージとしては、SDRAMはこのように使用する、
そして、PIOの取り扱いは以下のレジスタマップとする。
それぞれのbitの動作を以下にまとめる
- DISPADDR
- 表示開始アドレスを示すレジスタ
- 回路は、読込み開始時に、この値を読込んで、SDRAMにアクセスするアドレスを決定する
- DISPON
- 画面表示が有効になっていることを示すbit
- 回路は同期信号のタイミングと、ここのbitが1であることを確認してVRAMから画像データを読込む。
- CLRVBLNK
- 回路はこのbitが1であった場合VBLANKを0にさせる。
- 検知前にフラグを0クリアするためのbit。
- VBLANK
- 垂直ブランキング、垂直同期信号が立ち下がった瞬間に1になる。
- プログラムでは、このbitで非表示領域であるか否かを判断し、VRAMに書込んだり、DISPADDRを切り替えたり等の処理を行う。
モジュール作成
disp_ip
- この回路のトップモジュール。以下の5モジュールの接続や、NiosIIのシステムやFPGAボードの入出力をやり取りする。
- 今回作成するdisp_ipモジュールはAvalon-MMのマスターを実装し、SDRAMからピクセルのRGBを読み取り、VGA端子に出力するので、前記事の文字表示回路よりも規模は大きい。
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
- 前記事でも作成した、水平・垂直のカウンタから同期信号を生成する回路。
- VGAに出力するクロック(PCK)を分周して作り出すことも担う。
- [前記事]では50MHzのクロック入力から25MHzのPCKを生成するために2分周だったが、今回の回路は100MHzのクロック入力がはいるのでPCKは4分周で生成する。
disp_out
- VGAに出力する画素の信号をFIFOから読込み、出力する回路。
- テキストではこのモジュールを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の空き容量が一定以下の時は待機する。
上記の仕様を実装するために、ステートマシン使った制御を行う。
状態遷移図は以下のような感じ。
各状態では以下のように処理を行う。
- HALT
- 初期状態、1画面が始まるのまで待機
- SETADDR
- 設定状態、waitrequestが0になるまでread信号と読込アドレスの値を保持する
- READING
- 読込み状態、VRAMから送られてきたデータをFIFOに書き込む
- バースト転送終了時に3状態に分岐する
- 1画面分読込終了: HALT
- FIFOの容量が少ない: WAITING
- 上記以外: SETADDR
- WAITING
- 待機状態、FIFOの容量が空くまで待機
NSLの記述
ヘッダファイル
#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;
}
}
2.2. disp_fifoの作成
準備としてプロジェクトフォルダ内にFIFO
フォルダを作成し、その中に作成する
IP Catalog で内蔵メモリを作成する場合、FPGAボードによって作成手順が異なる。
- 今回は Cyclone V 準拠のものを作成する。
- その他、IP Catalogの詳しい説明はテキスト参照
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分周に変わった所以外は
前記事の回路と同様の回路である。
- 詳しい説明はそちらの記事を見てほしい。
PCKの生成は以下のようになっている。
- 1bitのregだったのを2bitに拡張して上位ビットをPCKとして扱うようになった。
reg pck_cnt[2] = 0;
pck_cnt++;
PCK = pck_cnt[1];
if (pck_cnt == 2'b01) {
PCK_posedge();
}
また、前回と同様に定数をまとめたvga_param.nsh
を使用する
- このファイルを別途
#include
しておく。
vga_param.nsh
#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
NSLの記述
ヘッダファイル
#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;
}
2.4. disp_outの作成
disp_outでは同期信号のタイミングに合わせてFIFOからデータを読み込んで
RGBの信号を作成し、VGA端子に出力する。
syncgenからのHCNT,VCNTの値に従って読み出し制御をおこなう、
FIFOの読込とRGBの出力作成にそれぞれクロックを要するので、
表示領域が開始する2PCK前(HCNT=158)から、FIFOの読み出しを開始する。
NSLの記述
ヘッダファイル
#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クロック遅らせて表示の最終イネーブルを作る */
/* さらに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クロック遅らせて表示の最終イネーブルを作る */
/* さらに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;
}
2.5. disp_flagの作成
垂直同期信号の開始を伝えるVBLANK
信号を生成するモジュール。
- VGA_VSの立ち下がりをもとに信号を生成
-
CLRVBLNK
を受け取ったときにクリアする。
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;
}
2.6. disp_ipの作成
Avalonバスの入出力や、レジスタ部分のに相当するPIOに接続する端子、FIFO、syncgen、disp_ctrl、disp_out、disp_flagを配線する。
また、FIFOに十分な空き(1/4以上)があるか判断する信号を作成している。
wire wrcnt[10];
wire FIFOREADY;
wrcnt = u0_disp_fifo.wrusedw; // FIFOの使用量
FIFOREADY = (wrcnt<10'd768); // 1/4の空きがある <--> 使用量が3/4未満である
NSLの記述
ヘッダファイル
#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();
}
}
2.7. NiosII システムの作成
以下のファイルを用意しdisp_ipフォルダを作成。その中にNSLをVerilogにコンパイルしたファイルと一緒に入れておく。
disp_ip_hw
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
Platform Designerで以下のコンポーネントを追加してNiosIIシステム構築をする。詳しい手順は前記事やテキスト参照
- NiosII CPU
- JTAG UART
- System ID
- disp_ip_hw
SDRAMコントローラとPLLの設定
上記のコンポーネントを追加したら今回の目玉であるSDRAMを扱えるように設定する。
[Memory Interfaces and Controllers] > [SDRAM] > [SDRAM Controller]
を選択し、
以下の画像(1)(2)のように設定する
PIOの設定
PIOを3つ追加し、PIO_0, PIO_1, PIO_2の設定を下図のように行う。
配線
下図を参考にクロックとリセット、外部端子、IRQ等の配線をする。詳しい手順は前々回の記事やテキスト参照
前と違う点としてNiosIIのベクタをon chip memoryにしていたのを、SDRAMコントローラを選択する。
2.8. 最上位階層の作成
Verilogで作成されたものを使用
- NSLだと入出力端子の配線がうまくいかない
-
テキストの最上位層を改変(端子名を変更)
- CLK -> m_clock
- RST -> p_reset
- ライセンスが不明なので記述ファイルの内容についてはは省略
- サイトのページからダウンロードできる
display.v
を使用してほしい
- サイトのページからダウンロードできる
2.9. ソフトウェアの作成
今回作成するソフトウェアプログラムはこんな感じ
- ヘッダファイル化したbitmapデータをSDRAMにコピーする
- 複数枚のbitmapを一定時間で切り替える
2.9.0. 準備
- プログラムで画像データを扱えるようにデータを加工する。
まずはフリー素材のサイトでダウンロードしてきた適当な画像ファイルを用意して、640x480のサイズに変換する。
リサイズしたファイルをGIMPで読込み、C言語のヘッダファイル形式(.h)にエクスポートすると、以下のようになっている。
/* 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
2.9.1. ヘッダファイル化したbitmapデータをSDRAMにコピーする
実際にプログラムを作成していく。
まずは扱う定数をdefineしておく。
#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)
また、垂直同期信号のタイミングにあわせて値を設定するために、
待機する関数を追加する。
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に書き込む。
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);
プログラム全体
#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;
}
2.9.2. 複数枚のbitmapを一定時間で切り替える
複数枚画像を使用するため。新たな画像を用意する。
用意した画像のリンク
- [フリー写真] カラフルな曲線のグラデーション - パブリックドメインQ:著作権フリー画像素材集
- [フリー写真] コップに入ったカラフルな液体 - パブリックドメインQ:著作権フリー画像素材集
- [フリー写真] 星空と船 - パブリックドメインQ:著作権フリー画像素材集
それぞれできたファイルを2.9.0.の手順を踏んで、プログラムで扱えるようにする。
基本的なプログラムは2.9.1のものと同じ、
ヘッダファイル化されている画像データをVRAMに書き込む。
- 書き込む画像が複数になった以外は同じ
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);
そして3秒待機(180回垂直ブランキングをチェックする)して
次の画像を表示するようにする。
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++;
}
プログラム全体
#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;
}
3. 動作チェック
以下は冒頭の写真の再掲だが画像の表示は問題なくできていそうだ。
- 黄色の縦線の所は水平同期信号が立ち上ってからVGAの画素が表示される部分。
- その部分の間隔は1920[ns]になっているが25MHzのPCK48周期分(48*40ns)なので、バックポーチ(40)+ボーダー(8)のタイミングと対応している
- 表示開始時の画素値がF7F7F7
- F7F7F7を実際に表示してみたものと左上を拡大した部分の比較(下図)
- 見た感じでは同じに見える
- F7F7F7を実際に表示してみたものと左上を拡大した部分の比較(下図)
4. まとめ
FPGAボード上のSDRAMを使用して画像をVGAに出力するシステムを作った。
- Avalon-MMのマスター回路で読込む
- 値をFIFOに入れて出力する
- 画像を用意してプログラムに組込めるように変換する。
- プログラムでSDRAMにコピーして実際に表示させる。
次回はテキストの第8章のCMOSカメラのキャプチャ回路を作ってみます。