news 2026/9/11 11:48:22

STM32F103 AB分区OTA从零实现:低成本高可靠空中升级方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103 AB分区OTA从零实现:低成本高可靠空中升级方案

1. 项目概述:为什么AB分区OTA在STM32F103上不是“锦上添花”,而是“生存刚需”

你手头那块焊着CH340、用杜邦线连着J-Link的STM32F103C8T6最小系统板,现在正跑着一个温湿度采集+LED状态指示的固件——它能稳定工作三个月,但只要你想加个Wi-Fi配置页面,或者改一行Modbus从机地址,就得拿烧录器插上去,断电、擦除、重烧、上电、验证……整个过程像给老式收音机换真空管,耗时、易错、现场不可控。这就是绝大多数基于STM32F103的工业传感器、楼宇控制器、智能硬件原型的真实现状。而“STM32F103_AB_OTA_从零复现教程”这个标题里的每一个词,都不是技术炫技,而是直指这个痛点的手术刀:AB分区是容错机制,OTA是远程能力,STM32F103是成本与生态的平衡点,从零复现意味着不依赖任何商业SDK或黑盒库,所有逻辑、跳转、校验、回滚全部亲手捏出来。我做过7个不同行业的STM32F103项目,其中4个因OTA失败导致设备集体变砖,最后靠售后工程师带烧录器上门才救回来。所以这不是“要不要做”的问题,而是“不做就等着被客户投诉到停产”的现实压力。本教程完全基于标准外设库v3.5(也就是你搜到的“stm32f103库v3.50下载”那个版本),不碰HAL库,不调CubeMX生成代码,所有寄存器操作、向量表偏移、Flash页擦除逻辑全部手写。你不需要懂RTOS调度原理,但必须清楚NVIC中断向量表怎么搬、SysTick怎么重装、Bootloader如何判断App是否合法——这些不是理论题,是每次升级后设备能不能亮灯的关键。如果你正在为产线设备升级发愁,或者手头有个即将量产的项目还在用串口手动烧录,那么这篇内容就是你今天最该花两小时读完的东西。它不讲抽象概念,只讲怎么让一块10块钱的STM32F103,在没有外部Flash、没有Wi-Fi模组、甚至只有RS232串口的情况下,实现真正可靠的空中升级。

2. AB分区设计与Bootloader核心逻辑拆解

2.1 为什么必须是AB分区?单分区OTA的致命缺陷在哪

很多人以为OTA就是把新固件通过串口传进来,擦掉旧代码,再写进去。这种单分区方案在STM32F103上极其危险,原因有三:第一,Flash擦除是以页为单位的(F103是1KB/页),而你的App固件可能跨多个页。如果升级过程中断电,比如刚好擦完第3页、正写第4页时市电跳闸,结果就是第1-2页是旧代码、第3页全0xFF、第4页是半截新代码——MCU复位后从0x08000000开始执行,第一条指令就是0xFF,直接触发HardFault,设备永久瘫痪。第二,F103的Flash没有ECC纠错,单bit翻转就会导致指令解码错误,而长时间运行的工业设备恰恰容易受电磁干扰影响。第三,没有回滚机制:新固件存在兼容性Bug(比如某个ADC采样率配置错导致传感器读数全0),用户无法一键退回上一版。AB分区正是为解决这三点而生:它把Flash划分为两个等大的区域(A区和B区),当前运行的App总是在A区,OTA升级时新固件写入B区,校验通过后仅修改一个标志位,下次复位时Bootloader就读取B区启动。即使B区写入失败,A区原始代码毫发无损,设备照常运行。这里的关键在于“标志位”的存储位置——不能放在RAM里(掉电即失),也不能放在Flash的任意页(擦写寿命有限)。我实测过,把标志位存在Option Bytes的User Option Byte区域最稳妥,因为它是独立于主Flash的寄存器,支持10万次擦写,且复位后可直接读取。具体操作是调用FLASH_OBProgram()函数写入,而不是去碰主Flash地址。很多教程把标志位存在0x0800F000这样的末尾地址,结果产线跑半年后发现升级失败率飙升,就是因为那一页Flash已接近擦写寿命极限。

2.2 Bootloader的启动流程:从复位到跳转的每一步都得亲手控制

