1. 为什么要把STM32和FPGA塞进同一块板子
第一次听到"STM32+FPGA双核系统"这个说法,很多人脑子里冒出来的第一个问题就是:一个MCU不够用吗,为什么还要再挂一颗FPGA?我刚开始接触这套架构的时候也有同样的疑惑,直到真正做过几个项目之后才明白,这两颗芯片凑在一起,解决的从来不是"算力不够"这么简单的问题,而是控制流和数据流分离这件事。
STM32这类MCU的强项在于顺序逻辑、协议栈处理、人机交互、任务调度,它的中断响应、外设丰富度、开发生态都是FPGA比不了的。但一旦遇到需要并行处理的场景——比如同时采集8路ADC、实时做图像预处理、精确测量纳秒级时间间隔——MCU就开始力不从心了。你让STM32去轮询多路高速信号,CPU占用率直接飙到80%以上,稍微加点协议栈就卡顿。而FPGA天生就是干这个的,它内部是真正的并行硬件逻辑,几百个通道同时跑互不干扰,延迟还是确定的。
所以双核系统的本质分工是这样的:STM32负责"决策"和"通信",FPGA负责"采集"和"预处理"。STM32跑FreeRTOS或者裸机主循环,处理业务逻辑、跑LWIP协议栈做物联网网关、驱动屏幕做UI;FPGA在背后默默地把高速数据流整理好,通过FSMC、SPI或者并口把结果丢给STM32。两者之间用一条清晰的"数据管道"连接,各司其职。
这套架构特别适合几类人:做工业数据采集的工程师,需要多通道同步采样;做图像处理的开发者,摄像头原始数据量太大MCU扛不住;做精密测量的,比如用FPGA进位链TDC测时间间隔,精度要求到皮秒级;还有做电机控制的,多轴联动需要硬件级PWM和编码器解码。如果你正在做STM32项目但发现某个环节CPU总是跑满,或者时序抖动大得没法接受,那大概率就是该考虑加一颗FPGA了。
下面我会从选型、接口设计、数据流架构、调试方法几个维度,把这套双核系统拆开讲清楚。内容基于我实际做过的几个项目经验,包括踩过的坑和后来总结出来的稳定方案,希望能让准备上手的朋友少走弯路。
2. 双核选型:STM32和FPGA到底怎么配
2.1 STM32这一侧该选哪个系列
选STM32不是越贵越好,关键看你的数据吞吐需求和外设接口。我一般按三个档位来分:
| 档位 | 推荐系列 | 典型场景 | 关键外设 |
|---|---|---|---|
| 入门 | STM32F103/F407 | 低速采集、简单网关 | FSMC、SPI、UART |
| 中端 | STM32F429/F743 | 图像显示、多路ADC | LTDC、DCMI、FMC |
| 高端 | STM32H743/H750 | 高速数据流、复杂协议 | FMC、DCMI、以太网、USB HS |
如果FPGA要通过并口给STM32送数据,那STM32必须有FMC(Flexible Memory Controller,老型号叫FSMC)。F103只有FSMC,位宽16位,够用但速率一般;F429和H743的FMC能到32位,配合DMA可以做到很高的吞吐。我做过一个项目用H743的FMC接FPGA,实测连续传输能稳定在80MB/s以上,跑图像数据完全没问题。
如果数据量不大,用SPI也完全可以。SPI的问题是速率受限于时钟,STM32的SPI最高一般到几十MHz,而且每次传输有协议开销。但胜在接线简单,FPGA那边实现也容易,适合控制类指令的下发和少量状态回传。
还有一个容易被忽略的点:STM32的DMA通道分配。双核系统里FPGA送来的数据通常走DMA搬运到内存,如果你同时还要跑ADC、UART、SPI,DMA通道会打架。选型时一定要翻参考手册的DMA请求映射表,确认你要用的外设不会抢同一个通道。我踩过一次坑,F407上FMC和SPI2共用DMA1的某个流,结果两个都跑不起来,最后只能把SPI换到SPI1才解决。
2.2 FPGA选型:别一上来就冲高端
FPGA这边选择面更广,从国产的安路、紫光同创,到Xilinx的Spartan、Artix,再到Altera(现在叫Intel)的Cyclone系列,价格从几十到几千不等。新手最容易犯的错就是按逻辑资源选型,觉得LE越多越好,结果买回来发现引脚不够、时钟资源不够、或者封装根本焊不了。
我的选型顺序是这样的:
- 先数IO。你的应用需要多少路输入输出?LVDS接收要成对算,MIPI更是一大把差分对。IO数量不够,逻辑资源再多也白搭。
- 再看时钟资源。需要几个PLL?有没有全局时钟缓冲?做频率测量、TDC这类应用对时钟抖动很敏感,要选带专用时钟网络的型号。
- 然后看硬核资源。有没有DSP slice(做滤波、FFT用)、有没有块RAM(做缓存用)、有没有高速收发器(做MIPI、LVDS用)。
- 最后才看逻辑单元数量。
举个具体例子:如果你要做FPGA图像处理,从摄像头接收MIPI或者DVP数据,做简单的ISP算法(去噪、白平衡、边缘检测),那Artix-7或者Cyclone IV这个级别就够了,不需要上Kintex。但如果你要做复杂的卷积神经网络加速,那DSP slice数量就是瓶颈,得往上选。
国产FPGA这几年进步很快,安路的EG4系列、紫光同创的Logos系列在工业控制和图像处理场景已经能替代进口中低端型号。用纯Verilog写ISP算法优化,在国产FPGA上跑通完全可行,而且成本优势明显。选型时注意确认开发工具链是否成熟,有些国产FPGA的时序约束和IP核生态还在完善中,项目周期紧的话要留足调试时间。
2.3 两者之间的物理接口怎么定
接口方案直接决定了整个系统的数据吞吐上限和布线复杂度。常见的几种:
- FMC/FSMC并口:速率高,适合大数据量。缺点是占用STM32大量IO,PCB布线要等长处理。
- SPI:接线少,速率中等。适合控制指令和低速数据。
- UART:最简单,但速率最低,一般只用来传调试信息。
- 自定义并口+握手信号:灵活,但两边都要写时序逻辑,调试麻烦。
我个人的经验是:主数据通道用FMC,控制通道用SPI,调试通道用UART。三条通道各管各的,互不干扰。FMC负责图像、ADC采样这类大块数据;SPI负责下发配置参数、读取FPGA状态寄存器;UART接个串口模块,方便printf调试。
这里有个细节要注意:FMC的时序配置。STM32的FMC可以配置地址建立时间、数据保持时间等参数,这些参数必须和FPGA那边的逻辑时序匹配。我一般先在FPGA里把读写时序做成最宽松的(比如地址建立给足5个时钟周期),等STM32能稳定读写了,再逐步收紧时序提高速率。一上来就追求最高速,很容易出现偶发性读写错误,而且这种错误很难定位。
3. 数据流架构:谁负责什么,边界在哪里
3.1 把"实时"和"非实时"分开
双核系统设计的第一原则:硬实时任务放FPGA,软实时和非实时任务放STM32。
什么叫硬实时?就是必须在确定的时间内完成,晚一个时钟周期都不行。比如:
- 多路ADC同步采样,通道间偏差不能超过几个ns
- 编码器信号解码,丢一个脉冲位置就错了
- 高速数据流的缓存和打包,不能溢出
这些任务在STM32上做,你永远无法保证确定性。中断延迟、任务切换、Cache命中率都会影响时序。但在FPGA里,这些就是几行always块的事,时序由综合工具保证,只要约束写对了,延迟就是固定的。
反过来,像协议解析、网络通信、文件系统、UI刷新这些任务,放STM32上做效率高得多。你让FPGA去实现TCP/IP协议栈,那简直是自找麻烦。
所以数据流的典型路径是这样的:
传感器/ADC → FPGA(采样、缓存、预处理)→ FMC/SPI → STM32(协议封装、业务处理)→ 以太网/WiFi/4G → 上位机
FPGA在这一链路里扮演的是"数据搬运工+预处理员"的角色,它把原始数据整理成STM32容易处理的格式,减轻MCU的负担。
3.2 双口RAM:最常用的数据交换方式
FPGA内部用Block RAM做一个双口RAM,一边给FPGA逻辑写,一边给STM32读,这是最经典的做法。具体实现时要注意几个点:
地址映射要清晰。我一般把双口RAM分成几个区域:状态区、命令区、数据区。状态区放FPGA的运行状态(比如采样完成标志、FIFO空满标志);命令区放STM32下发的控制字;数据区才是真正的采样数据。这样STM32读的时候不用猜,直接按地址偏移访问就行。
握手信号不能省。光有双口RAM还不够,必须有中断或者标志位告诉对方"数据准备好了"。我通常用两种方式:一是FPGA写完后拉高一个GPIO,STM32用外部中断捕获;二是STM32轮询状态寄存器的某个bit。中断方式实时性好但增加CPU开销,轮询方式简单但浪费CPU时间。数据量大用中断,数据量小用轮询。
位宽要匹配。FPGA内部数据可能是12位ADC值,但STM32的FMC是16位或32位总线。这时候要么在FPGA里做位宽转换(把两个12位拼成一个24位或者补零成16位),要么在STM32里做移位处理。我建议在FPGA里处理好,让STM32拿到就是对齐好的数据,减少MCU的运算负担。
3.3 FIFO跨时钟域:最容易出问题的地方
FPGA内部经常有多个时钟域:ADC采样时钟、系统时钟、FMC接口时钟。数据从一个时钟域传到另一个时钟域,必须用FIFO或者双寄存器同步,否则会出现亚稳态。
我见过太多项目在这里翻车。现象是:大部分时候数据正常,偶尔出现一个错误值,而且很难复现。用逻辑分析仪抓半天也抓不到,因为亚稳态本身就是概率事件。
正确的做法是:任何跨时钟域的信号,都必须经过同步处理。单bit信号用两级触发器同步;多bit数据用异步FIFO;如果是总线,用握手协议。Xilinx和Intel的FPGA都有现成的异步FIFO IP核,直接调用就行,不要自己手写。
还有一点:FIFO的深度要算够。深度不够会导致溢出丢数据,深度太大浪费Block RAM。计算方法:写入速率乘以最坏情况下的读取延迟。比如ADC以10MHz写入,STM32最坏情况1ms才来读一次,那FIFO至少需要10M×1ms=10000个字的深度。实际设计时再留一倍余量。
4. 几个典型应用场景的实战拆解
4.1 多通道ADC采集:STM32切换通道 vs FPGA并行采样
用STM32做多通道ADC采集,常规做法是配置ADC规则组,用DMA搬运,然后靠定时器触发切换通道。但这里有个问题:STM32的ADC是分时复用的,多个通道轮流转换,通道之间有时间差。如果你要做相位敏感的应用(比如功率测量、阻抗分析),这个时间差会引入误差。
用FPGA就不一样了。你可以外挂多片高速ADC,每片ADC配一个独立的采样时钟,所有ADC同时启动转换,真正实现并行同步采样。FPGA内部为每路ADC开一个FIFO,采完一批数据后打包通过FMC送给STM32。STM32拿到的是已经对齐好的多通道数据,直接做算法处理就行。
具体实现时,ADC的接口通常是SPI或者并行LVDS。SPI接口的ADC速率一般不高(几MSPS),适合中低速场景;高速ADC(几十到几百MSPS)基本都是LVDS或者JESD204B接口,必须用FPGA接收。FPGA的LVDS接收要注意差分对的阻抗匹配和端接电阻,PCB上走线要等长,否则眼图会很难看。
4.2 频率测量:STM32定时器捕获 vs FPGA等精度测量
STM32测频率的常规方法是定时器输入捕获:信号上升沿触发捕获,记录计数值,两次捕获之差就是周期。这种方法在低频段很准,但频率越高误差越大,因为定时器时钟频率有限。比如72MHz的定时器时钟,测1MHz信号,每个周期只有72个计数,量化误差就有1.4%。
FPGA做频率测量可以用等精度测量法:用一个标准高频时钟(比如100MHz)去数被测信号的周期数,同时用被测信号去数标准时钟的周期数,两个计数相除就得到频率。这种方法在整个频率范围内精度都一致,而且FPGA可以同时开多路测量通道,互不干扰。
如果要做时间间隔测量(TDC),FPGA的进位链是个好东西。利用FPGA内部进位链的固有延迟(每个进位单元约几十ps),可以把时间间隔测量精度做到皮秒级。这在激光测距、粒子物理实验里很常用。实现时要注意温度漂移,进位链延迟会随温度变化,需要做校准。
4.3 图像处理:从摄像头到显示的完整链路
FPGA图像处理的典型链路是:摄像头 → MIPI/DVP接收 → 图像预处理 → 缓存 → 显示或传输。
MIPI接收对FPGA来说是个挑战,因为MIPI的速率高、协议复杂。低端FPGA没有MIPI硬核,只能用LVDS加软件解包,资源消耗大。如果项目必须用MIPI,建议选带MIPI硬核的FPGA,或者用专门的MIPI转并行芯片。
图像预处理在FPGA里做,常见的操作有:Bayer转RGB、白平衡、Gamma校正、边缘检测。这些算法都是像素级并行操作,FPGA做起来效率极高。用纯Verilog写ISP算法,一个时钟周期处理一个像素,1080p@60fps也就148.5MHz的像素时钟,中端FPGA完全能跑。
处理完的图像数据通过FMC送给STM32,STM32用LTDC驱动LCD显示,或者用JPEG编码后通过网络传出去。这里STM32的角色是"显示控制器"和"网络传输器",繁重的像素处理已经由FPGA完成了。
4.4 电机控制:STM32算PID,FPGA出PWM
多轴电机控制是双核系统的经典应用。STM32跑PID算法,计算量不大但需要定期执行;FPGA负责生成多路PWM、解码编码器信号、做硬件保护。
这样做的好处是:PWM的精度和同步性由硬件保证。STM32的定时器虽然也能出PWM,但多轴之间的同步需要软件协调,会有抖动。FPGA里所有PWM通道共用一个时钟源,相位关系精确可控,而且可以做到纳秒级的分辨率。
编码器解码也是同理。STM32用定时器的编码器模式,高速时容易丢脉冲;FPGA用硬件状态机解码,再高的转速也不会丢。我做过一个项目,电机转速到10000RPM,编码器输出频率几百kHz,STM32根本处理不过来,换成FPGA后稳如泰山。
STM32和FPGA之间的接口:STM32通过SPI或者FMC把目标位置/速度写给FPGA,FPGA生成对应的PWM;FPGA把编码器计数值和限位开关状态回传给STM32。整个控制环路:STM32算PID(1kHz左右),FPGA执行底层驱动(MHz级),分工明确。
5. 调试双核系统时那些让人抓狂的坑
5.1 时序约束没写对,综合出来能用是运气
FPGA开发新手最容易忽略的就是时序约束。很多人写完Verilog,综合通过了,下载进去也能跑,就觉得没问题了。但实际上,没有时序约束的FPGA设计,工作频率是"看运气"的。温度变了、电压变了、换一批芯片,可能就不工作了。
双核系统里,FPGA和STM32之间的接口时序尤其重要。FMC的读写时序必须用set_input_delay和set_output_delay约束,告诉综合工具外部信号的到达时间。如果约束写得太松,综合工具可能给出一个不满足建立/保持时间的结果;写得太紧,又可能过度优化导致资源浪费。
我的做法是:先用示波器或者逻辑分析仪实测STM32的FMC输出信号,量出地址、数据、控制信号之间的实际时序关系,然后据此写约束。约束写完后,一定要看时序报告,确认所有路径都满足。有违例的路径必须解决,不能心存侥幸。
5.2 STM32读FPGA数据偶尔出错,查了三天才发现是Cache
STM32H7系列有Cache,这玩意儿在双核系统里是个大坑。FMC映射的地址空间如果被Cache了,STM32读到的可能是缓存里的旧数据,而不是FPGA刚写的新数据。
现象是:大部分时候数据正确,偶尔读到旧值。你以为是FPGA写时序有问题,查了半天FPGA,最后发现是STM32的Cache没配置对。
解决方法有两种:一是把FMC的地址区域配置成Device模式或者Strongly Ordered模式,禁止Cache;二是在读之前调用SCB_InvalidateDCache_by_Addr()手动失效Cache。第一种方法简单粗暴,但会降低访问速度;第二种方法效率高,但要小心Cache一致性问题。
我一般用第一种,因为双核系统里FMC的访问速度本来就不是瓶颈,稳定压倒一切。
5.3 FPGA的IO电平标准和STM32不匹配
STM32的IO是3.3V LVCMOS,FPGA的IO电平标准是可以配置的。如果FPGA的Bank电压设成1.8V,而STM32输出3.3V,长期工作可能会损坏FPGA的IO。
更隐蔽的问题是Hysteresis Input Mode。有些FPGA的IO支持迟滞输入模式,可以增强抗干扰能力,但迟滞窗口会影响阈值判断。如果STM32输出的信号边沿不够陡,或者有振铃,FPGA在迟滞模式下可能误判。
我的建议是:接口电平统一用3.3V LVCMOS,FPGA的IO Bank电压设成3.3V。如果必须做电平转换,用专用的电平转换芯片,不要用电阻分压,分压会引入延迟和边沿退化。
5.4 跨时钟域问题:逻辑分析仪都抓不到
前面提过跨时钟域,这里再强调一次,因为这是双核系统里最难查的问题。
我遇到过一个案例:FPGA内部ADC采样时钟是50MHz,FMC接口时钟是100MHz,数据从ADC时钟域传到FMC时钟域时,偶尔出现一个错误值。用逻辑分析仪抓FMC总线,数据看起来是对的;抓ADC数据,也是对的。但STM32读到的就是偶尔错一个。
最后发现是异步FIFO的读使能信号没有做同步,导致FIFO在读的时候写指针正在变化,读出的数据不稳定。加上两级同步触发器后问题消失。
这类问题的排查思路是:先确认所有跨时钟域的信号都做了同步处理。单bit信号用双触发器,多bit数据用异步FIFO或者握手协议。不要相信"大部分时候是对的",亚稳态是概率问题,批量生产时一定会暴露。
5.5 printf调试在双核系统里怎么用
STM32上printf重定向到UART是常规操作,但在双核系统里,FPGA内部的状态你看不到。我的做法是:在FPGA里也做一个UART发送模块,把关键状态(FIFO空满、状态机当前状态、错误计数)实时打印出来。
这样调试的时候,STM32的printf和FPGA的printf可以接到同一个串口终端上(注意加个前缀区分),两边的情况一目了然。FPGA实现UART发送很简单,一个状态机加一个移位寄存器就行,资源消耗可以忽略。
还有一个技巧:用FPGA的嵌入式逻辑分析仪(Xilinx叫ILA,Intel叫SignalTap)。把关键信号接到ILA上,设置触发条件,可以抓取FPGA内部任何信号的波形。这比用外部逻辑分析仪方便得多,而且不占用IO引脚。缺点是消耗Block RAM,采样深度有限,但对于调试时序问题足够了。
6. 从原型到产品:双核系统的工程化建议
6.1 PCB布局:把数字噪声关在笼子里
双核系统的PCB比单MCU复杂得多,主要是高速信号和模拟信号共存。ADC的模拟输入、FPGA的高速差分对、STM32的晶振,这些都对噪声敏感。
我的布局原则:
- 分区:模拟区、数字区、电源区分开布局,地平面要完整,不要被信号线割裂。
- 晶振靠近芯片:STM32的晶振和FPGA的晶振都要尽量靠近对应的引脚,走线短而直,下面不要走其他信号。
- 差分对等长:LVDS、MIPI这些差分信号,正负两条线必须等长,误差控制在5mil以内。
- 电源去耦:每个电源引脚都要有0.1uF的去耦电容,大容量电容放在电源入口。FPGA的核电压和IO电压要分开供电,用LDO或者DC-DC,纹波要小。
如果板子上有ADC,模拟地和数字地一般建议单点连接,避免数字噪声通过地平面串到模拟部分。但具体怎么处理要看ADC的型号和精度要求,高精度ADC(16位以上)对布局要求很苛刻,低速ADC可以适当放宽。
6.2 固件升级:STM32和FPGA都要能在线更新
产品化之后,固件升级是个必须考虑的问题。STM32的固件升级好办,用Bootloader通过UART、USB或者网络接收新固件,写到Flash里就行。STM32的LD文件(链接脚本)要规划好Bootloader区和App区,别让它们重叠。
FPGA的固件升级稍微麻烦一点。FPGA的配置数据存在外部SPI Flash里,上电时FPGA从Flash加载配置。要在线升级,可以让STM32通过SPI去写这颗Flash,写完后再触发FPGA重新加载。
具体做法:STM32通过SPI接口连接到FPGA的配置Flash,升级时STM32把新的bit流文件写入Flash,然后拉低FPGA的PROGRAM_B引脚(或者用软复位命令),FPGA重新从Flash加载新配置。这样整个系统不用断电就能完成FPGA固件更新。
注意:升级过程中要保证电源稳定,Flash写入时断电会导致FPGA无法启动。可以在Flash里存两份配置(Golden Image和Update Image),升级失败时回退到Golden Image。
6.3 功耗和散热:别小看FPGA的发热
FPGA的功耗和它的工作频率、资源利用率成正比。一个中等规模的FPGA在100MHz下跑,功耗可能到1-2W,加上STM32和其他外设,整板功耗可能到3-5W。如果是手持设备或者密闭外壳,散热必须认真考虑。
降低FPGA功耗的几个手段:
- 时钟门控:不用的模块把时钟关掉,动态功耗和时钟频率成正比。
- 降低工作频率:在满足性能的前提下,尽量用低的时钟频率。很多应用不需要100MHz,50MHz就够了。
- 优化逻辑:减少不必要的翻转,比如用独热码(One-hot)编码状态机可以减少组合逻辑的毛刺,但会增加触发器数量,需要权衡。独热码和二进制编码的选择要看具体设计,状态少的时候独热码有优势,状态多的时候二进制编码更省资源。
- 散热设计:FPGA下面铺散热焊盘,PCB上打过孔到背面的大面积铜皮,必要时加散热片或者小风扇。
6.4 量产测试:怎么快速验证每一块板子
双核系统的量产测试比单MCU复杂,因为要同时验证STM32和FPGA。我的做法是做一个自测试固件:
STM32端跑一个测试程序,依次测试:UART通信、SPI通信、FMC读写、ADC采集、网络通信。FPGA端配合,在收到测试命令后生成已知的测试数据(比如递增序列、棋盘格图像),STM32读回后比对,判断FPGA是否正常。
测试结果通过LED或者串口输出,产线工人只需要看指示灯或者扫码记录就行。这套自测试固件在打样阶段就要开始写,不要等到量产了才临时抱佛脚。
还有一个经验:关键信号留测试点。FMC总线、SPI时钟、FPGA的配置完成信号(DONE)、STM32的复位信号,这些都要引出测试点,方便产线用示波器快速判断问题。测试点不用多,但一定要覆盖最可能出问题的环节。
7. 一些零散但实用的经验
关于STM32的GBK转UTF8:如果双核系统要显示中文,STM32端经常需要做编码转换。GBK是双字节,UTF8是变长编码,转换需要查表。如果字库不大,可以在STM32里存一张GBK到UTF8的映射表;如果字库很大,建议在上位机端就转好,STM32直接显示UTF8。
关于STM32禁用JTAG:F103系列上JTAG和某些GPIO复用,如果项目里用到了这些引脚,需要在代码里禁用JTAG保留SWD。调用__HAL_AFIO_REMAP_SWJ_NOJTAG()就行,但要注意禁用后JTAG引脚就变成普通GPIO了,不能再用来调试。
关于FreeRTOS下的Flash写入:STM32在写Flash时会阻塞总线,如果此时有中断触发,可能导致写入失败。正确做法是写Flash前挂起所有中断和调度器,写完再恢复。H743这类双Bank Flash的芯片可以Bank对Bank操作,但也要注意时序。
关于FPGA实现串口发送ASCII字符串:在FPGA里做一个简单的UART发送状态机,把要发送的字符存在一个ROM里,上电后依次读出并发送。调试信息用这种方式输出很方便,不需要额外的调试器。
关于STM32的DWT:DWT(Data Watchpoint and Trace)可以做精确的代码执行时间测量,比用定时器更准。在调试PID算法、优化性能时很有用。初始化时使能DWT的CYCCNT,然后在代码前后读这个寄存器,差值就是时钟周期数。
这套双核架构我用了几年,从最初的磕磕绊绊到现在的得心应手,最大的体会就是:边界要清晰,接口要简单,调试要留后路。STM32和FPGA各干各擅长的事,中间的接口越简单越好,调试手段越丰富越好。只要这三点做到了,双核系统其实比单核更稳定,因为每个核的负载都轻了,出问题的概率反而更低。