当前位置: 首页 > news >正文

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

| 变量 | 大小 | 说明 |
|------|------|------|
| `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 要求 VTOR[7: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)*

http://www.zskr.cn/news/1500528.html

相关文章:

  • python3.7-数据存储与运算-赋值运算符
  • 科华UPS全系列产品汇总:主流型号与应用场景解析
  • 全国地下水位深度数据集
  • 微信网页版终极解决方案:wechat-need-web浏览器扩展完整指南
  • LinkSwift网盘直链下载助手:告别限速,5分钟开启高速下载新时代
  • Teamcenter许可回收,两种触发方式到底哪个更及时?
  • 2026年辽宁党建文化墙公司推荐榜单:红色阵地设计、党建展厅与氛围营造实力品牌解析 - 品牌发掘
  • 四路LED灯控芯片 ECJ240024掉电循环切换LOGO霓虹灯专用闪灯芯片
  • Claude Code 的 Skills:AI Agent 真正需要的不是提示词,而是组织记忆
  • 2026年 耐高温丁晴密封圈品牌推荐榜:高温耐油、高压耐用与长寿命品质之选 - 品牌发掘
  • AI中医ChatiSS查体大模型全流程解析,辨证准确率凭什么可以做到95.8%
  • 2026年惠州中央空调回收品牌推荐与选择攻略 - 广东再生资源回收
  • 本地运行的年会抽奖工具,改JS名单就能抽,中奖实时可见
  • 深入解析MC68HC805P18:经典8位MCU架构、中断与EEPROM编程实战
  • 揭秘AI教材写作技巧:低查重工具加持,5天完成30万字教材编写!
  • 数字电源开发实战:JTAG与SCI接口在DSC调试中的协同应用
  • 欧奥电子车载移动UFS4.1验证:mSMP与B2B 高保真探测技术详解
  • 2026年6月佛山回收中央空调公司推荐,正规资质环保处理更合规 - 广东再生资源回收
  • AZ系、ZK系、WE系——一张牌号选型对照,加四种成型工艺的匹配逻辑
  • 当信号与系统遇见深度学习:我用傅里叶变换和拉普拉斯算子,看懂了CNN的本质
  • 集合 USB,AI ENC,AEC,BF,全面功能的语音处理模组
  • 如何在Windows上高效读写Btrfs分区:实用跨平台文件系统指南
  • MC68HC908MR24 TIMB定时器与SPI模块实战配置与避坑指南
  • 如何挑选正宗新疆干果:无添加养生特产选购攻略
  • NX许可回收无感测试,对比4款工具谁更隐形
  • 零成本启动的安全生产月巡检工具,安全检查 + 隐患上报一步到位
  • 【手把手教学】:OpenClaw 解压安装与运行全流程(包含安装包)
  • java feign调用第三方服务出现序列化错误的排查过程
  • 状态压缩 DP 与树形 DP:从空间优化到树状结构的动态规划
  • 计算机毕业设计之django基于大数据的旅游景区推荐系统