はじめに

ここでは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の原型を作成します.あくまで「原型」ですので,記憶するデータ(最終的にはインストラクションになります)のアドレスをトグルスイッチで代用します.

環境

以下の環境で開発を行いました.

作成する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)に表示します.

作成手順

まずはざっと流れについて説明します.

  1. FPGAのIP Coreを使ってBlockRAMを制御するi4001ROMモジュールを生成
  2. 上記モジュールをテスト動作させるためにMainモジュールを作成
    1. Mainモジュール内でi4001ROMをインスタンス化

以上の順番で作成していきます.

BlockRAMを制御するモジュールを生成

 まずはFPGAのIP CoreでBlockRAMを制御するコードを生成しましょう.IP CoreとはIntellectural Property Coreのことで,FPGAなどに特定の機能をまとめられているコンポーネントのことを指します.今回はBlockRAMですが,そのほかにも通信を行うものや画像を扱うものなどがあります.

ではGowin EDAを立ち上げてください.立ち上げましたら下の図のようにNewProjectを選択します.

01

 

現れたダイアログに対して下記のようにOKボタンを押します.

02

 

今回のモジュールを下の図のようにi4001Blockとします.

03

 

次にターゲットデバイスを選択します.下の図のようにGW1NRシリーズを選んでください.

04

 

これで設定完了です.

05

 

次にIP Coreを生成します.下の図のようにGowin EDA上部にあるアイコンを押してください.

06

 

次にIP Coreの種類を選択します.下の図のようにHard Module⇒Memory⇒Blok Memoryの中にあるSP(Single Port)をダブルクリックします.これら4種の中で最もシンプルなものがこれです.SP以外にはDP(Dual Port)のものなどがあります.ここでPortとは入出力の端子を表していて,1つしかないもの(=Single)や2つあるもの(=Dual)というような違いがあります.

07

 

 次に作成する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を選択しておく必要があります.

08

 

ファイルを追加してよいか問い合わせるダイアログが現れますのでOKを選んでください.

09

 

生成されると下のようにi4001Block.vファイルが出来上がります.このファイルに含まれるi4001Blockモジュールを制御するため,下図の右側にあるようにインスタンスを生成します.

10

 

生成された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を選択してください.

11

 

下の図のようにVerilog Fileを選択してください.

12

 

ここではMainと名付けました.

13

 

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;

書き込み時と読み込み時で信号を分ける

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は完成したはずです.