STM32 OTA自动化分区:CubeMX+VS Code一键生成Bootloader与App固件

发布时间:2026/10/5 1:32:13

STM32 OTA自动化分区:CubeMX+VS Code一键生成Bootloader与App固件 做嵌入式开发这些年最让我烦躁的既不是硬件查错也不是Bug调试而是每次要做一个带Bootloader的OTA工程时都得去手动改链接脚本、算Flash地址、折腾分散加载文件。Keil里点半天鼠标改错一个地址就是启动就跑飞查起来能让人怀疑人生。后来换了CubeMX加VS Code这套组合把分区和编译流水线彻底自动化了从新建工程到生成Bootloader和App两个固件一条命令搞定再也没在地址上翻过车。这篇就把这个方案的完整思路和实操过程整理出来希望能帮到同样被手动分区折磨的朋友。这套方案的核心是用STM32CubeMX负责芯片初始化和Makefile工程生成用VS Code当编辑器用arm-none-eabi-gcc工具链完成编译然后用脚本把链接脚本的Flash地址自动改成Bootloader和App两个版本最终一键产出两个独立固件。适合所有用STM32做产品、需要OTA升级、又不想被厂商IDE绑架的开发者。我会把原理、步骤、代码和坑全部拆开讲。1. 方案设计与原理拆解1.1 为什么放弃Keil手动分区先说说我原来在Keil里的工作流。Keil管理STM32工程Flash分区靠的是分散加载文件.sctApp和Bootloader各自建一个工程每个工程都要单独指定ROM起始地址和大小。听起来不复杂但实际项目里很容易出问题。首先是地址容易写错。Bootloader占了32KBApp的起始地址就是0x08008000长度是总Flash减去Bootloader区再加参数区。一旦Bootloader升级变大App的起始地址全得跟着改还有中断向量表偏移、链接脚本、下载算法好几处都要同步。漏改任何一处轻则编译出来的固件没法跑重则直接覆盖Bootloader区域把引导程序冲掉设备变砖。其次是Keil工程文件的可维护性太差。一个工程里所有源码路径、宏定义、编译选项都堆在一个工程文件里和实际目录结构脱节得厉害。多人协作时工程文件冲突几乎每周都发生。更重要的是Keil的编译过程很难脚本化想在CI里自动编译Bootloader和App固件要么装命令行工具要么得用第三方插件折腾一圈下来自动化程度还是很低。换到CubeMX加Makefile之后情况完全变了。CubeMX根据芯片型号生成初始化代码和一个标准的MakefileVS Code负责写代码Makefile负责编译。CubeMX抽取出工程配置后分区这件事就变成了往Makefile里传一个链接脚本参数或者脚本里改一行ORIGIN地址。所有东西都变成了文本可以进Git可以diff可以在命令行一键执行。1.2 Bootloader与App双分区与OTA基础原理要理解这套自动化分区得先把STM32的Flash布局说清楚。STM32内部Flash是从0x08000000开始映射的芯片上电后CPU从0x08000000取栈指针MSP从0x08000004取复位向量然后跳过去执行。如果这个地址上放的是Bootloader那么Bootloader跑起来后由它决定是进入升级模式还是跳转到App。一个典型的双分区布局长这样区域起始地址大小说明Bootloader0x0800000032KB引导程序负责启动检查与固件接收App0x08008000剩余空间业务应用固件运行时占绝大部分Flash参数区0x0803F8002KB保存升级标志、版本号、回滚标记Bootloader在启动流程里做三件事检查参数区是否有升级请求标志如果有就进入串口接收固件如果没有就检查App区的固件头是否有效有效就跳转无效就停在Bootloader等待升级。跳转的关键是向量表重定向。App编译时链接脚本把代码放在0x08008000但CPU的向量表默认还是从0x08000000读。所以App启动后第一件事就是把向量表偏移寄存器SCB-VTOR设置到App的起始地址同时把栈指针切成App自己的栈。这两步漏一个App就会在跳转后瞬间HardFault。这里有一个容易忽略的细节Bootloader和App的固件头建议统一格式。我一般会在App固件开头放一个固定的魔数、固件版本号、固件长度和CRC校验值。Bootloader在跳转前检查魔数和CRC不合法就不跳。这样即使中途断电写入了一半的固件Bootloader也能识别出来并拒绝启动App至少保证设备还能重新升级不至于彻底变砖。1.3 双Bank与A/B分区的选型思考这次方案里我用的是单Bank下的Bootloader加App双分区这也是绝大多数STM32项目的主流做法。不过做产品前要考虑清楚OTA方案按更新策略还可以分三类。第一种就是经典的双分区方案有些人也把升级区和备份区叫做A/B分区。Bootloader在App区运行新固件同时保留一个备份区。升级时新固件优先写入备份区校验成功后再切换失败就回滚旧固件。这个方案的优点是安全缺点是Flash占用翻倍对Flash容量小的芯片不友好。第二种是基于STM32自身双Bank特性的方案。部分芯片比如STM32F767、H743内部Flash物理上分成两个Bank支持在Bank2执行代码的同时擦写Bank1。这种方案在OTA时能实现无缝切换但只在中高端芯片上才有对项目选型有要求。第三种就是我现在用的最简方案Bootloader固定App一个区。升级时Bootloader直接覆盖写App区。简单可靠但对“升级中途断电”这件事容忍度低。为了弥补这个短板我在参数区保存了一个升级状态机Bootloader在完整接收并校验固件前不会把新固件标记为可执行。如果升级中断重启后检测到App区固件不完整就停留在Bootloader等待重新升级。对于大多数产品、尤其是Flash在256KB以上的STM32我建议你直接上A/B分区安全性和运维体验会好很多。这也是很多联网设备的新趋势。不过为了这篇教程聚焦自动化分区这个主题我下面的实操还是以最简双分区为例原理是相通的。2. 环境准备与工程生成2.1 工具链安装清单这套方案依赖的工具不多但每一样都得装对。先列个清单标注清楚用途。工具用途说明STM32CubeMX芯片初始化、生成Makefile工程6.x以上版本均可VS Code代码编辑器配合C/C扩展做代码提示arm-none-eabi-gccARM交叉编译器推荐9.x或10.x版本老版本对Cortex-M7支持一般GNU Make构建工具Windows下建议用xpack或MSYS2的makeSTM32CubeProgrammer烧录与Flash读取提供命令行工具STM32_Programmer_CLIOpenOCD或st-flash可选的烧录工具调试和命令行烧录更方便如果你的操作系统是Windows我建议把arm-none-eabi-gcc和make安装后手动加入系统PATH。记得装完在终端里敲一句arm-none-eabi-gcc --version和make --version能显示版本号才说明环境OK。这一步不过关后续所有自动化都是空中楼阁。VS Code跨平台、插件生态好配置C/C扩展之后代码补全和跳转体验已经非常接近甚至超过IDE。这块不是本文重点但建议把IntelliSense的includePath指到CubeMX生成的Inc目录否则你会看到一堆红线。2.2 CubeMX工程配置关键步骤用CubeMX新建工程这一步很多教程都写过我只讲和分区自动化相关的几个关键点。时钟树配置这里不展开直接用外部晶振配上合适的PLL就行。关键是Project Manager设置页在Project Settings里Toolchain选择Makefile。这是整个自动化方案的基石选错就前功尽弃。勾选Generate Under Root让工程文件和固件库在同一个根目录下目录结构更清爽。Linker Settings里设置Minimum Heap Size和Minimum Stack Size。这里先不要为了OTA节省RAM把栈设太小默认0x200就够后面真正跑任务再调。Code Generator里勾选Generate peripheral initialization as a pair of .c/.h files per peripheral。这个选项让每个外设独立成一对文件可维护性好很多。工程名称建议带上下划线比如ota_app_base避免空格和中文路径。CubeMX对中文路径的支持一直很迷别在这上面浪费时间。时钟和外设配置完了在Project Manager里点击Generate CodeCubeMX会生成一个标准的GCC工程骨架。这时打开目录你会看到核心文件Makefile、.ld链接脚本、startup_.s启动文件、Core/Src下的main.c和stm32xxx_it.c等。后面所有自动化的重点就是这两个文件Makefile和.ld链接脚本。2.3 CubeMX生成文件结构解析在动手改写之前先花两分钟看懂生成的文件结构后面脚本写起来心里才有底。链接脚本通常在工程根目录下名字类似STM32F407VGTx_FLASH.ld里面最关键的是MEMORY段MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 1024K RAM (xrw) : ORIGIN 0x20000000, LENGTH 128K }ORIGIN就是Flash的起始地址LENGTH是可用长度。Bootloader不改直接用App把这行的ORIGIN改成0x08010000以64KB Bootloader为例LENGTH改成1024K减掉64K再加一个_estack等符号的地址验证即可。Makefile里则定义了编译规则关键变量有这么几个TARGET ota_app_base BUILD_DIR build C_SOURCES \ Core/Src/main.c \ ... LDSCRIPT STM32F407VGTx_FLASH.ld后面想要对Bootloader和App定制编译就是动态改LDSCRIPT路径和TARGET名字或者直接传入不同的宏定义。3. 自动化分区链接脚本与一键构建3.1 Flash偏移参数与链接脚本自动改写方案整个自动化方案最核心的部分就是链接脚本的自动改写。我在项目里放了一个python脚本专门干这件事。为什么不用sed因为sed跨平台在Windows下经常出幺蛾子python只要装了就能跑而且逻辑更清晰出错也能报错信息。脚本的核心逻辑很简单读原始的.ld文件把FLASH的ORIGIN和LENGTH按参数替换然后另存为另一个文件名。下面这段是我实际在用的脚本做了简化但可以直接跑#!/usr/bin/env python3 import os import sys def generate_ld(original_ld, output_ld, flash_origin, flash_length): with open(original_ld, r, encodingutf-8) as f: content f.read() # 找到FLASH那一行整行替换 lines content.splitlines() new_lines [] for line in lines: if FLASH in line and ORIGIN in line: new_lines.append( fFLASH (rx) : ORIGIN 0x{flash_origin:08X}, LENGTH {flash_length}K ) else: new_lines.append(line) with open(output_ld, w, encodingutf-8) as f: f.write(\n.join(new_lines)) print(f[LD] {output_ld} generated: origin0x{flash_origin:08X}, length{flash_length}K) if __name__ __main__: # 参数原始ld 输出ld Flash起始偏移KB Flash总长度KB original_ld sys.argv[1] output_ld sys.argv[2] bootloader_size_kb int(sys.argv[3]) flash_total_kb int(sys.argv[4]) app_origin 0x08000000 bootloader_size_kb * 1024 app_length flash_total_kb - bootloader_size_kb generate_ld(original_ld, output_ld, app_origin, app_length)这个脚本接收四个参数原始链接脚本路径、输出脚本路径、Bootloader占用大小KB、总Flash大小KB。比如总Flash是512KBBootloader分32KB那么执行python3 tools/gen_ld.py STM32F103C8Tx_FLASH.ld App_FLASH.ld 32 512脚本就会生成一个新链接脚本App的起始地址自动算成0x08008000长度自动算成480KB。你不用再去手算十六进制地址也不用担心算错。实际项目里我还会做一点扩展在脚本里同时生成一个flash_layout.h头文件把Bootloader地址、App地址、App大小、固件魔数、版本号等等统一定义出来App和Bootloader的源码都引用这个头文件保证编译参数和运行时代码看到的地址永远是一致的。这是解决“链接脚本改了但代码里的宏没改”这类Bug的根本办法。3.2 Makefile参数化与编译入口统一链接脚本生成好之后编译规则怎么统一我采用的做法是让Bootloader和App共用同一个CubeMX工程通过Makefile变量来区分。正常的CubeMX工程Makefile有这一行LDSCRIPT STM32F407VGTx_FLASH.ld我把它改成一个变量默认是Bootloader但允许从命令行覆盖# 默认链接脚本Bootloader LDSCRIPT ? STM32F407VGTx_FLASH.ld BUILD_DIR ? build_bootloader TARGET ? bootloader然后在源码里通过编译宏告诉代码现在编译的是Bootloader还是Appifeq ($(APP_MODE),1) LDSCRIPT build/App_FLASH.ld BUILD_DIR build_app TARGET app C_DEFS -DAPP_MODE1 -DAPP_START_ADDR0x08008000 else TARGET bootloader C_DEFS -DAPP_MODE0 endif这样同一个工程目录下执行make编译出Bootloader固件执行make APP_MODE1编译出App固件。两条命令两个固件地址完全由脚本自动算好不会出现手改地址导致的不一致。为了实现真正的“一键”我把生成链接脚本、编译Bootloader、编译App三条命令串成一个shell脚本build_all.sh#!/bin/bash set -e echo Generate App linker script python3 tools/gen_ld.py STM32F407VGTx_FLASH.ld build/App_FLASH.ld 64 1024 echo Build Bootloader make clean make -j$(nproc) echo Build App make clean make APP_MODE1 -j$(nproc) echo Build finished ls -lh build_bootloader/bootloader.bin build_app/app.bin脚本里有个细节要注意在make clean之后再编译App目的是避免两个目标共用同一个build目录时的对象文件冲突。如果不想每次clean也可以把BUILD_DIR严格区分开我在上面的Makefile里已经做了隔离其实clean都不太必要。但在项目初期clean一次能省掉很多“改了代码但没生效”的排查时间。3.3 VS Code任务配置与一键触发脚本写好了在终端里敲命令当然可以但VS Code的Tasks功能可以把这三条命令封装成一个快捷键体验和Keil的Build按钮差不多。在项目根目录创建.vscode/tasks.json{ version: 2.0.0, tasks: [ { label: build-all-ota, type: shell, command: bash build_all.sh, group: { kind: build, isDefault: true }, problemMatcher: [] }, { label: build-app, type: shell, command: make APP_MODE1 -j4, group: build, problemMatcher: [] }, { label: build-bootloader, type: shell, command: make -j4, group: build, problemMatcher: [] } ] }配置好之后VS Code里按CtrlShiftB就能看到这三个任务选“build-all-ota”一杯咖啡没喝完Bootloader和App固件就都在build目录里了。有编译错误VS Code的Problems面板会直接显示配合C/C插件还能直接跳转到出错行。VS Code里我还会装一个Cortex-Debug插件配合OpenOCD可以做在线调试。调试时注意如果调试App需要在启动配置里把svdFile和device指对并且设置好启动前的烧录偏移。这个后面在常见问题里展开。4. 实现OTA升级闭环4.1 Bootloader端启动检查与跳转代码分区和构建自动化搞定了接下来是Bootloader和App各自的业务代码。这部分很多教程写得比较零散我把关键代码贴出来按实际可用的标准写。Bootloader的主流程很简单核心逻辑我放在了jump_to_app函数里#define APP_START_ADDR 0x08008000 #define APP_MAGIC_NUM 0xA5A5A5A5 typedef void (*pFunction)(void); typedef struct { uint32_t magic; uint32_t version; uint32_t length; uint32_t crc32; } app_header_t; void jump_to_app(void) { app_header_t *header (app_header_t *)APP_START_ADDR; uint32_t app_stack *(volatile uint32_t *)APP_START_ADDR; pFunction app_reset (pFunction)(*(volatile uint32_t *)(APP_START_ADDR 4)); // 1. 校验固件头魔数防止跳到随机地址 if (header-magic ! APP_MAGIC_NUM) { // 进入升级模式 enter_update_mode(); return; } // 2. 校验栈指针是否在RAM范围内 if ((app_stack 0x2FFE0000) ! 0x20000000) { enter_update_mode(); return; } // 3. 关闭全局中断避免跳转途中被中断打断 __disable_irq(); // 4. 设置主栈指针并跳转 __set_MSP(app_stack); app_reset(); while(1); }注意第二步的栈指针检查。很多跳转失败就是因为App固件的第一个4字节不是一个合法的RAM地址。如果App编译出来栈指针不是0x20000000开头那说明App的链接脚本有问题跳过去必死。这个检查能帮你提前发现问题。Bootloader里还要处理升级请求。我一般用一个串口命令或者一个按键来触发触发后Bootloader调用HAL_UART_Receive接收固件数据边收边写Flash。写Flash用HAL_FLASH_Program一页一页写每页写完做个校验。全部收完后更新App区的固件头把魔数、版本号、长度和CRC填好。下次重启Bootloader看到魔数有效才会跳转。4.2 App端向量表重定向与关键宏定义App这边要处理的核心事情是把执行环境从Flash起始位置搬到App自己的地址上。CubeMX生成的main.c里SystemInit是第一个被调用的函数标准的做法是把向量表偏移放在SystemInit之后、主循环之前。但实际上在main()开头设置也可以只要保证在第一次使用中断之前完成就行。int main(void) { HAL_Init(); // 向量表重定向到App区 SCB-VTOR APP_START_ADDR; // 如果是从Bootloader跳转过来的需要把所有用到的外设重新初始化一遍 SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); // ... }有一个同时使用Bootloader和App时容易踩的坑Bootloader初始化的外设状态在跳转后仍然残留如果App不重新初始化就直接用可能工作不正常。最稳妥的做法是在App的main里对所有用到的外设都执行一遍DeInit再Init保证外设状态是干净的。链接脚本方面App编译时已经通过3.1节的脚本把ORIGIN改为0x08008000所以链接出来的固件里所有地址都是相对App基址的。FLASH的LENGTH改成了剩余空间万一固件超出Flash容量链接时直接报错不会出现固件悄悄盖掉Bootloader的情况。这就比Keil分散加载文件里那种“写了超出范围的地址最终烧录失败”要好得多。App固件头在链接脚本里放了一个特殊段确保它落在App起始地址的开头。我建议在工程里加一个app_header.c用__attribute__((section(.app_header)))把结构体放到Flash最前面#if APP_MODE const app_header_t app_header __attribute__((section(.app_header), used)) { .magic APP_MAGIC_NUM, .version APP_VERSION, .length 0, // 链接后由脚本自动填充 .crc32 0, // 链接后由脚本自动计算 }; #endif这两个字段可以让构建脚本在编译完成后用arm-none-eabi-size或python读elf/bin文件自动填进去。实际工程我一般用脚本来改或者干脆在Bootloader里计算整段固件的CRC。简化起见上面先保留占位说明重点是让链接脚本保留这个段不被丢弃。在链接脚本的SECTIONS段加上.app_header : { KEEP(*(.app_header)) } FLASHKEEP很关键不加的话链接器发现这个段没有被引用会直接优化丢掉。4.3 OTA传输、固件打包与烧录链路OTA的传输链路有很多种串口、CAN、以太网、WiFi模块透传等等。不管哪种Bootloader侧接收数据写入Flash的逻辑是通用的。我以最常用的串口Ymodem协议为例把固件传送这块讲透。Ymodem的好处是带CRC校验和分包确认传输大文件比裸串口协议可靠很多。STM32的官方AN4655里有Ymodem移植参考代码但代码风格比较老。我建议你自己用状态机实现一个精简版流程大概是Bootloader发送C字符表示等待接收。上位机发送文件名和数据长度Bootloader解析后分配接收缓冲区。按128字节或1024字节分包接收每包校验CRC后回ACK。全部接收完Bootloader计算整体CRC确认无误后写入固件头。如果不想自己写上位机可以直接用STM32CubeProgrammer的串口下载模式它内置Ymodem协议Bootloader侧实现了Ymodem接收就能配合。命令行烧录很方便STM32_Programmer_CLI -c portCOM10 -d app.bin -ob nboot0这里注意不要用-e all全片擦除会连Bootloader一起擦掉。正确做法是用-e 1只擦App区或者干脆不擦直接下载CubeProgrammer会自动处理。A/B分区方案下上位机要额外告诉Bootloader这一包是写到A区还是B区。实现上我在包头加一个固定字段Bootloader根据这个字段选择不同的Flash起始地址。这也是热词里AB分区在工程上的落地方式原理不复杂就是把Flash一分为二两个都放固件启动时选有效的那一个执行。5. 常见问题与排查技巧实录5.1 跳转后立刻HardFault的排查顺序这是做OTA工程时最高频的问题。现象是Bootloader跑完跳转指令App没起来直接进HardFault。按我的经验90%的跳转失败都逃不出下面三个原因。第一个是栈指针不对。跳转前先打印一下App起始地址的前4个字节看是不是0x2000xxxx这样的值。如果不是检查链接脚本ORIGIN是否生效是不是还有别的地方把App的起始地址写错了。第二个是编译优化导致__disable_irq之后的外设中断标志卡住。跳转前最好把SysTick、UART、DMA等外设的中断全部禁用否则跳到App后App还没来得及重新初始化中断就来了直接跑飞。第三个是链接脚本和向量表偏移不一致。App的VTOR设置的值必须和链接脚本的ORIGIN严格一致。比如链接脚本ORIGIN是0x08010000代码里SCB-VTOR就得是0x08010000差一个字节都不行。注意Cortex-M0/M0芯片没有VTOR寄存器部分M0有比如STM32G0、L0系列。如果你是M0核向量表重定向要做成在RAM里复制一份向量表再改VTOR或者用启动时的函数指针跳转。M3/M4/M7直接设置SCB-VTOR即可。5.2 链接脚本被CubeMX重新生成覆盖CubeMX每次Generate Code都会重新生成链接脚本你在里面手动改的地址会丢。这个问题有几种解决思路。最推荐的做法是把我上面3.1节的脚本纳入构建流程每次都从CubeMX生成的原始ld再生成App的ld。这样原始ld随手可恢复App ld永远是从现有工程推导出来的不怕覆盖。这个方法也是我在项目里一直用的。还有一个做法是给CubeMX生成的文件加了只读属性但这样CubeMX自己反而会报错不推荐。另外提醒一句CubeMX生成代码后别去手动改main.c里由CubeMX管理的初始化部分。外设配置变了重新生成时会覆盖。自定义代码放在/* USER CODE BEGIN */和/* USER CODE END */注释块里这块是CubeMX保留的每次重新生成不会动。5.3 烧录到错误偏移或擦除过宽很多问题的根源是烧录工具没有正确设置Flash偏移。用OpenOCD烧App时要指定地址openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \ -c program build_app/app.bin 0x08010000 verify reset exit用st-flash则是st-flash --reset write build_app/app.bin 0x08010000最容易犯的错是擦除整片。像st-flash erase会把Bootloader也擦掉一旦擦了你就得用SWD重新烧Bootloader。所以我在烧录脚本里留了一行日志每次烧录前都打印即将操作的起始地址和长度防止手滑。另外烧录时建议用bin文件而不是elf文件。elf里包含了调试信息和链接地址信息烧录软件可能根据elf里的信息自动决定烧到哪里而且不同工具处理方式不一样不如指定bin加偏移来得直观。链接脚本生成bin文件的规则我在Makefile里加了这样一段$(BUILD_DIR)/$(TARGET).bin: $(BUILD_DIR)/$(TARGET).elf arm-none-eabi-objcopy -O binary $ $5.4 编译通过但启动跑飞与CRC校验还有一种情况是编译链接都OK但设备上电后行为诡异有时能跑有时不能。这种情况多半是固件头校验逻辑不严导致的。我举一个真实案例。有一次设备升级后重启有时正常有时卡死在Bootloader。排查后发现问题出在固件头的CRC字段我当时为了省事CRC只算了固件正文没算固件头本身而且长度字段在链接脚本里没有自动填充实际写入的值是0。Bootloader读取长度0校验的逻辑直接绕过去了结果等于没校验。后来我把固件大小和CRC的填充彻底自动化了。在build_all.sh里加了这样一段用python读取生成的bin文件计算CRC和大小再回填到头文件import zlib with open(build_app/app.bin, rb) as f: data f.read() crc zlib.crc32(data) 0xffffffff size len(data) print(fapp.bin size{size} crc0x{crc:08X})然后把这两个值写成头文件编译时让Bootloader引用。这样每次构建固件头信息都能和固件本体严格对应避免了手填导致的不一致。这算是我踩过坑后总结出来的关键经验。还有一个小建议不要直接在Bootloader里用HAL库的Flash写函数去写正在执行的Flash段。Bootloader本身在Flash开头App区在后面写App区一般没问题但如果你把固件烧到了Bootloader自己的区域芯片会直接进入HardFault。所以Bootloader在接收固件前一定要判断待写入地址是否落在自己的区域之外。写在最后的一点经验这套CubeMX加VS Code的自动化分区方案我用在好几个量产项目上了稳定性比当年Keil手动改分区高了不少。最大的体会是嵌入式工程的痛苦很多时候不是来自硬件或业务代码而是来自构建过程的不可控。把链接脚本、编译参数、固件头、烧录地址全部纳入脚本管理之后一个人维护多个固件的成本明显下降。如果你现在还在为OTA分区手动改地址建议抽一个下午把这套流程搭起来。第一次理顺可能花点时间但之后每次发版、每次改Flash布局你都会觉得当初的投资太值了。最后再分享一个小技巧不要把Bootloader和App当成两个完全割裂的工程来维护尽量让它们共享同一个CubeMX工程、同一套链接脚本生成工具、同一套编译脚本。代码仓库里只保留一份工程配置两份固件的差异全部收敛在Makefile变量和脚本参数里。这样无论后续怎么改Flash布局改的都是一处跑一遍脚本两个固件同步更新不会出现Bootloader改了地址、App忘了同步这种低级事故。
延伸阅读

