FPGA+STM32高速多通道数据采集系统架构设计与FMC通信实战
简介基于FPGA与STM32的数据采集系统设计PDF是一份完整的本科毕业设计论文面向电子、自动化、嵌入式方向的毕业生和嵌入式初学者可作为虚拟仪器与数据采集课程设计、毕设选题或入门参考。内容从虚拟仪器背景与发展现状展开依次覆盖前级信号调理电路、80MHz AD转换、FPGA端Verilog设计的FIFO缓冲与时钟分频、STM32端数据处理与传输以及上位机数据管理软件的功能划分同时对系统总体方案、芯片选型、电源模块和主控模块设计进行了论述并可据此理解FPGA与STM32的协作流程与调试思路。全文共42页资源包仅含1个PDF文档大小约4.24MB便于直接阅读和资料归档。目前已有38人浏览学习适合需要快速建立数据采集系统整体框架、了解FPGA与STM32协作方式、完成课程设计或查找毕业设计参考资料的读者。 去年做一个多通道数据采集项目时我一开始的想法很简单信号调理完进入ADCADC出来接STM32定时器触发一轮轮读就行。结果用示波器一测直接被打脸——STM32内置ADC采样率到不了高速场景外挂并行ADC时GPIO吞吐又顶不住多通道同步更是连相位都对不齐。折腾了两周最后老老实实改成FPGASTM32的架构才算把整套系统跑稳。这篇就围绕这个方案把架构设计、FMC通信时序、跨时钟域处理、以及实测中踩过的坑完整拆一遍给正在做数据采集系统设计或者搞毕设的朋友一个可以直接参考的思路。先说清楚这套方案的核心不是FPGA替代STM32也不是STM32带着FPGA跑而是两者各干各擅长的事FPGA负责跟时间较劲的采样控制和数据缓冲STM32负责和用户打交道的人机交互、协议解析和数据上传。明白了这个分工后面的设计就不会跑偏。1. 为什么这套系统非要FPGASTM32不可1.1 单靠STM32做高速多通道采集的瓶颈很多人刚开始的想法都是STM32外设那么丰富内置ADC不够就外挂一片高速ADC但真正动起来会发现三个绕不开的问题。第一是吞吐量上限。以常见的12位并行ADC为例如果做到50MSPS采样率裸数据率就是50M×12bit600Mbps换算下来大约是75MB/s。STM32即使跑在240MHz用GPIO模拟并行总线来读这种速率的数据几乎不可能——GPIO翻转频率、中断响应抖动、循环读数据时的指令开销全都会成为瓶颈。就算用外部存储总线接口连续搬运这种带宽的数据也会把CPU耗尽根本没有余量做协议处理和上传。第二是多通道同步问题。工业采集和生物电信号采集这类场景通道之间的相位一致性是硬指标。STM32内置ADC虽然有多通道但本质上是一个ADC加模拟多路开关轮流采样通道间天然存在时间差。如果外挂多片ADC让STM32同时去触发多片ADC的转换并读取结果时序很难做到严格同步片间偏差甚至能达到几百纳秒到几微秒对很多应用来说直接不可用。第三是实时预处理能力。采集系统不是把原始数据丢给上位机就完事了往往需要滤波、抽取、触发判断。这些算法如果全部压在STM32上跑采样率稍高一点CPU就跑满了更不用说同时还要维护USB或以太网协议栈。1.2 FPGA在这个系统中的真实分工FPGA解决的核心问题是确定性时序。以一组8通道、12位、1MSPS的采集需求为例FPGA内部产生一个精确的采样时钟同时送到8片ADC的采样时钟引脚理论上各通道的采样时刻偏差可以控制在亚纳秒级别这是MCU很难做到的。FPGA还承担数据缓冲和简单预处理。ADC转换结果并行进来之后FPGA先把数据对齐、拼成16位或32位写入内部FIFO。STM32需要数据时通过FMC接口像读外部SRAM一样把FIFO里的数据批量读走。这个过程中如果FPGA内部还挂了FIR滤波器或者抽取模块就能顺便把10MSPS的原始数据降采样到1MSPS再交给STM32通信带宽压力瞬间小一个数量级。1.3 组合架构带来的隐性收益除了性能这种架构还有一个容易被忽略的好处改功能不用改电路板。采样率、通道数、滤波系数、触发逻辑都可以通过修改FPGA逻辑来调整STM32侧的固件甚至不用动。对于产品迭代或者毕业设计这种经常改需求的场景这个灵活性比什么都值钱。另外调试便利性也高很多。FPGA内部有现成的逻辑分析仪IPVivado里的ILA或者Quartus里的SignalTap采样时钟、FIFO状态、接口时序都能实时拉出来看出了问题定位比盲猜MCU寄存器快得多。2. 先定方案再写代码整体架构与数据链路2.1 从模拟信号到上位机的完整数据流我在项目中确定的需求是8通道模拟输入、最高采样率1MSPS、12位分辨率支持连续采集和触发采集两种模式数据通过USB上传到PC做波形显示和分析。整个数据链路是这样的模拟信号经过调理电路放大、偏置、滤波进入ADCADC由FPGA统一提供采样时钟。转换结果以并行12位数据线发给FPGAFPGA内部完成通道标识、数据对齐、打包成16位字后写入异步FIFO。STM32检测到FIFO半满或者收到FPGA的中断信号就通过FMC接口以DMA方式批量读取数据进入STM32的RAM缓冲区后再通过USB虚拟串口或以太网发送到上位机。这套链路里模拟前端的信号调理和数字端的时序控制是分开设计的。前端负责保证信号在这些采样率和幅度范围内不失真数字端负责保证数据不丢、不错、不乱序。两边各司其职出了问题也容易隔离。2.2 FPGA内部的模块划分配置FPGA内部我划分成四个模块每一个都不复杂但组合起来要非常小心。采样时钟管理模块负责通过PLL/MMCM生成ADC采样时钟和FPGA内部工作时钟同时产生分频后的触发时钟。ADC接口模块负责根据ADC芯片手册的时序要求在采样时钟的适当边沿锁存并行数据这一步最关键的是建立保持时间要满足建议直接用IOB内的寄存器打拍。数据缓冲模块是核心用异步FIFO实现写入侧是采样时钟域读出侧是FMC总线时钟域两侧完全解耦。控制与通信模块则是FPGA的大门接受STM32发过来的配置命令回传状态信息并把FIFO里的数据送到数据总线上。模块划分的原则是每个模块只有一个时钟域跨时钟域只通过异步FIFO或者同步器。硬要在两个时钟域之间直接传数据后续调试一定会因为亚稳态问题付出代价。2.3 STM32侧的资源规划和接口职责STM32这边反而不需要太多性能开销关键是资源分配要清楚。我用的是一款带FMC接口的STM32H7系列芯片FMC负责和FPGA通信DMA负责数据搬运一个定时器负责周期性检查FIFO状态USB或者以太网负责和上位机通信。FMC通道分配好后整个FPGA在STM32眼里就是一段外部SRAM。我在FPGA内部做了寄存器映射STM32往某个地址写控制字FPGA就启动或者停止采集STM32从某个地址读数据FPGA就把FIFO里的一个16位采样值送到总线上。这样STM32端的驱动代码非常简洁——就是普通的内存读写操作不需要关心FPGA内部的实现细节。3. FMC通信是核心时序、寄存器与可运行骨架3.1 为什么选择FMC而不是SPI或普通IOSTM32和FPGA之间的通信方式有几种可选SPI速度不够、GPIO模拟并行时序不稳定、自定义高速串行口开发复杂。FMC并行接口几乎是为这种场景量身定做的——硬件自己产生片选信号、读写信号、地址信号还能配置建立时间和保持时间STM32端只需要像访问内部RAM一样访问FPGA即可。以16位数据总线、地址总线按需扩展4到8位为例单次16位读写的总线操作在H7系列上可以做到几十纳秒量级。对于1MSPS、8通道的采集应用STM32通过DMA批量读FIFO数据率完全可以覆盖CPU占用率还很低。3.2 FMC时序参数的调试心得FMC的时序参数不是随便配的需要结合FPGA的实际响应时间和PCB走线长度来定。我常用的调试方法是从慢开始先把地址建立时间和数据建立时间都设得很大确认通信功能正常然后逐步减小参数每减一次就做一次全地址读写测试和长时间FIFO读取测试直到出现错误再回调一两档。这里要特别提醒FPGA端宿主的读写响应不是立刻完成的。FMC在写周期里给出片选、写使能和数据FPGA需要一拍到两拍才能完成寄存器写入FMC在读周期里给出片选和读使能后FPGA必须保证在总线读取窗口内把数据送到数据线上否则STM32读到的就是高阻态表现为随机数或固定0xFFFF。解决思路通常是FPGA内把读地址和写地址各打一拍缓存确保数据总线只在真正读操作时被驱动。3.3 寄存器地址映射设计我的寄存器映射表如下这个设计类似PCI设备的BAR空间STM32驱动只要按这个表操作就行偏移地址方向宽度功能说明0x0000读16位版本号与设备ID固定返回0xAA010x0002读写16位控制寄存器bit0启动/停止bit1触发模式bit2复位FIFO0x0004读16位状态寄存器bit0 FIFO空bit1 FIFO满bit[15:8]当前通道数0x0010读16位数据FIFO端口每次读取弹出一个采样点地址线我用了4位所以偏移地址到0x0010。0x0000和0x0002之间留了一个字的空隙是为了避免版本寄存器和控制寄存器在将来调整时冲突。数据FIFO放在0x0010是为了和前面的状态寄存器隔开防止DMA连续读时误读到状态字导致数据流错乱。3.4 Verilog通信骨架与HAL配置FPGA端的FMC从设备逻辑用一个简化的寄存器读写状态机就能搞定。核心代码如下// FMC从设备接口骨架16位数据总线4位地址总线 module fmc_slave( input wire clk, input wire rst_n, input wire cs_n, // 片选低有效 input wire wr_n, // 写使能低有效 input wire rd_n, // 读使能低有效 input wire [3:0] addr, // 地址总线 inout wire [15:0] data, // 数据总线 output reg fifo_rd, input wire [15:0] fifo_data, input wire fifo_empty, output reg [15:0] ctrl_reg ); wire write_en !cs_n !wr_n; wire read_en !cs_n !rd_n; // 写操作 always (posedge clk or negedge rst_n) begin if (!rst_n) ctrl_reg 16h0000; else if (write_en addr 4h2) ctrl_reg data; end // 读操作地址0x0返回ID地址0x4返回状态地址0x10弹出FIFO数据 reg [15:0] read_data; always (posedge clk or negedge rst_n) begin if (!rst_n) read_data 16h0000; else if (read_en) begin case (addr) 4h0: read_data 16hAA01; 4h4: read_data {12h000, 1b0, fifo_empty, 2b00}; default: read_data 16hFFFF; endcase end end assign data read_en ? read_data : 16hZ; // 数据FIFO读取脉冲 always (posedge clk or negedge rst_n) begin if (!rst_n) fifo_rd 1b0; else fifo_rd read_en (addr 4h1); end endmodule这段代码里有个很容易犯的错FIFO读信号必须只有一拍不能在整个读周期里一直有效否则会连续弹出多个数据导致数据流错位。所以我用了打拍的方式生成fifo_rd确保STM32每读一次地址0x0010FIFO只弹出一个采样点。STM32端的HAL库配置相对简单核心是把FMC的BaseAddress设为FMC_BANK1数据宽度设16位然后按调试结果填时序结构体FMC_NORSRAM_TimingTypeDef Timing {0}; Timing.AddressSetupTime 9; // 地址建立时间 Timing.DataSetupTime 9; // 数据建立时间 Timing.BusTurnAroundDuration 0; // 总线转换时间先设0 Timing.AccessMode FMC_ACCESS_MODE_A; // 使用标准SRAM读写模式然后是常规的HAL_SRAM_Init和HAL_SRAM_Read/Write调用。真正读FIFO时我推荐直接用DMAHAL_SRAM_Read_DMA(h_sram, FPGA_BASE 0x10, (uint8_t *)buffer, data_len_words);DMA触发方式可以是定时器周期性触发也可以由FPGA的FIFO半满标志通过一个GPIO接到STM32的外部中断引脚来触发。实际项目中我用的是FIFO半满中断这样数据积累到一定量才搬一次DMA效率高也不会频繁打断CPU。4. 采集链路的关键模块设计ADC、跨时钟域与中断4.1 ADC采样时钟的产生与多通道同步ADC接口设计里最重要的就是采样时钟。我使用FPGA的PLL生成一个高质量的低抖动时钟同时送到所有ADC的采样时钟引脚PCB上做等长处理。这样8路通道的采样时刻可以做到几乎一致比STM32轮询采样可靠得多。ADC数据读取时刻要按芯片手册来。并行ADC通常在采样时钟的下一个沿或者几个沿之后输出转换结果FPGA要在数据输出有效窗口内锁存。保险做法是用一个使能信号把数据打拍进去并且在约束文件里给ADC数据引脚加上输入延迟约束否则综合工具可能把布线布得很随意高速时出现偶发错码。4.2 跨时钟域处理异步FIFO与亚稳态整个系统有三组时钟ADC采样时钟、FPGA内部逻辑时钟、STM32的FMC总线时钟。其中最危险的就是FMC总线时钟域和采样本时钟域之间的数据交换。我直接把采样数据处理成16位字后写入异步FIFO读写时钟分别是采样时钟和FMC时钟。异步FIFO的空满标志本身已经做了格雷码同步处理所以在FPGA侧不需要额外操心亚稳态。但是FMC的片选、读写使能信号进入FPGA时也要先经过两级同步器打拍再参与后续逻辑不然状态机偶尔会进入非法状态。这里分享一个真实经历我最初图省事把FMC地址信号直接接入组合逻辑判断没有打拍。结果系统运行十几分钟后出现一次控制寄存器误翻转导致采集意外停止。加了同步器之后问题彻底消失。这种偶发亚稳态问题最难查因为复现概率低很多项目交付后现场才出现一定不要省。4.3 中断与DMA配合避免丢数STM32读取FIFO的机制直接决定丢不丢数。我最后采用的是FIFO半满中断 DMA搬运 乒乓缓冲组合。FPGA端当FIFO内数据超过半满时拉高一个GPIO中断引脚。STM32外部中断服务函数里只做一件事启动一次DMA传输把FIFO中的数据搬到RAM缓冲区A。在DMA传输完成中断里检查缓冲区A的数据处理完后再把DMA目标切换到缓冲区B。两个缓冲区交替使用CPU在DMA搬运期间可以处理USB协议栈和上位机交互互不阻塞。乒乓缓冲的关键在于DMA传输启动后不要在中途操作同一个缓冲区否则数据会被覆写。我踩过这个坑之后总结出一条原则读FPGA FIFO的DMA只和缓冲区绑定任何处理逻辑都不要直接引用正在被DMA写入的缓冲区。5. 实测中踩过的坑与下一步优化5.1 数据错位/乱码的完整排查链路有一次我连续长时间采集后上位机波形开始出现通道间错位比如第2通道的数据跑到第1通道的曲线上。这层排查花了我一整天链路如下。我先怀疑FMC时序不稳定于是降低时序参数跑测试问题依旧。然后怀疑DMA传输错位但我按照半满中断触发的块大小对比了收到的数据量没有发现长度错误。最后老老实实在FPGA内部用ILA抓FMC读时序相邻几拍的数据发现问题是出在状态寄存器读取上——我为了图方便把通道信息放在状态寄存器的bit[15:8]而FIFO数据端口又是16位宽当STM32用DMA连续读取FIFO时偶尔会把状态寄存器的高字节残留带到数据流里。问题的根因是FIFO读脉冲时序fifo_rd信号在地址等于0x1时拉高但如果STM32连续执行两次读操作地址切换时前一个读周期还没真正结束我打拍产生的新FIFO读脉冲把第二笔数据也挤了出来。修复方法是把读数据总线的三态控制和FIFO弹出严格分开只有在读地址确实为0x1时才允许FIFO弹出并且弹出数据锁存到读数据寄存器后再输出。5.2 丢数据的三个根因与修复丢数据的问题通常有三个根因按出现频率排序。第一个是读取速度跟不上产生速度。处理办法是加FIFO深度。我最初用了1K深度的FIFO在突发采集时STM32稍一卡顿FIFO就满后来换成8K深度才稳定。计算依据很简单FIFO至少要能容纳STM32响应中断的最坏延迟时间内产生的数据量。假设采样率1MSPS、8通道、16位1毫秒就是16KB数据所以8K字深的FIFO也就是缓冲0.5毫秒左右。第二个是DMA配置错误。我在核对FMC DMA数据宽度时发现配置成了字节宽度而不是半字宽度导致明明读了4096次实际数据却只有一半。FMC总线宽度和数据宽度必须保持一致否则必须先做窄宽到宽宽的封装处理。第三个是上位机来不及收。USB虚拟串口的流控如果没做好缓冲区满了以后数据会被底层协议栈丢弃。我采用的办法是在帧协议里加入序列号上位机发现序列号不连续就主动要求重传最后一段保证数据链路完整。5.3 从能用到好用的优化方向这套架构跑通之后我梳理了几个后续可以扩展的方向给同样在做这类项目的朋友做个参考。如果还停留在FPGASTM32但需要更高采样率场景可以试试在FPGA内部做抽取滤波先用高速ADC全速采样FPGA内部的FIR滤波器把中间频段滤掉后只传低频分量通信带宽需求会大幅降低。这也是很多软件无线电设备的前端做法。如果想进一步降低STM32的负担可以把FMC数据总线扩展到32位同时读取两路16位采样值DMA带宽直接翻倍。代价是FPGA引脚占用更多PCB布线更复杂。如果对存储深度有要求比如需要连续采集几十秒甚至几分钟的波形用于事后分析FPGASTM32架构里FPGA一侧还可以再接一片DDR3或者DDR4做大容量缓冲STM32只负责从DDR里读取历史数据。这时候FMC的寄存器映射就要从原来的FIFO端口改为带地址索引的存储读取端口设计复杂度会上升一个台阶但系统能力也完全不一样。从我个人的实际使用体会来说FPGASTM32这个组合最打动我的地方不是某个单一指标特别强而是两个平台各自的工具链都已经非常成熟。FPGA端用Vivado或Quartus做在线逻辑分析STM32端用HAL库快速调通驱动两边分离开发、联调时又有清晰的接口协议这种开发节奏对于一个人要同时搞定逻辑、驱动、上位机的项目来说是能真正按时交付的关键。最后再分享一个小技巧整套系统的寄存器映射表和FMC时序参数一定要写成文档放在工程目录里不要只存在自己脑袋里。隔两个星期再回来看代码那份文档能帮你省下大量重新理解上下文的时间。本文还有配套的精品资源点击获取