4. Verilog構造(概要)
? PicoRV32 の合成を通じて学ぶべきこと
1. RISC-V マイクロアーキテクチャの理解
-
RISC-V RV32I の命令セットの基本
-
単周期/パイプライン化しないシンプルな CPU の構造
-
レジスタファイル、ALU、PC更新、ロード/ストア処理
-
命令デコード・制御信号生成の仕組み
? PicoRV32 はシンプルなので、マイクロアーキテクチャ理解の教材として最適。
2. Verilog HDL 記述スタイルとハードウェア実装の対応
-
「always @(posedge clk)」がどのようなフリップフロップに合成されるか
-
組み合わせ回路・順序回路の分離
-
状態機械(FSM)の実装方法
-
配線の遅延やスループットに影響する記述
? PicoRV32 は状態機械ベースで記述されているため解析に向く。
3. SoC 向け CPU コアの設計手法
-
バスインターフェース(メモリ・ペリフェラルとの接続)
-
Ready/Valid ハンドシェイク
-
命令メモリ・データメモリの制御
-
クロックイネーブル・コンフィギュレーション
4. 論理合成ツールの基本操作
(Yosys, Vivado, Quartus, Design Compiler など)
-
シンセシススクリプトの書き方
-
RTL の構造をどう最適化するか
-
Timing/Area/Power のトレードオフ
-
合成レポートの読み方
-
LUT/FF 使用量
-クリティカルパス -
最大動作周波数(Fmax)
-
5. 合成エラーの読み解き・デバッグ
-
ラッチ生成警告
-
未接続ワイヤ・未使用シグナル
-
幅の不一致(bit width mismatch)
-
多重ドライバ(multiple driver)
? 特に PicoRV32 の大量パラメータをいじるとエラーが発生しやすく、実践的。
6. FPGA 実装の流れ
-
ピン割り当て
-
タイミング制約(SDC/XDC 記述)
-
配置配線後のタイミング閉鎖(Timing Closure)
-
ROM/RAM の初期化方法
? PicoRV32 を学習するための “提供すべき学習課題”
以下は、基礎から応用まで段階ごとに進められる構成になっています。
? 【初級】PicoRV32 を読む・動かす
課題 1:PicoRV32 の階層構造を調べる
-
top module の端子を説明せよ
-
mem_valid,mem_readyのプロトコルを説明せよ -
RV32I で使う主要命令と、PicoRV32 内でどの部分が処理するか対応させよ
課題 2:PicoRV32 を合成してリソース使用量を調査
-
FPGA(例:Artix-7)に対して synthesis
-
LUT/FF 使用量をレポート
-
Fmax を確認
レポート例:
-
使用LUT数は何に依存する?(ALU?デコーダ?FSM?)
? 【中級】PicoRV32 の改造・検証
課題 3:命令デコーダ部分の動作をトレース
-
ADD / LW / JAL の実行サイクルを波形で説明
-
ステートマシンの遷移図を描く
課題 4:PicoRV32 の構成パラメータを変更して挙動を比較
-
ENABLE_COUNTERS -
ENABLE_MUL -
ENABLE_FAST_MUL -
ENABLE_REGS_16_31
など
比較レポート:
-
面積はどう変化したか?
-
Fmax はどう変化したか?
-
合成時間は増えたか?
? パラメータ変更は合成の理解に非常に良い。
課題 5:バスインターフェースの簡易 SoC を作成
メモリ(BRAM)1つを接続して “Hello World” を出力する最小 SoC を構築。
要素:
-
ROM に firmware をロード
-
PicoRV32 と BRAM の read/write 接続
-
UART を追加して文字送信
? 【上級】CPU の機能拡張と最適化
課題 6:自作命令(Custom instruction) の追加
-
新しい ALU オペレーションを追加
-
命令デコード → 実行ユニット → 結果書き戻しまで拡張
到達目標:
-
設計・実装・合成という CPU 開発の一連の流れを体験。
課題 7:Timing Closure の改善
-
クリティカルパスを特定
-
パイプラインレジスタを挿入
-
Fmax を向上させる
? まとめ:PicoRV32 合成を通して得られるスキル
| 分野 | 学べる内容 |
|---|---|
| CPU アーキテクチャ | 命令実行の流れ、デコード、ALU、メモリアクセス |
| Verilog HDL | FSM、順序回路、組み合わせ回路の設計 |
| 論理合成 | 最適化、レポート解析、エラー修正 |
| FPGA 実装 | RAM 初期化、制約記述、タイミング閉鎖 |
| SoC 開発 | バス設計、周辺回路接続、システム検証 |
必要なら、
-
学習用資料
-
実践講義スライド案
-
ステップバイステップの教材構成
-
PicoRV32 の動作解析レポートの例
なども作成できます。
続けますか?
4. Verilog構造(概要)
? 全体イメージ(ひとことで)
4段階動作のミニRISC-V CPU
1命令ずつ「命令取得 → 実行 → メモリアクセス → 結果書き戻し」を順番にこなす。
⚙️ 基本構成
| 要素 | 内容 |
|---|---|
| 命令セット(対応) | ADD, SUB, ADDI, LW, SW, BEQ, LUI, JAL, JALR |
| 構造 | 単一コア / 単一メモリI/F(命令とデータ共用) |
| レジスタ | 32本(x0〜x31)※x0は常に0固定 |
| FSMステージ | FETCH → EXEC → MEM → WB(1命令を4サイクルで処理) |
| 主要信号 | pc(命令アドレス), alu_out(演算結果), mem_addr, mem_wdata, mem_rdata, mem_we |
? 動作サイクル(ざっくり流れ)
| ステージ | 主な動作 |
|---|---|
| FETCH | pcのアドレスをメモリに出して命令を読み取る。instr_regに保持。 |
| EXEC | 命令を解読し、ALU演算や分岐先・メモリアドレスを計算。 |
| MEM | LWなら読んだデータを保持、SWならレジスタの内容を書き込む。 |
| WB | 結果(ALU値やメモリデータ)をレジスタに書き込み、pcを更新。次の命令へ。 |
? 命令ごとのざっくり動作
| 命令 | 役割 |
|---|---|
ADD, SUB, ADDI |
演算結果をレジスタに書く |
LW |
メモリからロードしてレジスタへ |
SW |
レジスタの値をメモリへストア |
BEQ |
条件成立なら分岐先にジャンプ |
LUI |
即値の上位20bitをロード |
JAL, JALR |
リンク(戻り先保存)+ジャンプ |
? コード構造(超簡略Verilog骨格)
module rv32i_simple_core(input clk, reset,
output reg [31:0] mem_addr, mem_wdata,
output reg mem_we,
input [31:0] mem_rdata);
reg [31:0] pc, regs[0:31], instr_reg, alu_out, mem_data, next_pc;
typedef enum logic [1:0] {FETCH, EXEC, MEM, WB} state_t;
state_t state;
always @(posedge clk or posedge reset) begin
if (reset) begin
pc <= 0; state <= FETCH; regs[0] <= 0;
end else begin
case (state)
FETCH: begin
mem_addr <= pc;
instr_reg <= mem_rdata;
state <= EXEC;
end
EXEC: begin
// 命令を解読 → ALU演算 or アドレス計算
state <= (isLWorSW) ? MEM : WB;
end
MEM: begin
// LW: 読み込み, SW: 書き込み
state <= (isLW) ? WB : FETCH;
end
WB: begin
// レジスタ書き込み, PC更新
pc <= next_pc_or_pc_plus4;
state <= FETCH;
end
endcase
regs[0] <= 0; // x0固定
end
end
endmodule
? 一言でまとめると…
「1命令を4ステップで順番に処理する最小構成RISC-V CPU」
命令を読んで、計算して、必要ならメモリを触って、結果をレジスタに戻す。
それを淡々と繰り返すだけの“シンプルで教育向け”な構造。
4. Verilog構造(概要)
なるほど、FPGAで実用レベルで動かすSoCを目指す場合は、先ほどの「学習用ミニCPU」から大幅に改良する必要があります。ポイントは以下です:
1. 実用レベルSoC設計のポイント
| 項目 | 説明 |
|---|---|
| CPUコア | 単純な順次実行より、最低限の パイプライン(5段階) を実装。RV32Iに加え、拡張命令(M, A)も検討可。 |
| メモリ構造 | FPGAのブロックRAMを活用。命令メモリとデータメモリは分離(Harvardアーキテクチャ)がおすすめ。 |
| クロックと同期 | すべてのモジュールを単一クロックで同期。FPGAクロックは50〜100MHz程度が一般的。 |
| I/O | UART、GPIO、Timerなどの周辺回路を追加可能。AXIやWishboneバスでCPUと接続すると拡張しやすい。 |
| FPGA適合 | ブロックRAMやLUTに最適化したメモリ、レジスタ配置、タイミング制約を考慮。 |
2. 実用レベルSoCの構成例(FPGA向け)
Top Module (SoC)
│
├─ CPU (RV32I 5-stage pipeline)
│ ├─ IF stage (Instruction Fetch)
│ ├─ ID stage (Decode)
│ ├─ EX stage (ALU)
│ ├─ MEM stage (Data Memory Access)
│ └─ WB stage (Write Back)
│
├─ Instruction Memory (Block RAM, 32-bit word, 4KB〜64KB)
│
├─ Data Memory (Block RAM, 32-bit word, 4KB〜64KB)
│
├─ UART Module (Optional, AXI/Custom Bus)
├─ GPIO Module (Optional)
└─ Timer Module (Optional)
3. FPGAでの実装の具体ポイント
-
パイプライン化
-
IF, ID, EX, MEM, WBの5段階
-
ハザード対応:データハザードはフォワーディングで解決、分岐ハザードはバブル挿入
-
-
メモリ
-
instr_memとdata_memは FPGA Block RAM にマッピング -
Verilogでは
reg [31:0] ram[0:N-1];で宣言し、(* ram_style = "block" *)を付与するとBRAMに配置されやすい
-
-
バス設計
-
CPUと周辺モジュール間は単純な バスインタフェース(Wishbone/AXI-liteなど)で接続
-
CPUはロード/ストア命令でバスアクセス
-
-
FPGA制約
-
クロック周波数制約、タイミング制約(.xdc)
-
FPGAによってはBRAMの初期値をCOE/Hexファイルからロード
-
module fpga_soc(
input clk,
input reset,
input uart_rx,
output uart_tx
);
wire [31:0] pc, instr, alu_out, mem_rdata;
wire mem_write;
// 1. CPU
rv32i_pipeline_cpu cpu(
.clk(clk),
.reset(reset),
.instr(instr),
.mem_rdata(mem_rdata),
.mem_addr(alu_out),
.mem_wdata(alu_out),
.mem_write(mem_write),
.pc(pc)
);
// 2. Instruction Memory (BRAM)
instr_mem imem(.clk(clk), .addr(pc), .instr(instr));
// 3. Data Memory (BRAM)
data_mem dmem(.clk(clk), .addr(alu_out), .wdata(alu_out), .rdata(mem_rdata), .we(mem_write));
// 4. UART
uart uart0(.clk(clk), .reset(reset), .rx(uart_rx), .tx(uart_tx));
endmodule
? 次のステップとしては:
-
5段階パイプラインCPUの具体Verilog
-
Block RAMに対応した命令/データメモリ
-
UARTやGPIOの周辺回路
この順で書くとFPGA上で実際に動くSoCになります。
UART モジュール設計例
以下は、仮想的なメモリマップを使った簡易的な UART モジュール (例えば SoC 上の UART ペリフェラル) を想定しています。「TX レジスタに書き込むだけで送信」という簡易 UART 実装を前提とします。
// UART のレジスタ仮定 (ベアメタル実装を想定)
#define UART_BASE 0x10000000
#define UART_TX_REG (*(volatile uint32_t*)(UART_BASE + 0x00))
#define UART_STATUS (*(volatile uint32_t*)(UART_BASE + 0x04))
#define UART_STATUS_TX_FULL (1 << 0)
// C 実装
void uart_init(void) {
// 必要なら、ボーレート設定等をここでやる
// 例: UART_CTRL = 設定値
}
void uart_send(char c) {
// TX FIFO が空くまで待つ
while (UART_STATUS & UART_STATUS_TX_FULL) {
; // 待機
}
UART_TX_REG = (uint32_t)c;
}
void uart_send_string(const char *s) {
while (*s) {
uart_send(*s++);
}
}
// C++ 実装
#include <string>
class UART {
public:
void init() {
// 初期化 (必要なら)
}
void send(char c) {
// 同じように送信
while (UART_STATUS & UART_STATUS_TX_FULL) {
; // 待機
}
UART_TX_REG = (uint32_t)c;
}
void send(const std::string &s) {
for (char c : s) {
send(c);
}
}
private:
// 必要なら内部状態(バッファとか)をメンバ変数で持てる
};
? 目的・学習ポイント
-
関数 vs メソッド
-
C 実装はグローバル関数。関数呼び出しは直接で、データ (文字列) はポインタで渡す。
-
C++ 実装は
UARTクラスのメソッド。オブジェクトとしての設計ができ、状態や責任 (初期化、送信) をメソッドに分割できる。
-
-
データのカプセル化 (オブジェクト設計)
-
C++ では
UARTクラスを使って、UART に関わる処理 (初期化, 送信) をまとめることができる。将来的にバッファ (リングバッファ)、割り込みハンドリング、FIFO チェックなどをクラス内部に持たせられる。 -
C では構造体+関数、または単純関数で実装するが、責務とデータが分かれておらず、可読性・拡張性で劣る可能性がある。
-
-
可読性・保守性の比較
-
C++ の
send(const std::string &s)は直感的で読みやすい。文字列送信ロジックも明確。 -
C の関数は非常に軽量でシンプルだが、文字列を送る部分 (
while (*s)) などを毎回自分で書かないといけない。
-
⚠️ 実装上の注意・リスク
-
ベアメタル環境でのメモリマップ
-
UART のベースアドレスやステータスビット (TX_FULL など) は、実際の SoC / FPGA 設計に合わせて正しく定義する必要がある。
-
-
同期とブロッキング
-
上記実装では
while (TX_FULL)で待機しているためブロッキング (同期) になる。割り込みや DMA を使った非同期送信を実装する場合は設計が変わる。
-
-
C++ の文字列ライブラリ
-
組み込み環境では
std::stringを使うとメモリ (ヒープ) を使ってしまう可能性がある。実装時にはメモリ確保 + デストラクタ/所有権 (RAII) を慎重に設計する。
-
-
ボーレート設定
-
uart_init()でボーレート (クロック、分周) を設定する必要がある。ここを省略している例は “簡易送信のみ” 用。
-
-
割り込み対応
-
将来的に受信 (RX) や割り込みで送信管理をしたい場合、クラス (C++) 側なら割り込みハンドラをメソッドにしたり、バッファを管理したりと柔軟に作れる。
-
もしよければ、Tang Nano 9K + PicoRV32 向けの具体的な UART モジュール (Verilog+C / C++ コード) を設計するひな形を出せます(UART ペリフェラル + ソフトドライバ)。どうしますか?