跳轉到

流水線實作#

規格與協定都清楚了,這一節動筆。目標:兩級流水線的定點 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 後長什麼樣子。