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
这些经验都是我在实际项目中一点点积累出来的。你设计的时候如果遇到奇怪的问题,不妨回头检查一下握手时序,八成是这里出了问题。
这张图把读通道的两个方向画得很清楚。红色箭头是地址流向,绿色箭头是数据流向。中间那块是地址译码和错误处理逻辑,它决定了返回的数据和响应状态。
好了,读通道的逻辑就讲到这里。下一章我们聊聊写通道和读通道的整合,以及如何用 Verilog 实现一个完整的 AXI4-Lite 从机接口。