3、AXI4-Lite协议详解:轻量级总线特点、寄存器访问、控制状态机设计
大家好,我是你们的FPGA设计老司机。今天我们来聊聊AXI4-Lite协议。说实话,在我刚接触AXI总线那会儿,看到AXI4-Full那密密麻麻的信号线,头都大了。后来发现,其实很多场景根本用不上那么复杂的东西——你只是想配个寄存器、读个状态,何必杀鸡用牛刀呢?
AXI4-Lite就是为这种场景量身定做的。它简单、轻量、易实现,说白了就是AXI4-Full的「精简版」。我在项目中做过不下20个AXI4-Lite的从机控制器,今天就把这些经验掰开了揉碎了讲给你听。
3.1 AXI4-Lite的特点:为什么需要它?
AXI4-Lite和AXI4-Full最大的区别在哪?我总结了三句话:
- 单次传输:每次只能传一个数据,不支持burst(突发传输)
- 数据宽度固定:32位或64位,不能变
- 信号线少:地址、数据、控制信号加起来也就20来根
你想想看,如果你只是想写一个控制寄存器,比如启动一个DMA传输、设置一个分频系数,用AXI4-Full的burst功能完全没必要。AXI4-Lite刚好够用,而且实现起来简单得多。
核心要点:AXI4-Lite适用于控制寄存器、状态寄存器、中断控制等轻量级访问场景。它不适用于大数据量的DMA传输。
我记得有一次,一个同事非要用AXI4-Full去配一个只有4个寄存器的模块。我问他为什么,他说「反正都是AXI,用Full总没错」。结果呢?调试了三天,光是把那堆信号线连对就花了一天。这就是典型的过度设计。
3.2 寄存器访问:读写操作的细节
AXI4-Lite的读写操作,说白了就是「地址+数据+握手」。但这里面的握手时序,很多人第一次看会懵。我带你一步步拆解。
3.2.1 写操作流程
写操作需要5个通道的配合:AW(写地址)、W(写数据)、B(写响应)。流程是这样的:
- 主机在AW通道上发送地址,同时拉高AWVALID
- 从机准备好后拉高AWREADY,完成地址握手
- 主机在W通道上发送数据,同时拉高WVALID
- 从机准备好后拉高WREADY,完成数据握手
- 从机处理完写操作后,在B通道返回响应(OKAY/EXOKAY/SLVERR/DECERR)
这里有个坑,我踩过好几次:地址握手和数据握手是独立的。也就是说,地址可以先传,数据可以后传;或者反过来,数据先到,地址后到。只要在最后一个数据传完之前地址到了就行。
避坑指南:我曾经在一个项目中,地址握手完成之后,数据迟迟不来。从机等了半天,以为出错了,直接返回了SLVERR。后来才发现,是主机那边数据路径上有流水线延迟。解决办法是:从机在地址握手完成后,不要立即开始处理,等数据到了再处理。
3.2.2 读操作流程
读操作更简单,只需要两个通道:AR(读地址)、R(读数据+响应)。
- 主机在AR通道上发送地址,拉高ARVALID
- 从机准备好后拉高ARREADY,完成地址握手
- 从机读取寄存器数据,在R通道上返回数据和响应
读操作没有独立的响应通道,响应是和数据一起在R通道上返回的。这一点和写操作不同,写操作有单独的B通道。
3.3 控制状态机设计:从零开始搭一个从机控制器
好了,理论说完了,我们来点实战的。下面是我个人习惯用的一个AXI4-Lite从机状态机设计,经过多个项目验证,稳定可靠。
3.3.1 状态定义
我一般把状态机分成5个状态:
| 状态 | 说明 |
|---|---|
| IDLE | 空闲状态,等待地址有效 |
| READ_ADDR | 读地址已接收,准备读取寄存器 |
| WRITE_ADDR | 写地址已接收,等待写数据 |
| WRITE_DATA | 写数据已接收,执行写操作 |
| WRITE_RESP | 写响应返回 |
为什么要把读和写分开?因为读操作不需要等数据,地址到了就可以去读寄存器;而写操作需要等数据和地址都到了才能执行。分开处理可以提升效率。
3.3.2 状态机代码示例
下面是一个简化的Verilog代码,展示了核心的状态机逻辑:
// AXI4-Lite从机状态机 - 简化版
localparam IDLE = 3'd0;
localparam READ_ADDR = 3'd1;
localparam WRITE_ADDR = 3'd2;
localparam WRITE_DATA = 3'd3;
localparam WRITE_RESP = 3'd4;
reg [2:0] state, next_state;
// 状态转移
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
// 下一状态逻辑
always @(*) begin
next_state = state;
case (state)
IDLE: begin
if (arvalid && arready) // 读地址握手成功
next_state = READ_ADDR;
else if (awvalid && awready) // 写地址握手成功
next_state = WRITE_ADDR;
end
READ_ADDR: begin
// 读数据返回后回到IDLE
if (rvalid && rready)
next_state = IDLE;
end
WRITE_ADDR: begin
// 等待写数据
if (wvalid && wready)
next_state = WRITE_DATA;
end
WRITE_DATA: begin
// 写操作完成,准备返回响应
next_state = WRITE_RESP;
end
WRITE_RESP: begin
// 响应握手完成,回到IDLE
if (bvalid && bready)
next_state = IDLE;
end
default: next_state = IDLE;
endcase
end
个人经验:这个状态机看起来简单,但实际项目中要考虑很多细节。比如,如果读地址和写地址同时到来怎么办?我的做法是:优先处理读操作,因为读操作通常更快,不会阻塞写操作太久。
3.4 握手信号的时序控制
AXI4-Lite的握手信号(VALID/READY)有几种组合方式,我见过很多新手在这里翻车。
- VALID先到,READY后到:最常见的情况,主机先发数据,从机准备好了再接收
- READY先到,VALID后到:从机提前准备好,主机数据到了直接接收
- 同时到达:一拍完成握手,效率最高
嗯,这里要注意:VALID不能依赖READY。也就是说,主机不能等从机READY了才拉VALID。VALID必须独立拉高,直到握手成功才能拉低。这是AXI协议的铁律,违反了就是协议违规。
关键点:VALID信号一旦拉高,必须保持到握手完成(VALID & READY = 1)才能拉低。READY可以随时拉高拉低,没有这个限制。
3.5 响应信号的生成
AXI4-Lite的响应信号有4种:
| 响应码 | 含义 | 使用场景 |
|---|---|---|
| 00 (OKAY) | 正常访问成功 | 读写操作正常完成 |
| 01 (EXOKAY) | 独占访问成功 | AXI4-Lite不支持,固定返回00 |
| 10 (SLVERR) | 从机错误 | 地址未映射、寄存器写保护等 |
| 11 (DECERR) | 译码错误 | 地址不在从机地址范围内 |
我建议,对于未使用的地址空间,统一返回DECERR。这样主机可以明确知道「这个地址不存在」,而不是误以为操作成功了。
避坑指南:我曾经在一个项目中,对未映射的地址返回了OKAY(默认值)。结果主机写了一个错误的地址,以为写成功了,但实际上数据根本没写进去。后来调试了整整两天才发现这个问题。从那以后,我所有的从机控制器都会对非法地址返回SLVERR或DECERR。
3.6 实战中的设计建议
最后,分享几个我在实际项目中总结的经验:
- 地址对齐:AXI4-Lite要求地址必须按数据宽度对齐。32位数据宽度时,地址的[1:0]必须为2'b00。如果不对齐,从机应该返回SLVERR。
- 寄存器地址映射:我习惯用参数化的方式定义寄存器地址,方便后期修改。比如用`define或localparam定义每个寄存器的偏移地址。
- 流水线处理:如果从机需要多个时钟周期才能完成读写操作,可以在状态机中插入等待状态,或者使用流水线寄存器。
- 超时处理:建议加一个超时计数器,防止主机一直不完成握手导致从机死锁。
好了,AXI4-Lite的内容就讲到这里。这个协议虽然简单,但用好了能解决80%的控制类问题。下一章我们会讲AXI4-Stream,那又是另一番天地了。