第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接口设计的核心逻辑串起来了。从顶层的Slave模块,到写通道、读通道和寄存器文件三个子模块,再到底层的仿真验证。你写代码的时候,就按这个结构来,不会乱。
好了,这一章的内容就到这。AXI-Lite虽然简单,但它是RISC-V SoC中寄存器配置的基石。把这一章吃透了,后面学AXI-Full就轻松多了。
公众号:蓝海资料掘金营,微信deep3321