深入Zynq-7000:手把手教你用Vivado IP核打通PS与PL的“任督二脉”
在工业控制、智能摄像头、无人机飞控这些对实时性和算力要求极高的场景里,你有没有遇到过这样的困境——ARM处理器跑软件太慢,加个FPGA又嫌通信延迟高?这时候,Xilinx Zynq-7000 系列器件就像是为你量身定制的“双核战神”:一边是双核 Cortex-A9 跑操作系统,另一边是 FPGA 实现硬件加速,两者共享内存、低延互通。而真正让这个架构“活起来”的钥匙,就是Vivado IP 核与 PL 端的无缝交互。
但问题来了:IP 怎么选?AXI 接口怎么连?地址怎么配?写寄存器没反应怎么办?别急,这篇文章不讲空话套话,咱们从实战出发,一步步拆解整个流程,让你彻底搞懂 Zynq 上 PS 和 PL 是如何“对话”的。
为什么非要用 Vivado IP 核?
先说点实在的。如果你打算自己写一个 I2C 控制器或者 DDR 接口逻辑,那恭喜你,准备好熬夜调时序吧。而 Xilinx 提供的 Vivado IP 核,本质上是一堆经过验证、可复用的功能模块,就像乐高积木一样,拖进来、配一下、连上线,就能用。
比如你要做个带 DMA 的 ADC 数据采集系统,传统做法是从头设计状态机、握手机制、缓存管理……而现在呢?只需要:
- 找到
AXI DMAIP; - 加上
AXI Interconnect做路由; - 再接一个你自己写的 AXI Slave 外设;
- 自动连线搞定时钟和复位。
几分钟的事,比写 RTL 快多了,还更稳定。
IP 核到底分几种?
| 类型 | 是否可见源码 | 能不能改 | 典型代表 |
|---|---|---|---|
| 黑盒 IP | ❌ | ❌ | GTX 收发器(加密) |
| 可编辑 IP | ✅ | ✅ | UART Lite, Timer |
| 加密 IP | ❌ | ❌(但能配置参数) | PCIe, Ethernet MAC |
建议初学者优先使用可编辑 IP,出了问题还能进去看代码,不至于一头雾水。
Zynq 的“神经系统”:AXI 总线是怎么连接 PS 和 PL 的?
很多人搞不清 Zynq 里 PS 和 PL 到底是怎么通信的。其实你可以把 PS 当成“大脑”,PL 是“手脚”。大脑发指令,手脚执行;手脚有感觉了,也得及时上报。
它们之间的“神经通路”就是 AXI 总线。具体来说,Zynq-7000 提供三类主要接口:
| 接口类型 | 带宽能力 | 用途场景 | 推荐与否 |
|---|---|---|---|
| M_AXI_GP0/1 | ~800 MB/s | 寄存器读写、低速外设控制 | ✅ 日常控制首选 |
| S_AXI_HP0~3 | ~6.4 GB/s(理论) | 高速数据搬运,如图像、DMA | ✅ 大数据必用 |
| S_AXI_ACP | 缓存一致性 | 多核协同、OpenAMP 架构 | ⚠️ 特定场景 |
📌 小贴士:GP = General Purpose,HP = High Performance,ACP = Accelerator Coherency Port
举个例子:你想做一个视频采集系统,CMOS 传感器进来的数据先由 PL 缓存,再送到 DDR,最后 CPU 来处理。这时候如果只用 GP 接口搬图,带宽根本不够,画面卡成幻灯片。正确的做法是:
- PL 写 DDR → 走 S_AXI_HP;
- CPU 控制开始/停止 → 走 M_AXI_GP;
- 完美分工,各司其职。
实战第一步:在 Vivado 里搭出你的 Block Design
打开 Vivado,新建工程,选择 Zynq-7000 芯片后,进入Block Design (BD)页面。这是整个硬件设计的核心舞台。
Step 1: 添加 ZYNQ7 Processing System
双击添加ZYNQ7 Processing SystemIP,它代表的就是 PS 部分。双击打开配置界面,关键操作如下:
- Page -> Clocking:启用 FCLK_CLK0(通常给 PL 用,比如 100MHz)
- Page -> Peripheral I/O Pins:打开你需要的外设,比如 SDIO、UART、Ethernet
- Page -> Interrupts:勾选
Fabric Interrupt Port,允许 PL 向 PS 发中断 - Page -> PS-PL Configuration→ AXI Interfaces:
- 启用
M_AXI_GP0(用于寄存器访问) - 启用
S_AXI_HP0(用于高速数据传输)
💡 经验之谈:不要一次性开太多接口!资源紧张的小板子(比如 Zybo Z7),开多了会布线失败。
Step 2: 添加你的自定义 IP 或标准外设
假设你现在要控制一个 PWM 波输出,可以添加AXI TimerIP;或者想读取 GPIO 状态,就加个AXI GPIO。
以AXI GPIO为例:
- 设置通道 1 为 8 位输入,通道 2 为 8 位输出;
- 连接到 M_AXI_GP0;
- Vivado 会自动提示:“Run Connection Automation”,点它!
这一步会自动完成:
- 时钟连接(接 FCLK_CLK0)
- 复位连接(通过proc_sys_resetIP 同步复位)
- 地址分配(跳转到 Address Editor)
Step 3: 地址分配与中断设置
点击菜单栏的Address Editor,你会看到每个从设备都被分配了一个基地址,例如:
| Module | Base Address |
|---|---|
| axi_gpio_0 | 0x4120_0000 |
| my_custom_ip | 0x43C0_0000 |
| axi_timer_0 | 0x4280_0000 |
记住这些地址!后面 SDK/Vitis 里要用。
再到Interrupts标签页,确认你的 IP 是否连接到了IRQ_F2P[0]。如果是,说明它可以触发中断。
最后生成 HDL Wrapper,导出.xsa文件,准备交给软件端。
软件怎么访问 PL?别只会Xil_Out32
硬件做好了,下一步是在 Vitis 中创建应用工程,导入.xsa,然后开始写 C 代码。
最基础的操作是读写寄存器。比如你的 IP 有一个控制寄存器偏移为 0x00:
#include "xil_io.h" #define MY_IP_BASEADDR 0x43C00000 #define REG_CTRL 0x00 #define REG_STATUS 0x04 void ip_enable(void) { Xil_Out32(MY_IP_BASEADDR + REG_CTRL, 0x01); } u32 ip_get_status(void) { return Xil_In32(MY_IP_BASEADDR + REG_STATUS); }看起来很简单对吧?但新手常踩的坑在这里:地址错了、没使能时钟、忘了复位同步。
如何快速验证是否连通?
一个小技巧:在 PL 侧拉一个 LED,连接到 IP 的某个输出位。然后你在 C 代码里不断写值:
for (int i = 0; i < 10; i++) { Xil_Out32(MY_IP_BASEADDR + REG_CTRL, 0x01); sleep(1); Xil_Out32(MY_IP_BASEADDR + REG_CTRL, 0x00); sleep(1); }如果板子上的灯真的在闪,说明软硬通道已经打通!
高级玩法:用 AXI DMA 实现千兆级数据搬运
前面说的是“发命令”,现在我们来玩“传大数据”。
比如你在 PL 侧采集 ADC 数据流(AXI-Stream),想送进 DDR,供 CPU 后续分析。这时候就得靠AXI DMA+S_AXI_HP组合拳。
硬件连接要点:
- 在 BD 中添加
AXI DMAIP; M_AXIS_MM2S连接到你的数据源(如 FFT 输出);S_AXI_LITE接 M_AXI_GP0(用于 CPU 配置);S2MM_SKT接 S_AXI_HP0(接收 CPU 写入的数据);MM2S_SKT接 S_AXI_HP0(发送数据到 DDR);- 时钟都来自
FCLK_CLK0,复位接ARESETN。
软件端初始化 DMA:
#include "xaxidma.h" XAxiDma dma; int init_dma() { XAxiDma_Config *cfg; cfg = XAxiDma_LookupConfig(XPAR_AXIDMA_0_DEVICE_ID); if (!cfg) { xil_printf("No config found for %d\r\n", XPAR_AXIDMA_0_DEVICE_ID); return XST_FAILURE; } int status = XAxiDma_CfgInitialize(&dma, cfg); if (status != XST_SUCCESS) { return XST_FAILURE; } // 确保不处于 Scatter-Gather 模式 if (XAxiDma_HasSg(&dma)) { xil_printf("Device configured in SG mode, this example requires simple mode only.\r\n"); return XST_FAILURE; } return XST_SUCCESS; }开始传输数据:
#define BUFFER_ADDR 0x10000000 // DDR 中的一段物理地址 #define DATA_LENGTH (1024 * 4) // 4KB 数据 void start_transfer() { // 启动从 PL 到 DDR 的传输(MM2S) XAxiDma_SimpleTransfer(&dma, BUFFER_ADDR, DATA_LENGTH, XAXIDMA_DEVICE_TO_DMA); // 等待完成(实际项目中建议用中断) while (XAxiDma_Busy(&dma, XAXIDMA_DEVICE_TO_DMA)); xil_printf("DMA transfer complete!\r\n"); }🔍 注意事项:
-BUFFER_ADDR必须是物理地址,且已被映射到用户空间(裸机下直接可用);
- 如果开了 Cache,记得调用Xil_DCacheFlushRange()刷新;
- 强烈建议配合中断使用,避免轮询浪费 CPU。
常见“翻车现场”及排错指南
❌ 现象一:Xil_In32读回来全是 0 或 -1
可能原因:
- 地址没对齐:检查 Address Editor 分配的地址是否和代码一致;
- 时钟没起来:FCLK_CLK0 没使能或频率为 0;
- IP 没响应:ILA 抓一下 AXI 信号,看看AWVALID进去了没有。
👉 解法:用 ILA 打两根线——s_axi_awvalid和s_axi_rvalid,运行程序看有没有握手。
❌ 现象二:中断死活进不去 ISR
典型症状:注册了中断服务函数,但就是不触发。
排查步骤:
1. 确认 BD 中 IRQ 已连接至IRQ_F2P[0];
2. 在 PS 端使能 GIC(通用中断控制器);
3. 检查是否调用了XScuGic_Connect()和XScuGic_Enable();
4. PL 端是否真的拉高了中断信号?
示例代码片段:
static void MyISR(void *CallbackRef) { xil_printf("Interrupt triggered!\n"); // 清除中断源(根据你的 IP 设计) Xil_Out32(MY_IP_BASEADDR + INTR_CLR_REG, 0x1); // 必须调用 EOI XScuGic_Eject(&InterruptController, XPAR_XSCUGIC_0_DEVICE_ID); }❌ 现象三:DMA 传一半卡住
常见于大块数据传输,尤其是开启了 Cache 的情况。
解决方案:
// 传输前刷新目标区域 Xil_DCacheInvalidateRange(BUFFER_ADDR, DATA_LENGTH); // 写完后也要刷一遍(如果是双向传输) Xil_DCacheFlushRange(BUFFER_ADDR, DATA_LENGTH);否则 CPU 看到的可能是旧数据!
最佳实践清单:老司机私藏笔记
| 项目 | 建议 |
|---|---|
| 时钟设计 | PL 所有时钟尽量来自 PS 提供的 FCLK,避免异步跨时钟域问题 |
| 复位同步 | 使用proc_sys_resetIP 输出的peripheral_aresetn |
| 地址规划 | 提前规划好地址段,避免冲突(推荐从 0x43C0_0000 起) |
| 资源监控 | 综合后查看 utilization,重点关注 LUT、FF、BRAM 使用率 |
| 版本匹配 | Vivado 2023.1 打开 2018.3 工程需升级 IP,务必执行Report IP Status |
| 文档留存 | 保存 BD 截图 + 地址表 + 中断映射表,后期维护省一半力气 |
结语:掌握这套组合技,你也能做异构系统高手
Zynq 不是一个简单的 SoC,而是一个软硬协同的设计平台。当你学会用 Vivado IP 核快速构建功能模块,并通过 AXI 总线将其与 PS 深度融合时,你就已经迈入了高级嵌入式开发的大门。
无论是做图像预处理、电机实时控制,还是边缘 AI 推理加速,这套“PS + PL + AXI + DMA”的组合技都能派上大用场。更重要的是,它教会你一种思维方式:哪里慢就用硬件加速,哪里复杂就用 IP 复用。
下次当你面对性能瓶颈时,不妨问一句:这个任务,能不能扔给 PL 去跑?
如果你正在尝试某个具体项目(比如摄像头采集、音频处理、SPI 协议扩展),欢迎留言交流,我们可以一起 debug,把想法变成现实。