第4章:AXI-Lite接口实战:寄存器配置接口设计、单次读写时序、状态机实现与仿真验证

好,咱们今天来聊聊AXI-Lite。说实话,在RISC-V的SoC设计中,AXI-Lite是我用得最多的接口之一。为什么?因为它简单、轻量,特别适合做寄存器配置。你想想看,CPU要配置外设,总不能每次都走AXI-Full那种复杂的突发传输吧?杀鸡焉用牛刀。

我个人习惯把AXI-Lite叫做「瘦身版AXI」。它保留了AXI的基本读写通道,但去掉了突发相关的信号。说白了,每次只能读写一个数据。但正是这种「简单」,让它成为寄存器配置接口的首选。

4.1 AXI-Lite信号概览

先快速过一下AXI-Lite的信号。和AXI-Full一样,它也有5个通道:

通道 信号 说明
写地址通道 AWADDR, AWVALID, AWREADY 写地址与握手
写数据通道 WDATA, WSTRB, WVALID, WREADY 写数据与字节使能
写响应通道 BRESP, BVALID, BREADY 写完成响应
读地址通道 ARADDR, ARVALID, ARREADY 读地址与握手
读数据通道 RDATA, RRESP, RVALID, RREADY 读数据与响应

嗯,这里要注意:AXI-Lite的WSTRB是必须的,不能像AXI-Full那样可以省略。我在项目中遇到过有人偷懒把WSTRB直接置为4'b1111,结果写32位寄存器没问题,但遇到8位寄存器就出错了。所以,老老实实按协议来。

4.2 单次读写时序

AXI-Lite的时序其实很直观。咱们先看写操作:

// 写操作时序示例
// 时钟上升沿采样
// AW通道:地址 + VALID/READY握手
// W通道:数据 + VALID/READY握手
// B通道:响应 + VALID/READY握手

// 关键点:AW和W可以同时发起,但B必须在AW和W都完成后才返回

为什么会这样设计?因为写操作需要地址和数据都准备好,才能开始真正的写入。我刚开始做的时候,以为AW和W必须严格对齐,后来发现协议允许它们交错。但为了保险起见,我建议让它们同时有效,这样状态机好写。

读操作更简单:

// 读操作时序示例
// AR通道:地址 + VALID/READY握手
// R通道:数据 + 响应 + VALID/READY握手

// 关键点:R通道的VALID必须在AR通道握手完成后才能拉高

说白了,读操作就是「你问我要地址,我给你数据」。但要注意,RVALID不能依赖ARREADY,这是新手容易犯的错。

4.3 状态机实现

好,咱们来写状态机。我个人习惯用三段式状态机,清晰、好维护。先看写状态机:

// 写状态机 - 三段式
// 状态定义
typedef enum logic [1:0] {
    IDLE,
    WRITE_ADDR_DATA,
    WRITE_RESP
} write_state_t;

write_state_t current_state, next_state;

// 第一段:状态转移
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        current_state <= IDLE;
    else
        current_state <= next_state;
end

// 第二段:下一状态逻辑
always_comb begin
    next_state = current_state;
    case (current_state)
        IDLE: begin
            if (AWVALID && WVALID)
                next_state = WRITE_ADDR_DATA;
        end
        WRITE_ADDR_DATA: begin
            if (AWREADY && WREADY)
                next_state = WRITE_RESP;
        end
        WRITE_RESP: begin
            if (BVALID && BREADY)
                next_state = IDLE;
        end
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑
always_comb begin
    // 默认值
    AWREADY = 1'b0;
    WREADY  = 1'b0;
    BVALID  = 1'b0;
    BRESP   = 2'b00;
    
    case (current_state)
        IDLE: begin
            // 等待主机发起请求
        end
        WRITE_ADDR_DATA: begin
            AWREADY = 1'b1;
            WREADY  = 1'b1;
        end
        WRITE_RESP: begin
            BVALID = 1'b1;
            BRESP  = 2'b00; // OKAY响应
        end
    endcase
end

读状态机类似,但更简单:

// 读状态机
typedef enum logic [1:0] {
    IDLE,
    READ_ADDR,
    READ_DATA
} read_state_t;

// ... 状态转移逻辑类似

// 输出逻辑
always_comb begin
    ARREADY = 1'b0;
    RVALID  = 1'b0;
    RDATA   = 32'h0;
    RRESP   = 2'b00;
    
    case (current_state)
        IDLE: begin
            if (ARVALID)
                ARREADY = 1'b1;
        end
        READ_DATA: begin
            RVALID = 1'b1;
            RDATA  = reg_file[ar_addr]; // 从寄存器文件读取
            RRESP  = 2'b00;
        end
    endcase
end

我曾经在项目中犯过一个低级错误:在IDLE状态就把ARREADY拉高。结果主机还没发地址,我就说准备好了,导致地址采样错误。嗯,这个坑你们别踩。

4.4 寄存器配置接口设计

