拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32F103 CAN Bootloader固件升级全解析:从协议设计到量产实践

STM32F103 CAN Bootloader固件升级全解析:从协议设计到量产实践

1. Bootloader项目到底解决什么问题

1.1 为什么要做CAN Bootloader

先交代一下背景。我这边做的产品是分布式的车载电控单元,主控用的STM32F103系列,节点之间走CAN总线。早期固件升级都是人工拿着ST-Link挨个开盖刷写,一条产线几十个节点,一遍刷下来少说一个多小时,而且后装市场返修更头疼——设备已经封在机壳里,有些还打了胶,压根没法拆。这时候远程升级就成了刚需,而CAN总线恰恰是现成的通道。

STM32F103在国产化替代和低成本项目里用得非常多,芯片本身自带CAN外设,支持标准帧和扩展帧,波特率可配,硬件上只要加一个CAN收发器(比如TJA1050、SN65HVD230)就能跑起来。所以我就把Bootloader方案定成了“CAN Bootloader”:上电先跑一段IAP程序,通过CAN总线接收升级包,写入内部Flash,完成后跳转到APP执行。整个过程不需要额外的下载器,只需要一个USBCAN卡或者现场总线上的上位机节点就能完成。

做这件事之前我踩过一个认知误区:以为Bootloader就是写一段“把接收到的数据往Flash里写”的代码,结果第一版出来问题一大堆——跳转过去就死机、写到一半写不进去、升级中断后设备变砖。后来才明白,Bootloader看起来简单,实际涉及Flash分区规划、中断向量表迁移、CAN协议设计、异常恢复机制、量产防呆设计一堆事。这篇文章把我在这个项目里的完整实践拆开讲,从协议、源码、调试到量产落地,一条线捋清楚。

1.2 方案选型:为什么是CAN而不是UART

很多教程讲Bootloader都喜欢用UART,因为简单,一个串口加一个USB转TTL就能演示。但在整车和工业现场场景里,UART有几个致命问题:

第一,很多设备没有拉出串口引脚,或者串口被其他功能占用。第二,RS232/TTL抗干扰能力弱,线缆一长误码率就上来,而工业现场动辄几米甚至几十米线束。第三,整车上已经是CAN网络了,单独为升级再拉一条调试串口,产线和售后都要多一套硬件。

CAN的优势是物理层本身就是差分信号,抗干扰强,通信距离远,而且天然支持多节点组网。同一根总线上的设备,可以通过ID过滤只响应升级指令,其他节点正常工作不受影响。对于量产部署来说,CAN还有一个好处:产线测试本来就要通过CAN做EOL(End of Line)检测,升级功能可以直接复用同一套工装和线束。

另外从成本角度看,F103的CAN外设几乎是“白送”的,不需要额外加芯片。唯一要注意的是CAN收发器选型,我建议用带隔离的模块,比如CTM1050,产线上经常有设备接地不一致的问题,隔离模块能直接把共模干扰拦在物理层外面,少很多莫名其妙的通信故障。

1.3 整体架构与功能拆解

这个项目最终落地的系统架构分成三层:

第一层是上位机/产线工装。我这边用的是周立功USBCAN-II,上位机软件自己用C#写了一个简单的升级工具,通过CAN发送升级包。如果现场总线里已经有主节点在做调度,也可以让主节点兼任升级发起方。

第二层是Bootloader程序,放在STM32F103内部Flash的起始区域。它负责上电初始化CAN、等待升级指令、接收固件包、擦写Flash、校验、跳转。

第三层是APP应用程序,放在了Flash的高地址区。APP里面也要配合做一件事:把中断向量表重映射到自己的起始地址,否则中断一进来就跑到Bootloader里去了。

整个升级流程大概是这样:设备上电先跑Bootloader,如果是冷启动并且没有收到升级指令,就延时几百毫秒后直接跳转到APP;如果收到了升级帧或者APP区无效,就留在Bootloader等待完整升级包。升级指令体一般包括握手、开始传输、数据帧、结束帧、校验结果回读这几类。

