ARTICLE DETAIL

资讯详情

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

STM32F103C8T6远程OTA实战:Bootloader与AI辅助开发

STM32F103C8T6远程OTA实战:Bootloader与AI辅助开发 前阵子清理工位翻出一块吃灰的STM32F103C8T6最小系统板。这块蓝板当年买也就十来块钱芯片64KB Flash、20KB SRAM听起来寒酸但正是这种“小芯片”才逼着人把每个字节都规划到位。我给自己定的目标是不接ST-Link不拆设备光靠手里这台电脑甚至一部手机就能把板子上的固件换成新版本。直白说就是给F103C8T6加上OTA远程升级能力。做着做着我发现现在AI编程工具确实能顶大用Bootloader、IAP协议、Flash擦写这些基础模块让AI先生成初版我再逐行审查修改开发速度比纯手写快了一大截。这篇内容就完整记录这个“能远程换固件”的AI编程STM32F103C8T6小项目是怎么从0到1跑通的里面涉及的分区设计、升级协议、固件加密校验、ESP8266透传、AI提示词写法和各种翻车现场如果你也在折腾这块芯片应该能直接抄作业。1. 项目整体设计与思路拆解1.1 为什么我选了F103C8T6做OTA先说硬件选型。STM32F103C8T6这颗芯片在圈内属于国民级入门型号资料多到溢出来从最小系统板原理图、引脚功能表到Keil4/Keil5下建立工程、标准库工程模板随便一搜就是成堆现成教程。正因为资料密度高做OTA这种进阶项目时踩了坑能快速找到排查参考不会卡死在环境问题上。但这颗芯片也有一堆限制。64KB FlashBootloader占掉一部分后留给APP的只有不到56KB没有硬件AES加密引擎固件加密得靠软件实现没有以太网MAC远程链路只能外挂模块。这些限制听起来是短板换个角度看反而是好事。如果你想把OTA能力带到量产项目里分区规划、空间抠算、协议精简这套经验是完全通用的能用64KB小容量芯片把OTA跑稳换到256KB、512KB的芯片只会更从容。另外我特别看重C8T6的复现门槛。蓝板几块钱一片CH340或CP2102的USB转串口直接就能连电脑不需要额外的调试器。新手拿到手就能开始搞不用在硬件搭建上消耗太多精力可以把所有注意力集中在固件升级这套软逻辑上。1.2 “远程换固件”到底在换什么远程换固件的正式叫法是IAPIn Application Programming也就是常说的OTA升级。传统玩法是用ST-Link/J-Link直接烧录但产品一旦装进外壳、部署到现场拆机换固件的成本会非常高。OTA解决的就是这“最后一公里”的交付问题。要跑OTAFlash里至少要有两个程序Bootloader引导程序。上电后不跑业务先检查升级标志、版本号、固件完整性然后决定是跳进APP正常干活还是留在原地接收新固件。APP应用固件。真正干活的程序比如点灯、跑电机、读传感器。两个程序分别编译、分别烧录各自占据Flash的一个区域。用个生活类比Bootloader像机场廊桥的调度员平时航班到了就引导乘客登机也就是跳到APP如果临时要换飞机也就是升级固件调度员先拦下乘客等新飞机就位、检查无误后再放行登机。这个结构听起来简单真正做扎实全是细节比如分区边界怎么划、串口帧协议怎么定、升级中途断电怎么办、CRC不过如何回退。后面每个问题都会展开。1.3 AI编程在这个项目里的真实分工最近一年让AI写代码已经成了我默认的开发方式尤其适合嵌入式里模板感极强的基础模块。串口状态机、Flash擦写、CRC32、跳转函数这些代码网上漫天都是但每家的芯片型号、库版本、管脚映射、Flash布局都不一样。把差异参数整理清楚后丢给AI它会在一分钟内给出初版我再对着芯片手册和工程环境逐行审查修改。我用的是在线免费大模型加付费AI编程工具混着来。付费工具能直接读整个工程上下文改代码、查编译报错、做重构效率更高免费模型更吃提示词工程对提示词的颗粒度要求比较高。这个项目的核心提示词模板我放在后面章节直接抄就能用。但AI生成嵌入式代码有个明显的坑它对具体芯片细节记忆得并不可靠。让它写F103C8T6它可能默认Flash页大小是2KB那是大容量F103才有的参数可能默认用HAL库而你的工程跑的是标准外设库V3.5可能默认中断向量表偏移已经配好实际工程里压根没动。任何一个假设偏差都可能导致真机上跑不起来甚至把Bootloader区擦掉。所以在这个项目里我对AI的定位始终是“提效助手”不是“免审代工”AI输出的每一行关键代码都要经过人工核验。2. 系统架构与Flash分区设计2.1 C8T6的64KB Flash怎么分才够用STM32F103C8T6的Flash起始地址是0x08000000总大小0x10000也就是64KB中容量产品每页1KB。我的分区方案非常简单直观区域地址范围大小作用Bootloader区0x08000000 – 0x08001FFF8KB引导、IAP协议解析、串口/WiFi驱动、Flash擦写、跳转逻辑APP区0x08002000 – 0x0800FFFF56KB应用固件主体参数区0x0800FC00 – 0x0800FFFF1KB版本号、升级标志位、CRC结果等运行参数Bootloader分8KB而不是4KB是为了调试期不自找麻烦。IAP协议解析加上串口驱动、Flash擦写和跳转逻辑用标准库编译下来通常有5到6KB8KB留了余量。如果你非要把Bootloader压到4KB也能做但协议稍微复杂一点就会溢出调试体验会很痛苦。如果你追求现场升级失败后能自动回滚可以用A/B方案Bootloader 8KB APP A区48KB APP B区48KB这里不行C8T6只有64KB算一下就知道了所以更实际的做法是Bootloader 8KB APP区48KB 备份区8KB。新固件先写入备份区CRC校验通过后复制覆盖APP区或者让Bootloader根据升级标志决定启动哪个区。代价是APP可用空间只有48KB。我的建议是先跑856的简单单区方案把链路搞稳定再考虑A/B回滚小容量芯片先把地基打牢。APP工程侧也必须同步改配置。Keil里要打开Options for Target在Target页把IROM1的起始地址改成0x08002000大小改成0xE00056KB。STM32CubeIDE则是改Linker Script里Flash的origin和length。同时APP主程序里要设置中断向量表偏移标准库直接用SCB-VTOR 0x08002000。新手最容易在这里翻车Bootloader写好了、跳转函数也写了但APP的中断向量表还指向0x08000000一进中断就跑去读Bootloader区域直接HardFault。2.2 升级协议与流程设计OTA要稳定通信双方必须先约定一套协议。我用的是一套极简帧格式串口和WiFi透传通用字段长度说明帧头2字节0xAA 0x55用于同步识别命令字1字节0x01查询版本 / 0x02开始传输 / 0x03数据传输 / 0x04传输结束包序号2字节大端序从0开始递增用于重传和排序数据长度2字节有效数据长度单包最多128字节数据可变固件分包内容CRC324字节对整个帧计算的CRC32低字节在前数据长度限定128字节是经过权衡的。C8T6只有20KB SRAM缓冲区不能开太大同时ESP8266透传链路本身不稳定小包在出错重传时的代价更低。后期你当然可以把单包加到256字节甚至512字节配上ACK确认机制吞吐量会更高但新手期先用128字节最不容易出问题。升级流程本质是一条状态机空闲状态收到0x02开始传输命令进入接收模式循环接收0x03数据帧每收到一帧就往Flash里写一页收到0x04结束帧后对整包固件做CRC32校验校验通过置升级完成标志延时后软复位跳转到APP。整个过程里通信双方还必须对版本号达成共识。我在参数区预留了4字节存版本号Bootloader启动时读出来上报上位机拿到当前版本号之后再决定要不要提示“检测到新固件”。2.3 固件加密、校验与升级标志位固件加密和校验做少了OTA就是给设备开后门。先说校验我不建议只做简单的累加和校验bin文件动辄几十KB个别bit翻转时累加和有可能碰巧相同误判率太高。CRC32基本是嵌入式OTA的标配我用标准多项式0x04C11DB7初值0xFFFFFFFF输出异或值也是0xFFFFFFFF整个bin文件算完把结果放在结束帧里一起下发。加密方面F103C8T6没有硬件AES引擎纯软件AES在72MHz主频下对几十KB固件虽然能跑但代码量大、计算耗时对Bootloader容量和升级时长都不友好。我采用了一层很务实的XOR混淆上位机按密钥序列把bin文件逐字节异或STM32收到数据后先解混淆再写入Flash。这个强度挡不住专业逆向但能挡住大多数人拿串口助手直接抓包、把固件明文dump出来的低级操作对个人项目足够。做成产品的话建议用带硬件加密的芯片或外置安全芯片那是另一个话题。升级标志位同样关键。我在参数区存了4字节魔数0xA5A5F00D表示APP有效可以跳转0xDEADBEEF表示升级未完成。Bootloader每次上电先读这个魔数如果不对就不跳APP而是进入接收模式等待新固件。有了这个设计升级中途断电、传输失败设备重启后仍然停在Bootloader不会变成一头跑不起来又烧不进去的“半砖”。这里也有个进阶方案是用BKP寄存器存标志位但BKP依赖VBAT持续供电一旦掉电信息就丢我只把它当成备份逃生方案主力方案还是放在Flash参数区可靠且直观。3. AI辅助开发提示词写法、代码生成与人工核验3.1 嵌入式场景的AI编程提示词怎么写AI编程能不能在嵌入式里发挥作用关键就看提示词写得到不到位。笼统问一句“帮我写个OTA程序”得到的答案基本不可用但把下面的参数一次性喂进去产出质量会有本质差别芯片型号STM32F103C8T6中容量64KB FlashFlash页大小1KB开发环境Keil MDK5 STM32标准外设库V3.5串口USART1PA9/PA101152008N1Flash布局Bootloader起始0x08000000APP起始0x08002000参数区0x0800FC00协议定义把上一章的帧格式表原样贴进去明确要求串口中断接收状态机、边收边缓冲、分包写Flash、CRC32整包校验、跳转函数下面这段提示词开头可以直接抄我为STM32F103C8T6编写基于标准外设库V3.5的IAP Bootloader。芯片Flash 64KB页大小1KB串口1接收波特率115200。固件通过帧头0xAA 0x55、命令字包序号长度数据CRC32的协议分包传输单包数据长度128字节。请用C语言实现串口接收状态机要求边收边缓冲到SRAM每收到一个完整帧就调用Flash写入函数写入目标地址从0x08002000开始累加传输完成后等待结束帧并做CRC32整包校验同时提供Flash擦写函数和跳转到APP的函数跳转前需校验向量表合法性、关闭中断、设置SCB-VTOR。AI给出初版之后我还会做三件事。第一逐行检查寄存器操作重点看SCB-VTOR、FLASH_ErasePage参数和地址范围判断第二把库函数名和头文件包含关系对着工程里的标准库源码核对第三直接编译把每个warning都当成潜在问题过一遍。做完这一步AI的产出才真正属于我的项目。3.2 Bootloader跳转代码AI生成后我改了什么AI生成的跳转函数典型长这样void jump_to_app(uint32_t app_addr) { uint32_t app_sp *(volatile uint32_t *)app_addr; uint32_t app_pc *(volatile uint32_t *)(app_addr 4); if (((app_sp 0xFFF00000) ! 0x20000000) || ((app_pc 0xFFF00000) ! 0x08000000)) { return; } __disable_irq(); SCB-VTOR app_addr; __set_MSP(app_sp); ((void (*)(void))app_pc)(); }这段代码本身基本能跑但直接搬进我的项目之前我改了三个地方。第一函数开头加了外设复位。Bootloader里我打开了串口NVIC可能还挂着中断状态直接关中断跳走外设的中断挂起标志还留在那APP初始化时一开外设就可能触发异常。稳妥做法是在跳转前调用RCC_DeInit()把时钟配置复位再关中断、设置VTOR、跳转。第二判断条件里我额外检查了app_pc低位的Thumb位。Cortex-M3必须保持Thumb状态如果取出的地址是偶数说明向量表已经损坏直接拒绝跳转避免跑飞。第三跳转前把SysTick停掉。Bootloader如果用了延时函数SysTick的倒数计数还开着APP里重新配置SysTick时可能出现旧回调残留。顺手把SysTick-CTRL清零能少踩一个很隐蔽的坑。这些差异网上教程很少系统讲全是真机调试时一个异常一个异常试出来的。3.3 串口IAP接收流程与Flash读写实现串口接收这块我用状态机而不是简单的中断攒数组。串口中断只负责把字节塞进环形缓冲区主循环里逐字节解析帧。这样串口波特率再高、Flash在擦写、WiFi偶发乱序都不容易丢数据核心逻辑类似这样uint8_t rx_buf[256]; uint16_t rx_index 0; uint8_t parse_state 0; while (ring_buffer_not_empty()) { uint8_t b ring_buffer_get(); switch (parse_state) { case 0: if (b 0xAA) parse_state 1; else parse_state 0; break; case 1: if (b 0x55) parse_state 2; else parse_state 0; break; // 后续case依次解析命令字、包序号、长度、数据内容 default: rx_buf[rx_index] b; if (rx_index expected_len) { handle_frame(rx_buf, expected_len); rx_index 0; parse_state 0; } break; } }这是示意代码实际工程里帧头同步、超时复位、长度越界校验都要补齐。Flash写入有一条必须严格遵守的纪律写入之前必须先擦除整页标准库的FLASH_ErasePage参数是页首地址C8T6每页1KB。AI很容易在这里写出2KB页参数那是大容量F103的参数肉眼看起来没问题真机上一擦就把相邻页面的代码干掉了。擦写期间要关中断或者至少把数据先完整缓存到RAM里再统一写Flash。千万不要在Flash操作中间开着串口中断接收后续包同一时刻芯片只有一个Flash控制器在工作操作被打断轻则BSY等待异常重则数据错乱。void flash_write_page(uint32_t addr, uint32_t *buf, uint16_t len) { FLASH_Unlock(); while (FLASH_GetFlagStatus(FLASH_FLAG_BSY) SET); FLASH_ErasePage(addr); for (uint16_t i 0; i len; i 4) { while (FLASH_GetFlagStatus(FLASH_FLAG_BSY) SET); FLASH_ProgramWord(addr i, buf[i / 4]); } FLASH_Lock(); }写入时还要做地址范围检查目标地址必须在APP区内防止越界写进Bootloader区。如果开了Flash写保护Bootloader区是根本擦不掉的这点后面安全部分会一起讲。4. 远程链路搭建与一次完整OTA实测4.1 从串口升级到ESP8266 WiFi透传先把有线串口IAP彻底调通再上无线。这个顺序千万别反过来否则协议问题和无线问题混在一起排查起来会非常痛苦。有线阶段很简单USB-TTL接PA9/PA10115200波特率直接把bin文件通过串口发给Bootloader验证整套接收、擦写、校验、跳转逻辑没问题再进入无线阶段。无线阶段我用的是ESP8266刷标准AT固件通过AT指令做WiFi透传ATCWMODE1 ATCWJAPMyWiFi,password ATCIPSTARTTCP,192.168.1.100,8080 ATCIPMODE1 ATCIPSEND这里有个很容易忽略的坑。ESP8266在透传模式下所有从串口进来的数据不加修饰直接进TCPSTM32分不清哪些数据是WiFi链路返回的AT回显、哪些是用户下发的固件数据。所以我用STM32的一个GPIO控制“升级模式”引脚只有收到上位机的开启升级指令后Bootloader才进入透传数据解析状态在透传之外的普通模式下串口数据照常处理不会被AT指令的杂讯干扰。硬件上还有两个注意点。ESP8266的CH_PD/EN脚必须接高电平不能悬空悬空会导致模块不启动或随机重启供电尽量单独用一颗AMS1117从5V转3.3V给ESP8266开发板上的3.3V稳压器输出电流往往不够ESP8266每次发射WiFi都有电流尖峰供电不足轻则丢包重则模块反复重启。我实测时一开始直接吃板子LDO的3.3V隔着一堵墙升级就失败后来换独立供电同样的距离稳定多了。4.2 上位机与手机App的简易发版流程上位机我做了两个场景。PC端用Python写了个不到一百行的脚本流程很简单读bin文件、计算CRC32、把版本号常量写入参数偏移、按协议分包封装成帧、建立TCP连接、逐帧发送并等ACK。Python脚本最大的优点是改协议不用重新编译加个简单的命令行参数就能适应调整。核心发送逻辑可以浓缩成这样fw open(app.bin, rb).read() crc binascii.crc32(fw) 0xFFFFFFFF frames pack_frames(fw, version, crc) for f in frames: tcp.send(f) ack tcp.recv(4) if not check_ack(ack, f.seq): tcp.send(f) # 超时或ACK不匹配重发当前帧这里我做了ACK等待和重发机制每帧带序号ACK返回序号表示接收成功。虽然是半双工慢速协议但配合ESP8266透传这种可变链路可靠性远大于满速狂发。手机端我没有写完整App而是用现成的TCP网络调试工具配一台手机热点就够应急。现场环境里如果笔记本不方便打开手机热点、让ESP8266连热点、再用TCP调试助手发送同样能完成升级。日常验证还是建议用PC脚本手机端手动操作毕竟繁琐。4.3 一次完整OTA升级实测记录记录一次最典型的升级过程。初始APP版本V1.0功能就是点灯每隔两秒通过串口打印一行“APP V1.0 running”。新固件改成V2.0打印内容变成“APP V2.0 running”LED闪烁周期从1秒缩短到300毫秒。我编译后用Keil的后处理命令fromelf生成bin文件整个APP编译出来大概4KB多。Python脚本读取bin算出CRC32和板子上报的V1.0版本比对发现版本号更高就提示确认。点击开始后脚本通过TCP连接发出第一帧ESP8266进入透传STM32的Bootloader开始逐帧接收。这里有个直观体验因为bin文件只有几KB整个传输在局域网内基本是秒级完成串口调试助手会打印出“CRC OK, jump to app…”紧接着系统自动复位。复位之后再看串口输出已经变成“APP V2.0 running”LED闪烁频率也肉眼可见地变了。整个流程大约3秒全程没有动过任何一根Flash烧录线。我把ESP8266连到手机热点STM32拿到另一个房间隔着一堵墙再次升级传输时间稍微变长主要是TCP重传和ACK等待占的时间但最终依然成功。这个场景让我彻底意识到OTA带来的不只是便利而是开发调试模式的转变——设备可以像服务端一样“远程发版”了。5. 常见故障排查与避坑指南5.1 问题与解决速查表把我在这个项目里遇到过的典型问题整理成一张表遇到类似现象可以直接按图索骥现象原因解决办法跳转后死机或进入HardFaultAPP中断向量表偏移没设置或SCB-VTOR未指向APP区APP初始化处加SCB-VTOR 0x08002000检查跳转函数确实执行Bootloader能收包写入后运行仍是旧固件Keil里APP的IROM1起始地址没改成0x08002000确认APP工程Target页IROM1配置重新编译并重新生成bin升级到一半卡死串口不再响应Flash擦写期间被串口中断打断擦写前关中断或先缓存数据Flash操作时不要并发处理串口帧发送完成但CRC校验失败上位机和MCU的CRC参数不一致或大小端字节序对不上统一CRC32多项式、初值、输出异或值确认CRC结果低字节在前ESP8266透传时串口收到乱码进入透传模式后AT指令回显和固件数据混在一起或波特率不匹配用GPIO控制升级模式在升级模式之外不解析接收数据确认ATCIPMODE1成功后再发数据重新上电后进不了APP升级标志位未正确清除或SP/PC范围校验失败检查参数区魔数写入和读取逻辑用串口打印SP/PC实际值确认校验逻辑AI生成的编译报错库函数名写错或工程是HAL库而AI写了标准库函数让AI对照stm32f10x_flash.h、stm32f10x_usart.h等头文件里的实际函数名重写板子断电后升级状态丢失用了BKP寄存器存标志位但VBAT没有接电池改用Flash参数区存标志位BKP方案只保留给有电池备份的场景5.2 几个值得长期保留的实操习惯踩坑踩多了自然沉淀出一些习惯这里挑几条对大部分人都有用的。第一Bootloader不要一上来就写复杂协议。先把跳转函数做好时序调到板上电就能正确跳进APP确认Flash分区和向量表偏移这套地基是稳的再开始叠加协议解析、CRC、WiFi透传这些功能。地基不稳后面所有功能都没法排查。第二每次编译完都顺手生成bin文件且固定放在同一个目录。裸机bin文件要随时准备刷回否则改代码改到一半突然失去基准想回退还得重新配置工程。Keil里在Options for Target的User页添加After Build命令fromelf --bin --output app.bin app.axfCubeIDE用arm-none-eabi-objcopy原理一样。第三给Bootloader加一个低成本看门狗。串口接收超时、写入超时都喂一下狗万一升级流程卡在某个未完成状态看门狗会强制复位把设备从“假死”里拉回来。这个设计成本几乎为零但能让升级失败后的恢复体验上一个档次。第四上位机先读版本号再决定发不发固件。不要每次都不管三七二十一刷一遍那样出了问题根本分不清是升级逻辑的锅还是固件本身的锅。先读后发既省流量又能留出清晰的日志线索。写在最后的想法这套项目做完我最大的体会是两句话。OTA不是“加一个跳转函数”那么简单它是一个完整的开发模式切换Bootloader要做得尽量小、尽量可靠APP要能够容忍升级失败上位机要真正理解协议而不是只会拖拽文件。而AI编程在这件事里的作用是把我从背模板的重复劳动里解放出来让我还有精力去关注那些真正要命的细节Flash页大小、向量表偏移、中断屏蔽、超时重传。如果你也打算在STM32F103C8T6上练这个项目我建议给自己定个硬指标先有线、再无线先单区、再A/B一步一步推。等哪天你可以拿着手机App就给几公里外的设备换固件那种成就感是接一百次ST-Link都换不来的。
返回列表