公開日:2021/03/20
title.png

15th Season


#15-6 CPU周辺回路を作る

 前回までに、オリジナル16ビットCPUコアMoscoviumが完成しました。ただ、CPU単体では思うようにシミュレーションもできないので、今回はROM、RAMと言った周辺回路を作って行きます。

CPUの単体テスト

 と、その前に、現時点でCPUの単体テストは本当にできないか?と言うと、そうでもありません。ちゃんと単体で検証できるようにしてあるので、その方法をご紹介します。
 まずは、アセンブラが必要です。拙作Olive+のスクリプトとしてMoscoviumアセンブラを作成しましたので、以下にリンクしておきます。

  Moscoviumアセンブラ:asm_mcvm.olv

 上のスクリプトをOlive+で実行すると、アセンブラのソースファイルを問い合わせるウインドウが開きますので、アセンブルしたいソースファイルを指定してください。アセンブラソースの文法に誤りが無ければ、ファイルの拡張子が.datwに変わったファイルにアセンブル結果が出力されます。
 出力されたデータはVerilog-HDLの$readmemh()システムタスクで読み込み可能なデータファイルとなっていて、iverilog等のシミュレータで読み込んで使用することが出来ます。


 アセンブラソースの作り方に関しては、Moscoviumの命令セットマニュアルの最後に、アセンブラの記法が記してありますので、そちらをご参照ください。
 以下、アセンブラソースのサンプルです。

  Moscoviumサンプルプログラム:asm_mcvm.asm

 上のサンプルをご覧いただくと、"asm"疑似命令によって、ファイル名が指定されていて、その拡張子が.vとなっています。拡張子.vはVerilog-HDLの拡張子で、これを指定すると$readmemh()システムタスク用のデータファイルではなく、Verilog-HDLの「組み合わせ論理ROM」コードが生成されます。
 同様に、拡張子を".mif"に指定すると、TD IDEのROMマクロにROMデータとして与えることが可能なMIFファイルを出力することが出来ます。

  アセンブル結果サンプル:asm_mcvm.v (組み合わせ論理ROM、RTL)

  アセンブル結果サンプル:asm_mcvm.datw ($readmemh()システムタスク用データ)

  アセンブル結果サンプル:asm_mcvm.mif (MIFファイル)

 ファイルasm_mcvm.vの中身はVerilog-HDLのモジュール記述なので、これを、完成したオリジナル16ビットCPUコアMoscoviumのフェッチバス(fadr[15:0]、fdat[15:0])に接続すると、この時点でCPUの単体テストが可能となります(以下)。

  // DUT
  wire brdy=1'b1;
  wire [15:0] fdat;
  wire [15:0] bdatr=16'h0000;  // always read 16'h0000
  wire [15:0] fadr;
  wire [15:0] bcmd;
  wire [15:0] badr;
  wire [15:0] bdatw;
  moscovium top (
    .clk(clk),
    .rst_n(rst_n),
    .brdy(brdy),
    .fdat(fdat[15:0]),
    .bdatr(bdatr[15:0]),
    .fadr(fadr[15:0]),
    .bcmd(bcmd[2:0]),
    .badr(badr[15:0]),
    .bdatw(bdatw[15:0])
  );

  // Combinational Logic ROM
  wire [15:0] romsiz;
  asm_mcvm rom_mat (
    .adr(fadr[11:1]),
    .dat(fdat[15:0]),
    .romsiz(romsiz[15:0])
  );


 上のコードを使って、Moscoviumコアの単体検証を実施しました。
 何と!組み合わせ論理ROM(asm_mcvm.v)は論理合成可能なRTLなので、そのままFPGAボードに書き込むこともできます。


 使い道があるかどうかわかりませんが、参考までに、Moscovium単体のテストベンチを以下にリンクしておきます。

  Moscovium単体テストベンチ:test_ut_mcvm.v

 シミュレーションの実行には、Anlogic社(TD IDE)のセルライブラリが必要となります。Anlogic社のセルライブラリは以下のディレクトリにあるので、必要なセルを探して追加してください。

  <TD IDE>\Anlogic\TD4.6.4\sim\

 <TD IDE>は、TD IDEのインストールディレクトリ(Program Files等)です。