STM32F103的启动流程是理解AB分区的基础。芯片复位后,硬件会从0x00000000地址取初始栈顶指针(MSP),从0x00000004取复位向量地址。但这个0x00000000并不是Flash起始地址0x08000000,而是由BOOT0/BOOT1引脚状态决定的映射关系。在AB分区方案中,我们强制BOOT0=0,让芯片永远从主Flash启动,然后在0x08000000处放置Bootloader代码。Bootloader的职责非常明确:检查A/B区App有效性 → 决定启动哪个区 → 搬运中断向量表 → 跳转执行。这里最容易被忽略的是中断向量表搬运。F103的中断向量表默认在0x08000000,但你的App可能编译在0x08004000(A区起始),此时App里的中断服务函数地址都是相对于0x08004000计算的。如果不把向量表复制到RAM中并设置SCB->VTOR寄存器指向它,一旦App里触发SysTick或USART中断,CPU还是会去0x08000000找ISR,结果执行Bootloader的代码,造成不可预测行为。我的做法是:在Bootloader中分配256字节RAM(足够放前64个向量),用memcpy把App首地址的256字节拷贝过去,再执行SCB->VTOR = (uint32_t)0x20000000;(假设RAM起始地址是0x20000000)。注意,这个RAM地址必须是你Bootloader链接脚本里定义的、未被占用的区域,我习惯用0x20000000~0x200000FF,因为F103C8T6的SRAM只有20KB,前面16KB留给App,后面4KB留给Bootloader专用。另外,跳转前必须关闭所有中断(__disable_irq()),清空流水线(__DSB()),否则跳转瞬间可能执行残留指令。这些细节在ST官方AN2606文档里提过,但没强调后果——我曾因漏掉__DSB(),导致跳转后第一个SysTick中断延迟了整整3个周期,温度采集数据全乱。

2.3 A/B区地址规划:如何在64KB Flash里挤出安全空间

STM32F103C8T6的Flash容量是64KB,表面看很充裕,但实际可用空间远小于此。Bootloader本身需要至少8KB(含USB DFU支持、CRC校验、串口协议解析),App通常要预留16KB用于未来功能扩展,剩下40KB才是A/B区的总和。我的经验是:A区和B区不必严格等大。因为生产固件(A区)通常比OTA包(B区)大——OTA包只包含差分更新或压缩后的bin文件。所以我把A区定为0x08002000~0x0800BFFF(40KB),B区定为0x0800C000~0x0800FFFF(16KB),中间留出0x0800A000~0x0800BFFF作为“黄金备份区”。这个设计有三重保险:第一,当B区写入失败时,Bootloader可从A区复制一份完整镜像到B区(需用户触发);第二,若A区损坏(如静电击穿),Bootloader检测到无效签名后,自动从黄金备份区恢复;第三,黄金备份区本身也存一份Bootloader备份,防止主Bootloader被误擦除。地址规划必须体现在链接脚本(.ld文件)里。以A区为例,其SECTIONS段应写为:

MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K FLASH_A (rx) : ORIGIN = 0x08002000, LENGTH = 40K FLASH_B (rx) : ORIGIN = 0x0800C000, LENGTH = 16K } SECTIONS { .text_a : { *(.text_a) } > FLASH_A }

关键点在于.text_a段名必须与App工程的__attribute__((section(".text_a")))声明匹配,否则链接器会把代码塞进默认区域。我见过太多人因为没改链接脚本,结果App代码被链接到0x08000000,直接覆盖Bootloader——烧录后芯片变砖,只能用J-Link的SWD模式强制擦除。

3. 核心细节解析与实操要点

3.1 CRC32校验:为什么不能只校验App头部,而要整片扫描

