1. 先把“可移动”这三个字掰开揉碎
1.1 三个让我下定决心重做工程的真实现场
先说结论:MSPM0L1306 的 Keil 工程能不能“可移动”,跟芯片本身一点关系都没有,全在工程文件里那几十条路径上。我最早那份工程是在自己主力机上建的,SDK 装在 D 盘某个深层目录,编译一切正常,下载一次点亮,感觉良好。然后事情就开始不对劲了。
第一次是同事要接手调试,我把整个工程文件夹压缩发过去,他解压到桌面,一编译满屏红色,第一条就是找不到ti_msp_dl_config.h,第二条是L6002U: Could not open file ...\mspm0l1306.sct。我当时的第一反应是“你 SDK 装了吗”,他说装了,装在默认路径。问题恰恰就出在这——我的工程里写的是我自己的 SDL 绝对路径,跟他装的路径对不上,跟我有没有装完全是两码事。
第二次更离谱,我自己把工程从D:\work\挪到E:\backup\,同机器同 Keil 同 SDK,照样编译不过。因为.uvprojx里<IncludePath>那一长串是绝对路径,文件挪了,路径没跟着挪。第三次是半年后想回头复现一个旧版本,发现当时用的 SDK 小版本已经升级了,路径变了,老工程直接躺平。
这三次之后我就明白了:所谓“可移动的 Keil 工程”,本质上是把工程从“依赖环境”改造成“自带环境”,思路跟当年大家追的“绿色免安装软件”是一模一样的——所有能塞进自己目录的东西全塞进来,对外部世界的依赖压到最低,剩下的那部分依赖明确写清楚、单独交代。
1.2 我给“可移动”定的六条验收标准
光说“能移动”太虚,我给自己定了一套可以逐条打勾的标准,后面所有改造都是围绕这六条做的:
| 序号 | 验收项 | 具体含义 |
|---|---|---|
| 1 | 换路径可编译 | 整个工程目录整体挪到任意盘符、任意层级,不改任何配置直接 Build 通过 |
| 2 | 换电脑可编译 | 拷到另一台装了同版本 Keil 的机器上,只补装器件支持包,其余零操作 |
| 3 | 不依赖 SDK 安装路径 | 工程里不再出现任何指向 SDK 安装目录的路径 |
| 4 | 不依赖 SysConfig 在线生成 | 配置文件已固化在工程内,即使不装图形化配置工具也能编译 |
| 5 | 输出物落在工程内 | Objects、Listings、hex 全部在工程目录下,不往 C 盘用户目录乱写 |
| 6 | 可入库 | 能直接扔进版本管理,忽略规则清晰,别人 clone 下来就能用 |
这六条里,第 3 条是最容易被忽略、也是最要命的。很多人做到第 1 条就以为完事了,结果换台机器还是崩,因为 SDK 路径还挂在 Include 里,只是自己机器上恰好存在而已。
提醒一句:器件支持包(Device Family Pack)这一层是没法跟着工程走的,它装在 Keil 的全局目录里。这一点必须接受,也要在交付说明里写清楚——换机器第一件事是装包,装完就再也不管了。幻想连 pack 都打包进工程,那是给自己找麻烦。
2. MSPM0L1306 在 Keil 里的依赖全景
2.1 芯片、器件支持包与编译器这三层关系
MSPM0L1306 是 Arm Cortex-M0+ 内核的小容量型号,32KB Flash、4KB SRAM,主频 32MHz,片上带 12 位 ADC、比较器、运放、多路定时器和常见的串行接口,做小家电控制、传感器采集、电机驱动这类活儿非常合适。它便宜、外设够用,但资源确实紧,Flash 只有 32KB,这意味着你对编译产物的体积要有概念,也意味着链接脚本一丁点都不能写错。
要在 Keil 里编译它,需要的三层东西是这样的:最底下是Arm Compiler,现在基本都用 AC6(ArmClang),老工程如果是 AC5 编译的,换到新版本 Keil 上会有一堆语法和启动文件不兼容的问题;中间是器件支持包,它提供芯片的器件描述、Flash 下载算法、调试配置模板和启动文件模板,装完之后在 Keil 的器件库里能选到具体型号;最上面才是工程本身,包含你的源码、配置、链接脚本和编译选项。
关键点在于:器件支持包是全局的,工程文件是局部的。可移动工程要做的,就是把“局部”这一层做到极致自洽,让全局那一层尽可能只承担“提供编译器 + 提供下载算法”这两件不可替代的事。器件描述里那些寄存器定义、中断号,其实 SDK 的头文件里也有一份,完全可以不依赖 pack 里的 CMSIS 头。
2.2 SDK 里真正用到的其实只有一小撮文件
很多人不敢动 SDK,觉得几十兆的目录一旦缺文件就完蛋。我一开始也这么想,直到我真的把一份工程用到的文件列了一遍,才发现实际被编译进去的只有二十来个.c文件。
以 MSPM0L1306 做串口 + 定时器 + ADC 的典型应用为例,需要的东西大致分四类:
- 启动文件:Keil 专用的汇编启动文件,负责初始化栈指针、拷贝.data 段、清零.bss 段、跳 main,以及那张中断向量表。这个文件必须用 Keil 版本的那一份,IAR 和 GCC 的版本不通用。
- 驱动库:
dl_common.c、dl_interrupt.c这类基础设施,加上你实际用到的外设驱动,比如dl_gpio.c、dl_uart.c、dl_timx.c、dl_adc12.c。没用到的一个都不用加,加了纯粹浪费 Flash。 - CMSIS 内核支持:Cortex-M0+ 的核内访问接口,中断使能、系统复位、SysTick 这些。这部分体量很小。
- 配置文件:图形化配置工具生成的
ti_msp_dl_config.c和.h,里面是引脚复用、时钟树、外设初始化的具体参数。
四类加起来,源码体积也就几百 KB,拷进工程目录毫无压力。这一步做完,“不依赖 SDK 安装路径”这条就已经完成大半了。
2.3 图形化配置生成的那一层千万别断链
现在的开发流程里,图形化配置已经成了习惯——点几下鼠标配好引脚和时钟,生成代码,然后就再也不碰了。问题在于,生成这一动作本身是需要工具的,而工具路径是绝对路径。
我在老工程的 Options for Target → User → Before Build 里就发现过这么一行:
"D:\...\sysconfig_cli.bat" --product "D:\...\mspm0_sdk\.metadata\product.json" ...这行东西的杀伤力极大:在你自己机器上它能跑,一换机器就报“找不到指定文件”,而且因为它是构建前步骤,报错信息的位置很靠前,很容易被当成编译器问题去查,白白浪费一晚上。
我的处理方案是直接删掉这一步。理由很充分:配置文件已经生成好了,内容是稳定的,工程一旦定稿,日常开发根本不需要反复重新生成。真要改配置,我手动打开工具重新生成一次,把新文件覆盖回来就行,这属于低频操作,不值得为它牺牲整个工程的可移动性。
如果你确实想在工程里保留自动生成,那也建议改成下面这种形式:在系统里设一个环境变量指向工具目录,命令行里用环境变量引用,而不是硬编码盘符。但说实话,多这一层复杂度,收益不大。
3. 目录骨架:把外部依赖搬进自家院子
3.1 我最终落地的目录结构
试过好几个版本,最后稳定下来的结构是这样的,看着朴素但极其抗造:
mspm0_portable/ ├── project/ │ ├── mspm0l1306.uvprojx │ ├── mspm0l1306.uvoptx │ └── Objects/ # 编译产物 │ └── Listings/ # 列表文件 ├── source/ │ ├── main.c │ ├── app_uart.c │ └── app_timer.c ├── config/ │ ├── ti_msp_dl_config.c │ └── ti_msp_dl_config.h ├── driverlib/ │ ├── dl_common.c │ ├── dl_interrupt.c │ ├── dl_gpio.c │ ├── dl_uart.c │ └── ... ├── include/ │ ├── dl_common.h │ ├── dl_gpio.h │ ├── dl_uart.h │ └── ... ├── startup/ │ └── startup_mspm0l1306_uvision.s ├── linker/ │ └── mspm0l1306.sct └── tools/ └── pack_check.md # 换机说明几个设计上的取舍说明一下。driverlib和include我拆成两个目录而不是原样保留 SDK 的层级,是为了让 Include 路径短一点、少一点——Keil 的 Include 路径是分号分隔的一大串,越短越不容易出错。config单独拎出来,是因为这一层是“生成物”,跟手写的source性质不同,混放会导致你某天覆盖文件时搞不清哪个该覆盖。tools目录纯属心理安慰,放一份换机说明,但实际有用——接手的人第一眼就能看到“哦,还得装个包”。
startup和linker必须独立成目录,这两个文件是链接阶段的关键,任何一个路径写错,前面所有编译都白干。
3.2 必须拷贝的文件清单
我把这份清单贴在下面,照着抄就行。需要注意的是,SDK 不同小版本之间文件名可能有细微差别,但主体结构是一致的。
| 类别 | 文件 | 说明 |
|---|---|---|
| 启动 | startup_mspm0l1306_uvision.s | 必须是 uvision 版本,别拿 IAR 或 GCC 的 |
| 驱动基础 | dl_common.c/h | 公共定义、位操作宏,几乎所有驱动都依赖它 |
| 中断管理 | dl_interrupt.c/h | 中断使能、优先级、NMI 处理 |
| GPIO | dl_gpio.c/h | 引脚方向、上下拉、中断触发沿配置 |
| 串口 | dl_uart.c/h | 顺便带上dl_uart_extend.h,很多例程会引用 |
| 定时器 | dl_timx.c/h | 通用定时器,做延时和 PWM 都靠它 |
| ADC | dl_adc12.c/h | 12 位 ADC 采样 |
| 时钟 | dl_sysctl.c/h | 系统时钟源切换、分频,时钟树全靠它 |
| 电源管理 | dl_pcm.c/h | 低功耗模式切换,不用可以不加 |
| 内核支持 | cmsis_gcc.h或对应的 AC6 版本 | 内核内联函数,编译时被 include |
| 配置文件 | ti_msp_dl_config.c/h | 图形化工具生成,引脚与时钟参数在这 |
我特意没有把整份驱动库全拷进来,理由有两条:一是全拷进来大概会多出几十个用不到的.c,编译时间从几秒变几十秒,且都是白等;二是多拷了之后你会不自觉地“反正都在,随便用”,代码里引用了不该引用的东西,Flash 悄悄涨上去,等发现空间不够时已经很难砍了。只拷用到的,是这个工程能长久维护下去的关键。
有个例外:
dl_timx.c这个名字带 x,说明它对应多个定时器实例,内部的静态变量和初始化逻辑是通用的,一定要整份拷,不能只截取一部分函数。我见过有人自作聪明只拷自己用到的那个函数,结果链接时一堆未定义符号,查了半天。
3.3 链接脚本和启动文件这两个雷
链接脚本(scatter file)在 Keil 里叫.sct文件,它是把编译出来的各段数据安排到具体地址的“施工图”。MSPM0L1306 的地址空间很简单:Flash 从0x00000000起,32KB 就是0x8000;SRAM 从0x20200000起,4KB 就是0x1000。写出来是这样的:
LR_IROM1 0x00000000 0x00008000 { ER_IROM1 0x00000000 0x00008000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) .ANY (+XO) } RW_IRAM1 0x20200000 0x00001000 { .ANY (+RW +ZI) } }这段东西必须原样跟着工程走,路径指向工程内的linker目录,绝对不能指向 SDK。*.o (RESET, +First)这一行是硬要求,它保证中断向量表被放在 Flash 最开头——Cortex-M0+ 上电后从地址 0 取栈顶、从地址 4 取复位向量,放错位置芯片直接不启动,而且现象非常隐蔽:下载显示成功,程序一动不动。
启动文件同理。SDK 里的启动文件有好几个版本,文件名里带uvision的那个才是给 Keil 用的,它用的是 ARM 汇编语法;带iar的用 IAR 语法,带gcc的是 GNU 汇编,互相不通用。拷错了编译器会直接甩你一脸语法错误,这种错误好查;怕就怕拷了个“看起来能编”的同名文件,链接过了,跑起来中断进不去——那是因为向量表里的符号名和驱动库里的处理函数名对不上。
4. Keil 工程配置的逐项改造
4.1 Device 与 Target 基础设置
打开 Options for Target,第一页 Target 里,器件选择MSPM0L1306,选中后右边的 Flash 和 RAM 大小会自动填上,但不要全信它,我习惯手动核对一遍:IROM1 填0x0和0x8000,IRAM1 填0x20200000和0x1000。这两组数字和.sct文件里的必须一致,否则会出“程序跑飞”的玄学问题。
编译器版本一定要锁死,我这边统一定在 AC6。锁死的原因是不同版本的 ArmClang 对某些 C 语言特性的处理、以及生成的代码体积都不一样,同一份代码在 AC6.16 和 AC6.19 上编出来的体积能差几个 KB——对 32KB 的芯片来说,几个 KB 是能决定项目生死的。工程里选好版本之后,交付说明里注明“请使用 AC6”,别指望别人猜。
Use MicroLIB这个勾选项,我建议勾上。MicroLIB 是精简版 C 运行库,体积小,对裸机程序足够用,而且它在处理printf重定向时行为更简单。代价是它不支持某些重量级库函数,但我们这种小容量 MCU 项目基本碰不到。
4.2 Include 路径从绝对改成相对
这一页是重灾区。改造前是十几条绝对路径,改造后应该长这样:
..\include ..\config ..\source就三条,干净利落。..\表示上一级目录,是相对于.uvprojx所在的project目录而言的。这样一来,整个工程目录挪到哪里都成立,因为它表达的是“工程内部的相对位置”,而不是“这台机器上的某个绝对位置”。
三条怎么来的:include放所有.h,config放生成的配置头文件(它也被各种源文件引用,所以必须加进来),source放你自己的应用头文件。至于driverlib目录,如果你的驱动.c文件都写成#include "dl_gpio.h"这种平级引用,而头文件都在include里,那就不需要额外加路径。
这里有个容易忽略的细节:Include 路径的顺序会影响同名头文件的优先级。如果工程里有个自己的.h和 SDK 里的同名,Keil 会按路径顺序查找,先找到谁用谁。所以我把include放第一位、config第二位,保证自己的定义优先。
4.3 预定义宏与编译选项的取舍
预定义宏这一栏,MSPM0L1306 需要加__MSPM0L1306__,驱动库里的条件编译靠它来区分具体型号——不同型号的外设数量、中断号都不一样,宏不加进去,编出来的寄存器地址可能是错的,这种错误极其难查。
除此之外我还加了:
__MSPM0L1306__ USE_FULL_ASSERTUSE_FULL_ASSERT是打开驱动库里的参数断言,引脚编号、外设实例这类参数传错时会直接卡在断言里,比默默跑飞好查一百倍。当然,正式发布版本要把它关掉,不然会占 Flash 还影响速度。我的做法是在调试配置里开、发布配置里关,用 Keil 的两个 Target 分别管理。
C 标准我选gnu11。选它是因为驱动库的代码里有少量 GNU 扩展(比如指定对齐、内联汇编),用严格c99会报警告。优化等级调试阶段用-O0,发布用-Os(优化体积)。注意-O0编出来的代码体积会明显大于-Os,如果你的工程在-O0下 Flash 占用已经逼近 30KB,那发布版反而是在赌,一定要实测。
4.4 链接器与 Scatter 文件的绑定
Linker 页签里,取消勾选Use Memory Layout from Target Dialog,然后手动指定 scatter file 为..\linker\mspm0l1306.sct。这一步很多人会漏,结果就是:Target 页里填的地址覆盖了.sct文件里的内容,两边一旦不一致,行为和你想的完全不同。
为什么建议手动指定而不是用对话框?因为对话框里的配置存在.uvprojx里,而.sct是独立文件,两者是两套真相。手动绑定.sct之后,地址空间只有一份定义,维护成本直接减半。而且.sct是纯文本,写注释、做版本对比都方便。
链接失败时的报错里,L6220E、L6406E这类是区域溢出,说明你的代码或数据超出.sct里定义的大小了。32KB Flash 的芯片很容易撞这个墙,撞了不一定是代码写多了,也可能是.sct里的0x8000被误写成0x800——我见过一次,查了三小时,最后发现少写一个零。
4.5 调试器、下载算法与输出目录
Debug 页签选你的调试器,CMSIS-DAP 或者 XDS110 看手头有什么。Settings 里 Flash Download 那一栏必须能看到MSPM0L130x系列的下载算法,这个算法来自器件支持包。如果这一栏是空的,说明支持包没装好或装错版本,这是换机器后最常见的卡点。
勾上Reset and Run,不勾的话下载完程序停在复位状态不动,新手百分之八十会以为“下载失败”。
Utilities 页签里,Output目录改成.\Objects\,Listing目录改成.\Listings\。默认设置会往 C 盘用户目录下的临时路径写,换机器后这些路径没了,虽然不影响编译,但你的.hex就找不着了。改成工程内相对路径,好处是清理和备份都简单——整个目录一压缩,产物和源码都在。
还有一点:Create HEX File建议勾上。虽然调试用.axf,但交付给生产或者给别人烧录时,.hex和.bin更通用。输出名我习惯用${ProjectName}_${TargetName},一眼能看出是哪个配置的产物。
5. 藏得最深的绝对路径:手工体检 .uvprojx
5.1 用文本编辑器直接打开那个 XML
Keil 的工程文件.uvprojx本质是个 XML,所有配置都在里面。图形界面能改的只是一部分,还有一些历史残留改不掉,只能手工处理。我的流程是:右键 → 用文本编辑器打开 → 全局搜索:后面跟盘符的字符串,比如C:\、D:\、E:\,一个都别放过。
会搜出来的东西大概有这几类:
<IncludePath>里的绝对路径(第 4.2 节已经改过)<FilePath>里的源文件路径,如果源文件是从 SDK 直接“添加已存在文件”加进来的,这里就是绝对路径<ScatterFile>里的路径<OutputDirectory>、<ListingPath>- 各种
<Misc>里被塞进去的自定义选项
第一类改完就没了;第二类要特别注意,因为它是逐个文件的,工程里有二十个文件就有二十处,漏一处编译就崩。解决办法很干脆:先删掉所有源文件分组,再用“添加已存在文件”从工程内的source、driverlib、config目录重新加一遍,加进去的自动就是相对路径。这个操作十分钟能干完,比一个个改路径靠谱得多。
5.2 我的替换清单和验证方法
改完之后我会做一轮自查,逐个确认:
- 所有
<FilePath>都以..\或文件名开头,没有盘符 <IncludePath>只有相对路径<ScatterFile>指向..\linker\<OutputDirectory>是.\Objects\<ListingPath>是.\Listings\- 没有残留的自定义构建步骤(Before/After Build)
- 没有
<RTE>里指向全局 pack 目录的绝对路径——这一条比较隐蔽,如果你的工程用了 RTE 组件,XML 里会有RTE_Components.h的生成配置,路径是全局的。
验证方法最有效的是“改目录名测试”:把工程父目录改一个名字,重新打开 Keil 编译。如果路径写死了,改名必然报错;如果全是相对路径,改了名照样通过,而且编译成功那一刻你就知道改造真的成了。这个测试我每次交付前都做,三十秒的事,能避免后面几百句解释。
另外一个经验:<TargetName>和<OutputName>保持英文和数字,不要用中文和空格。Keil 对文件路径里出现空格和中文的处理一向不算稳妥,有些老版本在带空格的路径下链接会报莫名其妙的错误,排查起来极其费神。工程名、目录名、输出名,一律用下划线和英文,这个习惯能救你很多次。
6. 三次迁移实测记录
6.1 同机器换路径:最基础的一关
改造完成后第一次测试是把mspm0_portable从D:\work\直接剪切到E:\test\。打开.uvprojx,Rebuild,32 个文件编译,链接通过,输出 12.4KB 的 hex。这一步过不代表万事大吉,但它是必要条件。
这一步最容易暴露的就是<FilePath>里的绝对路径,因为 Include 路径改起来显眼,文件路径藏在每个 Group 下面,得一个一个点开看。
6.2 换电脑同版本:真正检验可移动性
第二关是拷到另一台机器。那台机器装了 Keil,但没装器件支持包。打开工程,Device 那一栏显示黄色的警告,编译报找不到器件定义,下载算法列表是空的。
装上支持包,重启 Keil,一切正常,Build 通过,下载运行也正常。这个结果说明我的依赖切割是对的:唯一的全局依赖就是支持包,而且这个依赖是可以在交付文档里写清楚、一次性解决的。
这里我踩过一个小坑:那台机器上的 Keil 是更新版本,默认编译器版本变成了新版本,导致我原本锁定的 AC6.16 不可用。解决方式是在 Target 页里显式选择编译器版本,选不到就去装对应版本的编译器。这也是为什么我强调在交付文档里写明编译器版本——不然你永远不知道对方机器上是哪一版。
6.3 换电脑不同版本:记录一下失败案例
第三关是拿一台装了更老版本 Keil 的机器试。结果并不理想:老版本不支持我用的某个 C 语言特性,编译报语法错误;同时老版本器件支持包的版本也很旧,下载算法对不上。
这件事让我明确了一个边界:可移动 ≠ 跨版本兼容。可移动工程的前提是“同版本工具链”,工具链差异带来的问题是另一回事,指望一个工程文件兼容所有版本的编译器和器件包是不现实的。所以我的交付说明第一条就写:Keil 版本不低于某个具体版本,编译器用 AC6,器件支持包装对应版本。写清楚,比事后扯皮强。
7. 常见问题排查速查表
7.1 编译阶段:找不到头文件
这是最高频的问题。报错形式是cannot open source input file "xxx.h"。排查顺序:
先确认这个头文件在不在工程目录里(不是 SDK 目录里)。在的话,再看 Include 路径里有没有它所在的目录——注意路径是相对于.uvprojx的,不是相对于项目根目录。曾经有个案例,工程文件和源码差了三级目录,路径写成..\include实际指向了别的地方,改成..\..\..\include才通。这种错误编译器不会告诉你“路径指错了”,只会说“找不到文件”。
如果文件确实存在、路径看着也对,检查一下文件名大小写。Windows 上不敏感,但一旦工程是别人从别处给的或者以后要在别的系统上处理,大小写不一致就会出问题。
7.2 链接阶段:区域溢出与符号未定义
区域溢出(L6220E等)先看.sct里的地址和大小写对没有,再确认优化等级。我在-O0下撞过一次,改-Os之后腾出 4KB 空间,问题就没了;但如果代码本身真的膨胀到接近 32KB,那该做的是砍功能、砍库函数,而不是靠优化硬顶。
符号未定义(L6218E)通常有三种可能:驱动.c文件没加进工程、驱动版本和头文件不匹配、某个extern的变量在对应源文件里被条件编译掉了。第三种最难查,因为编译是通过的,只在链接阶段暴露。我的做法是直接去搜索符号定义,看它所在的代码块被哪个宏包着。
7.3 下载阶段:连不上、下载成功但不跑
连不上调试器先查硬件:线序对不对、目标板供电有没有、SWD 两根线有没有被别的东西占用。软件侧查 Debug 页签里的端口设置,SW还是JTAG,时钟频率降到 1MHz 试试。
下载成功但不跑,十个里有八个是向量表位置不对。用调试器暂停,看 PC 值——如果 PC 还在启动文件里的死循环,说明没跳到 main,那多半是.sct的+First丢了,或者启动文件不是 uvision 版本。还有一种可能是链接地址和下载算法的擦写范围对不上,.sct里写0x0起,但你下载算法配的是别的起始地址,烧进去的程序其实不在它该在的位置。
7.4 一张表看完全部高发问题
| 现象 | 大概率原因 | 处理动作 |
|---|---|---|
找不到ti_msp_dl_config.h | Include 路径缺失或写错 | 检查..\config是否在路径列表 |
找不到dl_*.h | 驱动头文件未拷入include | 补齐头文件并确认路径 |
L6002U打不开 sct | Scatter 路径写死或文件丢失 | 指向..\linker\并确认文件在 |
L6220E区域溢出 | 代码/数据超限或 sct 尺寸写错 | 核对0x8000、0x1000,调优化等级 |
L6218E符号未定义 | 驱动源文件未加入工程 | 补加对应.c并 Rebuild |
| 下载算法列表为空 | 器件支持包未装或版本不符 | 重装对应版本支持包 |
| 下载成功但程序不动 | 向量表位置错或启动文件不匹配 | 检查.sct的+First、启动文件后缀 |
| Build 前步骤报错 | 残留了绝对路径的自定义构建命令 | 删除该步骤,手动生成配置 |
| 换机器后一段时间正常,随机崩 | 编译器和器件包版本不一致 | 锁定 AC6 版本并记录 |
8. 让这个便携工程更好用的几个附加动作
8.1 Git 忽略规则要一次写对
既然做成了可移动工程,顺手入库是自然的事。忽略规则里必须排除的有:Objects/、Listings/、*.uvoptx(这个文件记录窗口布局和断点,属于个人偏好,提交了会天天冲突)、各种*.bak、*.dep。要保留的是.uvprojx、所有源码、启动文件、.sct、配置文件。
有个细节值得注意:.uvoptx里也会存绝对路径(断点位置、打开的文件夹),如果它进了库,别人 clone 下来打开会看到一堆指向你机器的失效路径,虽然不影响编译,但观感很糟,而且容易误导人以为工程没改造干净。
8.2 写个一键打包脚本
我写了个极简的批处理,做完验证后一键打包发布:
@echo off set SRC=mspm0_portable set DST=release\mspm0_portable_v1.0 if exist %DST% rd /s /q %DST% mkdir %DST% xcopy %SRC%\project %DST%\project /E /I /Y xcopy %SRC%\source %DST%\source /E /I /Y xcopy %SRC%\config %DST%\config /E /I /Y xcopy %SRC%\driverlib %DST%\driverlib /E /I /Y xcopy %SRC%\include %DST%\include /E /I /Y xcopy %SRC%\startup %DST%\startup /E /I /Y xcopy %SRC%\linker %DST%\linker /E /I /Y xcopy %SRC%\tools %DST%\tools /E /I /Y打包的时候刻意跳过了Objects和Listings——产物不该跟着源码走,接收方自己编译一遍才是干净的。这个脚本简陋但有用,特别是需要频繁给不同人发工程的时候。
8.3 把版本信息钉在代码里
最后一个小技巧,我觉得收益比想象中大。在main.c里加几个常量字符串,编译进去:
const char fw_ver[] = "V1.0.3"; const char fw_date[] = __DATE__ " " __TIME__; const char tool_ver[] = "AC6";然后把这些字符串放到一个固定的、不会被优化掉的位置(比如加volatile或者放进一个专门的段)。好处是:任何时候拿一个烧录好的板子,你用调试器读一下这段内存,就知道它是哪个版本的固件、什么时候编的、用什么编的。工程的可移动性解决的是“能编”,这几个字符串解决的是“能追溯”,两件事合起来,才算是把一个工程真正管起来了。
我在实际交付里有次就靠它救过场:对方说“你给的固件有 bug”,我一读日期,那是三个月前的旧版本,根本没经过最近两轮修复。省了一场毫无意义的争论。
反复打磨这个工程的过程,让我对“工程管理”这件事的看法变了不少。以前总觉得写代码才是正事,配置、路径、目录结构这些属于杂活,能跑就行。真被坑过几次才发现,这些杂活恰恰决定了你的代码能不能被别人用起来,能不能在半年后还被自己认出来。MSPM0L1306 这颗芯片本身没什么脾气,32KB Flash 虽然紧,但规划好了够用;真正让人抓狂的从来都是那些看不见的路径和依赖。把这些理清楚,剩下的就是把精力老老实实花在业务逻辑上了。