本站为个人非经营性网站,仅用于技术学习与分享,不提供任何商品、服务、报价或收费咨询。 合规信息

RS485 总线上的广播式 Bootloader 与 OTA

一台主站、一条 RS485 半双工总线、挂在总线上的多台现场设备。 这个实验想解决的事情很具体:怎么让这些设备既能一台一台地升级,也能一次把同一份固件广播给所有设备。 过程中我改了分区表、写了一套带站号的主从协议、把 8 KB RAM 的账算了一遍又一遍, 也踩了几个“跑起来了但哪里都不对”的坑。下面是完整的记录。

Bootloader OTA RS485 STM32G030 主从协议 已发布
已发布 · 个人学习项目

RS485 总线上的广播式 Bootloader 与 OTA

这个实验做的是:让挂在 RS485 半双工总线上的多台设备,通过一套自己设计的主从协议, 从同一个主站接收同一份固件——既支持指定站号的一对一升级,也支持一次下发给多台设备的广播升级。 之所以没有用现成的 Xmodem / Ymodem,一是它们只能点对点,二是接收端需要的缓冲能力, 这颗只有 8 KB RAM 的芯片给不起。最后的结论是:带站号的分包协议、CRC32 校验、 参数区的 CRC 与恢复默认值、以及跳转这几件事都在个人样机上跑通了; 断点续传、加密升级和失败回滚还没有做,我把原因也写在最后一节里。

PLATFORMSTM32G030C8T6(Cortex-M0+)
STACK裸机 + 自定义主从协议 + RS485 半双工
TOOLSVS Code + STM32 VS Code 扩展
STATUS分包 / 写入 / CRC32 / 跳转已跑通

一、需求:为什么是“一对多广播”而不是“一对一升级”

需求本身很简单:需要对总线上的多台设备做固件更新。麻烦的地方在于,这些设备挂在 RS485 半双工总线上,而且我要同时支持两种方式——1 对 1(只升级其中某一台) 和 1 对多(广播,一次把所有设备都更新掉)。

为什么一定要广播?因为同一批设备烧的往往是同一份固件。如果只能一台一台升级, 主站就要把完全相同的流程重复 N 遍:等待设备、进入升级模式、分包下发、逐包确认、收尾跳转。 总线上设备越多,这个重复越难以接受,而且中间任何一台失败都要单独重来一次, 整体时间会随设备数量线性增长。

但广播并不是“把目标地址去掉”这么简单。半双工总线上任何时刻只能有一个设备在说话: 如果广播的时候每台从站都按点对点的习惯逐包回复,总线上立刻会出现多台设备同时抢总线的情况, 结果是所有回复互相叠加、谁也读不出来,比不回复还糟。所以这套协议里从站的处理原则是 按需回复,而不是“收到什么就回什么”。 这也是我没有照搬一问一答节奏的原因——我在 《Modbus RTU 从站实现笔记:寄存器表、CRC 与异常码》 里写过,一问一答在点对点轮询里非常好用,但放到广播场景下不成立。

另一个约束来自 RS485 本身:收发共用一对差分线,收发方向的切换在硬件上已经固化, 从站根本没有主动开口的机会。这一条决定了协议只能是主从式: 主机发送带站号的报文,从站解析站号,与自身 ID 一致才处理,并按需决定是否回复。 站号在这里承担了两个作用:一对一升级时它是收件人地址,广播时它决定哪些设备参与, 同时也让从站在“我要不要回这一条”上有判断依据。

一台主机接在总线一端,总线上挂多台从站,用广播图形表示一帧被所有从站同时接收,站号过滤在从站侧完成
图 1 · 广播升级拓扑图

二、硬件与资源约束(8 KB RAM 决定了整个方案)

