STM32F103假芯片导致FreeRTOS死机?系统化排查指南
2026/9/8 8:53:45 网站建设 项目流程

上周一个做门禁项目的朋友丢过来一段日志:“STM32F103跑裸机好好的,一上FreeRTOS就没反应,帮我看看是不是代码问题?”我第一次看到也是怀疑代码,裸机正常、RTOS死机,这种坑多半出在中断配置或任务栈上。但等他发来芯片实物照片后,我反倒先问了一句:你这片子哪买的?照片里芯片顶面的丝印字体发虚,ST的logo做得歪歪扭扭,批量丝印的痕迹一眼就能看出来不对。

这颗芯片最终被证实是翻新打磨片。这件事让我想把整个排查过程整理出来,因为“买到假芯片”在STM32F103这个型号上太常见了,尤其是配合FreeRTOS使用时,故障表现极具迷惑性——它不会一上电就烧,而是让你在代码里翻来覆去找问题,最后才发现是芯片本身有问题。这篇文章会从假芯片的来源、外观初筛、寄存器级身份校验,到FreeRTOS场景下的系统化排查思路,完整走一遍,希望能帮被同样问题卡住的朋友少走弯路。

1. 假芯片不是段子:它们从哪来,又为什么会让你怀疑人生

先说结论:STM32F103C8T6是假芯片重灾区,这是市场结构决定的。这颗料出货量巨大、价格便宜、应用范围广,而且封装是LQFP48,引脚外露,给翻新和打磨留下了很大的操作空间。我不是说所有低价的F103都是假的,而是说你在非授权渠道买到假货的概率,远比你想象的高。

1.1 市场上最常见的三种“伪F103”

我这些年经手过、帮人鉴定过的假片,大致能分成下面三类,特征差异非常大。

第一种叫打磨重印片。把旧板上拆下来的原装STM32芯片用打磨机把顶面丝印磨掉,再重新印上STM32F103C8T6的字样。这种片子的最大问题是:它本体可能是个更小容量的型号,比如STM32F103C6T6(Flash只有32KB),甚至可能是别的品牌、别的型号的单片机,被打上C8T6的标。你把它当64KB的C8用,程序小的时候没事,程序一旦超过实际Flash容量,烧录器可能提示成功,但运行起来就是各种随机死机。更坑的是,有些打磨片连Flash容量都能伪装,实际容量不够时,写入会失败或者写进去了读出来是错的。

第二种是降级片。芯片本身就是STM32,但不是C8T6,而是C6T6、C4T6这些低配型号,被重新打标冒充C8T6。这类芯片的Flash容量实际只有32KB或16KB,RAM也可能只有4KB或6KB。裸机点个灯、跑个串口完全没问题,因为你根本没用到那么多Flash。但FreeRTOS一上来就露馅了:内核代码、任务栈、堆内存、各种中间件,轻轻松松把Flash和RAM撑爆。最迷惑的是,编译时Keil默认按C8T6的64KB Flash来链接程序,会警告代码段溢出或不会警告但运行时出错,你根本想不到是Flash缩水了。

第三种是国产兼容片冒充。最常见的是GD32F103C8T6打上ST的标。GD32和STM32在寄存器层面大体兼容,但内核是M3的变种,运行时钟、Flash访问时序、部分外设行为有差异。有些GD32冒充片在跑裸机GPIO、串口、ADC时表现几乎一样,但跑FreeRTOS时会有一些奇怪的现象,比如任务切换偶尔卡死、定时器漂移,原因在于GD32的Flash执行效率和中断延迟跟原装ST有区别。

1.2 为什么“裸机正常、FreeRTOS死机”几乎是假芯片的照妖镜

很多人不理解:为什么裸机好好的,上RTOS就死?是不是FreeRTOS本身有什么门槛?其实FreeRTOS本身很成熟,门槛不在于“用”,而在于它对硬件完整性的要求比裸机高得多。

