STM32F103 + FreeRTOS:芯片没反应?你可能买到“假芯片”了
前阵子调试一块STM32F103C8T6的最小系统板,现象很诡异:J-Link能连上,芯片ID能读到,程序下载也提示成功,可FreeRTOS任务就是跑不起来——点灯任务没反应,串口打印一个字符都不出。起初我以为是优先级配置写崩了、堆栈开小了或者时钟初始化有问题,前前后后折腾了两天,代码翻来覆去改了好几版。
后来实在没辙,把芯片拆下来换了一片,同一个工程直接跑起来了。问题不在代码,在这颗芯片本身——严格来说,是"假芯片"。
这个经历让我意识到,很多人遇到STM32F103没反应的第一反应是查代码、查配置、查焊接,唯独没有怀疑芯片本身。但在ST官方2022年就宣布STM32F103系列进入不推荐用于新设计的状态之后,市面上这颗芯片的翻新片、打磨片、国产替代片鱼龙混杂,芯片本身出问题的概率比很多人想象得高得多。这篇文章就以我自己的实际排查过程为主线,把假芯片的特征、鉴别手段、以及确认芯片没问题之后FreeRTOS上电就跑通的几个前提,一次性讲清楚。
1. 先复盘那颗"不干活"的芯片:问题到底出在哪
先说结论:那颗芯片能被调试器识别,但内部Flash容量、设备ID、内核ID都和ST官方的C8T6参数对不上。这就解释了为什么程序下载成功却运行异常——有一部分指令根本没被正确执行,准确地说,芯片内部可能连完整的Cortex-M3内核都没有,或者Flash容量严重缩水导致程序写入后只有前一部分落进真实存储区。
1.1 程序下载成功和程序正常运行,是两回事
很多人习惯把"下载成功"等同于"芯片没问题",这是个误区。下载成功只代表调试器通过SWD协议和芯片内部的调试访问端口完成了握手,然后往Flash里写了一串数据。这个过程走的是芯片自己的调试接口,就算内核本身有缺陷,只要调试接口还能工作,下载动作就能完成。
但FreeRTOS启动之后就完全是另一码事了。调度器要切换任务栈、要操作NVIC中断控制器、要执行SVC和PendSV异常,这些全靠Cortex-M3内核的底层机制。如果芯片内部用的是兼容性不够好的内核或者Flash容量被改小导致代码溢出,运行阶段就会出各种奇怪问题:死循环、HardFault、任务不切换、外设寄存器写进去读出来不对。
我当时那颗芯片有个很典型的特征:程序下载后,如果只跑一个不带RTOS的裸机点灯,居然也能闪烁。但只要一跑FreeRTOS,必死无疑。原因很简单,裸机程序代码量小,可能就几KB,全部落在了真实Flash范围内;FreeRTOS工程随便编译一下都是二三十KB,超过了假芯片的真实容量,后面的代码其实没写进去,一执行到超出范围的中断向量就直接崩溃。
1.2 为什么假芯片能通过ST-Link/J-Link的识别
这里要解释一下调试器的识别机制。ST-Link或者J-Link连接STM32时,会读取目标芯片的IDCODE寄存器,这个寄存器在芯片出厂时由ST写入,记录了设备型号。但假芯片的厂商在Copy的时候,直接把正品芯片的IDCODE一起抄了过来,或者用一块能响应同样IDCODE的MCU冒充。所以调试器显示"Detect: STM32F103C8"根本不能说明什么,只能说明对方在IDCODE这一层做了伪装。
真正能暴露问题的,是ST官方提供的校验机制。在STM32F103的参考手册里,有几个关键寄存器:DBGMCU_IDCODE(0xE0042000)、设备唯一ID(0x1FFFF7E8起96位)、Flash容量寄存器(0x1FFFF7E0)。正品芯片的DBGMCU_IDCODE值、Flash容量值、唯一ID的年份编码段都有一定规律,而假芯片在这些寄存器上的表现经常会露馅。
1.3 排查第一步:排除一切软件因素
在怀疑芯片之前,我建议你还是先做一遍常规软件排查。顺序如下:
- 检查FreeRTOS的configTOTAL_HEAP_SIZE是否设置合理,太小会导致任务创建失败,但注意任务创建失败在标准库版本的FreeRTOS里有时候不会报错,只是任务不运行。
- 检查启动文件是否正确。STM32F103C8T6是64KB Flash,用的启动文件应该是startup_stm32f10x_md.s。很多人从网上拷工程用的hd.s(大容量),在C8T6上也能编译通过,但实际运行会出问题。
- 检查外部晶振是否起振。FreeRTOS的时基依赖于SysTick,但只要系统时钟正常,SysTick就会正常。如果你用的是HSE外部晶振,晶振没焊好会出现能下载、不能运行的问题。
- 检查PA13/PA14是否被程序意外配置成普通GPIO。SWD引脚被复用后,调试器连接会变得不稳定,甚至出现"连接一次之后再也连不上"的情况。
这四步都排除掉之后,如果芯片还是没反应,再往下查硬件本身。
2. 解剖"假芯片":从内核身份ID到Flash容量的完整验证过程
我在确认软件没问题后,做了下面这套验证流程。每一条都有明确的寄存器依据,不需要贵重的专业设备,一个ST-Link和一个串口模块就能完成。这也是我推荐所有怀疑芯片有问题的同行去做的标准流程。
2.1 第一步:读设备ID和内核ID,核对ST官方定义
STM32F103系列调试接口开放了大量只读寄存器,都能通过调试器直接访问。最常用的是下面几个:
| 寄存器 | 地址 | 作用 | 正品F103C8T6典型值 |
|---|---|---|---|
| DBGMCU_IDCODE | 0xE0042000 | 设备识别码 | 0x20036410(Rev Z)等 |
| Flash容量寄存器 | 0x1FFFF7E0 | Flash大小(KB) | 0x00000040(即64KB) |
| 芯片唯一ID起始 | 0x1FFFF7E8 | 96位唯一ID | 无法伪造的随机值 |
第一步,用J-Link Commander或者STM32CubeProgrammer读一下DBGMCU_IDCODE。如果你拿到的是正品,这里应该是一个和官方枚举值对得上的数字。ST在RM0008参考手册里列出了F103系列各型号的DEV_ID值,0x410是F103中容量系列的标准值,后面跟着的是RevID。如果你的芯片读出来DEV_ID是0x411甚至0x414,那就要高度警惕了——那不是F103C8T6该有的值。
第二步,读Flash容量寄存器。STM32F103C8T6应该读出0x40,也就是64KB。如果你读出来是0x20(32KB)甚至是别的值,可以基本断定这颗芯片不是原厂C8T6,很可能是拿更低容量的型号打磨后冒充的。这一条非常准,因为Flash容量寄存器是出厂固化的,一般造假者不会去改它,改了反而容易露馅。
2.2 第二步:用CoreSight调试组件扫描内核
Cortex-M3内核的ROM表地址在0xE00FF000,调试器可以通过这个地址扫描出芯片内部CoreSight组件的分布。正品STM32F103的ROM表结构里,能看到Cortex-M3 ROM、Debug ROM、DWT、BPU等标准组件。如果你用J-Link的命令行工具执行mem32 0xE00FF000 4,会读到一组特征明显的地址。
假芯片如果用的是其他家的M3或者M0+内核,ROM表的布局会完全不同。我有一个朋友测过一颗国产GD32F103冒充的芯片,它的ROM表里有些地址的排列顺序和ST不完全一致,仔细对比官方手册就能发现差异。对于不熟悉寄存器级的同行,这一步可能有点门槛,但如果你真想确认芯片真伪,这个方法是核弹级的确凿证据。
2.3 第三步:跑一个全内存读写测试
假设前面两步都通过了,还能不能有其他猫腻?有。有些翻新片是正品芯片,但已经经历过高温焊接或长期存储,内部Flash的某个扇区可能已经写不进去了。程序下载时调试器没报错,但运行到那个坏扇区就会卡死。
验证方法是写一小段测试代码,对每一页Flash执行擦除-写入-读取-比对。STM32F103C8T6共有64页,每页1KB。如果某一页写入后读出的数据和写入的数据不一致,说明这页有问题。这种按页遍历的测试能找出绝大部分Flash有坏块的芯片。我见过的情况是,有些翻新片只有后半段Flash坏掉,前半段正常,所以跑小Demo没问题,跑大工程就挂,和FreeRTOS这种动辄二三十KB的场景简直是天作之合。
2.4 第四步:核对唯一ID的年编码段
STM32的96位唯一ID格式公开在应用笔记里,其中包含晶圆坐标、批次号、年份编码等字段。正品芯片的年份编码不可能晚于生产日期,如果一颗"全新"芯片的ID表明它是2015年生产的,那你拿到的多半是库存芯片甚至翻新片。这个方法需要对照ST的应用笔记AN3364,虽然不能直接证明是假货,但可以给你一个采购溯源上的参考。
我把这套流程走完之后,基本可以100%确认那颗芯片是假货。而且它的DEV_ID读出来是0x410不假,但Flash容量寄存器只有0x20——也就是说,这是一颗32KB的芯片打磨成64KB来卖。32KB跑FreeRTOS加一堆任务,编译出来的镜像都奔着40KB去了,怎么可能正常工作。
3. 顺手做一次"芯片心电图":上电时序、复位、时钟与BOOT配置
在把锅甩给芯片之前,有一个环节特别容易被忽略,就是最小系统电路的硬件状态。假芯片问题确实存在,但也不排除有些"没反应"其实是因为电路设计或者焊接工艺造成的。这里我总结一套自己的检查顺序,建议遇到问题都过一遍,成本很低,但能帮你少走弯路。
3.1 电源不是"电压对就行"这么简单
STM32F103需要三组供电:VDD(2.0V~3.6V)、VDDA(模拟供电)、VBAT(备份电池供电)。很多最小系统板把VDDA和VDD直接连一起,问题不大,但关键要看电源的纹波和上电斜率。
用示波器看3.3V的上电波形,如果上升沿坡度太缓(超过几十毫秒),芯片内部的POR(上电复位)电路可能无法正确触发,导致芯片上电后处于一种未定义状态。这时候你按复位键有时候能恢复,有时候不能,表现就是"时而能跑、时而死了"。FreeRTOS对复位时序没有额外的苛刻要求,但如果连裸机都偶尔不跑,先查电源,别查调度器。
还有一个高频坑:有些假芯片的功耗比正品高不少,如果板子用的是低压差线性稳压器,比如AMS1117-3.3,它的最大输出电流是1A,按理说够用。但有些翻新片的内核电路已经被老化损坏,短路电阻下降,上电瞬间电流冲击特别大,电源被拉垮,芯片自然起不来。测一下整板电流,如果待机比你预期的正常值高出两三倍以上,多半有猫腻。
3.2 复位电路和BOOT引脚的状态要板级验证
STM32F103的NRST引脚内置POR复位电路,外部电容不是必须的,但绝大多数官方参考设计都加了100nF左右的电容。如果电容漏电或者虚焊,复位引脚电压会被拉低到门限附近,芯片反复复位,表现出来就是程序下载成功但运行几毫秒后又从头开始,你以为任务没跑,其实是被打断复位了。
BOOT0和BOOT1引脚也要实测电压。正常从Flash启动的要求是BOOT0=0(接地),BOOT1任意。如果BOOT0被外部电路意外拉高,芯片上电后会进入系统存储器引导模式,程序根本不会从你的Flash启动。这个事我在不少自制板上见过,有些是飞线接错了,有些是引脚间距太近焊锡连锡了。串口没输出、点灯没反应、调试器又“一切正常”——就差这一根引脚。
3.3 外部晶振不起振排查法
FreeRTOS工程默认用的是HSE+锁相环倍频到72MHz,或者HSI直接跑。如果用HSE,晶振不起振的典型症状是芯片完全无反应,因为SystemInit里等待HSE就绪会卡死。在标准库的SystemInit函数里,如果你定义了SYSCLK_FREQ_72MHz,系统会一直等HSE就绪,起不来就卡在while循环里。
排查方法:用示波器探头点OSC_IN引脚,正常起振时能看到8MHz正弦波。看不到波形就查晶振负载电容、焊接是否虚焊、晶振本体是否损坏。如果是廉价晶振,建议直接换一个。另一个技巧是,把工程改成HSI启动,如果HSI模式下能跑起来而HSE模式不能跑,那问题基本锁定在外部晶振电路。
注意,假芯片对晶振起振条件更挑剔。因为打磨片的内核可能不是原厂ST的,内部振荡器放大器的驱动能力和正品有差异,配上质量差的晶振就会出现"正品能起振、假芯片死都不振"的情况。我遇到过一颗芯片,在同一个板子上换上正品,晶振正常起振;换回假芯片,示波器上就是一条直线。所以晶振问题也可以反向佐证芯片真伪。
3.4 把工程切成最小验证集
如果上面的硬件检查都没问题,但FreeRTOS仍然不跑,我强烈建议你做一个最小验证集工程:只保留一个LED翻转任务,关闭一切无关外设初始化,关闭中断,关闭低功耗模式,使用HSI内部时钟。这个工程应该控制在10KB以内。
如果最小验证集能跑,说明芯片基础功能正常,问题在复杂工程里。这时再逐步加回外设、加回任务,二分定位。如果最小验证集都不跑,那芯片本身或者四周围电路一定有问题,这时候再走芯片真伪鉴别流程。这个策略能帮你把“软件问题”和“硬件问题”快速切分,不至于在一个可能有问题的主板上反复编译两三天。
4. 正品芯片无误之后:FreeRTOS上电就跑通的几个前提
确认芯片是正品之后,接下来才是真正考验FreeRTOS工程能力的时候。同样的源码,在正品芯片上能不能一次跑通,取决于几个关键配置。这些配置我在多个项目里反复踩过坑,整理成清单,建议对照检查。
4.1 FreeRTOS的启动文件与中断向量优先级设置
FreeRTOS在Cortex-M3上正常运行,依赖两个核心中断:SysTick(时基)+ PendSV(任务切换)+ SVC(启动第一个任务)。这三个异常都在arm架构内部,不受外部外设影响。
其中最容易出错的是NVIC中断优先级分组。Cortex-M3支持最多8位优先级,但STM32F103只实现了4位,也就是16级优先级。FreeRTOS的configKERNEL_INTERRUPT_PRIORITY和configMAX_SYSCALL_INTERRUPT_PRIORITY这两个宏的值必须对应到硬件可用的优先级片段上。
在STM32F103的标准库工程里,通常调用NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4),也就是全部4位都用作抢占优先级,没有子优先级。此时硬件优先级范围是0~15,数值越小优先级越高。FreeRTOS的要求是:
- configKERNEL_INTERRUPT_PRIORITY必须设置为最低优先级,即15。
- configMAX_SYSCALL_INTERRUPT_PRIORITY建议设置为5或更低(数值更大),才能保证FreeRTOS的临界区不被外部中断干扰。
如果你用了别的分组方式,比如分组2,那么优先级分布是2位抢占+2位子优先级,情况会复杂很多,F103上直接建议用分组4。
4.2 堆大小与任务栈分配:不能拍脑袋
FreeRTOS的任务栈分配是一个需要算清楚的事情,不是随手填一个256或者512就完事。每个任务创建时会从FreeRTOS的堆里分配任务栈,任务栈大小以字为单位(4字节)。
一个典型的计算方法是:先估算任务里用到的最大局部变量数组和函数调用链路的深度。比如你的任务里有一个char buf[128]的局部数组,那就是32字,再加上函数调用可能用到的栈帧空间,包括printf这类可变参数函数的栈消耗,一个任务至少需要128字到256字才稳妥。
而configTOTAL_HEAP_SIZE这个值决定所有任务栈和内核对象能用的总内存。STM32F103C8T6只有20KB SRAM,通常在工程里设为12KB到15KB比较合理。如果你开三个任务,每个任务256字(1KB),加上队列、信号量、定时器等内核对象,12KB的堆完全够用。太高了会导致链接失败或启动时堆初始化失败。
要注意的是,FreeRTOS自带的heap_4.c实现里,堆大小定义在configTOTAL_HEAP_SIZE,但底层实际利用的是静态数组ucHeap[configTOTAL_HEAP_SIZE],这个数组一般放在.bss段。如果编译后发现RAM超了,优先减小堆大小而不是削减任务栈,因为任务栈不够的后果是任务跑飞,而堆不够的后果是创建任务返回NULL,至少还在可控范围。
4.3 标准库版本的SysTick处理
STM32F103最常见的是标准外设库+FreeRTOS的组合。这里有一个很经典的坑:你在其他外设工程里可能会调用SysTick_Config()函数来产生毫秒延时,但如果同时跑FreeRTOS,SysTick的中断服务函数会被FreeRTOS接管,你再去配置SysTick或者修改它的中断优先级,等于和调度器抢时钟源。
正解是FreeRTOS里的延时用vTaskDelay(),不要用HAL_Delay()或者自己写的Delay_ms()循环。因为在FreeRTOS调度器启动后,SysTick中断已经被FreeRTOS的xPortSysTickHandler()占据了。你如果又写了一个SysTick_Handler中断函数,两个Handler同时存在,链接器会报重复定义,或者只有其中一个生效。信号灯、点灯任务这类短延时,用vTaskDelay配合时间片轮转就够了。
4.4 中断服务函数的FreeRTOS语义
在FreeRTOS里,中断服务函数里调用FreeRTOS API有严格限制。比如在UART接收中断里,如果你调用了xQueueSendFromISR(),必须在退出中断前调用portYIELD_FROM_ISR()来判断是否有更高优先级的任务被唤醒。很多人在裸机程序里习惯直接在中断里做逻辑处理,移植到FreeRTOS后还保留这种风格,就会出现很诡异的现象:外部中断来了,标志位置了,但对应的处理任务迟迟不执行。
这是因为在Cortex-M3上,FreeRTOS利用PendSV异常实现上下文切换。如果中断里没有触发PendSV,调度器就一直运行当前任务,即使你已经把信号量给了更高优先级的任务,它也不会真正切换过去。必须调用portYIELD_FROM_ISR(pxHigherPriorityTaskWoken)执行一次上下文切换请求。
4.5 启动文件与FreeRTOS的初始化顺序
一个常见的错误是在main函数里先把所有外设都初始化完,再调用vTaskStartScheduler(),而外设初始化里又开启了和FreeRTOS互斥的全局中断。标准库的默认定时器初始化一般不会开全局中断,但如果你在创建任务之前就配置了UART中断并且NVIC_EnableIRQ()了,中断可能在调度器还没有完成基础初始化时就触发,导致不可预期行为。
推荐的做法是:main函数里先做时钟初始化、GPIO初始化和基础外设初始化,然后创建任务,最后才调用vTaskStartScheduler()。确保在调度器启动之前,FreeRTOS需要的SVC和PendSV异常处于默认状态,不要有任何外部中断在此之前触发。
5. 从假芯片事件里沉淀的采购与调试清单
经历了这次假芯片风波后,我把采购和调试流程都沉淀成了固定动作。每次新来一批芯片,我先抽几颗做快速验证,用不上全套鉴别流程,但几个关键点必须过。
5.1 采购端的快速筛查
| 检查项 | 操作方法 | 通过标准 |
|---|---|---|
| 外观 | 看激光打标、引脚是否氧化 | 字迹清晰、无打磨痕迹 |
| 调试器识别 | ST-Link连接,读IDCODE | 与官方参数一致 |
| Flash容量 | 读0x1FFFF7E0 | 0x00000040(C8T6) |
| 唯一ID | 读0x1FFFF7E8起12字节 | 随机且不重复 |
| 实测电流 | 空载3.3V供电 | 待机电流在合理范围 |
| 烧录大镜像 | 下载一个带FreeRTOS的Demo | 运行稳定不跑飞 |
这个表格我打印出来贴在工位上,新到芯片就对照着过一遍,十分钟内完成。尤其是Flash容量这条,简直是假芯片照妖镜。
5.2 调试FreeRTOS跑飞问题时的检查顺序
当你确定芯片真伪没问题,代码还是跑飞,我的调试顺序是:
- 开启FreeRTOS的堆栈溢出检测功能,把
configCHECK_FOR_STACK_OVERFLOW宏设为2,然后在vApplicationStackOverflowHook里点亮一个独立的警示LED或者设置全局标志位。这是定位任务栈溢出最快的手段。 - 用
vTaskList()把所有任务的运行状态和栈高水位线打印出来,通过串口输出到PC端。这个函数会显示每个任务的栈剩余最小值,如果某个任务的高水位线逼近满值,就加大那个任务的栈。 - 检查中断优先级分组和FreeRTOS的优先级配置是否匹配。不匹配时FreeRTOS会静默死掉,调度器好像卡住一样。
- 检查是否在中断上下文里调用了
xQueueSend(非FromISR版本),这会直接触发高优先级中断下的死锁。 - 最后再怀疑芯片。用前面说的Flash容量读取法快速排除,别一开始就花一整天去调代码。
5.3 测试所有功能前,先做一遍长时间稳定性测试
很多问题不是上电就能发现的,需要跑一段时间才暴露。FreeRTOS的多任务系统尤其需要长时间运行来暴露内存碎片、任务优先级反转、资源竞争等问题。我建议在正式交付前,至少连续运行48小时,监控温度、电流和关键串口日志。我那次假芯片问题,其实早该在收到货后的抽检环节就被拦截下来,结果因为偷懒跳过了静电测试和Flash容量检查,直接浪费了两个人天。
6. 一颗芯片引发的思考:别急着优化代码,先确认硬件说了实话
这次经历让我对"程序下载成功"这个信号彻底失去了以前的信任感。调试器说连接正常,只代表调试链路通;程序下载完成,只代表一串字节写入了某个存储区域;甚至芯片的型号识别,都可能只是一层伪装。真正的硬件是否在按Cortex-M3的规定执行每一条指令,只有运行起来才知道。
有人可能会说,假芯片这种事情毕竟少见,何必这么较真。但STM32F103系列因为生命周期长、用量大,是目前市场上翻新片和打磨片的重灾区。尤其从2022年之后,原厂新片越来越少,渠道里流通的大多是库存料或者回收料。你在淘宝、闲鱼甚至某些授权代理商不到万分之一的概率碰到假货不会心疼,但这个概率落在项目交期上就是灾难。
个人建议,做产品前先花十分钟做一次芯片身份验证。调试FreeRTOS无反应的故障时,检查软件配置和电路设计固然重要,但一定记得在怀疑清单里加上一条:确认你面前这颗芯片,真的是它标称的那颗芯片。有时候,问题不在代码里,而在芯片的身份里。
最后分享一个小经验:如果你手头没有高档的芯片测试仪,最简单可靠的假芯片鉴别方式就是读Flash容量寄存器。我后来习惯了在新项目建工程的时候,第一步就写一段代码,把芯片型号参数和Flash容量通过串口打印出来,既方便调试,也顺便确认芯片身份。这个习惯帮我拦下了三次假芯片事故,每次都省掉了后面铺天盖地的排查时间。