【CAN通讯bootLoader】小白起手(1)
本文介绍了基于CAN总线的Bootloader开发心得,主要包含三个部分:前言、基础原理和架构设计。首先阐述了Bootloader的作用(远程升级、维护方便、降低成本)和选择CAN总线的原因(工业/汽车常用,可靠性强)。接着详细讲解了Bootloader的工作流程、存储区划分、启动跳转逻辑,以及固件校验(CRC)等关键技术。最后提出了模块化设计方案,包括通信模块、协议解析、Flash操作和状态机控
CAN通讯bootLoader开发心得
一、前言
1、为什么要做 Bootloader(远程升级、维护方便、降低成本)
2、为什么选择 CAN 总线作为通信接口(工业/汽车常用,可靠性强)
3、项目背景简述(MCU 平台、Flash/RAM 资源、应用需求)
MCU平台为:STM32L431C8T6
Flash:256Kb;RAM:64Kb
需求:实现上位机烧录bin文件
二、Bootloader 基础原理
1、Bootloader 的作用和工作流程
1.1 作用:
- BootLoader 本质上是一个“小程序”,它位于 MCU 的 Flash 起始地址,在主应用(App)启动前先运行
1.2 工作流程:
- 开机 → 先到 Bootloader → 看要不要升级
要升级 → 接收固件 → 写 Flash → 校验 → 成功 → 下次直接进 App
不升级 → 直接跳到 App
2、Boot 区与 App 区的划分方式
2.1为什么要划分区域?
- MCU 的 Flash 就像一块存储空间,从 0x08000000 开始排到最后,我们要在里面同时放:
| Bootloader 程序(小管家/引导员) | 应用程序 App(真正干活的程序) |
|---|
- 如果不提前规划,就会出现“两个程序写到一起”的混乱情况。所以必须在 Flash 里划出“地盘”:前面一段给 Boot,用来引导和升级;后面一大段给 App,用来跑业务逻辑。
2.2 启动流程:上电 → 判断是否进入 Boot → 跳转 App
-
上电启动(进入 Bootloader)
MCU 上电/复位后,CPU 会从 Flash 起始地址 (0x08000000) 读取中断向量表:先取出第 0 项设置栈顶地址 (MSP),再取出第 1 项设置程序计数器 (PC),于是程序从 Reset_Handler 开始执行。
这里存放的就是 Bootloader,所以第一条执行的指令一定来自 Boot 区。
Bootloader 会先做一些初始化工作,比如:
配置时钟
初始化 CAN/UART 等通信口
准备 RAM 缓冲区
通俗点理解:MCU 一开机,先去找门卫(Bootloader),问清楚情况,再决定进不进小区(App)。 -
判断是否进入 Boot 模式
Bootloader 的主要任务之一,就是决定接下来要干啥。常见的判断方式:
按键/跳线触发:比如按住某个按键上电,进入 Boot 模式。
标志位触发:上位机提前写了某个 Flash 标志或 RTC 寄存器,Bootloader 看到后进入升级。
App 校验失败:比如 CRC 错误,说明应用坏了,只能留在 Boot 模式等待重新下载。
超时机制:Bootloader 等待几秒钟,如果上位机没有发“升级请求”,就自动跳转到 App。
通俗点理解:门卫(Bootloader)会检查:要不要维修?房子是不是安全?如果有人要求维修(升级),它就停留;否则就直接放人进小区(App)。 -
跳转到应用程序 (App)
如果判断结果是不需要升级,Bootloader 就会跳到应用程序:
修改 中断向量表偏移量 (SCB->VTOR),指向 App 的起始地址。
读取 App 的栈顶地址和复位入口地址(在 App 向量表的前 8 个字节)。
把栈顶地址写到 MSP,把入口地址赋给 PC。
MCU 就会像刚上电一样,从 App 的 Reset_Handler 开始执行。
通俗点理解:门卫确认没问题后,就把钥匙交给房主(App),自己退到一边,从此之后一切交给房主打理。
中断向量表:事件 → 函数入口地址的“电话簿”。
位置:默认在 Flash 开头,App 也会有一份自己的。
为什么改 SCB->VTOR:告诉 CPU:“别再用 Bootloader 的电话簿了,现在要用 App 的电话簿”。
在 ARM Cortex-M 的规则里:
第 0 项(前 4 字节):是栈顶地址,告诉 CPU “一上来栈应该从哪开始”。
第 1 项(再 4 字节):是程序入口地址,也就是 App 的 Reset_Handler 地址
栈顶地址 (MSP):就像写文章前要有纸,告诉 CPU 临时数据放哪
程序入口地址 (PC):有了纸,还要知道第一句话从哪里写,这就是 Reset_Handler 的位置。
栈:像自动分配的“小桌子”,方便快捷,但只能放临时的、小的东西。
堆:像仓库,可以放大件、长期的东西,但要自己管理,否则就乱套。
上电/复位
↓
进入 Bootloader (Flash 起始地址)
↓
做初始化 (时钟/通信接口等)
↓
判断:
├── 需要升级 → 留在 Boot 模式 (接收固件、烧写、校验)
└── 不需要升级 → 跳转到 App (交出控制权)
三、CAN Bootloader 架构设计
1、 整体模块划分(通信模块、协议解析、Flash 操作、升级流程控制)
- 启动与判断模块
上电/复位后最先运行。
决定是进入升级模式,还是跳转到应用程序 (App)。
判断依据:按键/引脚状态、寄存器标志、App 校验结果、上位机握手信号。
我使用的是寄存器标记
👉 类比:门口保安,负责“开门检查” - 通讯模块
CAN 驱动层:负责收发 CAN 报文,和 HAL 底层打交道。
协议解析层:定义报文格式(ID、命令字、数据块),实现握手、应答、重传机制。
👉 类比:邮局 + 翻译官,上位机发的“包裹”得有人接收、拆分、理解。 - Flash 操作模块
擦除 App 区域 Flash。
分块写入新固件数据。
读出校验(CRC/Checksum))。
需要注意写 Flash 时的中断屏蔽和掉电保护。
👉 类比:工地施工队,负责拆旧楼(擦除)、盖新楼(写入)、验收(校验)。
这里提到读出校验,有的同学可能就要问了,接收校验或者是写入前校验不是更好吗?
其实,Flash 写入不一定 100%可靠
Flash 在写入时,可能出现:
写入失败(某些 bit 没烧进去)。
擦除不完全(旧数据残留)。
突然掉电,导致写到一半。
使数据在写入前是正确的(CRC 已经算过),也不能保证“烧到 Flash 里的结果”还正确。
所以必须在写入 完成后再从 Flash 读出来,重新算一遍 CRC,和原始数据比对。
-
升级控制模块(状态机)
负责整个升级流程的调度:握手 → 擦除 → 下载 → 校验 → 完成/失败处理。
常用状态:IDLE —— 空闲状态(等待命令或事件)
WAIT_CMD —— 等待命令(比如握手、开始升级指令)
ERASE —— 擦除 App 区域 Flash
RECV —— 接收数据(固件分块下载)
VERIFY —— 校验固件完整性(CRC/Checksum)
DONE —— 升级完成(准备跳转或标记成功)
ERROR —— 出错状态(传输失败、校验失败、写入错误等)
遇到错误要能回退或重新进入升级模式。
👉 类比:项目经理,安排施工队一步一步干活,出错了还能协调补救。 -
跳转模块
升级完成后,正确配置中断向量表 (SCB->VTOR)。
读取 App 的栈顶和入口地址。
设置 MSP 和 PC,跳转到 App。
👉 类比:交房手续,钥匙交到业主手里(App),Bootloader 退场。 -
辅助模块(可选)
日志/版本管理:记录升级时间、固件版本号。
容错机制:断电恢复、双 App 区设计。
👉 类比:物业服务,保证小区(系统)的安全和可靠性。
2、 Boot 与 App 的切换逻辑
2.1 上电后的第一步:先跑 Boot,初始化时钟、CAN/UART 等最基本的外设,然后进入判断流程
2.2 判断是否要进入 Boot 模式
- 外部条件:
按键/跳线触发:比如长按某个键,上电就进 Boot 模式。
上位机信号:比如 CAN 发来“进入升级”的命令。 - 内部条件:
Flash 里没有有效的 App(比如全是 0xFF)。
App 校验失败(CRC 错误、签名不对)。
RTC/备份寄存器里有特殊标志位。
👉 如果判断结果是“要升级”,Boot 就停在 Boot 模式;
👉 否则,Boot 就准备跳转到 App。 - 跳转到 App 的步骤
当决定进入 App 时,Boot 要做一系列“交接工作”:
修改中断向量表地址
SCB->VTOR = APP_START_ADDR;
告诉 CPU:以后所有中断都去 App 的向量表找,不要再跑回 Boot。
读取 App 向量表前 8 个字节
第 0 个字:栈顶地址 → 写入 MSP(主栈指针)。
第 1 个字:复位入口地址 → 写入 PC(程序计数器)。 - 执行跳转
CPU 设置好 MSP 和 PC 后,就会从 App 的 Reset_Handler 开始执行。
跳转是否需要复位?
不一定要复位。
在 Bootloader 里常见有两种做法:
方法 A:直接跳转(最常见)
Bootloader 读出 App 的栈顶地址和入口地址。
手动设置 MSP 和 PC。
直接跳转到 App 的 Reset_Handler。
👉 从外部看,和复位启动效果一样,但 MCU 并没有真正复位,只是“逻辑上像重新开机”。
👉 好处:快,不会丢失当前外设状态(除非你想手动清理)。
方法 B:触发一次软复位
Bootloader 设置一个“启动标志”(比如写入备份寄存器)。
然后调用 NVIC_SystemReset(),让 MCU 硬件复位。
复位后,MCU 上电流程走一遍,Bootloader 检查到标志,就直接跳到 App。
👉 好处:环境更干净,所有外设、寄存器都回到默认值。
👉 坏处:多经历一次复位,时间上略慢。
3、 固件完整性校验(CRC、校验和、签名)
- 为什么要做校验?
在 Bootloader 的升级过程中,固件数据要经过 上位机 → 通信链路 (CAN) → MCU → Flash 写入。
这个过程中可能会出问题:
传输错误:CAN 报文丢包、噪声干扰。
写入错误:Flash 擦写不完全、某些 bit 没写成功。
中途掉电:只写了一部分,导致固件不完整。
👉 如果不校验,MCU 可能会运行一份“坏掉的程序”,轻则死机,重则设备彻底失效 - 常见的校验方式
(1) 简单累加和 (Checksum)
把所有字节加起来(可能取低 16 位或 32 位)。
实现简单、速度快,但容易“撞值”。
👉类比:就像简单点人数,可能有人混进来你还没发现。
(2) CRC 校验 (Cyclic Redundancy Check)
工业界最常用的方法,比如 CRC16、CRC32。
能检测到绝大部分传输错误和乱序。
速度快,可以硬件加速(STM32 有 CRC 外设)。
👉类比:像超市收银台的条形码,扫描一下就知道货品对不对。 - 校验的时机
分包传输时的校验
每一小块数据传输后就做 CRC,确保通信链路没出错。
整包写入后的校验
固件全部写入 Flash 后,再从 Flash 读出来做一次 CRC,确保写入正确。
👉 两层校验结合,才能既保证 传输正确,又保证 存储正确。
4、超时/失败处理机制
- 为什么需要?
在固件升级过程中,可能出现:
上位机中途断电或软件崩溃。
CAN 通信丢包、延迟太大。
MCU 写 Flash 时遇到掉电或异常。
👉 如果没有超时/失败处理机制,Bootloader 可能会一直“卡死”在某个环节,设备就失去响应。 - 超时处理机制
常见做法:
握手超时:Boot 等待上位机发“开始升级”命令,若超过 N 秒无反应 → 自动跳到 App。
接收超时:Boot 接收固件数据块时,若长时间没收到新帧 → 认定失败,回到等待状态。
应答超时:上位机发完数据,若 Boot 没在规定时间内回复 ACK → 上位机自动重发。
👉类比:就像快递员送件,敲门没人应 → 等一会儿再走,不会一直死等。 - 失败处理机制
Bootloader 端:
校验失败:如果 CRC 错误,丢弃这块数据,返回 NACK,让上位机重发。
整包失败:下载完成但最终校验失败 → 标记固件无效,保持在 Boot 模式,不跳 App。
多次失败:可设置最大重试次数,超过就进入错误状态,等人工干预。
上位机端:
重发机制:若未收到 ACK,自动重发该数据块,最多尝试 N 次。
断点续传(可选):支持从失败的块继续传输,而不是重头再来。
四、协议设计与实现
1、CAN 报文格式定义(ID、数据字段分配)
- 为什么要定义格式
CAN 报文的 数据域只有 8 字节,但 Bootloader 升级要传输整份固件(几十 KB ~ 上百 KB)。
👉 必须设计一套报文格式,把指令、数据、校验都规范好,上位机和 MCU 才能对得上。 - 报文组成部分
一个完整的 CAN 报文一般分为两层:
CAN 标准帧头/扩展帧头
ID:标识消息类别(命令/响应/数据)。
DLC:数据长度(0~8 字节)。
数据域(最多 8 字节)
需要自定义格式,一般包含:
命令字:告诉 MCU 要干什么(握手、擦除、写入、校验…)。
块号/序号:数据属于哪一块,防止乱序。
数据内容:固件的 1~N 个字节。
CRC/校验字段:保证这一帧或一块数据的正确性(这里我采用的是块CRC验证)。
2、分包/重组策略(固件分块、帧序号、ACK/NACK 机制)
- 为什么要分包
CAN 一帧最大 8 字节,其中还要占用命令字、块号等控制信息,真正能放的固件数据更少(通常 6~7 字节)。
如果固件有 64 KB,就得拆成上万帧。
👉 所以需要 分包传输 + 接收端重组。
分包的基本思路 - 按块划分
固件先按固定大小分成“块”(Block),比如 256 字节/512 字节。
每块单独做 CRC 校验。
块内再分帧
一块再切成多个 CAN 帧,每帧 6~7 字节数据。
每帧带上序号,避免乱序丢失。
接收端重组
Bootloader 把帧按序号拼接回原始块。
收齐后做 CRC 校验,通过则写入 Flash。 - ACK / NACK 机制
Bootloader 收到一整块并校验成功 → 回复 ACK(确认正确)。
如果校验失败 → 回复 NACK,上位机重发该块。
好处:降低重传代价(只重发坏掉的块,不用重头来)。
五、存储空间划分与内存管理
1、 Flash 分区设计(Boot 区、App 区、参数区)
-
为什么要分区
STM32 的 Flash 是一整块连续的存储空间,但 Bootloader 和 App 都要用,如果不提前规划好,就会互相覆盖、冲突。
👉 所以必须 划分区域:
Boot 区:存放 Bootloader 程序,保证稳定、不可覆盖。
App 区:存放应用程序,可以反复擦写升级。
参数区(可选):存放标志位、版本号、升级状态等小数据。 -
基本划分方式
(1) Boot 区
地址:Flash 起始地址(一般是 0x08000000)。
大小:常见预留 16 KB / 32 KB(要根据 Bootloader 功能复杂度决定)。
特点:一旦写好,几乎不再改动。
(2) App 区
地址:紧接 Boot 区之后(比如 0x08008000)。
大小:占据 Flash 的大部分空间(几十 KB~几百 KB)。
特点:可以擦写升级,但不能覆盖 Boot 区。
(3) 参数/信息区(可选)
地址:Flash 的最后几页。
大小:1 页 ~ 多页(根据需要,常见 2 KB~8 KB)。
存内容:固件版本号、有效标志、升级状态、设备参数。
特点:App 和 Boot 共用,必须提前设计。
2、 如何选择 Boot 区大小(结合编译结果和扩展预留)
- 看功能复杂度
- 结合编译结果
- 考虑对齐原则
STM32 的 Flash 擦除是按“页”进行的(比如 2 KB 一页)。
Boot 区大小最好对齐到页边界,比如 16 KB、32 KB、64 KB。
这样擦除 App 区时,不会误伤 Boot 区。
3、RAM 占用优化(缓冲区、分段校验)
- Bootloader 的 Flash 占用往往不是问题(几百KB 空间很充裕),真正需要小心的是 RAM。因为 MCU 的 RAM 容量普遍比 Flash 小得多(比如 STM32L431 只有 64 KB RAM),Boot 和 App 还要共享。
- 为什么要优化 RAM
Bootloader 需要 RAM 做缓存:
接收 CAN/UART 的数据包
存放一块固件数据(Block)
做 CRC 校验
如果缓存区设计太大,可能导致 挤占 App 运行内存,甚至写不下。
👉 所以 Boot 的 RAM 占用要 尽量小、够用即可。 - 优化思路
合理选择缓冲区大小
固件接收缓冲区(Block Buffer)不要一次性开太大。
常见做法:256B、512B 一块 → 在 RAM 里放一个块就够。
Boot 收到一块 → 校验 → 写入 Flash → 清理缓存。
不需要整个固件都放在 RAM 里。
👉 错误思路:一次性开 64KB 缓存。
👉 正确思路:只要能装下一块数据就行。 - 避免全局变量过多
Bootloader 逻辑简单,不需要很多全局状态。
用局部变量 + 静态变量替代大数组。
编译时可以用 Map 文件确认 .bss 和 .data 的大小。
更多推荐



所有评论(0)