← 講義ポータルへ戻る 第8回〜第10回講義:CPU(本講座の醍醐味)

第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.vpicorv32.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命令ずつ実行している姿そのものです。そして 0x08sw を実行した瞬間に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 この章のまとめ

これで、本講座の本編(第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章は用語集とトラブル対処のリファレンスです。