ZYNQ7010 与 AD7768 高精度 ADC 采集:从硬件到数据落地全攻略
简介这份资源面向具备软硬件协同开发基础的电子工程师与FPGA学习者聚焦ZYNQ7010 SoC采集AD7768高精度ADC的完整工程实现可用于工业测量、声学分析、无线通信及高端音频等高速数据采集场景。压缩包共1726个文件约69.75MB涵盖dtsi设备树、h头文件、c源码、tcl脚本、v与sv硬件描述文件、xdc约束、bit比特流及elf可执行文件等完整覆盖FPGA逻辑、ARM软件与工程配置各环节。资源围绕AD7768_Time_20m采集任务包含ADC参数配置、数据流采集与预处理、FFT分析及以太网传输等模块可帮助读者理解FPGA与ARM任务划分、信号完整性设计与抗干扰策略。目前已有265人学习下载适合作为软硬件联合调试与数据采集系统搭建的参考范例。1. ZYNQ7010 采集 AD7768从硬件搭到数据落地的完整路径手里有一块 ZYNQ7010 和一片 AD7768想把高精度 ADC 的数据稳定收上来这件事听起来简单做起来却容易在某个环节卡住。AD7768 是八通道同步采样的 24 位 Σ-Δ 型 ADC单通道最高 256 kSPS输出是标准 I2S 风格的串行位流ZYNQ7010 的 PL 端逻辑资源不算宽裕PS 端跑 Linux 或裸机都行。两者凑在一起核心矛盾就一个ADC 的位流时钟和帧同步信号必须由 PL 精确产生数据要在不丢帧的前提下搬进内存再交给 PS 做后续处理。这套方案适合做电力监测、振动分析、多通道传感器采集的团队也适合想从 STM32 加外置 ADC 升级到 SoC 架构的开发者。下面按硬件接口、PL 逻辑、PS 搬运、避坑、验证五块展开每一步都给到能直接抄的参数和代码。2. 硬件接口与时钟方案AD7768 到底怎么连到 ZYNQ70102.1 先搞清楚 AD7768 的引脚角色和时序关系AD7768 的串行接口不是普通 SPI它用独立的帧同步信号 DRDY 和位时钟 DCLK 来输出数据。每个通道的数据在 DRDY 下降沿之后按位输出DCLK 的上升沿采样。八通道模式下数据按通道顺序依次移出每通道 24 位加上状态位和 CRC 可选一帧总位数在 192 到 256 位之间。关键参数是 DCLK 频率它等于 MCLK 分频后的结果典型 MCLK 给 16.384 MHzDCLK 可以设到 8.192 MHz 甚至更高。ZYNQ7010 的 PL 端要产生 MCLK 和 DRDY同时用 DCLK 去采样数据线。这里最容易翻车的地方是很多人把 DCLK 当成普通 SPI 时钟去接结果发现数据错位。正确做法是把 DCLK 当作源同步时钟在 PL 里用 IDDR 原语或者直接过采样来对齐。我一般会先确认 AD7768 的配置模式。它支持 SPI 配置寄存器和引脚模式两种引脚模式下通过上下拉电阻设定采样率、滤波器类型、功耗模式。如果只是固定参数采集引脚模式最省事不需要额外写 SPI 配置逻辑。如果要动态改采样率或切换滤波器就得用 SPI 去写寄存器。ZYNQ7010 的 PL 端可以例化一个 AXI SPI IP 或者自己写一个简单的四线 SPI 状态机PS 端通过 AXI-Lite 下发配置。2.2 引脚连接表和电平匹配的硬约束下面这张表是我实际用过的连接方案ZYNQ7010 侧全部走 PL 的普通 IO电平标准设为 LVCMOS33 或 LVCMOS18取决于 AD7768 的 IO 电压。AD7768 的 DVDD 如果是 3.3V就设 LVCMOS33如果是 1.8V就设 LVCMOS18。注意 ZYNQ 的 Bank 电压必须和外部一致否则会烧 IO。AD7768 引脚方向ZYNQ7010 引脚说明MCLK输入PL 普通 IO 输出主时钟典型 16.384 MHzDCLK输出PL 普通 IO 输入位时钟由 AD7768 输出DRDY输出PL 普通 IO 输入帧同步下降沿有效DOUT0~DOUT7输出PL 普通 IO 输入八通道数据线可并联CS输入PL 普通 IO 输出SPI 片选引脚模式可悬空SCLK输入PL 普通 IO 输出SPI 时钟引脚模式可悬空SDI输入PL 普通 IO 输出SPI 数据输入SDO输出PL 普通 IO 输入SPI 数据输出RESET输入PL 普通 IO 输出复位低有效如果八通道全用DOUT0 到 DOUT7 可以都接上也可以只接 DOUT0 然后靠通道顺序解析。AD7768 支持两种输出模式每通道独立数据线或者所有通道共用一条线按顺序输出。共用一条线时PL 端只需要一个输入引脚但解析逻辑要按位计数。独立数据线时PL 端可以并行采样逻辑更简单但占用引脚多。我一般推荐共用一条线因为 ZYNQ7010 的 PL 引脚有限省下来可以接别的外设。2.3 时钟树设计MCLK 从哪来DCLK 怎么对齐MCLK 可以由 PL 的 MMCM 产生也可以从 PS 端通过 AXI 时钟输出。我习惯用 PL 的 MMCM 把 50 MHz 晶振倍频到 16.384 MHz因为 16.384 MHz 是 2 的幂次相关频率分频到 DCLK 时没有累积误差。MMCM 的配置参数输入 50 MHz倍频系数 32分频系数 97输出 16.494 MHz再经过一个小数分频到 16.384 MHz。如果嫌麻烦直接用 PS 的 PLL 输出也行但 PS 时钟抖动比 PL MMCM 大对 ADC 的 SNR 有影响。DCLK 是 AD7768 输出的PL 端要用它采样 DOUT。这里有个坑DCLK 和 DOUT 的相位关系不是固定的AD7768 的数据手册给了一个建立保持窗口但实际 PCB 走线长度不同会导致偏移。稳妥做法是在 PL 里用 IDELAY 或者 IDDR 原语把 DCLK 延迟几个 tap 再去采样。Xilinx 的 SelectIO 原语 IDDR 可以同时采上升沿和下降沿输出双沿数据然后自己拼成 24 位。如果不想用原语也可以用 oversampling用 4 倍 DCLK 频率去采但 ZYNQ7010 的 PL 跑 4 倍 DCLK 在 8 MHz 以上时时序会紧张。// 用 IDDR 采样 DOUTDCLK 作为时钟 IDDR #( .DDR_CLK_EDGE(SAME_EDGE_PIPELINED), .INIT_Q1(1b0), .INIT_Q2(1b0), .SRTYPE(SYNC) ) iddr_dout ( .Q1(dout_rise), // 上升沿数据 .Q2(dout_fall), // 下降沿数据 .C(dclk), // DCLK 输入 .CE(1b1), .D(dout), // DOUT 输入 .R(1b0), .S(1b0) );这段代码把 DOUT 在 DCLK 的上升沿和下降沿分别锁存输出两个位。后续用一个移位寄存器把 dout_rise 和 dout_fall 交替拼成 24 位。注意 IDDR 的 DDR_CLK_EDGE 要设成 SAME_EDGE_PIPELINED这样 Q1 和 Q2 在同一时钟周期输出方便后续处理。如果设成 OPPOSITE_EDGEQ2 会晚半个周期拼接时要多打一拍。3. PL 端采集逻辑从位流到 24 位并行数据的转换3.1 帧同步状态机怎么写才不丢帧AD7768 的 DRDY 下降沿表示一帧数据开始之后 DCLK 连续输出 8 个通道各 24 位。PL 端要做一个状态机检测 DRDY 下降沿启动位计数器每来一个 DCLK 上升沿和下降沿就移入一位计满 24 位就锁存一个通道再计下一个通道直到 8 个通道全部收完输出一个有效标志。这个状态机的难点在于 DRDY 和 DCLK 的相位关系DRDY 下降沿可能正好在 DCLK 的某个边沿附近如果直接用 DRDY 去复位计数器容易采到半个位。稳妥做法是用 DCLK 同步 DRDY打两拍后再检测下降沿。// 帧同步状态机核心片段 reg [1:0] drdy_sync; always (posedge dclk) begin drdy_sync {drdy_sync[0], drdy}; end wire drdy_fall (drdy_sync 2b10); // 检测下降沿 reg [4:0] bit_cnt; // 0~23 位计数 reg [2:0] ch_cnt; // 0~7 通道计数 reg [23:0] shift_reg; // 移位寄存器 reg [23:0] ch_data [0:7]; // 八通道数据缓存 always (posedge dclk) begin if (drdy_fall) begin bit_cnt 5d0; ch_cnt 3d0; end else begin shift_reg {shift_reg[22:0], dout_rise}; // 先移入上升沿数据 // 下降沿数据在下一个周期移入或者用双沿拼接 if (bit_cnt 5d23) begin ch_data[ch_cnt] shift_reg; bit_cnt 5d0; if (ch_cnt 3d7) ch_cnt 3d0; else ch_cnt ch_cnt 1b1; end else begin bit_cnt bit_cnt 1b1; end end end这段代码的逻辑是用 DCLK 同步 DRDY检测下降沿后清零位计数和通道计数。每个 DCLK 周期移入一位计满 24 位就把 shift_reg 存到对应通道然后通道计数加一。注意这里只用了 dout_rise实际要同时用 dout_rise 和 dout_fall每周期移入两位位计数到 11 就够 24 位。我为了简化只写了单沿实际工程里要改成双沿拼接。参数说明bit_cnt 宽度 5 位足够计 0 到 23ch_cnt 宽度 3 位计 0 到 7。shift_reg 是 24 位对应 ADC 的分辨率。3.2 数据缓存与跨时钟域处理PL 端采集到的数据在 DCLK 时钟域要传给 PS 端或者 PL 内部的 AXI 总线必须做跨时钟域处理。常见做法是用一个异步 FIFO写时钟用 DCLK读时钟用 AXI 时钟。FIFO 深度至少 16因为一帧有 8 个 24 位数据加上帧间隔深度 32 比较稳妥。Xilinx 的 FIFO Generator IP 可以直接例化或者自己写一个简单的双口 RAM 加读写指针。注意 FIFO 的写使能要在每个通道数据锁存时拉高一个 DCLK 周期写数据是 24 位通道数据加上通道号。// 异步 FIFO 写侧 wire fifo_wr_en (bit_cnt 5d23) (ch_cnt 3d7); wire [27:0] fifo_wr_data {ch_cnt, ch_data[ch_cnt]}; // 通道号 24 位数据 fifo_generator_0 u_fifo ( .wr_clk(dclk), .rd_clk(axi_aclk), .din(fifo_wr_data), .wr_en(fifo_wr_en), .rd_en(fifo_rd_en), .dout(fifo_rd_data), .full(fifo_full), .empty(fifo_empty) );这段代码把通道号和 24 位数据拼成 28 位写入 FIFO。通道号占 3 位数据占 24 位总共 27 位我多留了 1 位做对齐。FIFO 的读侧由 AXI 主机或者 PS 端的 DMA 控制。如果 PS 端用 DMA可以把 FIFO 的读数据直接接到 AXI-Stream 接口DMA 自动搬运到 DDR。注意 FIFO 的写使能条件要严格只在 bit_cnt 计满 24 且 ch_cnt 有效时拉高否则会写入重复数据。3.3 AXI-Stream 打包与 DMA 搬运PL 端采集完一帧后最好打包成 AXI-Stream 格式每包包含 8 个通道的数据加上一个帧头。帧头可以是一个固定的 32 位标识比如 0xAA55AA55方便 PS 端解析。AXI-Stream 的 tdata 宽度设成 32 位tvalid 和 tready 握手。DMA 用 Xilinx 的 AXI DMA IP配置成简单模式每次搬运一帧或者多帧。DMA 的目标地址是 PS 端 DDR 里的一块连续内存大小按采样率和帧率算。比如 256 kSPS 八通道每通道 24 位一秒钟数据量是 256k * 8 * 3 字节 6 MB/sDDR 完全扛得住。// AXI-Stream 打包状态机 reg [31:0] axis_tdata; reg axis_tvalid; wire axis_tready; always (posedge axi_aclk) begin if (!fifo_empty axis_tready) begin axis_tdata {4b0, fifo_rd_data}; // 28 位扩展到 32 位 axis_tvalid 1b1; fifo_rd_en 1b1; end else begin axis_tvalid 1b0; fifo_rd_en 1b0; end end这段代码从 FIFO 读数据扩展到 32 位后送到 AXI-Stream。tvalid 和 tready 握手成功后DMA 会把数据搬到 DDR。注意 fifo_rd_en 要和 tready 配合如果 tready 一直为高可以连续读如果 tready 有停顿要保证 fifo_rd_en 不丢拍。实际工程里我会加一个小的 skid buffer防止 tready 反压时数据丢失。4. PS 端数据搬运与解析从 DDR 到应用层4.1 DMA 配置与中断处理PS 端用 Xilinx 的 AXI DMA 驱动配置成简单模式源地址是 PL 端的 AXI-Stream 接口目标地址是 DDR 的物理地址。DMA 搬完一帧后产生中断PS 端在中断服务函数里处理数据。中断处理要快不能在里面做复杂运算一般只做标记把数据指针传给应用线程。DMA 的配置参数数据宽度 32 位最大突发长度 256帧长度按实际一帧的字节数设。比如一帧 8 通道 24 位加上帧头 4 字节总共 8*3428 字节但 AXI-Stream 是 32 位对齐实际一帧 8 个 32 位字32 字节。DMA 的传输长度设成 32 的整数倍。// DMA 中断服务函数示例 void dma_isr(void *callback) { u32 irq_status XAxiDma_IntrGetIrq(dma_inst, XAXIDMA_DEVICE_TO_DMA); if (irq_status XAXIDMA_IRQ_IOC_MASK) { // 一帧搬完标记数据就绪 frame_ready 1; // 重新提交 DMA 传输 XAxiDma_SimpleTransfer(dma_inst, (u32)rx_buffer, FRAME_SIZE, XAXIDMA_DEVICE_TO_DMA); } XAxiDma_IntrAckIrq(dma_inst, irq_status, XAXIDMA_DEVICE_TO_DMA); }这段代码在中断里重新提交 DMA 传输保证连续采集。frame_ready 标志给应用线程轮询或者用信号量通知。注意 DMA 的源地址要设成 PL 端 AXI-Stream 的地址目标地址是 DDR 的物理地址虚拟地址要提前用 mmap 映射或者用 dma_alloc_coherent 分配。如果 PS 端跑 Linux要用 UIO 或者 DMA 引擎框架裸机就直接操作寄存器。4.2 数据解析从 32 位字还原 24 位有符号数DMA 搬到 DDR 的数据是 32 位对齐的每个字低 24 位是 ADC 数据高 8 位可能是通道号或者填充。解析时要先判断帧头然后按通道顺序提取。AD7768 输出的是二进制补码24 位有符号数范围是 -8388608 到 8388607。解析成整数后再根据参考电压换算成电压值。参考电压典型是 4.096 V满量程是 ±4.096 V所以电压 码值 / 8388608 * 4.096。// 解析一帧数据 #define FRAME_HEADER 0xAA55AA55 void parse_frame(u32 *buf, int len, float *voltage) { int i; if (buf[0] ! FRAME_HEADER) return; // 帧头不对丢弃 for (i 0; i 8; i) { u32 raw buf[i1] 0x00FFFFFF; // 取低 24 位 if (raw 0x00800000) raw | 0xFF000000; // 符号扩展 int32_t code (int32_t)raw; voltage[i] (float)code / 8388608.0f * 4.096f; } }这段代码先检查帧头然后循环 8 个通道取低 24 位做符号扩展再换算成电压。注意符号扩展的判断条件是 bit23 是否为 1如果是就补高 8 位 0xFF。参数说明FRAME_HEADER 是自定义的帧头可以改成别的值只要 PL 端和 PS 端一致。voltage 数组存 8 个通道的电压值单位伏特。4.3 数据落盘与实时性权衡如果要做长时间采集数据落盘是必须的。PS 端可以用 fwrite 把电压值写成二进制文件或者 CSV。二进制文件更省空间CSV 方便用 Excel 看。写文件的速度要跟上采集速度6 MB/s 的原始数据换算成 float 是 8 MB/sSD 卡或者 eMMC 都能写。但如果用文件系统要注意缓冲最好用 O_DIRECT 或者自己管理块设备。实时性要求高的话可以在 DDR 里开一个环形缓冲区采集线程写处理线程读中间用信号量同步。环形缓冲区的大小按最长处理延迟算比如处理一帧要 1 ms采集一帧 31.25 us缓冲区至少 32 帧。5. 避坑与排查AD7768 采集常见的五个翻车点5.1 现象数据全是 0 或者全是 0xFFFFFF原因AD7768 的复位引脚没有正确释放或者 MCLK 没有输出。AD7768 上电后需要至少 1 ms 的低电平复位然后等 MCLK 稳定后才开始转换。如果复位引脚一直拉低或者 MCLK 没接DOUT 会保持固定电平。解决用示波器量 RESET 引脚确认上电后有低到高的跳变量 MCLK 引脚确认有 16.384 MHz 的正弦或方波。如果 MCLK 幅度不够检查 PL 的 IO 驱动能力或者加一个时钟缓冲器。5.2 现象数据偶尔错位通道顺序乱原因DRDY 和 DCLK 的相位关系不稳定或者状态机的位计数没有在 DRDY 下降沿正确复位。AD7768 的 DRDY 下降沿和第一个 DCLK 上升沿之间有一个建立时间如果 PL 端用 DRDY 直接复位计数器可能采到半个位。解决用 DCLK 同步 DRDY打两拍后再检测下降沿确保复位发生在 DCLK 的稳定区域。另外检查 DCLK 的 PCB 走线尽量等长避免反射。5.3 现象DMA 传输报错中断不触发原因AXI-Stream 的 tvalid 和 tready 握手失败或者 DMA 的传输长度不是 4 字节对齐。AXI DMA 要求传输长度是 4 的整数倍如果帧长度不是会报错。解决把帧长度补齐到 4 的倍数比如 28 字节补到 32 字节。检查 AXI-Stream 的 tvalid 是否在 tready 为高时保持如果 tvalid 提前拉低DMA 会认为传输结束。可以在 PL 端加一个 FIFO 缓冲保证 tvalid 连续。5.4 现象采集到的数据噪声大SNR 差原因MCLK 抖动大或者电源纹波大。AD7768 的 SNR 对 MCLK 抖动很敏感如果 MCLK 来自 PS 的 PLL抖动可能比 PL MMCM 大。解决改用 PL MMCM 产生 MCLK或者在 MCLK 输出端加一个 LC 滤波器。电源方面AD7768 的 AVDD 和 DVDD 要分开供电模拟地和数字地单点连接。如果 PCB 已经做好可以在电源引脚并 10 uF 和 0.1 uF 电容。5.5 现象长时间采集后数据丢失原因DMA 缓冲区溢出或者 PS 端处理速度跟不上。如果 DMA 搬完一帧后没有及时重新提交PL 端的 FIFO 会满导致丢帧。解决在中断里尽快重新提交 DMA或者用 scatter-gather 模式提前准备好多个缓冲区。PS 端的处理线程优先级要提高避免被其他任务抢占。如果跑 Linux可以用 RT 补丁或者把线程绑到特定 CPU 核。6. 验证与进阶用已知信号源校准整条链路6.1 用直流校准验证码值线性度整条链路搭好后先别急着接传感器用一个高精度直流源给 AD7768 的输入加几个已知电压比如 0 V、1 V、2 V、3 V看采集到的码值是否线性。AD7768 的输入范围是 ±VREF如果 VREF 是 4.096 V加 1 V 应该得到 1/4.096 * 8388608 ≈ 2048000 码。实际码值和理论值的偏差应该在几个 LSB 以内。如果偏差大检查参考电压是否准确或者输入端的 RC 滤波器是否引入了衰减。# 用 Python 验证线性度 import numpy as np codes np.array([0, 2048000, 4096000, 6144000]) # 理论码值 measured np.array([100, 2048100, 4095900, 6144100]) # 实测码值 error measured - codes print(误差(LSB):, error) # 如果误差超过 10 LSB检查参考电压和输入滤波这段代码算理论码值和实测码值的差单位是 LSB。1 LSB 对应 VREF/2^23 ≈ 0.488 uV。如果误差在 10 LSB 以内说明链路基本正常。如果误差大先查参考电压再查输入端的运放是否饱和。6.2 用 FFT 看噪声底和谐波接一个纯净的正弦波信号源比如 1 kHz幅度 2 Vpp采集 8192 个点做 FFT看噪声底和谐波分量。AD7768 在 256 kSPS 下的 SNR 典型值是 108 dB如果实测只有 90 dB说明 MCLK 抖动或者电源噪声大了。FFT 的窗函数用 Hann 或者 Blackman-Harris避免频谱泄漏。计算 SNR 时把信号频点的能量和其余频点的能量分开注意排除直流和前几个谐波。import numpy as np from scipy.fft import fft # 假设 data 是采集到的 8192 个电压值 data data - np.mean(data) # 去直流 window np.hanning(len(data)) spectrum np.abs(fft(data * window)) signal_bin np.argmax(spectrum[:len(spectrum)//2]) signal_power np.sum(spectrum[signal_bin-1:signal_bin2]**2) noise_power np.sum(spectrum[:len(spectrum)//2]**2) - signal_power snr 10 * np.log10(signal_power / noise_power) print(SNR: %.2f dB % snr)这段代码去直流后加 Hann 窗做 FFT找最大频点作为信号算信号功率和噪声功率的比值。注意信号频点要取附近几个 bin 的能量因为窗函数会让能量扩散。如果 SNR 不达标先查 MCLK 的相位噪声再查电源的纹波。6.3 一个具体技巧用 ILA 抓 PL 内部信号ZYNQ7010 的 PL 端可以例化 ILA集成逻辑分析仪抓 DRDY、DCLK、DOUT 和状态机的内部信号。ILA 的采样深度设 1024 或者 2048触发条件设成 DRDY 下降沿。抓到的波形可以导出 CSV用 Python 画出来看时序关系。这个技巧在调试初期特别有用能直接看到 DRDY 和 DCLK 的相位差以及状态机的位计数是否正确。我一般会在 ILA 里同时抓 fifo_wr_en 和 fifo_wr_data确认写入 FIFO 的数据和通道号对得上。ILA 的时钟用 DCLK采样率就是 DCLK 频率抓 1024 个点够看 4 帧数据。如果 DCLK 太高ILA 可能跑不到可以先用低速模式比如 MCLK 降到 1 MHzDCLK 降到 512 kHz调通后再升上去。这套链路我前后调了大概两周最耗时的不是写代码而是等 PCB 打样和查时序。血泪经验是AD7768 的 DRDY 和 DCLK 相位关系一定要用示波器或者 ILA 确认不要凭感觉写状态机。另外 DMA 的缓冲区一定要开够不然丢帧丢到怀疑人生。希望帮到你。本文还有配套的精品资源点击获取