CPUセルフテストプログラム

 ここで、CPUのテスト用プログラムをご紹介しておきます。
 セルフテストプログラムと言って、CPUが自身の命令を使って、自身の動作が期待値通りに動作していることを確かめるプログラムです。

 以下のプログラムは、実行した時の期待値が解ります(自明)。

  ldbiu r0,0x3a
  ldbiu r1,0x53
  add r0,r1  // **


 実行後、r0=0x32+0x53=0x8dで、状態レジスタsrの演算フラグは、N=0,C=0,V=0,Z=0です。
 よって、以下の様に期待値比較すると、コメント「// **」で示したadd命令の動作が期待通りに実行されたかどうか?を知ることが出来ます。

  movfc r2,sr
  cmpi r0,0x8d  // 計算結果r0=0x8dと比較
  bne tstfail
  andi r2,0xf0
  cmpi r2,0x00  // フラグN,C,V,Z=0,0,0,0と比較
  bne tstfail
  // ここまで到達したらテストはpass


 本当は、r1レジスタの値が変化してない事等も確かめた方が検証カバレージは上がります。でも、それを言い始めたら、上のプログラムに関係ないr3〜r7レジスタの値が変わってませんね?まで確かめないとならなくなります。
 計算(add命令)に使用したデータにも依存して、命令実行後の演算フラグ(N,C,V,Z)値が変わるので、データ依存まで追い始めると、キリがありません。
 どこまで本気でテストするか?という、程度問題なわけです。
 今回作成したCPUのセルフテストプログラムは、検証カバレージが70%とか80%とか言う(高)レべルを期待した物ではなくて、「ザっと全体をチェックできればOKで、無いよりはマシ」って具合です(1^^;)。

 ある命令をテストする時には、その命令以外の命令は「正常動作している」前提で、テストしています。この前提があればこそ、「自分で自分の正しさを検証する」という、一見矛盾するテストが可能となっています。
 何か回路記述を修正したり、改修してRTLを変更した時に、他の命令の動作に影響が出ないかどうか、軽く確かめるのに重宝するアイテムです。

 作成したMoscoviumセルフテストプログラムを以下にリンクしておきます。

  Moscoviumセルフテストプログラム:mcvm_self.asm

 リンクしたプログラム(mcvm_self.asm)には、外部にRAMが接続されたことを前提としたテストがある(ldw命令やpopw命令等)ので、まだ、現段階のCPU単体テスト(プログラムROMだけが接続されている状態)では実行できないテストが存在します。
 この段階ではテスト不能な命令の実行部分はコメントにして単体テストを実行し、動作を検証しました。

 やはり、プログラムROMだけ繋いだCPUの単体テストでは限界があります。どんどん周辺回路を作り込んで行くことにしましょう。

ROM

 TD IDEが提供するROMマクロを使って、命令フェッチ用のROMを作って行きたいと思います。
 TD IDEのROMマクロは普通のクロック同期ROMとなっていて、クロック立ち上がりエッジclk↑でアドレスを取り込むと、そのアドレスに記憶されたデータが次のサイクルに出力される仕様です。このようなタイミングで出力されるデータをリードレイテンシ1と呼びます。
 ところが、Moscoviumの基本設計で決めた様に、フェッチバスのアクセス仕様は「リードレイテンシ0」となっています。アドレスが変化したら、同じサイクル内で出力データも変化して、次のclk↑でデータを取り込めるのがリードレイテンシ0のメモリなので、仕様が合いません。

 そこで、ROMマクロに逆相クロック~clkを接続して、アドレスとデータを半サイクルで転送して使います(下図)。

a156_romtmg.png

 本来1サイクルかけてアクセスすべきROMマクロに対して、半分の時間でのアクセスが必要となり、タイミング的には厳しくなります。ただ、fcpu=24MHz程度のクロック周波数であれば、問題なく実装できると踏み👠ました。最終的には、タイミング解析(STA)でメットするのを確かめてOKとします。
 この場合、CPUから見た動作上はリードレイテンシ0と同じことになるので、期待通り動作するはずです。……上図の場合、ROMマクロから見ると、「リードレイテンシ0.5」となっています。

