1. 为什么STM32+FPGA成了工控与仪器项目里的“双核”标配
1.1 单芯片搞不定的两类活:协议栈与硬实时并行
先说个结论:STM32+FPGA这套“双核”组合,本质不是双CPU,而是异构双处理器。一个负责“管”,一个负责“干”,各干各擅长的事,合起来才叫真正的双核技术系统。
STM32这类MCU,优势在“生态”和“协议栈”。RCU、TCP/IP、USB、SDIO、文件系统、GUI,一堆现成库直接调,跑个FreeRTOS做任务调度非常顺。你要让它去实时处理几十路高速信号、精确输出纳秒级脉冲,它就力不从心了。原因很简单:MCU是串行取指执行,中断响应再快,也架不住高频事件源源不断涌进来,更别说还要同时维护通信协议栈。
FPGA的优势在“并行”和“时序”。几百个计数器可以同时跑,逻辑门延迟决定了处理速度,和软件执行完全两个维度。但FPGA做复杂状态机、跑通信协议栈就很痛苦,写一个TCP/IP协议栈试试,光状态机就能绕晕你。就算塞个软核进去,效率也远不如ARM核。
所以这两个芯片凑在一起,不是叠性能,而是补短板。我在实际项目里的直观感受是:STM32+FPGA技术系统,基本覆盖了从传感器采集、实时控制到网络协议上云的完整链路。谁在做这类东西?搞仪器仪表、运动控制、机器视觉预处理、物联网边缘网关的工程师,几乎都会碰到这个架构。
1.2 从搜索热度看大家的真实痛点
我整理了一下去年年底到今年大家搜得多的关键词,很有参考价值:
- FPGA侧的:频率测量、串口发送ASCII字符串、图像处理、LVDS接收、MIPI接口、进位链TDC测量时间、信号发生器、串口升级QSPI。
- STM32侧的:定时器捕获测频率、ADC切换通道、GBK转UTF8、巴法云、FreeRTOS物联网网关、LWIP协议栈、CAN通信连不上、ILI9341读ID是a1a1、DWT、LD文件、J-Link下载环境。
看出来了吗?这些需求单独看都很零散,但拼起来其实就是一套典型的“STM32+FPGA”系统零件:FPGA负责高速采集和接口转换,STM32负责协议处理和上云。搜FPGA频率测量的人,很可能正在做一台需要STM32读取测量结果的频率计;搜LVDS接收和MIPI的人,多半在给图像传感器做数据搬运,搬完还是要交给ARM核跑算法或通讯。
这篇文章就是按这套思路拆解的。不管你拿FPGA做数据采集、接口转换还是实时控制,也不管你用STM32做网关、显示还是通讯,下面这些设计取舍和调试链路,应该都能对得上。
2. 双核系统的架构分工:先分活,再选通信
2.1 任务划分的几条硬规则
不少初学者拿到STM32+FPGA的开发板,第一反应是“FPGA是不是抢STM32的活?我全用STM32行不行?”这个问题问得挺好,答案也简单:先看你有没有硬实时需求。
我给自己定过几条任务划分规则,用了几年还没出过错:
**规则一:超过几十kHz的周期性信号处理,交给FPGA。**比如PWM输出20kHz以上的多路方波、高速ADC采样、编码器正交解码,STM32做起来要么占用大量CPU时间,要么精度上不去。
**规则二:复杂但低频的状态逻辑,留在STM32。**比如人机交互界面、报警状态机、参数配置菜单、云端指令解析,这种活让FPGA做纯属折磨。
**规则三:对外通信协议统一由STM32接管。**串口、CAN、Ethernet、MQTT、HTTP,都放STM32。理由很简单,MCU生态里有现成协议栈,FPGA写协议栈费时费力还容易出bug。
**规则四:硬实时闭环控制放FPGA。**比如电流环、位置环、需要固定延时的信号链,一般放在FPGA里跑,保证时钟级确定性。STM32顶多算“闭环下位机”,负责参数给定和状态上报。搜“STM32控制伺服电机485”这类需求,本质就是STM32做主控下发参数,FPGA做高速插补或脉冲输出。
总结成一句话:FPGA管“每个时钟周期都在发生的细节”,STM32管“需要长期权衡的事情和对外交流”。
这套分工定下来,系统架构就自动清晰了。
2.2 双核之间的通信选型:FSMC、SPI、UART怎么取舍
分完活,接下来最关键的是STM32和FPGA之间怎么通信。通信带宽直接决定你能把多少数据从FPGA搬到STM32,也决定延迟。
常用三种方案,直接上对比表格:
| 通信方式 | 典型速率 | 特点 | 适合场景 |
|---|---|---|---|
| FSMC/可变静态存储控制器并行总线 | 几十MB/s以上,受IO口和时序制约 | STM32把FPGA映射成外部SRAM,读写像操作内存一样简单;占用IO多 | 高速ADC采集、图像数据搬运、大块数据块交换 |
| SPI从机模式 | 1-10Mbps左右(取决于分频和从机能力) | 引脚少,实现简单,适合中低速数据块 | 温度采样、状态寄存器轮询、配置命令下发 |
| UART | 115200bps-1Mbps | 最简单,但速率低,传输有波特率容错问题 | 调试链路、低频率状态上报 |
我从项目实操角度说点文档里不太会写的体会:
FSMC这个名字听起来高大上,其实就是STM32片内外设的一个接口控制器,可以把外部器件当作内存来访问。FPGA端只需要实现一个“异步SRAM读写的状态机”,STM32这边配好时序参数,接下来用*(uint16_t *)0x64000000 = data;这么一行代码就能写数据到FPGA。反过来读也一样。这是速度最快、代码最干净的方案。缺点也明显:要占用GPIO一大片,对PCB布线要求高。
SPI是我个人推荐的“起步方案”。STM32做主机,FPGA做从机,两根数据线加一根时钟线,一版就能通。很多项目一辈子用SPI就够了,没必要一上来就搞FSMC。需要注意SPI从机模式下,FPGA的响应时机必须足够快,否则主机拉低片选后等不到数据,就直接读超时。早期建议在FPGA里做一个简单的8位寄存器阵列,把配置参数、状态标志、测量结果都映射成寄存器,STM32通过SPI读写寄存器,逻辑清晰也不容易错。
UART只建议用在调试链路上。有个隐蔽的坑:两边都是“近似波特率”,哪怕误差在±1%以内,长时间连续收发大数据包也可能偶发出错。所以UART帧格式里一定要带校验,一旦收到错误帧就重发,别裸奔。
2.3 握手协议与数据帧格式设计
通信物理链路选好了,还得定一套双方都认的数据帧格式。我踩过的最痛的一个坑就是:前期图省事,直接定义了一个固定长度的结构体数组,结果需求一改,双方同步改代码,改到后面版本错乱,简直无法维护。
后来我学乖了,所有通信都走“统一帧格式”,用最朴素的方案:
帧头(0xA5) | 命令字 | 数据长度 | 数据区 | 校验和帧头1字节,命令字1字节,长度2字节,数据区最多4096字节,校验和用简单的累加即可。FPGA收到帧后先解析帧头和长度,再接收数据区并计算校验;校验通过就执行命令,不通过直接丢弃并回报错误标志。
为什么这么做?因为STM32和FPGA两边是不同的开发工具链,调试时经常一边在改Verilog一边在改C代码。有了统一帧格式,两边各自维护状态机,只要遵守帧格式这条“契约”,各自内部怎么改都不影响对面。
这里再分享一个高级技巧:可以把FPGA内部所有寄存器设计成“地址映射表”,STM32读写一个寄存器地址,FPGA返回对应的状态/数值。就像你访问一条内存地址一样,调试和维护都极其方便。尤其是搜“STM32 ADC切换通道”或者“FPGA信号发生器”这类项目,有了寄存器映射,STM32改频率、切换通道、读测量结果,全部变成对某个地址的读/写操作,代码可读性提升一个等级。
3. FPGA端三个值得练手的模块:从频率测量到高速接口
3.1 频率测量:等精度测量原理与实现要点
先说一个热词:“FPGA实现频率测量”,这在项目里几乎必做,也是最容易理解FPGA并行优势的练手模块。
频率测量主要有三种思路:
测频法:在固定闸门时间内数被测信号的脉冲个数,频率=脉冲数/时间。适合测高频,低频测不准。
测周法:测量被测信号一个周期内的高频基准时钟个数,频率=基准频率/计数。适合测低频,高频反而测不准。
等精度测量法:把测频法的闸门设计成“跟随被测信号边沿”的同步闸门。闸门开启和关闭都自动对齐被测信号的边沿,同时计数被测脉冲数和基准时钟数,无论高频率低频率,相对误差都一样,而且只取决于基准时钟的准确度。
FPGA实现等精度测量非常自然,因为里面对被测信号和基准时钟各有一个计数器,两者并行跑,闸门信号一拉高就开始计,闸门结束同时锁存两个计数器的值。这个“同时锁存”是FPGA的天然能力,换成MCU需要精确的中断配合,很难做到。
伪代码层面长这样:
// 等精度频率计核心逻辑(简化) always @(posedge clk_100m) begin gate_sync <= test_signal ? 1 : 0; // 闸门同步到被测信号 end // 基准计数器和被测计数器 always @(posedge clk_100m) begin if (gate_sync && !gate_sync_prev) cnt_ref <= 0; // 闸门打开,开始计数 else if (gate_sync) cnt_ref <= cnt_ref + 1; end // 频率结果 = 被测计数 / 基准计数 * 基准频率实际项目里还要加几个细节:多次测量取平均消抖动、自动量程切换、防止计数溢出。代码本身不难,难的是理解“同时计数”背后对时序的要求。我见过不少初学者把这个逻辑写成先数被测信号再数基准信号,那就完全没有并行意义了。你要想清楚,FPGA让你能同时干两件事,这才是价值所在。
3.2 串口发送ASCII字符串:一个看似简单但容易踩坑的模块
搜“FPGA实现串口发送ASCII字符串”特别多,原因很实在:FPGA需要把测量结果发出去,最简单的就是通过串口用ASCII码发出数字和字符。但真写起来,发现一堆细节。
UART发送模块的核心是波特率发生器。用一个计数器分频出波特率时钟,比如100MHz系统时钟,要发115200bps,计数器分频系数大约是868,在每个波特率时钟沿按位发送。发送一帧的顺序是:起始位0,8位数据(低位在前),停止位1。这就是基础。
容易踩的坑有三个:
第一个坑是字符串状态机。你设计了一个“发送字符串”的模块,但字符串不是一个字节,你要用一个状态机控制:数据选择、总线占用、字节间时间间隔。如果状态机写得不仔细,很容易出现字节之间间隔太短或太长的问题。间隔太短接收方跟不上,间隔太长又拉低整体效率。
第二个坑是ASCII转换。FPGA里没有sprintf这个函数,你要自己把二进制测量结果转换成十进制的一串ASCII码再发送。可以写一个“除10取余”的转换状态机,先用组合逻辑做除法取余,再查表变成字符。这个过程占资源不大,但是初学者容易写错顺序。我自己习惯先在仿真里验证一遍转换结果,再上板。
第三个坑是不要为了追求简洁而把发送逻辑全塞进一个always块。拆分模块是FPGA好习惯:波特率发生器一个模块,ASCII转换一个模块,发送状态机一个模块。每个模块单独测试,出问题只改一小块。很多FPGA项目后期的难易程度,完全取决于前期模块切分是否合理。
3.3 进阶:TDC时间测量、LVDS接收与MIPI接口为什么只能靠FPGA
如果频率测量和串口让你打好了基础,那该聊聊真正让FPGA不可替代的东西了。搜“FPGA进位链TDC测量时间”“FPGA的LVDS接收”“FPGA实现MIPI”,这些才是双核系统里FPGA的核心价值所在。
TDC,即时间数字转换器,简单说就是测量一个时间间隔有多长。用MCU实现,分辨率基本在微秒级别,靠定时器捕获。而FPGA里可以利用进位链的延迟时间做TDC,分辨率能达到几十皮秒甚至更高。原理很简单:信号通过FPGA内部的进位链(CARRY4)传播,每一级延迟在皮秒量级,用D触发器把传播结果锁存,翻译成二进制时间值,就能测出信号到达的精确时刻。搜“使用FPGA进位链TDC测量时间”的人,通常是在做激光雷达、超声测距、粒子物理实验这类对时间精度要求极高的项目,用MCU根本不可能实现。
LVDS接收属于高速串行接口。LVDS信号是差分对,速率经常上百Mbps甚至更高,需要用FPGA的专用收发引脚和高速时钟管理模块去做数据采样恢复。STM32的IO基本不支持LVDS这种电平标准,就算外接转接芯片,数据率也跟不上。所以只要你的系统需要接高速ADC、LVDS摄像头或高速背板总线,FPGA几乎是唯一选择。
MIPI接口更典型。MIPI是移动设备摄像头和显示屏常用的高速串行接口,D-PHY速率经常到Gbps级别。FPGA需要做lane对齐、时钟恢复、数据字节打包,然后才把图像数据交给STM32做处理或显示。这块逻辑复杂,时序要求高,但只要你跟着官方例程做一次D-PHY接收,对FPGA高速设计的理解会立马上一个台阶。
我做这些模块的一个重要心得是:高速接口设计优先看官方IP核和参考设计,不要自己硬造轮子。Xilinx有MIPI CSI-2 IP核,Altera有LVDS IP核,先把官方demo跑通,再逐步改成自己需要的接口参数,能省掉至少两周的调试时间。等你有经验了再去手写协议逻辑也不迟。
4. STM32端实现细节:从HAL库到物联网网关
4.1 时钟树、DWT和定时器捕获:测频率/测脉宽的MCU方案
把FPGA分了那么多活,STM32这边也不是没事干。搜“STM32定时器捕获测频率”“STM32 DWT”这类关键词的人,说明很多人其实想在MCU侧直接测频率,或者用DWT做精确计时。
STM32定时器捕获的原理是:输入引脚检测到上升沿/下降沿时,定时器计数器的当前值被自动锁存到捕获寄存器,同时触发中断。连续两次捕获的计数值之差乘以定时器时钟周期,就是信号周期。测量低频信号很准,高频信号会由于中断响应时间抖动产生误差。
实操时HAL库写法很简洁:
// 初始化定时器输入捕获(以TIM2为例) TIM_IC_InitTypeDef sIC; sIC.ICPolarity = TIM_ICPOLARITY_RISING; // 上升沿捕获 sIC.ICSelection = TIM_ICSELECTION_DIRECTTI; sIC.ICPrescaler = TIM_ICPSC_DIV1; sIC.ICFilter = 0; // 滤波,可设为2-15以抗毛刺 HAL_TIM_IC_ConfigChannel(&htim2, &sIC, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);然后在中断回调里读取两次捕获值求差即可。注意一个陷阱:如果信号频率很低,定时器可能溢出,需要加一个溢出计数器,把溢出也计入周期。很多初学者漏掉这点,低频信号测出来是错的。
DWT则是一个容易被低估的工具。Cortex-M3/M4内核自带Data Watchpoint and Trace单元,其中有一个32位周期计数器,频率等于内核时钟。用一行代码就能启动:
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;之后读DWT->CYCCNT就可以获得精确到周期的计时。我常用它来进行代码片段耗时统计和分析,比写一堆GPIO翻转测量准得多。做双核系统联调时,用DWT测STM32读写FPGA某个寄存器整个流程耗时,定位瓶颈非常有效。
这里顺便回应一个热词:“STM32 HAL库下载”。HAL库其实就在STM32CubeMX的安装包里,不需要单独下载。用CubeMX生成工程初始化代码,你会发现外设配置工作量直接减半。唯一的建议是生成代码后,自己手动添加的逻辑块和生成区之间,做主备版本管理,避免CubeMX重新生成把代码冲掉。
4.2 ADC多通道切换与DMA,以及ILI9341读ID为a1a1这类“玄学”问题
ADC多通道切换是STM32常见的需求。用HAL库写ADC时,如果你的扫描模式没有正确配置,切换通道后第一次采样值常常不准,这是因为通道切换后需要稳定时间。我当时解决方法是把每轮采样的第一次结果丢弃,从第二次开始使用,或者干脆用DMA一直在循环采样,DMA缓冲区里的前几个数据直接忽略。不要追求“一上电就采到完美值”,工程上做“过滤掉暂态样本”反而是最省事的。
再吐槽一个经典问题:STM32使用ILI9341读ID返回a1a1。搜这个词的人基本都被这个坑绊过。a1a1这个值其实不是芯片坏了,而是时序没配对或者ILI9341的ID寄存器读取方式不对。正常ILI9341读ID应该返回0x93,如果你读回来全是0xA1A1,多半是你在发“读ID命令”前没有先进入SPI正确的读写模式,或者时钟相位极性配错了。换到双核系统里,这种“读回来永远是同一串值”的现象同样常见——比如STM32通过SPI读FPGA寄存器,如果两边时钟极性或相位不一致,读回来的数据永远是0xFF或0x00,看起来像“玄学”,实际就是协议细节没对齐。
解决这类问题的通用思路:用逻辑分析仪抓SPI的CS、SCK、MOSI、MISO四根线,对比数据手册时序图。比起漫无目的地改软件,一次波形分析比什么都管用。
4.3 FreeRTOS+LWIP+巴法云:STM32端作为网关的实现思路
双核系统里STM32还有个重要身份:物联网网关。搜“FreeRTOS STM32物联网网关”“STM32网关lwip协议栈”“STM32 巴法云”的用户,就是要把采集到的数据送上云,同时接收云端的控制指令。
我的标准框架是:FreeRTOS做任务调度,LWIP协议栈跑网络,巴法云或者自建MQTT服务器做上云通道。整体任务划分可以做成三四个任务:
- 数据采集任务:定期从FPGA那边读测量结果,存到共享区。
- 网络任务:运行MQTT客户端,周期发布数据到主题。
- 控制任务:订阅云端主题,收到指令后解析成参数,再通过SPI/FSMC下发FPGA执行。
任务与任务之间用FreeRTOS队列或者信号量传递,避免裸机循环带来的CPU浪费。这也是STM32相对于FPGA最大的优势所在:你有完整的RTOS生态,写网络应用和云端对接几乎都是搭积木。
这里提醒一个和云端交互直接相关的细节:如果你设备需要处理云端下发的中文消息,就绕不开编码转换。搜“STM32 GBK转UTF8”的人,多半是显示用户名称或中文指令时出现了乱码。云端消息基本都是UTF-8编码,而很多串口屏或文件系统用的是GBK。你没做转换就显示,必然乱码。实现思路很简单:先查表记住GBK和UTF-8的对应关系,按字节流判判断当前是ASCII还是双字节中文,再做映射。这个表比较大,可以放到外部Flash或SD卡里,按需加载。
5. 联调阶段最容易翻车的地方与排查链路
5.1 电平与IO标准:3.3V对3.3V也可能出问题
很多人以为STM32和FPGA都是3.3V电平,直接连就行了。实际上两个芯片的IO电气特性本质上都是3.3V,但驱动能力和灌电流能力差别很大。如果FPGA的IO管脚配置成1.8V的Bank电压,而你直接和3.3V的STM32引脚相连,电流灌入可能导致芯片锁死甚至烧毁。
所以联调第一件事,确认FPGA各个Bank的VCCO电压和STM32的电平标准匹配。FPGA里像LVDS这类高速接口,需要专用差分引脚,切记不能接到普通IO上,那些普通IO不支持LVDS电气标准。
还有一点,开发板上FPGA和STM32经常分布在两个区域,走线经过排针或转接板。一旦信号质量不佳,现象就是“时好时坏”“高温后频繁出错”。排查方式很简单:用示波器看波形上升沿是否光滑,有没有回沟。看到回沟优先怀疑信号完整性问题,而不是代码问题。
5.2 时序违例与毛刺:用示波器和逻辑分析仪复现一次真实排查
我印象最深的一次联调问题,现象是STM32读FPGA的数据,偶尔会读到FF,程序里怎么查都查不出原因。后来抓了CS信号和数据信号的波形,发现FPGA发出的数据在CS拉低的时候还没有完全稳定,STM32在CS下降沿一采就采到了中间状态。
这个问题的根因是FPGA端输出的建立时间不够。解决方式三种:
- 调整STM32侧的读写时序参数,在CubeMX的时序配置里把地址建立时间和数据建立时间适当放宽。
- FPGA侧对输出数据打一拍,也就是加一个寄存器延迟输出,让数据在CS有效前就已经稳定。
- 换用更慢的时钟或者下降沿采样。
排查链路清晰的话,这类问题不超过半天就能定位。最怕的是直接怀疑“是不是硬件坏了”,换芯片重焊,问题依旧,然后又回到软件死循环。所以我强烈建议双核联调时,示波器和逻辑分析仪必须配合使用,尤其是看协议总线的时序,一个逻辑分析仪能省几倍的时间。
5.3 从“STM32 CAN通信突然连不上”延伸的通信故障排查思想
搜“STM32 CAN通信突然连不上”这项也很热。CAN通信出问题的常见原因有:波特率配置不匹配、总线缺少终端电阻、某个节点错误率过高导致总线被动关闭。排查顺序通常也是先量总线波形,再查配置,再查节点状态寄存器。
我把这套排查思想推广到双核系统里,特别适合任何莫名其妙的通信故障:
第一步,隔离层。先把双核通信断开,STM32自发自回环测试,FPGA用仿真激励测试,分别确认两端自身是否正常。
第二步,波形层。用示波器或逻辑分析仪抓通信引脚的电气波形,确认电平、时序、握手信号是否和协议一致。
第三步,协议层。加打印或者把FPGA收到的字节流回传,确认帧头、长度、校验每个字段是否变换。
第四步,环境层。排除干扰源,检查供电稳定性,确认没有地电位差。
这条排查链路我用了很多年,几乎通吃所有双核通信问题。而不是一上来就盯着代码反复看,很多问题根本不在代码里。
6. 项目落地后的改进空间与我的个人体会
6.1 可扩展方向:从双核到更复杂的异构系统
STM32+FPGA双核架构用熟了以后,你可以自然往两个方向扩展。
一是往“软核SoC”方向走。现在主流FPGA都支持在内部例化ARM软核或硬核处理器,比如Zynq系列就是ARM核与FPGA在同一个芯片内,通信带宽远高于外部并口。如果你的项目需要更高速率的数据交换,比如图像处理流水线每秒传输几百MB,可以考虑直接上Zynq这类SoC FPGA。STM32+FPGA是入门和中等成本的王道,Zynq则是高性能扩展。
二是往“多核分工”方向走。在双核基础上再加一颗DSP做音频或振动信号处理,或者加一颗NPU做AI推理,FPGA仍然充当前端的接口汇聚和时序控制角色。搜“FPGA图像处理”和“FPGA创新设计大赛选题”的人,很多都会走向这种综合异构架构。
6.2 我做过的几个坑位总结与给新手的建议
最后说说我个人实际操作中的体会。
如果让我给刚接触STM32+FPGA双核系统的人提几条建议,第一条是:先把链路跑通,再追求性能。很多人一上来就设计高深的DMA和并行总线,结果基础通信还没通,反而怀疑人生。我建议第一个demo就用UART或者SPI,FPGA返回一个固定寄存器值,STM32读到就表示通信成功。在这个基础上再逐步增加功能,效率会高很多。
第二条是:FPGA逻辑一定要做仿真。哪怕是一个状态机,也值得在仿真里把波形看清楚了再上板。FPGA调试难度远高于MCU,因为内部信号你看不到,而仿真能让你看到一切。我见过太多人直接上板,信号出问题又不知道内部状态走到哪,瞎猜半天。
第三条是:想清楚“哪些数据必须实时,哪些可以缓存”。双核系统的性能瓶颈经常不在芯片本身,而在通信链路的带宽和延迟。一开始就把数据流分类,高频实时数据在FPGA内部就地处理,只把结果传给STM32;低频状态数据才走完整帧协议。这个设计的良好与否,直接决定系统最终性能。
最后一条,版本管理一定从一开始就做。STM32代码、FPGA工程、引脚约束文件、通信协议版本,全部纳入版本管理。两个芯片的项目,最怕的就是改了FPGA工程忘改STM32工程,然后莫名其妙花了三天找“bug”,结果发现协议字段错位了。别问我怎么知道的,这个坑我摔过不止一次。