平台是 STM32G030C8T6:Cortex-M0+ 内核,64 KB Flash、8 KB RAM。 开发环境是 VS Code 加 STM32 的 VS Code 扩展,构建产物按 STM32G030C8Tx_FLASH.ld 链接。这一点在做分区规划时很关键:地址是在链接脚本里定的, 不是在一个图形配置界面里点出来的,所以“改了地址但忘了改脚本”这类错误不会有人提醒我。

整个方案里真正的硬约束是 8 KB RAM。设计文档里给“更新的数据框架”划了三块地方:

存储区域功能大小
传输状态存储区(RAM)暂存更新包大小、分包数、已更新包号64 字节
数据包缓冲区(RAM)存储由主站发送的数据包2048 字节
数据包写入区(FLASH)存储更新的代码暂定 32 KB

我算过一笔账:2048 + 64 = 2112 字节,占 8192 字节 RAM 的约 25.8%,也就是四分之一还多。 剩下的六 KB 左右要放栈、堆、业务变量,还要留出中断嵌套的余量。 结论很直接:APP 侧几乎不能有大的常驻缓冲,只能“收到一包、校验一包、写一包”; 想在这套方案里再塞一个 FreeRTOS 或者一个完整的协议栈,基本是不可能的。 这也是我最终没有选 Xmodem / Ymodem 的直接原因——那两个协议假设接收端有缓冲和流控的配合, 而我这边的接收端只有 2048 字节的余地。

表里还有一个数字是“暂定”的:数据包写入区暂定 32 KB,而分区表里 APP 区大约 46 KB。 这两个数字放在一起看,意思是这套方案在设计的时候只打算支持到 32 KB 左右的固件; 如果 APP 长到 40 KB,写入区就不够了,需要重新规划分区。 我现在把它当成一个需要盯着的上限,而不是一个已经解决的问题—— 固件是会长的,这一点在画分区表的时候就得认。

三、分区规划与参数区

分区表是这套方案的地基,我在学习笔记 《Bootloader 跳转与 OTA:分区规划、向量表偏移与容易踩的坑》 里完整讲过一遍取舍过程,这里只贴最终结果,并补上“每一段在升级时扮演什么角色”:

区域地址范围大小升级时的角色
Bootloader0x08000000 ~ 0x08003FFF16 KB不参与升级,负责接收、写入、校验、跳转
应用程序区(APP)0x08004000 ~ 0x0800F7FF约 46 KB每次升级替换的目标
参数段0x0800F800 起约 2 KB升级过程中不动,保存系统参数

参数段采用双区 Flash 存储,偏移量在 system_param.c 里由 STM32_FLASH_PARAM_OFFSET 定义。“参数在哪”这件事在代码里只有一个来源, 这在排查参数问题时非常有用:如果读出来的参数不对,地址只有一种出错可能, 就是这一处和分区表不一致——而这是可以一眼对出来的。

但 0x08004000 这个偏移并不是只写在链接脚本里。它还要在 system_stm32g0xx.c 里以 VECT_TAB_OFFSET = 0x00004000U 的形式再出现一次, 否则会出现那个让我头疼了很久的现象:跳转过去了、LED 会闪、串口能打印, 但所有依赖 SysTick 的延时函数和 HAL 超时判断全部失灵。 详细的原因和排查过程我写在笔记页的第四节( 中断向量表偏移:SysTick 为什么不动了), 这里只留一句结论:同一个地址出现在两个文件里,我在这个项目里印象最深的耦合就是它。

四、通信协议设计:帧格式、功能码与报文示例

协议是自己设计的,目标只有三个:能寻址(这一包是给谁的)、能分包(一包多大)、 能自校验(这一包对不对)。帧格式最后定成这样:

AA BB | 长度(2字节) | 站号(1字节) | 功能码(1字节) | 闲置(1字节,不参与CRC) | 数据(N字节) | CRC32(4字节) | BB AA

拆成字段看,一帧的固定开销是 13 个字节:

