STM32L051K6U6 IAP要点记录-LL库

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**,而全局缓冲区一不小心就超了:

| 变量 | 大小 | 说明 |

|------|------|------|

| `iapbuf512` | 2048 | 512 个 word = 2KB |

| `flash_cache2048` | 2048 | 缓存 |

| `updatefilebuf2048` | 2048 | 被 iapbuf 替代后注释掉 |

| `USART1BUF600` | 600 | 串口接收缓冲 |

| `updatebuf512` | 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 字节对齐**(`VTOR7: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->ICER0 = 0xFFFFFFFF; // ← 关闭所有 NVIC 中断

NVIC->ICPR0 = 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 检查 ISERUSART1=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->ICER0 = 0xFFFFFFFF;

NVIC->ICPR0 = 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->ICER0 = 0xFFFFFFFF (关所有NVIC中断)

  6. NVIC->ICPR0 = 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)*

相关推荐
MAR-Sky2 小时前
stc8h系列单片机使用中断号超过32的插件解决办法
单片机·嵌入式硬件
kebidaixu2 小时前
FreeRTOS 移植到 STM32F407VETX 记录(四)
stm32
结城明日奈是我老婆2 小时前
基于stm32f103c8t6最小系统板俩块版通讯
stm32·单片机·嵌入式硬件
weixin_456808383 小时前
【沁恒蓝牙开发】从机判断主机是否使能CCCD
单片机·嵌入式硬件
深圳英康仕3 小时前
一款面向AGV智能搬运机器人的RK3588工控机的数据资料整理
嵌入式硬件·rk3588·工控机·agv·智能搬运机器人
fengfuyao9853 小时前
STM32F030 SD卡文件系统读取实例
stm32·单片机·嵌入式硬件
kebidaixu3 小时前
FreeRTOS 移植到 STM32F407VETX 记录(三)
stm32·单片机·嵌入式硬件
普中科技13 小时前
【普中STM32F1xx开发攻略--标准库版】-- 第 45 章 FSMC-外扩 SRAM 实验
stm32·单片机·嵌入式硬件·fsmc·普中科技·外扩sram·is62wv51216