从零手写Cortex-M0+启动文件:向量表重定位与中断优先级分组的实战验证
一、为什么需要手写启动文件?
启动文件是MCU上电后执行的第一段代码,负责初始化堆栈、向量表、时钟及跳转至main。虽然厂商提供标准启动文件,但手写能让你彻底掌控:
- 向量表重定位:支持Bootloader+App双区升级,或动态切换中断向量。
- 中断优先级分组:Cortex-M0+仅支持2位优先级,但分组策略影响抢占与子优先级。
- 精简代码:去除冗余,适配资源受限场景。
本文以STM32G0(Cortex-M0+)为例,从零构建启动文件并验证关键机制。
二、Cortex-M0+启动流程与向量表
2.1 上电序列
- 从地址0x00000000读取初始MSP(主堆栈指针)。
- 从0x00000004读取复位向量(Reset_Handler地址)。
- 执行Reset_Handler,初始化.data/.bss,跳转main。
2.2 向量表结构
向量表是函数指针数组,前16项为内核异常,之后为外设中断。Cortex-M0+的向量表必须对齐到128字节(最低7位为0)。
// 向量表定义(简化)
__attribute__((section(".isr_vector")))
const void *vector_table[] = {
(void*)0x20001000, // 初始MSP
(void*)Reset_Handler, // 复位
(void*)NMI_Handler,
(void*)HardFault_Handler,
// ... 其他异常
(void*)EXTI0_IRQHandler, // 外设中断
};
2.3 向量表重定位
通过SCB->VTOR寄存器可重定位向量表。例如Bootloader跳转App前,将VTOR设为App向量表地址:
// 重定位到0x08004000
SCB->VTOR = 0x08004000;
__DSB();
__ISB();
注意:Cortex-M0+的VTOR仅支持128字节对齐,且重定位后需确保新向量表有效。
三、中断优先级分组实战
3.1 Cortex-M0+优先级模型
- 仅2位优先级(0-3),数值越小优先级越高。
- 无子优先级概念,但可通过分组模拟抢占与子优先级。
- 分组寄存器:SCB->AIRCR的PRIGROUP位。
3.2 分组配置
Cortex-M0+支持4种分组(PRIGROUP=0~3),但实际仅2位有效:
| PRIGROUP | 抢占优先级位数 | 子优先级位数 | 说明 | |----------|----------------|--------------|------| | 0 | 2 | 0 | 全部抢占 | | 1 | 1 | 1 | 1位抢占1位子 | | 2 | 0 | 2 | 全部子(无抢占) | | 3 | 0 | 2 | 保留 |
配置代码:
// 设置分组为1(1位抢占,1位子优先级)
SCB->AIRCR = (0x5FA << 16) | (1 << 8);
3.3 优先级设置
每个中断的优先级寄存器为8位,但仅高2位有效。设置时需左移6位:
NVIC_SetPriority(EXTI0_IRQn, (1 << 6)); // 抢占1,子0
NVIC_SetPriority(EXTI1_IRQn, (2 << 6)); // 抢占2,子0
四、完整启动文件实现
4.1 汇编启动代码(startup.s)
.syntax unified
.cpu cortex-m0plus
.thumb
.global Reset_Handler
.section .isr_vector,"a",%progbits
.align 7
vector_table:
.word 0x20001000 @ 栈顶
.word Reset_Handler
.word NMI_Handler
.word HardFault_Handler
@ ... 其他向量
.section .text.Reset_Handler
.weak Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
ldr r0, =_estack
mov sp, r0
ldr r0, =_sdata
ldr r1, =_edata
ldr r2, =_sidata
b copy_data
copy_data:
cmp r0, r1
bge fill_bss
ldr r3, [r2]
str r3, [r0]
adds r0, r0, #4
adds r2, r2, #4
b copy_data
fill_bss:
ldr r0, =_sbss
ldr r1, =_ebss
movs r3, #0
bss_loop:
cmp r0, r1
bge call_main
str r3, [r0]
adds r0, r0, #4
b bss_loop
call_main:
bl main
b .
4.2 链接脚本关键段
MEMORY {
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 8K
}
SECTIONS {
.isr_vector : { KEEP(*(.isr_vector)) } > FLASH
.text : { *(.text*) } > FLASH
.data : { *(.data*) } > RAM AT > FLASH
.bss : { *(.bss*) } > RAM
}
4.3 向量表重定位与优先级分组验证代码
#include "stm32g0xx.h"
void EXTI0_IRQHandler(void) {
// 中断处理
EXTI->RPR1 = EXTI_RPR1_RPIF0; // 清除标志
}
void EXTI1_IRQHandler(void) {
EXTI->RPR1 = EXTI_RPR1_RPIF1;
}
int main(void) {
// 1. 重定位向量表到0x08004000(假设App区)
SCB->VTOR = 0x08004000;
__DSB(); __ISB();
// 2. 设置优先级分组:1位抢占,1位子优先级
SCB->AIRCR = (0x5FA << 16) | (1 << 8);
// 3. 配置EXTI0抢占1,EXTI1抢占2
NVIC_SetPriority(EXTI0_IRQn, (1 << 6));
NVIC_SetPriority(EXTI1_IRQn, (2 << 6));
NVIC_EnableIRQ(EXTI0_IRQn);
NVIC_EnableIRQ(EXTI1_IRQn);
// 4. 触发中断验证
EXTI->SWIER1 = EXTI_SWIER1_SWI0; // 触发EXTI0
__NOP();
EXTI->SWIER1 = EXTI_SWIER1_SWI1; // 触发EXTI1
while(1);
}
验证现象:EXTI0(抢占1)可打断EXTI1(抢占2)的执行,体现优先级分组效果。
五、注意事项与避坑指南
- 向量表对齐:Cortex-M0+要求VTOR低7位为0,重定位地址必须128字节对齐。
- 优先级分组陷阱:M0+仅2位优先级,分组后实际可用级别有限,避免过度设计。
-
内存屏障:修改VTOR或AIRCR后必须加
__DSB()和__ISB(),确保流水线同步。 -
启动文件符号:确保链接脚本中
_sidata、_sdata等符号与汇编一致。 - 中断服务函数:弱定义(weak)允许用户重写,避免链接错误。
六、总结
手写Cortex-M0+启动文件不仅加深对启动流程的理解,还能灵活实现向量表重定位与中断优先级分组。本文提供的完整代码可直接用于STM32G0等M0+内核MCU,通过实战验证了分组对中断嵌套的影响。建议在Bootloader设计中优先采用向量表重定位,提升固件升级的可靠性。