news 2026/9/13 3:00:53

STM32F103 AB分区OTA从零实现:Bootloader与IAP硬核实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103 AB分区OTA从零实现:Bootloader与IAP硬核实战

1. 项目概述:为什么AB分区OTA不是“炫技”,而是嵌入式产品落地的刚需

你手头那块STM32F103C8T6最小系统板,烧录一次固件要插拔USB线、打开ST-Link Utility、选文件、点下载、等进度条、再拔线——这在实验室调试阶段还能忍;但一旦设备部署到工厂产线、农田传感器节点、或是客户现场的工业网关里,每次升级都要人工跑一趟,成本就不是时间问题,而是真金白银的运维费用。我做过一个远程水文监测项目,57台设备分散在三个县的河道边,光是做一次固件热修复,光差旅和人工就花了近两万。后来我们切到AB分区OTA方案,整个过程变成后台点一下按钮,设备自动拉取、校验、切换、重启,全程无人值守。这才是AB分区OTA的真实价值:它不是工程师写在简历里的技术亮点,而是把“可维护性”从PPT指标变成合同条款的技术底座。

核心关键词“STM32F103”“OTA”“AB分区”“Bootloader”“IAP”背后,是一整套嵌入式系统工程化思维的落地。STM32F103作为经典主流型号,Flash只有64KB或128KB,RAM仅20KB,资源极度受限;而OTA意味着必须在不中断业务的前提下完成固件替换——这就排除了单分区覆盖写入(一旦升级失败,设备变砖);AB分区通过物理隔离两个应用区,用一个“运行中”的分区保底,另一个“待升级”的分区接收新固件,再通过跳转指针切换,实现原子性升级;而这一切的前提,是独立于应用代码之外的Bootloader,它必须能接管复位向量、解析固件头、校验CRC32、控制Flash擦写时序、安全跳转到APP入口。IAP(In Application Programming)则是Bootloader得以运行的底层能力支撑,它要求开发者彻底理解STM32的Option Bytes配置、Flash编程时序、中断向量表重映射、以及最关键的——如何让APP不干扰Bootloader的Flash操作权限。

这个标题里的“从零复现”,不是指照着某篇博客敲代码就能跑通,而是要亲手推演每一个字节的流向:从上位机打包的.bin文件结构,到Bootloader如何从串口逐帧接收并缓存,再到如何把接收到的数据按扇区(Sector)擦除后写入B区,最后如何修改启动标志位并跳转。中间任何一个环节出错——比如没等Flash写入完成就去读校验值,或者APP启动时没重设SysTick中断优先级导致定时器失灵——整套机制就会静默失效,设备卡在黑屏或反复重启。所以这篇内容,不讲虚的原理图,不堆砌CubeMX配置截图,只聚焦在你焊好最小系统、接上J-Link、打开Keil后,接下来该改哪几行汇编、该算哪几个地址偏移、该在哪个函数里加延时、该用什么方式验证CRC——全部基于真实产线踩坑经验,每一步都经得起示波器抓波形、逻辑分析仪看时序的检验。

2. 整体架构设计与关键决策依据:为什么必须放弃“单分区覆盖”和“裸机跳转”

2.1 AB分区不是唯一解,但它是STM32F103上最稳的解

网上很多教程一上来就教你怎么用HAL库的HAL_FLASH_Unlock(),然后直接往APP区地址写数据,美其名曰“IAP升级”。这种做法在实验室可能跑通十次,但放到野外设备上,第11次断电就会让你收到客户凌晨三点的夺命连环call。原因很简单:STM32F103的Flash擦除是以扇区(Sector)为单位的,最小扇区大小为1KB(前4个扇区),而APP固件通常远大于1KB。如果你采用覆盖写入,就必须先擦除整个APP区——擦除过程不可逆,且耗时约20~40ms(实测数据)。在这几十毫秒内,如果外部电源波动、看门狗超时、或用户误操作断开USB,Flash就处于半擦除状态,所有代码丢失,芯片彻底变砖。AB分区的本质,是用空间换时间、换可靠性:A区和B区各占一半Flash(比如64KB芯片分32KB+32KB),升级时只擦除空闲的B区,A区始终运行着完好无损的旧固件。即使B区写入中途失败,设备重启后仍能从A区启动,用户无感知,运维人员后台收到告警后,再安排下一次升级即可。

