🐕

TangNano9kにRISC-Vを実装した

2022/12/24に公開

この記事はEEIC Advent Calendar 2022の23日目の記事です。

はじめに

この記事では秋月電子などで2500円程度で購入可能なTang Nano 9Kに大学の実験で実装したRISC-V(RV32I) Coreを移植した方法を残します。移植した上でCoreMarkのベンチマークが動作するところまでを目標とします。

移植したいCPUは5段ステージ、フォワーディングや分岐予測がついたものです。まず前提となるCPUの構成とそれが実験でどのようなFPGAに実装されていたかをTang Nano 9Kとの比較で確認したあと、前調べとして、5段ステージ分けされているがパイプライン化されていないCPUを移植します。その後、パイプライン化されたCPUを移植し、CoreMarkのスコアを出してPicoRV32と軽く比較しようと思います。

なお、今回のCPUの実装は以下で公開しています。
https://github.com/yutyan0119/rv32I-TangNano9K

移植したいCPUの構成

移植したいCPUのブロック図は以下です。
CPUのブロック図

  • 5段パイプライン
  • 各種フォワーディング有り
  • 2bit分岐予測器付き
  • フォワーディング及びALUの入力選択はDecodeステージで行う

CPUについてもっと詳しいことを知りたい人は以下のスライドの第一世代と第三世代CPUを見てください。
https://speakerdeck.com/yutyan0119/maikuropurosetusanoshe-ji-toshi-zhuang-shi-yan-cheng-guo-suraido

搭載するFPGAの比較

実験で使用したFPGA

Nexys Video Artix-7

  • LUT(4-6 input)33650個
  • BRAM 13Mbits
  • Clock 450Mhz
  • DDR3 512MB
  • 秋月参考価格64,000円
    などなど…

今回移植するFPGA

Tang Nano 9K

  • LUT(4 input) 8640個
  • BRAM 468Kbits
  • Clock 27Mhz
  • SDRAM 64Mbits
  • 秋月参考価格2480円
    などなど…

FPGAの核となる要素はLUTです。LUTとはLook Up Tableの略で、任意のn入力論理関数が実現できます。4inputなら2^(2^4)通りの論理回路が実現できます。これが多いほど複雑な回路を組むことが出来ます。今回使用するTangNano9KはLUTが4inputのみでかつ数も4分の1ほどしかありません。

また、FPGA上にメモリが必要なときに使われるBRAMも重要な要素です。今回使用するTangNano9Kは1分の3ほどしかBRAMを持っていません。

クロックは計算の速度をそのまま表していると思えば大丈夫です。当然安いほうが遅いです。

以上のように、今回は結構貧相なFPGAの上にCPUを実装することになります。

CPUを実装しよう(前調査編)

とりあえず下調べとして、パイプライン化されていないCPUを実装していこうと思います。このCPUは分岐予測もなく、フォワーディングもないため、必要とするリソース量が比較的少なく、簡単に移植できることが期待できます。
まずは何も考えずにソースファイルをコピペしてビットストリーム(FPGAをどう使うかのコードみたいなもの)を生成してみます。

すると予想通り以下のような警告が出てきます。
BRAM使いすぎの警告

The number(194) of BSRAM in the design exceeds the resource limit(26) of current device.

端的にBRAMが足りねえよと怒られました。このようになった原因は、実験の際のCPUのメモリマップにあります。実験のwikiを見返すと、以下のようにしてRAM及び命令メモリを実装するように指示されています。

アドレス サイズ 内容
0x00000-0x08000 32KiB 未使用
0x08000-0x10000 32KiB .text(ROM)
0x10000-0x18000 32KiB .rodata+.data+.bss+.comment(RAM)
0x18000-0x20000 32KiB stack(RAM)

実際の実験では、実装の簡略化のためにROM/RAMともに0x00000-0x20000を取りうるように以下のように実装していたのでした。

