version.c
version.c:3-6 注释明确写死)4.2 预处理器分支的选择逻辑(version.c:7-20)4.3 静态存储与"防止被丢弃"的真实生效路径4.4 通信协议帧格式4.5 定时 / uCOS-II 配合4.6 关键全局变量如何被读写5. 对外接口6. 依赖关系7. 备注与风险点源文件路径:
user/version.c所属模块:user(主程序入口 / 版本与构建信息常量) 代码行数:20
1. 文件职责
本文件是 EMS 3.6 固件的构建信息登记处:它把"固件型号 + 软件版本号 + 编译时间"三要素拼成一条固定格式的只读字符串常量 BUILD_INFO,供云端/上位机在设备重启上报时读取,同时也让发布脚本能从产出的 .bin 里直接反查"这个二进制是哪份源码、什么时候编译的"。它不参与任何业务数据流,也没有任务、状态或周期调用——唯一的写者就是编译器(__DATE__、__TIME__),唯一的读者是 net/ems.c 的 fill_send_info()(net/ems.c:2429,引用点 net/ems.c:2459)。它被 user/EMS_3_0.uvprojx 直接编译进工程(<FileName>version.c</FileName>,路径 .\version.c),是典型的一次性链接资料型源文件。
2. 关键数据结构与宏
| 名称 | 类型 | 说明 |
|---|---|---|
BUILD_INFO | 全局只读字符串常量(const char []) | 本文件唯一定义的对象(version.c:9 或 version.c:14,两个互斥分支)。内容为 "FW:<fw>;VER:<ver>;TS:<date time>\0",带 NUL 结尾 |
FW_TYPE_EMS3_6 | 宏(引用外部定义) | 定义于 net/server.h:137,值 "EMS3.6"。经 version.c:1 的 #include "server.h" 引入 |
EMS_SOFT_VERSION | 宏(引用外部定义) | 定义于 net/server.h:138,等于 VERSION_STRING |
VERSION_STRING | 宏(间接引用) | 定义于 net/server.h:113-114:当 VER_TYPE == VER_TYPE_RELEASE 时等于 VER_MAIN |
VER_MAIN | 宏(间接引用) | 定义于 net/server.h:109,当前为 "V0.11.676" |
VER_SEQUENCE | 宏(间接引用) | 定义于 net/server.h:110,当前为空串 ""(临时版本必填) |
VER_TYPE | 宏(间接引用) | 定义于 net/server.h:111,当前为 VER_TYPE_RELEASE(= 0,server.h:102) |
__DATE__ / __TIME__ | 编译器预定义宏 | C 标准预定义宏。__DATE__ 展开为 "Mmm dd yyyy" 形式(单数字日期用空格补齐,如 "Oct 9 2025"),__TIME__ 展开为 "hh:mm:ss" |
__ARMCC_VERSION / __GNUC__ / __clang__ | 编译器预定义宏 | 用于选择 BUILD_INFO 的两种定义方式(见 4.2) |
__attribute__((used)) | GNU/Arm 扩展属性 | 附着在 BUILD_INFO 上,提示编译器"该符号被使用",防止被优化删除 |
本文件不定义任何结构体、枚举、联合体或位域,只有字符串常量与预处理器分支。
2.1 当前宏展开后的实际字符串
按本版本 net/server.h 的取值(VER_MAIN = "V0.11.676"、VER_TYPE = VER_TYPE_RELEASE、VER_SEQUENCE = ""):
FW:EMS3.6;VER:V0.11.676;TS:Oct 9 2025 16:48:00
FW:段来自FW_TYPE_EMS3_6="EMS3.6"VER:段来自EMS_SOFT_VERSION=VERSION_STRING=VER_MAIN="V0.11.676"TS:段来自__DATE__ " " __TIME__,形如"Oct 9 2025 16:48:00"(注意月份与日期之间在单数字日期时是两个空格,来自__DATE__的固定宽度格式)- 末尾的
"\0"是显式补的第二个 NUL,字符串字面量本身还会自动追加一个 NUL,因此该数组总长度为strlen(内容) + 2
3. 函数清单
| 函数名 | 作用域 | 说明 |
|---|---|---|
keep_build_info | 全局(version.c:19) | 空函数,内部只把一个指向 BUILD_INFO 的 volatile const char * 取出并 (void) 丢弃,用途是"制造一个外部可见的引用,防止链接器把 BUILD_INFO 扔掉"。仅在 #else 分支中编译,当前工具链下不参与编译(见 4.2) |
本文件不存在 static 函数、不存在函数指针,也没有"函数清单意义上的多个函数"——除 keep_build_info(条件编译)外全部是数据定义。
4. 核心逻辑解析
4.1 字符串格式契约(version.c:3-6 注释明确写死)
"FW:<fw>;VER:<ver>;TS:<date time>\0"
| 字段 | 分隔符 | 内容来源 | 示例 |
|---|---|---|---|
FW: | 分号 ; | FW_TYPE_EMS3_6(net/server.h:137) | EMS3.6 |
VER: | 分号 ; | EMS_SOFT_VERSION(net/server.h:138) | V0.11.676 |
TS: | 无后续分隔符 | __DATE__ + 空格 + __TIME__ | Oct 9 2025 16:48:00 |
三个字段全部出现,顺序固定,字段名与冒号不可省略——这一点由 user/rename_bin.ps1 的正则反向锁定:rename_bin.ps1:89 用 'FW:([ -~]{1,300});VER:([ -~]{1,300});TS:([ -~]{1,300})' 在整个 .bin 的 ASCII 视图中做匹配(rename_bin.ps1:86 把二进制整体按 ASCII 解码),成功时把 TS 段经 NormalizeTimestamp 归一化为 yyyyMMdd_HHmmss,并用 FW/VER/TS 三段重命名 ../OBJ/EMS3.bin。因此本文件与发布脚本之间存在隐式的格式耦合契约:只改 version.c 的字符串格式而不改脚本,发布流程会静默退化成"拿当前时间当时间戳"(rename_bin.ps1:104、112-114)。
4.2 预处理器分支的选择逻辑(version.c:7-20)
#if defined(__GNUC__) || defined(__clang__) || defined(__ARMCC_VERSION) // version.c:7
const char BUILD_INFO[] __attribute__((used)) = ...; // version.c:9-12
#else
const char BUILD_INFO[] = ...; // version.c:14-17
void keep_build_info(void) { ... } // version.c:19
#endif // version.c:20
| 分支 | 触发条件 | 防优化手段 |
|---|---|---|
#if 分支(version.c:9-12) | GNU GCC、Clang、任意版本 Arm Compiler(__ARMCC_VERSION) | __attribute__((used)) |
#else 分支(version.c:14-19) | 上述三个宏都未定义(理论上只剩 IAR、Tasking 等) | 额外定义一个全局函数 keep_build_info() 制造引用 |
本工程实际走 #if 分支:工程文件 user/EMS_3_0.uvprojx 中 <uAC6>0</uAC6>,即使用 Arm Compiler 5(armcc),它会定义 __ARMCC_VERSION(且默认不定义 __GNUC__),因此条件成立、__attribute__((used)) 生效。结论:keep_build_info()(version.c:19)在当前配置下是死代码,#else 分支整体不可达。
4.3 静态存储与"防止被丢弃"的真实生效路径
BUILD_INFO是文件作用域的非 staticconst数组。在 C 语言中,文件作用域const对象默认具有外部链接(与 C++ 不同),所以它可以被其它编译单元以extern const char BUILD_INFO[];引用——net/ems.c:127正是这样声明的。__attribute__((used))只约束编译器不要因为"本单元内无人引用"而删除它,不约束链接器。真正让链接器保留它的是net/ems.c:2459的这次实际引用。- 因此本文件存在一个隐含的双保险依赖:如果某天
net/ems.c的引用被删掉、同时工具链换成不认__attribute__((used))的编译器,BUILD_INFO会被整体丢弃,rename_bin.ps1就只能退化成读server.h源码(rename_bin.ps1:116-149的回退路径),发布产物就丢失了"编译时间"这一维度。 BUILD_INFO没有指定__attribute__((section(...)))或固定地址,因此任何要定位它的工具都必须全镜像扫描(这正是rename_bin.ps1:85-86的做法)。
4.4 通信协议帧格式
本文件不涉及通信。但它产出的字符串会被嵌进云端上报的 JSON 里:net/ems.c:2450-2459 在 get_info_status() & SYSTEM_RESTART 成立时,用 snprintf 拼出 {"MsgType":"System","MsgContent":"System restart:...! Type %s bootver:%s Build info:%s",...},其中最后一个 %s 就是 BUILD_INFO(net/ems.c:2459)。所以这条字符串会出现在"设备重启"事件的 MsgContent 文本中,字段名固定为 Build info:;同一条消息里还有 Type(来自 inverter_read_inv_type(),ems.c:2457)和 bootver(来自 get_ems_boot_ver(),定义于 net/ems_boot_handle.c:196)。
4.5 定时 / uCOS-II 配合
无。本文件不创建任务、不使用信号量/消息邮箱,也不包含任何 uCOS-II 头文件——注意它只 include 了 server.h(version.c:1),而 server.h 自身会链式引入工程庞大的类型头链。BUILD_INFO 是只读常量,位于 flash 的只读段,可被任意多个任务并发读取而无竞态。
4.6 关键全局变量如何被读写
| 变量 | 写者 | 读者 |
|---|---|---|
BUILD_INFO | 无运行时写者(编译期常量) | net/ems.c:127(extern 声明)、net/ems.c:2459(snprintf 的 %s 实参);离线工具 user/rename_bin.ps1:89 通过扫描 .bin 读取 |
5. 对外接口
本文件导出(供外部使用)
| 名称 | 类型 | 使用方 |
|---|---|---|
BUILD_INFO | 全局 const char[](外部链接) | net/ems.c:127 声明、net/ems.c:2459 使用(设备重启上报的 Build info: 字段);离线发布脚本 user/rename_bin.ps1(二进制扫描) |
keep_build_info | 全局函数 | 无调用方;当前工具链下不编译(见 4.2)。设计意图是给 IAR 等无 __attribute__((used)) 的编译器提供"强制引用"锚点 |
本文件引用(外部符号)
| 引用 | 来源 |
|---|---|
#include "server.h" | net/server.h(提供 FW_TYPE_EMS3_6、EMS_SOFT_VERSION,并链式引入 VERSION_STRING / VER_MAIN / VER_SEQUENCE / VER_TYPE) |
__DATE__、__TIME__ | 编译器预定义 |
__attribute__((used)) | Arm/GNU 编译器扩展 |
本文件不调用任何函数、不引用任何外部变量实体(只引用宏)。
6. 依赖关系
依赖:
net/server.h(version.c:1)。这是一个体量很大的头文件(300 行,含固件升级平台枚举UPGRADE_NXP/UPGRADE_GD32、各类文件名掩码FILE_MASK_*、版本类型定义、升级结构体等),仅仅为了拿 2 个字符串宏就把它整份引入,编译期成本与耦合面都偏大。更合适的位置是把FW_TYPE_*/EMS_SOFT_VERSION移到独立的version.h。- 编译器预定义宏
__DATE__/__TIME__(因此本文件的内容每次重新编译都会变)。
被依赖:
net/server.h:137-139提供被引用的宏(反向依赖)。net/ems.c:127、net/ems.c:2459使用导出的BUILD_INFO。user/rename_bin.ps1(rename_bin.ps1:84-149)在离线打包环节扫描.bin中的BUILD_INFO。user/EMS_3_0.uvprojx:417-419把本文件列入user组参与编译。
7. 备注与风险点
keep_build_info()在当前工具链下是死代码:version.c:7的条件包含__ARMCC_VERSION,而工程<uAC6>0</uAC6>表明用的是 Arm Compiler 5,必然定义__ARMCC_VERSION,所以走#if分支,#else分支(version.c:14-19)永远不会被编译。也就是说这段"备选方案"从未经过实测,若将来真换到 IAR,需要重新验证它是否真能阻止链接器丢弃(volatile const char *p = BUILD_INFO;只是在函数体内取了地址,函数本身若无人调用,仍可能被链接器整体移除 —— 除非加上__root/__used之类的编译器专属属性)。- 构建不可复现(non-reproducible build):
__DATE__/__TIME__会让同一份源码在不同时间编译出不同二进制。这是本文件的设计目的(要留编译时间),但对"逐字节比对两个 .bin 是否来自同一源码"的校验流程是干扰项;做差异比对时必须先把该字符串区间归一化。 - 隐式的格式契约没有机器可校验的保护:
version.c:4-5的格式说明只是注释,rename_bin.ps1:89的正则才是真正的消费者。任何一方改动(例如把;换成,、把TS:改名)都不会有编译期报错,只会让发布脚本静默降级为"用当前时间"(rename_bin.ps1:104),产出的文件名时间戳与实际编译时间不符。建议把格式定义抽到一个头文件里,让 C 代码与脚本都以它为准。 - 显式
"\0"冗余:version.c:12与version.c:17的末尾"\0"会在字符串字面量自动追加的终止符之外再补一个 NUL,使数组比内容多出 2 字节。功能上无害(多一个 NUL 反而让正则扫描更稳妥),但属于对 C 语义的重复表达;若将来有人按sizeof(BUILD_INFO)计算长度并参与协议长度字段,会多算 2 字节。 BUILD_INFO的链接可见性依赖 C 语言规则:文件作用域const数组在 C 中有外部链接,这是net/ems.c:127的extern const char BUILD_INFO[];能链接成功的前提。若本文件被误改扩展名为.cpp、或工程切到 C++ 编译,const会变成内部链接,net/ems.c会直接报未定义引用。当前工程是纯 C(Keil 中.c由 armcc 按 C 编译),安全,但这是一个语言层面的隐性约束,未在任何注释中说明。- 为 2 个宏引入整份
server.h:version.c:1的 include 会把net/server.h的 300 行全部拉进来(含UPGRADE_*枚举、FILE_MASK_*常量、多个升级结构体)。对本文件的实际需求而言是过度包含,也造成了user/层对net/层的反向编译依赖——按工程的分层约定,user/应当是最上游、最不依赖具体业务模块的一层。同时,user/层对net/层的依赖还使得version.c无法脱离网络模块单独编译。 FW:EMS3.6与目录名platform-ems-3-6的版本标识重复:FW_TYPE_EMS3_6是常量"EMS3.6"(net/server.h:137),而软件版本V0.11.676会随每次发布变化(server.h:109)。rename_bin.ps1:128-133的回退路径甚至用了#define FW_TYPE_EMS3_6 "EMS3.66"这样的示例正则做匹配演示,与实际值"EMS3.6"不一致——仅注释/示例层面的不一致,不影响脚本逻辑(正则只捕获引号内内容)。- 无
static限定:BUILD_INFO是全局符号,其名字比较通用,理论上存在与其它编译单元重名的风险(当前工程内无重名)。若改用static const char,则会失去被net/ems.c与rename_bin.ps1引用的能力,所以这里的"不带 static"是有意的,属于正确取舍,但注释中未说明,容易被后续维护者"顺手加上 static"而破坏。