4、防回滚计数器实现:三种主流方案深度解析
防回滚,说白了就是让固件版本只能往前走,不能往后退。这个机制的核心,就是那个计数器。计数器一旦写进去,就再也回不来了。今天咱们聊聊三种最常见的实现方案:eFuse、OTP 和 TPM。
我个人习惯把防回滚计数器比作「单向门」。你推开门走进去,门就锁死了。想退回去?没门。这三种方案,本质上都是造这种单向门,只是材料和工艺不同。
核心原则:防回滚计数器的写入操作必须是不可逆的。一旦增加,就不能减少。这是所有方案的底线。
4.1 基于 eFuse 的计数器方案
eFuse,电子熔丝。你可以把它想象成芯片内部的一根根小保险丝。出厂时都是通的(0),一旦你烧断它(1),就再也接不回去了。
我在项目中第一次用 eFuse 做计数器时,心里还挺没底的。烧断了还能再烧吗?后来发现,每个 eFuse 位只能烧一次。所以,你要用多个 eFuse 位来拼成一个计数器。
实现方式
- 二进制加权法:用 N 个 eFuse 位表示 0 到 2^N-1 的计数值。每次升级,烧断对应的位组合。
- 独热码法:每个版本对应一个独立的 eFuse 位。升级到版本 N,就烧断第 N 个 eFuse。
- 混合法:结合上面两种,用少量位表示大版本,用独热位表示小版本。
| 方案 | 位效率 | 安全性 | 适用场景 |
|---|---|---|---|
| 二进制加权法 | 高(N 位表示 2^N 个值) | 中(可能被中间值攻击) | 版本数量多,但安全要求一般 |
| 独热码法 | 低(N 位只能表示 N 个值) | 高(每个版本独立,无法篡改) | 版本数量少,安全要求极高 |
| 混合法 | 中 | 高 | 大多数实际项目 |
我曾经踩过一个坑:用二进制加权法时,没有考虑烧断过程的原子性。如果烧断过程中断电,eFuse 可能处于中间状态,导致计数器值既不是旧版本也不是新版本。后来我加了校验位和冗余存储,才解决这个问题。
代码示例:eFuse 写入与校验
// eFuse 写入函数(伪代码)
// 假设使用 8 个 eFuse 位,独热码方式
#define EFUSE_BASE_ADDR 0x40020000
#define MAX_VERSION 8
int efuse_write_version(uint8_t version) {
if (version >= MAX_VERSION) return -1;
uint32_t *efuse_reg = (uint32_t *)EFUSE_BASE_ADDR;
uint32_t pattern = 1 << version;
// 检查该位是否已经烧断
if (*efuse_reg & pattern) {
return 0; // 已经烧过,无需重复操作
}
// 写入 eFuse(烧断操作)
*efuse_reg = pattern;
// 回读校验
if ((*efuse_reg & pattern) != pattern) {
return -2; // 写入失败
}
return 0;
}
// 读取当前版本
uint8_t efuse_read_version(void) {
uint32_t *efuse_reg = (uint32_t *)EFUSE_BASE_ADDR;
uint32_t value = *efuse_reg;
// 找到最高位的 1(假设版本号从 0 开始递增)
for (uint8_t i = 0; i < MAX_VERSION; i++) {
if (value & (1 << i)) {
return i;
}
}
return 0; // 没有烧断任何位,版本为 0
}
4.2 基于 OTP 存储的计数器方案
OTP,One-Time Programmable,一次性可编程存储器。它和 eFuse 原理类似,但实现方式不同。OTP 通常使用浮栅晶体管,通过注入电荷来改变存储状态。写入后,电荷被「困」在浮栅里,无法移除。
你想想看,OTP 和 eFuse 最大的区别是什么?OTP 通常以字节为单位操作,而 eFuse 是位级别的。这意味着 OTP 的写入粒度更大,但管理起来也更方便。
实现方式
- 字节递增法:每个字节代表一个版本号。写入时,将对应字节从 0xFF 改为 0x00(或相反)。
- 区域划分法:将 OTP 区域分成多个槽位,每个槽位存储一个版本号。升级时写入新的槽位。
- 磨损均衡法:循环使用 OTP 区域,避免频繁写入同一位置导致物理损坏。
我个人建议:如果 OTP 空间充足,优先使用区域划分法。每个版本独立占一个槽位,读取时扫描所有槽位取最大值。这样即使写入过程中断,也不会破坏已有数据。
代码示例:OTP 区域划分法
// OTP 区域划分法实现
#define OTP_BASE_ADDR 0x40030000
#define SLOT_SIZE 4 // 每个槽位 4 字节
#define MAX_SLOTS 32 // 最多 32 个槽位
#define SLOT_MAGIC 0xA5A5A5A5 // 槽位有效标志
typedef struct {
uint32_t magic; // 有效标志
uint32_t version; // 版本号
uint32_t checksum; // 校验和
uint32_t reserved; // 保留
} otp_slot_t;
int otp_write_version(uint32_t version) {
otp_slot_t *slot;
// 找到第一个空的槽位
for (int i = 0; i < MAX_SLOTS; i++) {
slot = (otp_slot_t *)(OTP_BASE_ADDR + i * SLOT_SIZE);
if (slot->magic != SLOT_MAGIC) {
// 写入新版本
slot->magic = SLOT_MAGIC;
slot->version = version;
slot->checksum = version ^ 0xFFFFFFFF;
return 0;
}
}
return -1; // 槽位已满
}
uint32_t otp_read_max_version(void) {
uint32_t max_ver = 0;
otp_slot_t *slot;
for (int i = 0; i < MAX_SLOTS; i++) {
slot = (otp_slot_t *)(OTP_BASE_ADDR + i * SLOT_SIZE);
if (slot->magic == SLOT_MAGIC) {
// 校验 checksum
if ((slot->version ^ 0xFFFFFFFF) == slot->checksum) {
if (slot->version > max_ver) {
max_ver = slot->version;
}
}
}
}
return max_ver;
}
4.3 基于 TPM 的计数器方案
TPM,Trusted Platform Module,可信平台模块。这东西是个独立的硬件安全芯片,自带防篡改能力。TPM 内部有专门的单调计数器(Monotonic Counter),硬件保证只能增加不能减少。
我记得第一次用 TPM 做防回滚时,感觉就像开了外挂。不用自己操心物理层的烧断问题,TPM 全包了。你只需要调用它的 API 就行。
TPM 计数器的优势
- 硬件隔离:计数器值存储在 TPM 内部,主 CPU 无法直接访问,只能通过命令接口操作。
- 原子操作:TPM 保证每次增加操作要么成功,要么失败,不会出现中间状态。
- 持久化存储:TPM 内部有 NVRAM,断电不丢失。
- 认证机制:可以通过 TPM 的签名功能验证计数器的真实性。
关键点:TPM 的单调计数器分为两种:全局计数器(整个 TPM 共享)和 特定计数器(每个应用独立)。做固件防回滚时,建议使用特定计数器,避免与其他应用冲突。
TPM 命令示例(使用 TPM 2.0)
// TPM 2.0 单调计数器操作(伪代码)
// 使用 TPM2_Increment 命令增加计数器
// 1. 创建计数器空间
TPM2_NV_DefineSpace(
.authHandle = TPM_RH_PLATFORM,
.auth = "platform_auth",
.nvIndex = 0x01C00001, // 自定义 NV 索引
.nvAttributes = TPMA_NV_COUNTER | TPMA_NV_AUTHWRITE,
.dataSize = 8
);
// 2. 增加计数器
TPM2_NV_Increment(
.authHandle = 0x01C00001,
.nvIndex = 0x01C00001,
.auth = "nv_auth"
);
// 3. 读取计数器值
TPM2_NV_Read(
.authHandle = 0x01C00001,
.nvIndex = 0x01C00001,
.auth = "nv_auth",
.size = 8,
.offset = 0
);
4.4 三种方案对比
| 特性 | eFuse | OTP | TPM |
|---|---|---|---|
| 写入次数 | 每位一次 | 每位一次 | 理论无限(有磨损) |
| 写入粒度 | 位 | 字节/页 | 计数器值 |
| 安全性 | 高(物理不可逆) | 高(物理不可逆) | 极高(硬件隔离+认证) |
| 成本 | 低(芯片内集成) | 低(芯片内集成) | 高(独立芯片) |
| 灵活性 | 低(烧断后无法修改) | 中(可规划多个槽位) | 高(可重置计数器) |
| 适用场景 | 低成本 MCU | 中端 SoC | 高端安全设备 |
4.5 我的选择建议
嗯,这里要注意。没有银弹。选哪种方案,取决于你的项目预算和安全等级。
- 如果你在做消费级 IoT 设备,成本敏感,用 eFuse 就够了。8 个 eFuse 位用独热码,支持 8 次升级,对大多数产品来说绰绰有余。
- 如果你在做工业控制器,生命周期长,升级次数多,用 OTP 区域划分法。32 个槽位,每个槽位 4 字节,总共才 128 字节,几乎不占什么成本。
- 如果你在做金融终端或汽车 ECU,别犹豫,上 TPM。虽然贵点,但安全等级完全不是一个量级。而且 TPM 有标准 API,代码移植性也好。
我曾经见过一个项目:用 eFuse 做计数器,但没考虑生产测试阶段的版本号消耗。结果产品还没出货,eFuse 位就用完了。后来他们不得不在产线上用专用工具预烧一个初始版本号,才解决这个问题。所以,设计时一定要预留足够的余量。