字段长度说明
帧头2 字节固定两字节,用来对齐帧起始
长度2 字节数据段长度,以字节为单位;取多少由这一帧带的数据决定
站号1 字节目标设备站号;一对一升级时是目标设备,广播升级时是约定的广播站号
功能码1 字节决定这一帧要做什么,见下文
闲置1 字节预留扩展位,不参与 CRC 校验
数据N 字节长度由“长度”字段给出
CRC324 字节对约定范围内的电文算出的校验值,闲置字节被明确排除在外
帧尾2 字节固定两字节,与帧头成对

关于“闲置字节不参与 CRC 校验”这个设计,我想单独说两句。 好处是它留了一个扩展位:将来想在这里放包序号、放标志位,都不会影响已有的 CRC 算法, 两端升级的节奏可以错开——主站先开始填,从站暂时忽略,也不会因为校验失败而互相不认。 坏处是它和“整帧参与校验”的直觉冲突,实现的时候太容易在某一端顺手把整个缓冲区都算进去。 只要两端对“算到哪一位”的理解差一个字节,现象就是“我算的 CRC 和你算的不一样”, 而且两边各自自测都是通过的。

目前支持的功能有:读取设备存储更新状态空间内的数据、读取设备当前状态、写入固件数据包、 写入硬件号、写入软件号,后续可视情况增加。 这里我不逐条写出功能码的具体编码值:编码是两端约定的常量, 记错一个会造成很隐蔽的错位;而且这份记录真正的价值在“每个功能做什么、什么时候用”, 不在于那几个字节的取值。

写入硬件号和写入软件号这两条命令,报文结构完全一样,只有功能码不同; 回复上也不一样:写入硬件号成功时,从站回复与请求完全相同的报文; 写入软件号成功时同样回原电文,不成功时回复当前软件号—— 这样主站一次交互就能知道设备现在跑的是哪一版,不用再单独发一条查询, 在广播升级之后逐台核对版本时省了不少事。

报文本身我不贴出来了。一条完整报文里带着站号、功能码和校验值, 原样贴出来就等于给了一份可以直接复用的样例;这里改成按字段说明取值含义, 真要动手的时候,照着上面那行帧格式和下面这张表自己拼即可:

字段取值含义
帧头(固定两字节)取值见上面的帧格式,作用是让从站能对齐帧起始
长度这一帧数据段的字节数;写硬件号 / 写软件号这类命令的数据段很短,长度字段的取值也随之很小
目标站号一对一升级时填目标设备的站号;广播升级时填约定的广播站号,总线上所有从站都参与处理
功能码区分“读更新状态 / 读设备状态 / 写固件数据包 / 写硬件号 / 写软件号”,具体取值按两端约定
闲置字节预留扩展位,不参与 CRC32 校验;两端必须先对这个例外达成一致,否则一对接就失败
数据写硬件号时是硬件号本身,写软件号时是软件号,写固件包时是这一包的固件内容
CRC32对约定范围内的电文算出的校验值,闲置字节被明确排除在外,随帧一起下发
帧尾(固定两字节)与帧头成对,用于确认一帧的结束

这两个号的写入时机也不一样:硬件号是收到后即写入 Flash;软件号是在跳入 APP 之前写入 Flash 的。 我理解这么设计的原因是:软件号代表“当前真正在运行的那一版”,由 Bootloader 在交棒之前写下去, 才不会出现“APP 还没跑起来、软件号已经改了”这种中间状态。 如果反过来让 APP 自己去写,一旦新固件启动失败,记录的版本号就是错的, 远程排查会被这个数字带偏。

分包上暂定每包 2 KB,由主机把固件切分成包依次下发, 单包通信内容长度不超过 2060 字节。这两个数字加上前面算过的 13 字节固定开销, 有一个必须一起核对的细节:能塞进一帧的数据段最多是 2060 − 13 = 2047 字节, 比 2048 字节的数据包缓冲区少 1 个字节。也就是说缓冲区刚好不会溢出, 但余量只有 1 字节,等于没有余量。如果重做一遍, 我会把“单包长度上限”和“缓冲区大小”一次性对齐, 而不是让两个数字各留各的余量——这种“差一点就溢出”的设计,是靠运气在跑。