OTA升级最怕“假成功”:Bootloader收到20KB数据,计算CRC值匹配,就认为传输完成,结果实际只收到了19KB,最后1KB是Flash擦除后的0xFF填充。这种情况下App启动必然崩溃。因此,校验必须覆盖整个A/B区的有效范围,而非仅头部。F103没有硬件CRC模块(那是F4系列才有的),必须用软件实现。我采用的CRC32算法是IEEE 802.3标准,初始值0xFFFFFFFF,多项式0x04C11DB7。但重点不在算法本身,而在校验时机和范围。正确流程是:接收完所有OTA数据 → 写入B区Flash →逐页读取B区已写入的所有页→ 累计计算CRC → 与接收到的CRC32值比对。注意“逐页读取”:不能一次性读整个B区,因为F103的Flash读取速度慢,且RAM不够缓存16KB数据。我的做法是分页读取(每页1KB),每读一页就更新CRC值,这样内存占用恒定为1KB。另外,校验前必须确认B区所有页都已成功擦除。擦除失败时Flash页内容不变,仍为旧数据,会导致CRC值错误。因此,在擦除B区后,必须读取每页首字节,确认为0xFF,否则报错退出。这个细节在ST的AN2567里没提,但我在某环境监测项目中就遇到过:产线工人用劣质USB转串口线,升级时偶发数据丢失,但Bootloader误判为擦除失败,直接跳过校验,结果设备启动后跑的是半截旧固件。

3.2 串口OTA协议设计:如何用最简指令实现可靠传输

既然标题强调“从零复现”,就不能依赖ESP32那种成熟的HTTP OTA。F103资源有限,必须设计极简协议。我的方案只有4条指令:

  • AT+OTA_START:通知Bootloader准备接收,返回OK后开始发数据
  • AT+OTA_DATA,<offset>,<len>:发送指定偏移和长度的数据块,长度≤256字节(避免串口缓冲区溢出)
  • AT+OTA_CRC,<crc32>:发送最终CRC值
  • AT+OTA_END:触发校验和跳转

协议关键在于超时机制。串口接收中断里,我用SysTick定时器维护一个rx_timeout变量,每次收到字节就清零,超时(设为500ms)则丢弃当前数据块。这样即使上位机卡死,Bootloader也不会一直等待。另一个重点是流控。F103的USART没有硬件RTS/CTS,全靠软件握手。我在AT+OTA_DATA响应中加入ACK/NACK:成功写入Flash返回+ACK:<page_num>,失败返回-NACK:<error_code>。上位机收到NACK必须重发该页。错误码定义为:0x01=Flash写保护开启,0x02=地址越界,0x03=页未擦除。这套协议实测在115200bps下,16KB固件升级成功率99.97%(1000次测试失败3次,均为外部电源波动导致)。比某些商用方案还稳,因为没加任何冗余字段,减少了出错概率。

3.3 Bootloader与App的协同:向量表、堆栈、时钟的无缝交接

跳转不是((void (*)(void))app_addr)();一句就能搞定的。App启动前,Bootloader必须完成三项清理工作:第一,重置SysTick。F103的SysTick寄存器在复位后不会自动清零,Bootloader里可能用过它做延时,若不清零,App的HAL_Delay()会立即超时。正确做法是SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0;。第二,恢复时钟。Bootloader为了省电可能把HSI关了,切到MSI,但App需要HSI或HSE。必须在跳转前调用RCC_DeInit(),让RCC回到复位状态,再由App自己初始化时钟。第三,清理NVIC。Bootloader注册的中断(如USART1_RX)必须取消,否则App的同名中断可能被屏蔽。调用NVIC_DisableIRQ(USART1_IRQn); NVIC_ClearPendingIRQ(USART1_IRQn);。最隐蔽的坑是堆栈指针。ARM Cortex-M3要求跳转前MSP必须指向有效的RAM地址,且该地址不能是Bootloader的栈空间(否则App运行时会覆盖Bootloader变量)。我的解决方案是:在Bootloader的.data段后静态分配1KB RAM作为App专用栈,跳转前执行__set_MSP((uint32_t)&app_stack_top);app_stack_top定义为uint32_t app_stack[256] __attribute__((section(".app_stack")));,链接脚本里确保它位于RAM末尾。这样App的main()函数拿到的就是干净的栈空间。

4. 实操过程与核心环节实现

4.1 开发环境搭建:Keil MDK v5.27 + 标准库v3.50的精准配置

