ARTICLE · 1026715
源码一模一样,一个能跑一个不行,真相出来真的绷不住了!
源码一模一样,一个能跑一个不行,真相出来真的绷不住了!
一个文件没加进 Keil 工程,也没有报错, SPI 直接哑了。
这篇把过程完整还原一遍,包括我走的每一步弯路。
它明明在 D 盘跑得好好的
上周我把一个工程复制了一份,从 D 盘搬到 E 盘。
原来的工程在D:\Desktop\GD260809,跑得挺好。我复制了一份到E:\ShareFile...\GD260809,准备在副本上改点东西。
结果副本一烧进去,SPI 读回来的数据全是 0x00。
不是乱码,不是偶尔错一两个字节,是干干净净的一片零。屏幕上一片空白。
我当时的第一反应是:"拷坏了?"
于是我把整个工程删了,重新复制了一遍。
一样的现象。
第一次怀疑:我怀疑代码
这很正常——数据不对,第一反应肯定是"代码有问题"。
我把两个工程的关键源文件一个一个拉出来做字节级对比:
spi.c、spi.h、gpio.c、main.c、main.h、Fontlib.c、screen.c、stm32f1xx_hal_msp.c。
全部一致,一个字节都不差。
到这一步我心里其实有点发毛了。
因为我做过很多年 STM32,见过"代码不对所以现象不对",也见过"硬件坏了所以现象不对"。但代码完全一样、现象完全不一样,这就说明我一开始怀疑的方向就不对。
代码是清白的。
那问题在哪?
第二次怀疑:我开始怀疑板子
代码没问题,那就往硬件想。
这是我的兵荒马乱阶段。我做了这些事:
• 换了一块开发板 —— 一样是 0x00 • 换了一根杜邦线 —— 一样 • 拿示波器夹 SCK —— 没波形 • 把 D 盘那份工程重新烧进同一块板子 —— 好了
到这里,结论就很硬了:
同一块板子、同一根线、同一个传感器,D 盘的工程能跑,E 盘的工程不跑。
那问题只能在这两个工程的"不一样的地方"。
而我已经确认过:源代码,一模一样。
转折点:我把怀疑对象从"代码"换成了"构建"
那天我在椅子上坐了一会儿,然后脑子里蹦出一句话:
"代码没错,那会不会……我根本就没编译那份代码?"
这句话听起来有点蠢,但它是我整个排查里最重要的一步。
因为我突然意识到一件事:源码放在磁盘上,和源码被编译进固件,是两件完全不同的事。
你以为你改了代码,实际可能是编译器根本没看你那个文件。这种情况在"手动复制工程"的时候特别容易发生——因为 Keil 的工程文件里记着一份"清单",记录哪些文件参与编译。你复制文件夹的时候,只要工程文件里的清单没对上,就会出现"文件在、但不参与编译"的情况。
于是我打开了那个通常没人会看的文件:.uvprojx。
它是 XML 格式,一坨一坨的标签,很难读。我就用最笨的办法,两边一起打开,一行一行对。
然后我看到了这个:
D工程(能跑的),Application/User/Core 这个组里,有这个文件:stm32f1xx_hal_msp.c
E 工程(不跑的),同一个组里,没有这个文件。
我盯着这两份清单看了好一会儿。
就是这个。但说实话,那一刻我还没完全确定——因为文件明明在磁盘上躺着,好好地放在 Core/Src 目录里。磁盘上有它、Keil 工程里没有它,这两个状态可以同时存在,而我以前从没意识到这一点。
我不放心,又查了两处
一个孤证不够。我又去翻了另外两个地方。
第一处是 .lnp 文件(Keil 生成的链接清单,列出所有参与链接的目标文件):
E 工程的链接清单里,确实没有 stm32f1xx_hal_msp.o。
反过来,我还在里面看到一个意外的名字:stm32f1xx_it_1.o。
那个 _1 后缀让我明白了——这个工程以前被人手工改过、复制过、重命名过。而手工复制,正是"文件漏加"这件事的最高发场景。
第二处是 .map 文件,也就是这次排查真正的判决书。
.map 文件会记录每一个函数最终来自哪个 .o 文件、占多少字节。我把 HAL_MspInit 这个函数在两个工程里都查了一遍。
E 工程(坏的)长这样:
HAL_MspInit 0x080009b1
Thumb Code 2 stm32f1xx_hal.o(i.HAL_MspInit)
D 工程(好的)长这样:
HAL_MspInit ...
Thumb Code 52 stm32f1xx_hal_msp.o(i.HAL_MspInit)
看两个地方就够了:
| stm32f1xx_hal.o | 2 字节 | |
| stm32f1xx_hal_msp.o | 52 字节 |
2 字节。
在 Thumb 指令集里,2 字节刚好够一条指令——BX LR,也就是"直接返回"。
说白了就是:这个函数被调用了,然后什么都没干,直接回来了。
如果它是空函数呢?它就是空函数。
这里就是整个问题的真相。
为什么"少一个文件"能让 SPI 直接哑掉
要讲清楚这件事,得先解释 HAL 库里的一个机制:__weak。
你可以把它理解成一份带备胎的合同:
HAL 库里给每个需要用户自己实现的函数,都准备了一份"空壳版本",而且标记为弱定义(__weak)。这个弱定义的意思是:
"如果用户提供了一份真正的实现,就用用户的;如果用户没提供(或者像这次一样,提供了但没被编译进去),那就用我这个空壳顶上,不报错。"
所以链接器看到的情况是:用户没有实现 HAL_MspInit。
它不报错。它不警告。它非常礼貌地把库里那个 2 字节的空函数塞了进去,然后一切照常链接,一切照常烧录,一切照常运行。
这就是这个坑最阴险的地方:
• 编译:通过 ✅ • 链接:通过 ✅ • 烧录:成功 ✅ • 运行:什么都不做 ❌
系统给你的所有反馈都是"正常",只有结果不对。
那这个空掉的 HAL_MspInit 到底该干什么活呢?
在 STM32F103 上,我要把 SPI1 重映射到 PB3 / PB4 / PB5 三个脚上。而这三个脚里的 PB3 和 PB4,出厂默认是 JTAG 调试口(PB3 = JTDO,PB4 = JNTRST)。
也就是说,这两个脚在默认状态下根本不属于你,它们是留给调试器的。你要拿它们当 SPI 用,必须先办这么几件事:
这三件事,我全都写在 stm32f1xx_hal_msp.c 里了。
代码写得没错。
但它没被编译。
所以那些寄存器配置,一行都没有执行。
于是链条就串起来了——而它是一根直线:
E 工程 .uvprojx 缺少
stm32f1xx_hal_msp.c│▼编译时找不到用户实现的
HAL_MspInit│▼链接器静默选中
HAL 库里的 __weak 空桩
(2 字节,什么都不做)│▼AFIO 时钟没开,
SWJ 配置没执行│▼PB3 / PB4 仍然被 JTAG 占着│▼__HAL_AFIO_REMAP_SPI1_ENABLE()
虽然调用了,但引脚切不过去│▼SPI1 的 SCK / MISO 不工作│▼读回数据全是 0x00
最讽刺的是最后几行。
我在代码里确实调用了"使能重映射"这个函数,那一行代码执行了,语法没错,函数也进去了。但前两步的前提没做,所以这一句就像拿到了钥匙,却发现门被人从里面锁上了。
修复只花了两分钟
真正修起来,比排查快得多。
在 Keil 的工程树里,右键 Application/User/Core 组:
Add Existing Files to Group...
选中 Core/Src/stm32f1xx_hal_msp.c → Add
然后全量重编译(Rebuild,不要用增量编译)。
我坚持全量的原因,是我在第三步发现的那些残留 .o:增量编译可能复用输出目录里的旧产物,而那个目录里本来就躺着不该有的东西。既然已经怀疑到构建环节,就干脆让它从零重来一遍,别给"看起来改了、其实没改"留机会。
修完之后,HAL_MspInit 在 .map 里就变回了一行让人安心的记录:
HAL_MspInit ...
Thumb Code 52 stm32f1xx_hal_msp.o(i.HAL_MspInit)
从 2 字节变回 52 字节。
SPI 立刻正常了。
我在这件事上记住的三件事
第一件:文件在磁盘上 ≠ 文件参与编译。
这是我最需要改的一个思维惯性。以前我看到 Core/Src 里有这个文件,就默认它"在工程里"。
顺带说个细节,很多人会把两种情况搞混:
• 文件没加进工程组:工程树里根本看不到它。(我这次是这种) • 文件加了但被排除编译(Exclude from Build):工程树里看得到,但图标是灰的,或者带个排除标记。
两种都是"不参与编译",但排查动作完全不同。前者要去 Add Existing Files,后者要去右键文件取消排除。别看到灰色就以为找到问题了,看清是哪一种。
第二件:编译链接成功 ≠ 行为正确。
__weak 这个机制,是我见过的"最礼貌的坑"。它不骂你,不拦你,什么都通过,然后在运行时给你一片 0x00。
所以我现在的习惯是:凡是"我明明写了这个函数"但行为不对,就去 .map 里查它的来源和大小。 大小不对,基本就实锤了。
第三件:__weak 之外,"有旧产物"也会骗你。
编译输出目录里残留的旧 .o 文件,可能被继续链接进去,让你误以为"我改的东西生效了"。这也是我这次要求自己用 Rebuild 而不是增量编译的原因。
这张表我建议你存下来
以后再遇到"源码明明一样、行为就是不一样"的情况,按这个顺序走:
| .uvprojx | ||
| .lnp | ||
| .map |
四步里,第四步是杀手锏。前三步都是在缩小范围,只有 .map 会直接告诉你答案。
还有一句话我想放在最后:
代码对,不代表工程对。
这句话看着像废话,但真到出问题的时候,大多数人会抱着代码看一整天——因为"我看得懂代码,我看不懂工程文件"。
我这次也是坐到椅子上,才想到要把怀疑对象换个方向。
最后聊两句:
你有没有遇到过这种"复制一个工程就坏掉"的情况?
或者更宽一点——有没有哪个问题,你查了半天代码,最后发现压根不是代码的问题?
评论区说说看,我把典型的几个挑出来,一起往下拆。
因为这类问题,光看结论记不住,非得自己也栽一次才真的记住。