提示:不要被“双分区”字面意思误导。AB分区不是指物理上划出两个独立Flash芯片,而是将同一片内部Flash逻辑划分为两个区域。STM32F103没有外部存储控制器,所有操作必须在片内Flash完成,因此分区边界必须严格对齐扇区起始地址。常见错误是把B区起始地址设为0x08004000(即A区后1KB),结果发现写入时触发HardFault——因为0x08004000并非扇区边界(F103扇区边界为0x08000000, 0x08001000, 0x08002000…),擦除指令会越界访问非法地址。

2.2 Bootloader必须独立存在,绝不能和APP“合体”

很多初学者试图把Bootloader功能写进APP里,比如在main()开头加个“检查升级标志位”的逻辑。这是重大设计缺陷。原因有三:第一,APP运行时,中断向量表位于0x08000000(默认复位向量位置),所有中断(包括SysTick、USART接收中断)都指向APP自己的中断服务函数。Bootloader需要接管串口接收,就必须能响应USART中断,但如果中断向量表没重映射,APP的USART_IRQHandler就会抢走中断,Bootloader永远收不到数据。第二,APP代码占用大量RAM,Bootloader需要的RAM空间(如接收缓冲区、CRC计算栈)可能被APP变量挤占,导致升级过程内存溢出。第三,也是最关键的一点:APP无法安全擦写自己正在运行的Flash扇区。STM32规定,正在执行代码的扇区不能被擦除,否则触发BusFault。因此,Bootloader必须是一个完全独立的、体积精简(我实测稳定运行的最小Bootloader为1.8KB)、且固化在Flash最前端(0x08000000)的程序,它只做三件事:初始化硬件、接收并校验新固件、跳转到APP。APP则必须从0x08004000(假设A区起始)开始链接,且启动后立即将中断向量表重映射到该地址。

注意:J-Link下载时,Bootloader和APP必须分两次烧录,且Bootloader的hex/bin文件必须包含完整的向量表(前16个字,含初始SP和Reset_Handler地址)。常见错误是用keil生成APP hex时勾选了“Copy ROM to RAM”,导致APP的向量表被复制到RAM,而Bootloader跳转时仍从Flash读取,造成地址错乱。正确做法是APP工程中关闭此选项,并在startup_stm32f10x_md.s里确保Reset_Handler标号为全局可见(声明为GLOBAL Reset_Handler)。

2.3 IAP能力的启用:Option Bytes不是摆设,而是安全开关

STM32F103的Flash编程权限由Option Bytes中的RDP(Readout Protection)和WPR(Write Protection)位控制。很多教程忽略这点,直接调用HAL_FLASH_Program(),结果在某些芯片上返回FLASH_ERROR_WRP。根本原因是出厂默认WPR位为0xFF,即所有扇区写保护开启。必须用ST-Link Utility或J-Flash先解除对应扇区的写保护。具体操作:打开ST-Link Utility → Target → Option Bytes → 将WRP0~WRP3全部设为0x00(表示无保护)→ 点击Apply。注意,此操作会触发芯片复位,且RDP级别会从Level 0降为Level 1(仍可调试,但无法读取Flash内容),这是必要的安全妥协。另外,RDP Level 2(完全锁死)不可逆,切勿尝试。

实操心得:我曾遇到一块国产兼容芯片,ST-Link Utility显示WPR已清零,但HAL_FLASH_Erase仍报错。用J-Flash读取Option Bytes发现,其WPR寄存器映射地址与ST官方手册不同(0x1FFFF800 vs 0x1FFFF804)。最终解决方案是绕过HAL库,直接用汇编指令操作FLASH_CR和FLASH_AR寄存器,手动发送扇区擦除命令。这提醒我们:IAP不是调用几个API就完事,必须深入到寄存器层面理解每个bit的含义。比如FLASH_CR寄存器的PER位(Page Erase Enable)必须置1才能擦除扇区,而PG位(Programming Enable)必须在写入时置1、写入后立即清零,否则后续读操作会异常。