2ポートROM

 ここで終了してしまうと、このROMはプログラムメモリとしてしか使用できません。フェッチバスからのフェッチアクセスには合致しましたが、Moscoviumメモリバスからのオペランド(データ)アクセスとは仕様が合わないためです。
 例えば、シリアルモニタにデータを送ろうと考えた時、"hello, world"などの文字列データを送信するケースが考えられます。この様な文字列データは通常、ROMに記憶しておく物なので、CPUのメモリバス側からもデータとして読み出せる必要があります。

 このためROMには、フェッチバス側からのアクセスと、メモリバス側からのアクセス両方に対応していることが求められ、このような構造を2ポートROMと呼びます。インストラクション用ROMは、2ポートになっている必要があります。

 ただし、普通の1ポートROMマクロを使う都合上、2つのポートからの同時アクセスは不可とします。どちらか一方のポートから排他アクセスすることが前提です。これについては、Moscoviumの制御CTLブロック(ステートマシン)を設計する時に、データリードアクセスとフェッチアクセスが同時に起きない様に制御しておく必要があり、その様にステートマシンを作り込んであります。

 この様に、同時アクセスが無い前提であれば、単に読み出し口が2つになるだけで、各々バスラッパを被せるだけの事ですので、詳細説明は省略します。

ROMマクロ

 ROMマクロはTD IDEの「Toolsメニュー ⇒ IP Generator」で、以下の様にして生成しておきます。

a156_romip.png

 ROMと言いながら、FPGAボードのRAMマクロをリードオンリで使っているだけなので、RAMマクロを消費します。
 こちらで説明した様に、9KビットRAMが64個と32KビットRAMが16個使用可能なので、その中から自由にROMに使用する分を確保できます。上の例では、ひとまず、32KビットRAMを1個だけROMとして割り当て、都合4KバイトのROMマクロとして生成しました。
 上図下の方で、「Load Init File」の所にアセンブラで生成した.mifファイル(例えばasm_mcvm.mif)を指定しておくと、そのデータがROMの値としてFPGAボードに書き込まれます。

上図をよく見ると、「Dual Port ROM」を選択できるようになっています。デュアルポートにすれば、フェッチバスおよびメモリバスからの同時アクセスも可能なのですが、将来、別な用途でデュアルポートROMが必要になることもあろうかと思いますので、温存しておきます。 → 「#17-1 双頭の狼」にてデュアルポートを活用しました。

RAM

 RAMもTD IDEが持っているRAMマクロを使います(下図)。

a156_ramip.png

 バイトアクセスが可能な16ビットバス幅のRAMである必要があるので、その様にRAMマクロを生成します。32KビットRAMマクロ2個で適用可能な8KバイトRAMとしました。

 こちらもクロック同期、リードレイテンシ1と、普通のクロック同期RAMなので、メモリバス仕様のラッパを被せたら終わりです。

バスステートコントローラ

 バスステートコントローラBUSCとは、その名の通りバス状態を制御するモジュールを指します。
 通常、CPUのメモリバス仕様と異なる仕様のメモリにアクセスする場合、バスステートコントローラがバス仕様の打ち換えを実行します。
 今回作成したROM/RAM含め、今後追加するI/Oユニットの類も、基本的にはCPUのメモリバス仕様に準ずる予定なので、この場合、基本的にバスステートコントローラの仕事はありません。今回は、アドレスbadr[15:0]をデコードして各種メモリやI/Oユニットのセレクト信号bcs_***_nを出力するのが唯一の仕事と言っても過言ではありません。


 メモリマップは以下図の通りとします。

a156_memap.png

