设备型号、固件名、固件版本、构建日期、构建时间唯一来源是固件包的
Cargo.toml([package] version)与固件编译期常量,不是手填、
不是 sign-tool 自己的版本。固件构建时把这五项写进镜像固定位置
的版本块;pack 打包时从 bin 自动读五项,写入 GWPK v3 包头;
pack 没有 --version 参数。包头版本与固件自报版本因此永远
一致,包头型号/固件名与固件自报身份也永远一致。
只有格式 2 块。块格式版本不是 2 直接报错,必须按格式 2 重建固件。
pack 只按 bin 偏移读,不用知道固件链接地址。主.次.补,如 1.2.3。三个点分十进制整数。v 前缀(Cargo.toml 本身也不接受 v1.2.3)。-alpha)和构建元数据(+build):块里只有 3 个
u16,没地方放;升级排序也不处理预发布。build.rs 读 CARGO_PKG_VERSION,解析成三个 u16,
生成版本常量编进版本块。格式不对(不是三段数字、有后缀、某段超
65535)则编译报错,版本错在构建时就暴露,不流到打包。v:如 pack 输出 v1.2.3。存储时不带 v。身份字段存储顺序:型号、固件名、版本、构建日期、构建时间。
| 偏移 | 长度 | 字段 | 取值要求 |
|---|---|---|---|
| 0..4 | 4 | 魔数 | 字节 FWVR(0x46 0x57 0x56 0x52) |
| 4 | 1 | 块格式版本 | 固定为 2 |
| 5..105 | 100 | 设备型号 | ASCII,零填充(规则见 docs/package-format.md 第 1.1 节) |
| 105..205 | 100 | 固件名 | ASCII,零填充(同上) |
| 205..207 | 2 | 主版本号 major | u16,小端序 |
| 207..209 | 2 | 次版本号 minor | u16,小端序 |
| 209..211 | 2 | 补丁号 patch | u16,小端序 |
| 211..219 | 8 | 构建日期 | ASCII,YYYYMMDD,如 20251031 |
| 219..225 | 6 | 构建时间 | ASCII,HHMMSS(UTC),如 171000 |
| 225..228 | 3 | 保留 | 固定为 0 |
bin 里没有版本块
时,0x1000 处是代码,恰好以 FWVR 开头且格式版本与保留字节全对的
概率可忽略。省掉 CRC 就不用在固件构建时算 CRC、打补丁,固件只要
放一个静态常量。bin 长度必须 ≥ 0x1000 + 228 = 4324 字节,否则视为无版本块。pack 没有 --version 参数,永远从 bin 版本块读型号、固件名、
版本、构建日期、构建时间,写入 GWPK v3 包头。给 --version
不生效(未知参数直接忽略)。设备收到的不是 bin,是 .pkg 包。.pkg = 312 字节 v3 包头 +
加密后的固件。包头里有型号(8-108 字节)、固件名(108-208 字节)、
版本(208-214 字节,三个 u16)、构建日期(214-222 字节)、构建时间
(222-228 字节)、明文长度(228-232 字节)、标志(232-236 字节,
恒 3)、包体随机数(236-248 字节)、签名(248-312 字节);
签名范围覆盖包头前 248 字节 + 固件明文,所以
包头里的型号、固件名、版本、日期、时间都改不了(改了签名就失效)。
注意:这里的“格式 2”只是固件内部版本块的格式号,与包格式 v3
是两回事——版本块仍是格式 2(228 字节),包头是 v3(312 字节)。
包密钥不再是包头字段,另以 80 字节包密钥包按设备下发。
pack 把 bin 块里的型号、固件名、版本、构建日期、构建时间原样抄到包头
(不做任何变换),所以包头五项与 bin 版本块里的五项是同一组,
固件自报(读同一个 FW_VERSION 常量)与它也一致,错位问题从源头消失。stm32h743vi、
固件名 gateway),不一致拒绝安装;其他格式版本一律拒绝。/pack 接口同样从上传固件的版本块读四项,不接受调用方
传版本(见 server/README.md)。build.rs(读固件包的 Cargo.toml 版本,解析成三个 u16,并取当前构建
日期与时间(UTC,YYYYMMDD/HHMMSS),生成版本常量;格式不对则编译报错。
build.rs 跑在主机上,不受固件 no_std 限制,chrono 作 build-dependency 即可):
// build.rs(示意):读 CARGO_PKG_VERSION,解析,写 OUT_DIR/version.rs
let v = std::env::var("CARGO_PKG_VERSION").unwrap();
let p: Vec<u16> = v.split('.').map(|s| s.parse().expect("version must be major.minor.patch, each 0..=65535")).collect();
assert_eq!(p.len(), 3, "version must be major.minor.patch");
let now = chrono::Utc::now();
let date = now.format("%Y%m%d").to_string();
let time = now.format("%H%M%S").to_string();
std::fs::write(format!("{}/version.rs", std::env::var("OUT_DIR").unwrap()),
format!("pub const VERSION_MAJOR: u16 = {};\npub const VERSION_MINOR: u16 = {};\npub const VERSION_PATCH: u16 = {};\npub const BUILD_DATE: [u8; 8] = *b\"{}\";\npub const BUILD_TIME: [u8; 6] = *b\"{}\";\n", p[0], p[1], p[2], date, time)).unwrap();
固件源码里的版本常量(静态结构,链接脚本把它定到 bin 偏移 0x1000)。
注意必须用 #[repr(C, packed)]:版本号 u16 在偏移 205 处(奇数地址),
普通 repr(C) 会按 2 字节对齐补填充,结构布局就不再是 228
字节、字段偏移全错位;packed 去掉填充,结构与块格式逐字节一致。
固件只初始化一次、按值读字段,不取字段引用,packed 的限制碰不到。
末尾的编译期断言保证结构大小永远是 228 字节,布局一漂移就编不过:
include!(concat!(env!("OUT_DIR"), "/version.rs"));
/// 编译期把字符串填成 100 字节零填充数组(超长会在编译期报错)。
const fn padded100(s: &str) -> [u8; 100] {
let mut out = [0u8; 100];
let b = s.as_bytes();
let mut i = 0;
while i < b.len() {
out[i] = b[i];
i += 1;
}
out
}
#[repr(C, packed)]
pub struct FwVersionBlock {
pub magic: [u8; 4], // b"FWVR"
pub block_version: u8, // 2
pub model: [u8; 100], // 如 b"stm32h743vi",零填充
pub firmware_name: [u8; 100], // 如 b"gateway",零填充
pub major: u16,
pub minor: u16,
pub patch: u16,
pub build_date: [u8; 8], // 如 b"20251031",ASCII
pub build_time: [u8; 6], // 如 b"171000",ASCII,UTC
pub reserved: [u8; 3], // 0
}
const _: () = assert!(core::mem::size_of::<FwVersionBlock>() == 228);
#[link_section = ".fw_version"]
#[used]
pub static FW_VERSION: FwVersionBlock = FwVersionBlock {
magic: *b"FWVR",
block_version: 2,
model: padded100("stm32h743vi"),
firmware_name: padded100("gateway"),
major: VERSION_MAJOR,
minor: VERSION_MINOR,
patch: VERSION_PATCH,
build_date: BUILD_DATE,
build_time: BUILD_TIME,
reserved: [0; 3],
};
链接脚本示例(固件链接地址为 FLASH_BASE 时,版本段定到
FLASH_BASE + 0x1000;向量表段在前、代码段在后,版本段 228 字节,
中间填充补齐,代码段不得与版本段重叠):
.fw_version (FLASH_BASE + 0x1000) : {
KEEP(*(.fw_version))
} > FLASH