本站为个人非经营性网站,仅用于技术学习与分享,不提供任何商品、服务、报价或收费咨询。 合规信息

拿到一份陌生固件工程的
前 30 分钟

不管是接手别人的代码,还是半年后回头看自己的老项目,第一件事都不该是 “先把工程打开编译一遍看看”。这篇是我在集中整理自己多个固件工程之后总结的速读流程: 按固定顺序做七件事,30 分钟内就能知道这份工程真正在跑什么、跑在什么芯片上、 有哪些“看着存在其实没被调用”的东西。

代码阅读 工程速读 方法论 踩坑复盘 已发布

一、为什么“先编译一遍”是错的顺序

我最开始读别人的工程,习惯是打开 IDE、点编译、看能不能过。这个做法有个隐蔽的坏处: 它给了你一个“这个工程是什么”的错误印象。

因为工程目录里的内容,和工程实际编译的内容,完全是两回事。 我在整理自己的工程时反复撞到这一点:

现象真实情况
目录名写着“A 功能”,里面也确实有 A 的驱动 那套驱动根本不在编译列表里,是同目录下的历史残留
一个工程目录里混着另一类产品的整套业务代码 那些文件一次都没被调用,只是复制目录时一起带过来的
目录名和实际功能对不上 例如目录名标着某个低压值,代码里实际判定的却是完全不同的量级
同一个驱动有两个版本的源文件并存 只有一个在编译,另一个是上一代的

所以正确的顺序是:先确定边界,再读内容。 先搞清楚“哪些文件真的参与编译、跑在哪颗芯片上、资源是多少”, 再去理解业务逻辑。下面是我现在的固定流程。

二、第 1 步:确认真实编译清单(约 3 分钟)

这是最重要的一步,也是最容易被跳过的一步。

Keil 工程的 .uvprojx(以及 .uvoptx)是 XML 文件, 里面每个参与编译的源文件都有一个 <File> 节点。 直接把它读出来,就是“真实编译清单”。

# 从 Keil 工程里列出真正参与编译的源文件(示意)
# .uvprojx 是 XML,所有被编译的文件都在 <File> 节点里
grep -o '<FilePath>[^<]*</FilePath>' Project.uvprojx | sed 's/.*>\(.*\)<.*/\1/' | sort

# IAR 工程看 .ewp,同样在 <file> 节点里
grep -o '<name>[^<]*\.c</name>' Project.ewp | sort

把这个列表和目录里实际存在的 .c 文件做个差集,你会得到两个集合: “在目录里但没编译”和“在编译但目录里不好找”。 前者是阅读时可以直接忽略的噪音,后者才是需要重点看的东西。

两个部分重叠的圆:左圆为“目录里存在的源文件”,右圆为“实际参与编译的文件”,不重叠的两块分别标注“历史残留”与“需要重
图 1 · 目录内容与编译清单的差集图

三、第 2 步:读出芯片的真实资源(约 3 分钟)

同一步里顺手把芯片信息也读出来。工程文件里通常直接写着器件名、厂商和内存布局:

# 从 Keil 工程里读出器件与内存布局
grep -o '<Device>[^<]*</Device>\|<Vendor>[^<]*</Vendor>\|<Cpu>[^<]*</Cpu>' Project.uvprojx

# <Cpu> 里的 IRAM / IROM 就是链接时用的内存布局,例如:
#   IRAM(0x20000000,0x0001C000) IRAM2(0x0001C000,0x00014000) IROM(0x08000000,0x00100000)
#   即 112KB + 80KB 的 RAM、1MB 的 Flash

为什么要专门读这个,而不是看芯片型号名字猜?因为型号名与资源配置不一定对得上。 我遇到过两种情况:

  • 器件字段与实际启动文件不一致。 工程里声明的型号是 A,实际引用的启动文件与系统文件却是同族的另一档 B。 这种情况必须以硬件实物为准,而不是以工程字段为准。
  • 声明的容量与型号命名规则矛盾。 型号后缀按命名规则应当是小容量,工程里声明的 Flash 却是大容量。两处必有一处是错的。

