# STM32L051K6U6 IAP Bootloader 开发踩坑实录

STM32L051K6U6 IAP Bootloader 开发踩坑实录

从 F413 移植 IAP 到 L051,32KB Flash、8KB RAM,LL 库,Keil MDK-ARM 编译器。

核心教训: 不要被"M0+ 架构简单"迷惑,它的 Flash 控制器(PECR)坑比想象中的多。


目录

  1. 硬件背景
  2. [问题 1: Flash 页大小搞错了](#问题 1: Flash 页大小搞错了)
  3. [问题 2: 页擦除触发写入了 0x00000000(写入 vs 读取触发)](#问题 2: 页擦除触发写入了 0x00000000(写入 vs 读取触发))
  4. [问题 3: PRGLOCK 必须用密钥序列解锁](#问题 3: PRGLOCK 必须用密钥序列解锁)
  5. [问题 4: 跨页写入没擦够页------死机](#问题 4: 跨页写入没擦够页——死机)
  6. [问题 5: 8KB RAM 根本不够用](#问题 5: 8KB RAM 根本不够用)
  7. [问题 6: Flash 布局必须给 VTOR 留对齐空间](#问题 6: Flash 布局必须给 VTOR 留对齐空间)
  8. [问题 7: 跳转到 APP 后串口中断不工作](#问题 7: 跳转到 APP 后串口中断不工作)
  9. [问题 8: 断电后读保护被意外使能(最离谱的问题)](#问题 8: 断电后读保护被意外使能(最离谱的问题))
  10. [问题 9: 跳转 APP 前缺少外设复位](#问题 9: 跳转 APP 前缺少外设复位)
  11. 最终验证结果
  12. 总结

硬件背景

参数
MCU STM32L051K6U6 (Cortex-M0+)
Flash 32KB, 所有页 128 字节, 256 页
RAM 8KB
工具链 Keil MDK V5.32, ARM Compiler 5, -O4
纯 LL 驱动 (STM32L0xx_LL_Driver)
串口 USART1, 115200 8N1

Flash 控制器是 PECR(Program/Erase Control Register),不是 F4/F1 系列那种 FLASH_CR。


问题 1: Flash 页大小搞错了

症状: 页地址计算错误,写入位置不对。

原因: 惯性思维,以为 STM32L0 的 Flash 像 F1/F4 一样有混合页大小(前几 KB 小页,后面大页)。

真相: STM32L0x1 系列 < 256KB Flash 的型号,所有页都是 128 字节,没有例外。

c 复制代码
// 错误:以为有 1KB 的大页
#define FLASH_PAGE_SIZE_128     128
#define FLASH_PAGE_SIZE_1K      1024

// 正确:全部统一
#define FLASH_PAGE_SIZE     128     /* 所有页均为 128 字节 */
#define FLASH_TOTAL_PAGES   256     /* 总页数: 32KB / 128B */

教训: 没看 RM0377 之前不要凭经验写 Flash 驱动。


问题 2: 页擦除触发写入了 0x00000000(写入 vs 读取触发)

症状: 擦除后页内数据变成 0x00000000,不是期望的 0xFFFFFFFF。相当于对整个页执行了编程操作。

原因: 看参考手册不仔细。RM0377 A.3.11 明确要求页擦除触发方式是写入 0x00000000 到页首地址。早期代码错用了读取操作。

c 复制代码
// 错误:读取不会触发擦除,反而可能触发意外行为
(void)(*(vu32 *)page_addr);

// 正确:写入 0x00000000 触发页擦除
*(__IO uint32_t *)page_addr = 0x00000000UL;

// 等 BSY=0
while (FLASH->SR & FLASH_SR_BSY) {}

同时还要设置 PROG 位: PECR 的 ERASE 位和 PROG 位必须同时置 1 才能触发擦除。只设 ERASE 不设 PROG,写触发字会被忽略。

c 复制代码
// 页擦除标准序列 (RM0377 A.3.11)
FLASH->PECR |= FLASH_PECR_ERASE;
FLASH->PECR |= FLASH_PECR_PROG;
*(__IO uint32_t *)page_addr = 0x00000000UL;
while (FLASH->SR & FLASH_SR_BSY) {}           // 等待 BSY=0
if (FLASH->SR & FLASH_SR_EOP) FLASH->SR = FLASH_SR_EOP;  // 清 EOP
FLASH->PECR &= ~(FLASH_PECR_ERASE | FLASH_PECR_PROG);     // 清位

问题 3: PRGLOCK 必须用密钥序列解锁

症状: 写入 Flash 后读回还是 0xFFFFFFFF,写入静默失败。

原因: STM32L0 的 PECR 控制器有两把锁 :PELOCK(PECR 写保护)和 PRGLOCK(编程保护)。PRGLOCK 不能像 F1 系列那样直接写寄存器位清除,必须通过 PRGKEYR 写入两把密钥解锁

c 复制代码
// 错误:直接位操作无效(L0 不支持)
FLASH->PECR &= ~FLASH_PECR_PRGLOCK;

// 正确:使用密钥序列
FLASH->PRGKEYR = 0x8C9DAEBF;   // PRGKEY1
FLASH->PRGKEYR = 0x13141516;   // PRGKEY2

三种锁的解锁:

寄存器 KEY1 KEY2
PELOCK PEKEYR 0x89ABCDEF 0x02030405
PRGLOCK PRGKEYR 0x8C9DAEBF 0x13141516
OPTLOCK OPTKEYR 0xFBEAD9C8 0x24252627

问题 4: 跨页写入没擦够页------死机

症状: 传输到第 4 包就超时死机,需要重新上电。

原因: STMFLASH_EraseAndWrite 只擦除了起始页(128 字节),但写入数据跨了 16 页(2048 字节)。写入未擦除的页 → Flash 控制器报错 → 总线错误 → HardFault。

c 复制代码
// 错误:只擦了一页
Flash_ErasePage(addr);                     // 只擦 1 页 (128B)
STMFLASH_Write(addr, buf, 512words);       // 写 16 页 (2048B) ← 跨页崩溃

// 正确:擦除所有涉及的页
first_page = GetPageNum(WriteAddr);
last_page  = GetPageNum(WriteAddr + NumToWrite * 4 - 1);
for (page = first_page; page <= last_page; page++) {
    Flash_ErasePage(GetPageAddr(page));
}
STMFLASH_Write(WriteAddr, pBuffer, NumToWrite);

最终方案: 每包改为 128 字节(刚好 1 页),不再跨页写入,简化逻辑:

c 复制代码
#define CACHE_SIZE 128  // 一页大小
iap_write_appbin(addr, buf, 128);  // 每包 128B,刚好 1 页

问题 5: 8KB RAM 根本不够用

症状: 链接器报错 Execution region RW_IRAM1 size (9728 bytes) exceeds limit (8192 bytes)

原因: STM32L051K6U6 只有 8KB RAM,而全局缓冲区一不小心就超了:

变量 大小 说明
iapbuf[512] 2048 512 个 word = 2KB
flash_cache[2048] 2048 缓存
updatefilebuf[2048] 2048 被 iapbuf 替代后注释掉
USART1BUF[600] 600 串口接收缓冲
updatebuf[512] 512 命令帧缓冲
Stack + Heap 1536 1KB + 0.5KB
总计 ~9.5KB 超了 1.5KB

解决: 把所有缓冲区压缩到极致:

c 复制代码
#define CACHE_SIZE    128    // 2048 → 128
#define iapbuf        32     // 512 word → 32 word (128B)
#define updatebuflen  128    // 512 → 128
#define UARTLEN       600    // 保持

最终 RAM 占用压到 ~4KB,留出充分余量。


问题 6: Flash 布局必须给 VTOR 留对齐空间

症状: 跳转到 APP 后,一旦发生中断就跑飞到 Bootloader。

原因: STM32L0 的 Cortex-M0+ 支持 VTOR(向量表偏移寄存器) 。根据 RM0377,VTOR 要求256 字节对齐VTOR[7:0] 必须为 0)。

原始中间布局中,APP 地址 = 0x08002C80

复制代码
0x08002C80 → 低字节 0x80 ≠ 0x00 → ❌ 非 256 字节对齐
0x08002C00 → 低字节 0x00 = 0x00 → ✅  256 字节对齐

VTOR 不能指向 0x08002C80,之前的方案需要用 SRAM 中转复制向量表(非常麻烦)。

解决: 重新调整 Flash 布局,把标志位和 APP 往前挪一页:

复制代码
旧布局:
  Bootloader: 0x08000000 ~ 0x08002BFF  (11KB)
  标志位:     0x08002C00                (页88)
  APP:        0x08002C80                (页89) ← 非256字节对齐 ❌

新布局:
  Bootloader: 0x08000000 ~ 0x08002B7F  (<11KB)
  标志位:     0x08002B80                (页87) ← 往前挤了一页
  APP:        0x08002C00                (页88) ← 256字节对齐 ✅

这样 VTOR 可以直接指向 0x08002C00,省去 SRAM 中转的麻烦:

c 复制代码
SCB->VTOR = 0x08002C00;  // 256字节对齐,直接指向Flash

问题 7: 跳转到 APP 后串口中断不工作

症状: APP 正常启动,printf 能正常输出,但 $msg\r\n 发过去没有响应。

原因: 中断使能链路断了一环。Bootloader 的 iap_load_app 跳转前调用了:

c 复制代码
__disable_irq();             // ← 设置 PRIMASK=1,全局关中断
NVIC->ICER[0] = 0xFFFFFFFF;  // ← 关闭所有 NVIC 中断
NVIC->ICPR[0] = 0xFFFFFFFF;  // ← 清除所有挂起

跳转到 APP 后,PRIMASK 仍然为 1 (CPU 内核寄存器,跳转不会复位)。APP 的 MX_USART1_UART_Init 虽然正确调用了:

c 复制代码
NVIC_EnableIRQ(USART1_IRQn);              // ✅ NVIC 使能
LL_USART_EnableIT_RXNE(USART1);           // ✅ 外设中断使能

没有调用 __enable_irq() 恢复全局中断。中断传递路径卡在最后一步:

复制代码
USART1 RXNE=1 → NVIC 检查 ISER[USART1]=1 ✅ → 检查 PRIMASK=1 ❌ → 中断被 CPU 内核屏蔽!

解决: APP main.c 中添加 __enable_irq()

c 复制代码
int main(void)
{
    SCB->VTOR = FLASH_BASE | 0x2C00;   // VTOR 重定位
    // ... system init ...
    MX_USART1_UART_Init();              // 配置 USART1 + NVIC
    /* USER CODE BEGIN 2 */
    __enable_irq();                     // ← 必须!恢复全局中断
    // ... 其他初始化 ...
}

为什么直接烧录(无 Bootloader)时工作正常?

答:硬件复位后 PRIMASK 默认为 0,从复位向量启动不需要 __enable_irq()。从 Bootloader 跳转过来时,PRIMASK 保留了 __disable_irq() 的状态。


问题 8: 断电后读保护被意外使能(最离谱的问题)

症状: 用 Keil 烧录程序 → 正常工作 → 断电再上电 → 读保护被使能 → Keil 只能擦除不能写入 → 需 STM32CubeProgrammer 解除 RDP。

原因: 从 F413 移植过来的 CheckAndClearFlashProtection() 函数在 L051 上严重作死

c 复制代码
static void CheckAndClearFlashProtection(void)
{
    // 步骤 1: 读取 "WRP 寄存器"
    uint32_t wrp0 = *(vu32 *)0x1FF80000;  // ← 这个地址在L051上没有映射!
    uint32_t wrp1 = *(vu32 *)0x1FF80004;

    // 步骤 2: 如果值不是 0xFFFFFFFF,认为有写保护
    if (wrp0 != 0xFFFFFFFF || wrp1 != 0xFFFFFFFF)
    {
        // 步骤 3: 解锁 OPTLOCK(允许修改选项字节)
        FLASH->OPTKEYR = 0xFBEAD9C8;
        FLASH->OPTKEYR = 0x24252627;

        // 步骤 4: 向 0x1FF80000 写数据(实际是错误地址!)
        *(vu32 *)0x1FF80000 = 0xFFFFFFFF;  // ← 选项字节的正确地址是 0x1FFF7800!
    }
}

问题链路:

复制代码
读 0x1FF80000 → Cortex-M0+ 对未映射地址返回 0x00000000
     ↓
0x00000000 ≠ 0xFFFFFFFF → 以为写保护已使能
     ↓
解锁 OPTLOCK → 启用选项字节编程模式
     ↓
向 0x1FF80000 写 0xFFFFFFFF → 地址不对 → 损坏选项字节 ECC
     ↓
断电再上电 → 选项字节 ECC 校验失败 → Flash 控制器默认启用 RDP
     ↓
芯片被锁死,需要 STM32CubeProgrammer 恢复

解决: 彻底删除这个函数。 Flash 写保护控制应由 STM32CubeProgrammer 手动管理,Bootloader 不应该自动解除写保护。

在 CubeProgrammer 中恢复选项字节:

字段
RDP Level 0 (0xAA)
WRP01 0xFFFFFFFF
WRP23 0xFFFFFFFF
其他 默认值不动

教训: 不要无脑移植 F413 代码到 L051。不同系列的 Flash 控制器完全不同。F413 的选项字节在 0x1FFF7800 区域但 F4 的 WRP 寄存器是 0x1FF80000?不对,F413 也不是这个地址。这个函数本身就是有问题的,不是移植的问题。


问题 9: 跳转 APP 前缺少外设复位

症状: APP 跳转后 USART1 打印正常,但串口接收中断不工作(和问题 7 是关联问题)。

原因: 除了 PRIMASK 的问题外,USART1 外设的状态也需要复位。Bootloader 使用 USART1 进行 IAP 通信后跳转到 APP,APP 重新初始化 USART1 时,外设内部状态(移位寄存器、状态标志等)没有被复位,导致初始化不完整。

c 复制代码
void iap_load_app(u32 appxaddr)
{
    printf("Jump to APP: 0x%08X\r\n", appxaddr);

    // 等待 printf 最后字节发送完成
    while (!LL_USART_IsActiveFlag_TC(USART1));

    // ★ 复位 USART1 外设,APP 初始化时状态干净
    LL_APB2_GRP1_ForceReset(LL_APB2_GRP1_PERIPH_USART1);
    LL_APB2_GRP1_ReleaseReset(LL_APB2_GRP1_PERIPH_USART1);

    __disable_irq();
    SysTick->CTRL = 0;
    NVIC->ICER[0] = 0xFFFFFFFF;
    NVIC->ICPR[0] = 0xFFFFFFFF;
    SCB->VTOR = appxaddr;
    __DSB();
    __ISB();
    __set_MSP(*(vu32 *)appxaddr);
    jump2app = (iapfun)(*(vu32 *)(appxaddr + 4));
    jump2app();
}

注意: ForceReset 前要先等待 TC(Transmission Complete),否则 printf 最后几个字节会被截断。


最终验证结果

IAP 升级全链路测试

复制代码
上位机 → Bootloader: # 999               → 进入 IAP 模式
上位机 → Bootloader: # 100 151           → 开始传输 151 包
上位机 → Bootloader: # 101 001 ~ 151     → 每包 128 字节,全部回复 1
Bootloader → Flash: 写入标志位 0x01       → IAP 完成
Bootloader → Jump to APP: 0x08002C00     → 跳转
APP: "2026-06-03-BYD-V01"                → APP 正常启动
上位机 → APP: $msg\r\n                   → APP 正常响应 ✅

最终的 Flash 布局

复制代码
0x08000000 ┌─────────────────┐
           │  Bootloader     │  页 0 ~ 86 (~10.9KB)
0x08002B80 ├─────────────────┤
           │  更新标志位      │  页 87 (128B)
0x08002C00 ├─────────────────┤
           │  APP            │  页 88 ~ 255 (~24KB)
0x08007FFF └─────────────────┘

最终的 iap_load_app 跳转序列

复制代码
1. printf("Jump to APP...") + 等待 TC
2. USART1 外设复位 (ForceReset + ReleaseReset)
3. __disable_irq()
4. SysTick->CTRL = 0
5. NVIC->ICER[0] = 0xFFFFFFFF  (关所有NVIC中断)
6. NVIC->ICPR[0] = 0xFFFFFFFF  (清所有挂起)
7. SCB->VTOR = 0x08002C00
8. __DSB() + __ISB()
9. __set_MSP(APP向量表[0])
10. jump2app = APP向量表[1]; jump2app()

APP 端必须的配置

复制代码
1. main.c: SCB->VTOR = FLASH_BASE | 0x2C00;  // VTOR 重定位
2. main.c: __enable_irq();                     // 恢复全局中断
3. 分散加载(.sct): LR_IROM1 0x08002C00         // APP 链接地址
4. 预处理器: USER_VECT_TAB_ADDRESS,
             VECT_TAB_OFFSET=0x2C00            // SystemInit 中设 VTOR

总结

给 L051 Bootloader 开发者的建议

  1. 不要相信经验 --- STM32L0 的 Flash 控制器(PECR)和 F1/F4 完全不同,所有操作必须严格按 RM0377 来
  2. 页擦除需要 ERASE + PROG 同时置位 --- 缺一不可。
  3. PRGLOCK 必须用 PRGKEYR 密钥解锁 --- 不能直接写 PECR 位。
  4. 跨页写入必须先擦所有目标页 --- 只擦一页会死机。
  5. VTOR 对齐要求 --- APP 地址必须是 256 的倍数(ST 手册 RM0377 要求 VTOR7:0=0,即 256 字节对齐)。
  6. 跳转前复位外设 --- 不清除外设状态的继承会导致 APP 初始化出问题。
  7. APP 必须调用 __enable_irq() --- Bootloader 跳转前关中断了。
  8. 不要自动操作选项字节 --- 清理选项字节的工作交给 STM32CubeProgrammer。

所有问题的根因归类

类别 问题数 占比
PECR Flash 控制器不熟悉 4 个 36%
F413 代码移植未适配 3 个 27%
对 Cortex-M0+ 架构不熟悉 2 个 18%
32KB/8KB 资源限制 2 个 18%

最值钱的教训: 从 F413 移植到 L051 时,不要以为都是 STM32 就差不多。Flash 控制器完全不同,中断控制系统也完全不同(VTOR 的可用性差异、NVIC 差异),串口外设也重新初始化不会自动清除旧状态。


文档日期: 2026-06-08

MCU: STM32L051K6U6

工具链: Keil MDK V5.32, ARM Compiler 5

库: STM32Cube FW L0 V1.12.4 (LL only)

相关推荐
菜鸟的学习日记、2 小时前
GPIO的几种模式——以STM32为例
stm32·单片机·嵌入式硬件·gpio
辰哥单片机设计3 小时前
STM32智能睡眠检测系统
stm32·单片机·嵌入式硬件
隔窗听雨眠5 小时前
在STM32上跑通TinyML:从模型训练到推理优化的完整实战指南
stm32·单片机·嵌入式硬件
机器视觉知识推荐、就业指导7 小时前
为什么同一个引脚不能同时做按键和串口
stm32·单片机·嵌入式硬件
DS小龙哥8 小时前
基于ESP32设计的智能养蜂监测系统
stm32·单片机·嵌入式硬件·物联网·华为云
夜月yeyue8 小时前
STM32 DMA 双缓冲采样
linux·stm32·单片机·嵌入式硬件·系统架构
凡人叶枫12 小时前
Effective C++ 条款15:在资源管理类中提供对原始资源的访问
linux·开发语言·c++·stm32·单片机
雯宝14 小时前
2.串口 IAP
stm32
HAPPY酷14 小时前
STM32 两种烧录方式对比:Keil Load vs FlyMCU 串口下载
stm32·单片机·嵌入式硬件