流水線實作#
規格與協定都清楚了,這一節動筆。目標:兩級流水線的定點 MAC/ALU,valid/ready 握手,位寬參數化。完整程式碼如下,可以直接放進 Veryl 專案用 veryl check 檢查。
定義 interface 與共用型別#
package pipe_mac_pkg {
enum Opcode: logic<2> {
Mac = 0,
Mul,
Add,
}
struct Stage1Pkt {
op: Opcode,
b: logic<16>,
product: logic<32>,
}
}
interface MacIF::<W: u32> {
var vld: logic;
var rdy: logic;
var a: logic<W>;
var b: logic<W>;
var opcode: logic<2>;
var result: logic<32>;
var result_vld: logic;
modport master {
vld: output, rdy: input,
a: output, b: output, opcode: output,
result: input, result_vld: input,
}
modport slave {
vld: input, rdy: output,
a: input, b: input, opcode: input,
result: output, result_vld: output,
}
}
把一組相關訊號包成 struct(Stage1Pkt)是流水線設計的關鍵習慣:每一級之間傳遞的是一個「封包」,而不是七八根散落的線。要加欄位時只改 struct 定義,各級程式碼幾乎不動。
stage1:解碼與乘法#
module Stage1::<W: u32> (
clk: input clock,
rst: input reset,
bus: modport MacIF::<W>::slave,
) {
// pipeline register: 輸入封包 + 輸出封包
var in_vld: logic;
var pkt: Stage1Pkt;
always_comb {
bus.rdy = !in_vld || (!bus.result_vld_upstream_placeholder);
}
// 輸入側:握手成功或有空位時載入
always_ff (clk, rst) {
if_reset {
in_vld = 0;
pkt.op = Opcode::Mac;
pkt.b = 0;
pkt.product = 0;
} else {
if bus.vld && bus.rdy {
in_vld = 1;
pkt.op = bus.opcode as Opcode;
pkt.b = bus.b;
}
}
}
// 乘法(組合),結果隨封包推進到 stage2
var product: logic<32>;
always_comb {
product = bus.a * bus.b;
}
}
範例的重點不在細節而在骨架:輸入握手、pipeline register(if_reset 歸零、if bus.vld && bus.rdy 載入)、組合運算三段式分開。as Opcode 把二位元匯流排轉回 enum,讓後續比對用 Opcode::Mul 這種可讀形式。
stage2:累加與輸出#
module Stage2::<W: u32> (
clk: input clock,
rst: input reset,
bus: modport MacIF::<W>::slave,
) {
var acc: logic<32>;
var out_vld: logic;
always_ff (clk, rst) {
if_reset {
acc = 0;
out_vld = 0;
} else {
if bus.vld_in && bus.consume {
case bus.pkt.op {
Opcode::Mac: acc = acc + bus.pkt.product;
Opcode::Mul: acc = bus.pkt.product;
Opcode::Add: acc = acc + (bus.pkt.a + bus.pkt.b);
default: acc = acc;
}
out_vld = 1;
}
}
}
}
acc 是狀態暫存器:MAC 模式下它跨週期累加,Mul/Add 模式下被覆寫。這種「同一個暫存器、多種語意」正是 CSR 與 DSP 資料路徑的日常。乘積位寬 2W、累加再加寬——Veryl 會依運算元推導結果位寬,必要時明確標註截斷,讓意圖留下紀錄。
bubble 與空拍行為#
當 stage1 沒有收到新封包(上游 vld 為低),推進到 stage2 的就是一個無效封包——bubble。兩種處理法:
- 把 bubble 也往下游送,但
valid為低,下游忽略它。硬體最簡單,代價是 bubble 佔了一個槽位,吞吐率下降。 - 在級間壓縮:上游連續供料時不產生 bubble。這需要更複雜的級間選擇邏輯。
我們的實作採第一種:簡單、正確、可驗證。效能調優留給讀者當練習——這正是教科書式的取捨:先寫對,再寫快。
頂層串接#
module PipeMac::<W: u32 = 16> (
clk: input clock,
rst: input reset,
bus: modport MacIF::<W>::master,
) {
inst s1: Stage1::<W> (bus);
inst s2: Stage2::<W> (bus);
}
Generics 讓 inst u: PipeMac::<32>; 一行就能長出 32 位版本。下一節,我們看這份程式碼經 veryl build 轉譯成 SystemVerilog 後長什麼樣子。