版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_33894122/article/details/81876832
栈空间初始化
CM3内核是小端格式的,栈也是满减栈,下面是任务TCB初始化的时候任务栈空间的初始化
(这部分内容与CM3内核紧密相连,需要读者非常熟悉CM3堆栈机制(MSP PSP双堆栈机制等),异常机制等处理)
/**
* This function will initialize thread stack
*
* @param tentry [the entry of thread]
* @param parameter [the parameter of entry]
* @param stack_addr [the beginning stack address]
*
* @return stack address
*/
smc_stack_t *smc_thread_stack_init(void (*entry)(void *parameter),
void *parameter,
smc_stack_t *stack_addr)
{
/* Align the stack to 8-bytes 主要是在CM3响应异常自动入栈的时候,强制SP对齐到双字上*/
/*且是满减栈,并没有先自减1,之后每次在栈里面存入一个数据就会先自减1(满减栈操作)*/
stack_addr = (smc_stack_t *)SMC_ALIGN_DOWN((smc_stack_t)stack_addr, 8);
/*顺序是xPSR R15 R14 R12 R3-R0 R11-R4,由于是初始化,在后来OS运行起来之后,xPSR R15 R14 R12 R3-R0 都是自动入栈和出栈的*/
/*任务切换函数只需要根据SP来保存上文的R11-R4,并根据TCB的第一个元素来恢复下文的R11-R4即可*/
*(--stack_addr) = (smc_stack_t)(1 << 24); /* xPSR */
*(--stack_addr) = (smc_stack_t)entry; /* R15 (PC) (Entry Point) */
*(--stack_addr) = (smc_stack_t)0; /* R14 (LR) */
*(--stack_addr) = (smc_stack_t)0; /* R12 */
*(--stack_addr) = (smc_stack_t)0; /* R3 */
*(--stack_addr) = (smc_stack_t)0; /* R2 */
*(--stack_addr) = (smc_stack_t)0; /* R1 */
*(--stack_addr) = (smc_stack_t)parameter; /* R0 : argument */
/* Remaining registers saved on process stack*/
/*之前的寄存器在每次出栈和入栈的时候会根据PSP还是MSP自动出栈入栈,之后的寄存器在任务切换的时候需要手动地入栈和出栈*/
*(--stack_addr) = (smc_stack_t)0; /* R11 */
*(--stack_addr) = (smc_stack_t)0; /* R10 */
*(--stack_addr) = (smc_stack_t)0; /* R9 */
*(--stack_addr) = (smc_stack_t)0; /* R8 */
*(--stack_addr) = (smc_stack_t)0; /* R7 */
*(--stack_addr) = (smc_stack_t)0; /* R6 */
*(--stack_addr) = (smc_stack_t)0; /* R5 */
*(--stack_addr) = (smc_stack_t)0; /* R4 */
/*这个返回值就是SP的任务栈指针(满减栈),返回后由任务TCB的第一个元素任务SP来保存这个值*/
/*在TCB的第一个元素来保存sp指针还有一个关键原因,就是在任务切换的时候,是根据TCB的优先级来的,在切换的新的优先级后*/
/*直接从TCB第一个元素地址取值给PC就可以继续上次任务切换的断点继续运行*/
return stack_addr;
}
任务切换
在任务切换过程中是用pendsv异常来实现的,为啥要用pendsv来实现任务切换呢?
在RTOS性能指标有一个是中断延迟时间,比如在IRQ10执行的时候systick来了,将会抢占IRQ10,如果尝试在systick中切换任务将会导致IRQ延期执行(而且延期时间较长),这对于中断函数来说是不能容忍的,还有在systick中任务切换后在中断活跃状态下返回线程模式将会导致处理器的错误。
后来许多OS在一些处理器上的操作是,在systick中检测如果嵌套了中断,则这次就不任务切换,在下次systick任务切换,就可以尽快执行玩systick异常,减少中断延迟。这种方法弊端也极其明显,任务切换可能会被拖一个tick的时间,如果连续每个tick都是抢占其他中断的,就拖得时间更长了,如果中断源中断频率跟tick频率相近就会共振,长时间任务切换不能完成,显然问题很严重。
pendsv的出现就是来完美解决这个问题的,其实就是延迟任务切换的过程,等到没有中断的时候立刻执行pendsv异常(需要设置pendsv优先级最低)。这样既可以防止在systick中直接任务切换导致中断延迟较大问题,又可以防止单纯地检测抢占中断就不进行任务切换等待下一次的任务切换延时甚至是sysick任务切换与中断的共振。
在systick_handler中置位置pendsv
void smc_scheduler(void)
{
if (smc_scheduler_lock_count == 0U) {
smc_uint32_t status = smc_cpu_disable_interrupt();
smc_thread_ready = smc_thread_highest_ready();
smc_cpu_enable_interrupt(status);
/* if the destination thread is not the same as current thread */
if (smc_thread_current != smc_thread_ready) {
/*当前进程 跟 已就绪的最高优先级进程不一样就会任务切换*/
if (smc_scheduler_hook)
smc_scheduler_hook();
/* switch to new thread */
if (smc_interrupt_nest > 0U)
/*中断和非中断中方式不一样,这里其实在CM3平台的实现是一样的*/
smc_thread_intrrupt_switch();
else
smc_thread_switch();
}
}
}
/*中断和非中断方式都是向pendsv控制寄存器中写一位,使能触发pendsv延迟暂缓异常*/
/**
* This function will make context switch.
*
* @note [switch not in interrupt]
*
*/
void smc_thread_switch(void)
{
smc_mem_write_32(NVIC_INT_CTRL, NVIC_PENDSVSET);
}
/**
* This function will make context switch.
*
* @note [switch in interrupt]
*
*/
void smc_thread_intrrupt_switch(void)
{
smc_mem_write_32(NVIC_INT_CTRL, NVIC_PENDSVSET);
}
在其余中断执行完成之后就会执行pendsv异常 PendSV_Handler
/**
* This function will make contex switch
*/
__asm void PendSV_Handler(void)
{
/*在OS运行起来之后其实刚刚进入systick(也可能是任务A主动放弃CPU,这时候就是pendsv不是systick)的时候A任务的一些寄存器就已经自动入栈了*/
/*在之后在此调度到A的时候,这些寄存器还会自动出栈根据PSP的值*/
/*在OS运行起来之前,第一次任务切换的时候,其实在进入pendsv(不是systick)的时候是一些寄存器在MSP中自动入栈*/
IMPORT smc_thread_current /*文件外导入符号*/
IMPORT smc_thread_ready
CPSID I /* Prevent interruption during context switch*/
/*任务上下文切换期间关闭中断*/
LDR R1, =smc_thread_current /*这里在系统没有运行之前,smc_thread_current是空,运行之后指向某个具体的任务TCB*/
/*结合后面两句指令,所以在系统没有运行的时候任务切换就不需要保存现场,在运行之后任务切换就需要保存现场*/
LDR R1, [R1]
CBZ R1, PendSV_Handler_Nosave /* skip save R4-R11 for first run user thread */
/*获取当前任务的栈顶地址,稍后会利用栈顶地址,结合相关操作将R4-R11入栈,其他寄存器在进入handler模式的时候已经入栈了*/
STMFD R0!, {R4-R11} /*将R4-R11入栈*/
STR R0, [R1] /* smc_thread_current->sp = PSP */
PendSV_Handler_Nosave
LDR R0, =smc_thread_current /* smc_thread_current = smc_thread_ready */
LDR R1, =smc_thread_ready
LDR R2, [R1]
STR R2, [R0] /*这就是所谓的任务切换了,就是将smc_thread_current指向smc_thread_ready*/
LDR R3, [R2]
LDMFD R3!, {R4-R11}
/*恢复B任务的R4-R11寄存器,其他寄存器会在退出pendsv的时候出栈并得到恢复*/
STR R3, [R2]
MSR PSP, R3
ORR LR, LR, #0x04
CPSIE I /* 任务切换完成开中断Prevent interruption during context switch。*/
/*可能有人会有疑问了,为啥其他地方进入临界区必须要用那个临界区的宏,因为那些地方是可能被中断的,就是被嵌套,如果直接使用开关中断,在最外面一层中断退出的时候就会导致系统中断全部打开*/
/*所以需要用局部变量来保存中断寄存器的一些状态,而这个pendsv中为啥就可以直接用开关中断状态呢,大概就是不能被中断吧。() */
BX LR /*这个地方并不是函数返回,这里LR已经被修改为EXEC_RETURN了,详见CM3权威指南*/
/*BX LR之后就会导致处理器的模式切换和特权级的切换,切换回原来进入pendsv之前的处理器模式和特权级状态。一旦模式切换回去,就会自动出栈一些寄存器R0-R3,R12,R14,R15,xPSR*/
/*PC已经更新了,PSP也已经更新了,其他寄存器也更新了,直接运行切换到的任务断点(假设原来运行的任务A,现在已经切换到任务B了*/
/*可能运行几个tick之后要切换回A,这时候由于刚刚切换到B的时候已经保存了进入pendsv前的R4-R11了,其实是进入A进入systick之前的状态)*/
NOP
}
进入任务切换只有两个接口函数
void smc_rtos_scheduler(void);/*第一次任务切换,其中smc_thread_current = NULL;*/
void smc_scheduler(void);/*系统运行后每次任务切换都必须调用这个接口*/