上电的一瞬间,板上5V、3.3V、1.2V的电源指示灯全亮了,MCU的调试串口也正常打印了启动日志,一切看起来都很正常,唯独FPGA没醒过来:DONE指示灯不亮,业务IO全部处于高阻,整块板子像被抽走了主心骨。这是我调第一块MCU与FPGA混合电路板时遇到的上电启动问题,更恼火的是,同一份FPGA逻辑通过JTAG下载器烧进去一切正常,仿真也全过,可一旦由MCU在上电时去配置,就出现了“看心情工作”的随机故障。后来我把整个启动链路的每个环节都抓了一遍波形,才发现这类问题几乎没有“器件损坏”的可能,全是电源时序、复位释放、配置握手这三件事没协调好。这篇文章就把完整的排查过程、根因和解决路径整理出来,给正在被MCU+FPGA混合电路上电启动问题折磨的工程师一个可以照做的参考。
1. 故障现象:电源正常,FPGA却没醒过来
1.1 板级架构与为什么让MCU去配置FPGA
先交代一下这块板子的背景。MCU用的STM32F103ZET6,FPGA用的一片中等规模的Spartan-6,FPGA负责LVDS接口转换、串口扩展和一部分信号预处理。设计时没有给FPGA单独配SPI Flash,而是把配置文件放在MCU侧NOR Flash里,由MCU上电后通过GPIO模拟被动串行(Passive Serial,Xilinx叫Slave Serial)直接加载。选这个方案有三个现实理由:
- 少一颗SPI Flash,降低BOM成本和PCB面积;
- MCU侧Flash方便在线升级FPGA镜像,以后改配置不用动硬件;
- 板子上本来就有MCU在跑业务,加载FPGA只是多写一段启动代码的事。
但坏处也明摆着:FPGA的启动进度被绑在MCU的启动速度上,MCU只要在复位、时钟初始化、GPIO默认状态任何一个环节掉链子,FPGA就会上电失败。这个取舍是我后来复盘时最想提醒大家的——被动配置的灵活性是用启动时序的控制权换来的,你必须对MCU上电后的每一毫秒行为有绝对把握。
1.2 三个最能迷惑人的故障表现
故障不是一次就定型的,前后表现出三种形态,每种都极具迷惑性:
- DONE指示灯不亮,按复位键偶尔能恢复。因为复位会让MCU重新跑一遍配置流程,相当于一次重试,哪次竞态被绕过去了就成功了,所以你会误以为是“偶发损坏”。
- 外设数据错乱。某个版本下FPGA看起来“启动了”,IO上有信号,但数据完全对不上。这种情况最坑——如果配置流程里没做DONE检查,你会一直怀疑FPGA内部逻辑写错了,反复改RTL、改约束,浪费好几天。
- 环境适应性差。温度低一点、供电波动大一点,故障概率明显升高。这说明整个启动链路的时序余量不足,问题本质是电平边界和时序窗口的“临界状态”,不是一次干净的逻辑bug能解释的。
这里说一个我用血泪换来的判断经验:如果FPGA逻辑在仿真里全通过、JTAG下载也正常,唯独MCU上电配置时才出问题,那不要怀疑FPGA代码,问题几乎必定在配置链路。因为JTAG下载验证了FPGA器件本身和你的逻辑没问题,剩下的变量只剩下MCU侧配置流程和硬件连接。这个结论能帮你把排查范围直接砍掉一半。
2. 先摸清电路里的“接力关系”:电源、复位、配置通路的布局
2.1 电源树与上电分工
上电启动本质是一场接力赛:电源爬坡、复位释放、POR初始化、配置加载、业务运行,每一棒交接都不能掉链子。先把电源树画清楚:5V适配器进来,经过DCDC出3.3V给MCU和FPGA的VCCIO,再经过LDO出1.2V给FPGA的VCCINT。理论上VCCINT要早于或至少同时于VCCIO稳定,我实测爬坡没什么大问题,3.3V和1.2V基本同时到齐,误差在几十毫秒内。所以电源轨本身不是主要矛盾,但这是排查时必须验证的第一步——不排除干净,后面所有猜测都是空中楼阁。
2.2 复位、时钟与配置通路
再理清四类关键网络的关系:
- 复位:板上用一个复位芯片同时复位MCU和FPGA,这个设计选择后来成了隐患源头之一;
- 系统时钟:MCU使用外部晶振;FPGA的业务时钟来自独立有源晶振;配置时钟CCLK/DCLK由MCU的GPIO翻转产生,不属于任何时钟域管理范围;
- 配置信号:包括PROG_B/nCONFIG、INIT_B/nSTATUS、DATA、DCLK、DONE;
- 业务信号:MCU和FPGA之间通过I2C、SPI、UART等接口交互。
被动配置的“被动”两个字要拆开理解:FPGA不会自己去找数据,它只是守在那等外部控制器把数据送进来。所以整个启动链条的主动权完全在MCU手上,MCU在什么时间点去碰FPGA、以什么顺序碰,决定了上电启动的成败。
2.3 一张关键网络风险清单
排查之前,我把所有和上电启动强相关的网络整理成了一张清单,每条都要回答“上电默认状态是什么、最容易出什么幺蛾子”:
| 网络 | MCU侧 | FPGA侧 | 上电默认状态 | 最容易出的问题 |
|---|---|---|---|---|
| RESET# | 复位芯片输出 | 复位芯片输出 | 从低变高 | MCU先解除复位,FPGA的POR尚未完成 |
| PROG_B/nCONFIG | GPIO输出 | 配置复位/触发 | GPIO浮空 | 浮空毛刺被FPGA误读为配置命令 |
| INIT_B/nSTATUS | GPIO输入 | 配置初始化状态 | 外接上拉,低表示初始化中 | MCU不看它就发数据,必然失败 |
| DCLK | GPIO输出 | 配置时钟输入 | 无时钟 | 翻转太快或时钟抖动 |
| DATA | GPIO输出 | 配置数据输入 | 不定 | 位序错误,配置流校验失败 |
| DONE | GPIO输入 | 配置完成状态 | 外接上拉,低表示未完成 | 漏检,导致“假成功” |
这张表后来我在画每一块带FPGA的板子时都会对着检查一遍。你可以先存下来用,再往里面补自己项目的特殊信号。
3. 第一轮排查:电源爬坡与复位释放为什么不能同步
3.1 用四通道示波器抓上电波形
第一轮排查的目标是验证“电源和复位是否存在硬伤”。示波器设置为探头×10、时间轴20ms/div、触发设在RESET#上升沿,同时抓5V、3.3V、1.2V和RESET#。波形显示电源本身没有明显异常,但有一个容易被忽略的细节:RESET#释放的时候,3.3V已经稳定,但FPGA内部的上电复位(POR)过程是否结束,示波器在普通电源通道上看不出来——得看INIT_B/nSTATUS引脚。于是我把逻辑分析仪挂在INIT_B上,发现复位释放后的几百微秒内,INIT_B仍然维持低电平,说明FPGA还在做内部初始化。如果MCU在这个窗口里去配置,数据就是在敲一扇还没打开的门。
3.2 复位同时拉两边的坏处
复位芯片同时输出给MCU和FPGA,设计本意是让两边同步复位、时序简单,但对被动配置架构来说恰恰相反:MCU解除复位后要从外部Flash读固件、初始化时钟树和引脚,这个过程至少几十到几百毫秒;而FPGA的POR完成后就开始等待配置。MCU越晚出现,中间这段“无人监管”的窗口就越长,所有配置引脚上的毛刺都有可能被FPGA当成真正的配置信号。实测中我就在PROG_B上抓到过几微秒宽的负向毛刺,仔细排查后发现来源正是MCU GPIO在固件接管之前的高阻浮空状态。这个阶段出现的问题不是某个器件的逻辑错误,而是整个复位链路的电平状态在配置窗口里失管了。
3.3 第一轮结论
第一轮的结论是:电源爬坡本身没有太大问题,问题出在“复位同时释放”导致MCU的活动窗口和FPGA的等待窗口错位。但这一步还不至于致死——FPGA在未配置状态下会持续等待配置,真正致命的是配置握手环节的时序参数。带着这个疑问,我进入第二轮排查。
4. 第二轮排查:配置握手中的三个“隐形参数”
4.1 参数一:GPIO初始态,上电瞬间的“看不见的手”
STM32这类MCU在复位后、用户代码配置GPIO之前,绝大多数引脚处于浮空输入状态。也就是说,在固件跑起来的最初那几十毫秒里,连接PROG_B/nCONFIG的引脚既不在输出状态,也没有可靠的高或低电平,它是一根高阻线。高阻引脚在PCB上相当于一根天线,旁边电源走线的翻转、甚至很轻微的电磁骚扰,都能让它的电平来回抖动。
FPGA对配置引脚在这个阶段非常敏感——配置逻辑在内部上电复位完成后会自动检查外部配置引脚的静态状态,任何不期望的跳变都可能被解释为一次配置触发。我当时无法百分之百证明那次毛刺被FPGA捕获了,但一个明确的事实是:设计规范里FPGA配置引脚不允许出现悬空状态。这一条“不符合规范但暂时无害”的隐患,正是以后随机故障的温床,必须消除。这类配置引脚的默认电平问题,在MCU+FPGA混合电路里是最常见、也最容易被忽略的上电启动坑。
4.2 参数二:PROG_B/nCONFIG低电平宽度与INIT_B握手
FPGA配置流程绝不是“把PROG_B拉低再拉高,然后发数据”这么简单。以Altera/Intel的Cyclone IV为例,nCONFIG拉低后需要维持足够长的时间,让内部配置逻辑彻底复位;拉高之后还要等nSTATUS变高,表示FPGA已经准备好接收配置数据,之后才能开始送DCLK/DATA。Xilinx的Spartan-6是同样的逻辑:PROG_B拉低有最小宽度要求,之后要等INIT_B拉高再开始送数据。
我当时的配置代码只做了一次“GPIO_ResetBits然后GPIO_SetBits”,两次操作间隔只有几十个时钟周期,远小于FPGA手册对复位引脚宽度的要求。很多初学者写的配置驱动都是这个风格,程序跑得飞快,看起来在“配置”,实际上FPGA根本没有进入配置状态。不同被动配置变种(Slave Serial、Slave SelectMAP等)的信号名略有差异,但核心握手原则完全一致:先复位配置逻辑,再确认就绪,最后才送数据流。这一步不能凭感觉,必须照着目标FPGA手册的时序图写代码,确保每个高/低电平的保持时间留出充足的余量。
4.3 参数三:字节位序和DONE回读
第三个坑比前两个更隐蔽,也最典型:位序。被动串行配置对“每个字节里先送哪个bit”有严格规定,不同厂商、不同工具链、甚至不同配置模式(串行/并行)下的位序可能完全不同。以我当时用的Xilinx Spartan-6为例,工具生成的配置流按字节取位时通常约定为LSB first,也就是每个字节先从bit0送起;而我的配置驱动里一个循环从bit7向下发到bit0,等于把整个配置流按字节位序全部反转了。配置流一旦位序错误,FPGA在校验配置数据时会失败,DONE永远拉不高。
更雪上加霜的是,程序里压根没有检查DONE。发完所有配置数据后,MCU直接认为任务完成,开始跑业务代码;FPGA那边一直是未配置状态,IO全高阻,整个系统表现出来就是“FPGA坏了”。后来我在程序里加上DONE回读,才发现每一次配置其实都是失败的,问题就是位序。把配置流按位反转之后,DONE立刻稳定拉高,故障消失了。
这里的教训非常直接:配置流程里强制要求发送完所有字节后读DONE/CONF_DONE,为高就继续业务,为低就按“重新拉低PROG_B、等待INIT_B、重新发送”的完整重试流程再来,最多重试三次。至少不要出现“发了就当成功”的裸奔设计。
提示:排查位序问题时,把逻辑分析仪挂在DCLK、DATA、DONE上,先抓一帧“JTAG配置成功时的波形”和“MCU配置失败时的波形”对照,位序错误在数据帧里几乎一眼就能看出来。这比反复改代码重新综合效率高得多。
4.4 逻辑分析仪上看到的实际事实
第二轮排查的证据来自一组很干净的逻辑分析仪记录。把探头挂在DCLK、DATA、DONE上,抓到的内容:
- DCLK出现了几千个上升沿,DATA上也有数据在翻转,看起来时序在正常“跑”;
- 但整个数据流发送结束后,DONE一直维持低电平,FPGA始终没给出成功应答;
- 把配置流按位反转后重测,DONE在最后一个字节接收完的边沿立即拉高,时间对齐得干净利落。
前后两张波形摆在一起,结论已经不需要再争论了:配置流位序错误+缺少DONE回读,就是配置失败的直接原因。这一轮的排查也再次验证了一个道理——被动配置失败时首先要怀疑的不是FPGA器件、不是配置文件、不是PCB焊接,而是MCU侧时序代码里那些不起眼的默认值。
5. 根因汇总:不是一个大bug,而是三个小问题叠加
5.1 把排查结果整理成一条证据链
修复之前,我把所有现象和根因整理成了一套完整的证据链,这个思路值得分享出来:
- 根因一:复位释放后,MCU尚未接管GPIO,PROG_B/nCONFIG处于高阻浮空状态,配置引脚存在被干扰的物理条件。证据是复位后2ms内抓到了PROG_B上的负向毛刺。
- 根因二:配置驱动中PROG_B/nCONFIG低电平宽度严重不足。跟手册时序对照后确认,代码里保持低电平的时间不到手册要求值的几十分之一,这导致FPGA在多次上电场景下根本没有进入配置流程。
- 根因三:配置流字节位序与FPGA要求不一致,且配置驱动不检查DONE就能“假装成功”。证据是位序反转后DONE稳定拉高,且加上DONE回读后能在代码里实时抓到失败状态。
三个根因都不算高深,但它们在启动链路上相互叠加,产生了非常混乱的外部表现。
5.2 为什么是“随机故障”而不是“必然失败”
三个问题单独看,每一个都不一定会导致上电失败:毛刺出现有随机性,某个温度、某块PCB布局下可能不出现;PROG_B低电平宽度不够在某些时候也能侥幸触发配置;位序错误本身会必然失败,但由于例程里没有DONE回读,失败被完全掩盖了。三件事叠在一起,表现出来就是极其迷惑的“看心情工作”——今天正常明天不正常,这块板子正常那块不正常,根本摸不到规律。
这也解释了为什么网上关于MCU+FPGA上电启动的讨论总是各执一词、难以统一。因为这类问题往往不是单一根因,而是多个“小毛病”在启动链路里共振。你只修掉其中一个,故障概率可能下降但不根治,过几天换个环境又冒出来。所以排查时一定要把所有可疑节点都列出来,全部清干净,不能因为找到一个原因就收工。
5.3 一个重要的排查视角转变
回头复盘,真正拖慢我进度的不是技术本身,而是排查的重心一开始就选错了:我不断改FPGA内部逻辑、反复重新综合、跑了无数遍仿真,却忽略了一个最基本的事实——FPGA可能根本没有被配置成功。如果从一开始就在MCU程序里加上DONE回读,并把这个状态通过串口打印出来,问题可能在半天内就能定位到配置链路,而不是被“FPGA逻辑bug”这个错误方向带进死胡同。善用状态输出和打印信息,是所有上电启动类问题排查的第一原则——先让系统明确告诉你“配置成功了吗”,再谈其他。
6. 解决路径:硬件固定初始态、软件重排时序、必要时引入CPLD
6.1 方案A:硬件层先堵住“初始态不确定”
硬件修复最优先,因为软件再严谨也管不了MCU固件启动之前那段真空期。
第一件事,给配置引脚加外部电阻固定默认电平。PROG_B/nCONFIG接一个10kΩ上拉到VCCIO,保证FPGA在上电后、没收到明确配置命令之前一直保持安全的高电平,即使MCU GPIO呈高阻也不会被干扰拉低。同时,DONE/CONF_DONE引脚也不能省掉上拉电阻——它是开漏输出,不加外部上拉的话,即使FPGA配置成功,MCU也读不到一个可靠的高电平。这一步成本只有两颗电阻,但把最玄学的随机性直接掐掉了。
第二件事,在复位释放链路上做加固。如果复位芯片输出直接同时接MCU和FPGA,建议改成两路控制:FPGA先解除复位,MCU晚解除复位。最快速的临时修复是在复位芯片到MCU复位脚之间加RC延时,R取10kΩ、C取10µF,时间常数τ=100ms,按3τ估算大约300ms后MCU才解除复位;量产设计里更推荐选用带可调复位延时的复位芯片,或者用MCU的GPIO通过简单逻辑去管理FPGA的复位时序,具体根据产品成本来取舍。
注意:RC延时的释放沿比较缓,而MCU对复位引脚的阈值并不是特别陡峭,这个方案只适合快速验证和原型修复。正式产品里尽量用带固定/可调延时的复位IC,或者干脆用一小颗可编程逻辑来解决,只要“MCU启动比FPGA完成POR更晚”这个关系确定,启动链路的余量就回来了。
6.2 方案B:软件用状态机重排配置流程
硬件固定好默认电平之后,软件侧把配置流程重写成一个状态机。原来的配置驱动是一口气顺序执行,改了之后变成带握手检查、带失败重试的完整状态流。核心伪代码如下:
enum { CFG_IDLE, CFG_START, CFG_WAIT_INIT, CFG_STREAM, CFG_CHECK_DONE, CFG_RETRY, CFG_OK, CFG_ERR } state; void fpga_load(void) { int retry = 0; while (retry < 3) { // 1. 拉低 PROG_B/nCONFIG,保持足够宽的复位脉冲 CFG_RESET_PIN = 0; delay_ms(2); // 保持低电平至少2ms,远超一般手册要求 CFG_RESET_PIN = 1; // 2. 等待 INIT_B/nSTATUS 变高,表示FPGA完成内部初始化并准备好接收数据 while (CFG_INIT_PIN == 0) { if (timeout_ms(200)) goto retry_label; } // 3. 按正确位序逐字节发送配置流 // 位序务必用逻辑分析仪和工具生成的预期波形对照过再定 for (i = 0; i < bitstream_len; i++) { for (b = 0; b < 8; b++) { CFG_DATA_PIN = (bitstream[i] >> b) & 1; pulse_dclk(); } } // 4. 回读DONE,确认配置成功;失败则拉低配置引脚重新来过 if (CFG_DONE_PIN == 1) { state = CFG_OK; return; } retry_label: retry++; state = CFG_RETRY; } state = CFG_ERR; }代码本身只做示意,重点是三个强制步骤:PROG_B/nCONFIG的低电平宽度必须足够、发送前必须等INIT_B/nSTATUS就绪、发完必须确认DONE。位序不要靠猜,用逻辑分析仪抓一次DCLK/DATA,和工具生成的预期波形逐字节对比,成功与否一目了然。这套状态机完成后,我把它放进MCU上电启动主状态机里,配置结果通过串口打印出来,后续每一次上电都能在日志里看到配置成功的信息。
6.3 方案C:FPGA较大或数量较多时,可以考虑CPLD做配置管家
方案B适合单板调试和中小规模产品。但如果FPGA比较大、配置文件有多个版本、上电时间要求苛刻,或者MCU在启动初期有几百毫秒级别的不可控窗口,我会直接加一颗小CPLD做配置时序管理。CPLD上电即跑,不依赖MCU固件的初始化速度,专门负责拉低PROG_B/nCONFIG、监听INIT_B/nSTATUS、产生DCLK、发送数据、确认DONE。MCU只给CPLD几个命令位,比如“加载版本A”“加载版本B”“读取配置状态”。这样MCU启动多慢都不影响FPGA配置,两个异质器件之间彻底解耦。
这个方案看起来多了一颗器件,但它把整个启动链路的“时序责任”集中到了一个可编程、可仿真、可验证的地方。对于量产项目和多FPGA镜像管理的场景,这种投入非常值得。
6.4 方案对比与我的选择
| 方案 | 改动成本 | 可靠性 | 调试难度 | 适合场景 |
|---|---|---|---|---|
| 方案A:外部电阻+RC延时 | 极低 | 中 | 低 | 快速修复、原型验证 |
| 方案B:软件配置状态机 | 低 | 高 | 中 | 常规产品、中等规模集成 |
| 方案C:CPLD配置管理器 | 中高 | 很高 | 中高 | 多镜像、大型FPGA、苛刻上电时间 |
我当时选择了方案A+方案B的组合。原因很简单:板子上只有一片中等规模的Spartan-6,MCU本来就在管理配置流程,加两颗电阻、改一版配置状态机就足够了,不需要引入额外器件。如果这个项目继续发展,需要支持多版本FPGA镜像和在线无损升级,我会毫不犹豫转向方案C。
7. 回归验证与设计规范固化:从随机故障到稳定复现再到彻底消除
7.1 搭建自动上下电测试环境
改完代码和硬件之后,我没有手动按几十次电源就当验证完成,而是搭了一个自动测试环境:用一个程控电源给板子供电,脚本控制电源开关,每次开关电之间留3秒等待,上电稳定后读取FPGA侧DONE状态和MCU串口打印的配置结果,跑了500次。测试分三类:冷启动(断电间隔较长)、热启动(快速通断)、随机断电(在DONE拉高前后随机打断再上电)。这个环境花了我一个晚上搭建,但之后每做一次改动都能直接拿来回归,省下的时间远远超过搭建成本。
7.2 修复前后的数据对比
修复前我手动测了100次,失败了12次,没有任何明确规律;修复后又跑了500次自动上下电,全部成功。除了成功率,另一个值得关注的指标是DONE拉高时间——它直接反映了配置流程的时序余量:
| 状态 | 样本量 | 成功率 | DONE拉高时间 | 最坏情况 |
|---|---|---|---|---|
| 修复前 | 100次手动 | 88% | 50~180ms随机分布 | 卡死、无DONE |
| 修复后 | 500次自动(冷/热/随机断电) | 100% | 63~68ms | 70ms |
从数据能清楚看到,修复前的成功全靠运气,修复后的DONE拉高时间收敛在一个窄窗口里,说明配置链路的时序余量被真正补上了。这种“从随机抖动到稳定收敛”的变化,就是硬件启动链路修好的最直观证据。
7.3 把这些教训固化成设计规范
这次排查结束之后,我把所有踩过的坑整理成一张checklist,之后画原理图、评审PCB、写底层启动代码时都对着过一遍:
| 检查项 | 设计要求 |
|---|---|
| 配置脚默认电平 | PROG_B/nCONFIG必须通过外部电阻固定为安全电平,禁止悬空 |
| DONE上拉 | DONE/CONF_DONE必须加外部上拉电阻,确保能被MCU稳定读到 |
| 复位释放顺序 | MCU解除复位应晚于FPGA完成POR,建议使用带延时的复位方案 |
| PROG_B最小低电平时间 | 必须按FPGA手册时序图确认,软件代码里留出充足余量 |
| INIT_B握手 | 发送配置数据前必须等待INIT_B/nSTATUS变高 |
| 配置流位序 | 必须与工具生成的格式一致,并用逻辑分析仪实测验证 |
| DONE回读 | 发送完配置流必须确认DONE,失败要重试并向上层上报状态 |
| 上电测试 | 至少300次自动上下电回归,记录DONE拉高时间的一致性 |
现在我做混合电路的上电启动设计时,第一件事不是写功能代码,而是先把复位、配置脚的电平和时序定义清楚,再动手布线、写驱动。这一套东西做完之后,我再也没有遇到过“FPGA上电看心情工作”的故障。