裸机点灯这个场景,只用了GPIO、RCC、几个寄存器。程序从Flash加载到CPU执行,不走定时器中断、不依赖异常向量表的完整运行,代码量小,对Flash容量和RAM容量几乎没有压力。而FreeRTOS一旦启动:

  • 需要创建任务、分配任务栈,全部依赖RAM容量和堆管理器的正常工作;
  • 依赖SysTick异常触发时间片调度;
  • 依赖SVC、PendSV异常完成任务切换;
  • 依赖完整的向量表,中断入口地址必须准确。

任何一个环节被“缩水”的硬件破坏,现象就是“死机”或者“没反应”。而这些问题在裸机上大概率不会触发,因为裸机程序压根不走这些机制。

所以我的判断习惯是:裸机点灯正常说明不了任何问题,它只能证明GPIO和RCC是完好的。FreeRTOS跑不起来,才是对芯片硬件完整性的真正压力测试。

2. 上电之前先“看”芯片:不用仪器也能做的外观初筛

在动代码之前,先做一次外观初筛,这个环节成本最低、效率最高。很多时候芯片是不是有问题,肉眼就能看出个七八分。

2.1 看丝印、看引脚、看批次:原装与翻新的一线之差

拿到芯片,先别急着焊,拿起放大镜或者手机微距镜头,对着顶面丝印看。

原装ST芯片的字迹特征大概是这样的:字体细腻清晰,笔画边缘锐利,深浅一致,激光刻字,手指甲刮过去有轻微的凹凸感。ST的logo是一个圆角方形加里面“ST”字母,比例协调。丝印内容至少包含三行:第一行是型号,第二行是封装和温度等级、前缀,第三行是生产批号/日期码。

翻新打磨片的字迹差异非常明显:打磨过的表面会有一层细微的磨砂感,跟周围没磨到的地方光泽度不一致。重印上去的字通常是油墨喷印,不是激光刻的,视觉上发白、发虚,边缘有晕染感,用指甲能刮掉一层。字体比原装的偏粗,笔画变形,有些甚至印歪了。

引脚也是重要线索。原装全新芯片的引脚是亮银色、均匀一致,没有焊接痕迹。翻新片引脚往往会残留焊锡、助焊剂痕迹,或者因为被打磨过,引脚表面有细微划痕。有些翻新片为了掩盖引脚氧化,会重新镀锡,但镀层偏厚,引脚看起来“胖一圈”。

还有一个容易忽略的细节:芯片底部的散热焊盘和模具痕迹。LQFP48底部有个裸露的焊盘(EP),翻新片底部的焊盘经常能看到明显的焊膏残留,原装片的焊盘是干净的哑光银。

再说批次。同一个批次买来的芯片,丝印内容、字体风格应该完全一致。如果你一次性买了十颗,发现其中两三颗字迹跟其他明显不同,哪怕说不清哪里不同,也要高度警惕——这往往是混料了。

2.2 用万用表和示波器做基础电学体检

外观初筛通过后,上电之前再用万用表做几项基础测试,能筛掉大部分有明显物理损伤的片子。

第一项:VDD与GND之间的二极管特性测试。万用表拨到二极管档,红表笔接VDD,黑表笔接GND,测出的压降通常在0.4V到0.6V之间。反过来接,应该是不导通或者压降很大。如果正反都接近0或者短路,那这颗芯片内部已经有问题了,大概率是ESD击穿或过流烧毁,直接换掉。

第二项:上电电流粗测。用一个可调电源或者带电流显示的开发板供电口,空载程序下,STM32F103C8T6在72MHz主频工作时的电流通常在20mA到50mA之间,看外设数量。如果上电瞬间电流冲到几百毫安,或者电流忽大忽小不稳定,芯片体质可疑。

