1. 为什么你的Keil项目需要一个自动化编译脚本?

如果你和我一样,是个经常和单片机打交道的嵌入式开发者,那你肯定对下面这个场景不陌生:每次在Keil里点下编译按钮,生成的那个HEX文件,名字永远是项目名加个.hex后缀,比如MyProject.hex。今天改了点代码,编译一次,覆盖掉昨天的MyProject.hex;明天修复个Bug,再编译一次,又把今天的覆盖了。过了一周,测试同事跑来说:“上周三那个版本好像挺稳定的,能发我一下吗?”你看着文件夹里孤零零的、最新的那个MyProject.hex,瞬间头大——哪个是上周三的?根本分不清。

更麻烦的是版本管理。我们通常会在代码里定义一个版本号,比如在main.c里写一行#define SOFTWARE_VERSION "V1.2.3"。但这个版本号只在代码里,生成的HEX文件名字上完全体现不出来。你发给硬件同事的可能是V1.2.3的HEX,他烧录测试后,你又在本地悄悄改了点东西,版本号没来得及更新,又编译了一个新的MyProject.hex发过去。得,版本混乱了,出了问题都不知道该回溯到哪个固件。

所以,一个理想的流程应该是:我点下编译,Keil不仅生成HEX文件,还能自动把这个文件复制出来,并且用“项目名_版本号_编译日期”这样的格式重新命名。比如MyProject_V1.2.3_20241115.hex。这样,每次编译的历史文件都清晰可查,版本一目了然,再也不用在文件管理上耗费精力。这就是我们今天要聊的Keil自动化编译的核心价值——把定制HEX文件命名和版本管理这两件事,完美地结合起来,让你的开发流程既专业又高效。

2. 动手之前:理解自动化脚本的四大核心模块

要实现这个自动化流程,我们需要一个“小助手”脚本。这个脚本会在Keil每次编译成功后自动运行,帮我们完成一系列“脏活累活”。别看它小,五脏俱全,主要包含四个关键功能模块,理解了它们,你就能掌握整个自动化流程的脉络。

2.1 版本号提取:从代码深处“挖”出版本信息

版本号是固件的身份证,必须准确无误。我们通常把版本号定义在源代码里,比如main.c文件的开头。脚本的第一个任务,就是像侦探一样,从这个文件里把版本号“挖”出来。

我常用的方法是在main.c里定义一个特殊的字符串,比如const char *Version = "FW_V2.1.5";。脚本会打开这个文件,逐行读取,并用正则表达式去匹配像V\d+\.\d+\.\d+这样的模式(匹配V开头,后面跟数字、点、数字、点、数字的字符串)。这里有个小坑要注意:代码里可能有很多数字和点,所以匹配规则要写得足够精确,避免误匹配到其他数据。一旦找到,就把这个字符串(比如V2.1.5)提取出来,作为后续命名的关键部分。如果没找到,脚本会立刻报错提醒你,防止生成一个没有版本号的“无名”固件。

2.2 日期时间标记:给每次编译盖上“时间戳”

光有版本号还不够。同一个版本(比如V1.0.0)的代码,我可能今天下午改了个注释,明天上午优化了某个函数逻辑,虽然版本号没变,但编译生成的HEX文件其实是有细微差别的。为了区分这些“同版本不同时间”的构建,我们需要引入编译时间。

脚本会调用系统函数,获取当前的本地时间,然后格式化成我们喜欢的样式。我个人的习惯是使用YYYYMMDD_HHMM的格式,比如20241115_1430,代表2024年11月15日下午2点30分。这个格式的好处是,按字符串排序时,文件会自动按时间先后顺序排列,非常直观。这个时间戳就像给每次编译盖了个章,精准记录了固件“出生”的时刻。

2.3 项目文件与HEX文件定位:找到“正主”

Keil工程文件的后缀是.uvprojx(或旧版的.uvproj),而编译输出的HEX文件通常位于项目目录下的特定子文件夹里,比如Objects文件夹。脚本需要聪明地找到它们。

