一,前言
RTOS中最吸引我的地方是带汇编的任务切换,没想到我在看port.c,全部看完后,有一个xPortPendSVHandler函数觉得理解的不太清晰,但是以前我肯定理解过的,所以呢,我又调试了下,等于再复习下。
二,xPortPendSVHandler源码分析
- 先做过铺垫,来看下什么时候会调用xPortPendSVHandler中断函数。
任务时间片切换#define xPortSysTickHandler SysTick_Handler
,xPortSysTickHandler函数就是一个中断。在启动第一个高优先级任务前,vPortSetupTimerInterrupt函数中已经设置了心跳包的频率。portNVIC_SYSTICK_LOAD_REG = ( configCPU_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
#define configTICK_RATE_HZ ( ( TickType_t ) 500 )
说明是500Hz,1/500=0.002就是2ms的一个心跳中断。
心跳中断中的函数中portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
就是请求pendsv中断,并增加tick值,检查任务就绪队列中是否有任务,存在任务则请求pendsv进行任务切换。如下c代码还是很好理解的。
void xPortSysTickHandler( void )
{
uint32_t ulPreviousMask;
ulPreviousMask = portSET_INTERRUPT_MASK_FROM_ISR();
{
/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE )
{
/* Pend a context switch. */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
portCLEAR_INTERRUPT_MASK_FROM_ISR( ulPreviousMask );
}
那么就说明是心跳时钟切换过程中通过portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
来设置pendsv中断请求,之后才进入xPortPendSVHandler中断函数。
2. xPortPendSVHandler源码分析
中文注释我已经直接写在函数中了。如下,这都是我经过debug单步调试验证过的。一开始没有看懂,主要是stmdb用于将寄存器压栈,ldmia用于将寄存器弹出栈等几个汇编指令忘记了含义。
__asm void xPortPendSVHandler( void )
{
extern vTaskSwitchContext
extern pxCurrentTCB
/* *INDENT-OFF* */
PRESERVE8
mrs r0, psp
ldr r3, = pxCurrentTCB /* Get the location of the current TCB. */
ldr r2, [ r3 ]
/* 保存现场,主要保存PSP中的r4~r11 */
subs r0, # 32 /* Make space for the remaining low registers. */
str r0, [ r2 ] /* Save the new top of stack. */
stmia r0 !, { r4 - r7 } /* Store the low registers that are not saved automatically. */
/* 因为thumb指令stmia只能访问r0~r7 ,所以下面r8~r11先保存到r4~r7,然后再push到psp栈中,等于腾出32自己(8个寄存器的地址空间)先push r4~r7再push r8~r11 */
mov r4, r8 /* Store the high registers. */
mov r5, r9
mov r6, r10
mov r7, r11
stmia r0 !, { r4 - r7 }
/* 执行vTaskSwitchContext 函数前tcb和lr入栈保护 */
push { r3, r14 }
cpsid i
bl vTaskSwitchContext
cpsie i
/* 恢复tcb和lr */s
pop { r2, r3 } /* lr goes in r3. r2 now holds tcb pointer. */
/* r2地址就是tcb指针的存储位置保存到r1 */
ldr r1, [ r2 ]
/* 将tcb内容保存到r0,tcb的内容就是通过vTaskSwitchContext运行后冲裁出待切换的tcb地址 */
ldr r0, [ r1 ] /* The first item in pxCurrentTCB is the task top of stack. */
/* 从r0栈顶地址+16,腾出4个寄存器r4~r7的空间 */
adds r0, # 16 /* Move to the high registers. */
/* 将+16~+32地址中的内容先pop到r4~r7然后移动入r8~r11 */
ldmia r0 !, { r4 - r7 } /* Pop the high registers. */
mov r8, r4
mov r9, r5
mov r10, r6
mov r11, r7
/* 临时保存下r0地址,就是栈底的地址,会保存到PSP,等bx从中断退出时候会用到*/
msr psp, r0 /* Remember the new top of stack for the task. */
/* r0从最开始预留16以及pop后自动增加16,总共加了32,现在前去32,等于还原到栈顶,目的是之后用来恢复r4~r11 */
subs r0, # 32 /* Go back for the low registers that are not automatically restored. */
/* 然后pop r4~r7,之前popr8~r11的原因就是16 bit thumb指令只能pop r0~r7,不能直接pop 8个寄存器*/
ldmia r0 !, { r4 - r7 } /* Pop low registers. */
/* 通过r3来查找返回地址,用来切换任务,返回到的是另外一个更高优先级的task函数*/
bx r3
ALIGN
/* *INDENT-ON* */
}
几个关键语句的调试步骤调试截图如下
stmia r0 !, { r4 - r7 }
运行前。
stmia r0 !, { r4 - r7 }
运行后。将寄存器r4到r7的值依次赋值给r0指定的地址单元(0x18000460),每次赋值一次r0就加4。4个寄存器赋值完成后,R0内容(就是地址)增加了4x4=16,变成了0x18000470。
push { r3, r14 }
含义为MSP入栈保护MSP地址为0x180006e8,然后将r3和r14保存到这个地址进行入栈入栈地址是做减法,所以先查看0x180006e0地址的内容。
运行push { r3, r14 }后0x180006e0和0x180006e4地址内容保存了r3和r14的内容。进行了入栈操作。
同理
pop { r2, r3 }
是出栈,将r3(tcb地址)和lr通过出栈,保存到r2和r3。adds r0, # 16
运行前adds r0, # 16
运行后,r0地址变更。4个mov后将0x180001D0后面的四个字节内容(就是自己构造栈空间中最后4个字节代表r8~r11)进行了pop。
从中断退出时候会用到。
subs r0, # 32
的含义是,r0从最开始预留16以及pop后自动增加16,总共加了32,现在前去32,等于还原到栈顶。ldmia r0 !, { r4 - r7 } ,之前已经pop过r8~r11,现在就是pop r4r7,之前popr8r11的原因就是16 bit thumb指令只能pop r0~r7,不能直接pop这8个寄存器
bx r3就是跳入0xFFFFFFFD,说明被中断前用的是PSP的地址。0xFFFFFFF9说明用的是MSP的地址。
所以找0x180001e0的地址。要从svc中断返回的地址PC保存在0x180001e0+7*4=0x180001fc的内容,就是0x1100150D,但是thumb指令需要LSB,所以0x1100150D-1就是0x1100150C地址。bx r3从中断出来后,会跳入0x1100150C执行task。
msr psp, r0
是保存r0的地址到PSP栈中,目的是把栈底保存,等bx三,小结
通过调试及查看cortex内核手册复习了下内核寄存器的用法,这个函数就很容易理解了,看来有空我要重新看下cortexM内核手册的全部内容。