第三项:复位引脚和晶振脚的波形测量。上电后,NRST引脚应该被外部上拉到3.3V,正常的复位释放过程是一个由低到高的跳变。用示波器看NRST,如果电压一直处于中间值或者有异常的毛刺,说明内部复位电路有问题。再看OSC_IN/OSC_OUT引脚,如果是HSE外部晶振,正常起振后示波器能看到8MHz正弦波,幅度大约在0.5V到3V之间;如果不起振,先查负载电容和晶振本身,排除这些外部因素后仍然不起振,那就要高度怀疑芯片内部的振荡器电路是否完好。

这三项测试只能筛掉“物理层面有明显问题”的芯片,没办法确认芯片身份。要确认身份,还得靠程序去读寄存器,这是下一章的内容。

3. 写一段身份校验程序:用寄存器结果给芯片“验DNA”

外观初筛不够,最可靠的辨真伪手段是直接读芯片内部的标识寄存器。这一招绕开了所有外观伪装,因为打磨片能改丝印,但它改不了已经烧录在芯片内部的ID和容量信息。

3.1 读DBGMCU_IDCODE设备ID寄存器

STM32F103内部有一个DBGMCU_IDCODE寄存器,地址是0xE0042000,里面存放着硬件版本和设备ID。F103系列的DEV_ID固定是0x410。注意,原装ST芯片读出来是0x410,GD32或者其它兼容芯片读出来的值往往不一样。

用标准库操作很简单,直接定义一个指针去读:

#include "stm32f10x.h" uint32_t ReadChipID(void) { // DBGMCU_IDCODE寄存器地址 #define DBGMCU_IDCODE_ADDR 0xE0042000UL return (*(volatile uint32_t *)DBGMCU_IDCODE_ADDR); } int main(void) { uint32_t idcode = ReadChipID(); uint16_t dev_id = idcode & 0xFFFF; // 低16位是设备ID uint16_t rev_id = (idcode >> 16) & 0xFFFF; // 高16位是版本号 // dev_id 应该等于 0x0410,否则芯片身份就存疑 while (1); }

把这一段程序烧进去,用调试器查看dev_id的值。如果读出来不是0x0410,这个芯片基本可以判定不是原装ST的F103。需要注意的是,旧版标准库和寄存器手册可能把这个寄存器描述为DBGMCU_IDCODE,在Keil里直接定义全局变量查看即可,没必要非得从串口打印出来。

3.2 读Flash容量寄存器与96位UID

设备ID只是第一关,光靠它还不能区分C8和C6这些兄弟型号。接下来读Flash容量寄存器,地址是0x1FFFF7E0,16位宽度,单位是KB。原装STM32F103C8T6读出来应该是0x0040(64KB),C6T6读出来是0x0020(32KB),C4T6读出来是0x0010(16KB)。

再看96位唯一ID,地址从0x1FFFF7E8开始,连续三个32位寄存器。这段ID是芯片出厂时激光烧录的,无法被修改,任何打磨重印都改变不了它。配套的读取代码如下:

#include "stm32f10x.h" void ReadUniqueID(uint32_t *uid_buf) { #define UID_BASE_ADDR 0x1FFFF7E8UL uid_buf[0] = *(volatile uint32_t *)(UID_BASE_ADDR + 0x00); uid_buf[1] = *(volatile uint32_t *)(UID_BASE_ADDR + 0x04); uid_buf[2] = *(volatile uint32_t *)(UID_BASE_ADDR + 0x08); } int main(void) { uint32_t uid[3]; ReadUniqueID(uid); // uid[0], uid[1], uid[2] 组合起来就是这个芯片的唯一身份 // 原装芯片读取时,三个寄存器都有值,且不是全0也不是全0xFF while (1); }

在Keil调试模式下,把这三个值记下来,跟另一颗正品芯片的UID对比,你会发现原装芯片的UID中间有一段包含明显的生产批次信息,而打磨片常常出现全0、全FF、或者所有芯片读出来居然一模一样的情况——一颗一颗的UID完全相同,这本身就是造假的最好证据,因为原装芯片的UID不可能重复。

3.3 芯片身份核验:各“假芯片”在实际读取中的表现