2. 核心原理:Flash分区与程序跳转机制

2.1 STM32F103内部Flash布局与分区

STM32F103的内部Flash从0x08000000开始,大小因型号而异,比如F103C8T6是64KB,F103RCT6是256KB,F103ZET6是512KB。Flash的擦除以页为单位,注意不同容量的页大小不一样:小容量和中容量(64KB以下)是1KB一页,大容量(128KB以上)是2KB一页。

我在这个项目里用了F103CBT6,Flash一共128KB,页大小1KB。分区很关键,规划好了后面所有逻辑都顺。我的分区如下:

区间地址范围大小用途
Bootloader0x08000000 ~ 0x08001FFF8KBIAP升级程序
参数存储区0x08002000 ~ 0x08002FFF4KB升级标志、版本号、校验和
APP程序区0x08003000 ~ 0x0801BFFF102KB应用程序
预留0x0801C000 ~ 0x0801FFFF16KB备份区或日志区

Bootloader给8KB是非常宽裕的,实际上我编译完只有4KB多。APP起始地址选0x08003000,注意这里有个对齐问题:虽然F103的页是1KB,但向量表要求按地址对齐到0x08000000或0x08010000这样的大地址边界吗?不需要,向量表只要对齐到自己所在地址即可,关键是偏移量按字对齐。0x08003000可以被4整除,满足要求。

2.2 向量表偏移与APP起始地址设置

APP程序编译时必须把ROM起始地址改成0x08003000。在Keil MDK里就是打开Target选项卡,把IROM1的Start改成0x08003000,Size改成0x1B000(102KB)。很多人改了ROM地址后程序还是跑飞,其实就是忘了初始化向量表偏移。

从F103启动流程来说,芯片上电后固定从0x08000000取栈指针和复位向量。如果APP放在0x08003000,那么APP的向量表也在0x08003000,但CPU并不会自动知道,它默认还是去0x08000000找向量表。所以Bootloader跳转前必须告诉CPU:“你的向量表搬家了,以后中断从这里查”。

标准做法是在APP的main函数最开始调用:

#define APP_VECTOR_TABLE_ADDR 0x08003000 SCB->VTOR = APP_VECTOR_TABLE_ADDR;

对F103来说,SCB->VTOR这个寄存器是0xE000ED08。注意第一次赋值时,要先确保当前代码本身不依赖中断,因为一旦改了VTOR,后面来的所有中断都会去新向量表取地址。所以这句要放在main最前面,最好在系统初始化之前。

2.3 跳转函数实现与临界区处理

Bootloader跳转APP的方法有讲究。不能简单地用函数指针,因为直接调用时栈指针和复位向量没重新初始化,很可能跳过去后第一句就进HardFault。

我用的跳转代码是业内标准的做法:

typedef void (*pFunction)(void); void JumpToApp(uint32_t app_addr) { uint32_t stack_addr = *(volatile uint32_t *)app_addr; uint32_t reset_addr = *(volatile uint32_t *)(app_addr + 4); if ((stack_addr & 0xFFF00000) != 0x20000000) { return; // 栈指针异常,不能跳转 } // 关闭全局中断 __disable_irq(); // 恢复默认时钟配置,避免APP初始化时钟时冲突 RCC_DeInit(); SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0; // 关闭AHB/APB外设时钟(可选,但建议做) RCC->AHBENR = 0; RCC->APB1ENR = 0; RCC->APB2ENR = 0; // 重新设置栈指针,然后跳转 __set_MSP(stack_addr); ((pFunction)reset_addr)(); }

有几个容易忽略的点:跳转前要把SysTick关掉,否则APP里初始化SysTick时会开在Bootloader残留的中断上。其次是中断标志位,CAN外设如果还有挂起中断,跳过去可能马上触发,但APP的CAN中断向量已经重映射了,没问题;主要是EXTI、USART这些,最好也清一下挂起位。另一个是硬件看门狗,如果Bootloader里开了IWDG,跳转前先关掉,否则APP如果初始化耗时长,看门狗先复位了。

为什么跳转前检查栈指针?因为如果APP区是空的或者数据损坏,第一个字不落在0x20000000开头的SRAM范围内,说明这个“栈指针”根本不是合法的,直接跳过去必然死机。实测中最常见的就是APP没烧录就跳转,加了这道检查能省很多返修。

3. CAN通信协议设计

3.1 协议分层与帧格式定义

CAN通信和UART的升级协议有个本质区别:UART可以一字节一字节流式处理,CAN一次最多传8字节有效载荷,而且帧和帧之间没有天然的“流”的概念。所以协议必须自己定义帧类型、帧序号、数据长度、校验方式,实现“CAN上的文件传输”。

我设计的协议比较简单,两层就够了:底层是CAN数据帧,上层是升级指令区。全部使用标准帧,11位ID,波特率500kbps。ID分配如下:

ID方向含义
0x100上位机 -> 设备:升级控制命令
0x101上位机 -> 设备:数据帧
0x102上位机 -> 设备:请求重传
0x110设备 -> 上位机:应答帧
0x111设备 -> 上位机:状态上报

注意一个细节:如果总线上还有其他业务节点,ID分配要避开业务ID的范围,防止升级帧被当成普通业务帧解析。我们这边业务帧ID都在0x200以上,所以升级帧用0x1xx完全隔离。

控制命令帧的Data[0]是命令码,Data[1]是参数。常用命令包括握手(0x10)、开始升级(0x11)、结束升级(0x12)、查询状态(0x13)、复位执行(0x14)。数据帧里Data[0]是帧序号,Data[1]~Data[7]是固件数据,一次最多传7字节。

3.2 分帧传输与多帧重组

F103的Flash页最小1KB,而CAN一帧最多7字节数据,所以一页数据要拆成147帧左右。整个固件如果有90KB,大概需要13000多帧。听起来很多,但500kbps的CAN总线上,每帧大约0.26ms,一秒钟能传3000多帧,实际上传90KB的固件大概10秒内能完成,这个速度在量产产线里完全可接受。

分帧传输的核心问题是丢帧和乱序。CAN本身有硬件仲裁和CRC校验,正常通信丢帧率非常低,但也不是万无一失——总线繁忙、错误帧、节点掉线都会导致丢帧。我采用的办法是“滑动确认”:上位机每发送64帧后,等待设备应答一个“接收进度”消息,设备告诉上位机“我已经正确收到了第N帧”,上位机从N+1继续发。这种方式比逐帧握手快得多,又比全包发完再校验保险得多。

设备端收到数据帧后,先把数据暂存在一个RAM缓冲区里。因为F103CBT6的SRAM是20KB,装不下整个固件,所以缓冲区只放一页(1KB)的数据,攒满一页后立刻写入Flash,然后清空缓冲区等下一页。这样RAM占用不到2KB,非常节省。

3.3 高可靠性的确认与重传机制

升级过程最怕的不是“传输慢”,而是“悄悄丢帧最后校验失败”。所以协议里在结束阶段做了一个双保险:CRC32校验和回读校验。

发送端(上位机)在发送固件前先计算整个固件文件的CRC32,放在结束升级命令帧里一起发过去。设备端收完所有数据后,也把收到的固件数据按同样的算法算一遍CRC32,和上位机发的值比对。如果一致,应答“校验通过”;如果不一致,应答“校验失败”,上位机可以重新发送整个固件或者逐帧重传。

只做CRC还不够,因为设备写Flash后可能因为供电不稳、Flash疲劳等原因出现个别字节写入错误。我在校验CRC之前还有一个“回读”步骤:所有数据写完后,把Flash里的内容和RAM缓存里的原始数据逐字节比较一遍,确认硬件写入没有异常。这个回读比较,看起来慢(90KB数据逐个字节读),但实际上Flash读速度很快,耗时不到一秒钟,这笔账花得很值。

3.4 和DBC文件的对接问题

做整车项目的朋友会关心CAN协议能不能用DBC文件管理。我的建议是:升级专用的私有协议不建议进DBC,因为DBC里的信号是周期或事件触发的“业务信号”,而升级协议里的数据帧是“连续字节流”,用DBC来表达反而别扭。如果产线工具非要通过DBC来对接,可以把控制命令和状态应答做成DBC里的两个消息,数据帧通过DBC的原始字节信号透传。但实际产线我都是让上位机直接走原始CAN帧。

4. 工程实现与源码解析

4.1 工程结构搭建

工程我基于STM32CubeMX生成底层,再在Keil MDK里加Bootloader逻辑。CubeMX里开了CAN1、UART1用来打日志(调试期用)、一个定时器TIM3作为升级超时计时,GPIO里留了一个LED做状态指示。

之所以用TIM3做超时计时而不是简单用延时,是因为Bootloader的接收循环中经常要判断“等一个握手应答等了多久”“等下一帧等了多久”,用定时器做基准可以避免阻塞式延时把CAN接收卡死。

CubeMX配置要点:CAN波特率500k,采样点设置在75%左右。F103的CAN外设时钟来自APB1,通常是36MHz,500k波特率对应的分频是:

// APB1 = 36MHz // BRP = 4, 则时基 = 4 * (1/36MHz) = 111ns // tq数 = (1/500k) / 111ns ≈ 18 // 采样点 = (1 + 13) / 18 ≈ 77.8%

具体在CubeMX里就是配置CAN的Prescaler=4,Time Quanta=18,重新同步跳跃宽度选2。采样点对老练的车载CAN网络很重要,总线线缆长、节点多时,采样点稍微不对就会出现“对方发得出来、自己收不到”的灵异问题。

4.2 Flash擦写底层实现

Flash驱动是本项目的硬骨头之一,F103的Flash写入有几个硬性规则:

  1. 写Flash前必须先擦除整个扇区(页),擦除后数据全变成0xFF。
  2. 擦除和编程操作需要在FLASH_CR寄存器配置,操作完成后要检查BSY位。
  3. 编程必须按16位(半字)进行操作,不能按字节写。
  4. 操作期间不能有中断嵌套访问Flash,否则会报错。

我封装了一个比较稳的写Flash函数,基本原理是“先擦除目标页,然后按半字写入”:

uint8_t FLASH_WritePage(uint32_t page_addr, uint8_t *buf, uint16_t len) { uint16_t i = 0; uint16_t *p = (uint16_t *)buf; uint16_t num_halfwords = (len + 1) / 2; // 补到半字对齐 FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); if (FLASH_ErasePage(page_addr) != FLASH_COMPLETE) { FLASH_Lock(); return 1; } for (i = 0; i < num_halfwords; i++) { if (FLASH_ProgramHalfWord(page_addr + i * 2, p[i]) != FLASH_COMPLETE) { FLASH_Lock(); return 2; } } FLASH_Lock(); return 0; }

注意这个函数里的FLASH_ProgramHalfWord来自标准外设库,操作时要确保全局中断是关闭的,或者至少保证没有其他更高优先级的中断在Flash编程期间进入。我在Bootloader的升级主循环里用临界区保护了整个擦写过程,代价是CAN接收中断在写Flash期间(大约1KB数据擦写要几十ms)会丢几帧。这个问题的解决方案是上位机发数据时设计了“每64帧等待应答”的机制,预留了足够的时间余量,所以丢帧不影响最终结果。

4.3 主循环状态机

Bootloader不能写成“顺序执行”的流水账代码,因为现场可能出现在任意阶段掉线、复位、中断等异常情况。我把它设计成一个有限状态机,这样每个状态对应明确的行为和超时处理。

状态有:

状态说明超时动作
IDLE初始等待无
WAIT_HANDSHAKE等待上位机握手超时跳APP
RECEIVING接收固件数据超时报错,返回IDLE
WRITING_FLASH写入Flash写失败返回IDLE
WAIT_CRC_CMD等待结束/校验命令超时报错,返回IDLE
CRC_CHECK校验固件校验失败进入ERROR
JUMP_APP跳转执行APP跳转失败进入ERROR

主循环伪代码如下:

while (1) { switch (state) { case IDLE: if (frame_received && cmd == CMD_HANDSHAKE) { send_response(RESP_ACK); state = WAIT_HANDSHAKE; timer_start(TIMER_UPGRADE_TIMEOUT); } else if (app_valid()) { delay_ms(500); // 冷启动短暂等待,给上位机一个握手窗口 JumpToApp(APP_START_ADDR); } break; case RECEIVING: page_buf_receive(frame); if (page_buf_full()) { FLASH_WritePage(...); send_ack(progress); } break; case CRC_CHECK: if (crc32_matched()) { send_response(RESP_CRC_OK); state = JUMP_APP; } else { send_response(RESP_CRC_ERR); state = ERROR; } break; case JUMP_APP: JumpToApp(APP_START_ADDR); break; case ERROR: // 停留等待重新握手,或超时后自复位 break; } }

状态机的好处是逻辑可控,出问题能快速定位到具体环节。调试的时候我在每个状态切换点打一条串口日志,配合上位机抓帧,哪里断的一目了然。

4.4 APP端配合改法

很多朋友只盯着Bootloader写,忘了APP也要配合。前面说了要设SCB->VTOR,还有几件事也别忽略:

一是中断服务函数的地址不能写死。比如老代码里有时会直接给某个外设中断回调赋函数指针,从旧地址跳到新地址后这个指针如果不重新初始化就会飞掉。

二是APP的启动文件里,堆栈大小要重新评估。Bootloader跳转到APP时,用的是APP向量表第一个字作为栈指针,所以APP启动文件开头的Stack_Size和Heap_Size要按APP的实际需求配,别沿用Bootloader的小栈配置。

三是APP里要预留一个“固件更新标志”。我是在参数存储区里放了4个字节的魔数,比如0xA5A5A5A5表示“下次复位进入Bootloader升级模式”,APP收到升级指令后,先在参数区写标志,然后软复位。这样就不需要每次上电都要等待Bootloader的超时窗口,用户体验好得多。具体做法:

void EnterBootloader(void) { *(volatile uint32_t *)PARAM_AREA_ADDR = UPGRADE_REQUEST_FLAG; NVIC_SystemReset(); }

Bootloader启动后先检查参数区的标志,如果是升级请求就直接留在Bootloader,否则延时跳APP。升级流程完成后清除标志。

4.5 计时与超时处理

升级过程中如果上位机掉线,设备不能永远死在“接收状态”,要有一个兜底超时。我用TIM3做毫秒级计时基准,每次收到有效帧就清零计数值,如果超过5秒没收到任何帧,就判定通信中断,自动复位到Bootloader起始状态,等待下一次升级指令。这样即使上位机中途崩溃,设备也不会卡死。

超时时间的选择我说一下:不能太短,因为CAN总线在某些瞬间可能连续几个错误帧导致总线阻塞,几帧收不到很正常;也不能太长,否则产线上发现升级失败后要干等半天。5秒是实践下来比较合适的值。

5. 量产部署的坑与对策

5.1 产线烧录流程设计

实验室里怎么玩都可以,但量产部署讲究的是流程标准化和防呆。我这边最终确定的产线流程是这样:

  1. 工装治具夹好设备,给设备上电,设备进入Bootloader等待握手。
  2. 上位机扫描总线上所有设备ID,确认数量与工位产品一致。
  3. 上位机发送握手帧,设备端回读版本号、芯片ID、上次升级状态。
  4. 上位机比对目标固件和当前固件版本,决定是否升级。
  5. 升级完成后,上位机发送“查询固件CRC”命令,把设备端Flash中固件的CRC32读回来,和上位机源文件比对,作为出厂验收项之一。
  6. 记录升级时间、设备序列号、固件版本、CRC结果到MES系统。

这个流程在产线上跑了两个多月了,最大的经验是:上位机不要每台设备都重新配置参数,最好是读一个固定的配置文件。固件版本号、存放路径、目标节点ID、是否强制升级这些统统从配置文件读,避免操作员手动点错。

5.2 防止刷成砖的机制

任何Bootloader最怕的就是升级失败后设备变砖,尤其量产阶段一旦砖了就要拆壳烧写,损失很大。我的防砖策略有三个层次:

第一是双保险启动逻辑:设备上电后先检查APP区首字(栈指针)和复位向量是否合法,不合法就留在Bootloader。这样即使APP刷得稀烂,只要Bootloader还在,就能重新刷回来。

第二是升级确认机制:写完固件后先校验CRC,校验通过才设置“APP可用”标志。这个标志单独放在参数存储区,是最后一步才写入的。如果固件还没校验就断电,标志不被置位,下次启动还是会进Bootloader。

第三是Bootloader本身的自保护:在擦除APP区之前,先校验Bootloader自己的CRC,如果Bootloader区损坏了才说明真没救了,但这几乎不会发生。F103内部Flash的寿命和稳定性在正常供电下是有保障的,真正会搞坏Bootloader的是在擦写过程中断电。

另外,我强烈建议给产线工装加一个检测程序:升级完成后设备自动发一条“版本+CRC”消息,工装显示绿色通过才允许装箱。虽然看起来多了一步,但能拦截掉几乎所有潜在问题。

5.3 升级认证与防回滚

如果产品对安全有要求,光有CRC还不够。我这边在协议里加了简单的序列号认证:设备端在握手时把96位唯一ID(芯片的Unique ID)回传给上位机,上位机把升级包绑定到这个ID上,防止A设备的固件被复制刷到B设备里去。这个在知识产权保护和产品管理上有实际价值。

防回滚机制也做了,比如当前版本是V1.2,上位机不允许下发V1.1。怎么实现?在结束升级命令帧里带上目标版本号和最低允许版本号,设备端判定版本号是否符合要求,不符合直接拒绝。这样能避免产线调试时误刷旧固件导致后期兼容性问题。

5.4 量产测试项与验收

量产验证踩过坑之后,我整理了一份验收清单,推荐给同样要做量产部署的朋友:

测试项操作通过标准
升级功能正常下发固件CRC一致,APP可正常运行
断电恢复传输中随机断电上电后进入Bootloader,可重新升级
非法固件发送损坏固件设备拒绝执行,APP不变
升级后用业务测试跑一遍正常业务无异常复位、无总线错误
长时间老化升级后连续运行24小时无复位、无死机

6. 调试经验与常见问题

6.1 CAN通信突然连不上

这个坑我遇到太多次了,现象是“之前明明好好的,突然一发数据就发不出去,或者对方收不到”。排查顺序我总结如下:

先看波特率。CAN没有“自动识别波特率”的功能,双方波特率不一致时表现为:发送方自己能看到总线报文吗?可以。但对方收不到,或者收到一帧后后续全是错误帧。用CAN分析仪(USBCAN、PCAN都可以)抓总线数据,如果看到一堆Error Frame,几乎可以断定波特率或采样点不匹配。

再看终端电阻。CAN总线的两端必须各接一个120欧电阻。我的产线工装里经常出现只在一端接了电阻的情况,短距离实验发现不了问题,但线缆长度超过几米后,信号反射会把波形搞崩,表现为升级过程中偶发性丢帧。

还要检查总线占用率。如果现有CAN网络里业务报文非常密集,升级帧的优先级低,可能一直无法获得仲裁。这种情况可以调高升级帧的ID优先级,或者暂时让业务节点静默。

我调这类问题一般用USBCAN的“总线检测”模式,抓一段波形看电平是否正常,再用“报文统计”看错误帧计数,基本能定位。

6.2 下载中途断开与Flash写入失败

升级过程中最恶心的事情是“传输到80%后设备断了”。原因五花八门,但我排查下来最多的是供电问题。CAN升级时设备通常要连续工作一段时间,如果供电用的是劣质电源适配器,电流一波动,Flash擦写瞬间电流大,直接把供电拉低到复位阈值,设备就重启了。

处理方式:给设备供电用稳压电源,电流余量留足。另外在Bootloader里加“掉电保护”:一旦检测到供电异常(比如用ADC采样VDD),立即停止Flash操作,等待供电稳定后再继续。

Flash写入失败还有一种隐蔽原因:写某个地址区间时,该地址对应的Flash扇区正在被代码本身占用。比如把缓冲区定义在Flash映射的地址空间(虽然这不可能,因为SRAM和Flash地址分开的),或者中断向量表所在的扇区被误擦除。真遇到了可以用CMSIS里的FLASH_GetStatus函数把错误状态打出来,常见的PGERR或WRPRTERR对应的问题不一样。

6.3 跳转失败死机的定位

跳转失败的现象通常是“Bootloader屏幕还活着(LED闪),但APP不启动”,或者是“直接进HardFault”。定位思路:

第一步,检查APP区是否真的有内容。用调试器读0x08003000处的前8个字节,正常应该是一个SRAM地址(比如0x20001234)和一个Flash地址(比如0x08003341)。如果读出来是0xFFFFFFFF,说明压根没刷进去。

第二步,检查APP的VTOR是否设置。在APP的main函数里设SCB->VTOR后,再查看该寄存器值是否等于0x08003000。

第三步,检查跳转时中断是否还挂着。可以把所有挂起中断清一遍再跳转:

NVIC->ICER[0] = 0xFFFFFFFF; NVIC->ICPR[0] = 0xFFFFFFFF;

另外还有一个容易忽视的点:如果APP用了FreeRTOS,跳转时APP的任务栈和系统时钟初始化都不能依赖于“从0x08000000开始的启动流程”,FreeRTOS的启动文件通常会调用SystemInit,这个函数里有时会写PLL和Flash等待周期,但它是用寄存器配置硬编码的,不会因为地址变了就出错。只要确保时钟和Flash等待状态在跳转前被妥善处理即可。

6.4 工具与资料推荐

关于工具和资料,我建议手头常备这么几样:

  • 周立功USBCAN-II或者PCAN,用来抓CAN报文、模拟上位机
  • ST-Link V2,用来烧录Bootloader和调试APP
  • STM32F103中文参考手册(RM0008),注意要看“Flash编程”和“CAN控制器”这两章,PDF记得下原版或者靠谱翻译版
  • Keil MDK工程,用CubeMX生成底层后,千万别手动画代码,能省掉大量无意义错误
  • 一个示波器或者逻辑分析仪,排查CAN物理层问题时必备

调试期如果嫌串口日志麻烦,我后来是直接把调试信息编码成CAN私有帧发出来,上位机一个窗口实时显示Bootloader内部状态。这样最贴近实际工作环境,也省得在板子上额外留串口。

7. 写在后面的几句体会

这个CAN Bootloader项目前前后后改了五六版,真正稳定是在加了状态机、超时处理和断电保护之后。回头看我踩过最大的坑,反而是最基础的两件事:一是Flash分区和向量表偏移,二是CAN总线的物理层参数。这两个问题如果提前吃透,后面很多折腾根本不会发生。另外一个深刻的体会是Bootloader这东西不是“写完就行”,它是出厂后你唯一能远程抓住设备的手,质量要求要比普通业务代码更高,多花几天时间把边缘情况想全,比后面出差返修划算得多。最后再提醒一句:量产部署前,一定要用坏固件、半截固件、断电场景充分折磨你的Bootloader,它扛得住,产线才能睡得着觉。

返回列表