乐于分享
好东西不私藏

嵌入式软件版本管理技巧:自动记录编译信息与Flash固化存储

嵌入式软件版本管理技巧:自动记录编译信息与Flash固化存储

写在前面

在嵌入式软件开发中,版本管理是至关重要但常被忽视的一环。你是否遇到过以下场景?

  • ❓ 现场运行的固件到底是哪个版本?
  • ❓ 客户反馈的问题对应哪次代码提交?
  • ❓ 多个工程师编译的固件如何区分?

本文将介绍两种实用的软件版本管理方法:

  • ✅ 方式一:使用 __DATE__ 和 __TIME__ 预定义宏,自动记录编译时间
  • ✅ 方式二:使用 __attribute__((at())) 将版本信息固化到 Flash 指定地址
  • ✅ 附录:__attribute__ 关键字详解

一、方式一:使用预定义宏记录编译时间

1.1 基本用法

C 编译器提供了两个标准预定义宏,用于记录编译时的日期和时间:

printf("CompileDate: %s %s\r\n", __DATE__, __TIME__);

1.2 输出效果

CompileDate: Aug  4 2026 14:35:22

1.3 预定义宏详解

宏名称
格式
示例
__DATE__
"MMM DD YYYY"
"Aug 4 2026"
__TIME__
"HH:MM:SS"
"14:35:22"

📌 注意:每次编译时,这两个宏会自动更新为当前编译时刻的时间。因此,它们记录的是 固件构建时间,而非代码编写时间。

1.4 组合使用示例

voidPrintVersionInfo(void)
{
printf("========== Firmware Info ==========\r\n");
printf("Version    : v1.2.3\r\n");
printf("Compile Date : %s\r\n", __DATE__);
printf("Compile Time : %s\r\n", __TIME__);
printf("===================================\r\n");
}

输出效果:

========== Firmware Info ==========
Version    : v1.2.3
Compile Date : Aug  4 2026
Compile Time : 14:35:22
===================================

二、方式二:在 Flash 指定地址固化版本信息

2.1 应用场景

将版本信息固化在 Flash 的固定地址,有以下好处:

场景
说明
BootLoader 读取
启动时读取版本信息,判断是否升级
远程升级
查询版本号决定是否需要 OTA
生产追溯
通过读取 Flash 内容确认固件版本
现场诊断
上位机直接读取地址获取版本

2.2 代码实现

#define VERINFO_ADDR_BASE   (0x0800FF00)  // Flash 存放版本信息的起始地址

// 软件版本号,存放在 VERINFO_ADDR_BASE + 0x00
constchar Software_Ver[] __attribute__((at(VERINFO_ADDR_BASE + 0x00))) = "Software: 1.0.0";

// 编译日期,存放在 VERINFO_ADDR_BASE + 0x40
constchar Compiler_Date[] __attribute__((at(VERINFO_ADDR_BASE + 0x40))) = "Date: " __DATE__;

// 编译时间,存放在 VERINFO_ADDR_BASE + 0x60
constchar Compiler_Time[] __attribute__((at(VERINFO_ADDR_BASE + 0x60))) = "Time: " __TIME__;

2.3 存储器布局

Flash 地址空间
┌─────────────────────────────────────────────────────────┐
│ 0x08000000                                            │
│          ┌─────────────────────────────────────────────┤
│          │  程序代码(.text)                          │
│          │                                            │
│          ├─────────────────────────────────────────────┤
│ 0x0800FF00 │ Software: 1.0.0\0   ← 版本号字符串      │
│ 0x0800FF40 │ Date: Aug  4 2026\0  ← 日期字符串       │
│ 0x0800FF60 │ Time: 14:35:22\0    ← 时间字符串        │
│          ├─────────────────────────────────────────────┤
│ 0x08010000 │ 保留 / 其他数据                          │
└─────────────────────────────────────────────────────────┘

2.4 地址偏移设计说明

变量
起始地址
偏移量
说明
Software_Ver
0x0800FF00
+0x00
版本号字符串
Compiler_Date
0x0800FF40
+0x40(64字节)
日期字符串,预留64字节
Compiler_Time
0x0800FF60
+0x60(96字节)
时间字符串,预留32字节

⚠️ 注意:每个字符串预留足够空间(如 64 字节),避免长度溢出覆盖后续数据。


三、__attribute__ 关键字详解

3.1 基本概念

__attribute__ 是 GCC 编译器 提供的一个关键字,用于向编译器传递属性信息,实现特殊的功能控制。

语法格式:

__attribute__((attribute-list))

3.2 三大应用方向

属性类型
说明
示例
函数属性
控制函数行为
__attribute__((noreturn))
变量属性
控制变量存储
__attribute__((at(addr)))
类型属性
控制结构体对齐
__attribute__((packed))

