
简介面向STM32F091平台移植完成的openBLT引导加载程序工程作者充分考虑了RAM的使用可在资源有限的Cortex-M0处理器上稳定运行。压缩包内共522个文件约22.76MB主要包含C源码与头文件、Keil集成开发环境工程文件、编译过程生成的中间文件、链接脚本、固件输出文件以及工程备份能够完整还原Boot与Prog双工程目录。已有562人学习下载。工程在完成基础移植的同时给出了Boot与应用程序的RAM区域划分方案涉及向量重定位、中断入口以及FOTA更新流程配套的HAL驱动源码可帮助核对I2C、定时器、串口、CAN等外设初始化。通过研读这份工程可以快速理解openBLT在STM32F091上从编译、链接到最终固件生成的完整链路为后续二次开发省去大量踩坑时间。整体目录层级清晰工程可直接导入尝试适合作为学习与二次开发的起点。1. openBLT 移植完成的工程 stm32f091为什么这颗 M0 芯片让 bootloader 移植变得不按套路走“openBLT 移植完成的工程 stm32f091”这句话我最初没太当回事——bootloader 移植嘛改改起始地址、调调链接脚本半天应该能交差。真正动手才发现F091 这颗芯片把 openBLT 的常规移植流程打破了大半M0 内核没有 VTOR中断向量表没法像 M3/M4 那样简单偏移Flash 是 2KB 一页擦除驱动和 F1 完全不同CAN 虽然是 bxCAN但时钟树和寄存器位定义都在细节上别着劲。这篇笔记把我在模拟项目X上从零把 openBLT 跑通到 STM32F091 的过程拆开讲哪些判断决定移植成败、Flash 和 CAN 驱动怎么改、跳转怎么绕开 M0 的硬件限制以及最后验证固件升级的完整流程。适合手里拿着 F091/F072 这类 M0 芯片、正被 CAN 升级方案卡住的嵌入式开发者。2. 移植前先认清 F091 与 openBLT 的三个边界问题2.1 官方例程覆盖不到 F091直接套 F1 的例程会踩哪些硬件差异openBLT 源码包里对 STM32 家族的支持并不是所有型号一碗水端平F1、F2、F4 这些老牌型号有现成工程F0 系列里能直接用的参考工程很少F091 基本属于要自己动手的那一类。这时候最省事的想法是拿 STM32F1 的例程来改但我劝你先停下来对比一下硬件差异因为 F091 和 F103 虽然都叫 STM32移植里要动的底层完全是两套思路。先把最影响移植的差异列出来这张表我在动手写代码之前反复确认过对比项STM32F091STM32F103中密度内核Cortex-M0无 VTORCortex-M3有 VTORFlash 页大小2KB/页1KB/页Flash 编程位宽32 位字16 位半字系统时钟上限48MHz单 APB 总线72MHzAPB1/APB2 双总线CAN 控制器bxCAN3 个 TX 邮箱bxCAN3 个 TX 邮箱Flash 等待状态0~1 个48MHz 时需 1 个0~2 个72MHz 时需 2 个这张表里最要命的不是内核频率而是两个点第一M0 没有 VTOR中断向量表无法用寄存器偏移这是 F091 移植和 F1 移植最大的分水岭第二Flash 页大小和编程位宽不同意味着擦除循环的次数、每次写 Flash 的字节数全要改。如果直接套 F1 的 flash 驱动最典型的翻车是擦除时把用户程序区算错页数把不该擦的地方抹了编程时按 16 位半字写写出来的数据错位。我一般会先用芯片参考手册把这三个外设确认一遍再做移植Flash 控制器的 CR/SR 寄存器位定义、CAN 控制器的 BTR 位时序、以及 SYSCFG 的重映射机制。把这三个点搞清楚移植工程就成功了一半。2.2 内存布局先定下来bootloader 区、用户区与 2KB 页对齐openBLT 移植的第一步不是写代码而是先划分 Flash 地址空间。F091 的 Flash 从 0x08000000 开始共 256KB页大小 2KB所以任何分区的边界都必须落在 2KB 的整数倍上否则后面擦除驱动写起来会多出一堆边界判断。我在这类移植里常用的分区方案是Bootloader 区占 32KB0x08000000 ~ 0x08007FFF用户程序区从 0x08008000 开始剩下 224KB 给应用。Bootloader 区留得比我实际需要的大了一倍原因是 openBLT 把 CAN 驱动、Flash 驱动、传输层、seed/key 校验这些模块全编进去后16KB 会比较紧32KB 给后续扩展留了余地而且 32KB 正好是 16 页对齐擦除逻辑简单。区域起始地址结束地址大小用途Bootloader0x080000000x08007FFF32KBopenBLT 固件用户程序0x080080000x0803FFFF224KB应用固件选项字节区0x1FFFF8000x1FFFF80F16B写保护/启动配置这个分区表的含义要在 openBLT 的配置头文件和用户工程的链接脚本里保持一致。openBLT 侧需要把应用区起始地址配成 0x08008000用户工程链接脚本的 Flash 起始地址也必须指向同一个位置两个地址一旦对不上上位机能连上 bootloader但下载完成跳转后程序一定跑飞。这个配置错位的坑后面避坑章节会专门提。2.3 M0 没有 VTORopenBLT 在 F091 上怎么解决中断向量表Cortex-M3/M4 上解决中断向量表偏移非常简单写一个 SCB-VTOR 新地址 就完事。但 STM32F091 的内核是 Cortex-M0ARMv6-M 架构里根本没有 VTOR 这个寄存器CPU 取中断向量永远从 0x00000000 开始。复位后 0x00000000 映射的是 Flash 首地址也就是 bootloader 的向量表用户程序在 0x08008000 跑起来后一旦发生中断CPU 查到的还是 bootloader 的向量整个中断逻辑全乱套。F0 系列上标准解法是利用 SYSCFG 的存储重映射功能把 SRAM 映射到 0x00000000。具体做法分两步先把用户程序的向量表从 0x08008000 复制到 SRAM 起始地址 0x20000000再把 SYSCFG_CFGR1 的 MEM_MODE 位设为 SRAM 映射。这样 CPU 查中断向量表时实际访问的是 SRAM 里的用户向量表。#include string.h #include stm32f0xx.h #define USER_VECTOR_TABLE_ADDR 0x08008000uL /* 用户程序向量表实际位置 */ #define RAM_VECTOR_TABLE_ADDR 0x20000000uL /* 向量表在 SRAM 中的位置 */ #define VECTOR_TABLE_SIZE 0xC0uL /* 48 个中断向量 192 字节 */ void RemapVectorTable(void) { /* 先把用户程序的向量表整体拷贝进 SRAM 开头 */ memcpy((void *)RAM_VECTOR_TABLE_ADDR, (void *)USER_VECTOR_TABLE_ADDR, VECTOR_TABLE_SIZE); /* 清掉 MEM_MODE 两位再设为 SRAM 映射(0x2) */ SYSCFG-CFGR1 ~((0x3uL) 24); SYSCFG-CFGR1 | (0x2uL) 24; }这段代码的关键在于 0x08008000 和 0x20000000 这两个地址要跟前面分区表严格对齐。向量表大小 0xC0 字节是按 F091 的 48 个中断向量算的如果你的用户程序实际使用的中断没有那么多也可以只拷贝到最后一个用到的中断号但我建议保守一点整表拷贝省得以后新增中断时踩坑。另外要注意SRAM 开头这 192 字节被向量表占用后用户工程的 RAM 起始地址就不能再从 0x20000000 开始了链接脚本里要往后挪不然启动代码的 .data 段初始化会把向量表冲掉。3. 改 Flash 驱动F091 的 2KB 页擦除与 32 位编程3.1 openBLT 需要的 Flash 底层接口F091 与 F1 的差异点openBLT 的移植分层很清楚Bootloader 主体代码不直接操作寄存器而是通过一组 Flash 底层函数完成擦除和编程。移植到新芯片时需要实现的接口大概有初始化、擦除、写入、完成校验这几类。F091 和 F1 在这些接口内部实现的差异集中在三个方面。第一个差异是擦除粒度。F1 中密度器件的 Flash 页是 1KBF091 是 2KB所以擦除一个区域时循环次数差了整整一倍如果沿用 F1 的循环条件擦除范围会只做一半。第二个差异是编程位宽。F1 的 Flash 编程是按 16 位半字写的F091 要求 32 位字写入写之前必须确认源数据和目标地址都是 4 字节对齐长度也要是 4 的倍数多出来的尾巴字节要单独处理。第三个差异是寄存器位定义F0 的 Flash 状态寄存器里错误标志的位置和 F1 不完全一样轮询和错误判断都要按参考手册重新对一遍。我一般把这三个差异写在驱动文件的注释里再对照参考手册的寄存器图逐位确认。F091 的 Flash 操作流程总结下来就四句话先等 BSY 清零再解锁 KEYR然后按页擦除或按字编程最后查状态寄存器的错误位。下面两个小节给出可以直接抄的寄存器级实现。3.2 页擦除函数寄存器时序与看门狗处理页擦除是 openBLT 下载固件时调用最频繁的操作F091 上一页是 2KB擦除一页的时间在毫秒级。如果主循环里有硬件看门狗在跑擦除前一定要确认喂狗机制否则擦除还没结束看门狗就先复位了表现为下载到一半目标板重启上位机报通信超时。static void FlashErasePage(uint32_t pageAddr) { /* 等待 Flash 空闲上一操作未结束前不能发新命令 */ while ((FLASH-SR FLASH_SR_BSY) ! 0) ; /* 解锁 FLASH_CR序列固定倒过来写也不行 */ FLASH-KEYR 0x45670123uL; FLASH-KEYR 0xCDEF89ABuL; /* 置页擦除模式 */ FLASH-CR | FLASH_CR_PER; /* 写入要擦除的页首地址2KB 低 11 位按页内偏移处理 */ FLASH-AR pageAddr; /* 启动擦除 */ FLASH-CR | FLASH_CR_STRT; /* 等待擦除完成 */ while ((FLASH-SR FLASH_SR_BSY) ! 0) ; /* 退出页擦除模式避免误擦 */ FLASH-CR ~FLASH_CR_PER; }这段代码有四个地方容易写错。一是 unlock 序列必须每次操作前重新执行因为一次 Flash 操作完成后 LOCK 位可能被硬件重新置位跳过解锁直接写 CR 的话寄存器完全没反应。二是 AR 寄存器保存的是页首地址地址低 11 位是页内偏移写入时会被忽略所以调用方传参时必须是 2KB 对齐的地址。三是擦除完成后应该主动退出 PER 模式不然下次误置 STRT 会重复擦同一页。四是擦除完要顺手查一下 SR 的 EOP 位和错误位生产环境里不能只等 BSY 清零就认为成功了。openBLT 擦除一个完整区域时通常按起始地址循环到结束地址每 2KB 调一次上面的函数。注意循环边界如果应用区大小不是 2KB 的整数倍最后一次循环的地址要补足一页否则固件末尾那一小段会残留旧数据启动后 CRC 校验失败。3.3 编程函数32 位字写入、对齐与错误位检查F091 的 Flash 编程和擦除共用同一个控制流程差别只在 CR 寄存器的工作模式。编程时把 CR 的 PG 位置 1然后直接向目标地址写 32 位数据硬件会自动触发编程时序写完一个字后要等 BSY 清零再写下一个字。uint8_t FlashWrite(uint32_t dstAddr, const uint32_t *srcBuf, uint32_t len) { uint32_t i; /* 目标地址和长度都必须是 4 字节对齐这是 F091 的硬性要求 */ if (((dstAddr 0x3uL) ! 0) || ((len 0x3uL) ! 0)) return 0; while ((FLASH-SR FLASH_SR_BSY) ! 0) ; FLASH-KEYR 0x45670123uL; FLASH-KEYR 0xCDEF89ABuL; /* 置编程模式 */ FLASH-CR | FLASH_CR_PG; for (i 0; i len / 4uL; i) { /* 向目标地址写整字写入动作触发编程 */ *(volatile uint32_t *)(dstAddr i * 4uL) srcBuf[i]; while ((FLASH-SR FLASH_SR_BSY) ! 0) ; /* 编程错误或写保护错误都要停下来 */ if ((FLASH-SR (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) ! 0) { FLASH-CR ~FLASH_CR_PG; return 0; } } /* 退出编程模式 */ FLASH-CR ~FLASH_CR_PG; return 1; }要注意这里的 len 是字节数而循环步进是按 4 字节一个字处理的。如果上位机下发的固件最后一段长度不是 4 的倍数openBLT 的通信层通常会把末尾补 0xFF 后再调写入函数但保险起见我在移植里还是加了对齐校验传错就直接返回失败避免写坏相邻区域。等待 BSY 清零这个动作是最容易被忽略的优化点很多人为了省时间只在每个字写入后查一次状态但 F091 的编程时序要求上一个字写完、BSY 真正清零后才能发下一个字。还有一点PG 模式结束后 CR 的 LOCK 位不会自动置位我习惯手动FLASH-CR | FLASH_CR_LOCK;防止其他模块的误操作碰掉 Flash 内容这个小习惯在复杂工程里能省不少排查时间。4. 时钟树与 bxCAN 驱动把通信层接到 F091 上4.1 48MHz 系统时钟与 Flash 等待状态先于一切外设配置F091 的 CAN 外设挂在系统 APB 总线上APB 不分频所以系统时钟是多少CAN 外设时钟就是多少。openBLT 的 CAN 波特率配置全部基于这个时钟算出来的时钟一错CAN 要么完全不通要么通几十秒后开始疯狂报错误帧这是移植中比较隐蔽的翻车点。我在这类项目里的习惯是先把系统时钟固定到 48MHz用的是内部 HSI 8MHz 经 PLL 倍频 6 倍不依赖外部晶振减少硬件差异带来的干扰。F091 在 48MHz 下 Flash 需要 1 个等待周期这个必须在切换系统时钟前配好顺序反了会在高频切换的瞬间取指失败。初始化代码大致是这样一个流程void SystemClockInit(void) { /* 开启 HSI 并等待就绪 */ RCC-CR | RCC_CR_HSION; while ((RCC-CR RCC_CR_HSIRDY) 0) ; /* PLL 源选 HSI倍频 6 倍 - 8MHz * 6 48MHz */ RCC-CFGR ~RCC_CFGR_PLLSRC; RCC-CFGR ~RCC_CFGR_PLLMUL; RCC-CFGR | RCC_CFGR_PLLMUL6; /* 开启 PLL 并等待锁定 */ RCC-CR | RCC_CR_PLLON; while ((RCC-CR RCC_CR_PLLRDY) 0) ; /* 切到 48MHz 之前先给 Flash 配上 1 个等待周期 */ FLASH-ACR | FLASH_ACR_LATENCY; /* 系统时钟切到 PLL */ RCC-CFGR | RCC_CFGR_SW_PLL; while ((RCC-CFGR RCC_CFGR_SWS_PLL) 0) ; }HSI 的精度在常温下一般能到 1% 以内对 CAN 通信来说 1% 的时钟偏差有点紧但 openBLT 握手和短报文传输通常能撑住。如果你的板子上有 8MHz 外部晶振我建议改用 HSE时钟精度会好很多CAN 长时间连续传输时的错误帧会明显减少。HSI 方案的优点是省晶振、上电就能跑缺点是 USB 和 CAN 同时长时间工作时稳定性略差开发阶段先用 HSI 跑通流程量产前再评估要不要切 HSE。4.2 bxCAN 初始化引脚映射、位时序与滤波器F091 的 CAN 控制器是 bxCAN和 F1 的寄存器风格接近但有两点必须确认一是引脚映射F091 的 CAN 默认引脚是 PA11/PA12部分封装可以通过 SYSCFG 重映射到 PB8/PB9板子走线用哪组引脚代码里就要对应配好二是时钟使能位F091 的 CAN 时钟挂在 AHB 上而不是 APB1这个和 F1 不一样很多人移植时在这里把 RCC 的寄存器写错导致 CAN 模块时钟没开寄存器怎么写都没有反应。void CanInit(uint32_t baudrate) { /* 使能 CAN 时钟注意 F091 挂在 AHB 总线 */ RCC-AHBENR | RCC_AHBENR_CANEN; /* 配置 CAN 引脚为复用功能PA11RX, PA12TX */ GPIOA-MODER ~(GPIO_MODER_MODER11 | GPIO_MODER_MODER12); GPIOA-MODER | (GPIO_MODER_MODER11_1 | GPIO_MODER_MODER12_1); GPIOA-AFR[1] | (0x4uL 12) | (0x4uL 16); /* 请求进入初始化模式 */ CAN-MCR | CAN_MCR_INRQ; while ((CAN-MSR CAN_MSR_INAK) 0) ; /* 位时序按 48MHz 计算500kbps 为例 */ CAN-BTR (0uL 24) /* SJW1 */ | (5uL 20) /* BRP5, 预分频 6 */ | (13uL 16) /* TS113 */ | (2uL 0); /* TS22 */ /* 退出初始化模式 */ CAN-MCR ~CAN_MCR_INRQ; while ((CAN-MSR CAN_MSR_INAK) ! 0) ; /* 配置过滤器 0接收所有报文openBLT 在应用层过滤 ID */ CAN-FMR | CAN_FMR_FINIT; CAN-FSR | CAN_FS_FSC0; CAN-FFA | CAN_FFA_FFA0; CAN-FA1R | CAN_FA1R_FACT0; CAN-FMR ~CAN_FMR_FINIT; }bxCAN 的位时序寄存器里SJW、BRP、TS1、TS2 四个参数共同决定波特率公式是波特率 48MHz / ((BRP1) * (1 TS1 TS2))。我上面这组参数算下来是48M / (6 * 16) 500kbps采样点约 87.5%是 CAN 采集节点常用的配置。如果你的总线负载很高采样点可以压到 80% 左右把 TS1 调小一格但标准 openBLT 的上位机默认参数要和你这里保持一致改之前先确认上位机那边能不能配。4.3 波特率公式与采样点参数表500kbps 和 250kbps 两组推荐值波特率参数是 openBLT 移植里最容易出现“能连上但传大固件失败”的区域。原因很简单握手报文短波特率轻微偏差也能通过固件数据长累计几百帧后采样点偏差就会造成收发器把错误帧丢掉。我习惯在移植前先用上位机的 CAN 工具测一下总线通信质量再定参数。波特率BRPTS1TS2SJW预分频总时间段采样点500kbps5132161687.5%250kbps111321121687.5%这两组参数都基于 48MHz 的 APB 时钟如果你改成用 HSE 12MHz 这类非 8MHz 晶振BRP 要重新算。还有一个细节openBLT 的 CAN 传输层用的是标准帧还是扩展帧、收发的 CAN ID 各是多少这些在配置头文件里都有默认值移植时不要只改波特率不改 ID。某次我帮同事排查一个“上位机能搜到节点但下载就超时”的问题最后发现是他的另一个 CAN 报文占用了相同的 ID总线上一有干扰就打断传输。这个排查方向可以在移植完成后先验证一下。5. openBLT 移植后的高频避坑现象、原因与解法5.1 跳转用户程序后中断集体失灵向量表根本没进 SRAM现象bootloader 下载固件成功跳转后用户程序主循环在跑但只要一开中断就死机或者中断完全不响应。原因F091 的 M0 核心没有 VTOR中断向量表始终查 0x00000000 地址。跳转前没有把用户向量表拷贝到 SRAM也没有做 SYSCFG 重映射CPU 拿到的还是 bootloader 的中断向量。解决确认跳转用户程序前调用了 RemapVectorTable 类似的函数。这里有个很容易漏掉的点如果你用调试器直接加载用户程序调试不走 bootloader重映射逻辑在用户程序启动代码里也要存在。我一般在用户程序的 SystemInit 里也放一份同样的重映射代码保证两条启动路径下中断都正常。5.2 擦除范围算错把用户程序头部擦没了现象固件下载到约 20% 时失败重新上电后 bootloader 还能连上但用户程序校验一直失败。原因openBLT 的擦除接口是按起始地址和长度计算页数的如果长度不是 2KB 的整数倍最后一个不完整页没有被纳入擦除范围新固件写到最后一个不完整页时页里残留旧数据导致校验不一致。反向的错误是把结束地址算大了把用户程序后面紧挨着的数据区也擦掉了。解决擦除循环的结束条件改成start len (2KB - 1)再向下取整到 2KB 边界保证覆盖全部受影响页。我在移植实现的擦除函数里加了一行日志把每次擦除的起始页和页数打出来第一次移植时用上位机下载一个整固件回读日志和 flash 实际内容比对一次就能发现边界问题。5.3 Flash 写入报 WRPRTERR解锁流程和保护位同时出问题现象上位机连接正常握手正常但每次写 Flash 都报写保护错误日志里能看到状态寄存器的 WRPRTERR 位置位。原因F091 的 Flash 在复位后默认是锁定的写入前必须先写解锁序列。有些移植为了省事把解锁放在初始化函数里只做一次但 F091 在每次擦除或编程操作结束后硬件会重新锁定 CR 寄存器。另一种可能是芯片的选项字节里 WRP 区域被设置过把用户程序区写保护了。解决把解锁序列放在每次擦除和编程函数的内部操作前先检查 SR 的 BSY再写 KEYR。如果确认代码没问题用下载器读一下选项字节把 WRP 对应的页清掉。开发阶段建议把 RDP 等级保持在 Level 0不然调试器重连也会被锁出去。5.4 上电直接跳走进不去 bootloader 等待窗口现象每次上电用户程序都能跑但怎么触发都进不了 bootloader上位机永远搜不到节点。原因openBLT 上电后会先检查用户程序区是否有效如果 CRC 校验通过就在短暂等待窗口后直接跳转。等待窗口太短或者跳转条件判断被配置成了“不检查触发信号直接跳”导致没有时间响应外部的编程请求。解决开发阶段把等待窗口调大到 1 到 2 秒用按键或者上电时拉高的电平作为进入 bootloader 的触发条件。同时确认用户程序的 CRC 校验标志位生成正确如果用户程序从来没写过有效的 CRC 尾部信息bootloader 反而会把有效程序也当成无效每次都停在 boot 等下载看起来像“用户程序丢了”。5.5 跳转前外设没有复位用户程序起来后串口乱码、CAN 报错现象单独调试用户程序时串口和 CAN 都正常但从 bootloader 跳转过去后串口输出乱码CAN 总线上不断发错误帧。原因bootloader 初始化过的 GPIO、UART、CAN、SysTick 外设在跳转时还保留着 bootloader 配置的状态。用户程序的启动代码通常只会初始化自己用到的外设不会把全部外设复位恢复默认于是出现时钟、复用功能、波特率寄存器互相打架的情况。解决跳转函数里加一个外设复位步骤把 bootloader 用过的外设 RCC 复位全部置位再清除最后关中断、清 SysTick、把挂起的中断清掉再跳转。我建议把跳转函数固定成一段独立代码不要分散在多个文件中改这样每个基于 F091 的项目都能直接复用。6. 移植完成的验收方法搭一条最小回环把升级流程跑通6.1 至少跑三笔测试连接、坏固件、好固件移植完成后先别急着发给别人用我一般用一套固定的验收流程整个流程大概十分钟就能跑完但能挡住绝大部分回归问题。第一笔测连接上电后触发进入 bootloader确认上位机能稳定握手反复拔插 CAN 线三次每次都能重新连上。第二笔测坏固件下载一个故意改坏的固件文件确认 bootloader 校验失败后停留在 boot 等待状态不会跳转执行。第三笔测好固件下载正常固件确认跳转后用户程序功能完整再主动触发一次跳回 bootloader确认 backdoor 入口可重复往返。这个流程不仅测了 Flash 驱动和 CAN 驱动还把向量表重映射、CRC 校验、跳转逻辑全部覆盖了一遍。我见过不少项目移植完只测了正常升级一次就放出去了结果现场固件刷写失败后目标板完全变砖就是因为坏固件路径从来没验证过。6.2 回读校验技巧脚本比对 bin 与 flash 内容比跑三次固件更严格的做法是加一道回读校验。openBLT 上位机本身有校验机制但那是基于传输层的数据校验不能证明 flash 里的内容排布正确。我在验收时会把下载完成的 flash 内容整体回读出来和本地编译出的 bin 文件做逐字节比对重点看两个位置用户程序起始地址的向量表和末尾的 CRC 校验数据。这段回读流程用一个简单的 Python 脚本就能完成核心逻辑是从 flash 起始地址读 N 个字节和 bin 文件内容做 diff任何不一致就把地址和期望值、实际值一起打出来。第一次跑通这个脚本时我发现固件末尾 2 字节总是被填充成 0x00 而不是 0xFF追下去才发现是链接脚本里没用--fill选项。补上之后跳转后程序跑得比以前稳定很多。移植 bootloader 这种底层工作最大的教训就是别相信“一次编译通过”就等于能用。某开发者分享过一句话我一直记着bootloader 是给用户程序擦屁股的它自己出了问题用户连后悔药都没地方买。我现在的习惯是所有 flash 操作函数都保留状态返回和日志输出哪怕多占几十个字节的 flash 空间换来的是现场出问题时能直接定位到是擦除失败、编程失败还是跳转失败。希望这篇 F091 的移植笔记能帮你绕开我踩过的这些坑把 openBLT 顺利跑起来。本文还有配套的精品资源点击获取