第一次在STM32F030上做IAP升级功能时,我遇到了一个诡异现象:Bootloader能正常跳转到APP,但APP里的USART中断死活不响应。调试器显示中断确实触发了,但程序就是跑不到中断服务函数里。后来查手册才发现,这其实是Cortex-M0内核的一个"特性"——没有VTOR寄存器。
在Cortex-M3/M4上,我们习惯用SCB->VTOR = 0x08004000这样的语句轻松重定向中断向量表。但M0内核压根没有这个寄存器,它的中断向量表永远从0x00000000地址读取。这就导致当APP存放在Flash的非0x08000000区域时(比如从0x08004000开始),所有中断都会失效。
翻遍STM32F0参考手册,在SYSCFG章节找到了救命稻草——内存物理重映射功能。这个设计非常巧妙:
实测发现这个方案有个隐藏优势:SRAM的访问速度比Flash快,理论上能减少中断延迟。我在72MHz主频下测试,中断响应时间比默认情况快了约3个时钟周期。
首先要在链接脚本里预留SRAM起始空间。以我的项目为例,使用STM32F030F4(4KB SRAM):
c复制/* 修改后的链接脚本片段 */
MEMORY
{
RAM (xrw) : ORIGIN = 0x200000C0, LENGTH = 4K - 0xC0 /* 保留前192字节 */
}
为什么是0xC0?因为我的中断向量表有48个条目(查看startup_stm32f030.s确认),48*4=192=0xC0。建议多留些余量,我遇到过因为少算一个DCD导致HardFault的惨案。
在APP的SystemInit函数中添加:
c复制#define APP_BASE 0x08004000
#define VECTOR_SIZE 0xC0
void SystemInit(void)
{
/* 先拷贝向量表到SRAM */
memcpy((uint32_t*)0x20000000, (uint32_t*)APP_BASE, VECTOR_SIZE);
/* 再启用重映射 */
SYSCFG_MemoryRemapConfig(SYSCFG_MemoryRemap_SRAM);
/* 其他初始化代码... */
}
这里有个坑:必须确保memcpy完成后再重映射。我有次调换了两行顺序,结果一进中断就死机。
Bootloader跳转前要恢复默认映射:
c复制void jump_to_app(uint32_t app_addr)
{
/* 禁用所有中断 */
__disable_irq();
/* 恢复默认内存映射 */
SYSCFG_MemoryRemapConfig(SYSCFG_MemoryRemap_Flash);
/* 设置堆栈指针 */
uint32_t sp = *((volatile uint32_t*)app_addr);
__set_MSP(sp);
/* 跳转到APP */
uint32_t reset_handler = *((volatile uint32_t*)(app_addr + 4));
((void(*)(void))reset_handler)();
}
实测发现如果不恢复映射,有些型号的芯片会在跳转时触发HardFault。这个细节在官方手册里都没明确说明。
我习惯在调试时添加校验代码:
c复制// 检查前两个向量是否有效
if(((*(uint32_t*)0x20000000) < 0x20000000) ||
((*(uint32_t*)0x20000004) < 0x08000000)){
while(1); // 触发调试断点
}
这个方法帮我抓到了好几次Flash烧写不完整的问题。
使用Keil的分散加载文件时,要这样预留SRAM空间:
code复制LR_IROM1 0x08004000 0x0000C000 {
ER_IROM1 0x08004000 0x0000C000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x200000C0 0x00000400 - 0xC0 {
.ANY (+RW +ZI)
}
}
注意RW_IRAM1的起始地址要与链接脚本一致。有次我两个文件配置不一致,导致变量覆盖了向量表。
在SRAM中运行向量表时,发现某些高优先级中断会触发异常。后来发现是堆栈空间不足导致的,解决方法:
c复制__set_MSP(0x20001000); // 设置到SRAM高端地址
最近在某国产M0芯片(型号不便透露)上移植时,发现它的重映射寄存器地址不同。通过以下方法快速适配:
c复制uint32_t syscfg_addr = 0x40010000; // 以GD32为例
uint32_t cfgr1 = *(volatile uint32_t*)(syscfg_addr + 0x04);
c复制#define MY_REMAP_REG (*(volatile uint32_t*)0x40010004)
#define MY_REMAP_MASK 0x03
对于时间敏感型应用,可以进一步优化:
c复制// 复制TIM1中断向量到快速访问区
*(uint32_t*)0x20000100 = *(uint32_t*)0x20000058;
c复制// 仅重定位USB和TIM1中断
memcpy((void*)0x20000058, (void*)0x08004058, 8);
除了SRAM重映射,还有两种方案可选:
| 方案 | 优点 | 缺点 |
|---|---|---|
| SRAM重映射 | 性能好,官方推荐 | 占用SRAM空间 |
| 中断代理 | 不占SRAM | 每个中断都要代理,代码臃肿 |
| 双BOOT区切换 | 无需特殊处理 | 需要双倍Flash空间 |
在资源紧张的场合(比如只有2KB SRAM),我会选择中断代理方案。虽然要写更多代码,但能省下宝贵的RAM空间。