这两种都属于“不知道该信谁”的状态。遇到时我的处理方式是: 先按较小的那组资源配置来理解代码(保守假设), 然后在需要精确结论的地方(比如内存预算)标注“以实物为准”。

四、第 3 步:核对启动文件与链接脚本(约 4 分钟)

这一步决定三件事:栈多大、堆多大、程序从哪开始跑。 这三件事几乎影响后面所有的阅读。

  • 栈与堆:在启动汇编文件里,通常是 Stack_Size / Heap_Size 两个 EQU 定义。 注意这两处的数字常常是十六进制—— 看到“400”要想到可能是 0x400 也就是 1 KB,而不是 400 字节。
  • 向量表位置:看启动文件里向量表的定义位置,以及系统初始化函数里 有没有设置向量表偏移。如果程序不是从 Flash 起始地址开始跑(有引导程序), 这里必须有偏移,否则中断会跳到引导程序的向量表里去。
  • 链接脚本:GCC 工具链看 .ld,Keil 看散列文件或界面配置。 链接脚本里的起始地址与长度,是“程序能占多大”的唯一事实来源。

五、第 4 步:找时钟与引脚(约 5 分钟)

时钟决定了所有定时器、串口波特率、ADC 采样时间的换算基准,所以必须在读业务代码之前弄清。

有 CubeMX 配置文件的工程最省事,直接读 .ioc:

# CubeMX 工程:.ioc 是纯文本键值对,直接看这几个键
grep -E '^Mcu\.|^RCC\.|^PCC\.|_Signal=' Project.ioc | sort

# 关注:
#   Mcu.Name / Mcu.Package     -> 封装与型号
#   RCC.SYSCLKFreq_VALUE       -> 系统主频
#   RCC.PLLSourceVirtual       -> 用内部还是外部时钟源
#   各引脚 _Signal=             -> 引脚复用分配

没有 .ioc 的工程(Keil + 厂商库),就要去找系统时钟初始化函数, 顺着“时钟源 → 倍频/分频 → 各条总线”读一遍,把这些记下来: 系统主频、各总线频率、各定时器的时钟来源。

读的时候特别留意时钟失效的兜底逻辑: 用外部晶振的工程,如果启动时晶振没起振,程序会不会卡死在等待起振的循环里? 我见过处理得比较好的做法是“检测到外部时钟失效就切到内部时钟继续跑”, 这样至少设备还能通信、还能被远程发现,而不是彻底失联。

六、第 5 步:建立版本时间线(约 5 分钟)

这一步的价值在于:知道“哪些代码是老的、哪些是后来补的”。 老代码和新补丁的风格、严谨程度往往差别很大,读的时候心里的预期要跟着调整。

三个信息源,按可靠性排序:

  1. 版本控制历史(如果有 .git):最可靠。 看提交信息能直接读到“当时遇到了什么问题、怎么解决的”。 我读过的提交信息里有直接写“解决了跳转前死机的问题”“定时器从休眠状态唤醒后喂狗”这类, 价值非常高。
  2. 工程内的开发记录文件:很多工程会有一个按日期维护的记录, 每次修改写一行“需求 → 完成情况”。这类记录里最有用的是那些标着“未完成”的条目—— 它们直接告诉你哪些地方是有已知缺陷的。
  3. 源码注释里的日期戳:最零散,但能用来交叉验证。 看到一处注释写“某年某月修改”,可以回去和上面两个源对一下。

七、第 6 步:识别“看起来在跑其实没被调用”的代码(约 5 分钟)

这一步是我在整理时收获最大的一步。目录里那些“看着像功能”的代码, 往往有几个共同特征。

特征说明
不在编译清单里 第 1 步已经能筛出来。这类是纯粹的噪音
在编译清单里,但没有调用点 文件被加进了工程,主流程里却从没调用。这类会占用 Flash,也最容易误导人
被条件编译排除 整段代码外面套着 #if 0 或未定义的宏,读起来像是启用的
初始化函数被注释掉了 外设初始化调用写着注释符号,但驱动文件还在,看起来“这个功能是有的”
主流程中途被截断 最危险的一种:主循环里插了一段调试代码,后面所有业务代码永远不会执行