五、升级流程:分包、写入、校验、跳转

把前面这些拼起来,一次升级的流程是这样的:

  1. 主站把固件按 2 KB 切分成包,带站号依次下发;一对一升级时站号写目标设备,广播时所有从站处理同一份数据。
  2. 从站收到一帧,先校验帧长、站号与 CRC32;通过之后把数据段写进“数据包写入区”对应的偏移,不通过就整帧丢弃,不做任何写入。
  3. 传输状态存储区(64 字节 RAM)记录更新包大小、分包数、已更新包号,用来判断当前进度。
  4. 全部包写完之后,主站做一次收尾确认,从站回复状态。
  5. 校验通过:关闭中断 → 写入软件号 → 跳转到 APP。

这个流程里有三个我认为最容易出问题的地方。 第一个是写之前必须先擦:Flash 擦除的最小单位是页,写入只能把 1 变成 0, 所以覆盖写之前一定要把目标页擦干净,否则写进去的数据会变成新旧值相与的结果, 而且读回来还能过 CRC 的概率极低,表现出来就是“升级完启动不了”。 第二个是跳转前必须写完软件号,而且要在关中断之后、跳转之前完成, 顺序反了就会出现“设备已经在跑新固件,但主站读到的还是旧版本号”。 第三个是传输状态在 RAM 里,掉电就没了。

第三点直接解释了为什么“断点续传”不是加几行代码就能有的功能:要续传, 先得知道“上次传到哪儿了”,这个信息必须持久化;一旦要持久化, 就撞上 Flash 擦写寿命那笔账——一颗固件几十包,如果每包都写一次进度, 就是几十次擦写,多升级几次这一页就废了。 所以断点续传真正要先解决的是“进度怎么低成本地持久化”,而不是“怎么把包号记住”。 这笔账我在笔记页的第六节( Flash 寿命:一个必须提前算的账)里算过一遍。

流程里还有一个容易被忽略的角色:看门狗。主程序里加了看门狗, 用来兜住“通信卡死”这类异常——测试用例里就有一条“发送不完整帧超过 200 ms 触发看门狗复位”。 它的存在意味着升级流程里每一步的耗时都要心里有数, 不能在某个等待循环里安静地停太久,否则升级到一半设备自己复位了, 而现场只会看到“升级失败”这一个笼统的结论。

流程:请求进入升级模式 → 主机分包下发 → 从站写入 → 逐包校验 → 整包校验 → 跳转运行,标出断电与校验失败两条
图 2 · 分包升级流程图

六、参数存储与 CRC 校验

参数这块相对独立,但它和升级流程有一个共同的敌人:Flash 写得不对,或者读得不对。

写操作由 STMFLASH_Write_Byte() 完成,实现的是 8 字节对齐写入 (不足的部分补 0xFF);擦除策略是按页擦除(stm32_FLASH_ErasePage)。 补 0xFF 不是随手选的:Flash 擦除之后就是 0xFF, 用 0xFF 补齐才不会破坏“这一段是空白”的语义—— 如果补 0x00,读回来就分不清“我写过 0”和“这里没写过”。

参数结构是 SysSet 和 SysSta,定义在 system_param.h 里; 启动时调用 ParameterIni() 从 Flash 加载参数。 校验方式是把结构体最后一个字当作 CRC 存放位置,前面所有的字参与计算:

/* 参数校验:结构体最后一个字存 CRC,其余字参与计算 */
parCRC = HAL_CRC_Calculate(&hcrc, ptr, sizeof(SysSet)/4 - 1);
if (ptr[sizeof(SysSet)/4 - 1] == parCRC) {
    /* 校验通过,加载参数 */
} else {
    defaultParameter();   /* 恢复默认参数 */
}

