一、案发现场:诡异的“无限重启”
在开发一个项目模板时,我需要一个最基础的功能:在 Flash 中保存一个计数值,以实现“根据上电次数(奇偶)执行不同动作”的需求。
我使用了官方 std_ 库,在 main.c 中写下了看似天经地义的逻辑:
- 上电,初始化硬件。
- 从 Flash 的
0x00004000 地址读取 boot_count 值。
- 计算
new_boot_count = boot_count + 1。
- 调用
std_flash_unlock()、std_flash_erase()、std_flash_word_program() 将新值写回。
- 根据本次读到的
boot_count 判断奇偶,执行“常亮”或“慢闪”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| int main(void) { uint32_t boot_count = 0; uint32_t new_boot_count = 0;
system_clock_config(); std_delay_init(); drv_led_init();
boot_count = *(uint32_t *)BOOT_COUNTER_ADDR;
if (boot_count == 0xFFFFFFFF) { new_boot_count = 0; } else { new_boot_count = boot_count + 1; }
std_flash_unlock(); if (STD_OK != std_flash_erase(FLASH_MODE_PAGE_ERASE, BOOT_COUNTER_ADDR)) { error_process(); } if (STD_OK != std_flash_word_program(BOOT_COUNTER_ADDR, new_boot_count)) { error_process(); } std_flash_lock();
if (boot_count & 0x01) { drv_led_on(); while(1); } else { while (1) { drv_led_toggle(); std_delayms(500); } } }
|
诡异的现象出现了:
用 pyocd 烧录,程序永远在执行“偶数”逻辑(慢闪)。
用“官方烧录器”烧录,程序永远停在 drv_led_init() 刚执行完的状态(LED 不亮)。
无论怎么按复位键,程序从未进入“奇数”逻辑(常亮)。
二、RCA (根本原因分析):致命的 XIP 陷阱
这个问题的核心,是 ARM Cortex-M 内核最经典的陷阱之一:XIP (Execute-In-Place) 冲突。
1. 什么是 XIP?
我们的 MCU 是“就地执行”的。main 函数编译后的机器码,存储在 Flash 中。CPU 核会从 Flash 读取一条指令,执行,再读取下一条。
2. Flash 的物理特性
Flash 在执行“擦除”或“编程”操作时,Flash 控制器会进入“忙” (Busy) 状态。关键点:在它忙碌时,整个 Flash 存储器(或至少是该 Bank)是无法被读取的!
3. 故障传导链 (The Crash Chain)
CPU (在 Flash 运行) 执行到 main 函数。
CPU 调用 std_flash_erase() 函数 (该函数也存储在 Flash 中)。
std_flash_erase() 命令 Flash 控制器:“开始擦除!”。
Flash 控制器进入 Busy 状态,锁死读取通道。
CPU 想要去读取下一条指令(例如检查 Busy 标志位的 while 循环指令)。
冲突 (Collision): Flash 正忙,拒绝了 CPU 的取指请求。
HardFault: CPU 取不到指令,触发总线错误或硬故障。
Reset: 看门狗或系统复位机制介入,芯片重启。
结论: 程序在执行到 std_flash_erase() 的那一瞬间必定崩溃。因为重启太快,肉眼根本看不到任何报错。
三、尝试与失败:__RAM_FUNC 的“半途而废”
标准的解决方案是:将操作 Flash 的代码复制到 RAM 中运行。Keil/GCC 提供了 __attribute__((section(".ramfunc"))) 宏来实现。
但我写的第一版方案依然失败了:
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| /* 方案一:错误的 __RAM_FUNC 用法 */ __RAM_FUNC void Flash_Write_Counter_V1(uint32_t new_count) { /* 我们的“外壳”函数确实在 RAM 里... */ std_flash_unlock(); /* ...但是它调用的 std_flash_erase() 仍然在 Flash 库函数里!*/ /* CPU 跳转回 Flash 去执行擦除 -> XIP 冲突 -> 崩溃 */ if (STD_OK != std_flash_erase(FLASH_MODE_PAGE_ERASE, BOOT_COUNTER_ADDR)) { error_process(); // error_process() 也TM在 Flash 里! } std_flash_word_program(BOOT_COUNTER_ADDR, new_count); std_flash_lock(); }
|
RCA (V2): 只要调用链中有任何一个环节跳回了 Flash,崩溃就会发生。必须做到 100% 隔离。
四、终极解决方案:“完全内联”的 RAM 函数
我们必须创建一个完全自洽的 RAM 函数,遵守以下白盒规则:
No External Calls: 不能调用任何 Flash 中的库函数。
No Error Handling: 不能调用 Flash 中的 error_process。
Manual Inline: 手动把寄存器操作代码复制进来。
Status Return: 通过返回值报告状态,由 Flash 中的主程序事后处理。
1. 定义状态码
C
1 2 3 4 5 6 7
| /* main.c */ typedef enum { FLASH_OK = 0, FLASH_ERR_ERASE, FLASH_ERR_PROGRAM, FLASH_ERR_VERIFY } Flash_Status;
|
2. 构建 RAM 函数
我们需要打开厂商提供的 std_flash.c,把核心的寄存器操作“偷”出来。
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| /* 兼容性宏定义 */ #if defined(__CC_ARM) || defined(__GNUC__) #define __RAM_FUNC __attribute__((section(".ramfunc"))) #elif defined(__ICCARM__) #define __RAM_FUNC __ramfunc #endif
/****************************************************************************** * @brief 【终极版】在 RAM 中执行 Flash 写入操作 * @note 此函数 100% 在 RAM 中隔离运行,不依赖任何外部 Flash 代码。 *****************************************************************************/ __RAM_FUNC Flash_Status Flash_Write_Counter(uint32_t new_count) { /* 1. 解锁 Flash (内联自 std_flash_unlock) */ if ((FLASH->CR & FLASH_CR_LOCK) == FLASH_CR_LOCK) { FLASH->CRKEY = FLASH_CR_KEY1; FLASH->CRKEY = FLASH_CR_KEY2; }
/* 2. 清除标志 */ FLASH->SR = (FLASH_FLAG_EOP | FLASH_FLAG_WRPERR);
/* 3. 执行擦除 (内联寄存器操作) */ MODIFY_REG(FLASH->CR, FLASH_CR_OP_MODE, FLASH_MODE_PAGE_ERASE); *(uint32_t *)BOOT_COUNTER_ADDR = 0xFFFFFFFF; // [关键] 在这里等待 Busy,此时 CPU 在 RAM 中自旋,是安全的 while ((FLASH->SR & FLASH_FLAG_BSY));
if ((FLASH->SR & FLASH_FLAG_WRPERR) != 0x00000000U) { FLASH->CR |= FLASH_CR_LOCK; return FLASH_ERR_ERASE; // 返回错误码,而不是调用函数 } // ... (中间省略类似的编程与校验步骤,原理同上) ... /* 6. 锁定 Flash */ FLASH->CR |= FLASH_CR_LOCK;
return FLASH_OK; }
|
(完整代码参考项目源码,此处展示核心逻辑)
3. 在 main 中安全调用
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| int main(void) { // ... 初始化 ...
/* 【安全调用】 */ /* CPU 跳转到 RAM 执行,Flash 忙碌期间 CPU 不会访问 Flash */ Flash_Status status = Flash_Write_Counter(new_boot_count);
/* 【安全处理】 */ /* Flash 操作已完成, CPU 回到 Flash 运行,可以处理报错了 */ if (status != FLASH_OK) { error_process(); } // ... 业务逻辑 ... }
|
五、工具链的“最后一刀”
代码改对了,工具链还可能坑你一把:
官方烧录器: 记得勾选 “Run after programming”,否则程序烧进去根本没跑,让你误以为又崩了。
Pyocd 报错 No ACK: 这反而是成功的标志。因为程序跑得太快,烧录完立刻复位开始擦 Flash,导致 SWD 总线无法响应 Pyocd 的收尾查询。
六、总结 (Summary)
在 Cortex-M 上进行 IAP 开发,必须时刻保持 “Memory-Aware” (内存感知) 的白盒思维:
XIP 陷阱是物理铁律,无法绕过。
__RAM_FUNC 只是把入口放到了 RAM,必须保证整个调用链都在 RAM。
完全内联 (Fully Inlined) 是实现 100% 隔离的最稳妥方式。
异步错误处理: 在 RAM 里只返回状态,回 Flash 里再处理错误。