MSPM0L1306 Keil工程可移动化改造实战 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现在基本都用 AC6ArmClang老工程如果是 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 处理GPIOdl_gpio.c/h引脚方向、上下拉、中断触发沿配置串口dl_uart.c/h顺便带上dl_uart_extend.h很多例程会引用定时器dl_timx.c/h通用定时器做延时和 PWM 都靠它ADCdl_adc12.c/h12 位 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 就是0x8000SRAM 从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和0x8000IRAM1 填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放所有.hconfig放生成的配置头文件它也被各种源文件引用所以必须加进来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. 藏得最深的绝对路径手工体检 .uvprojx5.1 用文本编辑器直接打开那个 XMLKeil 的工程文件.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\。打开.uvprojxRebuild32 个文件编译链接通过输出 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.hInclude 路径缺失或写错检查..\config是否在路径列表找不到dl_*.h驱动头文件未拷入include补齐头文件并确认路径L6002U打不开 sctScatter 路径写死或文件丢失指向..\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 SRCmspm0_portable set DSTrelease\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 虽然紧但规划好了够用真正让人抓狂的从来都是那些看不见的路径和依赖。把这些理清楚剩下的就是把精力老老实实花在业务逻辑上了。