校验失败就调用 defaultParameter() 恢复默认参数。这个兜底很重要: 一块从来没烧过参数的板子,或者参数页被意外擦掉的板子,上电之后不会带着一堆随机值跑业务, 而是回到一组已知的默认值(其中就包括一个约定的默认站号)。 对一个要在现场长期运行的设备来说,“回到已知状态”比“保留未知数据”重要得多。

把 CRC 放在结构体尾部、用 sizeof(SysSet)/4 - 1 当长度,是很省事的做法: 不用额外定义偏移量,加一个成员就自动纳入校验范围。 但它对结构体的对齐和填充非常敏感——一旦编译器因为对齐在结构体里插入了填充字节, 或者结构体成员的顺序变了,sizeof 就变了,CRC 立刻对不上, 表现出来就是“固件一升级,参数全丢”。更麻烦的是,这种问题在小改动里不一定出现, 可能等到某次加了一个成员才炸,而现象看起来完全像是“升级把参数擦坏了”, 排查方向一开始就会跑偏。

我的结论是:这类结构体最好显式对齐(比如 __attribute__((packed)), 或者干脆全部用 4 字节成员,把填充空间消灭掉),并且把布局固定下来—— 成员顺序、每个成员的含义、CRC 占哪个字,都写进注释里。 参数结构体的布局,本质上和分区表、寄存器表是同一类东西: 它是一个对外承诺,不是一个可以随手调整的内部细节。

七、测试计划与用例矩阵

我把测试分成硬件测试和功能测试两部分。硬件测试关心“这块 Flash 能撑多久、 在电压边缘还准不准”;功能测试关心“异常情况下系统会不会走到我预期的分支”。

测试计划:

  • 硬件测试:Flash 擦除/写入寿命测试(10 万次循环);不同电压下的参数存储稳定性测试(2.7 V ~ 3.6 V)。
  • 功能测试:模拟 Flash 参数损坏——把 CRC 位置写成 0xFFFF,调用 ParameterIni(),验证是否加载默认参数(包括默认站号)。

这里有两个数字要分清楚:数据手册给出的 1000 次擦除是“保证能正常使用”的下限, 而寿命测试的 10 万次循环是破坏性测试——一直写到出错为止,看它实际能撑多少次。 前者用来做设计判断(能不能拿它写日志),后者用来了解余量。 把这两个数字混在一起谈,结论会完全反过来。

用例矩阵:

测试项测试方法预期结果
Flash 写入对齐传入 63 字节数据自动补 1 字节,按 8 字节对齐写入
CRC 校验失败修改 Flash 存储的 CRC 值系统加载默认参数
通信超时发送不完整帧(>200 ms)触发看门狗复位

这三条里我最看重的是最后一条“通信超时”。因为它验证的不是“正常流程能不能走通”, 而是“异常流程会不会把设备锁死”——前者跑通一次就够,后者要失败很多次才能确认。 后来那次“频繁拔插导致通信卡住”的问题里,正因为看门狗这条路是通的, 设备才没有彻底失联,还留出了自恢复的余地。

八、问题修复记录(现象 → 原因 → 办法)

下面这几条是我调试过程中记下来的,按“现象 → 原因 → 办法”的顺序写。 我特意保留了这个顺序,因为结论过一段时间就会忘,而现象和排查路径才是下次能复用的部分。

1. 参数加载异常(2024-04-28)

  • 现象:首次启动时 NTC 参数未初始化。
  • 修复:在 system_param.c 的参数初始化里,给 NTC 参数补上一个明确的默认值。
  • 验证:连续重启 10 次,参数保持稳定。