首先,脚本会在它所在的目录(通常就是工程根目录)下搜索.uvprojx文件,并从文件名中提取出项目名称(去掉后缀)。这个项目名将作为新HEX文件名的前缀。接着,脚本需要知道Keil把HEX文件生成到哪里了。这里有两种思路:一种是像很多开源脚本那样,硬编码路径,比如去.\Objects\下面找;另一种更通用的方法是,解析.uvprojx这个XML格式的工程文件,从中读取输出目录配置。对于新手,我建议先用第一种简单方法,等脚本跑通了,再研究第二种更健壮的方法。找到原始的项目名.hex文件,我们才能进行下一步的“加工”。

2.4 文件复制与重命名:生成最终的“定制艺术品”

这是最后一步,也是成果展示的一步。脚本会使用找到的项目名、提取的版本号、格式化的日期时间,拼接出一个全新的文件名。例如:MyProject_FW_V2.1.5_20241115_1430.hex

然后,脚本将原始的HEX文件从它的输出目录(如Objects)复制到我们指定的目录(比如项目根目录,或者一个专门的Release文件夹),并赋予它这个全新的、信息丰富的名字。至此,一个带有完整版本和编译信息的定制化HEX文件就诞生了。整个过程完全自动,无需你手动复制、粘贴、改名,彻底解放双手。

3. 一步步搭建你的自动化流水线

理论说完了,咱们来点实在的。下面我就手把手带你搭建这套自动化流程,我会以最常用的Python脚本为例,因为它跨平台,语法清晰。

3.1 编写核心Python脚本

首先,在你的Keil工程根目录下,创建一个新的Python文件,比如叫auto_rename_hex.py。我们把前面提到的几个模块功能用代码实现出来。

import os
import re
import shutil
import glob
from datetime import datetime

def get_version_from_source(file_path):
    """
    从指定的C源文件中提取版本号字符串。
    假设版本号定义格式为: #define FW_VERSION "V1.2.3"
    """
    version_pattern = r'#define\s+FW_VERSION\s+"([Vv]\d+\.\d+\.\d+)"'
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
            match = re.search(version_pattern, content)
            if match:
                version = match.group(1)
                print(f"[INFO] 成功提取版本号: {version}")
                return version
            else:
                # 尝试其他常见格式
                alt_patterns = [
                    r'const\s+char\s*\*\s*Version\s*=\s*"([Vv]\d+\.\d+\.\d+)"',
                    r'Ver[_\s]*([\d\.]+)'
                ]
                for pattern in alt_patterns:
                    match = re.search(pattern, content)
                    if match:
                        version = match.group(1)
                        if not version.upper().startswith('V'):
                            version = 'V' + version
                        print(f"[INFO] 从备选模式提取版本号: {version}")
                        return version
                print(f"[ERROR] 在文件 {file_path} 中未找到版本号定义!")
                return None
    except FileNotFoundError:
        print(f"[ERROR] 找不到源文件: {file_path}")
        return None

def get_formatted_datetime():
    """获取当前时间,并格式化为 YYYYMMDD_HHMM 的字符串。"""
    now = datetime.now()
    date_str = now.strftime("%Y%m%d")
    time_str = now.strftime("%H%M")
    return f"{date_str}_{time_str}"

def find_keil_project_and_hex(current_dir):
    """
    在当前目录查找.uvprojx文件和对应的.hex文件。
    返回 (项目名, 原始hex文件路径)。
    """
    # 查找Keil项目文件
    project_files = glob.glob(os.path.join(current_dir, "*.uvprojx"))
    if not project_files:
        print("[ERROR] 在当前目录未找到 .uvprojx 文件!")
        return None, None

    # 默认取第一个找到的项目文件
    project_path = project_files[0]
    project_name = os.path.splitext(os.path.basename(project_path))[0]
    print(f"[INFO] 找到项目: {project_name}")

    # 尝试在常见输出目录查找HEX文件
    common_hex_paths = [
        os.path.join(current_dir, "Objects", f"{project_name}.hex"), # MDK-ARM常见路径
        os.path.join(current_dir, f"{project_name}.hex"),            # 可能直接在根目录
        os.path.join(current_dir, "Output", f"{project_name}.hex"),  # 其他可能路径
    ]

    hex_file_path = None
    for path in common_hex_paths:
        if os.path.exists(path):
            hex_file_path = path
            print(f"[INFO] 找到HEX文件: {hex_file_path}")
            break

    if not hex_file_path:
        print(f"[ERROR] 未找到项目 {project_name} 对应的 .hex 文件,请检查Keil输出配置。")
        return project_name, None

    return project_name, hex_file_path

