05.1 FPGA 上 SoC 整合與開源核心比較#
學習目標#
讀完本節後,你應該能夠:
- 說明 SoC(System on Chip)整合的基本架構:CPU、BRAM、UART、GPIO 如何透過匯流排連接。
- 使用 Yosys / nextpnr 開源工具鏈,將 Verilog 設計合成、佈局繞線並產生 bitstream。
- 在 Digilent Arty A7-35T 開發板上實際燒錄並驗證一個最小的 RISC-V SoC。
- 比較四個常見開源 RISC-V 核心(PicoRV32、VexRiscv、Ibex、Rocket)的設計取捨。
5.1.1 為什麼要整合成 SoC?#
前三章我們建立了一個單週期/多週期的 RISC-V 處理器,並在模擬器中驗證。但「處理器」本身不能算是一台電腦——它需要記憶體存放程式與資料、需要輸入輸出介面與外界溝通。SoC 整合就是把處理器與周邊模組接在一起,形成一個可在矽晶(此處為 FPGA)上獨立運作的系統。
一個最小的 SoC 包含:
flowchart LR
subgraph SoC
CPU[CPU 核心\nRISC-V RV32I]
BUS[匯流排互連\n(位址解碼器)]
BRAM[BRAM\n64KB 程式+資料]
UART[UART 16550\n串列輸出入]
GPIO[GPIO\nLED/開關]
CPU <--> BUS
BUS <--> BRAM
BUS <--> UART
BUS <--> GPIO
end
UART --- PC[主機終端機]
GPIO --- Board[板上 LED/按鈕]記憶體映射(Memory-Mapped I/O)#
RISC-V 採用記憶體映射 I/O:周邊的暫存器佔用位址空間的一部分,CPU 用一般的 lw/sw 指令存取。我們採用如下配置:
| 位址範圍 | 裝置 | 說明 |
|---|---|---|
0x0000_0000 – 0x0000_FFFF | BRAM | 64KB,程式與資料 |
0x1000_0000 | UART 資料暫存器 | 寫入送出 byte、讀取收 byte |
0x1000_0004 | UART 狀態暫存器 | bit0: TX busy、bit1: RX ready |
0x1000_0008 | GPIO 輸出 | 控制 LED |
0x1000_000C | GPIO 輸入 | 讀取開關/按鈕 |
位址解碼與匯流排#
最簡單的互連是位址解碼式匯流排:CPU 發出 addr 與 wen/wren,解碼器依高位位元決定哪個裝置被選中,並回傳 rdata 與 ready。以下是最小解碼器:
module bus_decoder (
input wire [31:0] addr,
output wire mem_sel, // BRAM
output wire uart_sel, // UART
output wire gpio_sel // GPIO
);
assign mem_sel = (addr < 32'h0001_0000);
assign uart_sel = (addr >= 32'h1000_0000) && (addr < 32'h1000_0010);
assign gpio_sel = (addr >= 32'h1000_0010) && (addr < 32'h1000_0020);
endmodule
UART 收發器在本書 04 章已實作過,此處只需注意:CPU 時脈遠快於 UART 鮑率(如 100MHz vs 115200bps),因此 CPU 寫 UART 時必須先查詢狀態暫存器(輪詢,polling),確認 TX idle 才能寫下一個 byte——這正是 05.2 節 Hello World 程式的核心邏輯。
BRAM 的推斷(Inference)#
FPGA 內部的 Block RAM 有嚴格的時序特性:同步讀取(一拍延遲)。若我們的 CPU 假設指令記憶體是組合讀取(單週期設計),直接接 BRAM 會有功能問題。兩種解法:
- 修改 CPU 為多週期,讓取指佔一個完整時脈週期(推薦,符合 BRAM 時序)。
- 使用分散式 RAM(LUT RAM)實現組合讀取,但大容量時浪費資源。
這說明了一個重要觀念:微架構設計必須考慮實作平台的物理特性,而非只看 RTL 模擬通過。
5.1.2 開源 FPGA 工具鏈:Yosys 與 nextpnr#
傳統 FPGA 開發依賴廠商工具(如 Vivado)。開源工具鏈讓整個流程透明、可重現、可教學:
flowchart LR
A[Verilog RTL] -->|yosys 合成| B[JSON 網表]
B -->|nextpnr 佈局繞線| C[FASM / 位元流]
C -->|openFPGALoader / fpga-loader| D[FPGA 晶片]
B -.->| formally| E[formal 驗證\nSymbiYosys]- Yosys:將 Verilog 合成為門級網表。可做優化(常數折疊、死碼刪除)、技術映射(techmap)。
- nextpnr:佈局(placement)與繞線(routing),針對特定 FPGA 架構(ice40、ecp5、xilinx 等)有不同後端。
- openFPGALoader:將 bitstream 燒錄至板子。
安裝(macOS/Linux):
# macOS
brew install yosys nextpnr openfpgaloader
# Linux (Debian/Ubuntu)
sudo apt install yosys nextpnr-ice40 openfpgaloader
合成腳本範例(Arty A7,Xilinx 7 系列)#
Arty A7-35T 使用 Xilinx Artix-7,開源流程可透過 nextpnr-xilinx 或 Project X-Ray 支援。以下以合成為例:
# 1. Yosys 合成(目標 7 系列 cell library)
yosys -p "
read_verilog -sv soc_top.v picorv32.v
synth_xilinx -Top soc_top -family xc7
write_edif soc.edif
write_json soc.json
"
# 2. nextpnr-xilinx 佈局繞線(需 Project X-Ray 的晶片資料庫)
nextpnr-xilinx --chipdb arty-35t \
--json soc.json --write soc_fasm.fasm \
--freq 100
# 3. 產生 bitstream 與燒錄
fasm2frames.py --db /usr/share/prjxray-db/artix7 soc_fasm.fasm > soc.frames
xc7frames2bit --part_file arty.xc7a35tsg236-1L.yaml \
--frm_file soc.frames --output_file soc.bit
openfpgaloader -b arty soc.bit
教學提示:初學者若在 Windows 上,可先使用 Vivado 完成相同流程(
synth_design→opt_design→place_design→route_design→write_bitstream),理解概念後再改用開源工具。兩者產生的 bitstream 功能等價。
時序約束#
100MHz 時脈代表 10ns 週期,工具必須確認所有路徑都滿足 setup/hold time。XDC 約束檔範例:
create_clock -period 10.000 -name sys_clk [get_ports clk]
set_property -dict { PACKAGE_PIN E3 IOSTANDARD LVCMOS33 } [get_ports clk]
set_property -dict { PACKAGE_PIN H5 IOSTANDARD LVCMOS33 } [get_ports {led[0]}]
set_property -dict { PACKAGE_PIN A8 IOSTANDARD LVCMOS33 } [get_ports {sw[0]}]
set_property -dict { PACKAGE_PIN D10 IOSTANDARD LVCMOS33 } [get_ports uart_rx]
set_property -dict { PACKAGE_PIN D11 IOSTANDARD LVCMOS33 } [get_ports uart_tx]
5.1.3 最小 SoC 的頂層模組#
將 CPU、BRAM、UART、GPIO 組合成 soc_top:
module soc_top (
input wire clk,
input wire rst_n, // 板上按鈕,低電位重置
input wire uart_rx,
output wire uart_tx,
input wire [3:0] sw,
output wire [3:0] led
);
// ---- CPU 訊號 ----
wire [31:0] mem_addr, mem_wdata, mem_rdata;
wire mem_wen, mem_ren;
wire mem_ready;
// ---- 位址解碼 ----
wire mem_sel, uart_sel, gpio_sel;
bus_decoder u_dec (.addr(mem_addr),
.mem_sel(mem_sel), .uart_sel(uart_sel), .gpio_sel(gpio_sel));
// ---- CPU 核心(此例假設 PicoRV32 介面)----
picorv32 #(
.PROGADDR_RESET(32'h0000_0000),
.STACKADDR(32'h0000_FFFF)
) u_cpu (
.clk(clk), .resetn(rst_n),
.mem_valid(), .mem_instr(),
.mem_addr(mem_addr), .mem_wdata(mem_wdata),
.mem_wstrb(), .mem_rdata(mem_rdata), .mem_ready(mem_ready)
);
// ---- BRAM(同步讀,2KB x 32 = 8KB 範例)----
wire [31:0] bram_q;
bram #(.WORDS(16384)) u_bram (
.clk(clk), .we(mem_sel & mem_wen),
.addr(mem_addr[15:2]), .di(mem_wdata), .do(bram_q)
);
// ---- UART ----
wire [31:0] uart_rdata;
uart_mmio u_uart (
.clk(clk), .rst_n(rst_n),
.sel(uart_sel), .addr(mem_addr[3:2]),
.wen(mem_wen), .wdata(mem_wdata),
.rdata(uart_rdata),
.rx(uart_rx), .tx(uart_tx)
);
// ---- GPIO ----
reg [31:0] gpio_out;
always @(posedge clk) begin
if (gpio_sel && mem_wen) gpio_out <= mem_wdata[3:0];
end
assign led = gpio_out[3:0];
wire [31:0] gpio_rdata = {28'b0, sw};
// ---- 讀資料多工器 ----
assign mem_rdata = mem_sel ? bram_q :
uart_sel ? uart_rdata :
gpio_rdata;
// ---- ready 訊號(BRAM 需 1 拍延遲)----
reg bram_ready_d;
always @(posedge clk) bram_ready_d <= mem_sel & mem_ren;
assign mem_ready = (mem_sel & mem_wen) | bram_ready_d |
(uart_sel) | (gpio_sel);
endmodule
注意幾點:
mem_ready告知 CPU 此筆傳輸已完成;BRAM 寫入可當拍完成,讀取需 1 拍,故用bram_ready_d延遲一拍。- 不同周邊的讀取延遲不同——這是實際 SoC 中常見的問題,商用系統會用 AXI/AHB 等標準匯流排協定處理。本書為了教學簡化為自訂簡單匯流排,但概念相通。
- PicoRV32 的原生記憶體介面(
mem_valid/ready)非常適合這種簡單匯流排,這也是它易於整合的原因之一。
驗證流程#
上板前先用模擬驗證整合正確性:
iverilog -o soc.vvp -g2012 soc_top.v picorv32.v tb_soc.v
vvp soc.vvp
gtkwave soc.vcd # 檢視波形
testbench 應模擬 UART 對方的行為,確認程式輸出 Hello, Arty! 後結束。
5.1.4 Arty 板上實測#
- 連接:USB 線接 Arty 的 J10(PROG/UART 共用介面)。
- 燒錄:
openfpgaloader -b arty soc.bit。 - 開終端機:
screen /dev/ttyUSB1 115200(macOS 為/dev/tty.usbserial-*;Windows 可用 PuTTY)。 - 觀察:應看到
Hello, Arty!,撥動開關時對應 LED 亮起。
常見問題排查#
| 症狀 | 可能原因 | 檢查方法 |
|---|---|---|
| 完全無輸出 | 重置極性錯誤 | Arty 按鈕按下為 0,確認 rst_n 極性 |
| 亂碼 | 鮑率不匹配 | 確認 UART 分頻常數(100MHz/115200 ≈ 868) |
| 程式不動 | BRAM 未初始化 | 用 $readmemh 或 bitstream 內嵌(updatemem) |
| 時序違例 | 關鍵路徑太長 | 檢視 nextpnr 報告,降低時脈或優化邏輯 |
檢視資源使用#
nextpnr 會報告 LUT/FF/BRAM 使用量。Arty A7-35T 有 33,280 個 LUT,一個 RV32I SoC 通常佔用 1500–3000 個 LUT,餘裕充足,可以繼續加上更多周邊或加速器(見 05.3)。
5.1.5 開源 RISC-V 核心比較#
選擇 CPU 核心是 SoC 設計的第一個重大決策。下表比較四個代表性開源核心:
| 特性 | PicoRV32 | VexRiscv | Ibex | Rocket |
|---|---|---|---|---|
| 授權 | ISC(極寬鬆) | MIT | Apache 2.0 | Apache 2.0(BSD 衍生) |
| 語言 | Verilog(手寫) | SpinalHDL(生成 Verilog) | SystemVerilog | Chisel(生成 Verilog) |
| 微架構 | 多週期、循序 | 流水線(可配置 2–5 級) | 2 級流水線 | 5 級流水線、雙發射(可選) |
| 指令集 | RV32I(E)M(A)C 可選 | RV32IM(A)C、可選 F | RV32IMC(B) 可選 | RV64GC(預設) |
| 效能 | ~2–4 CoreMark/MHz | 3.5–5 CoreMark/MHz | ~2.3–4.8 CoreMark/MHz | >3 CoreMark/MHz(64 位元) |
| 面積(7 系列 LUT,約) | 750–2000 | 1000–2500 | 2000–3000 | 15000+ |
| 記憶體介面 | 原生簡單 valid/ready | Wishbone/AXI 可選 | OBI(Simple Bus) | RoCC/TileLink |
| 主要應用 | 小型控制、教学 | 中型 SoC、彈性配置 | 嵌入式 MCU(lowRISC 官方) | 學術研究、標桿級 SoC |
| 開源工具鏈相容性 | 極佳(純 Verilog) | 需 Java/SpinalHDL 產生 | 需 SystemVerilog 支援 | 需 Chisel/SBT |
選型建議#
- PicoRV32:純 Verilog、極小面積、介面簡單,最適合教學——你能讀懂每一行。本書前四章實作的處理器本質上就是簡化版 PicoRV32。
- VexRiscv:以 SpinalHDL(Scala DSL)描述,核心參數(流水線級數、是否加 M 指令、C 指令、CSR)皆可插拔配置。想學「可參數化硬體生成」的學生必看。
- Ibex:lowRISC 官方維護,有完整的驗證環境(Google/OpenTitan 採用),適合想學工業級驗證的學生。
- Rocket:UC Berkeley 經典,支援 RV64GC、可加浮點、可掛 RoCC 協同處理器(正是 05.3 自訂指令的範本),是研究等級的基準。缺點是面積大、建置鏈複雜。
理論視角:這四個核心體現了晶片設計的面積-效能-功耗三角。PicoRV32 犧牲效能換取最小面積;Rocket 犧牲面積與設計複雜度換取高效能。在嵌入式場景,多數應用其實「夠用就好」——理解取捨比追求極限更重要。
本章小結#
- SoC = CPU + 記憶體 + 周邊,用記憶體映射 I/O 與位址解碼匯流排互連。
- BRAM 是同步讀取,微架構必須配合物理特性調整(多週期取指)。
- 開源工具鏈 Yosys(合成)→ nextpnr(佈局繞線)→ openFPGALoader(燒錄)可完整取代廠商工具,且過程透明可教學。
- 四個開源 RISC-V 核心各有定位:教學選 PicoRV32、彈性選 VexRiscv、驗證選 Ibex、研究選 Rocket。
實作題#
- 基本整合:將 04 章的多週期 RISC-V CPU 與本節的
bus_decoder、BRAM、UART 整合成soc_top,在模擬中執行一段寫 UART 的測試程式,貼出波形截圖。 - GPIO 控制燈:撰寫一段組合語言(或 C),從 GPIO 輸入讀取開關值,將其反相後寫到 LED 輸出。上板驗證。
- UART 回聲:修改程式,使 UART 收到的任何字元立即回送(echo),驗證雙向通訊。
- 時脈量測:將系統時脈從 100MHz 逐次上調,記錄 nextpnr 的時序報告,找出該 SoC 的最高可運作頻率,並解釋關鍵路徑在哪裡(提示:可用 nextpnr 的
--freq與報告)。 - 核心替換:將
soc_top中的 CPU 換成 VexRiscv(用其官方產生器產生最小配置的 Verilog),比較兩者的 LUT 使用量與最高時脈。