复盘:“结构体整体校验通过”并不等于“每个成员都被赋过值”。 如果出厂固件里某个成员恰好是 0 或者 0xFFFFFFFF,CRC 是能过的,但业务上是错的。 所以每个成员都应该有明确的默认值来源:要么在默认参数函数里赋值,要么在首次上电时初始化。 这一条我认为比修复本身重要——它解释了为什么“校验通过”不能当成“数据正确”, 也解释了为什么参数结构体每加一个成员,都要顺手问一句“它的默认值从哪来”。

2. Flash 擦除失败(2024-04-29)

  • 现象:擦除非页对齐地址时返回 HAL_ERROR。
  • 修复:在 stm32_FLASH_ErasePage() 里增加地址合法性检查。
  • 测试:随机地址写入测试通过率 100%。

复盘:Flash 擦除的最小单位是页,非法地址应当被提前拦截并返回明确的错误码, 而不是直接透传给 HAL、最后得到一个笼统的 HAL_ERROR。 一个笼统的错误码会把排查方向引到错误的地方—— 我一开始怀疑的是 Flash 被写坏了,查了一圈才发现只是地址没有对齐。 后来我给自己的规矩是:凡是“参数不合法”这一类前置条件,都要在函数入口拦住, 并且返回能区分原因的错误码。

3. 频繁拔插插头导致通信终止,无法恢复(APP 侧)

  • 现象:频繁拔插插头会导致通信终止,而且不会自己恢复。
  • 原因:频繁拔插会让串口异常地识别到空闲标志位,结果是数据仅仅接收了 1 个字节;该问题多为连接通信线时接收电文的时序异常导致的。
  • 办法:出现该问题之后,把 DMA 填满、重新启动接收中断才能继续正常接收。目前按 Modbus 标准数据长度 256 字节限定 DMA 的长度。
  • 实测效果:在广播测量电文以 10 Hz 的频率发送的情况下,出现异常后可在 6 秒内自动重连。

4. 版本演进:三次提交里的三类问题

这个实验的提交记录一共只有三次,但每一次都能对上一类问题:

  • 新建——初始版本。
  • 第二次提交:1、优化了boot逻辑,解决了跳转前死机的问题。 2、主程序新增了看门狗,定时器从休眠状态唤醒后喂狗。 3、主程序原本用来计时用的tim1更换为了tim14。
  • 第三次提交:1、修改了软件版本与硬件版本的对应关系

“跳转前死机”是这类项目里最难定位的问题之一,因为现场没有调试器, 只看到“设备没起来”。最常见的成因有三类:跳转前没有关中断 (残留的中断在向量表已经被改掉的情况下取不到正确入口)、没有清理外设、 或者栈指针的设置顺序不对。复盘的时候我建议按 “关中断 → 反初始化外设 → 设置向量表偏移 → 重设 MSP → 取复位向量跳转” 这个顺序逐项核对,比凭记忆改代码靠谱得多。

“定时器从休眠状态唤醒后喂狗”这一条,是我对低功耗与看门狗关系理解最深的一次。 设备进低功耗休眠时 CPU 停住,普通的喂狗位置(比如主循环里的那一句)根本不会被执行, 而看门狗照样计数,结果就是设备一休眠就被复位。 所以喂狗点必须放在每次唤醒之后,而不是放在主循环里。 换句话说,喂狗的位置应该跟着“CPU 什么时候在跑”走,而不是跟着代码结构走。

“tim1 换成 tim14”属于定时器资源的重新分配。 通常是因为原来的定时器要被别的功能(比如 PWM 输出)占用, 或者它的中断优先级、唤醒能力不满足新的需求。这类改动本身不难, 难的是改完之后所有依赖这个时基的代码都要跟着核对一遍: 谁在用这个定时器做计时、谁的超时判断建立在它的中断上、休眠唤醒之后它还能不能正常工作。 这些依赖在代码里往往不写在一起,所以改时基这件事, 风险不在改动本身,而在那些看不见的引用。