def create_custom_hex(project_name, version, datetime_str, original_hex_path, output_dir=None):
    """
    创建定制化的HEX文件。
    """
    if not output_dir:
        output_dir = os.path.dirname(original_hex_path) # 默认输出到原始HEX文件所在目录

    # 构建新文件名
    new_filename = f"{project_name}_{version}_{datetime_str}.hex"
    new_file_path = os.path.join(output_dir, new_filename)

    try:
        # 复制并重命名文件
        shutil.copy2(original_hex_path, new_file_path)
        print(f"[SUCCESS] 定制HEX文件已生成: {new_file_path}")
        return new_file_path
    except Exception as e:
        print(f"[ERROR] 复制文件失败: {e}")
        return None

# 主程序逻辑
if __name__ == "__main__":
    print("=== Keil HEX文件自动化定制脚本开始运行 ===")

    # 1. 设置路径 (根据你的实际项目结构调整)
    current_directory = os.getcwd() # 脚本运行目录,建议放在工程根目录
    source_file_for_version = os.path.join(current_directory, "Core", "Src", "main.c") # 版本号所在源文件

    # 2. 提取版本号
    firmware_version = get_version_from_source(source_file_for_version)
    if not firmware_version:
        print("脚本因版本号提取失败而终止。")
        exit(1)

    # 3. 获取编译时间戳
    compile_datetime = get_formatted_datetime()
    print(f"[INFO] 编译时间戳: {compile_datetime}")

    # 4. 查找项目和原始HEX文件
    proj_name, original_hex = find_keil_project_and_hex(current_directory)
    if not original_hex:
        print("脚本因找不到HEX文件而终止。")
        exit(1)

    # 5. 创建定制化HEX文件
    # 你可以指定一个固定的输出目录,比如项目根目录下的'Releases'文件夹
    release_dir = os.path.join(current_directory, "Releases")
    os.makedirs(release_dir, exist_ok=True) # 如果目录不存在则创建

    final_hex_path = create_custom_hex(proj_name, firmware_version, compile_datetime, original_hex, release_dir)

    if final_hex_path:
        print("=== 脚本执行成功 ===")
    else:
        print("=== 脚本执行过程中出现错误 ===")

这段代码是一个功能完整的示例。你需要根据自己项目的实际结构,调整source_file_for_version这个变量的路径,确保它能正确找到你的main.c文件。同样,如果Keil的输出目录不是Objects,你需要在find_keil_project_and_hex函数的common_hex_paths列表里添加或修改查找路径。

3.2 在Keil中配置自动执行

脚本写好了,怎么让Keil在编译后自动调用它呢?这就需要用到Keil的User Command功能。

  1. 打开你的Keil工程,点击菜单栏的 Project -> Options for Target...,或者直接按快捷键Alt+F7
  2. 在弹出的选项对话框中,切换到 User 标签页。你会看到BuildRebuild后面都有命令输入框。
  3. 我们希望在每次编译(Build)成功后执行脚本,所以把命令填在 After Build/Rebuild 区域的 Run #1 框里。
    • 如果你直接使用Python脚本,命令类似:C:\Python39\python.exe "$P\\auto_rename_hex.py"。这里需要替换为你电脑上Python解释器的实际路径。$P是Keil的内置变量,代表当前项目文件所在的目录。
    • 更推荐的做法是将脚本编译成.exe:这样可以避免团队其他成员电脑上没有安装Python的麻烦。使用pyinstaller工具可以轻松打包:在命令行进入脚本目录,执行 pyinstaller --onefile --console auto_rename_hex.py。完成后,会在dist文件夹生成一个独立的auto_rename_hex.exe文件。Keil里的命令就可以简化为:"$P\\dist\\auto_rename_hex.exe"
  4. 勾选上 Run #1 前面的复选框,确保命令启用。
  5. 点击确定保存配置。

现在,你每次点击Keil的编译按钮,在编译过程结束后,Keil就会自动运行我们的脚本,在指定的Releases文件夹(或你设置的其他目录)里生成一个带有版本和日期的定制HEX文件。你可以试着修改一下main.c里的版本号,或者隔几分钟编译一次,看看生成的文件名是不是按预期变化了。

4. 进阶玩法与避坑指南