reg [31:0] mem [0:'h8000];

これ1つで128KiB = 1024 Kbitsを使うので、他にBRAMを使うものがなくてもFPGAのresourceをはるかに超えています。おまけに2つもあるなんてとてもじゃないですが実装できません。そこで、今回は大人しく以下のように…は出来ません。

reg [31:0] rom [0:'h2000];
reg [31:0] ram [0:'h4000];

これ2つで768Kbitsなので当然ダメです。ここは諦めて以下に少ないメモリにプログラムが載せられるかを検討します。

現状のリンカスクリプト(プログラムをメモリのどこに載せるかを示してくれる)を載せると以下のようになっています。

OUTPUT_FORMAT("elf32-littleriscv");
OUTPUT_ARCH("riscv")

ENTRY(_start);
MEMORY {
    ROM(rxai) : ORIGIN = 0x08000, LENGTH = 32k
    RAM(wa) : ORIGIN = 0x10000, LENGTH = 32k
}

SECTIONS
{
    .text : { *(.text) } > ROM
    .rodata : { *(.rodata) } > RAM
    .data : { *(.data) } > RAM
    . = ALIGN(4);
    __bss_start = .;
    .bss : {*(.bss)} > RAM
    __bss_end = . ;
    .comment : { *(.comment) } > RAM
}

これのROM, RAMをそれぞれ変えて、どこまで小さく出来るか見ていきましょう。二分探査でもすればよしです。まずROMを小さくして20kにしたところ確か2040byte足りねえよみたいなメッセージが出ました。プログラム本体(データ除く)は、どうやら22KiB以上ないと載りきらないようなので、一旦キリよく24KiBにします。RAMの方は打って変わって1KiBで怒られはしましたが、2KiBでOKなようです。

ここでRAM, ROMのメモリサイズを変え、リンカスクリプトとメモリを以下のように書き換えましたが、シミュレーションが通りません。RAMのサイズを変えたときにおかしくなったので、RAMの設定の仕方に問題がありそうです。

MEMORY {
    ROM(rxai) : ORIGIN = 0x100, LENGTH = 24k
    RAM(wa) : ORIGIN = 0x6100, LENGTH = 2k
}
reg [31:0] ram [0:'h1a40];
reg [31:0] rom [0:'h0200]; 

一応、コンパイルしたELFファイルの中身を見ておきます。

readelf -a prog.elf
# ELFファイルのセクションヘッダのアドレスを見る
セクションヘッダ:
  [番] 名前              タイプ          アドレス Off    サイズ ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000100 000100 004d0c 00  AX  0   0  4
  [ 2] .text.startup     PROGBITS        00004e0c 004e0c 000ae4 00  AX  0   0  4
  [ 3] .rodata           PROGBITS        00006100 006100 00025c 00   A  0   0  4
  [ 4] .rodata.str1.4    PROGBITS        0000635c 00635c 000517 01 AMS  0   0  4
  [ 5] .data             PROGBITS        00006874 006874 00000c 00  WA  0   0  4
  [ 6] .sdata            PROGBITS        00006880 006880 000018 00  WA  0   0  4
  [ 7] .sbss             NOBITS          00006898 006898 000008 00  WA  0   0  4
  [ 8] .comment          PROGBITS        000068a0 006898 00001b 01  MS  0   0  1

ぱっとみ収まってそうです。今回、Coremarkの開始時に"Start Benchmark"という文字列が表示されるようにしてあるのにそれすら表示されないのはなにか問題がありそうです。色々考えた結果、スタックポインタの値を変更しわすれていたことがわかりました。

スタートアップルーチンの最後にスタックポインタに値を代入する部分があるので、ここを変えてやります。メモリの最後尾は0x6100 + 2KiB = 0x6900です。

li sp,0x6900

これでシミュレーションを実行したところ以下のような表示が出ました。

stack overflow

これは原義stack overflowですね。さっきのELFファイルの中身を見たときに.commentセクションが0x68a0からはじまって長さが0x08であることから、.commentセクションは0x68a8まであることがわかります。今のRAMは0x6900までしか取れないので、残るは96bitsです。12byte。それは足りないわ。

何KiB確保したらいいのかはわかりませんが、10KiB確保してもビットストリーム生成時に怒られないことが確かめられたので、10KiB確保しましょう。リンカスクリプトは以下のままです。

MEMORY {
    ROM(rxai) : ORIGIN = 0x0100, LENGTH = 24k
    RAM(wa) : ORIGIN = 0x6100, LENGTH = 2k
}

このときのスタックポインタの初期値は0x6100+10KiB = 0x8900なので、スタートアップルーチンには以下のように書いておきます。

li sp,0x8900

また、verilogのRAM/ROMは以下のようになります。

reg [31:0] ram [0:'h1a40];
reg [31:0] rom [0:'h0a00];

これでシミュレーションしたところ、無事シミュレーションが通ったので、実機に焼いて動作確認です。

coremark

Iterations/Secが7という驚きの低さですが、CPI(Cycles per Instruction)が5で固定かつ27MhzのCPUならこんなもんでしょう。単純な仕組みのCPUで動作が確認出来たので、次は本命のパイプライン化したCPUで動作させていきましょう。

CPUを実装しよう(本命編)

問題のリソース問題(主にBSRAM)はクリアしたので、本命のパイプライン化したCPUを実装していきましょう。といってもソースコードはもうあるので、これをそのまま貼り付けてメモリ周りの修正をさっきやったようにするだけでOKです。

さーてビットストリームを生成するか、としたところ以下のようなエラーが出ました。

The number(27) of BSRAM in the design exceeds the resource limit(26) of current device.

え??BSRAM足らない??なぜ??と思って調べたところ、分岐予測するモジュール内に予測内容を保持するregisterを予測内容用・PC一致判定用でそれぞれ128個持っており、これがBSRAM判定されたためにさっきと違ってリソースが足りない判定出てるっぽいです。ここで取りうる選択は2つあります。

  1. 分岐予測を簡素化
    1. 常に分岐しない予測や常に分岐する予測にすればリソースが減るが性能は落ちる
  2. RAMを減少させる

1は性能が下がるのでここでは論外として2の方針で行きます。(内心またRAMの最適容量を決めるところに戻るのかという気持ちはあるが)

まぁ、BSRAM要求量を1つ減らすだけやから1KiBずつ減らせばすぐ見つかるやろの精神で1KiBずつ減らしていったところ7KiBでようやくビットストリームが生成出来ました。最終的なメモリマップや、スタートアップルーチンなどなどは以下のようになります。

アドレス サイズ 内容
0x0000-0x0100 256B 未使用
0x0100-0x6100 24KiB .text(ROM)
0x6100-0x6900 2KiB .rodata+.data+.bss+.comment(RAM)
0x6900-0x7d00 5KiB stack(RAM)
MEMORY {
    ROM(rxai) : ORIGIN = 0x0100, LENGTH = 24k
    RAM(wa) : ORIGIN = 0x6100, LENGTH = 2k
}
li sp,7d00
reg [31:0] rom [0:'h1a40]
reg [31:0] ram [0:'h0900];

これで一旦シミュレーションしたところ通ったので実機で動作させてみます。

coremark2

Iterations/Secが28と4倍になりました。分岐予測が100%成功すると5倍なはずなので、まぁまぁ良い成績と言えそうです。

PicoRV32との比較

Coremarkの結果や要求リソース数を比較してみようと思います。なお、PicoRV32を自前でTang Nano 9Kに実装する時間が取れていないので、使用したリソース量はInterface 2022年12月号別冊付録「2500円ボードで始めるFPGA開発」の応用編第4章 「RISC-V CPUコアの搭載とマトリクスLEDの制御」を参考にします。また、CoreMark/Mhzの値についてもPicoRV32はA Catalog and In-Hardware Evaluation of Open-Source Drop-In Compatible RISC-V Softcore Processorsを参考にします。

名称 PicoRV32 5段ステージCPU 5段パイプラインCPU
LUT 1951/8640 1360/8640 2748/8640
FF 799/6693 328/6693 783/6693
CLS 1207/4320 836/4320 1790/4320
BSRAM 3SDPB/1pROM 10SDPB/16pROM 7SDPB/16pROM
最高動作周波数 57.198Mhz 27.493Mhz 32.089Mhz
CoreMark/Mhz 0.4くらい(目視) 0.259 1.04

5段ステージCPUは構成自体は簡単なので要求リソース量が非常に少ないです。PicoRV32は内部でFFを多用している点でFFの数が多く要求されています。BSRAMについて、参考にしたPicoRV32はROM/RAMともに2KiBでの実装だったのでまぁこんなもんだろうという感じです。タイミング制約の点ではPicoRV32は凄く優秀です。JALR命令に6サイクルかけるとか、ALUの命令では3サイクルしか使わないなど、命令ごとにサイクルを可変にすることで、なるべくクロックを高められるようにしているのがわかります。5段ステージと5段パイプラインのタイミング制約の違いはALUの入力を選択するマルチプレクサがDecodeステージに入っているかExecuteステージに入っているかの違いでしょう。

今後の展望

マイクロカーネルでもなんでもいいのでOS動かしたい!!流石にRAMが足りなくなりそうなのでもう少し良いFPGAを買わないといけなくなりそうです…。あと普通にM命令やCSR命令を実装しないといけなくなるはず。

おわりに

この記事の多くのリソースはEEICの後期実験の1つである、「マイクロプロセッサの設計と実装」から得られています。実験を計画していただいた入江先生、及びTAの方々に深く感謝します。

参考文献

Discussion