第三次提交改的是软件版本与硬件版本的对应关系。 我理解这一改动要解决的是“主站拿到一台设备,怎么知道该给它发哪一版固件”: 硬件号标识这块板子是哪一版硬件,软件号标识它现在跑的是哪一版程序, 两者之间的对应关系如果散落在代码各处,主站就没有可靠的判断依据, 广播升级时更没办法确认“这份固件适不适合总线上这几种设备”。 把它集中理清楚之后,升级工具在下发之前就能先做一次版本判断—— 这是后面做版本兼容检查的前提。

九、这个实验的结论与还没做完的部分

做完这一轮,我自己的结论有五条:

  1. 8 KB RAM 才是这套方案的真正约束。它决定了不能缓冲整个固件、不能上 FreeRTOS 或完整协议栈,只能“收到一包、校验一包、写一包”。
  2. 广播式升级是可行的,但前提是把应答收敛。半双工总线不允许一堆从站同时说话,所以从站必须“按需回复”,不能照搬一问一答的节奏。
  3. 跳转只有几行代码,前提却一个都不能少。向量表偏移、中断使能、外设清理、栈指针设置顺序,缺任何一项,现象都是“跑起来了但哪里都不对”。
  4. 参数“校验通过”不等于“数据正确”。CRC 能挡住一整块数据被破坏,挡不住“某个成员从来没被赋过值”。
  5. 通信卡死要靠“强制复位状态机”解决,不能靠重试。这是我在这个项目里学到的最通用的一条。

还没做完的部分,我也如实写在这里:

  • 断点续传:传输状态现在放在 64 字节的 RAM 区里,掉电就丢。真正要做,得先把进度持久化,而这又绕回 Flash 擦写寿命那笔账——我还没想出一个足够省的方案,所以暂时没做。
  • AES 加密升级:目前固件在总线上是明文传输的,广播方式尤其把这一点暴露得更明显——总线上任何一台设备都能听到完整的固件内容。加密不只是“加一个解密函数”,还牵扯密钥怎么存、怎么换,以及 Bootloader 里能放多少密码学代码(16 KB 里要塞下这些并不宽裕)。
  • 失败回滚:现在没有双区备份,如果新固件写进去之后启动失败,只能靠 Bootloader 重新下发一次。而 Bootloader 自己不参与升级,所以这一块的风险我目前是靠“Bootloader 尽量小、尽量不再改”来规避的——这是一种规避,不是一种机制,我很清楚它不算解决方案。

目前真正做完的是基本的分包与校验:分包下发、逐包 CRC32、参数区的 CRC 校验与恢复默认值, 以及跳转。这些只在个人样机上验证过,样本量很小,也不代表任何行业标准或产品方案。 下一个想动的是进度持久化——因为断点续传、失败回滚这两件事,最后都卡在同一个问题上。

这套方案里关于分区规划与跳转的通用部分,我整理在学习笔记 《Bootloader 跳转与 OTA:分区规划、向量表偏移与容易踩的坑》 里;协议分层与“把自己的业务塞进一张地址表”的思路,可以参考 《Modbus RTU 从站实现笔记:寄存器表、CRC 与异常码》。

参考资料与说明

  • STM32G030 系列参考手册与数据手册中关于 Flash 擦写寿命、页擦除、系统初始化与中断向量表的章节,属公开文档。
  • ARM Cortex-M0+ 技术参考手册(Technical Reference Manual)中关于向量表与中断屏蔽寄存器的说明,属公开文档。
  • Modbus 应用协议规范(Modbus Application Protocol Specification)中关于主从模式与串行链路数据长度的定义,属公开标准;本文仅在“数据长度取 256 字节”这一处参照了它。
  • 文中芯片型号仅用于说明技术方案,与相关厂商无隶属或授权关系。
  • 示例代码为按个人理解重写的最小片段,不代表任何产品或交付代码。
  • 出于对个人项目实现细节的保护,本文只公开方法与思路;涉及核心实现的部分以步骤与字段说明代替代码,示例代码为按个人理解重写的通用片段。