はじめに
ここではGowin GW1NR-9 FPGAを搭載したTang Nano 9Kにインテル4001のROM(正確に言えばRAMをROMのように扱う)を作成する方法について説明します.1個の4001には1ワード8ビットのインストラクションを256個,記憶できます.実際の4001はROMですが,ここではRAMにインストラクションを書き込み,それを後からフェッチしてエグゼキュートするようにします.まずはその第一歩として,8ビット幅,256個のRAMを作成します.ここで利用するRAMは上記FPGAに備わるBlockRAMです.これにより,フリップフロップのような分散RAMと異なり,ある程度まとまった大きさの記憶を行えます.ここでは,4001内にあるROMの原型を作成します.あくまで「原型」ですので,記憶するデータ(最終的にはインストラクションになります)のアドレスをトグルスイッチで代用します.
環境
以下の環境で開発を行いました.
- FPGA: Gowin GW1NR-9 FPGA
- 開発ボード: Tang Nano 9K を搭載したボード
- 開発環境: Gowin EDA V.1.9.9 Beta-4 Education
作成する4001内のROM
オリジナルの4001に備わるROMのサイズに準じて作成します.このため,8ビット幅,256ワードとなります.このようなROMをFPGA内のBlockRAMに作成します.そしてそのモジュール上部にアドレス入力用スイッチ,データ入力用スイッチ,クロック用スイッチ,読み書き切替用スイッチ,RAM内容表示機(ドットマトリクスディスプレイ),ページ表示器(7セグメントLED)を作成したいと思います.ここでページについて説明します.ドットマトリクスディスプレイには32列しかないため,4001に備わるROMすべてを一度に表示することができません.このため,8個に分けて表示できるようにします.このように32ワードを1ページとここでは呼びます.つまり0~7までのページがあることになり,これを7セグメントLEDに表示することとします.
スイッチの役割
各スイッチとその役割について説明します.下のようにしましょう.
スイッチ | 役割 | ポート名 | ビット幅 |
SW1~SW8 | アドレス入力用スイッチ(SW1が最下位,SW8が最上位) | iAddr | 8 |
SW9~SW16 | データ入力用スイッチ(SW9が最下位,SW16が最上位) | iData | 8 |
SW17 | クロックスイッチ(レバーが下だとLow,上だとHigh) | iManualClk | 1 |
SW18 | 読み書き切替用スイッチ(レバーが下だとRAMから読み込み,上だとRAMへ書き込み) | iWriteEn | 1 |
RAM内容表示器とページ表示器
1ワードをRAM内容表示器(ドットマトリクスディスプレイ)1列に表示します.右側が下位アドレス,左側が上位アドレスとします.また,ページ番号をページ番号表示器(7セグメントLEDの一番右側にあるDP1)に表示します.
作成手順
まずはざっと流れについて説明します.
- FPGAのIP Coreを使ってBlockRAMを制御するi4001ROMモジュールを生成
- 上記モジュールをテスト動作させるためにMainモジュールを作成
- Mainモジュール内でi4001ROMをインスタンス化
以上の順番で作成していきます.
BlockRAMを制御するモジュールを生成
まずはFPGAのIP CoreでBlockRAMを制御するコードを生成しましょう.IP CoreとはIntellectural Property Coreのことで,FPGAなどに特定の機能をまとめられているコンポーネントのことを指します.今回はBlockRAMですが,そのほかにも通信を行うものや画像を扱うものなどがあります.
ではGowin EDAを立ち上げてください.立ち上げましたら下の図のようにNewProjectを選択します.
現れたダイアログに対して下記のようにOKボタンを押します.
今回のモジュールを下の図のようにi4001Blockとします.
次にターゲットデバイスを選択します.下の図のようにGW1NRシリーズを選んでください.
これで設定完了です.
次にIP Coreを生成します.下の図のようにGowin EDA上部にあるアイコンを押してください.
次にIP Coreの種類を選択します.下の図のようにHard Module⇒Memory⇒Blok Memoryの中にあるSP(Single Port)をダブルクリックします.これら4種の中で最もシンプルなものがこれです.SP以外にはDP(Dual Port)のものなどがあります.ここでPortとは入出力の端子を表していて,1つしかないもの(=Single)や2つあるもの(=Dual)というような違いがあります.
次に作成するRAMの設定を行います.上からFileNameとModuleNameがあり,ともにi4001Blockとここではしました.次にAddress Depth とData Widthです.4001には256ワードであるため256とし,1ワードが8ビットであるためData Widthを8にしました.その左側にあるRead/Write Modeについては特に変更する必要はありません.なお,この中にあるRead modeにはBypassとなっていますが,これは後述のOCE(Output Clock Enable)端子を使わないときにはBypassを選択しておく必要があります.
ファイルを追加してよいか問い合わせるダイアログが現れますのでOKを選んでください.
生成されると下のようにi4001Block.vファイルが出来上がります.このファイルに含まれるi4001Blockモジュールを制御するため,下図の右側にあるようにインスタンスを生成します.
生成されたi4001Blockのポート
各ポートの役割を説明します.
doutポート(出力)
RAMから出力されるデータです.
clkポート(入力)
RAMに入力されるクロックです.この立ち上がりエッジのとき,各種動作が行われます.
oceポート(入力)
Ouput Clock Enableポートです.前にIP CodeでRAMを生成したとき,Read modeをByPassにしましたが,その場合にはoceポートは何の役割も果たしません.反対にRead modeがPipelineの場合,oceポートがLowの時にはdoutポートへ作用を及ぼします.詳しくは省略します.
ceポート(入力)
Clock Enableポートです.この端子はHighの時に動作する,いわゆるHigh-activeです.役割としては,複数のRAMが存在するような回路構成の時,どのRAMをアクティブ(High)にしたりインアクティブ(Low)にしたりするときに用います.ただし今回の例ではRAMが1個しかないため,Highにしておきます.
resetポート(入力)
RAMをリセットするときに用います.この端子もHighの時に動作する,いわゆるHigh-activeです.今回はリセット機能を使わないため,ずっとLowにしておきます.
wreポート(入力)
Write Enableポートです.読むとき(Low)と書くとき(High)を切り替えるときに使います.
adポート(入力)
Addressポートです.
dinポート(入力)
RAMへ書き込むためのポートです.
Mainモジュールを作成
ではROMを操るMainモジュールを作成しましょう.下図のようにi4001ROMを右クリックし,New Fileを選択してください.
下の図のようにVerilog Fileを選択してください.
ここではMainと名付けました.
Mainモジュールには前に説明したスイッチのほか,入力ポートとしてiClk(27MHzのクロック)と,下の表に示すドットマトリクスディスプレイと7セグメントLEDを表示するための出力ポートが必要です.ドットマトリクスディスプレイについてはこちら,7セグメントLEDについてはこちらをご覧ください.
ポート名 | 役割 | 幅 |
oPattern | 表示したいパターン信号です.同じセグメント(たとえばセグメントA)は 電気的に接続されているため,もしDIGITが1111の場合にはすべてが同じ数字を 表示します.実際に使用するときには,上記DIGITは1個しかビットが立たないように 制御すれば,各桁は別の数字が表示されているように見えます. |
8 |
oDigit | ダイナミック点灯方式では,4個の7セグメントLEDから1個を指定してから, 表示するパターンを全7セグに送ります.oDigitは1個の7セグを指定するときに 用います.Highとなっている桁のみ表示するよう,回路設計がなされています ので,0001⇒0010⇒0100⇒1000⇒0001…を繰り返すことになります. |
4 |
oDmdClr | この信号が1'b1のときリセットされます. | 1 |
oDmdClk | 4個のシフトレジスタのクロックに接続されています. | 1 |
oDmdSeg | 4個のシフトレジスタのシリアルインと接続されており,同じくシフトレジスタに接続されているoClkの立ち上がりエッジのタイミングで,oSsgの信号をシフトレジスタは取り込みます. | 4 |
oDmdColumn | このドットマトリクスはダイナミック点灯方式を利用しており,目には見えませんが高速で1列ずつ描画しています.その1列のパターンを表しているのがこの信号です. | 8 |
この時点でのMainモジュールを下に示します.
module Main(iAddr, iData, iManualClk, iWriteEn, iClk, oPattern, oDigit, oDmdClr, oDmdClk, oDmdSeg, oDmdColumn); input [7:0]iAddr; input [7:0]iData; input iManualClk; input iWriteEn; input iClk; output [7:0] oPattern; output [3:0] oDigit; output oDmdClr; output oDmdClk; output [3:0]oDmdSeg; output [7:0]oDmdColumn; endmodule
内部信号
ポート信号以外に以下にある内部信号をwireにしておきます.
信号名 | 役割 | 幅 |
outputData | i4001Blockから出力されるデータを受け取る信号 | 8 |
loadForDmd | DMDへ表示するデータを書き込むタイミングとなる信号 | 1 |
columnIdForDmd | DMDに表示する場所を表すデータで,0が右端,31が左端 | 5 |
columnForDmd | DMDに表示するデータで下位ビットがDMDの上,上位ビットがDMDの下 | 8 |
clkForRam | i4001Blockへ送るクロック信号 | 1 |
addr | i4001Blockへ送るアドレス信号 | 8 |
loadForDmdAtRead | RAMに記憶したデータを読み込み,そのデータをDMDへ送る時にきっかけとなる信号 | 1 |
加えて,RAMに記憶したデータを読み込み,DMDへ表示するとき,そのデータをどの列に書き込むか指定するための信号としてcntForColumnIdをregで作成しておきます.DMDには32列あるため,幅は5ビットとなります.この信号は後ほど,loadForDmdAtReadの立下りエッジをトリガにしてカウントアップしていきます.
以上のことをまとめると,次のようになります.
wire [7:0]outputData; wire loadForDmd; wire [4:0]columnIdForDmd; wire [7:0]columnForDmd; wire clkForRam; wire [7:0]addr; wire loadForDmdAtRead; reg [4:0]cntForColumnId = 5'b00000;
i4001ROMをインスタンス化
先ほど生成したi4001BlockモジュールをMainモジュールで利用するため,インスタンス化します.インスタンス化した時に接続する端子の多くは固定もしくはinputポートにありますが,dout,clk,adについては先ほど記述した内部信号にしておきます.結果として次のようにMainモジュール内へ追記します.
i4001Block i4b( .dout(outputData), //output [7:0] dout .clk(clkForRam), //input clk .oce(1'b1), //input oce .ce(1'b1), //input ce .reset(1'b0), //input reset .wre(iWriteEn), //input wre .ad(addr), //input [7:0] ad .din(iData) //input [7:0] din );
書き込み時と読み込み時で信号を分ける
RAMへの書き込み時と読み込み時で信号を切り分ける必要がある信号があります.ここではそれについて考えてみましょう.
loadForDmd
この信号はDMDへ表示するデータを書き込むタイミングとなります.書き込み時にはiManualClk,読み込み時にはloadForDmdAtReadとなります.
columnIdForDmd
この信号はDMDに表示する場所を表すデータです.書き込み時にはアドレス信号iAddrのうち下位5ビット,読み込み時にはcntForColumnIdとなります.
columnForDmd
この信号はDMDに表示するデータです.書き込み時にはiData,読み込み時にはoutputDataとなります.
clkForRam
これはi4001Blockへ送るクロック信号です.書き込み時にはiManualClk,読み込み時にはloadForDmdAtReadを反転した信号となります.
addr
これはi4001Blockへ送るアドレス信号です.書き込み時にはiAddr,読み込み時にはiAddrの上位3ビットの信号とcntForColumnId(全5ビット)の信号を連接した信号となります.
以上の場合分けはすべてRAMの書き込み時と読み込み時で分ければよいため,iWriteEnを条件にした3項演算子を使います.その結果,下のようになります.
assign {loadForDmd, columnIdForDmd, columnForDmd, clkForRam, addr} = iWriteEn ? {iManualClk, iAddr[4:0], iData, iManualClk, iAddr} : {loadForDmdAtRead, cntForColumnId, outputData, ~loadForDmdAtRead, {iAddr[7:5],cntForColumnId}};
DMDへのデータ表示
DMDへデータを表示するため,以下の3つの信号については内部信号を用います.
iLoad
これにはloadForDmd信号を接続します.
iColumnId
これにはcolumnIdForDmd信号を接続します.
iColumn
これにはcolumnForDmd信号を接続します.
それ以外にiClrNは常にクリアしないため1'b1,iRstは常にリセットしないため1'b0を入れておきます.以上を踏まえると下のようになります.
Dmd dmd(.iClk(iClk), .iLoad(loadForDmd), .iColumnId(columnIdForDmd), .iColumn(columnForDmd), .iClrN(1'b1), .iRst(1'b0), .oClr(oDmdClr), .oClk(oDmdClk), .oSeg(oDmdSeg), .oColumn(oDmdColumn));
RAMからデータを読み込み時,ドットマトリクスディスプレイへのデータ送信
RAMへデータを書き込む時にドットマトリクスディスプレイへデータを送信するには,columnForDmdの信号を取り込むタイミングはトグルスイッチSW17(iManualClk)の立ち上がりです.一方,RAMへデータを読み込む時にはドットマトリクスディスプレイへ送信するには,columnForDmdの信号を取り込むタイミングはトグルスイッチではありません.このため,発振器のクロック信号を分周したloadForDmdAtRead信号を作り出し,この信号の立ち上がりをcolumnForDmdの信号を取り込むタイミングにします.従って下のようになります.
DividerForDmdLoad dfdl(.iClk(iClk), .oClk(loadForDmdAtRead));
なお,DividerForDmdLoadモジュールは64分周(iClkが27MHzのため,loadForDmdAtReadは42.1875kHzとなる)するように作成してください.
RAMからデータを読み込み時,ドットマトリクスディスプレイへ指定する列番号
RAMへデータを書き込む時にドットマトリクスディスプレイへ表示するデータの列番号はiAddrの下位5ビット(つまりトグルスイッチ)を用います.一方,RAMへデータを読み込むときにはドットマトリクスディスプレイへ表示するデータの列番号はトグルスイッチではありません.このため,先ほど作成したloadForDmdAtRead信号の立下りエッジをトリガにしてcntForColumnIdをカウントアップしていきます.従って,下のようになります.
always@(negedge loadForDmdAtRead) begin cntForColumnId <= cntForColumnId + 5'b00001; end
7セグへのデータ表示
7セクへはRAMに記憶するデータとアドレスを表示します.これらデータとアドレスはトグルスイッチSW1から16で入力します.具体的には,SW1(下位)からSW8(上位)がアドレス,SW9(下位)からSW16(上位)がデータとなります.そして,DIS3(下位4ビット)とDIS4(上位4ビット)にはデータ,DIS1(下位4ビット)とDIS2(上位4ビット)にはアドレスを表示します.いずれも16進数で表示します.なお,RAMからデータを読み込んでいるときでも,常に7セグにはトグルスイッチに応じた表示がなされます.言い換えれば,RAMの読み書きに関係なくSW1から16に応じた値を7セグに表示し続ける回路を作ればよいといえます.従って,下に示すコードのようになります.
SevenSeg ss(.iClrN(1'b1), .iDis1Num(iAddr[3:0]), .iDis2Num(iAddr[7:4]), .iDis3Num(iData[3:0]), .iDis4Num(iData[7:4]), .iClk(iClk), .oPattern(oPattern), .oDigit(oDigit));
これで4001の中にあるROMは完成したはずです.