简介:基于FPGA的多通道数据采集与UART传输完整工程包,定位于电子工程与嵌入式开发学习者,用于解决8通道16位模拟信号同步采集、AD转换及串口回传的实际设计问题。压缩包共112个文件,以Verilog源码(.v)、Quartus工程文件(.qpf/.qsf)、仿真波形(.vwf)、综合报告(.rpt)及FPGA配置文件(.sof)为主,辅以备份与说明文档,整体仅2.64MB,目录结构清晰。目前已有698人学习下载。内容覆盖AD_RX_module、AD_8C_16B、AD_Volt、Uart_tx_Module等多个核心模块,包含模块化Verilog代码、仿真测试平台与工程脚本,可帮助读者完整走通多通道采集、16位AD转换控制、BCD电压转换与UART异步发送链路,适合作为数字系统设计课程设计或FPGA入门实战的参考资料。 前阵子朋友找我评估一套工业设备振动监测的小板子,需求看起来并不算复杂:八个通道同步采样,单通道 200kSPS,16 位分辨率,数据打包发给上位机做频谱分析。但真把方案摊开算,用 MCU 做同步多通道采集就非常吃力了——要么外挂一堆 ADC 做轮询切换,要么牺牲采样率,通道一多相位就全乱了。最后我们定的是 FPGA 方案,这也是目前多通道数据采集系统里最主流、也最稳妥的路线。
这篇文章不打算讲空泛的概念,直接把一个完整的多通道采集系统设计过程拆开:从需求怎么倒推关键参数,到 Verilog 模块怎么组织,再到调试现场踩过的几个典型的坑,以及上板之前的仿真验证手段,一次说透。适合正在做数据采集项目、或者刚接触 FPGA 想找一个完整工程练手的朋友。
1. 从应用需求倒推:多通道采集系统到底要解决什么问题
1.1 先算清楚采样率、分辨率、通道数这笔账
很多方案翻车,不是代码写得不好,而是需求没算清楚就直接开写了。多通道采集系统的第一笔账,是总数据率:
总数据率 = 通道数 × 采样率 × 位宽
以我刚才说的振动监测项目为例:8 通道 × 200kSPS × 16bit = 25.6Mbps,约 3.2MB/s。这个数字意味着什么呢?UART 422 最高常用波特率 921600bps,理论有效负载也才 92KB/s 左右,差了三十多倍。所以系统架构的第一步不是画框图,而是先确认:这个数据率你的上行接口扛不扛得住。
常见的上行接口和数据率匹配关系,我整理了一张表:
| 上行接口 | 典型有效带宽 | 适用场景 | 实现难度 |
|---|---|---|---|
| UART(115200/921600) | 约 11.5KB/s / 92KB/s | 低速传感器采集、调试 | 低 |
| SPI / I2C | 1Mbps ~ 12Mbps | 板级短距离传输 | 低 |
| USB 2.0 High-Speed | 约 40MB/s | 便携式采集设备 | 中 |
| 千兆以太网 UDP | 约 100MB/s 以上 | 工业在线监测、多节点同步 | 中高 |
| PCIe / DDR 缓存 | GB/s 量级 | 高速示波器、软件无线电 | 高 |
这笔账算完,很多“优化”方向就清楚了。如果必须用 UART,就得把总数据率压到 100KB/s 以内,要么降采样率,要么减少通道数,要么在 FPGA 里做抽取(decimation)。如果带宽仍然不够,就应该考虑换 USB 或以太网方案,而不是继续压榨 UART。
第二个关键选择是同步采集 vs 轮询采集。同步采集的意思是所有通道在同一个时刻完成采样保持,ADC 芯片内部或者多颗 ADC 之间用统一的触发信号保证相位一致性。轮询则是单颗 ADC 配合模拟开关逐个切换通道,硬件简单但通道之间存在时间差。
如果应用是做振动分析、声源定位、电能质量检测,通道之间的相位一致性是刚需,必须上同步方案。如果只是慢速的环境温度监测,轮询就够了。这个区别会在 ADC 选型和 FPGA 内部逻辑上有完全不同的走向。
1.2 整体架构:FPGA 里到底要装哪些模块
多通道数据采集系统的典型链路是这样的:
模拟前端(信号调理、抗混叠滤波)→ ADC 芯片 → FPGA → 上行接口 → 上位机
FPGA 作为数据中枢,内部模块通常可以分成这几个部分:
- AD 接口时序控制:产生 CONVST、CS、RD 等控制信号,读取转换结果;
- 数据对齐与拼接单元:把多通道并行读回的数据按通道顺序整理成统一格式;
- 异步 FIFO 缓冲:跨越采样时钟域和发送时钟域,解决时钟频率不一致的问题;
- 协议组帧模块:加上帧头、通道号、时间戳、校验码,方便上位机解析;
- 上行接口控制器:UART / USB / 以太网物理层控制器的实现;
- 可选的数据预处理模块:FIR 滤波、滑动窗口滤波、FFT、特征值提取等。
为什么要用 FPGA 而不是高端 MCU 或者 DSP?核心原因是并行性和确定性的时序。多通道同步采集中,每个通道的采样时刻必须一致,FPGA 可以用一个全局触发信号同时控制所有 ADC 进行采样保持,这个时序是硬件级确定的。MCU 即使是高速内核,通道多了之后中断响应、数据搬运也会引入不确定延迟,时间长了通道间相位误差就会跑偏。
另外一个客观优势是扩展性。FPGA 引脚多,管脚电平标准可配置,同一颗芯片可以从 4 通道扩展到 32 通道甚至更多,只需要换 ADC 板卡和改 FPGA 内部逻辑,不用改底层架构。这就是“多通道数据采集系统”标题背后的真正核心:不是简单读几个 ADC,而是把接口、缓冲、协议、扩展这些要素全部在 FPGA 内部组织好。
2. Verilog 核心模块逐个落地:从 AD 接口到数据组帧
2.1 AD 接口时序控制:状态机驱动是最稳的做法
ADC 芯片选型决定接口时序复杂度。以最常用的 8 通道同步采样芯片 AD7606 为例,它的并行接口包含这些关键信号:
- CONVST:转换启动信号,上升沿触发所有通道同步采样;
- BUSY:转换期间为高,下降沿表示本次转换完成;
- CS:片选信号;
- RD:读信号,下降沿后数据总线输出当前通道数据;
- DB[15:0]:16 位并行数据总线。
接口逻辑的核心是一个状态机,流程是:空闲状态拉低 CONVST 启动转换,之后等待 BUSY 下降沿,而不是傻等固定时钟周期。为什么一定要等 BUSY?因为 ADC 的转换时间会随温度、供电电压有小幅变化,如果你用固定延时去读,在最坏情况下可能转换还没完成就读了,数据就是乱的。等 BUSY 下降沿是天然的握手方式,自适应 ADC 的实际转换时间,这是做 AD 接口最基础也最重要的一个习惯。
这里给出一个简化版的 AD7606 读取状态机核心代码框架:
typedef enum logic [3:0] { IDLE, START_CONV, // 拉低 CONVST WAIT_BUSY, // 等 BUSY 下降沿 READ_CH0, READ_CH1, READ_CH2, READ_CH3, READ_CH4, READ_CH5, READ_CH6, READ_CH7, CONV_DONE } adc_state_t; adc_state_t state, next_state; always_ff @(posedge clk_50m or negedge rst_n) begin if (!rst_n) state <= IDLE; else state <= next_state; end always_comb begin next_state = state; case (state) IDLE: next_state = START_CONV; START_CONV: next_state = WAIT_BUSY; WAIT_BUSY: if (!busy) next_state = READ_CH0; READ_CH0: next_state = READ_CH1; READ_CH1: next_state = READ_CH2; READ_CH2: next_state = READ_CH3; READ_CH3: next_state = READ_CH4; READ_CH4: next_state = READ_CH5; READ_CH5: next_state = READ_CH6; READ_CH6: next_state = READ_CH7; READ_CH7: next_state = CONV_DONE; CONV_DONE: next_state = IDLE; default: next_state = IDLE; endcase end实际工程里,读取每个通道时还要给 RD 信号一个合适的脉冲宽度,不能小于 ADC 数据手册要求的低电平脉宽,否则数据还没稳定就被采进寄存器了。这个细节就是后面调试踩坑的重灾区,第三节详细说。
2.2 多通道数据对齐与拼接:顺序必须锁死在硬件上
同步采样的 ADC 虽然所有通道同时完成采样保持,但读数据的时候是串行读出的。AD7606 在读模式下,每次 RD 下降沿之后,数据总线上的值依次是通道 1、通道 2……通道 8,靠 CS/RD 的脉冲个数来选择通道。
所以设计里必须在状态机里用一个通道计数器,从 0 数到 7,每读一次就把 DB[15:0] 存入对应的通道寄存器。这个顺序锁定是在硬件层面完成的,不依赖任何外部干预。如果计数器计数范围和 ADC 的通道寻址次序不一致,比如 ADC 从 0 开始输出、你的计数器从 1 开始,最后所有数据会整体错位——经典的低级错误,但真的会出现在有经验的工程师身上。
拼接的时候还要注意另一个问题:字节序。16 位数据要拆成两个 8 位字节发送或者存储,高字节在前还是低字节在前,必须和上位机的解析代码保持一致。我曾经有过一次很无语的调试经历:上位机显示的波形整体是对的,但幅值完全不对,后来发现是 FPGA 发的是高字节在前,上位机按低字节在前解析,每个采样点被做了字节交换。这类问题肉眼很难发现,最好在组帧之前就用已知测试数据去验证拼接逻辑。
2.3 异步 FIFO:跨时钟域缓冲是系统的“稳压罐”
多通道采集系统里天然存在两个或以上的时钟域:
- ADC 采样时钟域:比如 50MHz,产生 CONVST、读取 ADC 数据;
- 上行发送时钟域:比如 UART 的波特率时钟,或者 USB 控制器的 60MHz 接口时钟。
这两个时钟之间没有相位关系,频率也可能不同。如果把采样时钟域的数据直接给发送端使用,那就是跨时钟域处理,极大概率出现亚稳态,具体问题后面会展开。
最稳妥的解决方案是用异步 FIFO做缓冲。采样端往 FIFO 里写,发送端从 FIFO 里读,读写时钟可以完全不同。异步 FIFO 内部用格雷码同步读/写指针,能最大程度降低亚稳态风险。
FPGA 厂商都提供现成的 FIFO IP 核,比如 Xilinx 的 FIFO Generator,Altera 的 FIFO Intel FPGA IP。我的建议是:工程上优先用 IP 核,不要自己写异步 FIFO。原因很简单:IP 核经过充分验证,支持各种位宽深度配置,还带 almost_full/almost_empty 等信号,调试方便。自己写一个能在仿真里跑的异步 FIFO 不难,但要做到批量生产稳定不出问题,需要非常苛刻的时序设计,性价比太低。
FIFO 的深度怎么确定?核心公式是:
FIFO 深度 ≥ 最大突发写入数据量 - 突发期间发送端能带走的数据量
具体算例我放到第三节,这里先记住一个原则:深度要覆盖你最恶劣的突发场景,再额外留 2 倍余量,别抠门,FIFO 深度不够导致的数据溢出是整个系统里最难排查的问题之一。
2.4 组帧与上行传输:协议帧格式的实用设计
从 FIFO 读出的数据是“裸数据流”,要可靠地传给上位机,必须在 FPGA 里做组帧。一个实用的、不啰嗦的帧格式设计:
| 字段 | 长度 | 说明 |
|---|---|---|
| 帧头 | 2 字节 | 固定值 0xAA 0x55,用于上位机同步 |
| 帧长 | 2 字节 | 整个帧的字节数,便于解析 |
| 通道号 | 1 字节 | 表示本帧第一笔数据的通道 |
| 时间戳 | 4 字节 | 计数器值,表示采样时刻 |
| 数据区 | N × 2 字节 | 通道数据,按通道顺序排列 |
| CRC16 | 2 字节 | 整帧校验,发现传输错误 |
帧头用两个字节而不是一个,是为了降低误同步概率。时间戳对多通道系统非常重要,它能让你在分析时精确定位每个采样点的绝对时刻,对振动分析和故障诊断是刚需。
上行传输这块,如果只是低速调试,UART 模块就够了。UART TX 的核心逻辑就是一个移位寄存器加一个波特率分频计数器,注意在开始时把 TX 线拉高(空闲态),发送完成之后再拉高。这里有一点容易被忽略:UART 的波特率时钟和 ADC 采样时钟没有任何关系,数据通过 FIFO 隔离,不需要任何同步操作。
3. 多通道采集调试:那些真正让人头疼的坑
3.1 通道数据错位:一次“灵异”故障的排查链路
做多通道采集,最典型的疑难杂症就是通道数据错位:示波器看数据总线波形明明完全正确,但上位机显示的通道顺序或者幅值就是不对。
有一次调试四通道采集,现象是:给通道 1 加一个标准正弦波,上位机却在通道 3 的波形上看到了正弦波。排查链路是这样的:
第一步,先确认不是上位机解析问题。FPGA 里插一个测试模式,直接发送固定数据,比如通道 1 发 0x1111、通道 2 发 0x2222,上位机收到的数据完全正确,说明组帧和解析没问题。
第二步,怀疑是 FPGA 的组帧顺序问题。用 ILA(集成逻辑分析仪)抓 FPGA 内部信号,看从 AD 读回来的数据顺序,发现读回的原始数据就错了:通道 1 的状态机位置读到的数据实际上是通道 3 的输出。
第三步,定位到 ADC 读时序。对照芯片数据手册,发现 RD 信号的低电平脉宽比手册要求的最小值短了约 10ns。因为 RD 脉宽不够,ADC 内部的数据切换还没完全稳定,下一次 RD 下降沿就把上一通道的残留数据采进去了,导致相邻通道的数据混叠。
这个故障的教训是:不要凭经验给 RD 信号一个“看起来差不多”的脉宽。不同 ADC 的手册要求不一样,有的要求最小 18ns,有的要求更大一些。状态机里的时钟周期如果刚好在临界值,温度一变就可能偶发错位。正确的做法是在满足采样率的前提下,把 RD 脉宽做得比手册要求宽 2~3 倍,用额外的时钟周期来换稳定性。
3.2 亚稳态:偶发错误往往藏在这里
多通道采集系统里最危险、最难复现的故障,就是亚稳态引起的偶发数据错误。它的典型特征是:系统大部分时间正常工作,但每隔几分钟或者几小时出现一次数据跳变,重启后恢复,极难稳定复现。
亚稳态的产生原因是跨时钟域的信号不满足触发器的建立保持时间。举个实际例子:ADC 的 BUSY 信号由 ADC 内部时钟产生,进 FPGA 后你用采样时钟去采它。采样时钟的上升沿可能正好落在 BUSY 信号变化的瞬间,触发器就会进入一个既不是 0 也不是 1 的中间状态,持续一小段时间后才稳定到某个值,这个“不确定值”如果被直接参与逻辑判断,就会导致后续错误。
应对办法是标准的两级触发器同步:
logic busy_sync1, busy_sync2; always_ff @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin busy_sync1 <= 1'b1; busy_sync2 <= 1'b1; end else begin busy_sync1 <= busy; busy_sync2 <= busy_sync1; end end然后检测 busy_sync2 的下降沿来表示“转换完成”。两级同步虽然不保证杜绝亚稳态,但能把亚稳态发生的概率降到极低,是工程上公认的可行手段。对于异步 FIFO 内部的跨时钟域指针同步,厂商 IP 核已经用格雷码加同步器处理好了,所以我在前面强调用 IP 核,实际上就是在帮你绕开一个很大的坑。
3.3 FIFO 深度量化:别凭感觉选深度
再回来说 FIFO 深度,这里给几个具体算例,照抄即可。
场景一:8 通道 × 1kSPS × 16bit,用 921600 UART 传输。每秒产生 16KB 数据,而 UART 有效带宽约 92KB/s,带宽是够的。因为采样率很低、数据平缓,FIFO 深度 512 个字(16bit)就非常充裕了,基本只起到缓冲解耦的作用。
场景二:8 通道 × 200kSPS × 16bit,仍然用 UART,每秒产生 3.2MB,带宽完全不够,FIFO 再深也没用。这个场景要么压缩采样率,要么换 USB/以太网。我之前见过一个项目,工程师把所有采样数据都塞 FIFO,深度配到 256K,结果 FIFO 还在满溢,因为他绕过了带宽约束这个根本问题。FIFO 解决的是突发吸收问题,不能解决平均带宽不足问题。
场景三:8 通道 × 200kSPS,上行走千兆以太网 UDP。以太网虽然带宽够,但发包是突发的,可能每隔几毫秒集中发一批,所以 FIFO 要能吸收这两个发包间隔之间持续积累的数据。如果发包周期是 1ms,那 FIFO 深度至少要 1ms × 3.2MB/s = 3200 字,留 2 倍余量用 8192 字比较稳妥。
经验公式是:FIFO 深度 ≥ 最大突发数据量 × 2。Xilinx FIFO IP 核配置里直接填这个值就行。
4. 仿真与验证:把问题拦在上板之前
4.1 Testbench 要模拟真实 ADC 行为
很多朋友写 testbench 就是给 ADC 数据总线赋个常量,然后看波形。这种验证能发现的问题非常有限。正确做法是写一个行为级 ADC 模型,它在 CONVST 拉低后,模拟 ADC 内部的转换延迟,把 BUSY 拉高一拍再拉低,然后每次 RD 下降沿之后,把预设的第 n 通道数据放到 DB 总线上。
核心结构大致是:
// 行为级 AD7606 模型片段 always @(posedge convst) begin #100 busy = 1'b1; // 模拟转换时间 #2000 busy = 1'b0; // 转换完成 end always @(negedge rd) begin if (!cs) begin case (channel_cnt) 4'd0: dbus = test_data_ch0; 4'd1: dbus = test_data_ch1; // ... endcase channel_cnt = channel_cnt + 1; end end有了这个模型,整个 FPGA 内部逻辑可以在完全没有硬件的情况下被完整验证:从 CONVST 触发、BUSY 等待、通道读取、FIFO 写入,到组帧发送,整条链路都能在仿真波形里看到。这也是 FPGA 开发区别于 MCU 开发的一个巨大优势——硬件逻辑可以在上板前被充分验证。
4.2 数据正确性自动检查与后处理
光看波形效率太低。我的习惯是在 testbench 里写自动检查 task:
task check_data; input [3:0] ch; input [15:0] expected; begin if (received[ch] !== expected) $error("通道 %0d 数据错误:期望 %h,实际 %h", ch, expected, received[ch]); end endtask另外,结合 Python 做波形后分析也是非常高效的手段。FPGA 仿真时用$fwrite把发送端输出的数据从 hex 转为十进制存成文件,然后用 Python 读入,做 FFT 分析,看频谱能量是否集中在预设的信号频率上。如果频谱出现明显杂散或者谐波,说明采样时序或数据拼接可能在特定码型下出问题。我在实际项目里用这个手段抓出过一次边缘通道在满量程附近的数据异常,靠肉眼盯波形根本发现不了。
仿真还有一个小技巧:在 testbench 里故意让 FIFO 写入速率高于读取速率,持续一段时间,检查 FIFO 满信号和上层的丢弃/节流逻辑是否按预期工作。这个“压力测试”能提前暴露 FIFO 深度不足和背压逻辑缺失的问题。
5. 下一步:从“采回来”到“在 FPGA 里直接算”
5.1 片上预处理:降低上行压力的关键
多通道采集系统做到后期,你会遇到一个矛盾:通道越来越多,采样率越来越高,上位机的处理压力越来越大。此时只做“采集+传输”是不够的,更好的做法是在 FPGA 里完成数据预处理,把有用的特征提取出来再上传。
比如振动监测系统,FPGA 里做完滑动窗口滤波、RMS 计算、FFT 峰值提取之后,上行数据量可以从每秒几 MB 压缩到每秒几百字节,普通 UART 都够用了。这已经是多通道采集系统设计的主流方向,从“采集系统”进化成“采集+边缘计算系统”。热搜词里出现的“卡尔曼滤波 fpga”“滑动窗口滤波verilog”“fpga图像处理”都指向这个方向。
5.2 高速存储与复杂接口扩展
如果应用继续提速,比如多通道数据并行灌入,CPU 来不及取走,就需要在 FPGA 里挂 DDR3/DDR4 做缓存,这个方向对应“ddr3读写控制实现verilog”这类需求。再往上走就是 PCIe、FMC 高速 ADC 采集卡的架构,核心概念其实和本文讲的多通道数据链路完全一致,只是接口从并行 GPIO 变成了 LVDS 串行、JESD204B,缓冲从 Block RAM 变成了 DDR,传输从 UART 变成了 PCIe DMA——但“接口时序控制、数据对齐、跨时钟域缓冲、组帧传输”这四板斧,一个都没变。
我个人做了几个采集项目之后的体会是:多通道采集系统最大的门槛不在某个单点技术,而在整条链路的时序一致性。采样触发的同步性、读取时序余量、FIFO 深度预算、跨时钟域处理,每个环节单看起来都不难,但串起来之后,出问题的地方往往是接口之间的衔接处。所以设计时一定要先在纸上把时钟域和数据流画清楚,再动手写 Verilog,这样后续的调试和扩展会轻松很多。
本文还有配套的精品资源,点击获取