结合我实测过的各种芯片,把它们读出来的寄存器表现整理成下表。这张表是我判断芯片身份的主要依据。

芯片类型DEV_IDFlash容量寄存器UID特征
原装STM32F103C8T60x04100x0040(64KB)三组数据各不相同,带明显信息熵
原装STM32F103C6T6降级打标0x04100x0020(32KB)仍为正常UID,但容量暴露
GD32F103C8T6冒充ST通常不是0x0410,部分会出现0x0410可能读到0x0040UID规律与ST原装不同,常见重复段
打磨重印的旧芯片可能正常,也可能异常读出来的值可能错乱可能是全F或异常值
国产其他M3兼容片五花八门无规律无规律

这里要特别提一下GD32冒充片。有些批次的GD32会把DEV_ID寄存器也模拟成0x0410,所以单独看DEV_ID还不能一锤定音。这时候Flash容量和UID的表现就更关键:正品ST的UID在读出时,三组32位数据看起来都是“有内容”的,不会出现一段全是0或全是F;而很多GD32片子在UID段会读出大段连续的0或F,这跟ST工厂的烧录特征明显不同。

4. 在FreeRTOS场景下做“没反应”的系统化排查

确认芯片身份之后,再回头看最初的故障——“芯片没反应”。这里要分清楚:芯片身份有问题,并不代表所有“没反应”都是芯片的锅。FreeRTOS移植过程中的代码问题同样会引发一模一样的现象。所以正确的做法是,先用一个系统化的思路去定位“没反应”到底发生在哪个环节,再决定是查代码还是查芯片。我自己习惯把这个过程分成三层。

4.1 先定位“没反应”发生在哪一层:上电复位、启动文件还是调度器

“没反应”是一个很模糊的说法,它可以指上电后LCD不亮、LED不闪、串口不打印,也可以指调试器连不上。要精确定位,我建议在三个关键位置各放一个“探针”,用最简单的GPIO翻转做标记。

第一个探针放在main函数的第一行。上电后,如果芯片的时钟、Flash读取、启动文件都没问题,第一行代码一定会被执行。在那里翻转一个LED或者留一个GPIO电平,如果这个探针都不亮,说明程序根本没跑到main,问题出在时钟配置、Flash配置或启动文件上。

第二个探针放在任务创建之后、vTaskStartScheduler之前。如果你能看到第一个探针亮,但第二个探针不亮,说明卡在了时钟初始化或外设初始化里。最常见的是SystemClock_Config里HSE起振等待超时,死在了while循环里。

第三个探针放在任务函数里。如果前两个探针都亮了,任务不执行,那就是调度器没跑起来,或者任务根本没被创建成功。

代码示意大致是这样:

int main(void) { // 探针1:程序是否能进入main GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET); SystemClock_Config(); MX_GPIO_Init(); // 探针2:初始化是否完成,调度器启动前 GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_SET); xTaskCreate(vTaskLed, "led", 128, NULL, 1, NULL); vTaskStartScheduler(); // 正常不会走到这里 while (1); } void vTaskLed(void *param) { // 探针3:任务是否真的被调度 GPIO_WriteBit(GPIOA, GPIO_Pin_2, Bit_SET); while (1) { vTaskDelay(pdMS_TO_TICKS(500)); } }

这个“三探针法”看起来简单,但非常有效。它能瞬间把“没反应”这个大问题,收敛到某一个具体的执行阶段。不然你面对的是几百行初始化代码和无头绪的排查。

4.2 FreeRTOS启动最容易被忽略的四个配置点

如果探针定位到“调度器没跑起来”这一层,而芯片本身的ID和容量校验又都正常,接下来要查的就是FreeRTOS移植最常见的四个配置坑。这四个坑我几乎每周都会在各种技术群看到一遍。

第一个坑:SysTick_Handler被裸机代码覆盖或删除。用标准库开发的朋友尤其容易踩这个坑。标准库的stm32f10x_it.c里默认有SysTick_Handler,但如果你用的是旧工程模板,这个函数可能是空的,甚至没有定义。FreeRTOS的时间片调度完全依赖SysTick中断,没有这个中断处理函数,任务永远得不到切换。解决办法是在中断文件里明确加上:

void SysTick_Handler(void) { if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) { xPortSysTickHandler(); } }

