固件版本块格式 2

设备型号、固件名、固件版本、构建日期、构建时间唯一来源是固件包的 Cargo.toml([package] version)与固件编译期常量,不是手填、 不是 sign-tool 自己的版本。固件构建时把这五项写进镜像固定位置 的版本块;pack 打包时从 bin 自动读五项,写入 GWPK v3 包头; pack 没有 --version 参数。包头版本与固件自报版本因此永远 一致,包头型号/固件名与固件自报身份也永远一致。

只有格式 2 块。块格式版本不是 2 直接报错,必须按格式 2 重建固件。

位置:bin 偏移 0x1000,固定不变

版本号格式

块格式 2(228 字节,整数小端序,字符串 ASCII)

身份字段存储顺序:型号、固件名、版本、构建日期、构建时间。

偏移 长度 字段 取值要求
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

pack 读块规则

pack 行为(无 --version)

与 GWPK 包头的关系

设备收到的不是 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 字节包密钥包按设备下发。

固件侧实现

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