4. AXI4-Lite从机接口设计(二):读通道逻辑实现

好,咱们接着往下聊。上一章我们把写通道的逻辑理清楚了,这一章重点讲读通道。读通道其实比写通道要简单一些,但有几个细节特别容易踩坑。我个人习惯先把读地址通道和读数据通道分开设计,最后再合到一起做握手逻辑。

4.1 读地址通道逻辑实现

读地址通道,说白了就是主机告诉从机:「我要读哪个地址的数据」。这个通道的信号很简单,就三个:ARADDR、ARVALID 和 ARREADY。

我在项目中遇到过一个问题:主机发地址发得很快,但从机还没准备好。这时候如果直接采样地址,数据就乱了。所以一定要等 ARVALID 和 ARREADY 同时拉高,才算完成一次地址传输。

来看看我常用的代码模板:

// 读地址通道
reg [C_S_AXI_ADDR_WIDTH-1:0] axi_araddr;
reg axi_arready;

always @(posedge S_AXI_ACLK) begin
    if (!S_AXI_ARESETN) begin
        axi_araddr  <= 0;
        axi_arready <= 0;
    end else begin
        // 默认拉高,表示随时可以接收地址
        axi_arready <= 1'b1;
        
        if (axi_arready && S_AXI_ARVALID) begin
            // 握手成功,锁存地址
            axi_araddr <= S_AXI_ARADDR;
        end
    end
end

嗯,这里要注意:ARREADY 我习惯默认拉高。为什么?因为读操作不像写操作需要缓冲,从机收到地址后可以直接去取数据。你想想看,如果 ARREADY 默认拉低,主机就得一直等,白白浪费一个时钟周期。

4.2 读数据通道逻辑实现

读数据通道稍微复杂一点。它要返回读到的数据,同时还要返回响应状态。信号包括 RDATA、RRESP、RVALID 和 RREADY。

这里有个关键点:读数据通道的 RVALID 不能和 ARVALID 同时拉高。为什么?因为从机需要时间从寄存器里把数据读出来。我一般会等地址握手完成后,再延迟一个周期拉高 RVALID。

核心逻辑:读数据通道的握手信号 RVALID 必须在数据准备好之后才能拉高,不能提前承诺。

我曾经犯过一个错误:把 RVALID 和地址握手信号直接连在一起。结果主机读到的数据全是上一拍的,调试了整整一天才发现。从那以后,我设计读数据通道时都会加一个状态机:

// 读数据通道状态机
localparam IDLE = 2'b00;
localparam READ = 2'b01;
localparam DONE = 2'b10;

reg [1:0] rstate, rnext;

always @(posedge S_AXI_ACLK) begin
    if (!S_AXI_ARESETN)
        rstate <= IDLE;
    else
        rstate <= rnext;
end

always @(*) begin
    rnext = rstate;
    case (rstate)
        IDLE: begin
            if (axi_arready && S_AXI_ARVALID)
                rnext = READ;  // 地址握手成功,进入读状态
        end
        READ: begin
            rnext = DONE;     // 一个周期后数据准备好
        end
        DONE: begin
            if (S_AXI_RREADY)
                rnext = IDLE; // 主机取走数据,回到空闲
        end
    endcase
end

4.3 AXI握手协议(VALID/READY)实现

AXI的握手协议,说白了就是「你准备好了告诉我,我准备好了告诉你,咱俩同时准备好就干活」。这个机制保证了数据传输的可靠性,但也带来了一些设计上的坑。

握手协议有几种情况:

场景 VALID READY 说明
主机先准备好 拉高 等待 主机发数据,从机还没准备好
从机先准备好 等待 拉高 从机可以接收,但主机还没发
同时准备好 拉高 拉高 一拍完成传输,效率最高

我个人建议:在设计从机时,尽量让 READY 信号在复位后默认拉高。这样主机发数据时,只要一拍就能完成握手。但要注意,如果从机内部有 FIFO 或者缓冲区,READY 就不能一直拉高,否则缓冲区满了会丢数据。

小技巧:如果你不确定什么时候拉高 READY,可以先拉低,等内部逻辑准备好再拉高。虽然多花一个周期,但不会出错。

4.4 总线空闲与错误处理

总线空闲状态,就是没有任何传输在进行。这时候所有 VALID 信号都应该拉低,READY 信号可以保持拉高(等待主机发起传输)。

错误处理这块,我踩过不少坑。AXI4-Lite 的 RRESP 和 BRESP 信号只有两位,用来表示传输状态:

  • 00:OKAY,正常完成
  • 01:EXOKAY,保留(Lite接口不用)
  • 10:SLVERR,从机错误
  • 11:DECERR,地址译码错误

我曾经遇到一个情况:主机读了一个不存在的地址,从机直接返回了 OKAY。结果软件那边读到了一个随机值,整个系统都跑飞了。正确的做法是:如果地址超出范围,返回 SLVERR 或者 DECERR。

警告:千万不要在地址越界时返回 OKAY!这会让软件误以为读到了有效数据。我建议在地址译码时加一个范围检查,超出范围直接返回错误响应。

来看看完整的读通道错误处理逻辑:

// 地址译码与错误处理
always @(*) begin
    if (axi_araddr >= BASE_ADDR && axi_araddr < BASE_ADDR + SIZE) begin
        // 地址有效,正常读取
        rdata = mem[axi_araddr];
        rresp = 2'b00;  // OKAY
    end else begin
        // 地址无效,返回错误
        rdata = 32'hDEAD_BEEF;  // 填充错误数据
        rresp = 2'b10;          // SLVERR
    end
end

嗯,这里有个细节:错误响应也要等握手完成才能返回。你不能在 RVALID 拉高之前就把 RRESP 置为错误,否则主机可能还没准备好接收。

最后总结一下读通道设计的几个要点:

  • 读地址通道:ARREADY 默认拉高,地址握手后锁存
  • 读数据通道:用状态机控制 RVALID,数据准备好再拉高
  • 握手协议:VALID 和 READY 同时拉高才算传输完成
  • 错误处理:地址越界返回 SLVERR,不要返回 OKAY

这些经验都是我在实际项目中一点点积累出来的。你设计的时候如果遇到奇怪的问题,不妨回头检查一下握手时序,八成是这里出了问题。

AXI4-Lite读通道逻辑结构图 主机 (Master) 从机 (Slave) 读地址通道 ARADDR, ARVALID → ARREADY 读数据通道 RDATA, RRESP, RVALID → RREADY 地址译码 & 错误处理 地址范围检查 → RRESP生成 图:AXI4-Lite读通道数据流向与握手关系

这张图把读通道的两个方向画得很清楚。红色箭头是地址流向,绿色箭头是数据流向。中间那块是地址译码和错误处理逻辑,它决定了返回的数据和响应状态。

好了,读通道的逻辑就讲到这里。下一章我们聊聊写通道和读通道的整合,以及如何用 Verilog 实现一个完整的 AXI4-Lite 从机接口。