3. 核心细节解析与实操要点:从向量表重映射到CRC32校验的硬核拆解

3.1 向量表重映射:不是memcpy,而是SCB->VTOR寄存器赋值

APP启动后第一件事,就是把中断向量表从默认的0x08000000搬到自己的起始地址(如0x08004000)。很多人用memcpy(dst, src, 256)复制前64个字(16个中断向量×4字节),这是错误的。STM32F103提供专用寄存器SCB->VTOR(Vector Table Offset Register)来实现重映射,只需一行代码:SCB->VTOR = FLASH_BASE | 0x4000;。其中FLASH_BASE为0x08000000,0x4000是偏移量。这样CPU在发生中断时,会自动从0x08004000处读取向量地址,无需复制任何数据,既节省RAM又避免复制错误。

但这里有个致命陷阱:VTOR寄存器只接受256字节对齐的偏移量。也就是说,APP起始地址必须是256的倍数(如0x08004000、0x08004200),否则写入VTOR后,CPU会截断低8位,导致向量表地址错乱。我曾因Keil scatter文件里设置了ALIGN=0x100,导致APP实际起始为0x08004100,VTOR写入后变成0x08004100 & ~0xFF = 0x08004100,但实际向量表在0x08004100,而CPU去0x08004100读,结果读到的是APP代码而非向量表,所有中断失效。解决方法是在scatter文件中强制指定APP入口为256字节对齐:LR_IROM1 0x08004000 0x00008000 { ER_IROM1 0x08004000 0x00008000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00005000 { .ANY (+RW +ZI) } },其中0x08004000已是256对齐。

3.2 Flash擦写时序:不是“调用函数”,而是“等待状态机”

STM32F103的Flash编程有严格时序要求。以写入一个32位字为例,流程是:1. 检查FLASH_SR寄存器BUSY位是否为0;2. 置位FLASH_CR寄存器PG位;3. 执行一次*(__IO uint32_t*)address = data;4. 等待BUSY位清零;5. 清零PG位。很多人省略第1步和第4步,直接写地址,结果在高速循环写入时,前一个字还没写完,后一个字就覆盖了FLASH_CR,导致写入失败。更隐蔽的错误是,在擦除扇区后,没有等待EOP(End of Operation)标志位,就立刻开始写入。实测发现,擦除一个1KB扇区后,EOP可能延迟10ms才置位,此时若立即写入,会触发PGERR(Programming Error)。

我的解决方案是封装一个带超时的写入函数:

#define FLASH_TIMEOUT_MS 100 ErrorStatus FLASH_ProgramWord(uint32_t Address, uint32_t Data) { uint32_t timeout = FLASH_TIMEOUT_MS * 1000; // 转换为微秒 while(FLASH->SR & FLASH_SR_BSY) { // 等待空闲 if(--timeout == 0) return ERROR; } FLASH->CR |= FLASH_CR_PG; // 使能编程 *(__IO uint32_t*)Address = Data; // 触发写入 timeout = FLASH_TIMEOUT_MS * 1000; while(FLASH->SR & FLASH_SR_BSY) { if(--timeout == 0) { FLASH->CR &= ~FLASH_CR_PG; // 清PG位 return ERROR; } } FLASH->CR &= ~FLASH_CR_PG; // 关闭编程 return SUCCESS; }

这个函数的关键在于超时判断——不是简单while(1),而是用计数器防死锁。我在产线测试中发现,某批次芯片在低温(-20℃)下,Flash写入时间延长至120ms,原100ms超时导致升级失败。最终将超时值改为200ms,并增加温度补偿逻辑,才彻底解决。

3.3 CRC32校验:不用第三方库,手算才是真可靠

OTA固件校验必须用CRC32,MD5/SHA1在MCU上计算太慢(需大量移位和查表),而简单累加和(Sum Check)抗错能力极差。STM32F103没有硬件CRC模块(F4/F7才有),必须软件实现。网上很多CRC32代码用查表法(256项uint32_t数组),占RAM约1KB,对F103来说太奢侈。我采用优化的“按字节计算+移位”算法,仅需32字节RAM:

uint32_t crc32_calc(const uint8_t *data, uint32_t len) { uint32_t crc = 0xFFFFFFFF; for(uint32_t i = 0; i < len; i++) { crc ^= data[i]; for(uint8_t j = 0; j < 8; j++) { if(crc & 1) crc = (crc >> 1) ^ 0xEDB88320UL; else crc >>= 1; } } return crc ^ 0xFFFFFFFF; }

关键点在于多项式0xEDB88320UL(IEEE 802.3标准),以及初始值和终值异或0xFFFFFFFF(保证全0数据CRC为0)。实测1KB数据计算耗时约8ms(72MHz主频),完全可接受。更重要的是,这个算法不依赖任何全局变量,可重入,适合在中断服务函数中调用(如在USART接收中断里边收边算CRC)。

实操心得:固件bin文件的CRC必须包含整个有效载荷,但不包括头部信息(如魔数、版本号、长度字段)。我曾把版本号也参与CRC计算,结果APP升级后读取版本号时发现CRC不匹配,排查半天才发现是Bootloader和APP对“校验范围”的定义不一致。最终约定:CRC只计算从0x08字节开始到文件末尾的所有字节,0x00~0x07为固定头部(4字节魔数0x55AA55AA,2字节版本,1字节保留,1字节CRC本身),这样APP启动时,先读头部,再用头部里的长度值去计算后续数据的CRC,比对成功才执行。

4. 实操过程与核心环节实现:从Keil工程配置到J-Link烧录的全流程手记

4.1 Bootloader工程搭建:Keil MDK下的精准内存布局

Bootloader必须占据Flash最前端(0x08000000),且大小严格可控。我将其限制在2KB以内(0x08000000~0x080007FF),为后续留足余量。在Keil中,打开Options for Target → Target → IROM1,设置Start=0x08000000,Size=0x00000800。然后进入User → Linker → Use Memory Layout from Target Dialog → 勾选。关键在scatter文件(或Keil自动生成的分散加载文件),必须显式指定向量表位置:

