2. AXI4-Lite接口原理与寄存器映射:信号定义、握手协议与地址映射机制

AXI4-Lite,说白了就是AXI总线家族里的“轻量级选手”。它不像AXI4-Full那样支持突发传输,也不像AXI4-Stream那样只管数据流。它专为控制类寄存器访问而生。我在Zynq项目里,十次有八次都用它来搭PS与PL之间的控制通道——配置参数、读状态、发启动信号,这些活儿它干得最顺手。

2.1 AXI4-Lite的信号定义

AXI4-Lite把读写通道分得清清楚楚。一共5个独立通道,每个通道都有自己的握手信号。我刚开始学的时候觉得信号太多,后来发现其实就三组:写地址、写数据、写响应,再加上读地址、读数据。嗯,就这么简单。

通道 信号名 方向 说明
写地址通道 AWADDR Master→Slave 写地址,32位
AWVALID Master→Slave 地址有效标志
AWREADY Slave→Master 从机准备好接收地址
AWPROT Master→Slave 保护类型(一般置0)
写数据通道 WDATA Master→Slave 写数据,32位
WVALID Master→Slave 数据有效标志
WREADY Slave→Master 从机准备好接收数据
写响应通道 BRESP Slave→Master 写响应状态(OKAY/EXOKAY/SLVERR/DECERR)
BVALID Slave→Master 响应有效标志
BREADY Master→Slave 主机准备好接收响应
读地址通道 ARADDR Master→Slave 读地址,32位
ARVALID Master→Slave 地址有效标志
ARREADY Slave→Master 从机准备好接收地址
读数据通道 RDATA Slave→Master 读数据,32位
RRESP Slave→Master 读响应状态
RVALID Slave→Master 数据有效标志
RREADY Master→Slave 主机准备好接收数据
我的习惯:写RTL时,我会把AWPROT和ARPROT直接接地。在Zynq的PS-PL通信场景里,这两个信号基本用不上。少接两根线,少操一份心。

2.2 握手协议:VALID与READY的舞蹈

AXI4-Lite的握手,核心就是VALID和READY这一对信号。你想想看,主机说“我有数据要发”(VALID拉高),从机说“我准备好了”(READY拉高),两边一拍即合,数据就传过去了。

握手有三种情况:

  • VALID先到,READY后到:主机先拉高VALID,等从机准备好后拉高READY,在时钟上升沿完成传输。这是最常见的场景。
  • READY先到,VALID后到:从机提前准备好,主机随后拉高VALID,同样在时钟上升沿完成。
  • 同时到达:VALID和READY在同一时钟周期拉高,一拍完成,效率最高。

这里有个坑——VALID一旦拉高,就不能等READY为低时又拉低。协议规定,VALID必须保持到握手完成。我曾经在一个项目里犯过这个错:主机看到从机没准备好,就把VALID撤了,结果从机后来READY拉高时,数据已经没了。嗯,从那以后我再也不敢乱动VALID了。

避坑指南:写从机逻辑时,如果当前无法处理请求,READY可以一直为低。但一旦你拉高了READY,就必须在同一个时钟周期完成握手。别想着“我先READY拉高看看,不行再拉低”——协议不允许反悔。

2.3 寄存器地址映射机制

地址映射,说白了就是把PL里的寄存器“挂”到PS的地址空间里。PS通过AXI总线发一个地址,PL根据这个地址找到对应的寄存器,然后读写数据。

在Zynq里,PS的M_AXI_GP0/GP1接口默认分配了0x4000_0000 ~ 0x7FFF_FFFF这段地址空间给PL。你可以在Vivado里通过Address Editor来分配具体的偏移地址。

地址映射的规则其实很简单:

  • 每个寄存器占用4字节(32位)
  • 地址按4字节对齐(地址最低2位为0)
  • 读操作返回指定地址的32位数据
  • 写操作只写指定地址的32位数据

举个例子,假设我在PL里设计了4个寄存器:

偏移地址 寄存器名 方向 功能描述
0x00 CTRL_REG 读写 控制寄存器,bit0=启动,bit1=复位
0x04 STATUS_REG 只读 状态寄存器,bit0=忙标志,bit1=完成标志
0x08 DATA_IN_REG 只写 输入数据寄存器
0x0C DATA_OUT_REG 只读 输出数据寄存器

PS端访问时,基地址是0x4000_0000,那么CTRL_REG的物理地址就是0x4000_0000,STATUS_REG是0x4000_0004,以此类推。

关键点:地址译码逻辑必须在PL侧实现。PS只管发地址和数据,PL要根据地址的低位(比如bit[15:2])来判断访问哪个寄存器。我习惯用case语句来做译码,清晰又不容易出错。

2.4 完整的读写时序与状态机

写操作的状态机,我一般这么设计:

  1. IDLE:等待AWVALID和WVALID同时有效
  2. WRITE:将WDATA写入地址对应的寄存器
  3. RESP:拉高BVALID,返回OKAY响应
  4. 回到IDLE

读操作更简单:

  1. IDLE:等待ARVALID有效
  2. READ:根据ARADDR读出寄存器数据
  3. DATA:拉高RVALID,输出RDATA
  4. 回到IDLE

下面是我画的一个AXI4-Lite从机状态机结构图,帮你理清整体脉络:

AXI4-Lite从机状态机与通道交互图 AXI4-Lite从机状态机与通道交互 写通道状态机 读通道状态机 IDLE WRITE RESP IDLE READ DATA 寄存器阵列 CTRL_REG @ 0x00 STATUS_REG @ 0x04 DATA_IN_REG @ 0x08 DATA_OUT_REG @ 0x0C 写数据/地址 读地址/数据 写通道:AWADDR + WDATA → 地址译码 → 寄存器写入 → BRESP返回

2.5 实战中的地址映射陷阱

地址映射看着简单,实际项目里坑不少。我挑几个常见的说说:

  • 地址未对齐:PS发了个0x4000_0001,PL译码时发现地址最低2位不是0,直接返回DECERR。我遇到过有人把寄存器地址配成0x4000_0002,结果死活读写不了。
  • 地址重叠:两个寄存器映射到同一个偏移地址,写一个另一个也跟着变。这种bug最难查,因为波形上看地址确实是对的。
  • 未使用的地址空间:PS访问了0x4000_0010,但PL只实现了0x00~0x0C的寄存器。这时候PL应该返回SLVERR,而不是默默忽略。
我曾经踩过的坑:有一次调试,PS读STATUS_REG总是返回0,但波形显示寄存器里明明有数据。查了半天,发现是地址译码时把bit[3:2]和bit[1:0]搞混了——0x04被译码成了0x08。从那以后,我写译码逻辑都会先列个地址表,再对着表写case。

2.6 小结

AXI4-Lite的核心就三件事:信号定义、握手协议、地址映射。信号定义告诉你每个引脚干什么用;握手协议保证数据传输的可靠性;地址映射把PL里的寄存器变成PS可以访问的内存空间。这三件事搞清楚了,写一个AXI4-Lite从机IP就是水到渠成的事。