第6章:RISC-V CPU(PicoRV32)をFPGAで動かす
いよいよ本講座の醍醐味です。これまで作ってきたのは「決まった動きをする固定の回路」でした。本章では、プログラムで自在に振る舞いを変えられるCPU――RISC-Vの小型コア「PicoRV32」――を自分のFPGAに載せます。しかも、ただ載せるだけでなく、CPUが命令を1つずつ実行していく様子を、自分の目で観察します。「同じCPU回路が、自分のFPGAの上で本当に走っている」を体感しましょう。
この章の進め方(3段階): ①仕組み=CPUが中で何をしているかを知る → ②可視化=CPUが動く様子をLED・7セグで見えるようにする → ③実走=実際にプログラムを走らせて確かめる。各ステップで「なぜそうするのか(考え方)→ だからこう書く」をセットで示します。設計は、理由が分かれば迷いません。
6.1 【仕組み】CPUは中で何をしているのか
CPU(プロセッサ)は、一見ものすごく複雑に思えますが、やっていることの骨格はとても単純です。「命令を1つ取ってきて、解読して、実行する」――これをひたすら繰り返しているだけです。この繰り返しをフェッチ・デコード・実行サイクルと呼びます。
CPUがひたすら繰り返している3ステップ ┌──────────────────────────────────────────────────────────┐ │ ① フェッチ:PC(プログラムカウンタ)が指す番地から命令を取る │ │ ↓ │ │ ② デコード:その命令が「何をしろ」なのかを解読する │ │ ↓ │ │ ③ 実行 :計算する/メモリを読み書きする/LEDを点ける等 │ │ ↓ │ │ PC を次の命令へ進める(+4) ── そして①へ戻る(無限に繰り返す)│ └──────────────────────────────────────────────────────────┘
ここで主役になるのが PC(プログラムカウンタ)です。PCは「次にどの番地の命令を実行するか」を覚えているレジスタで、CPUが1命令進むたびにPCも進みます。つまり――PCが動いている=CPUが命令を進めている=CPUが生きている、ということ。この事実が、後の「可視化」で効いてきます。
これまで(第5章まで):固定の回路
Lチカもカウンタも、回路の動きは作った時点で決まっていました。動きを変えるには、RTLを書き直して合成し直す必要があります。
これから(CPU):プログラムで動く回路
CPUは、回路自体は同じまま、「命令の並び(プログラム)」を変えるだけで振る舞いが変わります。これが決定的な質の違い。回路を焼き直さずにソフトで動きを変えられる――汎用性の源です。
CPUの“体”もLUTとFFでできている: PC、レジスタ(計算用の小さな記憶)、命令を解読する論理、足し算を行うALU……CPUを構成するこれらの部品も、すべて第1章で見たLUTとフリップフロップで組み上げられています。PicoRV32という設計図をFPGAに流し込めば、その“CPUの体”がArtix-7の中に実体として現れる、というわけです。
6.2 【設計方針】CPU本体は無改造、まわりは自分で用意する
なぜそうするのか: CPUは「命令を外部のメモリから読む」前提で作られています。PicoRV32も、自分の中に命令を持たず、「この番地の中身をくれ」とメモリへ要求する口(メモリインターフェース)を持つだけです。だから、CPU本体(picorv32.v)はそのまま使い、命令の置き場(ROM)と番地の振り分け(デコーダ)を私たちが用意する、という役割分担になります。実績あるCPUコアには手を入れず、まわりだけ自作する――これは大きな回路を扱うときの王道の考え方です。
だからこうする: まず、CPUコア本体を公式から取得します(無改造で使います)。
# RISC-V CPUコア本体(picorv32.v)を公式リポジトリから取得(無改造で使用) curl -O https://raw.githubusercontent.com/YosysHQ/picorv32/master/picorv32.v
役割分担(CPUは無改造、まわりを自作) ┌───────────────┐ 「0x08の中身ちょうだい」 ┌────────────────────────┐ │ PicoRV32 │ ───────────────────────────► │ 自作のメモリ係 │ │ (CPU本体・ │ │ ・命令ROM(プログラム) │ │ 無改造) │ ◄─────────────────────────── │ ・番地の振り分け │ │ mem_addr 等で │ 「これだよ(命令/データ)」 │ ・LED用の特別番地(MMIO) │ │ 要求を出す │ └────────────────────────┘ └───────────────┘
6.3 動かすプログラム ― まずは最小の4命令
なぜ最小から: 一度にいろいろやらせると、動かないとき「どこが悪いのか」が分からなくなります。まずは「確実に1つ動く」ものを作り、それから足していくのが鉄則です。ここでは「LED用の番地に1を書く」だけの4命令にします。
なぜハンドアセンブル: 本格的にはC言語などをコンパイラで機械語に変換しますが、それは導入の手間が大きい。仕組みを理解するには、命令と機械語(16進)の対応を手で見るのが一番の近道です。だからまずは手で書きます(ツールチェーンは6.8の発展で触れます)。
番地 命令 意味 機械語(16進) ───────────────────────────────────────────────────────────────────────── 0x00 lui x1, 0x40000 x1 = 0x40000000(LEDの番地) 400000B7 0x04 addi x2, x0, 1 x2 = 1 00100113 0x08 sw x2, 0(x1) 0x40000000 に 1 を書く → 点灯 0020A023 0x0C jal x0, 0 自分へジャンプ(無限ループ) 0000006F
「番地を用意し(lui)、値1を作り(addi)、その番地へ書き込み(sw)、あとは止まる(jal の無限ループ)」。CPUはこの4命令を 0x00→0x04→0x08→0x0C と順に実行し、最後は 0x0C で足踏みします。このPCの歩み(0→4→8→C→C…)を、次の可視化で実際に目で見ます。
6.4 【可視化の設計】PCをどうやって見るか
「CPUが動いている」を見たい。なぜPCを見るのか: 6.1のとおり、PCが進むことがCPUが命令を進めている証だからです。では、どうやってPCを取り出すか。ここで設計上の壁にぶつかります。
設計判断①:PCは外に出ていない → 別の信号で代用する。 PicoRV32の内部にはPC(reg_pc)がありますが、これは外部のピン(ポート)には出ていません。無改造で使う方針なので、内部信号を勝手に引き出すことはしません。そこで公開されているポートを見ると、mem_addr(いまアクセスしている番地)があります。CPUが命令を取りに行く瞬間(mem_instr が1)、この mem_addr はまさにPCそのものです。だから「命令フェッチ中の mem_addr を記録すれば、PCが見える」。CPUには一切手を触れずに済みます。
設計判断②:100MHzでは速すぎる → CPUをわざとゆっくり動かす。 100MHzのままだと、CPUは1秒間に何百万命令も実行してしまい、PCの歩みは目で追えません。そこで第4章で学んだ分周を使い、CPUに数Hz程度の遅いクロックを与えます。すると1命令ずつ、ゆっくりPCが進むのを観察できます。(分周クロックを直接使うのは簡易な方法で、厳密にはClocking Wizard等が本筋――第4章の注記と同じです。観察用と割り切ります。)
表示には、第5章で作った7セグメント表示器(seg7)をそのまま流用します。記録したPCを7セグに16進で出せば、0000→0004→0008→000C と歩く様子が見えます。自分で作った部品が、ここで再利用できるわけです。
6.5 【具体】トップを書く(cpu_top.v)
方針が決まったので、組み上げます。cpu_top.v は、(1)分周で遅いクロックを作り、(2)PicoRV32を無改造で置き、(3)命令ROMと番地デコーダ(LED用MMIO含む)を用意し、(4)フェッチ中のPCを記録して7セグへ出します。
`default_nettype none
module cpu_top (
input wire clk, // 100MHz(W5)
input wire rst, // リセット(btnC:U18、押す=1)
output wire led0, // プログラムが書き込むLED(U16)
output wire [6:0] seg, // 7セグ:PC(フェッチ番地)を16進表示
output wire [3:0] an // 7セグ桁選択
);
// (1) 分周:100MHzは速すぎるので、CPUは遅いクロックで動かす(PCの歩みを見るため)
reg [24:0] slow;
always @(posedge clk) slow <= slow + 1'b1;
wire cpu_clk = slow[24]; // 約3Hz:1〜2秒に1命令ペース(目で追える)
wire resetn = ~rst; // picorv32は負論理。ボタン(押す=1)を反転して合わせる
// PicoRV32 のメモリインターフェース
wire mem_valid, mem_instr;
reg mem_ready;
wire [31:0] mem_addr, mem_wdata;
wire [3:0] mem_wstrb;
reg [31:0] mem_rdata;
reg [7:0] led_reg; // LED用MMIOレジスタ
reg [31:0] pc_disp; // 表示用に記録したPC(フェッチ番地)
// (2) CPU本体(無改造)。乗除算オフの小型構成。
picorv32 #(
.ENABLE_COUNTERS(0), .ENABLE_COUNTERS64(0), .ENABLE_REGS_16_31(1),
.TWO_STAGE_SHIFT(1), .BARREL_SHIFTER(0), .COMPRESSED_ISA(0),
.ENABLE_MUL(0), .ENABLE_FAST_MUL(0), .ENABLE_DIV(0), .ENABLE_IRQ(0)
) cpu (
.clk(cpu_clk), .resetn(resetn), .trap(),
.mem_valid(mem_valid), .mem_instr(mem_instr), .mem_ready(mem_ready),
.mem_addr(mem_addr), .mem_wdata(mem_wdata), .mem_wstrb(mem_wstrb),
.mem_rdata(mem_rdata),
.irq(32'b0)
);
// (3) 命令ROM(6.3の4命令)。FPGAは初期値つきメモリを作れる。
reg [31:0] rom [0:3];
initial begin
rom[0] = 32'h400000B7; // lui x1, 0x40000
rom[1] = 32'h00100113; // addi x2, x0, 1
rom[2] = 32'h0020A023; // sw x2, 0(x1)
rom[3] = 32'h0000006F; // jal x0, 0
end
// メモリ応答+PCの記録(CPUと同じ遅いクロックで動かす)
always @(posedge cpu_clk) begin
if (!resetn) begin
led_reg <= 8'b0; mem_ready <= 1'b0; mem_rdata <= 32'b0; pc_disp <= 32'b0;
end else begin
mem_ready <= 1'b0;
if (mem_valid && !mem_ready) begin
if (mem_instr) pc_disp <= mem_addr; // 命令フェッチ中の番地=PC を記録
if (mem_addr == 32'h4000_0000) begin
if (mem_wstrb[0]) led_reg <= mem_wdata[7:0]; // LED書き込み
mem_ready <= 1'b1;
end else if (mem_addr[31:4] == 28'b0) begin
mem_rdata <= rom[mem_addr[3:2]]; // 命令ROMを返す
mem_ready <= 1'b1;
end else begin
mem_rdata <= 32'h0000_0013; // それ以外はNOP
mem_ready <= 1'b1;
end
end
end
end
assign led0 = led_reg[0];
// (4) PCの下位16ビットを7セグに16進表示(第5章の seg7 を流用)
seg7 u_disp (.clk(clk), .sw(pc_disp[15:0]), .seg(seg), .an(an));
endmodule
(1) 遅いクロック
slow[24] で約3Hz。CPUとメモリ係を、この遅いクロックで動かします。だからPCがゆっくり進み、目で追えます。速さは slow のビットで調整可能。
(2) CPUは無改造
picorv32 を設定だけ与えてインスタンス化。中身には触れません。mem_addr という公開ポートを使って、外からPCを覗きます。
(3) ROM+デコーダ+MMIO
番地0x00〜0x0Fは命令ROM、0x40000000はLED、それ以外はNOP。CPUの「中身ちょうだい」に、この係が答えます。
(4) PCを7セグへ
フェッチ中の番地を pc_disp に記録し、第5章の seg7 で16進表示。自作部品の再利用です。
6.6 【確認=可視化+実走】CPUが歩くのを見る
Vivadoで、3つのファイル(cpu_top.v・picorv32.v・第5章の seg7.v)を design source に追加し、cpu_top をトップに設定します。制約は次の .xdc を使います。
## cpu_top 用ピン制約
set_property CFGBVS VCCO [current_design]
set_property CONFIG_VOLTAGE 3.3 [current_design]
set_property -dict { PACKAGE_PIN W5 IOSTANDARD LVCMOS33 } [get_ports clk]
create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports clk]
set_property -dict { PACKAGE_PIN U18 IOSTANDARD LVCMOS33 } [get_ports rst]
set_property -dict { PACKAGE_PIN U16 IOSTANDARD LVCMOS33 } [get_ports led0]
## 7セグ セグメント(a..g)
set_property -dict { PACKAGE_PIN W7 IOSTANDARD LVCMOS33 } [get_ports {seg[0]}]
set_property -dict { PACKAGE_PIN W6 IOSTANDARD LVCMOS33 } [get_ports {seg[1]}]
set_property -dict { PACKAGE_PIN U8 IOSTANDARD LVCMOS33 } [get_ports {seg[2]}]
set_property -dict { PACKAGE_PIN V8 IOSTANDARD LVCMOS33 } [get_ports {seg[3]}]
set_property -dict { PACKAGE_PIN U5 IOSTANDARD LVCMOS33 } [get_ports {seg[4]}]
set_property -dict { PACKAGE_PIN V5 IOSTANDARD LVCMOS33 } [get_ports {seg[5]}]
set_property -dict { PACKAGE_PIN U7 IOSTANDARD LVCMOS33 } [get_ports {seg[6]}]
## 7セグ 桁選択(an)
set_property -dict { PACKAGE_PIN U2 IOSTANDARD LVCMOS33 } [get_ports {an[0]}]
set_property -dict { PACKAGE_PIN U4 IOSTANDARD LVCMOS33 } [get_ports {an[1]}]
set_property -dict { PACKAGE_PIN V4 IOSTANDARD LVCMOS33 } [get_ports {an[2]}]
set_property -dict { PACKAGE_PIN W4 IOSTANDARD LVCMOS33 } [get_ports {an[3]}]
合成 → Implementation → ビットストリーム → 書き込み(手順は第4章と同じ)。書き込むと、いよいよCPUが歩く様子が見えます。
7セグに表示されるPC(数秒ごとに進む)と、LED0の様子 ─────────────────────────────────────────────────────────── 0000 → lui を実行中(番地0x00) LED0: 消灯 0004 → addi を実行中(番地0x04) LED0: 消灯 0008 → sw を実行中(番地0x08)★ LED0: 点灯!(ここで0x40000000へ書込) 000C → jal を実行中(番地0x0C) LED0: 点灯のまま 000C → 0x0C で無限ループ(PCは進まない) LED0: 点灯のまま ─────────────────────────────────────────────────────────── = 自分のFPGAの上で、CPUがプログラムを1命令ずつ実行している!
これがこの講座の醍醐味: 7セグの数字が 0000→0004→0008→000C と進むのは、CPUがあなたの書いたプログラムを1命令ずつ実行している姿そのものです。そして 0x08 の sw を実行した瞬間にLED0が点く。「ソフトの1命令」が「回路の動き(LED点灯)」になる瞬間を、自分の目で確かめられます。リセット(btnC)を押せば、PCは 0000 に戻り、また最初から歩き出します。
6.7 どれだけの資源を使ったか ― CPUでも数%で載る
Implementation後、「Report Utilization」で利用率を見ましょう(数値は構成・合成で変わるので自分のレポートで確認)。CPUは第4章のLチカより桁違いに大きい一方、Artix-7にはまだ余裕があります。
設計 LUT(目安) FF(目安) XC7A35T比(LUT)
───────────────────────────────────────────────────────────────────
Lチカ(第4章) 数個 約26 ごくわずか
PicoRV32(本章, 小型) 数百〜1000弱 数百 数% 程度
XC7A35T 全体 20,800 41,600 100%
※実数は自分のVivado利用率レポートで確認。
CPUという大きな回路ですら、Artix-7全体の数%。「自分のFPGAには、CPUを載せてもまだまだ余裕がある」と実感できます。残りの資源で、周辺回路(タイマー、通信、表示)を足していけば、自分だけのSoC(System on Chip)に育てられます。
🔧 さらに先へ:点滅プログラム/本物のツールチェーン/自分のSoCへ
ここまで来たら、もう「FPGAで自在に遊ぶ」入口に立っています。次の一歩を示します。
※PicoRV32の“徹底分解”は実践編で: 本章はあくまでさわり――「CPUが自分のFPGAで本当に走る」を体験するところまでです。PicoRV32の内部(パイプライン、命令デコードの詳細、周辺回路の作り込み)や、それを土台にした本格的な応用は、別途の実践編(新講座)で深く扱う構想です。まずは本章で「走った!」を味わってください。
- 点滅プログラムにする: 「LEDを点ける → 一定回数ループして待つ → 消す → 待つ」を命令で書けば、CPU制御の点滅になります。命令数が増えるのでROMの語数を増やします(ハンドアセンブルでも書けますが、だんだん大変になります)。
- ツールチェーンを導入: 命令が増えたら、RISC-V用コンパイラ(
riscv32-unknown-elf-gcc等)でC言語からプログラムを作り、ROMへ流し込めます。「自作CPUで好きなソフトを動かす」世界です。 - 入力・周辺を足す: スイッチを
mem_addrの別番地に割り当てれば、CPUがスイッチを読んで動きを変えられます。タイマーやUART(通信)を足せば、自分だけのSoCになります。
6.8 この章のまとめ
- 仕組みを知った: CPUは「フェッチ→デコード→実行」を繰り返し、PCが進むことが“動いている”証であることを理解した。
- 設計方針を立てた: CPU本体は無改造、ROM・デコーダ・MMIOを自作、という役割分担。可視化はPCを直接出せないので
mem_addrで代用し、速すぎるので分周した、という理由つきの判断を踏んだ。 - 可視化して実走した: 7セグでPCが
0000→0004→0008→000Cと歩くのを見、sw実行の瞬間にLEDが点るのを確かめた。 - 資源を読んだ: CPUでもArtix-7の数%で載り、まだ余裕があることを確認した。
これで、本講座の本編(第1〜6章)は完走です。自分でRTLを書き、部品で遊び、ついにCPUを載せて走らせ、その様子を観察するところまで来ました。FPGAを「使える」ようになったと、胸を張ってよい到達点です。
★この先に見えている“夢”
本章で動かしたPicoRV32は、まだ最小構成です。でも、これを土台にして、まわりにタイマーや通信、表示、独自の計算回路を足していけば――それはあなたたち自身のオリジナルなRISC-V SoC(System on Chip)になります。FPGAなら、その設計を何度でも書き換えて、実機で動かして、納得いくまで育てられます。
そして、FPGAで固めたそのRTLは、ASIC設計講座(LibreLane)でもんで(合成・配置配線・検証して)、本当にテープアウト(製造)することができます。自分たちで設計したオリジナルチップが、現実のシリコンになる――それが、この講座がその先に見ている夢です。その具体的な道筋(FPGAで作ったRTLをLibreLaneへ渡す方法)は、次の第7章【連携編】にまとめています。
※この先の2章は“おまけ”です: 第7章【連携編】は、ASIC設計講座(LibreLane)と行き来したい方向けの独立章です(FPGAだけで十分な方は読み飛ばしてOK)。第8章は用語集とトラブル対処のリファレンスです。