Cortex-M0 通过Bootloader实现中断向量表重映射

Cortex-M0 通过Bootloader实现中断向量表重映射

1. 原理

Cortex-M0 内核由于没有 VTOR(Vector Table Offset Register),这是它与 Cortex-M3/M4 等内核的重要区别之一。Cortex-M0 的中断向量表固定在复位地址(通常是 0x00000000),无法通过寄存器直接重映射。

芯片提供了三种不同的启动方式:

  1. 从 Flash 程序存储区启动
  2. 从 SRAM 启动
  3. 从系统存储区启动

因此,在 Cortex-M0 上实现 Bootloader 的中断向量表重映射,需要通过硬件映射或代码跳转的方式间接实现。

2. 实现方法

2.1 硬件映射

启动模式选择后,如果要跳转到APP运行,在跳转之前Bootloader可以通过特定的寄存器来修改存储器访问方式。可以将APP的中断向量表重定位到 SRAM 中:

  • 将APP的中断向量表从对应的 Flash 分区复制到 SRAM 的基地址 0x20000000。
  • SRAM 重映射到 0x00000000 地址。
  • 一旦发生中断,内核将从重定位到 SRAM 中的向量表获取中断处理函数的起始地址,跳转到
    Flash 中的中断处理处理函数。
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
// 跳转到APP
static void jump_to_app(uint32_t app_addr)
{
// 关闭所有中断
__disable_irq();
// 复制中断向量表到 SRAM
memcpy((void *)SRAM_BASE_ADDR, (const void *)app_addr, VECT_TAB_COPY_SIZE);
// 重映射 SRAM 到 0x00000000 地址
// 如果不用 HAL 库,用寄存器重映射,需要开启时钟
HAL_SYSCFG_SetRemapMemory(SYSCFG_BOOT_SRAM);

uint32_t app_sp = *(uint32_t *)(SRAM_BASE_ADDR + 0);
uint32_t app_reset = *(uint32_t *)(SRAM_BASE_ADDR + 4);
if ((app_sp < SRAM_BASE_ADDR) || (app_sp >= SRAM_END_ADDR))
{
NVIC_SystemReset();
}
// 初始化 APP 程序栈顶
__set_MSP(app_sp);
// 跳转到 APP 入口地址
void (*AppEntry)(void) = (void (*)(void))(app_reset);
AppEntry();
while (1)
__NOP();
}

2.2 代码跳转

如果芯片没有硬件映射功能(如部分低端 Cortex-M0 芯片),可通过在 Bootloader 中保留向量表,并将 APP 程序的中断向量 转发,Bootloader 的向量表固定在 0x00000000,其中除了复位向量外,其他中断向量均指向一个 跳转函数。跳转函数读取 APP 程序向量表中对应的中断服务程序(ISR)地址,并跳转到该地址执行。

  1. 定义 Bootloader 向量表,除复位向量外,其他中断均指向 VectorRedirect 函数。
  2. VectorRedirect 函数根据中断号,计算 APP 程序向量表中对应的 ISR 地址,并跳转。
  3. 跳转到 APP 程序前,记录 APP 程序起始地址(供 VectorRedirect 使用)。
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
43
44
45
46
47
48
49
50
51
// 全局变量: APP 程序起始地址(供中断跳转使用)
volatile uint32_t userAppStart = 0x08005000;

// 中断向量重定向函数
void VectorRedirect(void) {
// 获取当前中断号(通过 IPSR 寄存器)
uint32_t irqNum = (SCB->IPSR & 0x1FF); // Cortex-M0 的 IPSR 寄存器保存中断号

if (irqNum == 0 || userAppStart == 0) {
// 非中断或 APP 程序未加载,直接返回
return;
}

// 计算 APP 程序向量表中对应的 ISR 地址(中断号 = 向量表索引)
uint32_t *userVectorTable = (uint32_t*)userAppStart;
uint32_t userIsrAddr = userVectorTable[irqNum];

// 跳转到 APP 程序的 ISR(通过函数指针)
typedef void (*pIsrFunction)(void);
pIsrFunction userIsr = (pIsrFunction)userIsrAddr;
userIsr();
}

// Bootloader 向量表
__Vectors const void *const g_pfnVectors[] = {
(void*)&_estack, // 0: 栈顶地址(Bootloader 的栈)
Reset_Handler, // 1: 复位向量(Bootloader 复位函数)
VectorRedirect, // 2: NMI 中断
VectorRedirect, // 3: 硬 fault 中断
// ... 其他中断均指向 VectorRedirect
};

// 跳转到 APP 程序
void jumpToApp(void) {
typedef void (*pFunction)(void);
pFunction appEntry;

// 记录 APP 程序起始地址(供中断跳转使用)
userAppStart = USER_APP_START;

// 关闭中断
__disable_irq();

// 初始化 APP 程序栈顶
__set_MSP(*(volatile uint32_t*)USER_APP_START);

// 跳转到 APP 程序复位函数
appEntry = (pFunction)*(volatile uint32_t*)(USER_APP_START + 4);
appEntry();
}

3. 注意事项

  1. 中断响应延迟:
    方案二通过 Bootloader 转发中断,会增加少量延迟,对实时性要求高的场景需谨慎使用。
  2. 栈指针管理:
    跳转前必须用 __set_MSP 初始化 APP 程序的栈顶指针(Cortex-M0 仅支持主栈 MSP)。
  3. 芯片差异:
    方案一依赖芯片硬件支持(如 STM32 的 SYSCFG),需参考具体芯片手册;方案二是通用方案,适用于所有 Cortex-M0 芯片。