寄存器配置接口,说白了就是一堆寄存器的读写控制。我一般这样设计:

// 寄存器文件示例
reg [31:0] reg_file [0:255]; // 256个32位寄存器

// 写操作
always_ff @(posedge clk) begin
    if (write_enable) begin
        for (int i = 0; i < 4; i++) begin
            if (wstrb[i])
                reg_file[write_addr][8*i +: 8] <= wdata[8*i +: 8];
        end
    end
end

// 读操作(组合逻辑)
always_comb begin
    read_data = reg_file[read_addr];
end

这里有个技巧:WSTRB的处理。很多新手直接写reg_file[addr] <= wdata,忽略了字节使能。但如果你要支持8位、16位写操作,就必须处理WSTRB。我建议用for循环逐字节处理,代码清晰且综合结果好。

核心要点:AXI-Lite寄存器配置接口的设计核心是「地址译码 + 读写控制 + 字节使能处理」。状态机负责握手时序,寄存器文件负责数据存储。

4.5 仿真验证

仿真验证这块,我习惯用SystemVerilog写一个简单的testbench。先验证基本读写:

// 基本读写测试
initial begin
    // 初始化
    AWVALID = 0;
    WVALID  = 0;
    ARVALID = 0;
    BREADY  = 1; // 从机总是准备好接收响应
    RREADY  = 1; // 从机总是准备好接收数据
    
    // 写操作:地址0x100,数据0xA5A5A5A5
    @(posedge clk);
    AWADDR  = 32'h100;
    AWVALID = 1;
    WDATA   = 32'hA5A5A5A5;
    WSTRB   = 4'b1111;
    WVALID  = 1;
    
    @(posedge clk);
    // 等待握手完成
    while (!(AWREADY && WREADY)) @(posedge clk);
    AWVALID = 0;
    WVALID  = 0;
    
    // 等待写响应
    while (!BVALID) @(posedge clk);
    @(posedge clk);
    
    // 读操作:地址0x100
    ARADDR  = 32'h100;
    ARVALID = 1;
    @(posedge clk);
    while (!ARREADY) @(posedge clk);
    ARVALID = 0;
    
    // 等待读数据
    while (!RVALID) @(posedge clk);
    // 检查读回的数据是否等于0xA5A5A5A5
    if (RDATA !== 32'hA5A5A5A5)
        $error("读回数据错误!");
    
    $display("基本读写测试通过!");
end

个人经验:仿真时一定要测试边界情况,比如地址对齐、WSTRB全0、同时读写等。我曾经遇到过WSTRB全0时,寄存器被意外清零的bug,就是因为没处理好这个边界。

4.6 避坑指南

最后,分享几个我踩过的坑:

  • 握手依赖问题:BVALID不能依赖BREADY,RVALID不能依赖RREADY。这是协议明确规定的,但很多人会搞反。
  • 地址对齐:AXI-Lite要求地址必须对齐到数据宽度。32位数据时,地址的低2位必须为0。我曾经在项目中忘了检查这个,结果地址译码出错。
  • 响应超时:如果从机长时间不返回响应,主机可能会挂死。建议在从机中加一个超时计数器,超时后返回SLVERR。
  • 字节使能覆盖:多次写同一个地址的不同字节时,要确保WSTRB正确。我见过有人用WSTRB=4'b0011写低16位,然后下次用WSTRB=4'b1100写高16位,结果中间字节被覆盖了。

警告:千万不要在组合逻辑中直接驱动AXI-Lite的VALID信号!必须用寄存器输出。否则综合时会出锁存器,仿真时会出现竞争冒险。

4.7 本章知识体系

下面这张图总结了AXI-Lite接口设计的核心逻辑:

AXI-Lite接口设计知识体系 AXI-Lite Slave 写通道状态机 寄存器文件 读通道状态机 写通道状态机细节 • IDLE → WRITE_ADDR_DATA • WRITE_ADDR_DATA → WRITE_RESP • WRITE_RESP → IDLE • 输出:AWREADY, WREADY, BVALID 寄存器文件细节 • reg_file[0:255] 32位 • 写操作:WSTRB字节使能 • 读操作:组合逻辑输出 • 地址译码:对齐检查 读通道状态机细节 • IDLE → READ_ADDR • READ_ADDR → READ_DATA • READ_DATA → IDLE • 输出:ARREADY, RVALID, RDATA 仿真验证:基本读写 + 边界测试 + 时序检查

这张图把AXI-Lite接口设计的核心逻辑串起来了。从顶层的Slave模块,到写通道、读通道和寄存器文件三个子模块,再到底层的仿真验证。你写代码的时候,就按这个结构来,不会乱。

好了,这一章的内容就到这。AXI-Lite虽然简单,但它是RISC-V SoC中寄存器配置的基石。把这一章吃透了,后面学AXI-Full就轻松多了。


公众号:蓝海资料掘金营,微信deep3321