跳轉到

AXI4-Lite Slave 暫存器模組#

匯流排的另一端是 slave。這一節實作一個完整的 AXI4-Lite slave:位址解碼、讀寫 FSM、一組 memory-mapped 暫存器。這個模組幾乎可以原封不動地搬進任何 SoC 專案——它是 SoC 設計裡重複率最高的程式碼之一。

設計#

暫存器組包含:

  • CTRL(位址 0x00):控制暫存器,bit0 = enable,bit1 = opcode 選擇。
  • STATUS(位址 0x04):唯讀狀態,bit0 = busy。
  • DATA_A(0x08)、DATA_B(0x0C):運算元。
  • RESULT(0x10):唯讀結果。
  • SCRATCH(0x14):可讀寫的草稿暫存器,測試匯流排用。

寫入流程:master 先送 AW 通道(位址),再送 W 通道(資料),slave 兩者都收齊後寫入暫存器,最後在 B 通道回 OKAY。讀取流程:master 送 AR,slave 解碼位址、把暫存器值放上 R 通道,回 OKAY。

完整程式碼#

import axi_pkg::*;

module Regs (
    clk: input clock,
    rst: input reset,
    bus: modport Axi4Lite::<16, 32>::slave,
) {
    const ADDR_CTRL:   u32 = 16'h0000;
    const ADDR_STATUS: u32 = 16'h0004;
    const ADDR_DATA_A: u32 = 16'h0008;
    const ADDR_DATA_B: u32 = 16'h000C;
    const ADDR_RESULT: u32 = 16'h0010;
    const RESP_OKAY:   logic<2> = 2'b00;

    // 暫存器本體
    var ctrl:   logic<32>;
    var data_a: logic<32>;
    var data_b: logic<32>;
    var result: logic<32>;
    var busy:   logic;

    // 讀寫 FSM
    enum St: logic<2> { Idle = 0, Write, Read, Resp }

    var st: St;
    var addr_q: logic<16>;

    always_comb {
        bus.awready = st == St::Idle;
        bus.wready = st == St::Idle;
        bus.arready = st == St::Idle;
        bus.bvalid = st == St::Resp;
        bus.rvalid = st == St::Resp;
        bus.bresp = RESP_OKAY;
        bus.rresp = RESP_OKAY;
    }

    // 位址解碼 + 寫入
    always_ff (clk, rst) {
        if_reset {
            st = St::Idle;
            addr_q = 0;
            ctrl = 0;
            data_a = 0;
            data_b = 0;
        } else {
            case st {
                St::Idle: {
                    if bus.awvalid && bus.wvalid {
                        addr_q = bus.awaddr;
                        if bus.wstrb[0] { ctrl[7:0] = bus.wdata[7:0]; }
                        case bus.awaddr {
                            ADDR_CTRL:   ctrl = bus.wdata;
                            ADDR_DATA_A: data_a = bus.wdata;
                            ADDR_DATA_B: data_b = bus.wdata;
                            default: {}
                        }
                        st = St::Resp;
                    }
                    if bus.arvalid {
                        addr_q = bus.araddr;
                        st = St::Resp;
                    }
                }
                St::Resp: {
                    if bus.bvalid && bus.bready || bus.rvalid && bus.rready {
                        st = St::Idle;
                    }
                }
                default: {}
            }
        }
    }

    // 讀資料 mux
    always_comb {
        case addr_q {
            ADDR_CTRL:   bus.rdata = ctrl;
            ADDR_STATUS: bus.rdata = 32'd0 | busy as logic<32>;
            ADDR_DATA_A: bus.rdata = data_a;
            ADDR_DATA_B: bus.rdata = data_b;
            ADDR_RESULT: bus.rdata = result;
            default:     bus.rdata = 0;
        }
    }
}

(範例為可讀性做了取捨:wstrb 的逐 byte 處理、寫入優先於讀取等細節,讀者可依需求強化。)

CSR 設計慣例與文件制度#

CSR(control/status register)設計有一套工程界累積出來的慣例,值得寫進任何專案的第一頁文件:

  • 位址表即規格:每個暫存器的位址、欄位、讀寫屬性(RW/RO/WO)、reset 值,寫成一張表格,放在 IP 的 README 或 doc/。動什麼位元、有什麼副作用,必須一行講清。
  • 保留位恆為 0:軟體寫 1 到保留位不應炸掉硬體;硬體讀回保留位必為 0。
  • W1C(write-1-to-clear)中斷暫存器、set/clear 對、shadow register 這些樣式,在 ARM 的文件與 Linux devicetree 綁定文件裡都有標準寫法。
  • 大公司用內部語言(如 Excel 或自製 DSL)定義 CSR,再自動產生 RTL、軟體 header、文件——三者同源,永不失同步。這是「單一事實來源」思想在暫存器上的體現。開源世界對應的工具是 regtool 等暫存器碼產生器;Veryl 專案則常用 package 的 const 位址定義搭配自動產生 markdown 文件。

暫存器是硬體與軟體的邊界。邊界處的紀律,決定的是整個團隊的速度。下一章,我們從接合暫存器,走向更宏大的題目:用 Veryl 打造一部 RISC-V 處理器。