
前阵子我用AI辅助做了个小项目核心就一件事让一块十几块钱的STM32F103C8T6最小系统板接上ESP8266模块能通过WiFi远程下载新固件、替换掉旧固件整个过程不用插下载器、不用拆机断电重启之后系统跑的还是新程序。这其实就是把手机系统“OTA升级”的思路搬到了单片机上。这个项目做完之后我觉得挺有代表性的正好把标题里的三块东西都串起来了AI编程怎么辅助写嵌入式代码STM32F103C8T6的资源怎么规划才够用以及OTA远程换固件到底是怎么实现的。适合手里有基础STM32开发经验、想试试AI辅助开发流程或者对IAP/OTA感兴趣的朋友参考。做嵌入式的人都知道F103C8T6这颗芯片最大的限制就是Flash只有64KB、RAM只有20KB想在里面塞Bootloader、App、下载缓冲区和回滚机制每一KB都要精打细算。本文我会把整个方案的选型逻辑、分区规划、核心代码、调试踩坑都摊开来讲尽量把“为什么这么做”也说清楚。1. 项目拆解F103C8T6、OTA和AI编程三件事怎么凑到一起的1.1 硬件选型背后的资源账先说为什么选STM32F103C8T6。不是因为它性能强恰恰相反是因为它足够便宜、足够常见、资料足够多。一块最小系统板在电商平台也就十来块钱随便就能买到。对于做OTA方案验证来说这个成本几乎是零门槛的。更关键的是F103C8T6虽然是老芯片但该有的东西都有Cortex-M3内核、72MHz主频、64KB Flash、20KB RAM以及足够用的USART、SPI、I2C、GPIO。这些资源做一个小型OTA系统其实非常紧但也正因为紧才能逼着你把固件体积、分区策略都考虑清楚。连接网络的模块我选了ESP8266而不是ESP32原因其实很朴素ESP8266模块便宜几块钱一个而且AT指令固件成熟稳定走串口和STM32通信非常省心。用ESP8266做HTTP GET请求把固件包拉到串口STM32再一帧一帧接收入RAM缓存。这套逻辑不管是局域网内的HTTP服务器还是云端的对象存储都能通用。很多人会问为什么要多绕一层WiFi模块而不是直接用STM32的以太网因为F103C8T6没有内置以太网MAC外接ENC28J60之类不仅增加成本而且驱动代码量不小。ESP8266用AT指令基本就是串口收发字符串代码负担小很多。选型的时候我一直提醒自己OTA的核心是“升级链路”而不是“炫技”能用最简单可靠的方式把字节搬进Flash就是好方案。1.2 “远程换固件”的本质IAP与双区架构“远程换固件”听起来很高端其实底层原理就是IAPIn Application Programming也就是应用程序在运行过程中通过调用芯片自带的Flash编程接口把新的程序写入Flash的指定区域。关键在于芯片不能把自己正在执行的代码区域随意擦掉否则程序跑飞了谁来做搬运工作所以必须把Flash分成至少两块一块放Bootloader引导程序一块放App应用程序。Bootloader永远不升级自己它只负责两件事第一判断要不要升级第二如果需要升级就从某个数据源读取新固件写入App区域然后跳转执行。App区域才是真正会被反复擦写替换的地方。这套逻辑在PC上其实也有对应物电脑的BIOS/固件引导模块负责检查磁盘里的操作系统是否完整不完整就进入恢复模式。单片机里的Bootloader就相当于那个“固件引导模块”。想通了这一点整个项目的主线就清楚了我并不是在写什么高深算法而是在做一个严格分工的内存搬运系统。而“远程”这个修饰词只是给IAP的“数据源”换了个更灵活的通道——从串口线换成了WiFi网络而已。这个思路一旦理解了后面所有代码都只是落实这个架构。1.3 AI编程在这个项目里到底干了什么活标题里提到“AI编程”很多人会怀疑是不是噱头其实不是。我用AI编程工具干了很实在的事情给它描述需求让它生成STM32标准外设库下的Flash读写函数、跳转函数、CRC校验代码以及ESP8266的AT指令解析框架。AI最大的价值不是替我做架构决策而是把我脑子里的成熟想法快速变成可编译的代码省去了大量查阅手册、敲模板代码的时间。但要说清楚AI不是全知全能的。它生成的代码经常有细节问题比如忽略了F103C8T6的Flash扇区大小、把串口中断和主循环的缓冲区管理搞冲突、CRC算法选型错误等。这个项目我的角色更像是“技术负责人”AI是“实习生”我给它非常具体的任务描述甚至精确到函数名、变量名、参数范围它输出初稿我再逐行review、调试、修正。说句实在话这一套流程下来我对F103的底层结构反而比纯手写更熟了因为AI逼着你必须清晰地表达需求也必须去验证它给的每段代码是否真的能跑。2. 系统架构与Flash分区规划2.1 64KB Flash的“寸土寸金”分配方案F103C8T6的Flash总容量是64KB起始地址0x08000000结束地址0x0800FFFF。Flash按扇区管理每个扇区1KB共64个扇区。注意STM32F103C8T6没有像F103ZE那样的大扇区结构而是统一1KB小扇区这对OTA来说反而是个好消息擦除粒度更细搬运更灵活。我的分区方案如下区域起始地址大小用途Bootloader0x0800000012KB引导程序、OTA接收与搬运逻辑App区0x0800300036KB用户应用程序含中断向量表下载缓存区0x0800C00012KB暂存通过WiFi下载的新固件包参数标志区0x0800FC001KB存储升级标志、版本号等关键参数这个分区方案有几个讲究。第一Bootloader只占12KB因为它的任务很单一不需要塞复杂功能我用标准外设库大概编译出来9KB左右留了余量。第二App区36KB足够放一个常规的中小型应用哪怕加了RTOS、FatFS之类的组件也能塞得下。第三下载缓存区12KB这里存的是完整的新固件包包含包头固件数据Bootloader在搬运之前需要先对整个包做校验校验通过才允许擦App区这个缓存区就是给“先校验后执行”留的余量。第四参数标志区放在Flash最后1KB用来记录“是否需要升级”“当前版本号”“上次升级是否成功”等信息Flash的擦写寿命是1万次但参数区写操作很频繁所以我加了简单的磨损均衡每次写都换一个偏移地址。2.2 OTA升级的完整流程设计整个OTA升级流程是这样的App正常运行期间通过ESP8266向服务器发起HTTP GET请求请求路径形如http://192.168.1.100:8080/firmware/update_v2.bin。服务器返回固件包文件字节流ESP8266通过串口把数据透传给STM32。STM32在App的main函数中接收到数据后将完整固件包写入下载缓存区。每写满一个扇区就擦除一个扇区再写入。接收完成后App在参数标志区写入“升级请求”标志然后执行系统复位NVIC_SystemReset()。Bootloader启动后首先读取参数标志区发现“升级请求”标志则对下载缓存区里的固件包做完整性校验魔数、设备型号、长度、CRC32。校验通过后Bootloader擦除整个App区把下载缓存区里的固件数据逐扇区复制到App区。全部搬运完成后擦除“升级请求”标志把“当前固件版本号”更新为包头里的新版本号。Bootloader跳转到App区起始地址执行新固件。这套流程最核心的设计思想是“解耦升级请求与升级执行”。发起升级的权力在App手里随时可以请求执行升级的动作在Bootloader手里复位后统一处理。好处是升级过程中哪怕App写崩了、断电了重启后Bootloader还在最多就是升级失败不会变成砖。只要Bootloader不坏就永远可以重新下载刷写。2.3 一个极其关键的点中断向量表重映射所有做IAP的人都会在某个深夜被“中断向量表”折磨过。为什么因为Cortex-M3内核的中断向量表默认固定放在0x08000000也就是Flash起始地址。如果App区的程序被链接到0x08003000但中断向量表还留在0x08000000那么App里的任何中断包括定时器中断、串口中断、SysTick都会跑到Bootloader的向量表里去查找处理函数结果就是完全找不到正确的处理函数程序直接进HardFault。解决办法就是在App启动代码里把自己所在的中断向量表地址告诉内核。F103用的是Cortex-M3内核标准做法是修改SCB-VTOR寄存器#define APP_BASE_ADDR 0x08003000 void app_vector_table_init(void) { SCB-VTOR APP_BASE_ADDR; }这行代码必须在App的main函数最开始的地方执行最好在初始化任何外设中断之前。执行之后内核收到中断时就会从0x08003000处读取向量表从而找到App自己的中断处理函数。另外还要注意App工程的链接脚本里Flash起始地址也要改成0x08003000否则编译出来的代码地址还是从0x08000000开始覆盖了Bootloader整个系统就彻底崩了。这两个地方链接脚本的起始地址和VTOR必须一一对应我实测过无数次只要忘记其中一个板子必定跑飞。3. 用AI辅助把核心代码真正落地3.1 我给AI的“精准提示词”长什么样很多人用AI写嵌入式代码效果不好抱怨AI不懂STM32。其实多半是提示词写得太模糊。我的经验是把任务拆到“函数级”颗粒度明确输入输出、约束条件、依赖的外设标准库版本。下面是我实际用过的提示词模板以生成Flash写入代码为例使用STM32标准外设库STM32F10x_StdPeriph_Driver 3.5.0编写一个函数uint8_t flash_write_buffer(uint32_t dst_addr, uint8_t *buf, uint16_t len)。要求调用FLASH_Unlock()解锁结束前调用FLASH_Lock()重新上锁。写入前确保目标地址偏移小于64KBF103C8T6容量如果越界返回错误码2。如果目标地址是扇区起始位置或剩余空间不足一扇区先调用FLASH_ErasePage()擦除对应1KB页。使用FLASH_ProgramHalfWord()每次写入16位数据注意buf长度按2字节对齐。程序设计为阻塞模式不依赖中断。 请直接给出完整函数代码并标注关键注释。这样一条提示词AI生成的代码基本上可以直接编译通过。为什么效果好因为我替AI把所有边界情况、API名称、硬件约束都定义好了它只需要做“翻译”。我建议在做类似项目时把提示词当成“写给实习生看的详细开发规格说明书”来写越具体AI就越少胡编。3.2 Bootloader核心代码的实现细节Bootloader里最核心的两段代码跳转函数和固件搬运函数。跳转函数的关键是校验栈指针和复位向量的合法性避免跳到垃圾地址typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t stack_addr *(volatile uint32_t *)app_addr; uint32_t reset_addr *(volatile uint32_t *)(app_addr 4); // 栈指针应该在RAM范围内0x20000000 ~ 0x2000500020KB SRAM if ((stack_addr 0x2FFE0000) ! 0x20000000) { return; // 非法栈指针不跳转 } __disable_irq(); SCB-VTOR app_addr; __set_MSP(stack_addr); pFunction app_entry (pFunction)reset_addr; app_entry(); }这里有一个很多人容易忽略的细节跳转之前最好关闭所有中断否则跳转瞬间如果有中断触发向量表可能还没完全切换到App处理器会读到旧向量表导致异常。__disable_irq()之后App自身在初始化时重新使能中断。固件搬运的代码其实更简单就是从下载缓存区逐字节读取擦除App区对应扇区再写入。这里唯一要提醒的是擦除Flash期间CPU是暂停状态Flash操作时总线繁忙所以搬运过程中任何中断都不会响应如果有看门狗在跑必须提前喂狗或在搬运期间暂停看门狗否则搬一半就被狗咬复位了那场面很尴尬。3.3 App端配合逻辑固件包接收与校验App端的核心工作是从ESP8266接收字节流、组装固件包、写入下载缓存区、最终置升级标志复位。我用了一个固定长度的固件包头自定义格式如下字段长度说明魔数4字节固定值0x5A5AA55A用于识别合法固件包设备型号ID2字节0x0001代表F103C8T6防止刷错型号固件版本号4字节例如0x00000003代表V3.0升级时要求新版本旧版本固件长度4字节固件数据的字节数最大不超过36KBCRC324字节对固件数据部分做CRC32校验固件数据N字节实际固件内容App每收到一定数量的字节就通过DMA串口空闲中断接收存入循环缓冲区。主循环里解析包头确认魔数、型号、长度都无误后再把数据搬运到下载缓存区。整个接收过程中如果发现CRC校验不通过就直接丢弃并重新请求。为什么非得先校验再搬运因为网络传输丢包是很正常的如果不校验就把坏数据搬进App区Bootloader虽然也会校验但那时候下载缓存区已经存满坏数据了只能重新下载整个包效率更低。我还加了一个小设计App在收到新的固件包后会先跟当前版本号比对如果新版本号不大于当前版本号就直接拒绝下载省掉无谓的流量。这个版本号校验逻辑同时也能防止误操作刷回旧版本。4. 固件安全与可靠性设计4.1 固件安全不只是防呆也要防“半吊子”恶意刷写说实话以F103C8T6的成本定位没必要上高大上的安全方案但至少要做到“不是随便一个文件刷进去就能跑”的水平。我的做法分了三层。第一层是完整性校验前面说的CRC32校验保证固件在传输和存储过程中没有被破坏。这是最基础的一层。第二层是身份识别包头里的设备型号ID和魔数防止拿着别的板子固件或普通bin文件瞎刷进去。第三层是简单的加密混淆我对固件数据做了异或加密密钥只有32字节放在Bootloader编译时写死的const数组里。App下载到数据后先解密再搬运或者直接在搬运时解密。这个加密强度不高但足以挡住绝大多数拿二进制编辑器打开固件就想逆向的人。想要更安全的方案可以考虑AES-128-CBC但F103做AES软解会拖慢下载速度而且密钥管理又是个大问题。对于这个项目异或加密加CRC校验已经是性价比很高的组合了。4.2 升级失败的兜底从“变砖”到“还能救”OTA最怕什么怕升级到一半断电。如果在擦除App区的过程中断电App区就是残缺的重启后Bootloader跳转过去程序跑飞。常规做法是加备份区先把新固件复制到备份区直接切换到备份区运行旧固件保留这样永远有一个能跑的版本。但F103C8T6的Flash才64KB实在分不出一个完整的备份区所以我换了一种妥协方案利用下载缓存区“半持久化”的特性。具体做法是Bootloader在搬运完固件、更新完标志后不立即擦除下载缓存区。也就是说下载缓存区里始终保留着“上次成功升级所使用的固件包”。如果系统运行一段时间后发现App频繁崩溃可以通过远程命令让系统进入Bootloader重新把缓存区里的旧固件包搬回App区实现回滚。当然这种回滚只能回到上一次的版本不能回到更早但对于远程维护场景已经够用了。有人可能会说直接在搬运前不擦旧App不就行了不行因为Flash必须整块擦除才能写F103C8T6的1KB小扇区虽然粒度小但36KB的App区依然会被整个覆盖不可能局部保留。4.3 一个能救命的参数看门狗与升级互斥我调试OTA时遇到最无语的坑就是看门狗。Bootloader里如果开了独立看门狗IWDG擦除Flash时CPU被占用看门狗没法及时喂结果升级到一半系统被狗咬复位复位后又回到BootloaderBootloader发现升级标志还在又开始搬运搬两秒又被咬复位无限循环。看起来像是系统卡死了其实是看门狗在捣乱。解决思路分两种第一种Bootloader在整个升级期间关闭或延长看门狗超时时间这是最直接的第二种把喂狗动作放在Flash操作的间隔里比如每擦完一个扇区就喂一次狗。我推荐第一种因为升级本身就是一个必须原子化完成的过程中间任何打断都可能造成不可恢复的损坏。另外如果App里也开了看门狗记住一个原则在发起升级请求、写标志位之前可以手动关闭看门狗或把超时调到最大因为接下来的擦写操作很可能超过正常喂狗周期。5. 实测结果与问题排查速查表5.1 测试环境与一次完整的升级记录我的测试环境非常简单STM32F103C8T6最小系统板一块ESP8266ESP-01S模块一个PC上跑了一个Python的HTTP文件服务器python -m http.server 8080固件包放在服务器目录下。整个系统用5V USB供电ESP8266通过3.3V LDO供电。App里放了一个LED每秒翻转一次的测试程序并带一个虚拟的版本号显示。升级前版本号是V1.0我把新固件编译成V2.0之后把bin文件放到服务器目录然后通过串口发送一个内部测试命令触发App去下载。实际测量的完整流程耗时大约3秒下载固件包到缓存区接近1秒受限于ESP8266串口波特率115200以及约12KB的包体大小Bootloader搬运约1.5秒Flash擦写时间为主重启后版本号变成了V2.0。这个速度对于远程维护场景完全够用。5.2 调试中遇到的真实问题和解决过程这里整理一份我实打实踩过的坑和对应的排查方法现象可能原因排查与解决跳转App后程序跑飞HardFault栈指针地址非法、VTOR未设置检查App起始地址前4字节是否为0x2000xxxx确认VTOR赋值在中断初始化之前完成App编译出来地址还是0x08000000链接脚本.icf或.sct没改FLASH起始地址把固件链接脚本里ROM起始地址改成0x08003000ESP8266偶发返回乱码串口波特率误差、供电不足用示波器看串口波形确认两边波特率一致ESP8266单独供电不要和开发板共用低功率LDO擦除Flash时系统卡死Flash操作期间中断响应被阻塞属于正常但看门狗会触发复位升级期间关闭看门狗或增加喂狗点升级成功后第一次运行正常重新上电后恢复旧固件参数标志区写入失败或顺序不对检查标志写入是否使用了Flash写操作而不是普通的RAM变量写入完成并读取回读校验下载过程中断网导致缓存区有半个固件没校验长度就允许搬运严格校验包头里的长度字段不足长度直接丢弃重新下载固件包太大超过缓存区容量下载区12KB太小或App区超过36KB压缩固件或精简功能始终保持整个固件包小于缓存区容量5.3 排查问题时的通用思路如果遇到OTA相关的疑难杂症我建议按这个顺序排查第一先用串口把Bootloader和App的关键日志都打出来OTA问题九成都能靠日志定位别看寄存器先看流程走到哪一步。第二检查地址空间是否越界F103C8T6的Flash只有64KB任何越界写都会产生灾难性后果。第三检查中断向量表和链接脚本这是IAP项目独有的坑普通开发很少遇到。第四检查掉电时序和复位逻辑特别是升级完第一次启动和第二次启动的行为差异往往能暴露标志位或Flash数据残留的问题。6. 关于AI编程最后说点真心话这个项目做完之后我对AI编程的态度从“试试看”变成了“认真用”。我的实际体会是AI编程在嵌入式领域的角色不是替代工程师而是把“手写样板代码”这个环节压缩到几乎为零。以前写一个USART驱动、一个Flash读写函数翻手册加调试怎么也得半天时间现在把需求和约束写进提示词AI几秒钟给初稿我再花十分钟验证和修边角效率提升非常明显。尤其是像OTA这种流程清晰、边界明确的技术非常适合用AI快速落地。但我也要提醒一句AI生成的代码尤其是涉及硬件寄存器的代码必须一个一个寄存器去核对。我这次就碰到过一次AI把FLASH_ProgramHalfWord的地址参数写错的情况编译能过跑起来直接HardFault。所以我的经验总结是AI可以用来写“骨架”但底层的寄存器操作、内存布局、中断优先级这些关键点必须自己心里有数亲手验证过才能放心。如果你也想复刻这个项目建议先手动跑通一个最简单的LED闪灯App再一步步增加Bootloader、OTA下载逻辑走通了整个链路你会对STM32这套体系理解得非常透彻。另外如果想做得更完善后续还可以把HTTP下载改成MQTT方式由服务器主动推送升级命令实现真正的“云管端”远程运维。