基础功能实现后,我们可以玩点更花的,让这个自动化流程更加强大和可靠。同时,我也分享几个我踩过的坑,帮你提前避开。

4.1 版本管理深化:集成Git提交哈希

对于使用Git进行源码管理的团队,仅仅使用手动定义的版本号(如V1.2.3)有时还不够精细。因为V1.2.3可能对应着多次代码提交。为了精确定位到某次编译具体对应了哪一次代码提交,我们可以把Git的提交哈希(Commit Hash)也加入到文件名中。

思路是:在脚本运行时,调用git命令获取当前分支最新的提交哈希的前7位(通常就足够唯一标识)。修改文件名生成逻辑,变成项目名_V1.2.3_git_a1b2c3d_20241115_1430.hex。这样,只要看到文件名,就能立刻在Git历史中找到对应的精确代码状态,对于问题排查和版本追溯来说,简直是神器。实现上,可以使用Python的subprocess模块来执行git rev-parse --short HEAD命令并获取输出。

4.2 编译配置区分:Debug与Release

一个正规的项目通常会有DebugRelease两种编译配置。Debug版包含调试信息,用于开发;Release版经过优化,用于发布。我们的脚本也应该能区分它们。

Keil的User Command允许我们使用内置变量。其中,$L 代表 List File的路径和名称,但我们可以从中提取出配置信息。更直接的方法是,在Keil的Options for Target -> Output里,可以为不同配置设置不同的输出文件夹名,比如Objects\DebugObjects\Release。然后在我们的脚本里,通过解析工程文件或检查当前存在的输出路径,来判断是哪种配置。最终生成类似MyProject_V1.2.3_20241115_1430_Debug.hexMyProject_V1.2.3_20241115_1430_Release.hex的文件,清晰无误。

4.3 错误处理与日志记录

自动化脚本最怕的就是“静默失败”。你以为它运行了,其实因为某个路径错误,它什么都没做,而你却不知道。因此,健壮的错误处理和日志记录至关重要。

  • 检查文件存在性:在尝试打开main.c或复制HEX文件前,一定要用os.path.exists()检查路径是否存在。
  • 捕获异常:使用try...except块包裹可能出错的操作(如文件读写、调用外部命令),并在except中打印明确的错误信息,而不是让程序崩溃。
  • 提供有意义的提示:错误信息不能只是“File not found”,而应该是“未找到main.c文件,请检查路径配置:C:\Project\Core\Src\main.c”。
  • 输出日志文件:除了在控制台打印,还可以将每次脚本运行的关键信息(时间、版本、源文件路径、是否成功、生成的文件路径等)追加写入到一个本地的build_log.txt文件中。这样即使你当时没看Keil的Build Output窗口,事后也能查证。

4.4 我踩过的那些坑

  1. 路径中的空格:这是最常见的问题。如果你的项目路径包含空格(例如D:\My Projects\STM32),在Keil的User Command里直接写python D:\My Projects\STM32\script.py会出错。务必给整个路径加上双引号"C:\Python39\python.exe" "$P\auto_rename_hex.py"。注意,$P变量Keil自己会处理,但我们的脚本路径如果包含空格,也需要用引号括起来。
  2. Keil编译未成功也执行脚本:Keil的After Build命令是在构建步骤之后运行,而不是在成功构建之后。这意味着即使编译有错误,生成了0 Error(s), 100 Warning(s),脚本也会被触发。而这时很可能没有新的HEX文件生成,脚本会报错找不到文件。一个简单的改进是,让脚本检查原始HEX文件的修改时间,如果它比脚本启动时间还旧,说明这不是一次新的成功编译,可以跳过重命名操作并给出提示。
  3. 版本号格式不一致:团队中不同成员可能在代码里用不同格式写版本号,有的写V1.0,有的写v1.0.0,有的写1.0。这会导致脚本提取的版本号格式混乱,生成的文件名不统一。最好在项目初期就定好版本号的定义规范,并在脚本的提取函数里做统一的格式化处理(例如,强制转为Vx.y.z的格式)。
  4. 防病毒软件误报:如果你将Python脚本打包成.exe,某些敏感的防病毒软件可能会将其误报为病毒而拦截,导致Keil调用失败。如果遇到这种情况,需要将生成的.exe文件或所在目录添加到防病毒软件的信任列表(白名单)中。
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