※バスステートコントローラBUSCが関与するのは上図メモリバス(左)側だけです。
※「#18-7 Moscovium色々拡張」にて、インストラクションRAM領域を拡張しました。

 アドレスのデコードと言っても、やることはこれ(↓)だけです。

  // area
  assign bcs_rom_n=(badr[15:0]<16'h5000)? 1'b0: 1'b1;
  assign bcs_ram_n=(16'h5000<=badr[15:0] && badr[15:0]<16'hf000)? 1'b0: 1'b1;
  assign bcs_iou_n=(16'hf000<=badr[15:0])? 1'b0: 1'b1;


 セレクト信号はどういう訳か負論理(0でアサート、1でネゲート)であることが多いので、負論理としました。後々I/Oユニットのレジスタ空間を設定した場合、I/Oユニット毎にbcs_***_n信号を追加して行くことになります。

 ROMとRAMのセレクト信号(bcs_rom_n,bcs_ram_n)は、先程作ったROM(4Kバイト)およびRAM(8Kバイト)の領域ではなくて、メモリマップのROM(24Kバイト)、RAM(32Kバイト)領域でアサートされるようにデコードしているので、各々のサイズ(4Kバイトと8Kバイト)を超える領域にアクセスした場合、元のメモリのシャドウが見えることに留意してください。

※シャドウ: 例えばRAMの実体は8Kバイトなので、RAMの先頭アドレス0x5000番地に書き込んだデータは、8Kバイト先の0x7000番地からも読み出すことが出来ます。bcs_ram_nの範囲内で、8KバイトのRAMが8Kバイト毎に繰り返し見えることを「シャドウが見える」と表現します。

リセット回路

 実は、FPGAボードで一番良く解っていないのがリセット信号です。どうやってリセットがかかっているのか?

 「#15-1 FPGAボードを買ってみた」で作成したハードウェアLチカ回路ですが、回路最上位でrst_nと言う名前の信号(負論理)を引き込んでいて、この信号が回路全体にリセットをかけています。そして、rst_nをFPGAボードのユーザスイッチ(K16端子)に割り当てています。確かに、ユーザスイッチを押すと、リセットがかかっているみたいですが、FPGAボードに回路を書き込んだ直後など、ユーザスイッチを押していない状態でも、ちゃんとリセットがかかっているように見えます。

 予想としては、FPGAなので、コンフィギュレーション終了時には、何か適切な値がFFにセットされた状態でスタートするのではないか?普通は、FFの値=1'b0でスタートするのではないか?それで、ユーザスイッチ(リセットボタン)を押さずにスタートした時も、それなりに動くのではないか?

 違うかもしれないけどね……。

 と言う事で、正直、どうやってリセットがかかって動いているのか、外から見ていてもさっぱりわからないのですが、少なくとも、ユーザスイッチを押した時には、きちっとリセットがかかってもらわなくては困ります。
 一方、スイッチにはチャタリングと言うのがつきものです。また、スイッチを操作するタイミングはクロックに同期していないので、クロックに非同期な信号でFFにリセットをかけるのはNGです。

 やはり、まともに動かすなら、リセット信号くらいはちゃんと供給しておく必要があるでしょう(↓)。

  // reset signal
  reg rst_n;
  reg [15:0] rst_cnt;
  always @(posedge clk)
    begin
      if (!reset_n)
        begin
          rst_n<=1'b0;
          rst_cnt[15:0]<=16'h0;
        end
      else
        begin
          if (rst_cnt[15:0]==16'hffff)
            rst_n<=1'b1;
          if (!rst_n)
            rst_cnt[15:0]<=rst_cnt[15:0] + 16'h1;
        end
    end


 上の回路記述は、FPGAボードのユーザスイッチから引き込んだ信号reset_n(非同期)から、内部リセット信号rst_n(同期)を生成するための論理です。
 rst_nは、reset_n=0となったclk↑でrst_n=0となって、回路にリセットをかけます。
 一方、reset_n=1に戻った後もrst_n=0の状態は解除されず、リセットrst_nの解除タイミングは16ビットのカウンタrst_cnt[15:0]がオーバフロウ(rst_ovf)したタイミングとなります。16ビットカウンタはclk↑でカウントアップするので、clk @24MHzでは、オーバフロウするまでに65,536/24MHz≒2.73msかかります。
 もし、チャタリングでreset_nが0⇔1でトグルする時間があったとしても、約2.73ms以内なら、もう一度rst_cnt[15:0]が0にリセットされて65,536カウントをやり直すので、チャタリングをキャンセルすることが出来ます。リセット信号にチャタリングが生じないのであれば、65,536カウントも引き延ばす必要は無いので、その場合にはカウント数を減らしても良いです。

※シミュレーションで65,536クロックも待たされるのはしんどいので、手は打っておきましょう

 この回路によって、rst_nの解除タイミングはclkに同期しますので、リカバリタイムやリムーバルタイムなどのタイミングエラーは起きないはずです。

※リカバリ/リムーバル: 同期リセットの場合はセットアップ/ホールドに対応。

 この回路をシステムコントローラSYSCという名前で、追加しておきます。

IDレジスタIDREG

 マイコンを動かすことに直接の関係はありませんが、そのマイコンがどのような物なのか、何かしらIDを読み出すことのできるレジスタを作っておこうと思います。

Addr Name R/W Bit:15〜0 Memo
0xf000 idrgcode R ID[15:0] ID code: (BCD)
Moscovium=0x1150
Moscovium-SS=0x115a
Nihonium=0x1130
Nihonium-SS=0x113a
Tennessine=0x1170
0xf002 idrgvers R VER[15:0] version number
(BCD; major(2)+minor(2))
0xf004 idrgfcpu R FCPU[15:0] fcpu [kHz]: eg.) 24,000
0xf006 idrgedit R EDIT[15:0] edition code
(ASCII; 2 characters)
0xf008 idrgromt R ROMT[15:4] RAME[3:0] rom top address:
 ROMT[15:0]=0x000(0)
eram size:
 2RAME[3:0][Kbyte]
0xf00a idrgroms R ROMS[15:0] rom size [byte]: eg.) 4,096
0xf00c idrgramt R RAMT[15:0] ram top address: 0x5000
0xf00e idrgrams R RAMS[15:0] ram size [byte]: eg.) 8,192

 BUSC出力のセレクト信号はbcs_idrg_nです。上表の範囲でアサートされるので、IDREGユニット内では、アドレスbadr[3:0]をデコードしてレジスタを特定しています。このI/Oユニットはバイトアクセス禁止です。必ずワードアクセスしてください。

