# STM32L051K6U6 IAP Bootloader 开发踩坑实录 STM32L051K6U6 IAP Bootloader 开发踩坑实录从 F413 移植 IAP 到 L05132KB Flash、8KB RAMLL 库Keil MDK-ARM 编译器。核心教训不要被M0 架构简单迷惑它的 Flash 控制器PECR坑比想象中的多。目录硬件背景问题 1: Flash 页大小搞错了问题 2: 页擦除触发写入了 0x00000000写入 vs 读取触发问题 3: PRGLOCK 必须用密钥序列解锁问题 4: 跨页写入没擦够页——死机问题 5: 8KB RAM 根本不够用问题 6: Flash 布局必须给 VTOR 留对齐空间问题 7: 跳转到 APP 后串口中断不工作问题 8: 断电后读保护被意外使能最离谱的问题问题 9: 跳转 APP 前缺少外设复位最终验证结果总结硬件背景参数值MCUSTM32L051K6U6 (Cortex-M0)Flash32KB, 所有页 128 字节, 256 页RAM8KB工具链Keil MDK V5.32, ARM Compiler 5, -O4库纯 LL 驱动 (STM32L0xx_LL_Driver)串口USART1, 115200 8N1Flash 控制器是PECRProgram/Erase Control Register不是 F4/F1 系列那种 FLASH_CR。问题 1: Flash 页大小搞错了症状页地址计算错误写入位置不对。原因惯性思维以为 STM32L0 的 Flash 像 F1/F4 一样有混合页大小前几 KB 小页后面大页。真相STM32L0x1 系列 256KB Flash 的型号所有页都是 128 字节没有例外。// 错误以为有 1KB 的大页#defineFLASH_PAGE_SIZE_128128#defineFLASH_PAGE_SIZE_1K1024// 正确全部统一#defineFLASH_PAGE_SIZE128/* 所有页均为 128 字节 */#defineFLASH_TOTAL_PAGES256/* 总页数: 32KB / 128B */教训没看 RM0377 之前不要凭经验写 Flash 驱动。问题 2: 页擦除触发写入了 0x00000000写入 vs 读取触发症状擦除后页内数据变成0x00000000不是期望的0xFFFFFFFF。相当于对整个页执行了编程操作。原因看参考手册不仔细。RM0377 A.3.11 明确要求页擦除触发方式是写入0x00000000到页首地址。早期代码错用了读取操作。// 错误读取不会触发擦除反而可能触发意外行为(void)(*(vu32*)page_addr);// 正确写入 0x00000000 触发页擦除*(__IOuint32_t*)page_addr0x00000000UL;// 等 BSY0while(FLASH-SRFLASH_SR_BSY){}同时还要设置 PROG 位PECR 的 ERASE 位和 PROG 位必须同时置 1才能触发擦除。只设 ERASE 不设 PROG写触发字会被忽略。// 页擦除标准序列 (RM0377 A.3.11)FLASH-PECR|FLASH_PECR_ERASE;FLASH-PECR|FLASH_PECR_PROG;*(__IOuint32_t*)page_addr0x00000000UL;while(FLASH-SRFLASH_SR_BSY){}// 等待 BSY0if(FLASH-SRFLASH_SR_EOP)FLASH-SRFLASH_SR_EOP;// 清 EOPFLASH-PECR~(FLASH_PECR_ERASE|FLASH_PECR_PROG);// 清位问题 3: PRGLOCK 必须用密钥序列解锁症状写入 Flash 后读回还是0xFFFFFFFF写入静默失败。原因STM32L0 的 PECR 控制器有两把锁PELOCKPECR 写保护和 PRGLOCK编程保护。PRGLOCK 不能像 F1 系列那样直接写寄存器位清除必须通过 PRGKEYR 写入两把密钥解锁。// 错误直接位操作无效L0 不支持FLASH-PECR~FLASH_PECR_PRGLOCK;// 正确使用密钥序列FLASH-PRGKEYR0x8C9DAEBF;// PRGKEY1FLASH-PRGKEYR0x13141516;// PRGKEY2三种锁的解锁锁寄存器KEY1KEY2PELOCKPEKEYR0x89ABCDEF0x02030405PRGLOCKPRGKEYR0x8C9DAEBF0x13141516OPTLOCKOPTKEYR0xFBEAD9C80x24252627问题 4: 跨页写入没擦够页——死机症状传输到第 4 包就超时死机需要重新上电。原因STMFLASH_EraseAndWrite只擦除了起始页128 字节但写入数据跨了 16 页2048 字节。写入未擦除的页 → Flash 控制器报错 → 总线错误 → HardFault。// 错误只擦了一页Flash_ErasePage(addr);// 只擦 1 页 (128B)STMFLASH_Write(addr,buf,512words);// 写 16 页 (2048B) ← 跨页崩溃// 正确擦除所有涉及的页first_pageGetPageNum(WriteAddr);last_pageGetPageNum(WriteAddrNumToWrite*4-1);for(pagefirst_page;pagelast_page;page){Flash_ErasePage(GetPageAddr(page));}STMFLASH_Write(WriteAddr,pBuffer,NumToWrite);最终方案每包改为 128 字节刚好 1 页不再跨页写入简化逻辑#defineCACHE_SIZE128// 一页大小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]2048512 个 word 2KBflash_cache[2048]2048缓存updatefilebuf[2048]2048被 iapbuf 替代后注释掉USART1BUF[600]600串口接收缓冲updatebuf[512]512命令帧缓冲Stack Heap15361KB 0.5KB总计~9.5KB超了 1.5KB解决把所有缓冲区压缩到极致#defineCACHE_SIZE128// 2048 → 128#defineiapbuf32// 512 word → 32 word (128B)#defineupdatebuflen128// 512 → 128#defineUARTLEN600// 保持最终 RAM 占用压到 ~4KB留出充分余量。问题 6: Flash 布局必须给 VTOR 留对齐空间症状跳转到 APP 后一旦发生中断就跑飞到 Bootloader。原因STM32L0 的 Cortex-M0 支持VTOR向量表偏移寄存器。根据 RM0377VTOR 要求256 字节对齐VTOR[7:0]必须为 0。原始中间布局中APP 地址 0x08002C800x08002C80 → 低字节 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 中转的麻烦SCB-VTOR0x08002C00;// 256字节对齐直接指向Flash问题 7: 跳转到 APP 后串口中断不工作症状APP 正常启动printf 能正常输出但$msg\r\n发过去没有响应。原因中断使能链路断了一环。Bootloader 的iap_load_app跳转前调用了__disable_irq();// ← 设置 PRIMASK1全局关中断NVIC-ICER[0]0xFFFFFFFF;// ← 关闭所有 NVIC 中断NVIC-ICPR[0]0xFFFFFFFF;// ← 清除所有挂起跳转到 APP 后PRIMASK 仍然为 1CPU 内核寄存器跳转不会复位。APP 的MX_USART1_UART_Init虽然正确调用了NVIC_EnableIRQ(USART1_IRQn);// ✅ NVIC 使能LL_USART_EnableIT_RXNE(USART1);// ✅ 外设中断使能但没有调用__enable_irq()恢复全局中断。中断传递路径卡在最后一步USART1 RXNE1 → NVIC 检查 ISER[USART1]1 ✅ → 检查 PRIMASK1 ❌ → 中断被 CPU 内核屏蔽解决APP main.c 中添加__enable_irq()intmain(void){SCB-VTORFLASH_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 上严重作死staticvoidCheckAndClearFlashProtection(void){// 步骤 1: 读取 WRP 寄存器uint32_twrp0*(vu32*)0x1FF80000;// ← 这个地址在L051上没有映射uint32_twrp1*(vu32*)0x1FF80004;// 步骤 2: 如果值不是 0xFFFFFFFF认为有写保护if(wrp0!0xFFFFFFFF||wrp1!0xFFFFFFFF){// 步骤 3: 解锁 OPTLOCK允许修改选项字节FLASH-OPTKEYR0xFBEAD9C8;FLASH-OPTKEYR0x24252627;// 步骤 4: 向 0x1FF80000 写数据实际是错误地址*(vu32*)0x1FF800000xFFFFFFFF;// ← 选项字节的正确地址是 0x1FFF7800!}}问题链路读 0x1FF80000 → Cortex-M0 对未映射地址返回 0x00000000 ↓ 0x00000000 ≠ 0xFFFFFFFF → 以为写保护已使能 ↓ 解锁 OPTLOCK → 启用选项字节编程模式 ↓ 向 0x1FF80000 写 0xFFFFFFFF → 地址不对 → 损坏选项字节 ECC ↓ 断电再上电 → 选项字节 ECC 校验失败 → Flash 控制器默认启用 RDP ↓ 芯片被锁死需要 STM32CubeProgrammer 恢复解决彻底删除这个函数。Flash 写保护控制应由 STM32CubeProgrammer 手动管理Bootloader 不应该自动解除写保护。在 CubeProgrammer 中恢复选项字节字段值RDPLevel 0 (0xAA)WRP010xFFFFFFFFWRP230xFFFFFFFF其他默认值不动教训不要无脑移植 F413 代码到 L051。不同系列的 Flash 控制器完全不同。F413 的选项字节在 0x1FFF7800 区域但 F4 的 WRP 寄存器是 0x1FF80000不对F413 也不是这个地址。这个函数本身就是有问题的不是移植的问题。问题 9: 跳转 APP 前缺少外设复位症状APP 跳转后 USART1 打印正常但串口接收中断不工作和问题 7 是关联问题。原因除了 PRIMASK 的问题外USART1 外设的状态也需要复位。Bootloader 使用 USART1 进行 IAP 通信后跳转到 APPAPP 重新初始化 USART1 时外设内部状态移位寄存器、状态标志等没有被复位导致初始化不完整。voidiap_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-CTRL0;NVIC-ICER[0]0xFFFFFFFF;NVIC-ICPR[0]0xFFFFFFFF;SCB-VTORappxaddr;__DSB();__ISB();__set_MSP(*(vu32*)appxaddr);jump2app(iapfun)(*(vu32*)(appxaddr4));jump2app();}注意ForceReset 前要先等待 TCTransmission 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_OFFSET0x2C00 // SystemInit 中设 VTOR总结给 L051 Bootloader 开发者的建议不要相信经验— STM32L0 的 Flash 控制器PECR和 F1/F4 完全不同所有操作必须严格按 RM0377 来。页擦除需要 ERASE PROG 同时置位— 缺一不可。PRGLOCK 必须用 PRGKEYR 密钥解锁— 不能直接写 PECR 位。跨页写入必须先擦所有目标页— 只擦一页会死机。VTOR 对齐要求— APP 地址必须是 256 的倍数ST 手册 RM0377 要求 VTOR[7: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-08MCU: STM32L051K6U6工具链: Keil MDK V5.32, ARM Compiler 5库: STM32Cube FW L0 V1.12.4 (LL only)