STM32G4 Bootloader开发实战:从CubeMX配置到IAP升级
做了三年嵌入式产品我一直觉得Bootloader这东西离自己很远直到客户现场那批设备需要升级协议我才意识到没有Bootloader意味着什么拆机、接线、插ST-Link、刷固件、贴封条一台机器折腾十分钟五十台就要一个下午。更崩溃的是升级完了发现还要改参数又得来一遍。所以当我在STM32G4上把Bootloader从CubeMX配置到Flash划分整个流程跑通之后第一反应是这玩意儿真没那么玄乎但坑也确实不少很多坑还是那种百度半天找不到答案的暗坑。这篇文章就是我这次完整开发过程的全记录从为什么选STM32G4、CubeMX怎么配置到APP区地址怎么规划、跳转代码怎么写、IAP协议怎么定最后把我踩过的几个典型问题和排查思路整理成速查表。不管你是第一次接触STM32G4 Bootloader还是之前在其他平台做过IAP想迁移过来这篇文章都能帮你少走弯路。1. 项目概述与整体设计思路1.1 为什么选STM32G4来做BootloaderSTM32G4是ST主推的数字电源和电机控制芯片主频170MHz带硬件数学运算内核浮点性能很强。但单从Bootloader的角度看选它不是因为性能而是因为它的Flash架构比老一代F1/F4更适合做IAP。首先是Flash容量G4系列从128KB到512KB可选常见型号如G431、G474都有较充裕的空间来划分Bootloader区、App区和参数区。其次是Flash的双Bank特性G4的部分型号支持双Bank启动这意味着可以做成A/B分区备份升级卡在升级中途设备也不会变砖。再有就是Flash的ECC校验、读写保护这些安全特性生产环境里对固件防读出、防篡改有要求的场景G4比F1那种裸奔式的Flash管理要顺手得多。当然选G4也有代价代价就是它的Flash操作细节和CubeMX的配置方式跟F1差别不小。如果你是从F103直接迁移过来很容易在擦除、编程、地址对齐这些地方翻车这也是我写这篇文章的直接原因。1.2 整体架构Bootloader加App的双区方案做Bootloader之前先把架构想清楚。我这边的方案是最经典的“Bootloader区 App区”结构外加一个跑在Bootloader里的串口升级协议。整个系统上电后先跑BootloaderBootloader做三件事检查升级标志位、判断App区是否有效、有效就跳过去执行App无效就留在Bootloader里等待上位机发固件。升级时上位机通过串口把固件包发给BootloaderBootloader负责擦除App区的Flash、接收数据包、写入Flash、校验CRC全部完成后再跳转。串口升级协议我选择了自定义的简单帧格式没有用Ymodem原因有两个第一Ymodem的包格式和流控机制对于小团队小产品来说偏重调试时出了问题不好定位第二自定义协议可以把握手、擦除、写入、跳转这几个关键状态拆得很清楚每一步都能通过应答帧确认排查问题的时候心里特别有数。2. CubeMX配置与工程初始化避坑2.1 时钟树配置先跑明白频率再说别的在CubeMX里新建STM32G4工程时我踩的第一个坑就是时钟树。G4最高主频170MHz但不是说随便配个PLL就能跑起来的PLL的输入频率、倍频系数、VCO频率范围都有硬性约束。如果你拿F1的习惯直接去配很可能会陷入“点开Clock Configuration面板调来调去就是绿色不了”的窘境。以外部8MHz晶振为例要跑到170MHz典型的配置路径是HSE 8MHz进PLLPLLM分频为2得到4MHz的PLL输入然后PLLN倍频到340MHz作为VCO输出最后PLLR分频为2得到170MHz的系统时钟。这三个参数牵扯到PLL输入范围必须在1~16MHzVCO输出需要在96~480MHz之间任何一个参数超出范围CubeMX就会把时钟树标红告诉你跑不了。我在Bootloader里其实没有跑170MHz。Bootloader的工作就是串口收数据和写Flash跑太快没有实际收益反而增加功耗和干扰风险。我的做法是让系统时钟按实际需求配置比如开发板方便就跑到170MHz验证稳定性量产Bootloader里则可以适当降低主频把重点放在UART波特率误差上。这里要特别注意G4的UART时钟源。G4的UART可以选PCLK、HSI16、LSE等时钟源。当初我图省事直接用内部HSI16来做USART时钟结果一个115200波特率实际误差偏大收包时偶发乱码。后来老老实实切回由HSE经过PLL得到的PCLK问题立刻消失。2.2 串口、GPIO与中断Bootloader工程该开哪些外设CubeMX里盗Base工程的配置我只开了USART1、一个LED引脚用来指示设备处于Bootloader模式以及Flash编程需要的必要选项。很多人习惯把外设全部默认初始化这在App里没问题在Bootloader里就是给自己埋雷。为什么这么说因为Bootloader最终是要跳转到App的跳转之前如果你把一堆外设全都初始化过、中断也挂上了跳过去之后App的初始化顺序稍有不同就可能导致两个程序之间的外设状态互相干扰。比如Bootloader里开着UART中断跳转到App后App还没来得及重配UART此时一个串口中断进来中断向量表又已经切换到App就可能直接HardFault。所以Bootloader里外设开得越少越好够用就行。UART的接收方式我建议初版不要一上来就上DMA加空闲中断虽然这样CPU占用率低但调试难度大。众所周知一次只干一件事的Bootloader场景用最朴素的字节中断加状态机解析反而更容易验证整套流程。先把帧协议跑通再回头优化成DMA版本也不迟。NVIC里只使能USART1全局中断其他外设中断全部不勾选这步看似不起眼实际对跳转稳定性影响很大。3. Flash划分与地址规划整个项目的命根子3.1 STM32G4的Flash结构扇区、Bank与双BankFlash划分是整个Bootloader开发里最基础也最关键的一步。很多人升级失败根源就是地址规划出了问题而不是代码问题。STM32G4的Flash跟F1那种“一页1KB全部等大”的结构完全不同。G4的Flash是按Bank组织的每个Bank又分成若干扇区扇区大小并不统一常见型号里前面的扇区可能是16KB后面的扇区可能是32KB甚至更大具体要看对应型号参考手册里的Flash memory organization表格。更需要注意的是G4的部分型号支持双Bank模式。以STM32G474为例512KB的Flash可以做成两个256KB的Bank支持从任意一个Bank启动。这意味着你可以把固件A放在Bank1固件B放在Bank2一个运行一个待升级升级完成后通过切换Bank的方式激活新固件。这个特性在需要OTA高可靠性的场景几乎是救命的因为哪怕写入新固件到一半断电旧固件依然完好无损。但千万别以为所有G4都支持双Bank像128KB的G431Flash较小设计上就不支持双Bank模式。做方案选型时一定要先查数据手册别等板子画完了才发现这个坑。3.2 地址划分实操Bootloader区、App区与参数区以我手上的512KB的STM32G474为例实际划分如下Bootloader区0x08000000 到 0x0800FFFF共64KB存放Bootloader固件。 App区0x08010000 到 0x0803FFFF共192KB存放应用程序固件。 参数区0x08040000往后存放升级标志位、设备配置参数、固件版本号等信息。我刻意把Bootloader区留到64KB虽然实际Bootloader固件编译出来可能只有20多KB但留出冗余有几个好处以后想加加密验签逻辑、想支持更多命令不需要动App区地址。App区起始地址选在0x08010000也考虑了页对齐的问题。G4的擦除操作是按扇区进行的而扇区大小不一定相同。如果App起始地址落在某个扇区中间擦除操作就很难设计。所以强建议你规划地址时先去查手册里Flash扇区表确保App起始地址正好是某个扇区的起始地址。整个Flash划分里有一个通用原则地址越早规划越好越晚改动代价越大。因为一旦App跑起来链接脚本、中断向量表偏移、固件升级包内的跳转地址全都跟这个地址绑定中途改地址等于全部返工。3.3 跳转App的核心代码MSP、VTOR与函数指针Bootloader跳转App表面上是把PC指到App的main函数但实际没那么简单。Cortex-M系列和普通MCU不一样它复位后是从向量表取值的向量表的第一个字是初始栈顶地址也就是MSP的初值第二个字是复位中断函数地址也就是程序入口。所以跳转的实质是从App区的向量表里读出MSP初值和复位函数地址然后把MSP寄存器设为那个值再把程序跳到复位函数地址。如果不能理解这个机理你在跳转这里踩坑是必然的。我实际使用的跳转函数大概长这样typedef void (*pFunction)(void); uint32_t JumpToApp(uint32_t app_addr) { uint32_t msp_value; uint32_t reset_vector; pFunction app_entry; msp_value *(volatile uint32_t *)app_addr; reset_vector *(volatile uint32_t *)(app_addr 4); app_entry (pFunction)reset_vector; __disable_irq(); HAL_UART_DeInit(huart1); HAL_RCC_DeInit(); SysTick-CTRL 0; SysTick-LOAD 0; SysTick-VAL 0; SCB-VTOR app_addr; __set_MSP(msp_value); app_entry(); return 0; }函数里有两个地方是新手最容易漏的。第一个地方是SCB-VTOR设置。如果不把中断向量表的地址改成App区的起始地址App里一旦触发任何一个中断CPU还是会去Bootloader区的向量表找中断处理函数表现就是跳转成功后程序跑一会儿一进中断就死机甚至直接HardFault。很多人在App里不设置VTOR总觉得链接脚本里改了地址就够了这是不对的。第二个地方是__set_MSP。跳转前必须把主栈指针切到App的栈顶地址。有人不切也能跑通这是因为App代码段刚执行时栈还没用多少Bootloader的栈暂时够用但只要App里随便调个稍深的函数栈就溢出了表现为极其诡异的死机。这种问题很难查所以跳转前老老实实设置MSP一步都不能省。对了跳转前最好先__disable_irq()并清掉SysTick延迟函数之类的先停掉避免跳转过程中系统滴答中断插进来造成状态错乱。4. IAP升级协议与Flash写入流程实现4.1 自定义帧协议带CRC的最小可用方案升级协议是整个系统上位机跟Bootloader之间的通信契约协议设计越清晰后期排查问题越容易。我设计的协议非常简单每帧固定这么几部分帧头2字节、命令1字节、数据长度1字节、数据区不定长、CRC16校验2字节。实际使用中的命令也不多握手、擦除、写数据、查版本、跳转就这五个。每个命令都有对应的应答要么应答ACK要么应答NAK并附上原因码。上位机收不到正确应答就重发当前帧这也算是一个最简单的超时重传机制。CRC16校验是不可省的环节。串口传数据偶发一两个bit的错误太常见了没有校验就把错误数据写进Flash后果是整个App区作废只能重新拆机上仿真器救砖。实测用标准的Modbus CRC16更新算法就够用代码很小消耗的时间对升级场景完全无所谓。uint16_t crc16_update(uint16_t crc, uint8_t a) { crc ^ a; for (int i 0; i 8; i) { if (crc 1) crc (crc 1) ^ 0xA001; else crc 1; } return crc; }4.2 擦除和写入G4的64位编程G4的Flash编程宽度是64位也就是一次必须写入8字节这是硬性要求。你写4字节甚至单字节HAL库会直接报错或者数据不对。所以上位机发包时最好每包数据就是8字节的整数倍Bootloader收满8字节再调用一次编程函数。我在实际操作中用的是HAL库的HAL_FLASHEx_Erase和HAL_FLASH_Program步骤如下先解锁Flash擦除目标扇区再按64位为单位写入数据最后锁住Flash。代码大致这样HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase; uint32_t page_error 0; erase.TypeErase FLASH_TYPEERASE_PAGES; erase.Banks FLASH_BANK_1; erase.Page app_start_page; erase.NbPages 1; if (HAL_FLASHEx_Erase(erase, page_error) ! HAL_OK) { return 0; // 擦除失败 } uint64_t data 0; data | ((uint64_t)rx_buf[0] 0); data | ((uint64_t)rx_buf[1] 8); data | ((uint64_t)rx_buf[2] 16); data | ((uint64_t)rx_buf[3] 24); data | ((uint64_t)rx_buf[4] 32); data | ((uint64_t)rx_buf[5] 40); data | ((uint64_t)rx_buf[6] 48); data | ((uint64_t)rx_buf[7] 56); HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, app_write_addr, data); HAL_FLASH_Lock();擦除这里有一个特别容易踩的坑Page编号。很多时候你拿着App起始地址0x08010000想擦除它所在扇区但G4的Page号跟地址之间不是简单的线性映射因为每个扇区大小不一样前几个扇区可能是16KB后面可能是32KB直接拿地址除以扇区大小是算不出正确Page号的。最稳妥的做法是去参考手册的扇区表里查一下App起始地址落在第几个扇区把这个编号写成宏定义而不是每次运行时动态计算。还有一种做法是你只在握手阶段擦除一次而不是每收一包数据就擦一次这样能大幅缩短升级总时间。但想清楚如果数据传了一半断电Flash里就是半旧半新没有备份机制的话只能靠恢复出厂固件兜底。所以我在量产的方案里把擦除放在最后一步先把固件全部收到RAM缓存里校验整个固件的CRC无误后再一次性擦除、写入。这个思路跟G4的双Bank配合起来基本可以做到升级不掉线。4.3 校验与跳转别让半包数据把设备搞成砖升级流程收尾这步最忌讳的就是“收到就跳转”。我见过太多人写Bootloader不管数据包对不对写完了就跳结果上电发现设备没反应原因是某个包的数据在传输中出了问题。所以收完所有数据之后必须对整个固件做一次CRC校验校验通过才允许跳转校验失败就返回错误码等待上位机重新发起升级。我的升级流程设计成这样的状态机空闲状态收到握手命令后进入等待固件信息状态收到固件长度、总CRC等元信息后开始计数收数据当实际收到的字节数与固件长度一致启动全固件CRC校验校验通过擦除App扇区并写入全部完成返回跳转命令收到跳转命令后执行前面那个跳转函数进入App。这个流程里我把擦除放在校验之后看起来多了一步但安全系数提高了一个档次。毕竟擦除是一件不可逆的事情一旦擦了又不能正确写入设备就只能卡在Bootloader里。如果你没有做双Bank备份这还是小问题顶多重新升级一次但如果你跳过了校验直接擦除然后又因为擦除时间太长导致上位机超时那才是真正的噩梦。5. 常见问题与排查技巧实录5.1 升级与跳转环节的典型问题速查我这次开发过程中遇到的问题不算少整理成一个速查表以后遇到类似问题可以对照排查。现象可能原因解决思路跳转后进入HardFaultMSP未设置或设置错误检查向量表第一个字是否为合法栈顶地址跳转后一进中断就死机App未设置VTOR或设置地址错误在App最早期设置SCB-VTOR为App起始地址波特率对但是全是乱码UART时钟源选择不当误差偏大优先使用PLL输出的PCLK作为UART时钟写入Flash失败未解锁、未擦除、地址未8字节对齐严格按HAL_FLASH_Unlock、擦除、写64位、Lock的顺序第二次升级失败写保护未关闭或地址越界检查Flash写保护配置以及App区长度是否超出收到完整数据但校验不过传输错误或协议帧错位加CRC校验收包状态机增加超时和重同步升级完成后App起不来跳转前未关闭Bootloader外设和中断先DeInit所有外设再关全局中断最后跳转5.2 强烈建议先做最小验证再上完整协议如果你现在准备在STM32G4上从零做Bootloader我强烈建议你不要一上来就把所有的升级流程全部实现。先做最小验证把链路跑通再逐步加功能。我个人的经验是分四步走。第一步先在Bootloader里写一个最简单的功能上电检测某个GPIO电平如果是低电平就跳过跳转停留在Bootloader里。第二步验证跳转手动构造一个最简单的App程序不用真正处理业务只要在main里点个灯然后把Bootloader的跳转函数跑通确保能从Bootloader跳到App。第三步验证Flash写入写一个测试程序把一串数据写入App区写完读出来比对确保编程宽度、地址对齐这些细节都正确。第四步才是把串口协议和命令状态机整体串起来。这套流程看起来很慢其实最省时间。因为每一步的验证范围都很小出了问题能快速定位是硬件问题、地址问题还是协议问题。我最开始直接写完整版协议结果一次调试同时面对串口乱码、Flash写入失败、跳转死机三个问题根本分不清先查哪个白白折腾了两天。5.3 升级中断电了怎么办双Bank与备份机制的兜底思路虽然上面把基本流程跑通了但真放到产品上的话还有一个问题必须考虑如果写入新固件写到一半断电了或者擦除后新的固件没写成功设备是不是就废了最简陋的方案是靠Bootloader兜底因为擦除和写入是在升级流程里独立的一步只要不是极极端情况导致Bootloader区也被擦坏设备始终能进入Bootloader你就可以通过串口重新升级。但这要求Bootloader区在整个设计里面处于独立保护区状态并且用户不能轻易破坏它。如果你想做得再稳一点强烈建议借助G4的双Bank特性。把App区进一步分成A份和B份固定一份作为备份一份作为运行固件。升级时先把新固件写入不运行的Bank等校验通过后再切换Bank启动。这样升级中断电顶多还在旧固件上运行数据不会丢设备不会砖。这就是我之前说G4做Bootloader比F1有天然优势的原因。个人经验与后续扩展最后分享一点我做完整个Bootloader以后的整体感觉。Bootloader本身并不复杂复杂的往往是那些不易察觉的边界条件Flash扇区表查没查对、跳转前外设关没关干净、App区地址在链接脚本和VTOR里是不是一致。这些细节每个单独拿出来都很小可一旦出错表现为各种玄学死机查起来特别痛苦。我在这次开发过程中还留了两个后续可以扩展的方向。一个是在Bootloader里加固件加密和签名验签防止固件被他人提取或篡改STM32G4的硬件加密模块和Flash读写保护都能配合使用。另一个是升级通道不局限于串口可以扩展到CAN、USB或者以太网协议框架只要设计成命令加数据的模式移植起来非常容易。如果你也在做STM32G4的Bootloader建议把这份流程按照最小验证的思路走一遍把底层跳转和Flash操作先跑通剩下的协议和交互都是锦上添花。说实话做完这些东西之后客户现场再提升级需求我心里是真的不慌了。