
1. 为什么要把STM32和FPGA凑到一起第一次听到“STM32FPGA双核技术系统”这个说法很多人脑子里冒出来的第一个问题就是这俩东西到底谁听谁的是不是把两颗芯片焊在一块板子上然后各跑各的程序就算“双核”了如果你也这么想过那说明你还没被实际项目毒打过。我最早接触这个组合是在一个高速数据采集项目里当时用STM32F407做主机想让它同时处理三路ADC采样、跑FFT、再通过串口把频谱数据吐给上位机。结果采样率刚过200kHzCPU占用率就飙到80%以上串口输出稍微频繁一点直接丢包。后来加了一片EP4CE10把ADC采样、数据缓存、FFT运算全部甩给FPGASTM32只负责读结果、做协议封装和人机交互整个系统瞬间就“呼吸顺畅”了。所以这里的“双核”并不是指同一颗芯片里有两个对等核心而是STM32作为控制核FPGA作为数据核两者通过FSMC、SPI或并行总线协同工作。STM32擅长的是流程控制、协议栈、人机界面和复杂状态机FPGA擅长的是并行采样、高速接口、硬实时逻辑和流水线运算。把这两类任务强行塞进一颗芯片要么选带FPGA硬核的SoC比如Zynq要么就得外挂一颗FPGA。前者成本高、开发门槛陡后者灵活、选型自由这也是为什么大量工业采集板、电机控制板、图像预处理板都采用“STM32FPGA”这种分立架构。这个系统适合谁如果你正在做高速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产生一个中断给STM32STM32来读另一页。这样读写互不干扰STM32读数据的速度不会影响采样连续性。乒乓RAM的深度根据STM32读取速度和采样率来算假设采样率50MSPSSTM32通过FSMC每100ns读一个16位数据那么一页RAM至少需要50MSPS × 100ns 5个采样点实际取256或512深度留足余量。3.2 频率测量与TDC时间数字转换频率测量在FPGA里有两种主流方案计数法和等精度法。计数法简单在固定闸门时间内数脉冲个数低频精度差高频精度好。等精度法用两个计数器分别对被测信号和标准时钟计数通过公式计算频率在全频段内精度一致。我一般用等精度法标准时钟取100MHz闸门时间取1ms这样在1Hz到10MHz范围内误差都能控制在0.01%以内。TDCTime-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 2b00, START 2b01, DATA 2b10, STOP 2b11; always (posedge clk) begin case(state) IDLE: if(tx_start) begin state START; tx_line 1b0; 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 {1b0, shift_reg[7:1]}; if(bit_cnt 7) state STOP; else bit_cnt bit_cnt 1; end STOP: if(baud_tick) begin tx_line 1b1; 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数据耗时不到20msCPU占用率几乎为零。// 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在数据没准备好时拉低NWAITSTM32的FSMC会自动插入等待周期不需要CPU干预。4.3 物联网网关中的协议转换与云平台对接STM32做物联网网关典型场景是FPGA采集传感器数据STM32通过串口或FSMC读取后封装成MQTT协议发到巴法云。巴法云的MQTT接入很简单用ESP8266或ESP32做WiFi模块STM32通过AT指令或者SPI接口跟WiFi模块通信。协议转换的重点是数据格式统一。FPGA传上来的原始数据可能是二进制补码STM32需要转成JSON或者键值对格式。比如FPGA测得的频率值是32位无符号整数STM32要转成字符串“freq12345678”。如果涉及GBK转UTF8STM32端可以用现成的转换表或者用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是必备工具。我一般把关键信号都加进SignalTapADC数据、FIFO读写指针、状态机当前状态、FSMC片选和读写使能。触发条件设成FIFO满或者状态机异常跳转这样出问题时能抓到现场。逻辑分析仪用来抓STM32和FPGA之间的接口信号。我一般抓FSMC的片选、读写、地址和数据还有中断握手线。采样率至少设成接口时钟的4倍以上否则会漏掉毛刺。抓到的波形跟时序图对照很快就能发现是建立时间不够还是保持时间不够。6.3 电源和地的处理不能省双核系统里FPGA和STM32的电源要分开处理。FPGA的核心电压和IO电压用独立的LDOSTM32的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 从双核到多核的扩展STM32FPGA的双核架构跑通后扩展方向很多。比如加一片ESP32做WiFi通信STM32通过SPI跟ESP32通信FPGA继续做数据采集形成“FPGASTM32ESP32”三核架构。或者用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。关键是找到性能、成本、开发周期的平衡点。我个人在实际操作中的体会是STM32FPGA这套架构最大的价值不在于性能有多强而在于任务划分的清晰性。控制归控制数据归数据各司其职系统稳定性比单核硬扛要高得多。但前提是接口设计要扎实握手逻辑要严谨否则两颗芯片互相等对方反而比单核还慢。最后再分享一个小技巧在FPGA里留一个“看门狗”计数器如果STM32超过一定时间没有访问FPGAFPGA就自动复位到安全状态避免系统卡死。这个逻辑只需要几十个逻辑单元但关键时刻能救你一命。