别用最新版Keil或STM32CubeIDE——它们默认生成HAL库工程,与本教程目标相悖。必须用Keil MDK v5.27(2020年发布,对标准库支持最完善),配合官网下载的“stm32f10x_stdperiph_lib.zip”(v3.5.0)。解压后,将Libraries\CMSIS\Device\ST\STM32F10x\Source\Templates\arm\startup_stm32f10x_md.s复制到工程目录,这是MDK专用启动文件。关键配置在Target选项卡:Xtal(MHz)填8(对应外部晶振),Pack里勾选STM32F1xx_DFPDebugJ-LINK/J-TRACEUtilities里Flash下载算法选STM32F10x High Density(C8T6属于High Density)。最容易错的是Output设置:勾选Create HEX File,但不要勾选Use Memory Layout from Target Dialog,否则链接脚本会被忽略。必须手动在Options for Target → Linker → Use Memory Layout from File里指定你的stm32f103c8t6.ld文件。这个ld文件要精确到字节:Flash_A的LENGTH必须是40*1024=40960,不能写40K;RAM的ORIGIN必须是0x20000000,LENGTH=20480(20KB)。我曾因LENGTH写成0x5000(20480的十六进制),导致链接器把App代码塞进RAM,烧录后直接硬启。

4.2 Bootloader工程实现:从main到跳转的237行核心代码

Bootloader的main.c必须极度精简。以下是我经过6个项目验证的核心框架(已去除注释,保留关键逻辑):

#include "stm32f10x.h" #include "stm32f10x_usart.h" #include "stm32f10x_flash.h" #define APP_A_ADDR 0x08002000 #define APP_B_ADDR 0x0800C000 #define APP_STACK_TOP 0x20004FFC // RAM末尾-4字节 uint32_t g_app_crc = 0; uint8_t g_rx_buffer[256]; volatile uint16_t g_rx_len = 0; void SystemInit(void) { RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET); RCC_SYSCLKConfig(RCC_SYSCLKSource_HSE); RCC_HCLKConfig(RCC_HCLK_Div1); RCC_PCLK2Config(RCC_PCLK2_Div1); RCC_PCLK1Config(RCC_PCLK1_Div2); RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET); RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); } void USART1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); } uint32_t CalculateCRC32(uint32_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 < 32; j++) { if (crc & 0x80000000) crc = (crc << 1) ^ 0x04C11DB7; else crc <<= 1; } } return crc; } void JumpToApp(uint32_t app_addr) { uint32_t *app_vector = (uint32_t*)app_addr; uint32_t app_msp = app_vector[0]; uint32_t app_reset_handler = app_vector[1]; __disable_irq(); SCB->VTOR = app_addr; __set_MSP(app_msp); ((void (*)(void))app_reset_handler)(); } int main(void) { SystemInit(); USART1_Init(); // 检查Option Bytes中的启动标志 uint16_t boot_flag = OB_GetUserOptionByte(); if ((boot_flag & 0x0001) == 0) { // 启动A区 if (*(uint32_t*)APP_A_ADDR != 0xFFFFFFFF) { JumpToApp(APP_A_ADDR); } } else { // 启动B区 if (*(uint32_t*)APP_B_ADDR != 0xFFFFFFFF) { JumpToApp(APP_B_ADDR); } } // 进入OTA等待循环 while (1) { if (g_rx_len > 0) { // 解析AT指令,处理OTA_DATA等 // ...此处省略200行协议解析代码... if (ota_complete && crc_ok) { OB_ProgramUserOptionByte(0x0000); // 清除B区启动标志 NVIC_SystemReset(); } } } }

这段代码的精髓在于:SystemInit()里强制使用HSE晶振(不是内部HSI),因为App可能依赖精确时钟;JumpToApp()SCB->VTOR = app_addr是向量表重定位的关键;OB_ProgramUserOptionByte(0x0000)写入0表示下次启动A区,这是AB切换的开关。所有Flash操作(擦除、写入)都封装在FLASH_ErasePage()FLASH_ProgramWord()里,调用前必须FLASH_Unlock(),结束后FLASH_Lock()。我特意没贴完整协议解析部分,因为那是业务逻辑,而上面237行是所有AB OTA项目的骨架——只要这个骨架稳,上层协议怎么变都不影响核心可靠性。

4.3 App工程配置:如何让App“忘记”自己是被Bootloader加载的

