1. 为什么ADC采集是Zynq bring-up的试金石
做过Zynq-7000平台bring-up的人都有一个共识:GPIO点灯只是热身,DDR跑通算入门,真正能把PS和PL协同跑顺的第一个综合考验,是ADC数据采集。原因很简单——它同时牵扯到PL侧的时序逻辑、PS侧的总线访问、中断响应、DMA搬运、数据后处理这一整条链路,任何一个环节出问题,现象都是"数据不对",但根因可能藏在完全不同的地方。
我前后在三个不同的Zynq-7000板子上做过ADC采集的bring-up,从最朴素的XADC内部采集,到外挂SPI接口的独立ADC芯片,再到并行接口的高速ADC配合AXI-DMA,每一轮都踩过不同的坑。这篇内容就把这套工程化的bring-up流程完整拆开,从方案选型、PL逻辑设计、PS端驱动、DMA配置到问题排查,尽量把每个"为什么这么做"讲透。适合正在做Zynq平台bring-up的嵌入式工程师,也适合刚接触PS/PL协同、想找一个完整实战项目练手的同学。哪怕你之前只点过灯、跑过Hello World,跟着这个思路走一遍,对整个Zynq系统的理解会上一个台阶。
核心关键词就几个:Zynq-7000、ADC数据采集、PS/PL协同、AXI总线、DMA搬运、工程化bring-up。下面按实际项目推进的顺序展开。
2. 方案选型:三种ADC采集路径怎么选
在动手写代码之前,先要把"用哪种方式采集ADC"这件事定下来。Zynq-7000平台上常见的ADC采集路径有三条,选错了后面会反复返工。
2.1 三条路径的对比与适用场景
第一条是片内XADC。Zynq-7000的PS部分自带一个XADC硬核,支持双通道12位1MSPS采样,可以直接采外部模拟电压,也可以通过内部通道监测芯片温度和供电电压。优点是零外围成本、PS端直接读寄存器就能拿到数据,缺点是采样率和通道数都有限,而且模拟输入范围固定(一般0到1V),前端需要分压或运放调理。
第二条是外挂SPI接口ADC。比如常见的12位/16位逐次逼近型ADC芯片,通过SPI和PL通信,PL再通过AXI把数据送到PS。优点是灵活、通道多、精度可选,缺点是采样率受SPI时钟限制,一般到几百kSPS量级,适合中低速多通道场景。
第三条是并行接口高速ADC + AXI-DMA。ADC输出并行数据加时钟,PL侧用IDDR或普通寄存器采样,攒够一批数据后通过AXI-Stream接口送进DMA,DMA直接搬到DDR。优点是能跑到几十甚至上百MSPS,适合波形采集、频谱分析这类场景,缺点是PL逻辑复杂、时序约束严格、调试难度大。
| 路径 | 采样率量级 | 通道数 | PL逻辑复杂度 | 典型场景 |
|---|---|---|---|---|
| 片内XADC | 1MSPS | 2 | 极低 | 电压监测、温度采集 |
| SPI外挂ADC | 100k~1MSPS | 4~8 | 中 | 多通道传感器采集 |
| 并行ADC+DMA | 10~100MSPS | 1~2 | 高 | 波形采集、频谱分析 |
2.2 选型背后的判断逻辑
选型的核心判断依据是采样率需求和数据吞吐量。这里有个容易忽略的换算:假设你用16位ADC、100MSPS采样,那么原始数据率就是200MB/s,这个量级已经接近Zynq-7000 DDR带宽的相当一部分,必须用DMA而不能靠CPU逐个读寄存器。反过来,如果你只是采几个慢变的电压信号,每秒采几次,那用XADC或者SPI都绰绰有余,上DMA纯属杀鸡用牛刀。
我的建议是:bring-up阶段先用XADC把整条PS/PL链路跑通,因为XADC最简单,能快速验证AXI互联、中断、驱动框架是否正常。等这套流程顺了,再换成SPI或并行ADC,此时你只需要替换PL侧的采集逻辑,PS侧的DMA和驱动框架基本可以复用。这个"先易后难、分层验证"的思路,是我踩过坑之后总结出来的——一上来就搞高速并行ADC,一旦数据不对,你根本分不清是PL时序问题、DMA配置问题还是驱动问题。
提示:不要跳过XADC直接上高速ADC。XADC是验证整个数据通路的最佳"最小可运行系统",它的价值不在于采集本身,而在于帮你隔离问题。
3. PL侧设计:从XADC到AXI-Stream的完整逻辑
PL侧是整个采集系统的数据源头,设计好坏直接决定后面调试的痛苦程度。这一章以XADC为例讲清楚PL逻辑,再说明如何平滑迁移到外挂ADC。
3.1 XADC的两种使用方式与选择
XADC在Zynq里有两种用法。第一种是PS直接访问,通过PS端的XADC寄存器接口读取,不需要PL参与,Vivado里甚至不用例化任何IP。第二种是PL侧例化XADC原语,把XADC的转换结果通过AXI-Stream接口输出,再配合DMA搬运。两种方式各有用途:前者适合低频监测,后者适合需要连续采集、批量搬运的场景。
做工程化bring-up,我推荐用第二种,因为它能完整走通"PL采集→AXI-Stream→DMA→DDR→PS处理"这条链路,这套链路一旦跑通,换成任何外挂ADC都是同样的框架。具体做法是在Vivado的Block Design里例化XADC Wizard IP,配置为连续采样模式,通道使能你实际用到的那个(比如VAUXP0/VAUXN0),然后把它输出的m_axis接口接到AXI-DMA的S2MM端口。
3.2 AXI-Stream握手时序的关键细节
AXI-Stream的握手规则看起来简单——TVALID和TREADY同时为高时数据才算传输成功——但实际调试时最容易在这里翻车。XADC输出的m_axis_tvalid是它自己控制的,而DMA的s_axis_tready取决于DMA当前是否准备好接收。如果DMA没准备好,XADC的数据就会被丢弃(因为XADC不支持背压,它采完就往外推)。
解决这个问题的标准做法是在XADC和DMA之间加一个FIFO做缓冲。FIFO的写侧接XADC的Stream,读侧接DMA的Stream,这样即使DMA短暂没准备好,数据也能先存进FIFO,不会丢。FIFO深度根据你的采样率和DMA响应延迟来定,一般512或1024深度足够。这个细节很多教程不讲,但不加FIFO在实际项目里几乎必丢数据。
3.3 从XADC迁移到外挂ADC的改动点
等你把XADC链路跑通,换成SPI或并行ADC时,PL侧需要改的其实只有"数据源"这一块。SPI ADC的话,你需要写一个SPI Master状态机,按ADC芯片的时序要求发出SCLK、CS、MOSI,读回MISO上的转换结果,然后把结果打包成AXI-Stream格式送进FIFO。并行ADC的话,用IDDR原语在时钟上下沿分别采样数据位,或者用普通的同步寄存器在ADC时钟域采样,再跨时钟域同步到AXI时钟域。
这里有个跨时钟域的坑要重点说:ADC的采样时钟和AXI总线时钟通常是异步的,数据从ADC时钟域进到AXI时钟域必须做异步FIFO或者双寄存器同步。我见过有人直接用同一个时钟驱动ADC和AXI,结果要么ADC采样率上不去,要么时序约束过不了。正确做法是PL里用两个时钟域,中间用异步FIFO隔离,FIFO的读写时钟分别接ADC时钟和AXI时钟。
注意:跨时钟域不做同步是Zynq PL设计里最常见的隐性bug,现象是数据偶尔错一位或者整批数据错乱,而且很难复现。养成"跨时钟域必加同步"的习惯。
4. PS侧驱动:DMA配置与中断处理实战
PL把数据推到DMA之后,PS侧的工作就是配置DMA、启动传输、处理中断、读取数据。这部分是软件工程师的主场,但同样有不少细节决定成败。
4.1 AXI-DMA的初始化顺序
AXI-DMA的初始化有一套固定顺序,顺序错了DMA不工作。标准流程是:先复位DMA(写S2MM控制寄存器的Reset位),等待复位完成;然后配置S2MM的数据宽度、突发长度等参数;接着设置目标地址(DDR里的缓冲区物理地址);最后写长度寄存器启动传输。每一步之间要读寄存器确认状态,不能一股脑写完就完事。
缓冲区地址这里有个关键点:必须用物理地址,不能用虚拟地址。Linux下要用dma_alloc_coherent分配一致性内存,裸机下要确保地址在DDR范围内且没有被其他代码占用。我见过有人在Linux下直接把malloc返回的指针写进DMA寄存器,结果DMA搬到了完全错误的地方,因为那是虚拟地址。这个坑非常典型。
4.2 中断 vs 轮询:怎么选
DMA传输完成有两种通知方式:中断和轮询状态寄存器。小数据量、低频采集用轮询就够了,代码简单。但连续采集场景必须用中断,否则CPU一直空转在轮询上,浪费算力。
中断配置要注意两点。第一,AXI-DMA的中断要连接到PS的GIC(中断控制器),在Vivado里要把DMA的mm2s_introut和s2mm_introut接到Zynq PS的pl_ps_irq端口。第二,中断服务程序里要先清DMA的中断状态寄存器,再清GIC的中断标志,顺序反了会导致中断反复触发或者丢失。这个顺序问题我在两个项目里都遇到过,属于典型的"文档不写但必须知道"的细节。
4.3 双缓冲(Ping-Pong)实现连续采集
单缓冲的问题是:DMA搬完一批数据,CPU处理期间DMA就停了,采集不连续。要连续采集必须用双缓冲,也就是准备两块缓冲区A和B,DMA搬A的时候CPU处理B,搬完A自动切到B,如此循环。
AXI-DMA本身支持简单的双缓冲(通过S2MM的循环模式),但更灵活的做法是在驱动层手动管理:DMA搬完A触发中断,中断里立刻启动B的传输,同时把A的数据交给上层处理。这样只要CPU处理速度跟得上采集速度,就能实现无缝连续采集。缓冲区大小要根据"CPU处理一批数据的时间"和"采集一批数据的时间"来平衡,一般让采集时间略大于处理时间,留点余量。
// 双缓冲中断处理的核心逻辑(伪代码) void dma_s2mm_isr(void) { u32 completed = read_dma_status(); if (completed & BUF_A_DONE) { clear_dma_irq(); start_dma_transfer(BUF_B, BUF_SIZE); // 立刻启动B process_buffer(BUF_A, BUF_SIZE); // 处理A } else if (completed & BUF_B_DONE) { clear_dma_irq(); start_dma_transfer(BUF_A, BUF_SIZE); // 立刻启动A process_buffer(BUF_B, BUF_SIZE); // 处理B } }5. 数据后处理与精度校准
数据搬进DDR只是第一步,真正有价值的是把原始码值转换成有物理意义的电压值,并且保证精度。
5.1 码值到电压的换算
XADC是12位,满量程对应1V(单极性模式),换算公式是:电压 = 码值 / 4096 × 1V。如果是双极性模式,满量程是±0.5V,公式要相应调整。外挂ADC的换算要看芯片手册,比如16位、参考电压2.5V、单极性的ADC,电压 = 码值 / 65536 × 2.5V。
换算本身简单,但要注意参考电压的精度直接决定测量精度。如果你用芯片内部的参考源,精度一般在±1%左右;要更高精度就得外接精密参考源。这个在选型阶段就要考虑,别等数据出来了才发现精度不够。
5.2 校准的实操方法
任何ADC都有零点误差和增益误差,工程化项目必须校准。校准方法很简单:输入一个已知的精确电压(比如用精密基准源给0.5V),读回码值,和理论码值比较,算出偏移和增益系数,之后所有数据都用这两个系数修正。
我一般做两点校准:输入接近零的电压和接近满量程的电压,各读一批数据取平均,然后解出偏移和增益。这样比单点校准更准,能同时修正零点和增益误差。校准系数存在Flash或EEPROM里,每次上电加载,避免每次都要重新校准。
5.3 数字滤波降噪
原始ADC数据往往有噪声,尤其是高速采集时。简单的降噪方法是滑动平均,把连续N个点求平均,噪声能降√N倍。但滑动平均会损失带宽,对快速变化的信号不适用。更好的选择是FIR或IIR数字滤波器,根据信号带宽设计滤波器系数。
在Zynq上做滤波有个优势:可以用PL做硬件加速。把FIR滤波器用HLS综合成PL逻辑,数据从DMA搬进来直接流过硬件滤波器,比CPU软件滤波快几个数量级。这是PS/PL协同的典型应用,也是Zynq相比纯MCU的核心优势。
6. 常见问题与排查技巧实录
这一章是我踩坑最多的地方,整理成速查表,遇到问题直接对照。
6.1 数据全为零或全为固定值
这是最常见的现象。排查顺序:先确认PL侧XADC或ADC是否真的在输出数据(用ILA抓AXI-Stream的TVALID和TDATA),如果PL没输出,问题在PL;如果PL有输出但DMA搬回来是零,检查DMA的目标地址是否正确、DMA是否真的启动了、DDR那块内存是否可写。我遇到过一次是DMA目标地址写成了虚拟地址,现象就是全零。
6.2 数据偶尔错乱或丢批
这种间歇性问题最难查。常见原因有三个:跨时钟域没同步、FIFO深度不够导致溢出、DMA中断处理太慢导致缓冲区被覆盖。排查方法是用ILA长时间抓波形,看FIFO的溢出标志、DMA的中断频率是否稳定。如果是跨时钟域问题,加异步FIFO;如果是FIFO溢出,加大深度或提高DMA响应速度。
6.3 采样率上不去
如果你设定了高采样率但实际数据率远低于预期,先算一下理论带宽:采样率×位宽×通道数,看是否超过了AXI总线或DDR的带宽。AXI-Stream在100MHz时钟、32位宽度下理论带宽是400MB/s,实际能到300MB/s左右。如果ADC数据率接近这个值,就要考虑用更宽的AXI位宽或者提高AXI时钟。
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| 数据全零 | DMA地址错/未启动 | 查DMA寄存器状态 | 用物理地址,确认启动 |
| 数据偶尔错乱 | 跨时钟域未同步 | ILA抓FIFO溢出 | 加异步FIFO |
| 丢批 | 中断处理慢 | 测中断响应时间 | 加大缓冲/优化ISR |
| 采样率低 | 带宽瓶颈 | 算理论带宽 | 加宽位宽/提时钟 |
| 精度差 | 参考源差/未校准 | 对比基准源 | 换参考源+两点校准 |
6.4 几个独家避坑技巧
第一个技巧:bring-up时先用固定测试数据代替ADC。在PL里写一个计数器,把计数值当数据推给DMA,这样能先验证DMA和驱动链路,排除ADC本身的干扰。等链路通了再接真实ADC,问题范围立刻缩小一半。
第二个技巧:ILA的触发条件要设得巧。不要只抓TVALID,要抓"TVALID为高但TREADY为低"这种异常组合,这才是丢数据的瞬间。我一般设两个触发:正常传输和异常握手,对比看差异。
第三个技巧:DMA缓冲区加魔数校验。在缓冲区头部和尾部写固定的魔数,DMA搬完后检查魔数是否被覆盖,能快速判断是否发生了缓冲区越界或覆盖。
7. 工程化bring-up的推进节奏建议
最后聊聊整个项目的推进节奏。我的经验是分四个阶段:第一阶段只跑XADC+PS直接读,验证PS基本功能;第二阶段加PL的XADC+AXI-Stream+DMA,验证PL/PS协同;第三阶段换成真实外挂ADC,验证时序和精度;第四阶段做双缓冲连续采集和数字滤波,达到工程可用状态。
每个阶段都要有明确的验收标准,比如第二阶段的标准是"连续采集10分钟数据无丢失、无错乱"。不要跳阶段,也不要在一个阶段没验收通过就急着进下一个,否则问题会累积,最后根本查不清。这套节奏我在多个项目里用过,虽然看起来慢,但总体返工少,反而更快。
后续如果要扩展,可以往两个方向走:一是多通道同步采集,多个ADC用同一个触发信号,PL里做通道对齐;二是把采集和实时处理结合,用PL做FFT或滤波,PS只负责显示和存储。这两个方向都是Zynq PS/PL协同的典型进阶应用,等基础链路稳了再往上加,会顺很多。