VerilogHDL

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での実装の具体ポイント

  1. パイプライン化

    • IF, ID, EX, MEM, WBの5段階

    • ハザード対応:データハザードはフォワーディングで解決、分岐ハザードはバブル挿入

  2. メモリ

    • instr_memdata_mem は FPGA Block RAM にマッピング

    • Verilogでは reg [31:0] ram[0:N-1]; で宣言し、(* ram_style = "block" *) を付与するとBRAMに配置されやすい

  3. バス設計

    • CPUと周辺モジュール間は単純な バスインタフェース(Wishbone/AXI-liteなど)で接続

    • CPUはロード/ストア命令でバスアクセス

  4. 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


? 次のステップとしては:

  1. 5段階パイプラインCPUの具体Verilog

  2. Block RAMに対応した命令/データメモリ

  3. 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 ペリフェラル + ソフトドライバ)。どうしますか?