ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Keil 编译生成 HEX 文件:配置、排错与批量构建实战

Keil 编译生成 HEX 文件:配置、排错与批量构建实战 手上有一块板子代码在 Keil 里敲完了点一下 BuildBuild Output 里蹦出0 Error(s), 0 Warning(s)然后就卡在下一句HEX 文件呢这个问题我在技术群里被问过太多次。Keil 下编译代码并生成 hex 文件说穿了就是把工程里的一堆 .c、.h、.s 文件交给工具链一路翻译成芯片认识的机器码最后打包成一份带地址信息和校验和的 Intel HEX 文本文件交给烧录器写进 Flash。道理简单真上手却容易踩坑选项勾了却找不到文件、老工程换编译器一片红字、生成的 hex 烧进去板子不跑、大工程编译一次要等好几分钟。这篇东西写给三类人看。第一类是刚接触单片机的同学从 Arduino IDE 或者图形化工具转过来第一次看到 Keil 那一排菜单有点懵第二类是做过项目但一直让同事帮忙出 bin/hex 的人想自己把这条链路摸透第三类是要把固件交付给产线或者放进自动构建流程的人需要稳定、可复现地批量出 hex。不管你在哪一类读完之后你应该能做到新建或者导入一个工程把编译选项配好按一个键拿到 hex并且知道这份 hex 里到底装了什么、出了问题从哪一层开始查。1. 先搞清楚Keil 的一次编译到底做了哪些事1.1 从 .c 到 .hex 的完整链条很多人把编译当成一个动作其实它是五步接力。第一步是预处理把#include的头文件内容原地展开处理#define宏和条件编译生成中间文件。第二步是编译把预处理后的 C 代码翻译成汇编这一步是真正做语法检查、类型检查、优化决策的地方。第三步是汇编把汇编代码转成可重定位的目标文件.o里面是机器码加上一堆还没确定地址的符号引用。第四步是链接链接器把所有 .o 文件、库文件按你的内存布局拼起来决定每个函数、每个变量最终落在哪个地址同时把符号引用填实输出 .axf 文件。第五步才是格式转换把 .axf 里的代码和数据段提取出来按 Intel HEX 格式写成文本。这里面最关键、也最容易被忽略的是第四步。链接器要解决的问题是谁放哪儿它得知道你这颗芯片的 Flash 从哪个地址开始、有多大RAM 又从哪开始、有多大。这些信息从哪来来自 Options for Target 里 Target 选项卡的 IROM1/IRAM1 配置或者来自你自己写的散布加载文件scatter file。地址配错了编译能过链接也可能过但烧进去必然跑飞。我见过有人把芯片型号选成了同系列但 Flash 更大的那款编译器不报错hex 出来了烧录器也写进去了就是板子不亮——因为程序被链接到了这片芯片根本不存在的地址段。所以你在 Keil 里看到的 Build 按钮实际上是把这五步按顺序跑了一遍。Build Output 窗口里那些compiling main.c...、linking...、Program Size: ...就是每步的进度提示。理解了这条链后面所有报错你都能大致定位到是哪一步出的问题。1.2 HEX 和 BIN 到底差在哪儿什么时候必须用 HEX这两个格式经常被混着说但它们的用途不太一样。BIN 是纯二进制就是一片内存的原始镜像从你指定的起始地址开始一个字节挨一个字节排下去文件里没有任何多余信息。它体积最小烧录最快但它有两个致命缺陷一是不知道自己该烧到哪个地址烧录工具必须额外告诉你起始地址二是没有校验传输或者拷贝过程中坏了一个字节烧录工具发现不了。HEX 是文本格式Intel HEX 的每一行都是一条记录格式是冒号开头后面跟长度、地址、记录类型、数据和校验和。举例来说一行:020000040800F2的含义是总长度 0x02 字节地址字段 0x0000记录类型 0x04扩展线性地址记录数据是 0x0800意思是接下来所有数据的地址高 16 位是 0x0800。STM32 的 Flash 起始地址正好是 0x08000000所以每条完整记录的地址就是 0x0800 打头拼上低 16 位。最后一行通常是:00000001FF记录类型 0x01表示文件结束。校验和那一字节是把前面所有字节相加后取反加一烧录器读进来会自己算一遍比对对不上就报错。记录类型一共五种常用的就三种0x00 是数据记录0x01 是文件结束0x04 是扩展线性地址32 位地址的高 16 位。另外还有 0x02 扩展段地址和 0x05 起始线性地址一般在 Cortex-M 上不太常见。什么时候必须用 HEX一个典型场景是给产线或者客户交付固件文件是纯文本邮件里丢过去不会因为编码问题损坏对方烧录工具校验不过会立刻报错比 BIN 安全得多。另一个场景是带 Bootloader 的双区升级Bootloader 和一个或多个 App 分别生成 HEX 之后再合并HEX 里自带地址信息合并工具不用你再手动指定偏移一把梭就行。反过来如果你要做差分包、做 OTA 压缩传输那 BIN 更合适体积小、处理起来直观。1.3 别忽略中间产物 .axf它才是真正的宝库大多数人拿到 hex 就完事了其实编译输出目录里的 .axf 文件才是信息最全的那个。它是一个 ELF 格式的可执行文件里面除了代码和数据还有完整的符号表、调试信息、段布局信息。你可以用 Keil 自带的 fromelf 工具把它拆开来看反汇编、符号列表、段大小、内存占用统统能拿到。我调试内存溢出的时候有个习惯先出一份反汇编和一份段大小列表看看哪个函数占了最多 Flash哪个数组把 RAM 吃满了。这一步比盲目改代码有效得多。命令是这样用的fromelf --text -c --outputObjects\app.dis Objects\app.axf fromelf --text -z --outputObjects\app.sizes Objects\app.axf第一条把反汇编输出到 app.dis第二条把各段的大小和起始地址输出到 app.sizes。打开 app.sizes你会看到每个段的加载地址、执行地址、大小还能看到Total RO Size、Total RW Size这些汇总。这时候再回头看你改的那几行代码到底省了多少字节心里就有数了。2. 生成 HEX 的开关就藏在 Output 选项卡里2.1 Create HEX File 这一项为什么总被漏勾打开 Options for Target快捷键 AltF7切到Output选项卡中间偏上有个复选框叫Create HEX File。勾上它编译完 Keil 就会在输出目录里生成同名 hex。旁边通常还有一个格式下拉框MDK 里一般是 HEX-80 和 HEX-386 两个选项默认 HEX-80 就够用它是标准的 8 位 Intel HEXHEX-386 是 32 位变体某些老烧录器只认这个遇到烧录器报格式不支持的时候可以换过来试试。这里有个细节值得说新建工程时这个选项默认是不勾的。为什么因为 Keil 的定位是完整开发环境你写完代码第一件事习惯是点 Debug 下载调试而不是导出一个 hex 交给别人烧。调试走的是调试器直接加载 .axf 的路子根本不需要 hex。所以官方默认关掉它算是一种按需开启的设计。但对于要给产线交文件的人来说这一步每次都要记得打勾忘了就是白编译一场。另一个常被忽略的位置是Select Folder for Objects也就是输出目录。默认是工程文件同级目录下的 Objects 文件夹。如果你的工程路径太长、带中文、或者放在同步盘里很容易出现编译中途写文件失败。我一般会把它改成一个短路径比如D:\build\proj_obj省心。2.2 勾了选项却没生成文件八成是这几个原因第一种情况是你编译的不是这个 Target。一个工程里可以有多套 Target 配置比如 Debug 和 Release两套的内存布局、优化等级、甚至源文件列表都可能不一样。你在 Release 的 Output 里勾了 hex结果当前选中的是 Debug那自然出不来。看 Build Output 窗口第一行的Build target xxx就能确认。第二种情况是只做了 Translate 没做 Build。工具栏上那个单文件编译按钮只编译当前文件不会触发链接更不会生成 hex。必须点 BuildF7或者 Rebuild全部重编才走完整链路才会执行转换步骤。第三种情况是编译其实失败了。Build Output 最后一行显示1 Error(s)的时候链接阶段直接中断后面的格式转换根本不会执行输出目录里可能只有一堆 .o 和半截 .axf。这时候别急着找 hex先把报错解决掉。第四种情况是被杀毒软件或者安全策略拦了。这个坑比较隐蔽表现是编译日志显示一切正常但输出目录里就是没有 hex或者有个 0 字节的同名文件。把输出目录加进杀软的白名单问题就没了。2.3 用 fromelf 在后构建里定制输出比勾选项更灵活勾选 Create HEX File 只能出 hex而且命名、路径都是 Keil 说了算。如果你还想同时出 bin、出反汇编、出带时间戳的文件名那就得用User 选项卡里的后构建命令。在 Options for Target 里切到User选项卡找到After Build/Rebuild那一栏勾上Run #1在输入框里写fromelf --i32combined --output.\Objects\L.hex .\Objects\L.axf这里L是 Keil 的按键码会被自动替换成当前目标输出文件的名字不含扩展名。如果你想同时产出一份 bin可以再用 Run #2 写fromelf --bincombined --output.\Objects\L.bin .\Objects\L.axf--i32combined生成的是合并后的 Intel 32 位 HEX--bincombined生成合并后的纯二进制。注意它们和 Output 选项卡里那个复选框是两条独立的路子可以同时存在也可以只用一条。我一般两种都留着因为产线要 hex内部做批量烧录的脚本要 bin。提示后构建命令里的路径一律用反斜杠并且用引号包住否则路径里出现空格就会执行失败而 Keil 给出的报错信息往往只有一句命令执行错误很难定位。3. 环境搭不对后面全是玄学问题3.1 器件包Pack的安装与路径迁移MDK5 之后芯片支持不再靠装一堆独立的驱动包而是靠CMSIS Pack。新建工程时你选的器件型号本质上就是从某个 Pack 的数据库里读出来的它决定了内存布局模板、启动文件、系统初始化代码。装不上 Pack 或者装错了 Pack 版本会出现一堆莫名其妙的现象器件列表里搜不到你的芯片、编译报缺system_xxx.c、链接报__initial_sp未定义。Pack 默认装在用户目录下路径大致是C:\Users\用户名\AppData\Local\Arm\Packs。这个位置有两个问题一是藏在 AppData 里清理垃圾的时候容易被误删二是如果用户目录在系统盘一堆 Pack 加起来能占好几个 G。我的做法是把它整体挪到 D 盘然后在系统环境变量里加一个CMSIS_PACK_ROOT指向新路径重启 Keil 就认了。这样以后重装系统、重装 KeilPack 都不用再下一遍。安装 Pack 常见两种方式一是在 Keil 的 Pack Installer 里在线下载二是去芯片厂商官网下.pack文件双击安装。在线下载偶尔会卡住或者报网络错误遇到这种情况不要一遍遍重试直接去官网下离线包更靠谱。安装完成后记得在 Pack Installer 里看一下版本号有些新版本 Pack 会改动启动文件导致旧工程的链接脚本对不上这种情况在项目中期千万别随便升级。3.2 Arm Compiler 5 还是 6选错就是一堆红字MDK5.37 之后默认的编译器换成了Arm Compiler 6它是基于 LLVM/Clang 技术路线的诊断更严格优化能力更强代码体积通常也更小。但代价是——大量老工程直接编译不过。原因主要有三一是汇编文件的语法不一样老工程用的是 armasm 的语法AC6 用的是集成汇编器接近 GNU 风格二是 C 语言方言更严格以前能过的一些隐式类型转换、未声明函数现在直接报错三是内联汇编的写法变了__asm那一套在 AC6 下要换成__asm volatile或者独立的.s文件。所以我的建议很直接接手老工程先看它原来用的哪个编译器能不动就不动。在 Options for Target 的 Target 选项卡里有个编译器版本选择框装上对应版本的编译器组件锁定版本就行。新开的工程直接用 AC6把警告当成错误来对待长期看是省事的。还有一个常被吐槽的点AC6 的全量编译确实比 AC5 慢一些尤其是在开了链接时优化LTO之后。如果你的项目本来就大又开了最高优化等级编译一次等两三分钟很正常。这个后面第 5 节会专门讲怎么优化。3.3 授权与许可证相关的报错怎么判断编译时报出和许可证有关的错误本质上是工具链没拿到有效的授权。正规做法是通过官方渠道获取授权企业用户走批量授权个人学习用免费版或者社区版如果你的器件和代码规模在限制范围内。这里我不展开别的路子因为绕过授权的做法既不合规工具链也可能被注入不明内容编译出来的固件跑在产线上是要出事的。遇到授权报错时先确认三件事Keil 的安装目录是不是在受保护的系统目录下比如 Program Files当前用户有没有写权限许可证文件的位置有没有被移动或者覆盖系统时间是不是正常。系统时间被改过会导致授权校验直接失败这个坑我在一台测试机上踩过一次查了半天。注意不要从不明来源下载所谓的激活工具或者补丁包。工具链一旦被替换或注入编译产物不可信出了问题排查成本极高而且合规风险全在自己身上。4. 一次完整的编译与 HEX 生成实操4.1 工程目录与文件分组先说我推荐的目录结构。源码和输出分离是最基本的别把 Objects、Listings 这些中间目录和 .c 文件混在一起否则你哪天要打个包发给别人压缩包能多出几百兆。我一般这么分Project/ ├── App/ 应用层代码 ├── Bsp/ 板级驱动 ├── Drivers/ 芯片厂商库或 HAL ├── Middlewares/ 中间件比如 RTOS、协议栈 ├── StartUp/ 启动文件 ├── MDK-ARM/ 工程文件和输出目录 │ ├── Project.uvprojx │ ├── Objects/ 编译输出 │ └── Listings/ 列表文件 └── Doc/ 文档然后在 Keil 里用Manage Project Items工具栏那个带文件夹图标的按钮建分组把文件按目录归类。分组只是显示上的归类不影响编译但一个几百文件的工程如果全堆在一个分组里找文件能找瞎眼。另外每个分组下面建议保持文件顺序稳定加了新文件就追加到末尾别随意拖动否则代码对比工具的差异会很难看。4.2 关键编译选项逐个说清楚C/C 选项卡里的东西最多逐个过一遍Define宏定义。这是给编译器传-D参数的地方。HAL 库的工程通常需要USE_HAL_DRIVER和具体型号的宏比如STM32F103xB。这两个宏缺一个就会报大量未定义符号。多个宏之间用英文逗号分隔不要用空格也不要带-D前缀。Include Paths头文件路径。这里填的是目录不是具体文件。每个路径单独一行。一个常见错误是路径写成了相对路径但基准不对——Keil 里相对路径的基准是工程文件所在目录不是源文件目录。乱七八槽的..\..\..基本都出在这种地方。建议统一用相对于工程文件的路径看着清晰也不容易错。Optimization优化等级。Options 里一般给 -O0、-O1、-O2、-O3 几档AC6 下还有针对体积的选项。调试阶段一律用 -O0原因很简单高优化等级下编译器会把变量塞进寄存器、会把顺序打乱、会内联函数你单步调试的时候看到的行号和变量值会和源码对不上断点还可能被优化掉那种断点飘了的感觉会让人怀疑人生。发布阶段再切到高优化通常能省下 20% 到 40% 的 Flash。One ELF Section per Function。这个选项在 AC5 里叫--split_sections作用是让每个函数单独占一个段。听起来是个内部细节实际影响很大链接器在做段回收的时候只有函数独占段它才能安全地丢掉那些没被调用的函数。整个工程编译成一个段的话只要这个段里有任何一个函数被引用整段代码就都会被塞进最终镜像。开启这个选项再配合--remove让链接器做未使用段回收一个用了 HAL 库但只调了几个函数的工程体积能从几百 KB 掉到几十 KB效果立竿见影。Use MicroLIB。这是 ARM 提供的一个精简 C 库砍掉了不少不常用的功能代码尺寸明显更小。代价是有些不常用的标准库函数不可用或者行为有差异比如浮点格式化输出、某些 locale 相关的函数。用 RTOS 的工程开这个选项要谨慎因为 MicroLIB 对可重入的支持和标准库不完全一致。我自己的判断标准是裸机项目、不太用标准库的开带 RTOS 或者大量用printf格式化的先不开实在缺 Flash 再逐个验证。C99 Mode / C11 Mode。现在写代码基本都会用到for(int i0;...)或者混合声明把 C99 打开就行了成本为零。要是你的编译器版本支持更高的标准也可以开 C11。Warnings 等级。默认是中间档我的习惯是直接开到最严。嵌入式代码里的隐式类型转换、有符号无符号比较、未使用变量这些警告背后往往就是真 bug。工程初期就把警告清零比上线前突击查要轻松得多。当然接手老工程的时候不能这么干一开全是红的先记下来逐步清理。4.3 看懂 Program Size 里的 RO、RW、ZI编译成功后Build Output 最后会打印一行类似这样的东西Program Size: Code11836 RO-data1620 RW-data64 ZI-data2600这四个数不是随便报的它们直接对应 Flash 和 RAM 的占用。先解释概念Code是机器指令也就是你写的所有函数编出来的代码。RO-data是只读数据包括const修饰的常量数组、字符串字面量、以及代码里用到的各种查表数据。RW-data是带初值的可读写变量比如int g_flag 5;它的初值 5 存在 Flash 里程序启动时由启动代码搬运到 RAM。ZI-data是零初始化的可读写变量比如int g_buf[100];它不占 Flash启动时由启动代码把 RAM 里对应区域清零。所以算 Flash 占用是Flash Code RO-data RW-data 11836 1620 64 13520 字节 约 13.2 KB算 RAM 占用是RAM RW-data ZI-data 64 2600 2664 字节 约 2.6 KB注意 RW-data 被算了两遍——它在 Flash 里存了一份初值在 RAM 里也要占一份空间这是正常的不是重复计算。这组数字最实用的地方是提前发现内存不够。你堆栈还没算进去呢芯片实际能用的 RAM 还要留出栈空间启动文件里Stack_Size定义的那个值常见 0x400 就是 1KB和堆空间。这俩通常用__initial_sp和堆指针之间的间隔来算。我一般会在工程里留一条规则ZI RW 加上栈堆别超过芯片 RAM 的 80%留出来的余量给临时性的递归、大数组初始化、动态分配用。超过这个线后面加一个功能就可能栈溢出而栈溢出的表现是随机的跑飞最难查。4.4 出 HEX 并做一次基础校验配置到位之后流程就一句话勾上 Create HEX File按 F7 编译去 Objects 目录拿文件。拿到文件之后别急着烧先做三件事。第一看文件大小和时间戳。一个正常固件的 hex 是几百 KB 到几 MB 量级的文本文件因为每个字节被展开成了两个 ASCII 字符还加了地址和校验体积大约是原始镜像的三倍多。如果你看到的是 0 字节或者几 KB那肯定是编译没走完。时间戳也要对确保你看的是刚生成的那一份而不是上次遗留的。第二核对地址范围。用记事本打开 hex看第一行数据记录里的地址。拿 STM32F103C8T6 举例Flash 起始是 0x08000000你应该能看到扩展线性地址记录:020000040800F2意味着后续地址高 16 位是 0x0800。如果看到的是别的数值说明链接脚本或者 Target 里的 IROM 配置写错了。第三做个交叉验证。同一个 .axf 既生成 hex 也生成 bin然后用烧录工具把 bin 按正确偏移烧进去读回 Flash 做比对。或者干脆用烧录工具的校验功能烧完读回比对一遍。这一步在手工调试阶段可以省但产线固件交付前强烈建议保留因为一致性问题的代价往往比多做一次校验高得多。5. 报错、HEX 异常、编译慢的排查速查5.1 编译期与链接期的典型报错工具链的报错分两类编译期和链接期。区分方法很简单看它说的文件类型——提到 .c、.h 的就是编译期提到 .o、.axf、section 的就是链接期。编译期最常见的是找不到头文件报错长这样fatal error: xxx.h: No such file or directory。原因基本就是 Include Paths 漏了目录或者目录写错了。也有一种情况是文件确实不存在——有些库是你从别处拷过来的只拷了 .c 没拷 .h。这时候按报错信息里那个文件名在整个磁盘搜一下看它到底在哪儿比盯着工程设置猜要快。第二类语法和类型错误。AC6 下比 AC5 严格得多典型的报错是隐式函数声明、指针类型不匹配、有符号无符号混用。这些老代码里特别多。处理方式有两条路一是老老实实改代码短期慢长期稳二是暂时在编译选项里加宽容参数把这类检查降级先把工程跑起来再逐步清理。我一般倾向第一条但如果这个工程明天就要出样机那就先降级记个 TODO 后面还债。链接期最常见的三类报错我在下面整理了成表格。报错关键词典型含义常见处理方向Undefined symbol xxx某个函数或变量被引用但没找到定义检查源文件是否加入工程、库文件是否链接、函数名拼写是否一致No space in execution regions某个段放不进指定的内存区域优化体积、开启段回收、调整内存布局或升级芯片型号Multiply defined symbol xxx同一个符号被定义了多次检查是否有全局变量定义写在了头文件里、是否重复加入了同一个源文件Cannot open source input file找不到源文件或头文件检查路径、检查文件名大小写、检查文件是否真的存在于磁盘Not enough information to list image symbols链接中断符号表无法生成先解决前面的报错这通常是个连带信息No space in execution regions这个报错值得多说两句。它经常不是真的 Flash 满了而是某个段被放到了一个容量很小的区域里。比如你的 scatter 文件里给某个.ARM.__at_段指定了一个固定地址结果那段代码变大之后越界了。这种情况看链接器给出的详细输出它会明确告诉你哪个段、多大、要放到哪儿、那个区域还剩多少对着数字改就行。Keil 的输出窗口里点开链接那一段信息都在。5.2 HEX 没生成、不更新、大小异常的三种典型情形情形一文件根本不存在。排查顺序是这样的先看 Build Output 最后一行是不是0 Error(s)不是的话先解决错误是的话看编译日志里有没有执行格式转换那一步勾选复选框的方式不会有明确日志用 User 后构建命令的话会打印执行的命令行再看输出目录的路径是不是你以为的那个最后看杀软拦截。这四步走完基本都能定下来。情形二文件存在但是旧的。最常见的原因是你看错了目录或者工程里有两套 Target 各自有输出目录。还有一种情况是文件系统的时间戳精度问题——某些网络盘和同步盘的秒级时间戳会导致构建系统判断失误出现我明明改了代码它却说没变化。解决办法是把工程放到本地硬盘别放同步盘里。情形三文件生成了但内容不对。比如 hex 里的起始地址不是预期的或者文件异常地小。起始地址不对去查 Target 选项卡里的 IROM1 起址和大小以及链接脚本。文件异常小可能是链接器把大部分代码当成未使用段回收掉了——这种情况下检查一下启动文件里的中断向量表是不是完整--remove加--split_sections的组合有时候会误伤那些只在汇编里被引用的函数需要显式保留。注意开了段回收之后那些只在中断向量表里被引用、或者只在汇编里被调用的函数务必确认它们真的还在镜像里。最保险的办法是用反汇编看一眼关键符号是否存在于最终的 .axf 中。5.3 Keil 编译很慢的六个真实原因这个问题问的人最多我把实际遇到过的原因按影响程度排一下。第一Browse Information 没关。Options for Target 的 Output 选项卡里有个Browse Information复选框勾上之后编译器会在编译每个文件时额外生成一份用于代码跳转索引的信息大工程下这一步能占掉整个编译时间的一大半。如果你不需要 CtrlClick 跳转和符号查找直接关掉编译速度提升非常明显。这个是我认为性价比最高的一条优化。第二优化等级和 LTO 开得太高。-O3 加链接时优化编译时间可能是 -O0 的好几倍。发布版本这么干没问题但开发阶段没必要。我一般是 Debug 配置 -O0Release 配置 -O3日常切换着用。第三杀毒软件的实时扫描。编译器要往输出目录写几百个 .o 文件杀软每个都扫一遍累积起来非常可观。把输出目录、工具链安装目录都加到排除列表里能省下不少时间。第四工程在机械硬盘或者网络盘上。编译是典型的密集小文件 IO机械盘和网络盘的随机读写性能差一个数量级。固态盘上编译一个中型工程可能只要十几秒机械盘上要一分多钟。这条没有什么技巧把工程搬到 SSD 就完了。第五改了头文件触发全量重编。头文件的依赖关系是传递的你改了一个被到处 include 的main.h所有包含它的源文件都要重编。这是正常行为不是 bug。要做的是控制头文件的粒度别搞一个什么都往里塞的万能头。真正的全局配置拆成多个小头文件按需包含。第六Pack 索引读取慢。有些版本在启动或者切换目标时会去扫描 Pack 目录Pack 装多了就慢。定期清理用不到的 Pack或者把 Pack 根目录迁到 SSD 上也能改善。6. 批处理编译与几个我常用的习惯6.1 用 UV4 命令行做自动化构建需要在构建服务器上自动出 hex或者在本地一键编译多个工程可以用 Keil 自带的命令行工具。假设 Keil 装在默认位置命令长这样C:\Keil_v5\UV4\UV4.exe -b D:\proj\MDK-ARM\Project.uvprojx -j0 -o D:\proj\build.log参数含义-b表示构建-r表示全量重编-c表示清理-j0表示不弹出界面-o指定日志输出文件。-t参数可以指定 Target 名称多 Target 工程里特别有用C:\Keil_v5\UV4\UV4.exe -r D:\proj\MDK-ARM\Project.uvprojx -t Release -j0 -o D:\proj\build_release.log这个工具的退出码很有用构建脚本可以直接判断0 表示没有错误也没有警告1 表示只有警告2 表示有错误3 表示致命错误11 表示工程文件打不开12 表示器件相关问题13 表示写文件失败15 表示读文件失败20 表示授权问题。脚本里判断退出码非 0 就直接失败退出别让一份有问题的固件流到下游。提示用命令行构建时Keil 的界面必须完全关闭。同一个工程被界面和命令行同时占用会报文件锁错误日志里一般写得很含糊实际原因就是进程冲突。构建服务器上要注意这一点。6.2 让每一份 HEX 都可追溯产线或者测试同事拿到一个 hex第一个问题往往是这是哪个版本。如果文件名只是个Project.hex那就得靠人肉记迟早出事。我的做法是用后构建命令把版本号和构建时间拼进文件名。在 User 选项卡的 After Build 里用 Keil 的按键码组合能拿到不少环境信息。比如L是输出名后面再拼上日期。更灵活的做法是后构建直接调用一个批处理脚本由脚本负责改文件名、写版本信息、算校验和、拷贝到发布目录。脚本大概长这样echo off setlocal set OUT..\Release set NAMEproject_%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2% if not exist %OUT% mkdir %OUT% copy /y .\Objects\Project.hex %OUT%\%NAME%.hex copy /y .\Objects\Project.bin %OUT%\%NAME%.bin echo %NAME% %OUT%\latest.txt这个脚本还能顺手做一件有价值的事记录 hex 的哈希值。用系统自带的证书工具算一个 SHA-256写进一个说明文件里。以后如果对某份固件有疑问重算一遍哈希就能确认是不是原件。产线出问题要回溯的时候这一步能省掉大量扯皮。6.3 几条踩过坑才总结出来的习惯第一条调试配置和发布配置分开成两个 Target。调试用 -O0、不优化、打开全部调试信息、关段回收发布用高优化、开段回收、去掉调试信息。好处是切换成本低而且不会出现我在调试配置上验证通过了结果发出去的是发布配置行为不一样这种尴尬。顺便说一句-O0 和 -O3 下的代码行为在某些边界情况下真的可能不一样尤其是依赖未定义行为的老代码所以发布前一定要在发布配置上跑一遍完整测试。第二条每次发布都看一眼 Program Size。把这一行数字记录下来形成趋势。今天 13.2KB下周 14.8KB一个月后 19KB等你发现没空间了再回头找是谁吃掉的成本很高。养成记录的习惯哪个提交导致体积跳涨一眼就能看出来。第三条hex 只用来交付不用来调试。调试走调试器直接加载 .axf 的路子能设断点、能看变量、能单步。hex 是给烧录器用的最终形态它里面没有调试信息出问题的时候几乎没有排查手段。我见过有人把 hex 烧进去然后用串口打印调试效率低得可怜。第四条给工程加一份 README。写清楚用的哪个编译器版本、装了哪个版本的 Pack、需要哪些环境变量、怎么出 hex、产物在哪个目录。这东西看着多余但半年后你自己接手回来或者同事要跟你并行开发的时候能省下半天时间。第五条别在生产构建上开自动保存工程之类的辅助功能。命令行构建要求工程文件稳定任何可能在编译期间修改工程文件的行为都会导致构建结果不可复现。最后再说一个很多人忽略的点hex 不是终点验证才算。我现在的习惯是每次出完 hex先用烧录工具做一次完整的擦除、写入、校验三步走然后跑一遍最基本的自检——比如点个灯、读一次芯片 ID、跑一遍通信自环。这一套下来多花两分钟但能挡掉绝大多数烧进去了但没跑起来的返工。曾经有过一次我改了链接脚本之后忘了确认向量表偏移编译一切正常hex 也生成了烧进去板子纹丝不动。后来用调试器挂上去看PC 指针压根没走到我的 main因为它跑到一个不存在的中断里去了。从那以后再小的改动我都会烧一遍自检不再相信编译通过就等于没问题。
返回列表