帧计数器回绕与重放攻击防护:32位FCntUpDown的持久化存储设计
LoRaWAN 的帧计数器持久化,核心矛盾不在于“要不要存”,而在于“存得太频繁会磨损 Flash,存得太少会丢计数器”。32 位 FCntUp 每发一帧就加一,如果每次递增都写一次 Flash,STM32WLE5 标称的 10K 次擦写寿命在几万帧之后就会耗尽。但如果只在入网时存一次,设备意外复位后计数器归零,网络服务器会将其视为重放攻击而丢弃所有上行帧。持久化设计的工程目标,是在“计数器安全”和“Flash 寿命”之间找到可接受的平衡点。
回绕的物理含义:32 位计数器与 16 位空中字段
LoRaWAN 1.0.4 强制要求设备维护完整的 32 位帧计数器,但空口传输的 FCnt 字段只有 16 位。这意味着设备内部必须维护一个 32 位的 FCntUp,发送时只取低 16 位填入帧头。网络服务器收到后,根据上一次记录的 32 位值推断高位:如果收到的 16 位值小于上次的低 16 位,服务器认为发生了 16 位回绕,将高位加一。
这一推断机制的边界是 65K:如果设备连续未 heard 的上行帧超过 65535 次(即超过一次 16 位回绕),网络服务器只会尝试一次回绕推断,解密将失败。工程含义是:设备在长时间离线后重新上线,如果期间累计发送超过 65K 帧但服务器未记录,需要重新入网。对于上报间隔为分钟级的传感器,65K 帧对应约 45 天——这意味着设备离线超过一个半月后,可能需要 OTAA 重新入网才能恢复通信。
1.0.4 还明确规定:32 位 FCnt 达到最大值后不允许回绕,设备必须重新入网。这与早期版本中 16 位计数器可无限回绕的行为截然不同,进一步强化了持久化的必要性——计数器一旦丢失,无法通过“让它自己转回来”的方式恢复。
持久化的存储策略:写频次与安全性的权衡
ST 社区的实际案例揭示了最直接的问题:开发者保存了 NVM 上下文,但复位后发送上行帧仍被服务器忽略,原因是帧计数器没有被正确保存。该开发者最终的解决方案是“每次设备发送上行帧时保存一次”。这确保了计数器不丢失,但直接指向了 Flash 磨损问题。
RAKwireless 论坛的讨论给出了更具体的警示:如果每次 FCnt 变化都同步到 Flash 的一页,STM32WLE5 的 10K 次擦写寿命将很快耗尽,且掉电发生在页擦除时刻会导致配置丢失。更合理的策略是周期性持久化而非每次递增都写:在 RAM 中维护 FCntUp 的实时值,每隔 N 帧(例如 100 帧或 1000 帧)将当前值写入 Flash。掉电时最多丢失最近 N 帧的计数增量,网络服务器端记录的计数器值会高于设备恢复后的值。
丢失 N 帧计数增量的后果取决于 N 的大小。如果设备恢复后从“上次持久化的值”继续计数,而服务器端的值已经高出 N,设备发出的帧会被服务器拒绝(FCnt 小于服务器记录值)。解决方法是:设备在恢复后,将 FCntUp 设置为“持久化值 + 安全余量”(余量大于 N),使设备的新计数器值超过服务器可能记录的任意值。代价是牺牲了 N 个帧的计数器空间,但避免了通信中断。安全余量的选择需权衡:余量越大,恢复后可用帧数越少(离 32 位上限更近);余量越小,掉电时丢失的帧数可能超过余量,导致恢复失败。
对于 ABP 设备,持久化是强制性的,因为 ABP 没有 Join 流程来重置计数器。LoRa Alliance 的文档明确指出,ABP 模式下帧计数器在非易失性存储中的备份是“mandatory”。OTAA 设备虽然每次 Join 会重置计数器,但 1.0.4 同样要求持久化 DevNonce,且频繁重新入网在功耗和网络负载上不可接受。
C 语言实现:结构体与写入时机
LoRaMac-node 的 NVM 数据结构中,帧计数器位于 `LoRaMacCryptoNvmData_t` 的 `FCntList` 字段,包含 `FCntUp`、`NFCntDown`、`AFCntDown` 等成员。持久化的最小实现是:在每次上行发送成功后,检查 `FCntUp` 与上次写入值的差值是否达到阈值(如 100),达到则触发 Flash 写入。
写入时需要注意原子性。如果 Flash 擦除过程中掉电,整个页的数据可能丢失。一个务实的方案是使用双页交替写入:页 A 和页 B 轮流作为当前有效页,写入新数据前先擦除目标页,写入后通过 CRC 校验确认成功,再标记另一页为失效。这样即使某一页在写入过程中损坏,另一页仍保留着上一次完整的数据。STM32WLE5 的 Flash 页大小和擦写时序需查阅参考手册,但核心逻辑与 RAK3172 社区讨论中提到的“单页写入、掉电即丢失”的缺陷形成对比。
对于 STM32WLE 的 LoRaWAN 例程,`LmHandler` 层提供了 `LmHandlerNvmDataStore` 回调,开发者可以在其中实现上述持久化逻辑。需要避免的是在 `OnTxDone` 中断上下文中直接操作 Flash——Flash 擦写耗时毫秒级,中断中阻塞会影响后续 RX 窗口的时序。正确的做法是设置一个标志,在主循环或低优先级任务中执行写入。
结语
32 位帧计数器的持久化,在 LoRaWAN 1.0.4 的语境下不是“优化项”而是“合规项”。回绕机制决定了设备离线超过 65K 帧后需要重新入网,32 位上限意味着计数器耗尽即会话终结。工程实现的关键不在于“每次递增都写 Flash”(这会杀死 STM32WLE5 的 10K 次寿命),而在于“周期写入 + 安全余量恢复 + 双页原子保护”。当设备意外复位时,恢复后的计数器必须高于服务器端可能记录的任何值,否则重放防护机制会忠实地将设备拒之门外。





