C → FPGA 実行までの設計フロー
C code (main_led.c)
│
▼
RISC-V compiler (riscv32-unknown-elf-gcc)
│
▼
ELF binary (main.elf)
│
▼
[Optional] HEX/BIN for FPGA memory init
│
▼
Verilog SoC design (soc_top.v, led.v, picorv32_wrapper.v)
│
▼
Synthesis & Place-and-Route (Yosys + nextpnr)
│
▼
Netlist → Bitstream (soc_top.bit)
│
▼
FPGA
ステップ 1: Cコード → ELFバイナリ
-
入力:
software/main_led.c -
ツール:
riscv32-unknown-elf-gcc -
目的: CPU(PicoRV32)用のバイナリ生成
# Makefile rule
main.elf: software/main_led.c
riscv32-unknown-elf-gcc -O2 -march=rv32im -mabi=ilp32 -Ttext 0x10000 -o main.elf software/main_led.c
-
出力:
main.elf -
この ELF は PicoRV32 が実行可能な形式
ステップ 2: ELF → HEX/BIN (Optional)
-
FPGA の RAM/ROM初期化用に変換
-
ツール:
riscv32-unknown-elf-objcopy
# Convert ELF to Intel HEX for FPGA
riscv32-unknown-elf-objcopy -O ihex main.elf main.hex
-
FPGA内のメモリにロードするためのフォーマット
ステップ 3: SoCのVerilog設計
-
入力:
soc_top.v,10_led.v,01_picorv32_wrapper.v -
役割:
-
PicoRV32 CPU をラップして接続
-
MMIO LED を接続
-
スタック、メモリマップを定義
-
// soc_top.v: CPU + LED MMIO top
soc_top cpu (.clk(clk), .rstn(rstn), .led(led));
-
出力: FPGA設計としての RTL(Register-Transfer Level)コード
ステップ 4: Synthesis → Netlist → Place & Route
-
ツール:
yosys+nextpnr-ecp5(例: Lattice ECP5 FPGA)
synthesize.sh
#!/bin/bash
# ==========================================================
# synthesize.sh: Verilog -> Bitstream
# ==========================================================
# Step 1: Synthesis with Yosys
yosys -p "
read_verilog 00_soc_top.v 01_picorv32_wrapper.v 10_led.v
synth_ecp5 -top soc_top
write_json soc_top.json
"
# Step 2: Place-and-route with nextpnr
nextpnr-ecp5 --json soc_top.json --textcfg soc_top.txt --package tq144:4k
# Step 3: Bitstream generation
ecppack soc_top.txt soc_top.bit
echo "Bitstream ready: soc_top.bit"
-
役割:
-
Yosys: RTL → ロジックゲートレベルネットリスト
-
nextpnr: ネットリストをFPGAの物理セルに配置(Place & Route)
-
ecppack: 最終的な
.bitビットストリームを生成
-
-
出力:
soc_top.bit→ FPGA に書き込む
ステップ 5: FPGAに書き込み
# Example FPGA flash command (device-specific)
fpgashell -f soc_top.bit
-
CPU は SoC上で ELF バイナリを実行
-
LED がメモリマップに従って点灯
まとめ: 各ステップの役割
| ステップ | 入力 | 出力 | ツール | 目的 |
|---|---|---|---|---|
| 1. C → ELF | main_led.c | main.elf | riscv32-unknown-elf-gcc | CPU用実行バイナリ作成 |
| 2. ELF → HEX/BIN | main.elf | main.hex | objcopy | FPGA RAM初期化用 |
| 3. SoC設計 | Verilog RTL | RTLネットリスト | 設計ファイル | CPU+周辺回路設計 |
| 4. Synthesis + P&R | RTL | Bitstream | yosys + nextpnr + ecppack | FPGA実装用 |
| 5. FPGA書き込み | bitstream | 実機動作 | FPGA programmer | 実際にLEDなどが動作 |
💡 ポイント
-
このフローでは ソフトウェアとハードウェアが並行して流れる
-
Makefile は ソフトウェア用、synthesize.sh は ハードウェア用
-
スタック、MMIO、CPU動作の理解があると、C→FPGAまでの挙動を追いやすい
もし希望なら、次に このフローを図解 して、
-
Cコード → アセンブラ / ELF → SoC / Verilog → ネットリスト / FPGA
までを一枚の図で見せることもできます。
作りますか?
あなた:
ChatGPT:
もちろんです。Cで作ったバイナリ(ELFファイルやHEX/BINファイル)を FPGA上のメモリモジュール(ROM/RAM)に書き込む 方法を、順を追って丁寧に解説します。SoC演習で非常に重要なステップです。
1. 目的の整理
-
FPGA上で CPU が Cプログラムを実行するためには、プログラムをFPGA内部のメモリに置く必要があります。
-
典型的には以下のいずれか:
-
初期化用ROMにHEX/BINを書き込む(Power-on時に自動ロード)
-
外部RAMにプログラムをロードする(FPGA起動後にDMAなどでロード)
-
演習用では通常 ROMに書き込む方法 が簡単です。
2. ELF → HEX/BIN変換
ステップ 2-1: ELF確認
riscv32-unknown-elf-objdump -d main.elf | less
-
ELF内の命令を逆アセンブルして確認できる
-
メモリ上の配置アドレスを把握するのに便利
ステップ 2-2: HEX/BIN変換
# Intel HEX形式
riscv32-unknown-elf-objcopy -O ihex main.elf main.hex
# バイナリ形式
riscv32-unknown-elf-objcopy -O binary main.elf main.bin
-
Intel HEX: FPGAツールで直接ロードできることが多い
-
Binary: 純粋なバイナリデータ(FPGA内部ROM初期化用)
3. Verilogメモリモジュールへの組み込み
FPGA上では、通常 $readmemh または $readmemb を使ってメモリを初期化します。
例: ROM モジュール
module rom #(
parameter ADDR_WIDTH = 12, // 4096 words
parameter DATA_WIDTH = 32
)(
input wire [ADDR_WIDTH-1:0] addr,
output reg [DATA_WIDTH-1:0] data
);
// メモリ本体
reg [DATA_WIDTH-1:0] mem [0:(1<<ADDR_WIDTH)-1];
// 初期化: HEXファイルをロード
initial begin
$readmemh("main.hex", mem);
end
always @(*) begin
data = mem[addr];
end
endmodule
-
ポイント
-
memが ROM/メモリ配列 -
$readmemh("main.hex", mem)が FPGAコンパイル時にバイナリを読み込む -
アドレスを指定して CPU が読み込むと、命令が返る
-
4. CPU(PicoRV32)からの接続例
wire [31:0] instr;
rom rom_inst(
.addr(mem_addr[11:0]), // CPUアドレスの下位12bit
.data(instr)
);
-
CPUの命令フェッチアドレス
mem_addrに基づき ROM から命令が返る -
これで CPUがCプログラムを実行可能
5. 実機FPGAでの手順
-
main.elf → main.hex に変換
-
Verilog ROMモジュールで
$readmemh("main.hex", mem)を記述 -
soc_top.v に ROM を接続
-
synthesize.sh で bitstream 作成
-
FPGAに書き込む
-
CPUがROMからプログラムを読み込み実行
6. 補足ポイント
-
$readmemhは コンパイル/シミュレーション時にファイルを読み込む -
$readmembは同じですが 2進形式 -
FPGAベンダーツールによっては ROMの初期値をHEX/BINから直接指定可能
-
大きなプログラムでは、ブロックRAM(BRAM)を複数使うこともある
💡 まとめ
-
C → ELF → HEX/BIN → Verilog ROM → FPGA
-
$readmemhで簡単に初期化可能 -
CPU(PicoRV32)は ROM のアドレスから命令をフェッチ
-
ソフトウェアとハードウェアを結ぶ重要な橋渡し
もし希望なら、私は main.elf → ROM → FPGA実行までの図解フロー を作って、
CPUが命令をフェッチする様子とスタック/LEDアクセス も可視化できます。
作りますか?