拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于CAN总线的Bootloader设计与实现:从协议到跳转全解析

简介基于CAN总线的Bootloader完整工程资源适合有嵌入式开发基础的工程师或汽车电子方向学习者用来解决通过CAN总线对节点进行程序升级、在线维护的实际需求。工程实现了节点自动识别与地址分配、在线状态检测与删除、指定节点程序升级、命令控制程序执行等完整功能配套上位机基于纬图Ginkgo USB-CAN适配器开发可直接配合硬件完成联调。压缩包共323个文件约14.59MB以C源码与H头文件为主还包含STM32工程配置文件、底层驱动库、编译过程生成的O/CRF/LST/MAP中间文件以及可直接烧录的BIN/HEX固件另有上位机运行所需的DLL/EXE覆盖从驱动到应用的完整开发链。附带少量文档与界面资源可辅助理解节点分配与升级流程。已有1609人学习或下载适合需要快速搭建CAN Bootloader方案、或研究CAN总线协议应用的工程师参考借鉴。 搞嵌入式开发的尤其是跟车和工业设备打交道的迟早要面对一个扎心的问题产品已经批量出货了现场固件有bug或者要升级功能怎么办拆壳子接J-Link累死也刷不了几台。所以基于CAN总线的bootloader就成了一个绕不开的实用方案。我最早接触CAN bootloader是因为一个车载控制器的项目供应商给的升级工具又贵又封闭后来索性自己把上位机、下位机、协议全包了。这一趟走下来踩了不少坑也淌出了一些经验。今天这篇就把整个方案从设计到落地掰开揉碎讲清楚从协议帧怎么定、Flash怎么规划、跳转怎么处理到上位机用什么写、实际刷写时那些能把你整崩溃的玄学问题一次性说透。1. 为什么非要用CAN来做bootloader先聊聊最简单的问题刷写方式那么多UART、SPI、USB不都行吗为什么非要折腾CANUART确实简单但一般也就半米一米内玩玩工业现场谁给你拉根串口线USB适合维护人员用电脑操作但现场检修未必有合适的笔记本电脑而且工业设备的接口环境往往很恶劣。CAN总线是双线差分传输抗干扰能力特别强传输距离在低速下能到千米级又是现成的车载/工业现场总线利用原有线束就能刷写不需要额外的通信接口。尤其对于ECU这类节点来说CAN总线本来就是它跟外界打交道的唯一通道。网关也好、诊断仪也好全都是通过CAN总线跟ECU交互的。bootloader挂在CAN总线上等于顺着现有的神经系统做远程手术不用开膛破肚这是它最核心的价值。再往深了说CAN本身是一种多主通信总线这意味着我可以用一个总线上的任意节点刷写工具、网关、甚至另一个ECU来触发刷写不需要物理上靠近目标设备。对于装车之后藏在仪表台里面的ECU这是唯一可行的在线升级手段。2. 整体方案设计与分区规划2.1 三个组成部分一个完整的CAN bootloader方案从架构上看是三段式的下位机端芯片内部执行升级的固件即bootloader本身应用固件正常运行的主程序也就是App上位机端通过CAN接口卡发指令给下位机的PC软件从用户角度看他们平时接触的是上位机从开发角度看真正决定成败的却是下位机的分区设计和跳转逻辑以及上下位机之间的协议制定。这套架构跟串口bootloader甚至网络bootloader的架构本质上是相同的只是物理层和应用层协议不同。把这三块先理清楚后面做起来才不会乱套。2.2 Flash分区给Bootloader和App都留好后路Flash怎么分是整个方案里最重要、也最容易被新手忽略的一步。假设用的是STM32F103C8T6它只有64KB Flash实际部分型号有128KB但C8T6标称64KB预算非常紧张分区必须精打细算。我常用的分配方式是区域起始地址大小说明Bootloader区0x0800000016KB存放启动引导代码App区0x0800400046KB应用固件标志信息区0x0800F0002KB存放刷写标志、版本信息、校验值等Option Bytes--配置读保护级别为什么Bootloader要16KB因为一个带CAN驱动、Flash驱动、简单的校验算法和命令解析的bootloader用标准外设库优化编译之后基本在10KB上下留16KB比较稳妥给后续增加协议命令也留有余地。App从0x08004000开始这个地址不是随便定的它是Bootloader区结束地址的下一页。对于F103来说Flash页大小是1KB所以边界必须按1KB对齐。如果Flash页大小是2KB的单片机很多F2/F4系列是2KB或更大那分区边界就要按相应页大小对齐否则没法做页擦除。16KB Bootloader加46KB App为什么不是48KB App因为在F103C8T6上最后2KB我要留给标志信息区用来存升级状态和版本号这个区域在App运行时不会去碰只有Bootloader会读写它。这样做的好处是App在运行过程中也可以把我需要升级这个愿望写进标志区等复位后Bootloader读到这个标志就知道要进入刷写模式而不是直接跳App。2.3 跳转逻辑Boot和App之间的过门技术Bootloader和App共存在一颗芯片里每次上电先执行Bootloader然后由Bootloader决定是留在原地等待刷写还是直接跳转到App。这个过门技术是很多新手翻车重灾区。跳转的本质就两步改栈指针改PC指针。但直接写两句代码就能跳成功吗不行里面有几处细节必须要处理干净。typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t jump_addr; pFunction jump_func; // 1. 检查栈顶值是否合法RAM地址范围 if (((*(volatile uint32_t *)app_addr) 0x2FFF8000) ! 0x20000000) { return; // 栈顶不合法说明App区没有有效程序 } // 2. 取出复位中断向量地址 jump_addr *(volatile uint32_t *)(app_addr 4); jump_func (pFunction)jump_addr; // 3. 跳转前必须关闭全局中断 __disable_irq(); // 4. 重新设置栈顶指针 __set_MSP(*(volatile uint32_t *)app_addr); // 5. 跳转 jump_func(); }这个函数里每一步都有讲究。第1步检查栈顶值是否落在RAM地址范围内是为了防止Flash里根本没有有效App时跳到随机地址造成硬件错误。第3步关闭中断很关键跳转前要确认所有外设中断都已经关闭或者不再触发否则中断服务函数在App还没完全初始化的时候触发会跑飞到Bootloader的中断向量表里去。这里还必须提一个所有做bootloader的人都会遇到的坑中断向量表重映射。App编译的时候中断向量表默认放在0x08000000但现在它运行在0x08004000如果不在App代码里重新设置中断向量表的位置任何一个中断发生都会把PC拉回Bootloader里的中断向量表表现为程序跑飞进HardFault或者中断完全无效。在STM32上这通常是在App初始化最开始调用的// 必须在SystemInit()之前或紧接着设置 SCB-VTOR 0x08004000;如果你用的是带BootROM的高端芯片比如S32K、TC275跳转机制不完全一样但核心思路是相通的向量表跟着App走中断环境要清理干净。3. 协议设计帧格式、命令集与握手流程3.1 底层依赖CAN报文和DBC中的地址CAN总线上的数据传输单位是报文帧标准帧有11位ID扩展帧有29位ID。设计bootloader刷写协议的时候要先确定项目用的是标准帧还是扩展帧以及总线上的其他节点会不会干扰刷写过程。对于车载网络CAN ID规划通常要参考DBC文件。比如某个ECU的应用报文ID是0x1A0刷写相关的诊断报文可能占0x7E0到0x7E7这段。自主设计的bootloader协议建议避开这些已经被占用的ID或者干脆在刷写模式下让App停止发送应用报文让出总线带宽。我一般会为bootloader预留几个专用ID报文方向CAN ID用途上位机 → EC U0x700命令帧握手、擦除、跳转等上位机 → ECU0x701数据帧固件内容ECU → 上位机0x708应答帧状态、错误码这里ID的规划要根据项目实际情况调整核心思想是命令帧和数据帧分离应答帧单独一个ID让上位机可以从ID上立刻区分这一帧是ECU对哪个请求的响应。3.2 应用层协议格式应用层协议也就是在CAN报文数据场里承载的协议需要自己定义。我推荐一种简单可靠的格式字节含义说明Byte0帧类型/命令0x01握手、0x02擦除、0x03下载、0x04跳转、0x05复位、0x06查版本Byte1序列号用于应对帧乱序从0递增回0后再加1Byte2数据长度本帧携带的有效数据长度0~8Byte3~Byte7数据数据段最多5字节有些设计会把Byte1空出来用作帧计数数据段放8字节对于STM32和绝大多数单片机来说数据场最多8字节所以一个CAN数据帧能携带的固件数据最多是8字节还是5字节取决于你留多少字节给协议头。我上面这个格式每帧只能带5字节固件数据效率其实偏低。实际项目里用的更高效的设计是双帧模式首帧带起始地址和总长度后续连续帧只带序列号和数据每帧8字节这样单帧有效载荷就变成8字节了。这里给出一个在博文中详细解释的连续帧格式第一帧起始帧Byte0Byte1-2Byte3-6Byte70x00起始帧标记数据总长度起始地址32位保留/段序号后续帧连续帧Byte0Byte1-70x01连续帧标记最多7字节固件数据这样设计的好处是单帧有效数据从5字节提升到7字节。对于128KB的固件如果每帧7字节需要大约18725帧以标准CAN 500kbps算光传数据本身约8秒加上擦除、编程和校验的耗时整个升级过程能控制在30秒内。如果每帧5字节帧数会变成26215帧虽然只差不到3秒但帧数增多也意味着出错概率变大。提示这里算的只是纯数据时间理论上每一次发送后还要等应答如果是逐帧应答模式实际耗时还要加上应答等待时间。工程上真正好用的方案是多帧发送窗口式应答比如连续发32帧再统一收一次应答效率能提升好几个数量级。3.3 一次完整刷写流程现在把一次完整的刷写流程串起来上位机发送进入Bootloader命令比如发送0x01ECU收到命令后复位进入Bootloader或者Bootloader在App运行过程中被标志位触发Bootloader发送就绪应答附带Bootloader版本号和App版本号上位机发送擦除命令Bootloader按地址范围擦除App区Flash上位机发送起始帧告知固件长度和起始地址上位机连续发送数据帧Bootloader收到后逐帧写入Flash全部数据发送完毕后上位机发送校验命令Bootloader对App区做CRC32校验上位机发送跳转命令Bootloader跳转到App执行整个流程里擦除和编程是最耗时的两步芯片内部Flash擦写时间通常在10~40ms不等设计命令应答超时要留够余量。3.4 校验不校验的bootloader都是在耍流氓固件下载到一半总线上出现了位错误怎么办不校验就跳转轻则App启动失败重则变砖。所以一套可靠的bootloader必须包含CRC校验而且要做到双重校验。第一重在数据帧级别做校验。每个数据帧的CRC可以用8位CRC或者更简单可靠的方案是每个帧的CRC16——虽然会占用数据场字节但对短帧来说足够。第二重在整个文件级别做校验。上位机计算整个固件的CRC32放在起始帧或专门的校验帧里Bootloader接收完毕后对写入的Flash做同样的CRC32比对一致才允许跳转。STM32F103上做CRC32要注意它的硬件CRC外设使用的是CRC-32/MPEG-2标准而上位机常用的CRC32是IEEE标准两者结果完全不同。如果上位机用zlib的crc32函数下位机用硬件CRC外设算出来的校验值对不上。注意STM32F1系列没有内置CRC32单元F1的CRC外设其实是CRC16ITU-T。F2/F4/F7系列才有硬件CRC32。F103上做CRC32校验只能用软件实现常用的查表法速度也不慢。查表法实现CRC32的代码量不大网上有现成实现性能足够处理128KB固件。4. 上位机与下位机实现要点4.1 上位机选择LabVIEW、Python还是.NET上位机最常用的几种方案是LabVIEW、Python配合python-can库、C#配合SocketCAN或者PCAN库。对于汽车电子行业的工程师LabVIEW熟练度一般比较高用它开发bootloader上位机的好处是界面搭建快跟NI的CAN卡如NI-XNET集成方便。但LabVIEW做协议解析、文件读取、CRC计算这些事时代码阅读性比较差。我实际项目里用了Python原因有三个python-can库对主流接口卡PCAN、Kvaser、CANable支持都很成熟协议逻辑用Python写起来直观很多而且后台还能用线程同时处理UI刷新和CAN接收。核心逻辑其实并不复杂一个简单的发送流程是这样的import can import time import zlib bus can.interface.Bus(channelPCAN_USBBUS1, interfacepcan, bitrate500000) # 发送握手命令 handshake_msg can.Message( arbitration_id0x700, data[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], is_extended_idFalse ) bus.send(handshake_msg) # 等待应答 response bus.recv(timeout1.0) if response.arbitration_id 0x708 and response.data[0] 0x01: print(握手成功Bootloader版本:, response.data[2])对于不熟悉Python的工程师LabVIEW里同样可以实现核心区别只在界面和语法层面。4.2 下位机Bootloader主流程下位机端我提供一个简化的主流程伪代码方便理解结构void bootloader_main(void) { // 初始化时钟、CAN外设 can_init(500000); // 读取标志位 update_flag read_update_flag(); // 如果没有升级请求直接跳转App if (update_flag ! 0xAA55) { jump_to_app(APP_ADDR); } while (1) { // 等待上位机命令 if (can_receive(msg)) { switch (msg.data[0]) { case CMD_HANDSHAKE: send_response(...); break; case CMD_ERASE: flash_erase(APP_ADDR, APP_SIZE); send_response(...); break; case CMD_DATA: flash_write(addr, data, len); send_response(...); break; case CMD_VERIFY: crc flash_crc32(APP_ADDR, APP_SIZE); send_crc(crc); break; case CMD_JUMP: clear_update_flag(); jump_to_app(APP_ADDR); break; } } // 喂看门狗 iwdg_refresh(); } }有一个细节clear_update_flag()必须在跳转之前执行否则下次复位Bootloader又会认为有升级请求反复进入刷写模式。这算是我见过最多人犯的一个低级错误。4.3 Flash驱动与编程时序Flash写入有几个硬性要求第一写之前必须先擦除F103按页擦除1KB一页不能只擦一个字节。如果要写的数据跨越两页两页都要先擦干净。第二Flash写入是按半字16位对齐的如果你的数据段是任意字节长度最后一帧不满足16位对齐要做填充。我一般填充0xFF因为擦除后的Flash本来就是0xFF填充0xFF不会引入无效数据。第三Flash编程期间不能有中断或者至少要保证中断服务函数不会访问Flash。大部分单片机在写Flash时CPU会暂停执行这个暂停会拉长中断响应时间如果中断里有时序敏感的操作比如CAN报文接收就可能丢帧。所以严谨的bootloader在写Flash时要先把CAN接收到的数据缓存到RAM写完一页Flash后再统一切换。我在实际项目中是这样处理的上位机发过来的数据先放进一个512字节的RAM缓冲区凑满一页如果Flash页大小是1KB就凑1KB然后一次性擦写一页。这样既满足了Flash按页操作的约束又不会因为频繁擦写导致传输速度太慢。5. 常见问题与故障排查实录5.1 跳转后跑飞或者HardFault这个问题排在坑榜第一名。跳转进去后App完全没反应或者进HardFault排查方向按照顺序来第一步确认App里的VTOR有没有设置。没设VTOR的人在F103上占比极高表现是跳转进去后刚使能中断就死了。第二步确认App编译时的起始地址是不是0x08004000而不是默认的0x08000000。很多人改了下位机跳转地址却忘了改工程的Linker脚本导致App里所有绝对地址都指向Bootloader区域。第三步确认跳转前有没有把所有外设中断都关掉。特别是如果App用到了DMA、定时器、CAN这些外设中断在Bootloader阶段可能处于开启状态跳转后中断来了但App的中断服务函数还没准备好直接HardFault。5.2 刷写速度太慢几十KB固件要刷一分钟如果发现刷写耗时远超预期先别急着怀疑CAN波特率把流程拆开看。先看一眼是逐帧应答模式还是窗口应答模式。逐帧应答时每个CAN帧发出后要等ECU回ACK再发下一帧一个周期大概2~3ms一帧有效数据7字节1KB固件就要发147帧耗时300ms以上1MB固件就要300秒。这还没算Flash写入的时间。优化手段是改成窗口应答上位机连续发32帧或64帧下位机收到后统一回一个ACK或者上位机一边发一边收收够了再继续。窗口大小要配合CAN控制器接收FIFO深度来定F103的bxCAN每个FIFO深度是3个邮箱窗口开的太大容易丢帧。另一个优化点是用中断接收而不是轮询接收。轮询状态下如果恰好碰到Flash擦写期间CPU被锁住CAN报文就会溢出丢失。F103的bxCAN在溢出时会丢弃新报文如果丢的是数据帧上位机还不知道只能超时重传一下就把速度拖垮了。5.3 CAN总线上的干扰导致刷写失败工业现场刷写时偶尔会出现总线没有完全断开、电机在运行、变频器在附近启动一次性刷写失败的案例。CAN总线虽然是差分信号抗干扰能力不错但线束过长或者屏蔽层接地不良的时候共模干扰能把报文直接打废。对策有几个层面第一是物理层CANH和CANL之间加终端电阻通常是120欧姆并且要确认总线上只有两端有终端电阻不要每个节点都加了120欧姆那样总线等效电阻过小信号幅值会被压低。第二是协议层CAN本身有CRC校验和错误帧机制但对于bootloader这种一次写错就可能变砖的场景超时重传和帧序号校验是必须的。上位机发送的每个帧都有递增序号下位机发现序号跳变就知道丢帧了回应一个NAK上位机从这个序号重新发送。第三是软件层擦除和写入Flash的关键步骤如果CAN控制器因为总线错误进入Bus-Off状态需要做恢复处理否则下位机一直不回包上位机超时后要么重发要么放弃用户体验很差。5.4 最容易忽略的看门狗问题很多人做bootloader的时候容易忘记看门狗。App里开了独立看门狗IWDG跳转之前如果Bootloader没有把IWDG关掉App刚启动还没喂狗就被复位了表现为刷写成功后设备反复重启。这个问题的根源在于IWDG一旦启动就不能被软件停止。Bootloader跳转前如果不清除IWDG跳转后的App必须在超时时间内完成初始化和喂狗。严谨的做法是Bootloader里跑主循环时也要喂狗而且跳转前把即将跳转这个状态写到RAM标志里App启动后如果发现这个标志就立刻喂一次狗再做其他初始化。5.5 擦除后掉电变砖了怎么办升级过程中掉电是所有bootloader方案里最可怕的事故。如果正在擦除App区突然断电Flash里可能一半是旧固件、一半是空白的0xFF甚至引导区都受到破坏。产品直接变砖。应对方案有两种主流思路。一种是双备份方案A/B分区。Flash里放两份App分别是A区和B区Bootloader启动时检查A的CRC如果A坏了就从B启动。刷写时先刷B成功后再切到B启动下次升级刷A交替进行。这个方案可靠性最高但Flash占用直接翻倍对于F103这种64KB的芯片不够用更适合256KB以上的芯片。这也是现在很多整车厂强制要求的设计。另一种是失败回滚方案。Bootloader在升级前先把App区的内容复制到备份区升级过程中任何一步失败Bootloader都用备份区恢复旧固件。这个方案兼容小Flash芯片但需要额外的时间和Flash空间开销。对于F103C8T6这种小容量片还有一种更极端的做法不放备份区但设计一个出厂Bootloader模式——在Bootloader里再烧一个最简的启动固件即使App区全空Bootloader也能通过CAN总线重新刷写。Bootloader本身只有16KB出问题的概率远小于App。只要Bootloader自己不坏哪怕固件刷了一半断电了重新上电还是能进Bootloader重新刷一遍完整固件就行。5.6 CAN总线上同时挂多台设备刷写会不会冲突做整车或多节点项目时会遇到这个问题总线上挂了好几台ECU刷其中一台的时候其他ECU的报文会不会干扰答案是会有冲突。刷写的那台ECU在擦写Flash期间CAN控制器是停摆的其他ECU的报文还是照常发不会影响刷写机跟目标ECU之间的通信。但如果总线上有其他节点在持续高频发送报文把总线利用率占满了刷写帧的实时性就会下降极端情况下可能超时。所以正规的方案是刷写前通过网关或者诊断命令让其他ECU先进入静默模式或者给刷写报文分配更高优先级更小的CAN ID保证刷写帧能及时被目标ECU收到。CAN的仲裁机制是ID越小优先级越高所以刷写帧的ID要选一个比总线上其他应用报文都小的值。还有一点如果总线上用的是CAN-FD或者CANopen等协议还要考虑RTR帧、远程帧、错误帧等特殊帧的影响。对于bootloader的协议设计最好不要用RTR帧因为很多CAN控制器的RTR处理方式不一致容易踩坑。6. 写在最后的几个实用的建议做一个CAN bootloader不要急着写代码。先把协议文档定下来把帧格式、应答机制、超时时间、重传策略这些在文档里画清楚再动手写代码。协议不定清楚就写代码到联调阶段会发现改一处牵一发动全身比从头写还痛苦。上位机和下位机的日志要尽量详细。每一个关键步骤都要打日志包括发送了哪条命令、收到了什么应答、CRC值是多少、Flash写到了哪个地址。我在项目里甚至会把CAN原始报文也dump下来出了疑难杂症直接看报文内容比猜靠谱得多。最后说一个我自己很深刻的体会关于超时时间的设置不能只看理论值。CAN报文的发送、应答、Flash擦写时间理论值算得再好到了实际硬件上都有偏差。一定要给上位机做压力测试连续刷几十次把最坏情况下的耗时测出来再在超时参数上留30%~50%的余量。我之前就是因为超时设得太紧隔三差五刷写失败后来放宽超时并且加上三次自动重试基本就没有再出过问题了。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门