1. 为什么要把STM32和FPGA凑到一起
第一次听到“STM32+FPGA双核技术系统”这个说法,很多人脑子里冒出来的第一个问题就是:这俩东西到底谁听谁的?是不是把两颗芯片焊在一块板子上,然后各跑各的程序就算“双核”了?如果你也这么想过,那说明你还没被实际项目毒打过。我最早接触这个组合是在一个高速数据采集项目里,当时用STM32F407做主机,想让它同时处理三路ADC采样、跑FFT、再通过串口把频谱数据吐给上位机。结果采样率刚过200kHz,CPU占用率就飙到80%以上,串口输出稍微频繁一点直接丢包。后来加了一片EP4CE10,把ADC采样、数据缓存、FFT运算全部甩给FPGA,STM32只负责读结果、做协议封装和人机交互,整个系统瞬间就“呼吸顺畅”了。
所以这里的“双核”并不是指同一颗芯片里有两个对等核心,而是STM32作为控制核,FPGA作为数据核,两者通过FSMC、SPI或并行总线协同工作。STM32擅长的是流程控制、协议栈、人机界面和复杂状态机;FPGA擅长的是并行采样、高速接口、硬实时逻辑和流水线运算。把这两类任务强行塞进一颗芯片,要么选带FPGA硬核的SoC(比如Zynq),要么就得外挂一颗FPGA。前者成本高、开发门槛陡,后者灵活、选型自由,这也是为什么大量工业采集板、电机控制板、图像预处理板都采用“STM32+FPGA”这种分立架构。
这个系统适合谁?如果你正在做高速ADC采样、多路电机控制、图像预处理、频率测量、TDC时间测量这类项目,并且发现STM32单核已经扛不住实时性要求,那这套架构就是为你准备的。如果你只是点个灯、读个温湿度,那完全没必要上FPGA,杀鸡用牛刀反而增加布线难度和调试成本。下面我会从架构设计、接口选型、核心模块实现、常见问题排查几个维度,把我在实际项目中踩过的坑和总结的经验完整拆开讲。
2. 双核架构的整体设计与任务划分
2.1 控制核与数据核的职责边界
在动手画原理图之前,第一件事是把任务清单列出来,然后逐条判断该分给谁。我通常用三个标准来划分:实时性要求、并行度要求、计算密度。实时性要求高且允许并行执行的任务,比如多路PWM互补输出、高速ADC连续采样、LVDS差分接收,优先给FPGA。需要跑协议栈、文件系统、网络通信、复杂UI的任务,比如巴法云物联网网关、GBK转UTF8、CAN通信管理,优先给STM32。计算密度高但可以流水线化的任务,比如FFT、数字滤波器、图像卷积,FPGA实现起来比STM32快一个数量级。
具体到项目里,我一般这样分:
| 任务类型 | 推荐归属 | 理由 |
|---|---|---|
| 多路ADC同步采样 | FPGA | 并行采样,不受CPU中断延迟影响 |
| FFT/数字滤波 | FPGA | 流水线结构,单周期吞吐 |
| 电机换向逻辑 | FPGA | 硬实时,纳秒级响应 |
| 串口/CAN/以太网协议 | STM32 | 协议栈成熟,开发快 |
| 人机界面/触摸屏 | STM32 | 图形库丰富,刷屏方便 |
| 物联网云平台对接 | STM32 | 巴法云、MQTT都有现成库 |
| 系统状态管理 | STM32 | 流程控制是强项 |
| 高速数据缓存 | FPGA | 片内RAM做乒乓缓冲 |
这个划分不是绝对的。比如STM32的定时器捕获测频率,在低频段完全够用,但到了MHz级别就必须换FPGA的进位链TDC或者计数器方案。再比如STM32的ADC切换通道,用DMA多通道扫描也能做,但通道间采样时刻有偏差,要求严格同步的场景还是得FPGA外挂多片ADC。
2.2 互联总线的选型与取舍
STM32和FPGA之间怎么通信,是整个系统设计里最关键的决策之一。我实际用过的方案有三种:FSMC并行总线、SPI高速串行、自定义并行接口。每种都有明确的适用场景。
FSMC是STM32上最像“总线”的外设,可以把FPGA当成一片SRAM来读写。地址线、数据线、读写控制线一应俱全,STM32端直接指针操作,FPGA端写个地址译码就能响应。优点是速度快,16位数据宽度下轻松跑到几十MB/s;缺点是占用引脚多,FSMC的地址数据线加起来动辄二三十根,PCB布线密度大。我一般用在数据吞吐量要求高的场合,比如图像预处理系统里STM32要从FPGA读回一帧320x240的灰度图,用FSMC就比SPI快得多。
SPI的方案最省引脚,四根线就能跑,STM32的SPI时钟拉到20MHz以上也没问题。缺点是全双工但半双工效率低,FPGA端要写状态机来响应,协议开销比FSMC大。适合控制指令下发、参数配置、低速数据回传这类场景。我有个项目用SPI让STM32配置FPGA内部的FIR滤波器系数,每帧几十个字节,完全够用。
自定义并行接口介于两者之间,用STM32的GPIO模拟时序,或者用定时器触发DMA搬运。灵活性最高,但时序要自己保证,调试起来最麻烦。除非FSMC和SPI都不满足需求,否则我不推荐新手一上来就搞自定义接口。
提示:如果选用FSMC,注意STM32的FSMC地址映射和FPGA端的地址译码要严格对应。我见过有人把FPGA的片选接到FSMC的NE1,但地址偏移算错了,结果读写全乱套,查了两天才发现是基地址搞错了。
2.3 时钟与复位系统的设计要点
双核系统里,时钟和复位是最容易出问题但又最容易被忽视的部分。STM32和FPGA各自有独立的时钟源,如果两者需要同步采样或精确时间戳,就必须共用一个参考时钟,或者至少保证时钟同源。我通常的做法是:板上放一颗50MHz的有源晶振,同时供给FPGA的全局时钟输入和STM32的外部时钟输入(通过PLL倍频到所需频率)。这样两者的时间基准是一致的,跨芯片通信时不会出现累积漂移。
复位方面,FPGA一般没有固定的复位脚,上电后靠内部配置完成后的初始化逻辑来产生复位信号。但STM32有专门的NRST引脚。我一般让STM32先完成初始化,然后通过一个GPIO给FPGA发软复位,确保FPGA在STM32准备好之前不会往总线上乱写数据。反过来,FPGA配置完成后也可以给STM32一个中断,告诉它“我准备好了”。这种握手逻辑看起来简单,但少了它,系统上电时经常出现STM32读FPGA返回全0或全F的情况。
3. 核心模块的FPGA侧实现细节
3.1 高速ADC采样与数据缓存
FPGA做高速ADC采样,核心思路是用硬件并行换时间。以常见的AD9226为例,12位并行输出,最高65MSPS。FPGA端只需要一个时钟驱动ADC的采样时钟,然后在每个时钟上升沿把12位数据锁进寄存器,再写入片内FIFO或乒乓RAM。整个过程不需要CPU干预,采样率只受ADC和FPGA的时序约束限制。
这里有个细节:ADC的采样时钟最好由FPGA的PLL产生,而不是直接用晶振分频。因为FPGA的PLL可以精确控制相位,方便调整采样时刻与数据输出之间的建立保持关系。我一般会把ADC时钟和FPGA内部处理时钟设成同源但不同相,通过时序约束来保证数据可靠锁存。
数据缓存用乒乓RAM是经典做法。写端口接ADC数据流,读端口接STM32的FSMC接口。当一页RAM写满后,FPGA产生一个中断给STM32,STM32来读另一页。这样读写互不干扰,STM32读数据的速度不会影响采样连续性。乒乓RAM的深度根据STM32读取速度和采样率来算:假设采样率50MSPS,STM32通过FSMC每100ns读一个16位数据,那么一页RAM至少需要50MSPS × 100ns = 5个采样点,实际取256或512深度留足余量。
3.2 频率测量与TDC时间数字转换
频率测量在FPGA里有两种主流方案:计数法和等精度法。计数法简单,在固定闸门时间内数脉冲个数,低频精度差,高频精度好。等精度法用两个计数器分别对被测信号和标准时钟计数,通过公式计算频率,在全频段内精度一致。我一般用等精度法,标准时钟取100MHz,闸门时间取1ms,这样在1Hz到10MHz范围内误差都能控制在0.01%以内。
TDC(Time-to-Digital Converter)是FPGA的独门绝技,利用进位链的传播延迟来测量皮秒级时间间隔。具体做法是把被测信号接入进位链的输入端,用系统时钟锁存进位链的输出状态,根据进位传播的位置反推时间差。Xilinx和Altera的进位链延迟大约在20-50ps每级,一条64级进位链就能覆盖1-3ns的量程。多级级联可以扩展到更大量程。这个方案在激光测距、飞行时间测量里非常常用,STM32的定时器捕获根本做不到这个精度。
注意:进位链TDC对温度敏感,温度变化会导致延迟漂移。实际项目中需要做温度校准,或者用双链差分结构来抵消共模漂移。
3.3 图像预处理与LVDS接收
FPGA做图像预处理,典型流程是:LVDS接收 → 解串 → 色彩空间转换 → 滤波/边缘检测 → 缓存 → STM32读取。LVDS接收需要FPGA支持差分输入,并且要处理源同步时钟的相位对齐。我一般用IDELAY和ISERDES原语来做,先通过训练序列找到最佳采样点,然后固定延迟值。
图像滤波用3x3卷积核在FPGA里实现,需要行缓存和窗口生成。行缓存用双口RAM,存两行像素,加上当前行组成3x3窗口。卷积运算用流水线乘法器,每个时钟出一个结果。整个流水线延迟只有几个时钟周期,比STM32软件卷积快几百倍。边缘检测用Sobel算子,两个方向的卷积核并行计算,最后求平方和开根号。开根号可以用CORDIC算法或者查表法,FPGA里都有现成IP核。
3.4 串口发送ASCII字符串的FPGA实现
FPGA实现串口发送,核心是波特率发生器和移位寄存器。波特率发生器用计数器分频系统时钟,比如50MHz时钟要产生115200bps,分频系数是50_000_000 / 115200 ≈ 434。计数器从0数到433,每个周期产生一个波特率脉冲。移位寄存器在起始位到来时加载数据,然后在每个波特率脉冲移位输出。
发送ASCII字符串时,我一般把字符串存在FPGA的ROM里,用状态机依次读出每个字符,送入发送模块。状态机有IDLE、START、DATA、STOP四个状态,START时拉低TX线一个波特率周期,DATA时按位输出8位数据,STOP时拉高TX线。整个过程不需要CPU干预,STM32只需要在需要发送时给一个触发信号。
// 简化的UART发送状态机 localparam IDLE = 2'b00, START = 2'b01, DATA = 2'b10, STOP = 2'b11; always @(posedge clk) begin case(state) IDLE: if(tx_start) begin state <= START; tx_line <= 1'b0; baud_cnt <= 0; end START: if(baud_tick) begin state <= DATA; shift_reg <= tx_data; bit_cnt <= 0; end DATA: if(baud_tick) begin tx_line <= shift_reg[0]; shift_reg <= {1'b0, shift_reg[7:1]}; if(bit_cnt == 7) state <= STOP; else bit_cnt <= bit_cnt + 1; end STOP: if(baud_tick) begin tx_line <= 1'b1; state <= IDLE; end endcase end4. STM32侧的关键实现与协同逻辑
4.1 FSMC读写FPGA的地址映射与DMA搬运
STM32通过FSMC访问FPGA,本质上是把FPGA当成一片外部SRAM。假设FPGA挂在FSMC的Bank1 NOR/PSRAM区域,基地址是0x60000000,地址线A0-A18接FPGA,数据线D0-D15接FPGA。FPGA端根据A0-A18的不同组合来区分是读数据、写命令还是读状态。
我一般把地址空间划分成几个区域:0x60000000-0x6000FFFF映射到FPGA的数据FIFO,读这个区域就是取采样数据;0x60010000映射到控制寄存器,写这个地址就是下发命令;0x60020000映射到状态寄存器,读这个地址就是查询FPGA状态。FPGA端用地址译码器产生片选和读写使能,再根据地址低位选择对应的寄存器。
DMA搬运是提高效率的关键。STM32的DMA可以配置成从FSMC地址读取数据,然后搬到内部RAM。配置好DMA后,STM32只需要启动DMA,然后等传输完成中断即可,CPU全程不参与。我实测过,用DMA从FPGA读1MB数据,耗时不到20ms,CPU占用率几乎为零。
// FSMC读FPGA数据FIFO的DMA配置示例 #define FPGA_DATA_ADDR ((uint32_t)0x60000000) #define BUFFER_SIZE 1024 uint16_t dma_buffer[BUFFER_SIZE]; void DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); DMA_InitStructure.DMA_Channel = DMA_Channel_0; DMA_InitStructure.DMA_PeripheralBaseAddr = FPGA_DATA_ADDR; DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)dma_buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; DMA_Init(DMA2_Stream0, &DMA_InitStructure); }4.2 中断握手与数据就绪信号
FPGA和STM32之间的中断握手,我一般用一根GPIO线。FPGA在数据准备好后拉高这根线,STM32配置成上升沿触发中断,在中断服务函数里启动DMA搬运。搬运完成后,STM32再给FPGA一个应答信号,FPGA收到应答后切换乒乓页,准备下一帧数据。
这个握手逻辑看起来简单,但实际调试时最容易出问题。常见的情况是:STM32中断响应不及时,FPGA已经写了新数据覆盖了旧数据;或者STM32应答太快,FPGA还没切换完页就收到了新请求。解决办法是在FPGA端加一个状态机,只有收到应答后才允许写下一页,同时STM32端在中断里先关中断,处理完再开,避免重入。
提示:如果数据率很高,GPIO中断可能成为瓶颈。这时候可以用FSMC的等待信号(NWAIT)来做流控,FPGA在数据没准备好时拉低NWAIT,STM32的FSMC会自动插入等待周期,不需要CPU干预。
4.3 物联网网关中的协议转换与云平台对接
STM32做物联网网关,典型场景是:FPGA采集传感器数据,STM32通过串口或FSMC读取后,封装成MQTT协议发到巴法云。巴法云的MQTT接入很简单,用ESP8266或ESP32做WiFi模块,STM32通过AT指令或者SPI接口跟WiFi模块通信。
协议转换的重点是数据格式统一。FPGA传上来的原始数据可能是二进制补码,STM32需要转成JSON或者键值对格式。比如FPGA测得的频率值是32位无符号整数,STM32要转成字符串“freq=12345678”。如果涉及GBK转UTF8,STM32端可以用现成的转换表,或者用FatFs自带的中文编码转换函数。我一般把常用汉字的GBK和UTF8码表存在外部Flash里,需要时查表转换,比实时计算省事。
巴法云的MQTT主题订阅和发布用STM32的MQTT客户端库就能实现。注意心跳包间隔不要设太长,巴法云默认60秒无数据就断开。我一般设30秒发一次心跳,同时把传感器数据也捎带发上去,一举两得。
5. 常见问题与排查技巧实录
5.1 STM32读FPGA返回全0或全F
这是双核系统最经典的问题,没有之一。现象是STM32通过FSMC读FPGA,读回来的数据要么全是0x0000,要么全是0xFFFF。原因通常有三个:片选没接对、地址偏移算错、FPGA端时序不满足。
排查步骤:先用示波器看FSMC的片选信号和读写信号,确认STM32确实在发起访问。然后看FPGA端的地址译码逻辑,确认片选和地址线都连到了正确的引脚。最后检查时序,STM32的FSMC默认时序可能太快,FPGA来不及响应。解决办法是在FSMC初始化结构体里增加等待周期,或者用NWAIT信号做流控。
我踩过的一个坑是:FPGA的IO口配置成了hysteresis input mode,导致输入阈值偏高,STM32的3.3V电平在FPGA端识别不稳定。后来改成普通LVTTL模式就正常了。所以FPGA的IO标准一定要和STM32的电平匹配,3.3V对3.3V,不要混用1.8V或2.5V。
5.2 CAN通信突然连不上
STM32的CAN通信在双核系统里突然断连,常见原因是FPGA的并行总线操作干扰了CAN收发器。FSMC高速读写时,地弹和电源噪声会耦合到CAN总线上,导致位错误。解决办法:CAN收发器的电源加磁珠和去耦电容,PCB布局时让CAN走线远离FSMC数据线,最好分层走线。
另一个原因是CAN波特率配置错误。STM32的CAN波特率由APB1时钟和分频系数决定,如果系统时钟改了但CAN初始化没跟着改,波特率就会偏。我一般用示波器测CAN_H和CAN_L的差分波形,量一下位时间,反推实际波特率,跟预期值对比就能发现问题。
5.3 FPGA配置失败或加载不成功
FPGA上电后不工作,首先看配置完成引脚(CONF_DONE)有没有拉高。如果没拉高,说明配置数据没加载成功。常见原因:配置芯片(EPCS或Flash)数据损坏、配置时钟太慢、电源上电顺序不对。
Altera的FPGA一般要求核心电压1.2V先上,IO电压3.3V后上。如果顺序反了,可能导致配置失败。我一般用电源管理芯片做时序控制,确保上电顺序正确。另外,配置时钟(DCLK)的频率不要超过数据手册规定的最大值,一般10MHz以内比较稳妥。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| STM32读FPGA全0 | 片选/地址错误 | 示波器看片选信号 | 检查地址译码逻辑 |
| STM32读FPGA全F | 时序不满足 | 看读写信号时序 | 增加FSMC等待周期 |
| CAN通信断连 | 电源噪声干扰 | 测CAN差分波形 | 加磁珠、远离高速线 |
| FPGA配置失败 | 上电顺序错误 | 测CONF_DONE引脚 | 调整电源上电顺序 |
| 采样数据错位 | 时钟相位不对 | 测ADC时钟和数据 | 调整PLL相位 |
| 串口乱码 | 波特率不匹配 | 测位时间 | 重新计算分频系数 |
6. 实操心得与避坑经验
6.1 先调通单核再联调
我见过太多人一上来就把STM32和FPGA焊在一起,然后两边同时写代码,结果出了问题根本不知道是谁的锅。正确的做法是:先把FPGA单独调通,用SignalTap或者逻辑分析仪确认ADC采样、FIFO读写、串口发送都正常。然后把STM32单独调通,用串口打印确认FSMC读写外部SRAM没问题。最后再把两者连起来,逐步增加功能。
这个顺序看起来慢,实际上最快。因为单核调试时问题定位简单,联调时只需要关注接口时序。我一般会在FPGA端预留一个测试寄存器,STM32写进去再读出来,如果一致就说明总线通了。这个测试寄存器花不了几个逻辑单元,但能省下大量调试时间。
6.2 用好SignalTap和逻辑分析仪
FPGA内部信号看不见摸不着,SignalTap是必备工具。我一般把关键信号都加进SignalTap:ADC数据、FIFO读写指针、状态机当前状态、FSMC片选和读写使能。触发条件设成FIFO满或者状态机异常跳转,这样出问题时能抓到现场。
逻辑分析仪用来抓STM32和FPGA之间的接口信号。我一般抓FSMC的片选、读写、地址和数据,还有中断握手线。采样率至少设成接口时钟的4倍以上,否则会漏掉毛刺。抓到的波形跟时序图对照,很快就能发现是建立时间不够还是保持时间不够。
6.3 电源和地的处理不能省
双核系统里,FPGA和STM32的电源要分开处理。FPGA的核心电压和IO电压用独立的LDO,STM32的3.3V也用独立的LDO,两者之间用磁珠隔离。地平面要完整,不要被信号线割裂。FSMC的数据线旁边最好有地线伴随,减少串扰。
去耦电容要就近放置,每个电源引脚配一个100nF,每对电源引脚再配一个10uF。FPGA的PLL电源尤其要干净,我一般用LC滤波再加一个低噪声LDO。这些措施看起来繁琐,但能避免大量莫名其妙的随机故障。
6.4 版本管理和文档记录
双核系统的代码分两部分:STM32的C代码和FPGA的Verilog/VHDL代码。我一般用Git做版本管理,STM32和FPGA放在同一个仓库的不同目录下。每次联调成功后打一个tag,记录当时的硬件版本、软件版本和测试结果。这样出了问题可以快速回退到已知正常的版本。
接口文档尤其重要。FSMC的地址映射、中断握手时序、数据格式定义,这些都要写成文档,放在仓库根目录。我吃过亏:项目做到一半,FPGA端改了数据格式但没通知STM32端,结果读出来的数据全错,查了半天才发现是格式不匹配。从那以后,接口文档成了强制要求,任何改动都要先更新文档再改代码。
7. 系统扩展与进阶方向
7.1 从双核到多核的扩展
STM32+FPGA的双核架构跑通后,扩展方向很多。比如加一片ESP32做WiFi通信,STM32通过SPI跟ESP32通信,FPGA继续做数据采集,形成“FPGA+STM32+ESP32”三核架构。或者用K210做图像识别,STM32做控制,FPGA做预处理,三者通过串口或SPI互联。
扩展时要注意总线仲裁。多个主机访问同一个FPGA资源时,需要加仲裁逻辑。我一般用轮询或者令牌环的方式,简单可靠。如果数据量不大,也可以用双口RAM做共享内存,每个主机分配独立的地址段,互不干扰。
7.2 用FreeRTOS管理STM32侧任务
STM32侧的任务多了以后,裸机的前后台架构会越来越吃力。这时候可以上FreeRTOS,把FSMC数据读取、MQTT通信、UI刷新、CAN通信分别做成独立任务,用信号量和队列做同步。FreeRTOS在STM32F4上跑很轻松,RAM占用也就几KB。
注意中断优先级和任务优先级的配合。FSMC的DMA完成中断优先级要高于普通任务,但低于系统节拍中断。中断服务函数里尽量只做标志位设置和信号量释放,具体处理放到任务里做。这样既保证实时性,又不会在中断里耗时太长。
7.3 FPGA侧的资源优化
FPGA资源有限时,优化方向主要有三个:复用乘法器、压缩缓存、降低时钟频率。乘法器用时分复用,一个乘法器分时处理多路数据,代价是控制逻辑复杂一点。缓存用乒乓RAM代替FIFO,节省Block RAM。时钟频率在满足吞吐量的前提下尽量降低,减少功耗和时序压力。
如果逻辑资源实在不够,可以考虑换更大容量的FPGA,或者把部分算法搬到STM32上做。比如FFT,点数少的时候STM32用CMSIS-DSP库也能跑,不一定非要FPGA。关键是找到性能、成本、开发周期的平衡点。
我个人在实际操作中的体会是,STM32+FPGA这套架构最大的价值不在于性能有多强,而在于任务划分的清晰性。控制归控制,数据归数据,各司其职,系统稳定性比单核硬扛要高得多。但前提是接口设计要扎实,握手逻辑要严谨,否则两颗芯片互相等对方,反而比单核还慢。最后再分享一个小技巧:在FPGA里留一个“看门狗”计数器,如果STM32超过一定时间没有访问FPGA,FPGA就自动复位到安全状态,避免系统卡死。这个逻辑只需要几十个逻辑单元,但关键时刻能救你一命。