第二个坑:PendSV_Handler和SVC_Handler被别的库占用。有些工程之前移植过别的操作系统,或者写过自己的PendSV中断服务函数,跟FreeRTOS的符号冲突了。FreeRTOS的port层需要接管PendSV_Handler和SVC_Handler,如果这两个符号被其他文件定义,链接时会报错,或者更隐蔽地,编译器用了错误的弱定义,导致任务切换不触发。搜索整个工程,确保PendSV_Handler和SVC_Handler只在FreeRTOS的port.c和相关中断文件里各定义一次。

第三个坑:NVIC优先级分组没设置成Group 4。FreeRTOS在Cortex-M3上强制要求所有优先级位都被配置为抢占优先级,也就是NVIC_PriorityGroup_4。如果你用的是Group 2或Group 3,中断里调用的临界区保护函数可能失效,导致系统随机崩溃。在main函数最开头加上:

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);

这一行必须在创建任何中断、初始化任何外设之前执行。

第四个坑:Heap不够或者任务栈不够。FreeRTOS的堆大小在FreeRTOSConfig.h里的configTOTAL_HEAP_SIZE控制,默认可能是8192字节。如果你的任务创建时栈大小设成512甚至1024,创建三四个任务堆就耗光了,xTaskCreate返回pdPASS失败但你却没检查返回值。任务没有成功创建,自然看不到任何执行现象。建议在调试阶段打开堆栈溢出检测:

#define configCHECK_FOR_STACK_OVERFLOW 2 #define configUSE_MALLOC_FAILED_HOOK 1

并在钩子函数里把操作停下来或者点亮一个错误灯,这样问题会立刻现形。

4.3 用“替身芯片”验证法做隔离测试

如果代码方面的配置检查了一遍还是找不到问题,下一步我会做一个非常有效的隔离实验:把这块板子上的芯片换成一颗从正规渠道买来、已经验证过身份的正品F103C8T6,程序不修改,重新烧录运行。

这个实验的信息量很大。

  • 如果换了正品芯片后一切正常,那之前的芯片有问题的结论基本坐实;
  • 如果换了正品芯片后还是没反应,问题就100%出在板子硬件或者代码上,跟芯片无关,需要回头检查PCB供电、复位电路、晶振电路这些外围;
  • 还有一种情况:换了正品芯片后出现了跟“假芯片”不一样的另一类报错,比如烧录能成功但运行到某个外设初始化就死,那可能是代码里某些外设配置本身踩了硬件的特性差异,要分情况处理。

这个“替身芯片”方法特别适合芯片来源可疑的板子。它用一个变量替换的思路,帮你把“芯片硬件”和“外部系统”两个变量彻底分开。

5. 确诊之后:留证据、做处置、防再犯

读到这一章,说明你已经拿到了ID读取结果、外观照片和实际故障现象,基本可以下结论了。但确诊只是第一步,接下来怎么跟卖家交涉、怎么避免下次再踩坑,同样重要。

5.1 与卖家交涉前先固化现场证据

很多人一发现芯片是假的,气得直接去找卖家吵,结果卖家一句“你怎么证明是芯片的问题,不是你自己程序的问题”就能把你噎住。我的建议是,吵架之前先把证据链路做完整,让你说的话有据可查。

至少要准备下面几样东西:

  • 芯片顶面丝印的高清微距照片,能展示字体形态和打磨痕迹;
  • ID读取程序的运行截图或串口打印记录,能证明DEV_ID、Flash容量、UID的实际值;
  • 同批次多颗芯片的检测对比,最好能做到“两颗以上读出来的信息互相矛盾”,比如两个UIF完全一样,或者容量寄存器明显不对;
  • 烧录工具和调试器的型号记录,用以说明测试环境和方法。

