4. ARM Cortex-M4/M7移植详解:中断向量表重定向、PendSV和SysTick异常处理、FPU寄存器上下文保存与恢复、MPU配置要点
好,咱们进入正题。Cortex-M4和M7,这两个核在基站设备里太常见了。M7性能更强,带FPU和Cache,M4则是性价比之王。移植RTOS,说白了就是让操作系统能管好这两个核的“脾气”。今天我把几个关键点掰开揉碎了讲,都是我在项目里踩过坑的地方。
4.1 中断向量表重定向:别让系统跑飞了
中断向量表,就是一张“谁来了该找谁”的地址表。芯片上电默认从Flash启动,向量表在Flash开头。但RTOS运行时,有时候需要把向量表搬到RAM里,为什么?因为动态修改中断入口啊。
我个人习惯,在系统初始化时就把向量表重定向到SRAM。Cortex-M系列有个寄存器叫VTOR(Vector Table Offset Register),写它就完事了。
/* 重定向向量表到SRAM */
#define SRAM_VECTOR_TABLE_BASE 0x20000000
void vector_table_relocate(void)
{
/* 1. 先把Flash里的向量表复制到SRAM */
memcpy((void*)SRAM_VECTOR_TABLE_BASE,
(void*)SCB->VTOR,
sizeof(interrupt_vector_table));
/* 2. 设置VTOR寄存器 */
SCB->VTOR = SRAM_VECTOR_TABLE_BASE;
/* 3. 数据同步屏障,确保写操作完成 */
__DSB();
__ISB();
}
嗯,这里有个小细节:复制向量表时,要连主栈指针(MSP)的初始值一起复制。否则复位后SP指向错误地址,系统直接HardFault。你想想看,这个坑我踩过不止一次。
4.2 PendSV和SysTick异常处理:RTOS的“心脏”
RTOS的上下文切换,靠的就是PendSV和SysTick。SysTick负责产生周期性滴答中断,PendSV则负责“延迟”执行上下文切换。
为什么要延迟?因为如果在SysTick中断里直接切任务,万一此时正在响应其他高优先级中断,就会出问题。PendSV的优先级可以设到最低,等所有中断都处理完了,它才执行。
4.2.1 SysTick配置
void systick_init(uint32_t ticks)
{
/* 重装载值 = 系统时钟 / 调度频率 - 1 */
SysTick->LOAD = ticks - 1;
/* 清零当前值 */
SysTick->VAL = 0;
/* 使能SysTick,使能中断,使用处理器时钟 */
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
/* 设置PendSV和SysTick优先级 */
NVIC_SetPriority(PendSV_IRQn, 0xFF); /* 最低优先级 */
NVIC_SetPriority(SysTick_IRQn, 0xF0); /* 比PendSV高一点 */
}
SysTick中断服务函数里,我一般只做两件事:滴答计数器加1,触发PendSV。千万别在SysTick里干重活,否则会影响系统实时性。
void SysTick_Handler(void)
{
/* 更新系统滴答 */
os_tick_increment();
/* 触发PendSV,请求上下文切换 */
SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
}
4.2.2 PendSV处理:上下文切换的核心
PendSV_Handler是RTOS的灵魂。它负责保存当前任务的寄存器,恢复下一个任务的寄存器。说白了,就是“换人干活”。
__asm void PendSV_Handler(void)
{
/* 保存当前任务上下文 */
MRS R0, PSP ; 获取当前任务栈指针
STMDB R0!, {R4-R11} ; 保存R4~R11到任务栈
/* 保存FPU寄存器(如果有) */
TST R14, #0x10 ; 检查EXC_RETURN的bit4
IT EQ
VSTMDBEQ R0!, {S16-S31} ; 如果使用了FPU,保存S16~S31
/* 更新当前任务控制块的栈指针 */
LDR R1, =current_tcb
STR R0, [R1]
/* 切换到下一个任务 */
BL os_schedule_next
/* 恢复下一个任务上下文 */
LDR R1, =current_tcb
LDR R0, [R1] ; 获取新任务的栈指针
/* 恢复FPU寄存器 */
TST R14, #0x10
IT EQ
VLDMIAEQ R0!, {S16-S31}
/* 恢复R4~R11 */
LDMIA R0!, {R4-R11}
/* 设置PSP并返回 */
MSR PSP, R0
ORR LR, LR, #0x04 ; 确保返回后使用PSP
BX LR
}
TST R14, #0x10。这是检查异常返回时是否使用了FPU。如果当前任务用了浮点运算,硬件会自动把S0~S15压栈,但S16~S31需要软件保存。这个判断逻辑,我当年调试了整整两天才搞明白。
4.3 FPU寄存器上下文保存与恢复:浮点运算的“隐形杀手”
M4和M7都带单精度FPU,M7还支持双精度。FPU寄存器(S0~S31)在任务切换时必须保存,否则任务A算到一半,切到任务B,B把FPU寄存器改了,再切回A,A的结果就错了。
但有个问题:不是所有任务都用FPU。如果每个任务都保存FPU寄存器,浪费栈空间和切换时间。怎么办?
我的做法是:懒加载(Lazy Stacking)。Cortex-M4/M7支持FPU的自动延迟压栈。通过设置FPCCR寄存器的LSPEN位,硬件只在真正使用FPU指令时才自动保存FPU上下文。
/* 使能FPU懒加载 */
void fpu_lazy_stacking_enable(void)
{
uint32_t fpccr = FPU->FPCCR;
/* 设置LSPEN=1,使能懒保存 */
fpccr |= FPU_FPCCR_LSPEN_Msk;
/* 设置ASPEN=1,自动保存 */
fpccr |= FPU_FPCCR_ASPEN_Msk;
FPU->FPCCR = fpccr;
}
关键点: 懒加载模式下,任务第一次使用浮点指令时,会触发一个UsageFault?不对,实际上是硬件自动插入一个“浮点上下文保存”操作。但如果你在中断里用了浮点,而中断优先级高于某个任务,可能会造成FPU上下文嵌套保存。我曾经在项目中遇到这个问题,最后解决办法是:中断服务函数里禁用浮点运算,或者把中断优先级分组设好。
另外,M7的双精度FPU有32个双精度寄存器(D0~D31),每个占64位。保存时要注意对齐。我建议统一用8字节对齐的栈指针,避免未对齐访问导致异常。
4.4 MPU配置要点:内存保护的“防火墙”
MPU(内存保护单元)在基站里太重要了。任务A不能乱写任务B的内存,内核态和用户态要隔离。M4和M7的MPU基本一样,最多支持8个或16个区域(看具体芯片)。
配置MPU,我总结了四个步骤:
- 禁用MPU:配置前先关掉,防止配置过程中产生异常。
- 设置区域属性:基地址、大小、权限、缓存策略。
- 使能区域:每个区域单独使能。
- 使能MPU:最后全局使能,并设置PRIVDEFENA等选项。
void mpu_configure(void)
{
/* 1. 禁用MPU */
MPU->CTRL = 0;
/* 2. 配置区域0:Flash(代码区),只读,不可缓存 */
MPU->RNR = 0;
MPU->RBAR = (0x08000000) | /* 基地址 */
MPU_RBAR_VALID_Msk | /* 区域有效 */
(0 << MPU_RBAR_REGION_Pos); /* 区域号 */
MPU->RASR = (MPU_RASR_XN_Msk) | /* 禁止执行?不,代码区要执行 */
(0x1B << MPU_RASR_AP_Pos) | /* 特权读写,用户只读 */
(0x04 << MPU_RASR_TEX_Pos) | /* 不可缓存 */
(0x00 << MPU_RASR_C_Pos) |
(0x00 << MPU_RASR_B_Pos) |
(0x19 << MPU_RASR_SIZE_Pos) | /* 512KB */
MPU_RASR_ENABLE_Msk;
/* 3. 配置区域1:SRAM(数据区),可读写,可缓存 */
MPU->RNR = 1;
MPU->RBAR = (0x20000000) |
MPU_RBAR_VALID_Msk |
(1 << MPU_RBAR_REGION_Pos);
MPU->RASR = (0x03 << MPU_RASR_AP_Pos) | /* 特权/用户均可读写 */
(0x01 << MPU_RASR_TEX_Pos) | /* 写通(Write-Through) */
(0x01 << MPU_RASR_C_Pos) |
(0x00 << MPU_RASR_B_Pos) |
(0x13 << MPU_RASR_SIZE_Pos) | /* 128KB */
MPU_RASR_ENABLE_Msk;
/* 4. 使能MPU,启用特权默认访问 */
MPU->CTRL = MPU_CTRL_PRIVDEFENA_Msk |
MPU_CTRL_ENABLE_Msk;
/* 数据同步 */
__DSB();
__ISB();
}
另外,M7的L1 Cache(I-Cache和D-Cache)和MPU配合使用时,要注意Cache策略。比如,D-Cache用“写回(Write-Back)”模式性能好,但需要定期Clean和Invalidate。而MPU的TEX、C、B位就是用来控制这些的。我一般把代码区配成“不可缓存”或“只缓存指令”,数据区配成“写通”或“写回”看具体需求。
4.5 总结一下
好,今天的内容就这些。总结几个要点:
- 向量表重定向:别忘了Cache一致性问题。
- PendSV和SysTick:SysTick只做轻量工作,PendSV负责切换。
- FPU上下文:懒加载是好东西,但要注意中断嵌套。
- MPU配置:向量表和DMA区域要配成不可缓存。
这些经验,都是我在基站项目里一点一点磨出来的。你想想看,一个基站跑着几十个任务,如果上下文切换出问题,整个基站就挂了。所以,移植RTOS时,这些细节一定要抠死。
下一章,咱们聊聊RISC-V架构的移植。RISC-V和ARM差别挺大,尤其是中断和特权级,到时候再细说。