lyricalmagical が 2026年10月05日22時27分46秒 に編集
初版
タイトルの変更
FPGAでRISC-V CPU(FemtoRV)を実装してみる[Tang Nano 9K]
タグの変更
FPGA
Tang-Nano
CPU
RISC-V
メイン画像の変更
記事種類の変更
製作品
ライセンスの変更
(CERN-OHL-P 2) CERN Open Hardware Licence Version 2 - Permissive
本文の変更
## 目標 FPGAに小規模なCPUを乗せて、Cで書いたプログラムを動かしたい。とりあえずCPUからGPIO触れればいい。 ## 概要 FPGAでハードウェアロジックで実装するにはちょっとめんどくさい処理など、簡易なCPUを乗せたい場合があります。 だいたい、「ちょっとめんどくさい処理」なので、アセンブラでゴリゴリ書くっていうのも避けたい感じです。 ですので、C言語などでコンパイルできる環境が簡単に構築できるものがいいです。 まあCH32Vなどを外に持つのが一番コスパよさそうな気はしますが、FPGA内部にCPUを持つメリットとしては、「ハードウェアロジックとのインターフェースにGPIOのピン数を気にする必要がない」というのがあります。 128bitパラレルでデータ渡すとかも容易にできますし、デュアルポートRAMを使ってCPUとハードウェア側がバス調停なしに同時にメモリアクセスするとかも可能です。これを外付けCPUでやろうとすると結構困難です。 というわけで、今回はターゲットとして、 - Tang Nano 9K [https://wiki.sipeed.com/hardware/en/tang/Tang-Nano-9K/Nano-9K.html](https://wiki.sipeed.com/hardware/en/tang/Tang-Nano-9K/Nano-9K.html) - FemtoRV のquarkコア [https://github.com/BrunoLevy/learn-fpga](https://github.com/BrunoLevy/learn-fpga) の [FemtoRV/RTL/PROCESSOR/femtorv32_quark.v](https://github.com/BrunoLevy/learn-fpga/blob/master/FemtoRV/RTL/PROCESSOR/femtorv32_quark.v) - 基本的にはWindows11 ただしメインはWSL上 で実験してみました。 ## ハードウェア関連 Tang Nano 9Kの開発環境の使い方は、 「[Tang Nano 9KでLチカ](https://elchika.com/article/d13ec9c3-a92d-418a-8550-8701dd7e75f1/)」にまとめてありますので参考にしてください。 今回、PLLを使用していますので、その部分のみ記載します。 ### PLL(rPLL)の設定 GOWIN FPGA DesignerのメニューのTool→IP Core Generatorと進み、ツリーが表示されますので、Hard ModuleのCLOCKを展開します。 その中に、rPLLがあるので選択(ダブルクリック)します。 Clock Frequencyに27.000(入力クロック)を設定し、Expected Frequencyに40.500を設定します。 ちなみにTang Nano 9Kの性能として、だいたい40MHz程度でしかFemtoRVは動かせないようです。 設定したらOKします。  ### RAM/ROM 適当にverilog記述から自動でBSRAMを推測して使用してくれます。 今回使用するFemtoRVコアは、32bitバス幅ですがバイトライトが可能なため、RAMは32bitワード長かつバイトライトアクセスが必要です。 Tang Nano 9K(というかGW1NR-9系)のBSRAMは1個18kbitで26個乗っていますが、(色々詳細を省略すると)これを32bit x 512WordのSRAMとして使用すると、バイトイネーブル(8bitごとの書き込み)ができません。 なので、8bitワード長のSRAMを4ブロック使用する必要があります。 BSRAMを8bitワード長とすると、ワード数は2048ワードになります。これ以上小さくしてもどうせ使用されるBSRAMの個数は減らないので、小さくする意味がありません。 なので、CPU動作用のワークRAMは、2048ワード×8bitワード長×4個なので8KByteが最小サイズ、増やすにしても8KByte単位で増やしていくのが効率的です。 ROMについてもBSRAMが使われますが、バイトアクセス制御は不要(CPUとしては起こりえるはずだが、データバスの要らないところは使わなければよいだけ)なので、32bitワード長x512ワードで2Kbyteが最小単位になります。 というわけで、BSRAMは最低でもRAM用に4つ+ROM用に1つが必要になります。 最初FemtoRVコアが小さいのでTang Nano 1Kでもいけるかなと思ったのですが、BSRAMが足りなくて無理でした(LUTでRAM構築すればできなくもないが、現実的に使える容量にはならない)。 今回はROMも実用性を見て1024ワード(4Kbyte)に設定しました。Tang Nano 9KであればどうせBSRAM余りますし。 ### GPIO CPUから外部リソースにアクセスする手段が必要です。 特定アドレスを読みに行くとGPIOに入力されている値が取得でき、 特定アドレスに書き込むとGPIOに出力される仕組みにします。 ArduinoなどでIOをネイティブで触ったことがある人であればこの辺は一般的な仕組みなのがわかるでしょう。 双方向制御などはめんどくさいのでとりあえずつけていません。 基板上のスイッチはS1とS2の2つあり、S2をGPIO入力として使っています。 また、GPIOの入力は押すとLになるようなアクティブロー設計です。ソフト側で扱いやすくするため、読み出し時に反転して1として扱っています。 気を付ける点は、GPIOからの入力はかならず非同期吸収をする必要があるということです。 非同期吸収とは何かというのは「[[FPGA]非同期設計の考え方](https://elchika.com/article/3cd2c3fd-90da-4c12-b3d1-45924877b5ab/)」を参考にしてください。 ざっくりいうと、GPIOからの入力はシフトレジスタを経由してから内部で使いましょうね、ということになります。 まあ現実的にはやらなくてもおそらく動くんですけどね。 ### ソースコード ```verilog:top.v module top( input RESETn, //pin 4 S1 input CLOCK , //pin 52 27MHz onboard input SW, //pin 3 S2 output reg [5:0]LED // pin 16,15,14,13,11,10 ); wire [31:0] mem_addr; wire [31:0] mem_wdata; wire [3:0] mem_wmask; wire [31:0] mem_rdata; wire pllclk; // ------------------------------------------------------------ // PLL // ------------------------------------------------------------ Gowin_rPLL pll( .clkout(pllclk), //output clkout .clkin(CLOCK) //input clkin ); // ------------------------------------------------------------ // GPIO IN(async) // ------------------------------------------------------------ reg [1:0] s_sw; always @(posedge pllclk) s_sw <= {s_sw[0],SW}; // ------------------------------------------------------------ // GPIO OUT // ------------------------------------------------------------ always @(negedge RESETn or posedge pllclk) if (RESETn == 1'b0) LED <= 6'b111111; else if ((mem_addr == 32'h00100000) && mem_wmask[0]) LED <= ~mem_wdata; //0点灯だと使いにくいので反転 // ------------------------------------------------------------ // ROM // ------------------------------------------------------------ reg [31:0] rom [0:1023]; reg [31:0] rom_rdata; initial begin $readmemh("firmware.hex", rom); end always @(posedge pllclk) rom_rdata<=rom[mem_addr[11:2]]; // ------------------------------------------------------------ // RAM // ------------------------------------------------------------ wire ram_cs; assign ram_cs = (mem_addr >= 32'h00800000 && mem_addr < 32'h00802000)?1'b1:1'b0; reg [31:0] ram [0:2047]; reg [31:0] ram_rdata; always @(posedge pllclk) begin if (ram_cs & mem_wmask[0]) ram[mem_addr[12:2]][7:0] <= mem_wdata[7:0]; if (ram_cs & mem_wmask[1]) ram[mem_addr[12:2]][15:8] <= mem_wdata[15:8]; if (ram_cs & mem_wmask[2]) ram[mem_addr[12:2]][23:16] <= mem_wdata[23:16]; if (ram_cs & mem_wmask[3]) ram[mem_addr[12:2]][31:24] <= mem_wdata[31:24]; end always @(posedge pllclk) ram_rdata<=ram[mem_addr[12:2]]; // ------------------------------------------------------------ // BUS // ------------------------------------------------------------ assign mem_rdata = (mem_addr < 32'h00001000) ? rom_rdata : (mem_addr == 32'h00100004) ? {31'h0,~s_sw[1]} ://PUSH lowだと使いにくいので反転 ram_cs ? ram_rdata : 32'h00000013; //NOP // ------------------------------------------------------------ // CORE // ------------------------------------------------------------ FemtoRV32 #( .RESET_ADDR(32'h00000000) ) cpu ( .clk (pllclk), .mem_addr (mem_addr), .mem_wdata (mem_wdata), .mem_wmask (mem_wmask), .mem_rdata (mem_rdata), .mem_rstrb (), .mem_rbusy (1'b0), .mem_wbusy (1'b0), .reset (RESETn) ); endmodule ``` ## ソフトウェア開発環境関連 ### Cコンパイラのインストール まずWSL(Ubuntu)のインストールが必要です。こちらについては解説しているページが世の中に多数ありますので、そちらを参照してください。以下の手順はまっさらなWSL Ubuntu環境がある前提です。 Ubuntu環境に入ったら、まずはCコンパイラをインストールします。以下のコマンドでインストールできます。 sudo apt update sudo apt install gcc-riscv64-unknown-elf Cコンパイラのインストールはこれだけです。 ### ソースファイルとリンクスクリプト作成 まずメインとなるCソースファイルです。 ```c:main.c void wait(void){ volatile int i; for(i=0;i<100000;i++){ if ((*(volatile unsigned int *)0x00100004) & 1){i++;} } } int main(void) { while (1){ *(volatile unsigned int *)0x00100000 = 0x00000001; wait(); *(volatile unsigned int *)0x00100000 = 0x00000002; wait(); *(volatile unsigned int *)0x00100000 = 0x00000004; wait(); *(volatile unsigned int *)0x00100000 = 0x00000008; wait(); *(volatile unsigned int *)0x00100000 = 0x00000010; wait(); *(volatile unsigned int *)0x00100000 = 0x00000020; wait(); }; return 0; } ``` つぎに、スタートアップ用のアセンブラコードです。基本的に改変する必要はないと思いますが、スタックの設定をしているので、ハードウェアでメモリのアドレスを変更した場合は修正が必要です。 ```asm:start.S .section .text.startup .global _start .type _start, @function _start: la sp, __stack_top call main 1: j 1b ``` 最後に、リンカスクリプトです。こちらもROM/RAM領域の大きさが変わった場合などは修正が必要です。 ```:link.ld ENTRY(_start) MEMORY { ROM (rx) : ORIGIN = 0x00000000, LENGTH = 4K RAM (rwx) : ORIGIN = 0x00800000, LENGTH = 8K } SECTIONS { __stack_top = ORIGIN(RAM) + LENGTH(RAM); .text : { *(.text.startup) *(.text*) *(.rodata*) } > ROM .data : { *(.data*) } > RAM .bss : { *(.bss*) *(COMMON) } > RAM . = ALIGN(4); _end = .; } ``` ### コンパイルなど 以下のコマンドでコンパイルできます。 ``` riscv64-unknown-elf-gcc \ -march=rv32i -mabi=ilp32 \ -nostdlib -nostartfiles \ -T link.ld \ start.S main.c \ -o firmware.elf ``` 出来上がったelfをreadmemhで読み込めるhexに変換します。 ``` riscv64-unknown-elf-objcopy \ -O binary \ --only-section=.text \ firmware.elf firmware_text.bin xxd -e -g4 -c4 firmware_text.bin | awk '{print $2}' > firmware.hex ``` これで、verilogのreadmemhで読み込めるhexファイルができました。 出来上がったhexファイルは、verilogソースファイルと同じ階層(厳密に言うとreadmemhするverilogと同じ階層)に置いてください。 注意する点としては、elfからROM領域(.text部。つまり純粋なプログラムコード部分やconst定数等)しかhexファイルにしていないので、初期値を持つRAM領域はhexファイルに反映されません。 グローバル変数に初期値を持つようなコードや、変数が0初期化されていることを想定するコードは書かないでください。 ※ちなみにこのようなマイコンでは、通常は、RAMに配置する初期値データをROMに保持しておき、起動時にスタートアップ処理でRAMへコピーします。また、ゼロ初期化領域は起動時に0クリアします。こうした処理はリンカスクリプトやスタートアップコードで実現します。完全にソフト領域の話になりますので、動作が複雑になることと本題から逸れることを避けるため、今回は対応していません。 ## 動かしてみる verilogソースのコンパイルと、FPGAへの書き込み方法は、「[Tang Nano 9KでLチカ](https://elchika.com/article/d13ec9c3-a92d-418a-8550-8701dd7e75f1#h_%E3%83%81%E3%83%83%E3%83%97%E3%81%AB%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89/)」の「チップにダウンロード」を参考にしてください。 FloorPlannerのピン設定は、top.vの冒頭のコメントを参照してください。CLOCKのみLVCOM33で、その他はLVCMOS18です。FloorPlannerのその他設定項目はデフォルトで問題ありません。 また、[FemtoRVのソース](https://github.com/BrunoLevy/learn-fpga/blob/master/FemtoRV/RTL/PROCESSOR/femtorv32_quark.v)も配置するのも忘れないでください。  上手く動くと、LEDが順に点灯していきます。 ちなみに速度はforループで適当に稼いでいます。タイマーなどは実装していないので、現段階では速度を正確に指定する方法はありませんね(CPUクロックとアセンブラのステート数を計算するとできますが・・・)。 S1はリセット、S2はGPIO入力として使っています。S2を押している間、GPIO入力はLになりますので、CPUからのリード値はS2を押している間は1になります。 ちなみに、Cソースを書き換えて再コンパイルした場合、GOWIN FPGA Designerはファイル更新を検出しないので、「Synthesize」を右クリックして「Rerun All」を選んでください。「Place & Route」のRerunだけでは反映されません。 ## おわりに とりあえずFPGAにCPUを載せてユーザーロジックとデータを受け渡しするという目標は達成できました。 ここまでできればあとは色々なものに好きなように接続できるかなと思います。 バス調停してDMAなどを実装することもできるとは思いますが、そこになるとややめんどくさいですね。 せっかくのハードウェアロジックなので、メモリ空間にDPRAMを接続して、そこで受け渡しをするほうが簡単でしょう。