把这些证据整理成一个简单的PDF或截图合集发给卖家,大多数正规做生意的商家会直接补发或退款,因为这套证据已经足够支撑你退货了。遇到死不认账的,平台介入时这些证据也派得上用场。

5.2 采购与验货SOP:让“假芯片”在进门那一刻就被拦住

经过这次折腾后,我给自己定了一条验货SOP,现在也推荐给团队里的硬件同事用。线下批量采购,坚持从正规代理商或授权渠道拿货;线上小批量购买,优先选择标注“原装正品”“可提供批次报告”的商家,价格异常低的直接放弃。收到芯片后统一做三步验证,不通过的一律不用。

第一步,拆包后做外观抽检,重点看丝印和引脚; 第二步,随机抽两到三颗烧录身份校验程序,确认DEV_ID、Flash容量、UID正常; 第三步,用正品芯片跑一遍完整固件,确认功能和性能指标,再批量烧录正式程序。

这三步看起来繁琐,但摊到十颗芯片上也就多花半小时。比起一块板子焊好了发现芯片有问题再返工,这点时间成本完全可以接受。

5.3 即使芯片是真的,FreeRTOS稳定运行的硬件底线

最后说一个容易被忽视的方面:即使芯片身份没问题,FreeRTOS要稳定运行,硬件上也必须达到几个底线要求。很多“芯片没反应”的疑案,最后查出来其实是供电和复位电路不到位,芯片本身背了锅。

第一,电源去耦必须到位。STM32F103的每个VDD引脚旁边都要放一个100nF的陶瓷电容,布局上尽量靠近引脚。一块只有电解电容、没有高频去耦电容的板子,在裸机低频运行时可能看不出来问题,但FreeRTOS的任务切换会产生高频电流尖峰,电源纹波一大,内部逻辑就容易误翻转,表现为随机死机。

第二,复位电路要可靠。NRST引脚上需要一个100nF左右的电容到地,加上一个10kΩ的上拉电阻。如果省掉这些,上电瞬间的复位脉冲不够干净,芯片可能进入奇怪的启动状态,表现就是“没反应”。

第三,晶振负载电容要匹配。8MHz晶振的两个负载电容通常在10pF到22pF之间,具体要看晶振的负载电容规格。负载电容选得不对,有可能导致晶振不起振或者起振后幅度过低。FreeRTOS对时间基准有要求,但HSE本身如果有问题,SystemClock_Config里就会死循环,探针1会直接暴露这个问题。

把这几个硬件底线守住,再叠加前面说的身份校验和代码排查,你才能放心地跟芯片说“要么你是对的,要么你能证明你是对的”。

尾巴:我现在的习惯是先“验明正身”再写功能代码

经历这次假芯片事件之后,我的开发习惯改了不少。现在任何一块新板子或者任何一批新芯片到手,第一步都是先烧一段身份核验例程,把DEV_ID、Flash容量、96位UID读出来,跟芯片丝印上的型号对一遍。确认无误后,才开始调外设、跑功能。如果芯片连身份校验都过不了,我不会在这一颗芯片上浪费任何调试时间,直接换料。

另外多说一句,这个校验例程不用写得复杂,直接放在main函数入口,输出到串口,或者干脆在调试器里看变量,项目定型后可以删掉,也可以保留作为量检工具。它在正式生产批量检测时同样有用——把一块板子接上ST-Link,一键读ID,几秒钟就能判断芯片来路正不正。

希望这篇东西能给正在被“STM32F103 + FreeRTOS没反应”折磨的朋友提供一个清晰的排查路径。遇到这种问题,先别急着怀疑自己的代码,也别急着把锅甩给FreeRTOS。按照从外观、到寄存器身份、再到运行机制的顺序走一遍,假芯片会现形,真问题也会暴露。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询