验证方法很直接:从一个已知入口出发,顺着调用链走一遍。 从主函数开始,看它调了谁;对每个业务模块,问一句“谁调用了它”。 查不出调用者的模块,就是需要单独确认的。

八、第 7 步:文档与代码冲突时以谁为准(约 5 分钟)

这一步其实是给前面几步做交叉验证。我在这轮整理里遇到的冲突类型非常集中:

冲突类型典型表现以谁为准
地址区间不一致 说明文档、用户手册、链接脚本三处给出的分区地址各不相同,甚至把两个区的角色写反 链接脚本与代码
字段长度差一 协议文档写“长度 = 整包 − 14”,代码里算的是减 13 按帧结构自己数一遍,再以对的那一方为准
流程描述比代码严格 文档说“要校验若干条件才跳转”,代码里只校验了一项 代码(但要记下来,这是隐患)
结构体布局不一致 引导程序与应用各自定义了一份参数结构体,字段数不同,却共用同一个存储地址 以实际写入的那一方为准,并把不一致记为风险

结论很朴素:文档描述意图,代码描述事实。 两者不一致时,以代码为事实依据,但把不一致本身当成一个发现记录下来—— 因为它通常意味着某一次改动只改了一半。

九、七步速读检查表

把上面七步压缩成一张表,可以照着做。

#做什么产出耗时
1从工程文件导出真实编译清单参与编译的文件列表 + 目录噪音清单3 分钟
2读器件字段与内存布局内核类型、Flash / RAM 大小3 分钟
3核对启动文件与链接脚本栈、堆、向量表位置、程序起始地址4 分钟
4找时钟与引脚分配系统主频、总线频率、定时器时钟源5 分钟
5建立版本时间线按时间的改动表 + 未完成项清单5 分钟
6识别未调用代码需要忽略的模块 + 需要注意的死代码5 分钟
7交叉验证文档与代码冲突清单(每条标明以谁为准)5 分钟

合计约 30 分钟。做完之后你会拿到四份东西: 一份真实的文件清单、一份资源账、一份时间线、一份冲突与风险清单。 有了这四样,再去读业务逻辑,效率完全不同——因为你已经知道哪些代码是活的、 哪些是死的、哪些地方本来就埋着问题。

七个方框按顺序串联:确认编译清单、读芯片资源、核对启动文件与链接脚本、找时钟与引脚、建立版本时间线、识别未调用代码、交叉
图 2 · 七步速读流程图

十、几条我反复用到的经验

  1. 先划边界,再读内容。目录不等于工程,工程不等于固件。
  2. 内存布局只信链接配置。器件名、型号后缀、目录名都可能不准。
  3. 栈和堆的数字先看进制。“400”很可能是 1 KB 而不是 400 字节。
  4. 向量表偏移是偏移类问题的总开关。程序挪了地方,中断也要跟着挪。
  5. 开发记录里的“未完成”比“已完成”更值得读。它直接标出了已知缺陷的位置。
  6. 查不出调用者的模块,先当它不存在。直到你能证明它被调用。
  7. 文档与代码冲突时以代码为准,但把冲突记下来。不一致本身就是线索。
  8. 把速读结果落成四份清单。读完不记,一周之后就得重读一遍。

参考资料与说明

  • Keil MDK 工程文件(.uvprojx / .uvoptx)与 IAR 工程文件(.ewp) 的格式说明,均为 XML 文本,可直接阅读。
  • STM32CubeMX 生成的 .ioc 配置文件格式说明。
  • ARM Cortex-M 系列技术参考手册中关于向量表、栈指针与复位序列的章节。
  • 本文归纳的做法来自个人对多个自研工程的整理过程,不针对任何具体工程或产品; 文中示例命令为按个人习惯重写的通用写法。
  • 出于对个人项目实现细节的保护,本文只公开方法与思路; 涉及核心实现的部分以步骤与字段说明代替代码,示例代码为按个人理解重写的通用片段。
  • 文中芯片与工具名称仅用于说明技术方案,与相关厂商无隶属或授权关系。