四、读取 Flash 中的版本信息

4.1 直接读取

// 声明外部变量(链接器会从 Flash 中获取)
externconstchar Software_Ver[];
externconstchar Compiler_Date[];
externconstchar Compiler_Time[];

voidReadVersionFromFlash(void)
{
printf("Version : %s\r\n", Software_Ver);
printf("Date    : %s\r\n", Compiler_Date);
printf("Time    : %s\r\n", Compiler_Time);
}

4.2 通过地址读取

#include<stdint.h>

voidReadVersionByAddress(void)
{
constchar* pVer = (constchar*)0x0800FF00;
constchar* pDate = (constchar*)0x0800FF40;
constchar* pTime = (constchar*)0x0800FF60;

printf("Version : %s\r\n", pVer);
printf("Date    : %s\r\n", pDate);
printf("Time    : %s\r\n", pTime);
}

4.3 上位机读取方法

方式
工具
方法
J-Link
J-Link Commander
mem 0x0800FF00, 64
ST-Link
STM32CubeProgrammer
读取指定地址
串口
自定义协议
MCU 主动上报

五、工程实践建议

5.1 版本号命名规范

推荐使用 语义化版本号(SemVer) 规范:

主版本号.次版本号.修订号(如 2.1.3)

● 主版本号:不兼容的API修改(重大变更)
● 次版本号:向下兼容的功能性新增
● 修订号:向下兼容的Bug修复
constchar Software_Ver[] __attribute__((at(VERINFO_ADDR_BASE + 0x00))) 
    = "Software: v2.1.3";

5.2 增加更多版本信息

#define VERINFO_ADDR_BASE   (0x0800FF00)

constchar Software_Ver[]    __attribute__((at(VERINFO_ADDR_BASE + 0x00)))  = "SW: v1.0.0";
constchar Compiler_Date[]   __attribute__((at(VERINFO_ADDR_BASE + 0x20)))  = "Date: " __DATE__;
constchar Compiler_Time[]   __attribute__((at(VERINFO_ADDR_BASE + 0x40)))  = "Time: " __TIME__;
constchar Git_Hash[]        __attribute__((at(VERINFO_ADDR_BASE + 0x60)))  = "Git: a1b2c3d";
constchar Build_User[]      __attribute__((at(VERINFO_ADDR_BASE + 0x80)))  = "User: " __USER__;
constchar MCU_Type[]        __attribute__((at(VERINFO_ADDR_BASE + 0xA0)))  = "MCU: STM32F407";

5.3 使用条件编译控制

#ifdef VERSION_DEBUG
#define VERINFO_ADDR_BASE   (0x0800FF00)
// ... 版本信息定义
#else
// 正式发布版本,不占用Flash
#endif

六、常见问题排查

问题
可能原因
解决方案
编译报错
编译器不支持 at 属性
使用 __attribute__((section())) 替代
地址被覆盖
地址与代码段重叠
查看 .map 文件,确认地址未使用
读取乱码
Flash 地址未对齐
确保字符串末尾有 \0 终止符
字符串长度溢出
预留空间不足
每个字符串至少预留 64 字节
IAR 编译器报错at
 语法不同
IAR 使用 @ 符号:const char ver[] @ 0x0800FF00

6.1 IAR 编译器写法

// IAR 中指定地址的方法(与 Keil/GCC 不同)
constchar Software_Ver[] @ 0x0800FF00 = "Software: 1.0.0";
constchar Compiler_Date[] @ 0x0800FF40 = "Date: " __DATE__;

6.2 GCC 通用写法(推荐)

// 使用 section 属性 + 链接脚本控制
#define VERINFO_SECTION  __attribute__((section(".version_info")))
constchar Software_Ver[] VERINFO_SECTION = "Software: 1.0.0";

七、总结

方法
优点
适用场景
__DATE__/__TIME__
简单,无需额外配置
串口打印、日志记录
__attribute__((at()))
固化存储,可远程读取
BootLoader、OTA、生产追溯

推荐组合方案:

// 1. 在 Flash 固定地址存储版本信息
constchar Software_Ver[] __attribute__((at(0x0800FF00))) = "SW: v1.0.0";
constchar Compiler_Date[] __attribute__((at(0x0800FF40))) = "Date: " __DATE__;
constchar Compiler_Time[] __attribute__((at(0x0800FF60))) = "Time: " __TIME__;

// 2. 在串口打印时一并输出
printf("Firmware: %s, Built on %s at %s\r\n"
       Software_Ver, Compiler_Date, Compiler_Time);

📌 一句话总结__DATE__ 和 __TIME__ 自动记录编译时间,__attribute__((at())) 将版本信息固化到 Flash 指定地址,两者结合实现固件身份的统一管理。