更多相关文章

2026/10/5 1:27:13

微信小程序+Java琴房管理系统毕业设计实战指南

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

2026/10/5 1:27:13

点云到地图:扫地机器人SLAM与Nav2全链路实战

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

2026/10/5 1:27:13

MRAM与PIC18F4550工业数据存储方案:SPI驱动与可靠性设计

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

2026/10/5 2:37:15

DeepSeek教学反思方案:课堂实录清洗、标注与行为模式挖掘实战

简介:面向教学反思数字化转型需求,这份535页的方案文档以DeepSeek-NLP为技术底座,围绕课堂实录文本自动标注与教学行为模式挖掘展开,适合教育研究者、一线教师、教研人员及NLP应用开发者参考。文档为单个PDF文件,压缩包…

2026/10/5 2:37:15

三甲医院知识库DeepSeek微调与内网部署实战:从文档解析到RAG问答

简介:这份PDF文档面向医疗信息化从业者、AI应用开发者及希望将大模型落地医疗场景的技术人员,系统讲解基于DeepSeek构建三甲医院知识库问答系统的完整实战路径。内容从医疗问答系统的背景意义切入,梳理三甲医院知识库的数据来源与特点&#x…

2026/10/5 2:37:15

YOLOv11边缘部署实战:量化压缩与NPU加速全链路解析

简介:围绕YOLOv11在边缘计算场景中的高效部署需求,这份32页PDF手册系统讲解模型量化压缩与NPU加速的完整技术链路,面向算法工程师、边缘计算开发者及计算机视觉学习者。内容按章节组织,从边缘计算与YOLOv11概述入手,分…

2026/10/5 2:37:15

175种ChatGPT指令模板拆解:提示工程师的工程化实践指南

简介:这份资源是面向AI提示词工程师、内容创作者与对话产品开发者的实战指令合集,围绕ChatGPT等大模型的内容生成能力,整理出175种可直接套用的训练指令模板,帮助使用者解决提示词设计零散、输出质量不稳定、场景覆盖不全等问题。…

2026/10/5 2:37:15

基于Java+SpringBoot+Vue的学生体质健康管理系统毕设实战与避坑指南

简介:这是一套面向高校计算机专业毕业设计的完整资料,主题为基于Java的学生体质健康管理系统,适合正在准备毕设的本科生及需要参考SSM/SpringBoot项目实战的开发者。资源包含论文与源码两部分,论文按绪论、相关技术、系统分析、系…

2026/10/4 0:01:02

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/4 0:01:02

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/4 1:01:05

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

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

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