RS485 总线上的广播式 Bootloader 与 OTA
这个实验做的是:让挂在 RS485 半双工总线上的多台设备,通过一套自己设计的主从协议, 从同一个主站接收同一份固件——既支持指定站号的一对一升级,也支持一次下发给多台设备的广播升级。 之所以没有用现成的 Xmodem / Ymodem,一是它们只能点对点,二是接收端需要的缓冲能力, 这颗只有 8 KB RAM 的芯片给不起。最后的结论是:带站号的分包协议、CRC32 校验、 参数区的 CRC 与恢复默认值、以及跳转这几件事都在个人样机上跑通了; 断点续传、加密升级和失败回滚还没有做,我把原因也写在最后一节里。
一、需求:为什么是“一对多广播”而不是“一对一升级”
需求本身很简单:需要对总线上的多台设备做固件更新。麻烦的地方在于,这些设备挂在 RS485 半双工总线上,而且我要同时支持两种方式——1 对 1(只升级其中某一台) 和 1 对多(广播,一次把所有设备都更新掉)。
为什么一定要广播?因为同一批设备烧的往往是同一份固件。如果只能一台一台升级, 主站就要把完全相同的流程重复 N 遍:等待设备、进入升级模式、分包下发、逐包确认、收尾跳转。 总线上设备越多,这个重复越难以接受,而且中间任何一台失败都要单独重来一次, 整体时间会随设备数量线性增长。
但广播并不是“把目标地址去掉”这么简单。半双工总线上任何时刻只能有一个设备在说话: 如果广播的时候每台从站都按点对点的习惯逐包回复,总线上立刻会出现多台设备同时抢总线的情况, 结果是所有回复互相叠加、谁也读不出来,比不回复还糟。所以这套协议里从站的处理原则是 按需回复,而不是“收到什么就回什么”。 这也是我没有照搬一问一答节奏的原因——我在 《Modbus RTU 从站实现笔记:寄存器表、CRC 与异常码》 里写过,一问一答在点对点轮询里非常好用,但放到广播场景下不成立。
另一个约束来自 RS485 本身:收发共用一对差分线,收发方向的切换在硬件上已经固化, 从站根本没有主动开口的机会。这一条决定了协议只能是主从式: 主机发送带站号的报文,从站解析站号,与自身 ID 一致才处理,并按需决定是否回复。 站号在这里承担了两个作用:一对一升级时它是收件人地址,广播时它决定哪些设备参与, 同时也让从站在“我要不要回这一条”上有判断依据。
二、硬件与资源约束(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:分区规划、向量表偏移与容易踩的坑》 里完整讲过一遍取舍过程,这里只贴最终结果,并补上“每一段在升级时扮演什么角色”:
| 区域 | 地址范围 | 大小 | 升级时的角色 |
|---|---|---|---|
| Bootloader | 0x08000000 ~ 0x08003FFF | 16 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 字节 | 长度由“长度”字段给出 |
| CRC32 | 4 字节 | 对约定范围内的电文算出的校验值,闲置字节被明确排除在外 |
| 帧尾 | 2 字节 | 固定两字节,与帧头成对 |
关于“闲置字节不参与 CRC 校验”这个设计,我想单独说两句。 好处是它留了一个扩展位:将来想在这里放包序号、放标志位,都不会影响已有的 CRC 算法, 两端升级的节奏可以错开——主站先开始填,从站暂时忽略,也不会因为校验失败而互相不认。 坏处是它和“整帧参与校验”的直觉冲突,实现的时候太容易在某一端顺手把整个缓冲区都算进去。 只要两端对“算到哪一位”的理解差一个字节,现象就是“我算的 CRC 和你算的不一样”, 而且两边各自自测都是通过的。
目前支持的功能有:读取设备存储更新状态空间内的数据、读取设备当前状态、写入固件数据包、 写入硬件号、写入软件号,后续可视情况增加。 这里我不逐条写出功能码的具体编码值:编码是两端约定的常量, 记错一个会造成很隐蔽的错位;而且这份记录真正的价值在“每个功能做什么、什么时候用”, 不在于那几个字节的取值。
写入硬件号和写入软件号这两条命令,报文结构完全一样,只有功能码不同; 回复上也不一样:写入硬件号成功时,从站回复与请求完全相同的报文; 写入软件号成功时同样回原电文,不成功时回复当前软件号—— 这样主站一次交互就能知道设备现在跑的是哪一版,不用再单独发一条查询, 在广播升级之后逐台核对版本时省了不少事。
报文本身我不贴出来了。一条完整报文里带着站号、功能码和校验值, 原样贴出来就等于给了一份可以直接复用的样例;这里改成按字段说明取值含义, 真要动手的时候,照着上面那行帧格式和下面这张表自己拼即可:
| 字段 | 取值含义 |
|---|---|
| 帧头(固定两字节) | 取值见上面的帧格式,作用是让从站能对齐帧起始 |
| 长度 | 这一帧数据段的字节数;写硬件号 / 写软件号这类命令的数据段很短,长度字段的取值也随之很小 |
| 目标站号 | 一对一升级时填目标设备的站号;广播升级时填约定的广播站号,总线上所有从站都参与处理 |
| 功能码 | 区分“读更新状态 / 读设备状态 / 写固件数据包 / 写硬件号 / 写软件号”,具体取值按两端约定 |
| 闲置字节 | 预留扩展位,不参与 CRC32 校验;两端必须先对这个例外达成一致,否则一对接就失败 |
| 数据 | 写硬件号时是硬件号本身,写软件号时是软件号,写固件包时是这一包的固件内容 |
| CRC32 | 对约定范围内的电文算出的校验值,闲置字节被明确排除在外,随帧一起下发 |
| 帧尾(固定两字节) | 与帧头成对,用于确认一帧的结束 |
这两个号的写入时机也不一样:硬件号是收到后即写入 Flash;软件号是在跳入 APP 之前写入 Flash 的。 我理解这么设计的原因是:软件号代表“当前真正在运行的那一版”,由 Bootloader 在交棒之前写下去, 才不会出现“APP 还没跑起来、软件号已经改了”这种中间状态。 如果反过来让 APP 自己去写,一旦新固件启动失败,记录的版本号就是错的, 远程排查会被这个数字带偏。
分包上暂定每包 2 KB,由主机把固件切分成包依次下发, 单包通信内容长度不超过 2060 字节。这两个数字加上前面算过的 13 字节固定开销, 有一个必须一起核对的细节:能塞进一帧的数据段最多是 2060 − 13 = 2047 字节, 比 2048 字节的数据包缓冲区少 1 个字节。也就是说缓冲区刚好不会溢出, 但余量只有 1 字节,等于没有余量。如果重做一遍, 我会把“单包长度上限”和“缓冲区大小”一次性对齐, 而不是让两个数字各留各的余量——这种“差一点就溢出”的设计,是靠运气在跑。
五、升级流程:分包、写入、校验、跳转
把前面这些拼起来,一次升级的流程是这样的:
- 主站把固件按 2 KB 切分成包,带站号依次下发;一对一升级时站号写目标设备,广播时所有从站处理同一份数据。
- 从站收到一帧,先校验帧长、站号与 CRC32;通过之后把数据段写进“数据包写入区”对应的偏移,不通过就整帧丢弃,不做任何写入。
- 传输状态存储区(64 字节 RAM)记录更新包大小、分包数、已更新包号,用来判断当前进度。
- 全部包写完之后,主站做一次收尾确认,从站回复状态。
- 校验通过:关闭中断 → 写入软件号 → 跳转到 APP。
这个流程里有三个我认为最容易出问题的地方。 第一个是写之前必须先擦:Flash 擦除的最小单位是页,写入只能把 1 变成 0, 所以覆盖写之前一定要把目标页擦干净,否则写进去的数据会变成新旧值相与的结果, 而且读回来还能过 CRC 的概率极低,表现出来就是“升级完启动不了”。 第二个是跳转前必须写完软件号,而且要在关中断之后、跳转之前完成, 顺序反了就会出现“设备已经在跑新固件,但主站读到的还是旧版本号”。 第三个是传输状态在 RAM 里,掉电就没了。
第三点直接解释了为什么“断点续传”不是加几行代码就能有的功能:要续传, 先得知道“上次传到哪儿了”,这个信息必须持久化;一旦要持久化, 就撞上 Flash 擦写寿命那笔账——一颗固件几十包,如果每包都写一次进度, 就是几十次擦写,多升级几次这一页就废了。 所以断点续传真正要先解决的是“进度怎么低成本地持久化”,而不是“怎么把包号记住”。 这笔账我在笔记页的第六节( Flash 寿命:一个必须提前算的账)里算过一遍。
流程里还有一个容易被忽略的角色:看门狗。主程序里加了看门狗, 用来兜住“通信卡死”这类异常——测试用例里就有一条“发送不完整帧超过 200 ms 触发看门狗复位”。 它的存在意味着升级流程里每一步的耗时都要心里有数, 不能在某个等待循环里安静地停太久,否则升级到一半设备自己复位了, 而现场只会看到“升级失败”这一个笼统的结论。
六、参数存储与 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 输出)占用, 或者它的中断优先级、唤醒能力不满足新的需求。这类改动本身不难, 难的是改完之后所有依赖这个时基的代码都要跟着核对一遍: 谁在用这个定时器做计时、谁的超时判断建立在它的中断上、休眠唤醒之后它还能不能正常工作。 这些依赖在代码里往往不写在一起,所以改时基这件事, 风险不在改动本身,而在那些看不见的引用。
第三次提交改的是软件版本与硬件版本的对应关系。 我理解这一改动要解决的是“主站拿到一台设备,怎么知道该给它发哪一版固件”: 硬件号标识这块板子是哪一版硬件,软件号标识它现在跑的是哪一版程序, 两者之间的对应关系如果散落在代码各处,主站就没有可靠的判断依据, 广播升级时更没办法确认“这份固件适不适合总线上这几种设备”。 把它集中理清楚之后,升级工具在下发之前就能先做一次版本判断—— 这是后面做版本兼容检查的前提。
九、这个实验的结论与还没做完的部分
做完这一轮,我自己的结论有五条:
- 8 KB RAM 才是这套方案的真正约束。它决定了不能缓冲整个固件、不能上 FreeRTOS 或完整协议栈,只能“收到一包、校验一包、写一包”。
- 广播式升级是可行的,但前提是把应答收敛。半双工总线不允许一堆从站同时说话,所以从站必须“按需回复”,不能照搬一问一答的节奏。
- 跳转只有几行代码,前提却一个都不能少。向量表偏移、中断使能、外设清理、栈指针设置顺序,缺任何一项,现象都是“跑起来了但哪里都不对”。
- 参数“校验通过”不等于“数据正确”。CRC 能挡住一整块数据被破坏,挡不住“某个成员从来没被赋过值”。
- 通信卡死要靠“强制复位状态机”解决,不能靠重试。这是我在这个项目里学到的最通用的一条。
还没做完的部分,我也如实写在这里:
- 断点续传:传输状态现在放在 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 字节”这一处参照了它。
- 文中芯片型号仅用于说明技术方案,与相关厂商无隶属或授权关系。
- 示例代码为按个人理解重写的最小片段,不代表任何产品或交付代码。
- 出于对个人项目实现细节的保护,本文只公开方法与思路;涉及核心实现的部分以步骤与字段说明代替代码,示例代码为按个人理解重写的通用片段。