拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Keil构建后自动备份HEX并写入版本号,让嵌入式固件版本可追溯

简介针对使用Keil进行STM32等单片机开发的工程师日常编译输出的HEX文件往往没有版本标识和编译时间容易在多次迭代后混淆甚至烧录旧固件。本工具提供了一套自动化的固化方案通过检索main.c中的“#define SOFTWARE_VERSION”字符串识别当前版本号并在Keil编译完成后自动调用生成带有类似“Ver_1.0_20250312-1530”命名的HEX文件同时保留原始输出。资源包含1个Python源码脚本和1个编译好的exe可执行程序压缩包仅6.85MBPython文件适合阅读与二次定制exe则便于不熟悉Python环境的用户直接集成到Keil的After Build步骤中。目前已有1319人学习或下载该资源适合需要规范固件版本管理、提升调试追溯效率的单片机项目团队或个人开发者。使用后无需再手动复制重命名既避免下载错版本也让每个固件都自带可读的版本与时间信息方便后续维护和现场升级。1. Keil反复覆盖同一个HEX代码版本与编译时间只能靠文件名猜Keil MDK的工程里每次按下F7链接后生成的就是同一个名字的HEX文件。上午还能用的固件下午一编译就被替代想找回“那版能跑的”只能凭对Windows“修改日期”的模糊记忆去试。手动复制成带日期的备份做了几次之后不是忘就是漏最终发版时还是靠猜。自动复制重命名的价值在于把“备份”这种高频低智的操作交给工具编译一成功立即复制输出HEX用一个“产品名_版本号_编译时间”的命名放入归档目录。同时把版本号和编译时间写进固件内部让产线烧录、OTA升级、远程定位问题时都能一眼看出固件是哪一次构建产生的。这套做法只依赖Keil自带的After Build事件和Windows的cmd/PowerShell不装额外软件任何维护过一段时间的嵌入式工程都值得配上。2. Keil生成HEX的时机与After Build事件是脚本能接管的边界2.1 HEX不是编译器直接产出而是链接完成后的后处理文件Keil处理一个STM32工程时会先“compiling xxx.c”生成目标文件再由armlink把目标文件链接成AXF可执行文件。HEX文件则是把AXF再转换出来的烧写格式这一步在uVision里由fromelf或链接器的HEX输出功能完成。打开Options for Target的Output页会看到“Create HEX File”这个选项它控制的就是这一步是否执行。这个顺序直接决定了自动化的挂载点After Build事件发生的时间是AXF和HEX都已完整写入磁盘之后也就是说脚本读到的一定是本次构建的完整产物。反过来只要编译或链接出错After Build流程根本不会触发不需要担心脚本把残破的HEX当成正常构建去归档。与其在脚本里写一堆文件是否存在、文件大小是否变化的判断不如直接把信任放在Keil的构建流程上。有个容易忽视的细节增量构建和Rebuild All都经过After Build事件。Rebuild All会重新链接并重写HEX这些都会落在脚本的归档范围里不需要区分。脚本里唯一需要关心的是“是否有新的HEX刚被写出来”而这一步不能靠文件系统时间判断所以要在构建记录或代码里直接获取版本信息和编译时间。2.2 After Build相关配置用“!H”把HEX路径传给脚本在Options for Target里切到User页右下角是“After Build/Rebuild - Run User Programs”区域。这里最多可以填6条命令执行顺序为上到下。最常见的配置法是第一条命令做复制归档第二条命令去生成BIN文件或运行静态检查工具。这里有个优先级很高的细节Keil会在命令真正交给cmd执行之前做变量替换。以!H为例它会被替换成HEX文件的完整路径!L对应的则是AXF文件路径。如果整个路径里含空格就必须让路径待在引号里否则cmd会把路径拆散。配置位置示例写法作用After Build #1call build\postbuild.bat !H把HEX路径作为参数传给批处理After Build #2call build\make_bin.bat !L从AXF生成BIN并留档脚本退出状态返回0视为成功非0则uVision标记本次命令失败让构建结果与归档结果一致为什么这里要写call而不是直接把脚本路径填进去因为在cmd里直接运行一个批处理文件时执行完控制权不会返回到调用者。加上call前缀之后uVision启动的cmd进程才能拿到脚本的返回值。Keil判断命令成功与否靠退出码脚本末尾的exit /b 0就是把“成功”这个状态显式交给Keil。2.3 选项框里只传参数不要把具体逻辑塞进User命令由于这条命令最终要在uVision的界面里维护最好让它保持简洁把HEX路径通过参数传过去具体流程都放在外部脚本里。原因一是cmd对%、、|这些符号的处理在校验界面里容易引发转义问题原因二是脚本改动不用反复进入Options窗口直接改文本文件更方便也方便用Git管理。如果需要把Pre-Build步骤也自动化uVision左侧的Before Build区域同样支持这些变量。后面第4章会用到这个位置来强制刷新编译时间。3. 复制重命名HEX的批处理脚本时间、版本号、归档目录一次成型3.1 工程目录保持约定脚本放buildHEX在Objects先定一个目录约定脚本逻辑才能随之确定。比较稳妥的布局是ProjectRoot/ |-- project.uvprojx |-- Objects/ | -- App.hex |-- release/ | -- version.txt -- build/ -- postbuild.bat脚本没有必要和HEX放在同一个目录。放到build目录有两个好处外层目录不会被增量编译结果污染后续如果要从Git的tag直接生成版本号脚本所在的目录也不影响链接器产物。!H被替换成绝对路径后脚本内部用%~dp1拿到HEX目录不需要在命令行里手工拼路径。3.2 最小可用脚本复制一份文件名只带时间戳先给一个不含版本号的最小版本把整个机制跑通echo off setlocal enabledelayedexpansion set HEX_FILE%~1 set HEX_DIR%~dp1 set HEX_NAME%~n1 for /f delims %%i in (powershell -NoProfile -Command Get-Date -Format yyyyMMdd_HHmmss) do set BUILD_TIME%%i set ARCHIVE_DIR%HEX_DIR%backup if not exist %ARCHIVE_DIR% mkdir %ARCHIVE_DIR% copy /Y %HEX_FILE% %ARCHIVE_DIR%\%HEX_NAME%_%BUILD_TIME%.hex nul if errorlevel 1 ( echo [ERROR] postbuild copy failed for %HEX_FILE% exit /b 1 ) echo [OK] archived as %ARCHIVE_DIR%\%HEX_NAME%_%BUILD_TIME%.hex endlocal exit /b 0这个脚本可以放在build目录直接运行。%~1去掉引号取出传入的文件完整路径%~dp1是完整目录但目录结尾自带一个反斜杠后面拼backup时不用额外加斜杠%~n1把文件名结尾的.hex剥掉否则会组合出App.hex_20250906_1435.hex这种重复后缀。时间戳这里特意调用PowerShell而不是用%date%。中文Windows的%date%返回“2025/09/06 周六”格式受系统和区域设置影响不同电脑解析结果不一致。PowerShell的Get-Date -Format yyyyMMdd_HHmmss输出固定格式for /f把它收进环境变量。一次进程启动的开销在几百毫秒内放在构建后流程里可以忽略。脚本里真正会被Keil注意到的是最后的if errorlevel 1和exit /b。复制失败必须返回非零Keil才会在构建结束后把这个错误标出来。写批处理时最容易漏的就是这一步Windows命令默认不会全程向上传递失败状态必须自己显式检查。3.3 加版本号从version.txt或当前Git提交里取值版本号不会凭空产生。最简单的做法是在工程里固定一个release/version.txt内容就一行“1.2.0”。构建时读取并拼进文件名for /f delims %%v in (type %HEX_DIR%..\release\version.txt) do set FW_VER%%v%HEX_DIR%..\release\version.txt利用..由Objects目录退回项目根再进入release。这个文件必须是无BOM的ANSI或UTF-8记事本保存时如果自动加了UTF-8 BOM批处理读到的第一行会带不可见字符拼出的版本号里混入异常字符文件名看着没问题脚本里做字符串比较时永远不相等。如果工程用Git可以用提交数或短哈希来做自动版本。每次提交之后编译出的版本号自动变化适合需要频繁出测试包的开发阶段for /f delims %%g in (git -C %HEX_DIR%\.. rev-parse --short HEAD) do set GIT_REV%%g这里一定要用git -C切到仓库根目录否则批处理的当前目录可能受uVision启动方式影响git找不到仓库。拼文件名时把哈希放到末尾比如App_1.2.0_20250906_1435_ab12cd3.hex从文件名就能直接定位到源码提交。三种版本取值策略的取舍用一张表看更清楚版本取值法更新粒度好处需要警惕的坑人工维护version.txt发布前才改稳定和Git tag能对上忘了改会造成不同代码同名git rev-list --count HEAD每次提交版本号自然单调递增依赖Git仓库适合放到CI里执行git rev-parse短哈希每次提交能直接定位代码状态可读性差适合做版本号后缀量产场景下常见组合是人工维护“1.2.0”作为主版本后面拼提交哈希保证唯一性。日常做测试包时用版本号加短哈希的方式可以完全解放人工到发布前再锁定主版本号。4. 把代码版本和编译时间埋进固件version.h、TIME与构建前刷新4.1 用一个独立的build_info.c让编译时间进入镜像光有文件名记录版本还不够。产线烧录后固件已经写入芯片没法靠文件名去追溯现场设备的构建时间远程调试时更需要设备自报版本。所以还应该把版本信息和时间戳写进固件的只读区做成运行时可见的字符串。常见做法是在工程里新增一个build_info.c内容只保留几个全局字符串#include version.h const char fw_version[] v APP_VERSION_STR; const char fw_build_time[] __DATE__ __TIME__; const char *get_fw_version(void) { return fw_version; }__DATE__和__TIME__是编译器提供的预定义宏取值是编译那个时刻的日期和时间字符串。放在build_info.c里之后主程序通过串口打印或命令解析就能把固件版本报出来日志系统里也会自动带上这条信息。这里有编译器优化相关的一个坑如果fw_version是static且没有任何函数引用连接器可能把整个字符串丢弃。上面的写法把它定义为全局且提供了get_fw_version()函数CLI命令里只要调用这个函数字符串就会被保留。后面会用findstr验证这一点。4.2 增量构建中“最新编译时间”必须强制刷新__DATE__和__TIME__的实际行为是只在“这个源文件被重新编译”时刷新。你改了main.cuVision会编译main.c并重新链接但build_info.c如果没有重新编译链接进去的时间还是上一次构建的。构建成功了文件名的归档时间是今天固件内部读到的却是昨天这类问题排查起来非常隐蔽。解决办法是让build_info.c的修改时间在每次构建前发生变化。Windows批处理里有一个很经典的技巧用copy /b把文件自身复制一遍只更新修改时间而不改内容copy /b src\build_info.c ,, nul把这一条放到uVision User页的Before Build区域每次构建开始就先“触摸”build_info.c。文件内容不变但修改时间变了Keil会把它当作变更过的文件重新编译一次__TIME__自然就跟随本次构建刷新。4.3 把版本号集中到一个来源由version.txt生成version.h如果version.txt和version.h各自维护总有一次会忘改。比较可控的做法是在构建前置阶段让脚本从version.txt生成version.h这样版本号只有一个输入源for /f delims %%v in (type %HEX_DIR%..\release\version.txt) do ( echo #ifndef APP_VERSION_STR echo #define APP_VERSION_STR %%v echo #endif ) src\version.h每次构建时这个文件都被覆写固件字符串、归档文件名、version.txt三处保持一致。仓库里保留version.txt作为手工编辑对象version.h变成生成物不手工改。如果团队里有人习惯直接改version.h那么在评审时就把它当只读文件来核对。4.4 把版本差异变成调试线索版本信息进入到设备的正常运行状态后调试方式会发生一个小变化现场反馈“设备升级失败”时先让用户在诊断界面读出fw_build_time。如果发现批次里设备版本不统一日志的时间戳和提交哈希能直接判断出哪一批设备还没执行升级。这样的排障依赖的不是某一个人“记得”而是固件自己会说话了。5. 构建后立即验证几个命令说明HEX确实是这一版5.1 重建之后先看一眼归档目录和固件内的版本字符串脚本接好之后第一步不是把HEX拷进烧录器而是删除归档目录里的旧备份重新Rebuild再确认uVision的Output窗口没有以非零退出码收尾。重点观察脚本打出的[OK] archived as这一行文件名里的时间应当和系统当前时间一致。在工程根目录打开一个cmd窗口用两条命令做第一次验证dir Objects\backup findstr /m /c:v1.2.0 Objects\App.AxF echo version string presentdir查看归档结果findstr在AXF这个二进制文件里查找连续的ASCII字符串。AXF体积通常只有几MB查找是瞬间完成的。如果第二条命令没有输出多半不是字符串不存在而是增量构建时build_info.c没有重新编译或者字符串被优化掉了。再用一条命令把三个文件的时间放在一起dir /tw Objects\App.hex Objects\App.AxF src\build_info.c如果看到build_info.c的修改时间还是上一次的说明Before Build里的touch命令没有生效。先确认uVision配置的是After Build还是Before Build这是最容易放错的位置。5.2 归档阶段容易反复出现的三个排错点问题现象大概率原因对策uVision构建完成后弹出命令返回非零After Build命令没有加call或脚本被别的exit覆盖了返回码命令改为call开头的形式参数保持引号所有归档文件名里的版本号相同version.txt没更新或生成version.h的脚本覆盖了手工改动锁定version.txt为唯一数据源核对版本差异fw_build_time永远是同一个值build_info.c在增量构建里没有被重新编译确认Before Build中的touch命令是否配置在正确位置还有一个Windows下极易踩的细节批处理里set varvalue的等号后不能有空格。写成set FW_VER1.2.0 变量值会带一个看不见的尾随空格拼出来的文件名在cmd里看起来正常在脚本里比较或复制时却会报错。排这种问题时用set FW_VER直接看变量有没有多余空格是最快的办法。构建时间刷新以后再设置如下把归档目录作为固定FirmwareHistory保留三个月发版时直接从目录里挑选上线前在设备日志里检查fw_build_time是否为本轮构建时间。这两个动作能拦截掉大部分“烧错版本”的事故。对最终产线还可以把get_fw_version()的返回值与烧录器记录的HEX文件做比对让版本控制从代码一直落到芯片内部。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门