
上电电源灯亮了。示波器点上去3.3V纹波很干净NRST也稳定在高电平。但是板子上的STM32F103就是没反应LED不闪串口没有输出SWD也枚举不出来。这台戏我太熟了。你以为是自己把FreeRTOS移植错了把启动文件、SystemInit、FreeRTOSConfig.h翻来覆去看了好几遍连Keil的编译器版本都换了两轮问题依旧。最后追到芯片头上才发现这颗“STM32F103C8T6”既不是ST原厂货也不是二手拆机料而是一颗被打上ST标记的国产兼容片。这类问题在STM32F103 FreeRTOS的调试阶段特别坑因为它藏得很深。芯片能上电、能跑启动代码、甚至能烧录但跑起来各种玄学问题任务不调度、串口乱码、定时器输出占空比异常、一进中断就死机。这篇文章不打算绕弯子直接讲清楚三件事芯片没反应到底该怎么查、假芯片为什么会造成这些现象、以及如何用标准库FreeRTOS写一个快速识别的验证工程。无论你是画板子的、写驱动的还是做产品的这套排查思路都能直接用。1. 芯片“没反应”先别慌从最小系统到烧录链路逐个过1.1 最小系统那几颗料最容易埋雷很多人一遇到芯片不工作第一反应是怀疑代码其实硬件最小系统的排查成本最低也最应该先做。F103的小系统本身非常简单但恰恰是简单才容易在细节上翻车。供电环节是最常见的问题点。STM32F103虽然标称2.0V到3.6V但实际项目里最好稳定在3.3V。除了主电源VDD还有VDDA模拟电源板子上VDDA和VDD之间要接一只10Ω电阻或磁珠否则ADC会有噪声但至少也要保证VDDA有电。我最常踩的坑是去耦电容缺失每个VDD引脚旁边必须放100nF陶瓷电容而且要尽可能靠近引脚不然芯片在FreeRTOS任务切换瞬间电流波动一大直接复位或跑飞。先前遇到一块板子电源灯亮得正常示波器看纹波也就几十毫伏但芯片就是复位的干活后来发现是VDDA没有单独滤波加上去就好了。复位电路也不难但很多人会省掉。NRST引脚需要一只10kΩ电阻上拉到3.3V再并联一只100nF电容到地这是手册里的标准接法。如果芯片一直处于复位状态自然什么反应都没有。BOOT0和BOOT1必须接好BOOT0要经过10kΩ电阻下拉到地不能直接悬空。BOOT0悬空时内部虽然默认是低但在电磁环境稍差的场合这根引脚就是一根天线偶发拉高后芯片会进系统存储器程序根本跑不起来看起来就像芯片坏了。时钟部分也值得仔细查一遍。F103默认是HSI内部8MHz时钟启动也就是说即使外部晶振不焊芯片也应该能跑只是串口波特率会偏精度不够。如果板子上焊了8MHz晶振要配两个15pF到20pF的负载电容否则起振困难。这里有个很常见的误会PA8引时钟。刚接触F103的同学以为PA8上电就会输出时钟实际上PA8的MCO功能默认是关闭的必须调用RCC_MCOConfig()并进行AFIO时钟使能之后才有输出。我见过不止一个人拿这个判断“芯片是不是没工作”量不到波形就认定芯片坏了其实只是没配置。提示如果板子没焊晶振或者晶振质量差先用HSI把系统跑起来。等串口、GPIO这些基础功能正常了再处理HSE的问题不要一上来就跟晶振死磕。1.2 调试器连不上先查这四处SWD连不上是“芯片没反应”最常见的表现形式。排查顺序是有讲究的按成功率从高到低排列能省下不少时间。第一确认调试器的线序和电平。ST-Link的SWD只需要SWDIO、SWCLK、GND三根线VCC可以不用接尤其是板子已经独立供电的时候。如果VCC接了又要保证调试器和板子共地电平不一致很容易把IO口搞坏。我自己吃过大亏某次板子供电3.3VST-Link也往外输出3.3V但两边“3.3V”的地电位差了几百毫伏结果SWD死活连不上换了三根线才好。第二检查复位脚。如果程序里把NRST引脚配置成了普通GPIO或者外部复位电路把NRST拉死SWD就可能连不上。这时候可以在Keil的Settings里把调试模式改成“Connect under Reset”让调试器在复位信号激活时抢占连接绕过已经跑飞的程序。这个技巧在排查FreeRTOS任务把SWD引脚复用了的时候非常管用。第三排除读保护。如果之前烧录过开启RDP Level 1的程序芯片默认不允许调试器读写Flash。表现是能识别到设备ID但读Flash全被拦截或者干脆连接后立刻断开。解决办法是用ST-Link Utility或者CubeProgrammer做全片擦除前提是确认你买的芯片对得上这个操作假芯片在擦除环节可能直接变砖。第四看IDCODE。Keil的Settings窗口里正常F103的IDCODE显示为0x1BA01477这一类的SWDIDDevice ID会对应到0x410、0x412、0x414。如果显示异常或者ID对不上宣传的型号那就要打起精神了。这里不展开第二节专门说假芯片的破绽。1.3 别忽略软件层面的“假死”硬件查完了还得排除一个常见陷阱芯片在工作但程序根本没跑对。FreeRTOS环境下这尤为突出。先说一种特别容易骗人的情况HSE起振失败后卡死在SystemInit()里。标准库的SystemInit()默认把时钟切换到HSE如果外部晶振没焊、焊错或者电容不对函数会卡在等待HSERDY置位的循环里。表面现象就是上电后电流正常但程序完全没动静连LED都不闪。这时候把示波器戳到OSC_IN引脚能隐约看到一个衰减振荡波形但始终达不到满幅基本就是这个原因。再说读保护。有些人烧录一次后勾选了Reset and Run程序开跑但下一次再想连接调试器就发现连不上了。这常常是程序里开启了RDP或者选项字错误。用串口ISP模式可以救回来具体方式是把BOOT0拉高复位后通过串口1用Flash Loader擦除全片。这里要特别提醒假芯片的ISP兼容性并不稳定有的能擦有的擦到一半就卡死。如果怀疑是假芯片尽量靠调试器验证不要在ISP过程中投入太多时间。还有一类“假死”表现为上电后程序运行正常LED也能闪但FreeRTOS的任务就是不调度。后面第三章专门展开。2. 假芯片的“行为画像”有些问题从一开始就注定了2.1 F103为什么成了假货重灾区聊这个问题之前先明确一个概念我所说的“假芯片”是广义的市场乱象包括翻新片、打磨片、冒用ST商标的兼容片不是特指某一种。这类芯片在行业里流通已久属于灰色地带正规研发和生产流程都不应该使用。STM32F103成为重灾区原因很直白。第一需求量太大大学实验、个人项目、工业控制、消费电子都在用市场空间巨大。第二生命周期实在太长从2007年发布到现在ST原厂早把产能倾斜到更高端的H7系列但F103的需求并没有减少供不应求自然有逐利的渠道商想出各种办法。第三封装太全LQFP48、LQFP64、LQFP100、LQFP144全系列都有打磨后套标很容易。C8T6、RCT6、ZET6这几个型号尤其泛滥几乎每一个中低容量封装都有对应的“伪装版”。核心原因还在于F103的兼容方案太成熟。国产的GD32F103、APM32F103等本来就在Pin-to-Pin上做了兼容外设大体一致源码级别都能跑通ST的标准库。这样的兼容芯片在正规授权渠道采购其实没问题但一旦被不良商家重新打标成ST以ST的价格卖问题就来了。它确实能跑FreeRTOS但细节差异会让你在调试阶段怀疑人生。2.2 三类“假芯片”翻新、打磨、直接打标我见过的“假芯片”大致分三类每一类对调试的影响都不一样。第一类是翻新片即拆机片。它们原本是ST真芯片但从旧板子上拆下来引脚有焊锡和老化痕迹重新规整后当新片卖。这类芯片最大的问题是引脚氧化导致焊接虚焊以及内部Flash的擦写寿命已经消耗很大。表现就是刚开始能用跑一段时间后程序随机丢失或者Flash写一半失败。如果你发现FreeRTOS运行一段时间后本来保存在Flash里的参数变成了0xFF可以先怀疑这一类。第二类是打磨片本质是低配冒充高配。商家把F103C8T6这种小容量芯片表面打磨掉原字重新激光打上STM32F103RCT6。打磨片的识别相对简单表面光泽不均匀、丝印颗粒感和原厂不同、批次号和封装标志对不上。上电后最明显的问题是Flash容量不足。你按256KB的RCT6编译程序实际芯片只有64KB烧录时可能成功因为烧录器读到的Flash大小信息被伪装过但程序跑到后面就死机。第三类是直接打标的兼容片。国内几款主流国产F103兼容芯片擦除原丝印后直接打上ST Logo以假乱真。它们大多是全新的性能也能跑但外设寄存器行为、ADC精度、内部Flash擦写时间、甚至SysTick的时钟源都可能跟ST原厂有细节差异。严格来说这类芯片并不是不能用而是你不能完全按照ST的手册来调试很多“玄学问题”其实是芯片自身差异。2.3 假芯片在调试器眼里的破绽如何用工具拆穿假芯片其实比很多人想象的简单。STM32全系都有96位唯一ID和Flash大小寄存器调试器能直接读出来。Flash大小寄存器的地址在0x1FFFF7E0这是一个16位的只读寄存器直接表示芯片内部的Flash容量单位是KB。用ST-Link Utility连接后左侧设备信息里显示的Flash Size来自这个寄存器。如果购买的是RCT6256KB读出来却是64KB那基本可以确定是打磨片。还有一个设备IDF103中容量是0x410、低容量0x412、高容量0x414不同密度对应不同Flash容量区间。比如C8T6是64KB属于中容量设备ID是0x410如果你买的是C8T6设备ID却是0x414那也不正常。96位UID存放在0x1FFFF7E8开始的12个字节里前两个字节是厂商标识。ST原厂的代号通常是0x0080开头。重点看如果UID全为0xFF或者连续两颗芯片UID完全相同就有大问题。原厂每一颗芯片UID都是唯一的克隆芯片做不到这一点打磨片则会完全丢失原来的UID信息。不过要注意测试结果异常只能作为“高度怀疑”的依据不能当作法律意义上的造假证据真要投诉还是得走渠道验货。另一个好用的手段是Flash边界测试。在调试器里对着芯片末尾地址写一个测试值比如64KB芯片写0x0800FF00读回如果发现地址无效或值不对说明容量造假。3. FreeRTOS项目里“没反应”的高频根因3.1 时钟树和启动时序先确认芯片真的在跑如果你确认芯片是真货但FreeRTOS项目跑起来还是“没反应”那问题大概率出在时钟和启动时序上。FreeRTOS的心跳依赖SysTick而SysTick的时钟源又取决于系统时钟树的配置。标准库的SystemInit()会把系统时钟切到PLL最终跑到72MHz。如果HSE没起振、PLL锁不住SystemInit()就卡死在里面FreeRTOS自然永远跑不起来。排查思路是分步做时钟验证。先不要急着启动RTOS写一个最简裸机程序配置SYSCLK为72MHz然后用RCC_GetClocksFreq()把SYSCLK、HCLK、PCLK1、PCLK2读出来通过串口打印。正常时PCLK1应该是36MHzPCLK2是72MHz。如果打印出来的值是HSI的8MHz或者完全不对就先修时钟树。还有一个很多人忽略的细节SysTick优先级的配置。FreeRTOS要求SysTick的优先级必须是可被屏蔽的也就是数值不是最低同时还要求PendSV的优先级在所有可屏蔽中断中最低。标准库初始化时用NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4)之后FreeRTOSConfig.h里的configKERNEL_INTERRUPT_PRIORITY和configMAX_SYSCALL_INTERRUPT_PRIORITY要对应。很多“芯片没反应”的例子其实是内核异常表现为任务创建了但不跑或者SysTick中断一进来就HardFault。3.2 FreeRTOS配置引发的“假死”FreeRTOS项目常见的“没反应”还有一批根因在配置本身而不是芯片。这里把最让人头疼的几个问题集中说一下。堆栈溢出是第一个高频坑。configMINIMAL_STACK_SIZE默认给的是128字对于简单的闪烁任务够用但如果你在任务里用了printf尤其格式化浮点数128字远远不够。任务栈溢出后内核不会立刻抛错而是悄悄把任务控制块和相邻变量踩掉表现就是系统跑一会就死机、或者任务调度变慢。建议在所有调试阶段把configCHECK_FOR_STACK_OVERFLOW设为2并实现vApplicationStackOverflowHook()钩子函数在栈溢出时点亮一个专门的错误LED这会省下大量排查时间。第二个高频坑是堆大小不够。FreeRTOS的信号量、队列、任务控制块都从configTOTAL_HEAP_SIZE分配。heap_4.c支持碎片合并但如果任务频繁创建删除仍然可能申请不到内存。一个很实际的经验是先不管美观与否把configTOTAL_HEAP_SIZE设成12KB起步跑一段时间看xPortGetFreeHeapSize()剩余量再逐步调整。第三个坑是中断里调用FreeRTOS API。串口中断、定时器中断里直接调用xQueueSend()这类非FromISR版本轻则任务不调度重则HardFault。尤其在做FreeModbus移植时串口接收中断里的事件通知最容易踩这个雷。正确的做法是用portYIELD_FROM_ISR()配合xQueueSendFromISR()把处理逻辑交给RTOS任务。还有一个容易被忽略的坑Keil编译器版本切换。现在很多人从AC5切到AC6FreeRTOS的移植层在AC6下的优化级别和标准库宏上会有微妙差异。如果一切换编译器就出现奇怪的死机试试把优化级别设为-O0或者回到AC5验证一次就能定位问题在编译器还是芯片。3.3 串口、PWMDMA、Flash这些外设在克隆芯片上的差异把芯片换成兼容片之后即使是真的兼容芯片FreeRTOS跑起来也会出现一批有规律的外设差异。我自己在一个PWMDMA的项目里深有体会。先说串口。F103的USART在原生芯片上非常皮实但兼容芯片的波特率误差和抗干扰能力通常不如原厂。之前帮朋友排查一个用FreeModbus v1.6做Modbus RTU从站的项目现象是串口收到一帧数据后偶发死机查了很久发现是UART收到错误帧时产生了HAL_UART_ERROR_FE这种帧错误标志。原生F103在同样线路下几乎不会触发这个错误兼容芯片在RS232经过转接电平后由于信号质量差经常会置上帧错误标志。解决办法是在串口中断里主动判断并清除FE、NE、ORE这些错误位并在Modbus协议解析时对异常帧直接丢弃。再说PWMDMA。热词里有人问PA1、PA3的PWMDMA问题PA1对应TIM2_CH2PA3对应TIM2_CH4用DMA搬运数据到CCR寄存器组就能做出呼吸灯或者WS2812B这类时序敏感的波形。在克隆芯片上DMA的触发时序和CCR更新时机有细微差异容易出现“占空比到不了100%”的现象。排查这类问题先把DMA关掉直接手动赋值CCRARR1注意不是CCRARR看输出是不是常高电平。如果手动赋值能到100%DMA搬运时到不了就去查DMA的传输宽度和内存地址对齐别先怀疑芯片。最后说Flash。F103本身没有EEPROM掉电保存数据一般用内部Flash模拟。原生F103擦写Flash时CPU会暂停兼容芯片的擦写时间会略长而且掉电时序更敏感。如果程序在掉电瞬间写Flash很容易出现只写了一部分数据的情况。建议的做法是把需要掉电保存的数据放到一个结构体里按4字节对齐写入时先擦除整页再连续写入整个结构体读取时校验关键字段不匹配就恢复默认值。不要把参数零散地写在Flash里那样在兼容芯片上极易出错。4. 一套可以直接抄的芯片验证与FreeRTOS健康检查工程4.1 用标准库v3.5写个身份读取程序排查假芯片最靠谱的方式不是靠肉眼而是写一个验证工程上电就把芯片身份信息打印出来。下面这段代码基于标准库v3.5可以直接扔进F103的最小工程里跑依赖文件就一个stm32f10x.h。#include stm32f10x.h #include stdio.h // Flash大小寄存器和UID基地址在标准库里没有现成宏直接定义 #define FLASH_SIZE_ADDR (0x1FFFF7E0u) #define UID_BASE_ADDR (0x1FFFF7E8u) void Chip_PrintInfo(void) { uint16_t flashSize *(volatile uint16_t *)FLASH_SIZE_ADDR; uint32_t uid[3]; uint8_t i; uid[0] *(volatile uint32_t *)(UID_BASE_ADDR); uid[1] *(volatile uint32_t *)(UID_BASE_ADDR 4); uid[2] *(volatile uint32_t *)(UID_BASE_ADDR 8); printf(Flash Size: %d KB\r\n, flashSize); printf(UID: ); for (i 0; i 3; i) { printf(%08X, (unsigned int)uid[i]); } printf(\r\n); // DBGMCU-IDCODE 里包含设备ID和版本ID printf(DBGMCU IDCODE: 0x%08X\r\n, (unsigned int)DBGMCU-IDCODE); }串口初始化比较简单用USART1的PA9、PA10波特率115200注意要把fputc()重定向到USART1才能用printf。int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }拿到打印结果后对照表格C8T6的Flash Size应该是64RCT6是256ZET6是512Device ID方面中容量是0x410高容量是0x414。如果Flash Size和标签对不上或者UID异常直接返厂不用再往下调FreeRTOS了。4.2 FreeRTOS最小工程配置清单身份验证通过之后再跑FreeRTOS才是健康的。下面这份FreeRTOSConfig.h配置我常用于F103调试工程可以直接作为起点。#define configUSE_PREEMPTION 1 #define configUSE_IDLE_HOOK 0 #define configUSE_TICK_HOOK 0 #define configCPU_CLOCK_HZ ( ( unsigned long ) 72000000 ) #define configTICK_RATE_HZ ( ( TickType_t ) 1000 ) #define configMAX_PRIORITIES ( 5 ) #define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) #define configTOTAL_HEAP_SIZE ( ( size_t ) ( 12 * 1024 ) ) #define configMAX_TASK_NAME_LEN ( 16 ) #define configUSE_16_BIT_TICKS 0 #define configIDLE_SHOULD_YIELD 1 #define configUSE_MUTEXES 1 #define configUSE_RECURSIVE_MUTEXES 1 #define configUSE_COUNTING_SEMAPHORES 1 #define configQUEUE_REGISTRY_SIZE 8 #define configCHECK_FOR_STACK_OVERFLOW 2 #define configUSE_MALLOC_FAILED_HOOK 1 #define configUSE_TIMERS 1 #define configTIMER_TASK_PRIORITY ( 2 ) #define configTIMER_QUEUE_LENGTH 10 #define configTIMER_TASK_STACK_DEPTH ( configMINIMAL_STACK_SIZE * 2 )configCHECK_FOR_STACK_OVERFLOW设为2会在任务上下文切换时做更严格的栈边界检查代价是每个切换周期多几十个CPU周期调试阶段完全值得。configUSE_MALLOC_FAILED_HOOK建议也打开vApplicationMallocFailedHook()里放一个死循环加错误灯能避免内存分配失败时的静默死机。创建一个LED任务和串口打印任务LED任务里闪烁串口任务每秒钟打印一次芯片身份信息和剩余堆栈。建议在任务里显式调用uxTaskGetStackHighWaterMark()打印任务剩余栈空间通过串口可以直观看到任务栈是否太紧张。这个习惯能提前暴露假芯片在堆栈行为上的细微差异。4.3 顺手把Modbus RTU、掉电保存的坑填了验证工程跑通后如果项目本身要做Modbus RTU从站用FreeModbus v1.6 标准库是最常见的组合。移植时最需要注意的一点是串口中断里会产生EV_READY、EV_FRAME_RECEIVED这些事件而eMBPoll()通常在主循环或RTOS任务里调用。在FreeRTOS环境下串口ISR和eMBPoll()之间的共享变量最好用临界区保护或者干脆把事件通过队列传给RTOS任务任务里再调用eMBPoll()。千万不要在串口中断里直接调用eMBPoll()那会让FreeRTOS的调度器直接被打乱。关于掉电保存前面说了用Flash模拟EEPROM的注意事项。这里补充一个代码层面的实现思路定义结构体保存所有参数结构体大小对齐到4字节在写入时先擦除整页再编程。如果项目里同时有Modbus寄存器区建议把参数区放在Flash最后一页避免和程序代码重叠。写完立即读回校验校验失败就返回错误而不是静默接受。注意FreeModbus v1.6的事件系统在裸机下是置标志位在RTOS下需要检查临界区是否配对。vMBPortEnterCritical()和vMBPortExitCritical()默认使用的是关闭中断方式如果某个外设中断优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY临界区可能遮不住它会留下隐蔽的竞态条件。5. 避坑速查表从现象到根因从采购到量产5.1 “没反应”问题速查表下面这张表是从我自己和周围人踩过的坑里总结出来的适合贴在工位上排查时按图索骥。现象可能根因验证方法解决思路上电后电流正常但程序完全不跑HSE起振失败、卡在SystemInit示波器测OSC_IN波形换晶振、调负载电容或改用HSISWD连不上调试器线序错、读保护、SWD引脚被复用换Connect under Reset看IDCODE查线序、解读保护、检查GPIO复用能烧录但进入调试后总是复位电源纹波大、NRST干扰示波器看NRST和VDD补齐去耦电容、复位RC参数调整程序能跑但FreeRTOS任务不调度PendSV优先级错误、SysTick未启动在任务外翻转LED检查FreeRTOSConfig.h和NVIC分组跑一段时间后死机错误灯不亮任务栈溢出、堆耗尽打开栈溢出检测和malloc失败钩子加大栈、用uxTaskGetStackHighWaterMark观察串口偶发乱码或丢帧波特率偏差、UART错误标志未清理检查帧错误标志、示波器看波形校准时钟、在ISR里处理FE/OREPWM占空比到不了100%CCR/ARR设置问题、DMA时序差异关DMA手动赋值CCRARR1检查DMA传输宽度和对齐Flash参数保存后随机丢失Flash擦写时序、掉电过程异常写入后立即读回校验结构体连续写、整页擦除、加校验这张表不能覆盖所有情况但能覆盖我见过的大多数“没反应”场景。核心思想是先软件后硬件、先时钟后外设、先排除芯片再折腾代码。5.2 买芯片和来料检验的一条龙建议关于买芯片我的建议只有一条从正规渠道采购哪怕贵一点。F103便宜但一颗假芯片浪费的调试时间远远超过芯片本身的差价。既然项目预算能cover就不要在芯片上赌运气。如果确实需要从市场现货里采购来料检验阶段务必做三件事。第一外观检查。用放大镜或体视显微镜看丝印、看引脚、看封装边缘是否有打磨痕迹。原厂丝印清晰锐利激光打标后表面有细微的颗粒感打磨片表面光泽不均匀引脚根部可能有残留塑封料。第二批量上机验证。不要只抽测一颗至少抽5%到10%的芯片跑一次之前那个身份读取程序记录Flash Size、UID、Device ID确认每颗都通过。第三跑一轮老化测试。让芯片在满载时钟和FreeRTOS多任务下持续运行24小时观察是否有随机复位或Flash数据丢失。这一步能筛掉绝大多数翻新片。5.3 最后再分享一个小习惯说个我自己的习惯吧。现在我接手任何F103 FreeRTOS项目第一件事不是写业务代码而是先做一个极简的“来料测试固件”把芯片身份打印、Flash读写回环、串口回环、LED任务调度、堆栈水位监控全部做成菜单项。新板子到手先烧这个固件通过后再进入业务开发。看似浪费了半天时间实际上每次都能在早期拦下一批硬件问题后面调试业务代码的时间省下来的何止一天。如果你也在调试一个怎么都“没反应”的F103 FreeRTOS项目不妨先把代码放一边按这个流程走一遍。很多时候问题根本不在你写的代码里而在那颗不起眼的芯片上。