LR_IROM1 0x08000000 0x00000800 { ER_IROM1 0x08000000 0x00000800 { startup_stm32f10x_md.o (+RESET, +FIRST) ; 复位向量必须在最前 *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00002000 { .ANY (+RW +ZI) } }

这里startup_stm32f10x_md.o (+RESET, +FIRST)确保启动文件中的Reset_Handler放在0x08000000,而.ANY (+RO)将其他只读代码(如main函数)紧随其后。编译后,用Keil的View → Memory Windows查看0x08000000地址,确认前16个字是正确的向量表(如0x20005000是初始SP,0x08000121是Reset_Handler地址)。

APP工程则完全不同。IROM1 Start设为0x08004000(A区起始),Size=0x00004000(16KB,留一半给B区)。scatter文件中,必须添加VECT_TAB_OFFSET宏定义,供启动文件调用:

LR_IROM1 0x08004000 0x00004000 { ER_IROM1 0x08004000 0x00004000 { startup_stm32f10x_md.o (+RESET, +FIRST) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00002000 { .ANY (+RW +ZI) } }

并在startup_stm32f10x_md.s中,将DCD VECT_TAB_OFFSET改为DCD 0x00004000,这样复位后,SCB->VTOR会被初始化为0x08004000。

4.2 串口OTA协议设计:不是AT指令,而是二进制流+状态机

Bootloader与上位机的通信协议,决定升级的鲁棒性。我摒弃了复杂的AT指令集(如AT+OTA_START),采用极简二进制协议:

  • 帧头:2字节 0x55 0xAA
  • 命令字:1字节(0x01=升级开始,0x02=数据块,0x03=升级完成)
  • 数据长度:2字节(大端,表示后续数据字节数)
  • 数据:变长,最大256字节(适配USART RX FIFO)
  • CRC16:2字节(XMODEM标准,多项式0x1021)

例如,发送第一个数据块(地址0x08004000,数据0x01020304...):55 AA 02 00 04 01 02 03 04 XX YY其中XXYY是前面6字节的CRC16。

Bootloader端用状态机解析:

typedef enum { IDLE, WAIT_HEAD1, WAIT_HEAD2, WAIT_CMD, WAIT_LEN1, WAIT_LEN2, WAIT_DATA, WAIT_CRC1, WAIT_CRC2 } ParseState; ParseState state = IDLE; uint8_t rx_buf[256]; uint16_t rx_len = 0, exp_len = 0; void USART1_IRQHandler(void) { uint8_t byte = USART1->DR; // 清RXNE标志 switch(state) { case IDLE: if(byte==0x55) state=WAIT_HEAD1; break; case WAIT_HEAD1: if(byte==0xAA) state=WAIT_CMD; else state=IDLE; break; case WAIT_CMD: cmd=byte; state=WAIT_LEN1; break; case WAIT_LEN1: exp_len = byte<<8; state=WAIT_LEN2; break; case WAIT_LEN2: exp_len |= byte; rx_len=0; state=WAIT_DATA; break; case WAIT_DATA: rx_buf[rx_len++] = byte; if(rx_len >= exp_len) state=WAIT_CRC1; break; case WAIT_CRC1: crc_rx = byte<<8; state=WAIT_CRC2; break; case WAIT_CRC2: crc_rx |= byte; if(check_crc()) process_cmd(); state=IDLE; break; } }

这个状态机不依赖DMA,纯中断驱动,内存占用小,且能处理任意长度的帧。关键是check_crc()函数必须高效,我用查表法(256字节表),10us内完成。

4.3 J-Link烧录实战:不是“Load File”,而是“Verify and Set Breakpoint”

用J-Link Commander烧录Bootloader和APP,必须分两步,且每步都要验证:

  1. JLinkExe -device STM32F103C8 -if SWD -speed 4000 -autoconnect 1
  2. loadfile bootloader.hex 0x08000000→ 烧录Bootloader
  3. verifyfile bootloader.hex 0x08000000→ 验证烧录正确性
  4. loadfile app_a.hex 0x08004000→ 烧录A区APP
  5. verifyfile app_a.hex 0x08004000→ 验证
  6. exit

常见错误是跳过verify步骤,结果发现APP启动失败,却以为是代码问题,浪费数小时。更隐蔽的问题是,J-Link默认使用SWD速度4000kHz,但某些自制板SWD线路阻抗不匹配,导致高速通信误码。我的解决方案是,在J-Link Commander中先执行speed 1000,降速到1MHz,再烧录,成功率100%。另外,烧录前务必执行unlock命令解除芯片保护,否则会提示"Could not download file"。

实操心得:第一次烧录Bootloader后,必须用J-Link的"Memory"窗口,手动读取0x08000000~0x0800001F地址,确认前8个字是正确的向量表(特别是Reset_Handler地址是否为0x08000121)。我曾因scatter文件路径错误,导致Reset_Handler被链接到0x08000200,烧录后芯片复位就跑飞,用J-Link的"Breakpoint"功能在0x08000200设断点,才定位到问题。

5. 常见问题与排查技巧实录:那些让老手也挠头的“幽灵Bug”

5.1 问题现象:设备上电后LED常亮,无任何串口输出,J-Link能连接但无法halt

排查思路:这是典型的向量表错乱。Bootloader的Reset_Handler地址写错了,CPU复位后跳转到非法地址,执行垃圾指令,进入HardFault_Handler,而你的HardFault_Handler里没加任何调试输出(如翻转LED),所以看起来“死机”。

解决步骤

  1. 用J-Link Commander连接,执行r查看寄存器,重点关注PC(Program Counter)值。如果PC=0xFFFFFFFE,说明复位向量为空(0x08000000处是0xFFFFFFFF)。
  2. mem32 0x08000000 4读取前4个字,看是否为有效的SP值(应在0x20000000~0x20005000范围内)。
  3. 如果SP值异常(如0x00000000),说明Bootloader hex文件没包含向量表,或scatter文件没把startup.o放在最前。重新检查Keil的Output选项,确保"Create HEX File"和"Use Memory Layout from Target Dialog"都勾选。
  4. 如果SP正确但PC异常,检查startup_stm32f10x_md.s中Reset_Handler标号是否为GLOBAL(非WEAK),且未被其他文件重复定义。

5.2 问题现象:OTA升级过程中,串口接收速率越来越慢,最后超时失败

根因分析:这是Flash写入时序问题。Bootloader在接收数据的同时,还在后台擦除和写入Flash。而Flash编程期间,CPU执行效率会下降(总线被Flash控制器占用),导致USART中断响应延迟,RX FIFO溢出,丢帧。

实测数据:在72MHz主频下,擦除一个1KB扇区耗时约35ms,期间若以115200bps接收数据,最多可接收约400字节,超出部分丢失。因此,协议必须支持“流控”。

终极方案:在Bootloader中加入硬件流控(RTS/CTS)。将PA1(USART1_RTS)和PA0(USART1_CTS)引出,连接上位机USB转串口模块的对应引脚。在Bootloader初始化USART时,启用硬件流控:

GPIO_InitTypeDef GPIO_InitStruct; USART_InitTypeDef USART_InitStruct; // 初始化PA0/PA1为AF Push-Pull GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStruct); // USART初始化 USART_InitStruct.USART_BaudRate = 115200; USART_InitStruct.USART_WordLength = USART_WordLength_8b; USART_InitStruct.USART_StopBits = USART_StopBits_1; USART_InitStruct.USART_Parity = USART_Parity_No; USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_RTS_CTS; // 关键! USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStruct);

这样,当Bootloader Flash忙时,自动拉高RTS信号,通知上位机暂停发送,彻底解决丢帧问题。

5.3 问题现象:APP升级成功,但运行后ADC采样值全为0,或TIM2 PWM无输出

深度解析:这是外设时钟未使能的典型表现。Bootloader为了省电,通常在跳转前关闭所有外设时钟(RCC->APB1ENR/RCC->APB2ENR全清零)。而APP启动后,如果没在SystemInit()或main()开头重新使能对应时钟,外设就无法工作。

验证方法:用J-Link在APP的main()第一行设断点,运行后查看RCC->APB1ENR寄存器值。如果为0x00000000,说明时钟全关。

标准修复:在APP的SystemInit()函数中,必须显式使能所需时钟。例如,若用ADC1和TIM2,则:

RCC->APB2ENR |= RCC_APB2ENR_ADC1EN | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN; RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;

注意,AFIOEN(Alternate Function I/O Clock)必须开启,否则GPIO复用功能(如ADC_IN0, TIM2_CH1)无效。这个细节在HAL库中由MX_GPIO_Init()自动处理,但裸机开发必须手动添加。

5.4 问题现象:AB分区切换后,设备启动卡在Bootloader,不再跳转到APP

核心线索:“启动标志位”被意外修改。AB分区方案中,必须有一个全局标志(如Flash中某个固定地址,如0x08007FF0)记录当前应运行的分区(0=A,1=B)。Bootloader每次启动都读此标志,决定跳转目标。如果此地址被APP误写(如数组越界),或Bootloader写标志时断电,标志位损坏,就会导致死循环。

安全加固方案:采用“双标志+校验”机制。在Flash末尾预留4字节:0x08007FF0(Flag_A)和0x08007FF2(Flag_B),分别存储A区和B区的状态(0x0000=无效,0x55AA=有效)。Bootloader启动时,同时读取两个标志,只跳转到状态为0x55AA的分区。写入新标志时,先擦除旧标志所在扇区,再写入新值,最后写入校验字(如0x08007FF4存Flag_A的CRC16)。这样,即使断电发生在写入中途,最多一个标志损坏,另一个仍可引导。

实操验证:用J-Link Commander执行mem32 0x08007FF0 4,查看四个字的值。正常情况下,应为0x000055AA(A区有效)或0x55AA0000(B区有效)。如果看到0x55AA55AA,说明两个分区都被标记为有效,Bootloader会按预设优先级(如B区优先)跳转,需检查写标志逻辑。

6. 工程化扩展与产线实践:从实验室Demo到批量部署的跨越

6.1 固件签名:不是“加个MD5”,而是ECDSA真加密

OTA升级最大的安全风险是固件被篡改。CRC32只能防误,不能防恶意。在产线环境中,必须引入非对称加密签名。STM32F103虽无硬件加密引擎,但ECDSA(椭圆曲线数字签名算法)的p256曲线,纯软件实现仅需约8KB Flash和2KB RAM,完全可行。我选用mbed TLS库的精简版,裁剪后仅保留ecp、ecdsa、sha256模块。

签名流程:上位机用私钥对固件bin计算ECDSA签名,追加到bin文件末尾(固定128字节)。Bootloader启动时,用预置的公钥(存于Flash特定扇区,如0x08007000)验证签名。验证失败则拒绝启动,进入安全模式(LED慢闪,串口输出错误码)。

关键优化:为加速验签,将公钥的曲线参数(Gx,Gy,p,a,b,n)预先计算好,存为常量数组,避免运行时重复计算。实测验签耗时约1.2秒(72MHz),在可接受范围内。这比“加个MD5哈希值再用AES加密”的方案更安全,因为AES密钥一旦泄露,所有固件都可伪造。

6.2 OTA升级包管理:从“单bin”到“差分升级”

每次升级都传输整个固件bin(如64KB),在2G/3G网络下耗时且昂贵。差分升级(Delta Update)只传输新旧固件的差异部分。我采用bsdiff算法生成差分包,Bootloader端用bspatch应用。bsdiff生成的差分包通常只有原固件的10%~30%,极大节省流量。

挑战在于F103 RAM不足(仅20KB)。bspatch需要约15KB RAM做滑动窗口。解决方案是分块patch:将差分包按1KB分块,每块patch后,将更新后的APP代码写入Flash,再释放RAM,处理下一块。这样峰值RAM占用降至5KB。实测一个64KB固件,差分包仅12KB,升级时间从45秒(全量)缩短至12秒(差分)。

6.3 量产烧录:J-Link Script自动化

产线烧录Bootloader和初始APP,不能靠人工点鼠标。我编写J-Link Script(.jlink文件)实现一键烧录:

si swd speed 1000 connect erase loadfile bootloader.hex 0x08000000 loadfile app_a.hex 0x08004000 r verifyfile bootloader.hex 0x08000000 verifyfile app_a.hex 0x08004000 g exit

然后用批处理调用:JLink.exe -CommanderScript burn.jlink。配合治具,10秒内完成一台设备的初始固件灌装,效率提升20倍。

最后分享一个小技巧:在Bootloader中加入“强制升级模式”。长按BOOT0按键上电,Bootloader跳过标志位检查,直接进入串口升级等待状态。这为产线返修提供了终极救急通道——哪怕APP彻底损坏,只要Bootloader完好,就能用一根USB线救活。这个功能,救过我三次产线紧急事故。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 3:00:38

MySQL数据库备份与恢复:从逻辑备份到binlog增量恢复的完整指南

Mysql数据库的备份与恢复做开发或运维的朋友应该都有过这种经历&#xff1a;深夜十二点&#xff0c;手机突然响起&#xff0c;电话那头业务方急得声音发抖——“刚才那张表的数据被误删了&#xff0c;能恢复吗&#xff1f;”先别慌&#xff0c;能不能恢复、能恢复到什么程度&am…

作者头像 李华
网站建设 2026/9/13 2:59:04

二进制反汇编原理与Python实现指南

1. 二进制与反汇编基础概念二进制文件是计算机程序的最终表现形式&#xff0c;它由处理器能够直接执行的机器指令组成。当我们谈论"二进制转反汇编"时&#xff0c;实际上是在讨论如何将这种机器可读的代码转换回人类可理解的汇编语言形式。1.1 二进制文件的本质二进制…

作者头像 李华
网站建设 2026/9/13 2:55:58

ICPC真题解析:Kruskal重构树与动态线性基实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 2:53:32

聚合路由器 vs 5G CPE:广电直播推流的播出级选型解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华