ERAM非搭載版では、RAME[3:0]=0です。また、ERAM搭載版のRAMS[15:0]=0xa000(40Kバイト)です。

マイコンを組み上げる

 今回作った周辺回路とMoscoviumを接続すると、一通りCPUが自立実行できるようになるので、色々なプログラムを実行することが出来るようになります。
 マイコンのプロジェクトを作成して、最上位記述を作るステップに移るので、これから作るマイコンの型名を決めておかないとなりません。

型名 意味
MCOC 115 MN 04 08 RAMサイズ: 08=8Kバイト
  99Kバイトを超える場合は3桁(128=128Kバイト)
  1Kバイト未満(=2Nバイト)の場合は、"L"+N
    (L7=128バイト=27バイト)
        ROMサイズ: 04=4Kバイト
  IRAM搭載版は+40=4Kバイト
  エディション:
 MN=minimum edition
 IP=interrupt and pwm output edition
 CP=multiplier and divider co-processor edition
 LA=logic analyzer edition
 等(以下省略、エディション表を参照)
  型番:
  115=Mcの原子番号(Moscovium搭載版)
  113=Nhの原子番号(Nihonium搭載版)
  117=Tsの原子番号(Tennessine搭載版)
  型式:
  MCOC=Moscovium On Chip
  MSOC=Moscovium-SS On Chip
  NHOC=Nihonium On Chip
  NSOC=Nihonium-SS On Chip
  TSOC=Tennessine On Chip

 何か名前が無いと、モジュール名もファイル名も決まりませんから。

 型名「MCOC115MN0408」……は、ちょっと長いので(1^^;)、「MCOC115」でTD IDEのプロジェクトを作成し、最上位モジュールを作ろうと思います。

 タイミング制約は、マルチサイクルパスとか、複数クロック間のデータ転送(フォルスパス)とか、複雑なことは一切していないので、シンプルにクロックclkを設定してやればOKです。


 デザインデータは以下のページでダウンロードできます。

  btn_moscovium.png

 次回、最低限必要なI/Oユニットを設計して、最上位記述を作ります。


 ROM,RAMなど、CPU周辺回路が完成して、CPUが自立できるようになりました。あと一息ですね。次は、定番の周辺I/Oユニットを作り込んで、いよいよ、マイコンの形を完成させたいと思います。


btn_arduino.png btn_moscovium.png

btn_prev.png btn_next.png

home