手把手教你用AXI4-Lite在ZYNQ上做个简易“聊天室”:PS发指令,PL回数据
用AXI4-Lite在ZYNQ上搭建PS与PL的"对话系统"
想象一下,ZYNQ芯片中的处理器系统(PS)和可编程逻辑(PL)就像两个需要频繁交流的同事。PS是大脑,负责决策和复杂计算;PL是四肢,擅长快速执行特定任务。要让它们高效协作,就需要一套可靠的"对话机制"——这就是AXI4-Lite总线协议的用武之地。本文将带你用Vivado和Vitis打造一个简易的"聊天室",让PS和PL能够自由交换数据。
1. 项目规划与AXI4-Lite基础
在开始动手前,我们需要明确几个关键概念。AXI4-Lite是ARM AMBA总线协议的精简版本,专为低复杂度、低功耗的寄存器级通信设计。它相比完整的AXI4协议,去除了突发传输、缓存支持等高级功能,保留了最基本的读写操作,特别适合PS与PL之间的控制信号和小数据量传输。
我们的"聊天室"项目需要实现以下功能:
- PS端能够发送32位整数指令到PL
- PL端能够处理指令并返回32位结果数据
- 通信过程稳定可靠,时序符合AXI4-Lite规范
硬件资源需求:
- Xilinx ZYNQ系列开发板(如Zybo、Pynq等)
- Vivado设计套件(2020.1或更新版本)
- Vitis统一软件平台
2. Vivado中的AXI-Lite IP核定制
2.1 创建基础AXI-Lite外设
启动Vivado后,按照以下步骤创建自定义IP:
- 在Tools菜单中选择"Create and Package New IP"
- 选择"Create AXI4 Peripheral"选项
- 设置IP名称为"axi_chatroom"(避免使用默认myip)
- 接口配置:
- Interface Type: Lite
- Interface Mode: Slave
- Data Width: 32
- Number of Registers: 4
注意:虽然我们只需要2个寄存器,但AXI-Lite IP核要求最少4个寄存器。多出的2个可以保持默认配置。
2.2 添加自定义通信端口
在自动生成的IP核代码中,我们需要添加两个关键端口:
// 在axi_lite_slave.v的端口声明部分添加 output reg [31:0] pl_response_data, // PL→PS的数据通道 input wire [31:0] ps_command_data // PS→PL的指令通道然后修改寄存器读写逻辑,将slv_reg0映射到PS指令,slv_reg1映射到PL响应:
// 接收PS指令的逻辑 always @(posedge S_AXI_ACLK) begin if (S_AXI_ARESETN == 1'b0) begin slv_reg0 <= 0; end else if (slv_reg_wren == 1 && axi_awaddr[3:2] == 2'b00) begin slv_reg0 <= S_AXI_WDATA; ps_command_data <= S_AXI_WDATA; // 将写入值同时输出到自定义端口 end end // 发送PL响应的逻辑 always @(posedge S_AXI_ACLK) begin if (S_AXI_ARESETN == 1'b0) begin slv_reg1 <= 0; end else begin slv_reg1 <= pl_response_data; // 持续将PL数据反映到寄存器 end end2.3 集成IP核到Block Design
完成代码修改后,执行IP打包操作,然后在Block Design中添加这个自定义IP:
- 右键画布选择"Add IP",搜索axi_chatroom
- 连接AXI接口到ZYNQ处理器的M_AXI_GP0端口
- 将自定义端口ps_command_data和pl_response_data引出到顶层
- 运行自动连接,验证设计无误后生成Bitstream
关键连接检查点:
- AXI时钟和复位信号正确连接
- 自定义端口在顶层有明确定义
- 地址空间分配合理(可在Address Editor中查看)
3. Vitis中的PS端程序设计
3.1 建立基础工程结构
导出硬件后,切换到Vitis环境:
- 新建Application Project,选择从Vivado导出的.xsa文件
- 创建空白C工程(比模板工程更灵活)
- 在src文件夹下新建main.c文件
- 包含必要的头文件:
#include <stdio.h> #include "xparameters.h" #include "xil_io.h" #include "axi_chatroom.h"3.2 实现双向通信逻辑
首先定义寄存器偏移量,提高代码可读性:
#define CMD_REG_OFFSET 0 // PS写指令的寄存器 #define RESP_REG_OFFSET 4 // PS读响应的寄存器然后编写主通信函数:
void chat_with_pl(u32 baseaddr, u32 command) { // PS发送指令 AXI_CHATROOM_mWriteReg(baseaddr, CMD_REG_OFFSET, command); printf("PS发送: %d\n", command); // 等待PL处理(简单延时) for(int i=0; i<100000; i++); // PS读取响应 u32 response = AXI_CHATROOM_mReadReg(baseaddr, RESP_REG_OFFSET); printf("PL回复: %d\n", response); }在main函数中初始化并测试通信:
int main() { u32 baseaddr = XPAR_AXI_CHATROOM_0_S_AXI_BASEADDR; printf("AXI-Lite聊天室启动...\n"); // 测试通信 for(int i=1; i<=5; i++) { chat_with_pl(baseaddr, i*10); } return 0; }4. PL端逻辑设计与功能扩展
4.1 基本响应逻辑实现
在PL端,我们可以设计简单的处理逻辑,比如将接收到的数值加1后返回:
// 在自定义IP的顶层模块中添加处理逻辑 always @(posedge S_AXI_ACLK) begin if (!S_AXI_ARESETN) begin pl_response_data <= 32'h0; end else begin // 简单示例:将PS发送的值+1后返回 pl_response_data <= ps_command_data + 1; end end4.2 进阶功能:状态机实现
要实现更复杂的交互,可以在PL端添加状态机:
// 定义状态编码 localparam IDLE = 2'b00; localparam PROCESS = 2'b01; localparam RESPOND = 2'b10; reg [1:0] state; reg [31:0] processed_data; always @(posedge S_AXI_ACLK) begin if (!S_AXI_ARESETN) begin state <= IDLE; pl_response_data <= 0; end else begin case(state) IDLE: if (ps_command_data != 0) begin processed_data <= ps_command_data * 2; // 示例处理:乘以2 state <= PROCESS; end PROCESS: begin pl_response_data <= processed_data + 1; // 再加1 state <= RESPOND; end RESPOND: if (ps_command_data == 0) // 等待PS清零 state <= IDLE; endcase end end4.3 调试技巧与性能优化
在实际开发中,有几个关键点需要注意:
时序约束:确保AXI接口满足时序要求
create_clock -name S_AXI_ACLK -period 10 [get_ports S_AXI_ACLK]调试信号:添加ILA核监控关键信号
ila_0 your_ila_instance ( .clk(S_AXI_ACLK), .probe0(ps_command_data), .probe1(pl_response_data), .probe2(state) );性能指标对比:
| 优化方式 | 延迟(周期) | 资源消耗(LUT) |
|---|---|---|
| 直接传递 | 1 | 50 |
| 流水线处理 | 3 | 75 |
| 状态机实现 | 5-10 | 120 |
在实际项目中,我遇到过PL响应不及时导致PS读取旧数据的问题。解决方案是在PL处理完成后生成一个中断信号,PS收到中断后再读取数据,这样既保证了数据新鲜度,又避免了PS不断轮询的开销。