App工程看似简单,实则陷阱最多。首要原则:App的链接地址必须与Bootloader约定的A/B区地址严格一致。在Keil的Target选项卡里,ROM Region填IROM1 0x08002000 0x0000A000(40KB),IRAM Region填IRAM1 0x20000000 0x00004000(16KB)。Startup文件必须用startup_stm32f10x_md.s,且其中Stack_Size设为0x400(1KB),Heap_Size设为0x200(512字节)——因为Bootloader已占用了RAM前16KB,App只能用后面4KB。最关键的是main()函数入口处理。标准库的main()会调用SystemInit(),但Bootloader已配置好时钟,重复初始化可能导致PLL锁相失败。我的做法是在App的main.c开头插入:

#ifdef BOOTLOADER_MODE // 跳过SystemInit,直接初始化外设 #else SystemInit(); #endif

并在Bootloader跳转前,通过写一个全局变量(如*(uint32_t*)0x20000000 = 0xDEADBEEF;)来标记启动模式。App启动时读取该地址,若值匹配则跳过SystemInit()。这样既保证了时钟稳定性,又避免了Bootloader与App的时钟配置冲突。另一个细节是中断优先级分组。Bootloader用NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0),App必须用相同分组,否则中断嵌套会出错。我在App的main()开头第一句就是NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);,宁可多写一次,也不赌运气。

5. 常见问题与排查技巧实录

5.1 升级后设备不启动:五步定位法

这是最高频问题,按以下顺序排查,90%能解决:

  1. 检查J-Link连接状态:用J-Flash Lite打开,看能否读出Flash内容。若显示“Cannot connect to target”,说明Bootloader已损坏或SWD引脚被重定义。此时需短接BOOT0=1,用J-Link强制擦除整个Flash。
  2. 验证向量表首地址:用J-Flash读取0x08002000和0x0800C000的前4字节(MSP)和次4字节(Reset Handler)。正常值应为非0xFFFFFFFF,且Reset Handler地址应在对应区域内。若为0xFFFFFFFF,说明App未成功写入。
  3. 确认Option Bytes:在J-Flash的Option Bytes页,查看User Option Byte的Bit0。若为1,表示启动B区;若B区无效,设备会卡在Bootloader。此时需用J-Flash写入0x0000恢复A区启动。
  4. 检查RAM分配冲突:用Keil的View → Memory Windows,输入0x20000000,看App的.data段是否覆盖了Bootloader的栈空间。若覆盖,调整App的IRAM Region起始地址。
  5. 抓取串口日志:在Bootloader的USART1_IRQHandler里添加printf("RX:%02X\r\n", RxData);,用串口助手看是否收到数据。若无输出,检查USART1的GPIO时钟是否开启(RCC_APB2Periph_GPIOA必须使能)。

提示:我自制了一个“OTA诊断卡”,用STM32F103最小系统+OLED屏,烧录专用诊断固件。它能自动读取Option Bytes、扫描A/B区CRC、显示启动标志,3秒内定位90%问题。比用J-Link手动查快10倍。

5.2 CRC校验失败:数据传输链路的七层排查

CRC失败不一定是算法错,往往是链路问题:

层级检查项工具/方法典型现象
物理层USB转串口线质量换原装线升级中途卡死,串口助手显示乱码
驱动层CH340驱动版本卸载重装v3.4115200bps下丢包率>5%
协议层AT指令结尾符抓包看是否\r\n返回OK但无后续响应
Flash层页擦除完整性J-Flash读取B区每页首字节某页首字节非0xFF,说明擦除失败
校验层CRC计算范围对比Bin文件hexdump计算范围少1字节,CRC必错
时钟层SysTick精度示波器测USART波形波特率误差>3%,导致误码
电源层VDD纹波示波器测VDD-GND纹波>100mV时,Flash写入失败率飙升

最隐蔽的是时钟层问题。F103的HSE晶振若负载电容不匹配(应为12pF),实际频率偏差可达±0.5%,导致115200bps通信误码。我的解决方案是:在Bootloader里用RCC_GetSYSCLKSource()确认时钟源,若非HSE则强制复位,避免用不准的时钟跑OTA。

5.3 AB分区切换失败:标志位失效的三种场景

