AXI4总线协议与接口设计:从理论到实战
说到Zynq开发,AXI总线是绕不开的核心。我刚开始接触Zynq时,面对AXI4-Full、AXI4-Lite、AXI4-Stream这三个变体,说实话有点懵。它们名字相似,但用法天差地别。今天咱们就把这三兄弟彻底讲透,再聊聊PS与PL之间的接口配置,最后手把手封装一个自定义AXI4-Lite IP核。
核心认知:AXI4是ARM AMBA协议家族的一员,Zynq的PS与PL之间通过AXI接口进行高速数据交换。理解AXI协议,就等于拿到了Zynq开发的钥匙。
AXI4三兄弟:Full、Lite、Stream
这三者的区别,说白了就是复杂度和适用场景的不同。我画了张图,帮你快速建立整体认知。
AXI4-Full:高性能内存映射
AXI4-Full是功能最完整的版本。它支持突发传输,一次发起地址后可以连续传输多个数据。我在做图像加速项目时,需要从DDR搬运一帧图像到PL处理,用的就是AXI4-Full。
它的核心特点:
- 五个独立通道:读地址、读数据、写地址、写数据、写响应
- 突发长度:1到256拍,每拍数据宽度可配置
- 乱序传输:支持ID标签,事务可以乱序完成
我的经验:AXI4-Full的握手信号VALID/READY必须严格遵循依赖关系。我曾经因为READY信号拉高时机不对,导致仿真通过但上板死锁。排查了两天才发现是地址通道的READY在未收到VALID时就拉高,违反了协议。
AXI4-Lite:轻量级寄存器访问
AXI4-Lite是AXI4-Full的简化版。它不支持突发,每次传输只有一拍数据。但正因如此,它的控制逻辑简单得多。我习惯用AXI4-Lite来做PS与PL之间的控制通道——比如配置加速器的参数、读取状态寄存器。
AXI4-Lite与Full的关键差异:
| 特性 | AXI4-Full | AXI4-Lite |
|---|---|---|
| 突发传输 | 支持(1-256拍) | 不支持(仅1拍) |
| 数据宽度 | 8/16/32/64/128/256/512/1024 | 32或64位固定 |
| ID标签 | 支持乱序 | 不支持 |
| 写响应 | 每笔写都有响应 | 每笔写都有响应 |
| 典型应用 | DDR、大块数据 | 控制寄存器、状态寄存器 |
AXI4-Stream:高速数据流
AXI4-Stream是三者中最特殊的——它没有地址通道。数据像流水一样从源头流向目的地。你想想看,视频数据、以太网包、ADC采样值,这些都不需要地址,只需要源源不断地传输。
它的握手信号只有两个:
- TVALID:发送方表示数据有效
- TREADY:接收方表示可以接收
再加上TLAST信号标记数据包的结束,TDATA传输数据,TKEEP和TSTRB用于字节对齐。嗯,这里要注意:TLAST在非包传输时可以不用,但大多数Stream接口都会用到。
避坑指南:我曾经在Stream接口上犯过一个低级错误——TVALID和TREADY同时拉高时,数据只在时钟上升沿传输一次。如果接收方在TREADY拉高后立刻拉低,发送方必须保持TVALID和数据稳定,直到握手完成。这个细节在高速传输中特别容易出问题。
Zynq PS与PL间AXI接口配置
Zynq的PS端提供了多个AXI接口,用于与PL通信。常用的有:
| 接口名称 | 类型 | 位宽 | 典型用途 |
|---|---|---|---|
| M_AXI_GP0/1 | AXI4-Full | 32位 | 通用数据访问 |
| M_AXI_HP0/1/2/3 | AXI4-Full | 64位 | 高速数据通路(DDR) |
| M_AXI_ACP | AXI4-Full | 64位 | 加速器一致性端口 |
| S_AXI_GP0 | AXI4-Lite | 32位 | PL访问PS寄存器 |
在Vivado中配置这些接口很简单:打开Block Design,添加Zynq Processing System IP,双击后在PS-PL Configuration页面勾选需要的接口。我个人习惯把HP接口留给大数据通路,GP接口用于控制。
自定义AXI4-Lite IP核封装
接下来是实战环节。咱们封装一个自定义AXI4-Lite IP核,包含4个32位寄存器:2个可读写(控制寄存器),2个只读(状态寄存器)。
步骤1:使用Vivado IP Packager
- Tools → Create and Package New IP
- 选择Create AXI4 Peripheral
- 设置IP名称、版本、接口类型(AXI4-Lite)
- 指定寄存器数量:4个
- 完成向导,生成模板
步骤2:修改RTL代码
Vivado生成的模板中,核心逻辑在slv_reg0到slv_reg3的读写控制。我通常这样修改:
// 写操作逻辑
always @(posedge S_AXI_ACLK)
begin
if (S_AXI_ARESETN == 1'b0)
begin
slv_reg0 <= 32'h0;
slv_reg1 <= 32'h0;
end
else
begin
if (S_AXI_WVALID && S_AXI_WREADY && S_AXI_AWVALID && S_AXI_AWREADY)
begin
case (S_AXI_AWADDR[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB])
2'h0 : slv_reg0 <= S_AXI_WDATA;
2'h1 : slv_reg1 <= S_AXI_WDATA;
default : ;
endcase
end
end
end
// 读操作逻辑
always @(*)
begin
case (S_AXI_ARADDR[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB])
2'h0 : reg_data_out <= slv_reg0;
2'h1 : reg_data_out <= slv_reg1;
2'h2 : reg_data_out <= slv_reg2; // 只读
2'h3 : reg_data_out <= slv_reg3; // 只读
default : reg_data_out <= 32'h0;
endcase
end
步骤3:编写Linux驱动
在PS端访问这些寄存器,我推荐用UIO(Userspace I/O)驱动,简单高效:
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#define REG_BASE 0x43C00000 // AXI-Lite基地址
#define REG_RANGE 0x10000 // 地址范围
int main() {
int fd = open("/dev/mem", O_RDWR | O_SYNC);
void *map_base = mmap(NULL, REG_RANGE,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, REG_BASE);
volatile uint32_t *reg0 = (uint32_t *)(map_base + 0x00);
volatile uint32_t *reg1 = (uint32_t *)(map_base + 0x04);
volatile uint32_t *reg2 = (uint32_t *)(map_base + 0x08);
volatile uint32_t *reg3 = (uint32_t *)(map_base + 0x0C);
// 写控制寄存器
*reg0 = 0x12345678;
*reg1 = 0xAABBCCDD;
// 读状态寄存器
printf("Status0: 0x%08X\n", *reg2);
printf("Status1: 0x%08X\n", *reg3);
munmap(map_base, REG_RANGE);
close(fd);
return 0;
}
调试技巧:在封装IP核时,我习惯先在Vivado中做行为仿真,用AXI Verification IP作为主设备发送读写事务。这样可以提前发现握手协议问题,避免上板后抓头皮。
总结一下
AXI4协议是Zynq开发的基石。Full负责大块数据搬运,Lite负责控制通路,Stream负责连续数据流。配置PS-PL接口时,根据带宽需求选择GP、HP或ACP。自定义IP核封装时,注意寄存器地址对齐和握手时序。
嗯,这一章的内容就到这里。记住:协议规范是死的,但应用是活的。多动手、多仿真、多上板验证,你很快就能掌握AXI的精髓。