4. AXI4-Lite从机设计实战(Verilog):状态机设计、地址译码逻辑、寄存器读写控制、仿真验证
好,咱们今天来点真格的。
前面几章把AXI4-Lite的协议讲了个七七八八,但说实话,光看波形图是学不会的。你得亲手写一个从机,跑一遍仿真,踩几个坑,才能真正理解这玩意儿是怎么工作的。
这一章,我就带你手撸一个AXI4-Lite从机。咱们的目标很明确:写一个支持读写操作的从机模块,挂4个32位寄存器。我会把状态机、地址译码、读写控制这些核心逻辑掰开揉碎了讲。
本章核心知识点
- 写地址通道与写数据通道的握手时序
- 读地址通道与读数据通道的握手时序
- 地址译码逻辑:如何从地址总线中提取寄存器偏移
- 状态机设计:写状态机与读状态机的独立实现
- 仿真验证:用Verilog testbench验证读写功能
4.1 模块接口定义
先搭个架子。我习惯把接口分成5组:全局信号、写地址通道、写数据通道、写响应通道、读地址通道、读数据通道。这样看着清爽,也方便后续维护。
module axi4_lite_slave #(
parameter ADDR_WIDTH = 32,
parameter DATA_WIDTH = 32
)(
// 全局信号
input wire ACLK,
input wire ARESETn,
// 写地址通道
input wire [ADDR_WIDTH-1:0] AWADDR,
input wire AWVALID,
output reg AWREADY,
// 写数据通道
input wire [DATA_WIDTH-1:0] WDATA,
input wire [DATA_WIDTH/8-1:0] WSTRB,
input wire WVALID,
output reg WREADY,
// 写响应通道
output reg [1:0] BRESP,
output reg BVALID,
input wire BREADY,
// 读地址通道
input wire [ADDR_WIDTH-1:0] ARADDR,
input wire ARVALID,
output reg ARREADY,
// 读数据通道
output reg [DATA_WIDTH-1:0] RDATA,
output reg [1:0] RRESP,
output reg RVALID,
input wire RREADY
);
小提示:WSTRB信号很多人一开始会忽略。它表示写数据中哪些字节是有效的。比如WSTRB=4'b0011,表示只写低16位。我在一个项目中没处理好这个信号,结果寄存器的高16位被意外清零了...嗯,血的教训。
4.2 写状态机设计
写操作需要三个通道协同:AW、W、B。说白了就是:先收地址,再收数据,最后回响应。
我设计了一个3状态的状态机:
localparam IDLE_W = 2'b00;
localparam ADDR_W = 2'b01;
localparam DATA_W = 2'b10;
localparam RESP_W = 2'b11;
reg [1:0] wstate, wnext;
// 状态转移
always @(posedge ACLK or negedge ARESETn) begin
if (!ARESETn)
wstate <= IDLE_W;
else
wstate <= wnext;
end
// 次态逻辑
always @(*) begin
wnext = wstate;
case (wstate)
IDLE_W: if (AWVALID) wnext = ADDR_W;
ADDR_W: if (WVALID) wnext = DATA_W;
DATA_W: wnext = RESP_W;
RESP_W: if (BREADY) wnext = IDLE_W;
default: wnext = IDLE_W;
endcase
end
你可能会问:为什么ADDR_W状态只等WVALID?因为AWREADY在IDLE_W状态就拉高了,地址在第一个周期就锁存了。数据可能晚到,所以得等。
注意:AXI协议允许AW和W同时有效,也允许W先于AW到达。我这个状态机假设了AW先到,W后到。如果你要处理乱序情况,状态机要更复杂一些。不过对于AXI4-Lite,master通常不会这么干,所以简化处理没问题。
4.3 地址译码逻辑
地址译码,说白了就是看地址落在哪个寄存器上。我习惯用偏移地址来判断:
// 地址译码
wire [ADDR_WIDTH-1:0] addr_offset;
assign addr_offset = AWADDR & 32'h0000_00FF; // 取低8位
reg [1:0] write_sel;
always @(*) begin
case (addr_offset[3:2]) // 按字对齐
2'b00: write_sel = 2'b00; // 寄存器0
2'b01: write_sel = 2'b01; // 寄存器1
2'b10: write_sel = 2'b10; // 寄存器2
2'b11: write_sel = 2'b11; // 寄存器3
default: write_sel = 2'b00;
endcase
end
这里有个细节:地址是字节地址,但寄存器是32位的,所以用addr_offset[3:2]来选。偏移0x00、0x04、0x08、0x0C分别对应4个寄存器。
4.4 寄存器读写控制
写寄存器的逻辑,我放在写状态机的DATA_W状态里:
reg [31:0] reg0, reg1, reg2, reg3;
always @(posedge ACLK or negedge ARESETn) begin
if (!ARESETn) begin
reg0 <= 32'h0;
reg1 <= 32'h0;
reg2 <= 32'h0;
reg3 <= 32'h0;
end else if (wstate == DATA_W) begin
case (write_sel)
2'b00: reg0 <= WDATA;
2'b01: reg1 <= WDATA;
2'b10: reg2 <= WDATA;
2'b11: reg3 <= WDATA;
endcase
end
end
读操作就简单多了。读地址通道和读数据通道是独立的:
// 读地址锁存
reg [31:0] read_addr;
always @(posedge ACLK or negedge ARESETn) begin
if (!ARESETn)
read_addr <= 32'h0;
else if (ARVALID && ARREADY)
read_addr <= ARADDR;
end
// 读数据输出
always @(*) begin
case (read_addr[3:2])
2'b00: RDATA = reg0;
2'b01: RDATA = reg1;
2'b10: RDATA = reg2;
2'b11: RDATA = reg3;
default: RDATA = 32'h0;
endcase
end
关键点:读数据是组合逻辑输出的,RVALID信号控制什么时候把数据发给master。我习惯在读地址锁存后的下一个周期拉高RVALID,这样数据已经稳定了。
4.5 仿真验证
写testbench,我个人的习惯是:先测写,再测读,最后测读写混合。别一上来就搞复杂场景,先把基本功能跑通。
// 写操作测试
initial begin
// 写寄存器0
AWADDR = 32'h0000_0000;
AWVALID = 1;
WDATA = 32'hA5A5_A5A5;
WVALID = 1;
@(posedge ACLK);
AWVALID = 0;
WVALID = 0;
@(posedge ACLK);
// 写寄存器1
AWADDR = 32'h0000_0004;
AWVALID = 1;
WDATA = 32'h5A5A_5A5A;
WVALID = 1;
@(posedge ACLK);
AWVALID = 0;
WVALID = 0;
@(posedge ACLK);
end
// 读操作测试
initial begin
#100;
ARADDR = 32'h0000_0000;
ARVALID = 1;
@(posedge ACLK);
ARVALID = 0;
@(posedge ACLK);
// 此时RDATA应该等于0xA5A5_A5A5
end
仿真跑起来后,重点看几个信号:
- AWREADY和WREADY:是否在正确的时间拉高
- BVALID和BRESP:写响应是否返回OKAY
- RVALID和RDATA:读数据是否与写入的一致
避坑指南:我曾经犯过一个错误——写状态机里忘了等BREADY。结果BVALID拉高后,master还没准备好收响应,我就跳回IDLE状态了。这会导致写操作丢失。记住:一定要等BREADY拉高才能结束写事务。
4.6 完整代码结构
最后,我把整个模块的代码结构整理一下。你写的时候按这个框架来,不容易漏东西:
- 端口声明:按AXI通道分组
- 内部寄存器定义:4个32位寄存器
- 写状态机:IDLE_W → ADDR_W → DATA_W → RESP_W
- 地址译码:从地址中提取寄存器选择信号
- 写数据锁存:在DATA_W状态写入寄存器
- 写响应生成:BRESP固定为OKAY,BVALID在RESP_W状态拉高
- 读地址锁存:在ARVALID&ARREADY时锁存地址
- 读数据输出:组合逻辑根据地址输出寄存器值
- 读状态机:IDLE_R → ADDR_R → DATA_R
嗯,这一章的内容就到这儿。代码量不大,但逻辑环环相扣。你写的时候,建议先画个状态转移图,再动手写代码。仿真的时候,把每个通道的握手信号都盯仔细了。
可可精品资料库