STM32L051K6U6 IAP Bootloader 开发踩坑实录
从 F413 移植 IAP 到 L051,32KB Flash、8KB RAM,LL 库,Keil MDK-ARM 编译器。
核心教训: 不要被"M0+ 架构简单"迷惑,它的 Flash 控制器(PECR)坑比想象中的多。
目录
- 硬件背景
- [问题 1: Flash 页大小搞错了](#问题 1: Flash 页大小搞错了)
- [问题 2: 页擦除触发写入了 0x00000000(写入 vs 读取触发)](#问题 2: 页擦除触发写入了 0x00000000(写入 vs 读取触发))
- [问题 3: PRGLOCK 必须用密钥序列解锁](#问题 3: PRGLOCK 必须用密钥序列解锁)
- [问题 4: 跨页写入没擦够页------死机](#问题 4: 跨页写入没擦够页——死机)
- [问题 5: 8KB RAM 根本不够用](#问题 5: 8KB RAM 根本不够用)
- [问题 6: Flash 布局必须给 VTOR 留对齐空间](#问题 6: Flash 布局必须给 VTOR 留对齐空间)
- [问题 7: 跳转到 APP 后串口中断不工作](#问题 7: 跳转到 APP 后串口中断不工作)
- [问题 8: 断电后读保护被意外使能(最离谱的问题)](#问题 8: 断电后读保护被意外使能(最离谱的问题))
- [问题 9: 跳转 APP 前缺少外设复位](#问题 9: 跳转 APP 前缺少外设复位)
- 最终验证结果
- 总结
硬件背景
| 参数 | 值 |
|---|---|
| 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 开发者的建议
- 不要相信经验 --- STM32L0 的 Flash 控制器(PECR)和 F1/F4 完全不同,所有操作必须严格按 RM0377 来。
- 页擦除需要 ERASE + PROG 同时置位 --- 缺一不可。
- PRGLOCK 必须用 PRGKEYR 密钥解锁 --- 不能直接写 PECR 位。
- 跨页写入必须先擦所有目标页 --- 只擦一页会死机。
- VTOR 对齐要求 --- APP 地址必须是 256 的倍数(ST 手册 RM0377 要求 VTOR7:0=0,即 256 字节对齐)。
- 跳转前复位外设 --- 不清除外设状态的继承会导致 APP 初始化出问题。
- APP 必须调用
__enable_irq()--- Bootloader 跳转前关中断了。 - 不要自动操作选项字节 --- 清理选项字节的工作交给 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)