启动标志存在Option Bytes,但并非万能:

  • 场景1:Option Bytes写保护开启。ST出厂默认关闭写保护,但某些量产工具会开启。用J-Flash的Option Bytes页,确认nWRP字段为0xFFFF(未写保护)。
  • 场景2:电压不足导致写入失败。Option Bytes编程需VDD≥2.7V,若用电池供电且电量低于3.0V,OB_WaitForLastOperation()会超时。我在Bootloader里加了电压检测:if (ADC_GetConversionValue(ADC1) < 1000) { /* 电压低警告 */ }
  • 场景3:Flash寿命耗尽。Option Bytes擦写次数有限(10万次),若频繁切换AB区,可能提前失效。我的对策是:每次切换后记录次数到EEPROM模拟区(用Flash最后一页),超过5万次就报警,提示更换设备。

注意:绝对不要用FLASH_ProgramHalfWord()往Option Bytes地址写数据!必须用FLASH_OBProgram()专用函数,否则会锁死芯片。

6. 扩展与优化:从基础OTA到工业级升级方案

6.1 差分升级:如何把16KB固件压缩到2KB传输

全量OTA在窄带信道(如GPRS)上太慢。差分升级只传新旧固件的差异部分。我的方案基于bsdiff算法,但针对F103做了裁剪:预先把旧固件(A区)的SHA256哈希存入Bootloader的保留区,OTA时上位机用bsdiff生成patch文件,Bootloader收到后用bpatch算法在RAM中重构新固件,再写入B区。整个过程RAM占用<8KB,时间<3秒。关键优化是:patch文件分块传输,每块附带CRC,支持断点续传。实测在9600bps下,16KB固件升级时间从120秒降至18秒。

6.2 安全加固:防刷机、防逆向的三道防线

工业设备必须防篡改:

  • 防线1:签名验证。Bootloader用ECDSA算法验证App签名,私钥存在安全芯片(如ATECC508A),公钥固化在Bootloader中。即使固件被dump,没有私钥无法生成有效签名。
  • 防线2:调试接口锁定。量产时用J-Link执行MEM WRITE32 0x1FFFF800 0x5AA5,永久禁用SWD,物理上剪掉SWD引脚。
  • 防线3:内存加密。App运行时,关键算法(如Modbus CRC计算)的代码段从Flash加载到RAM后,用AES-128实时解密执行,执行完立即清零。这需要额外2KB RAM,但能阻止静态分析。

6.3 多协议支持:不止串口,还有CAN和LoRa

F103的CAN控制器完全可用于OTA。协议帧ID设为0x7FF(高优先级),数据域放OTA_DATA指令。优势是抗干扰强,适合工厂车间。LoRa方案更激进:用SX1276模组,自定义轻量协议,128字节/包,ACK重传机制。我实测在3km距离下,16KB固件升级成功率92%,比GPRS稳定得多——因为LoRa的扩频特性天然抗窄带干扰。

我在最后一个项目里,把AB分区OTA、差分升级、ECDSA签名、CAN传输全部集成进一个64KB的Bootloader,留给App的空间仍有32KB。这证明F103的资源完全够用,关键是你敢不敢亲手拆解每一个寄存器、每一行汇编、每一个Flash页。现在你手边的开发板,已经不是一块学习板,而是一台能自我进化的工业终端。下次升级,不用再拔电源、插烧录器、等3分钟——敲个命令,喝口咖啡,设备就完成了蜕变。这才是嵌入式开发该有的样子。

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

栈的实现与选型:数组栈与链表栈的原理、复杂度及工程实践

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

作者头像 李华
网站建设 2026/9/11 11:46:12

SAP传输请求管理:核心类型与跨系统传输实践

1. SAP系统间传输请求概述 在SAP系统环境中&#xff0c;传输请求&#xff08;Transport Request&#xff09;是系统变更管理的基础单元。作为SAP项目实施和运维的核心机制&#xff0c;它记录了从开发系统到测试系统再到生产系统的所有配置变更、程序开发和数据调整。我经历过多…

作者头像 李华
网站建设 2026/9/11 11:45:37

2026年iOS开发选型与工具链全解析:从原生到跨平台,绕开上架坑

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

作者头像 李华
网站建设 2026/9/11 11:44:42

G-Helper 完全教程:如何给华硕笔记本换上轻量级性能控制中心

G-Helper 完全教程:如何给华硕笔记本换上轻量级性能控制中心 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops with nearly the same functionality. Works with ROG Zephyrus, Flow, TUF, Strix, Scar, ProArt, Vivobook, Zenbook, Expertb…

作者头像 李华