任务的状态
- 运行态:正在占用CPU, 对于单核cpu, 任何时候只有一个正在运行的任务
- 就绪态:被登记到就绪表的任务,等待占有CPU的任务释放CPU后,可能获得调度
- 挂起态/等待态:不在上述状态的任务
抢占式调度
一旦就绪态中出现优先级更高的任务,会立即剥夺当前运行的任务,把CPU分配给这个优先级更高的任务;这样CPU总是执行处于就绪条件下优先级最高的任务;
在freeRTOS中,任务的优先级就是任务的唯一编号,所以不能两个任务是同一个优先级;
心跳定时器与延时
OSTimeDly函数通过心跳定时器的节拍来完成延时,功能就是先挂起当前任务,然后设置延时节拍,然后通过任务切换;
在指定的时钟节拍到来后,将当前任务恢复到就绪状态;
关于空闲任务
实现多任务的关键
- 程序代码
- 私有堆栈
- 任务控制块
保存当前任务的SP指针不是C语言能干的事,这个是和CPU硬件体系相关的,所以只能借助于汇编;以ARM为例
ldr r4, =p_OSTCBCur 的作用是把p_OSTCBCur的地址加载到r4; 因为p_OSTCBCur不是汇编能直接处理的符号,加上=取了它的地址就可以给ldr进行操作了;
ldr r5,[r4] 则是对r4的内容作为地址,解引用,给到r5,此时r5就是p_OSTCBCur;
str sp, [r5] 则是把当前的sp指针复制到*p_OSTCBCur, 完成了sp保存到任务控制块中;
再谈函数
这里可以看到,任务是以函数的形式组织的,这和为什么sp指针很重要有关,这里我们来深入探讨这个问题;
首先我们知道,函数很重要,因为不管是多任务还是单任务,我们都离不开函数,它把代码按功能组织成一个又一个块;
有了函数,代码的编写者可以完成自己想要的功能的抽象,以及代码组织的更为高效;
我们可以通过跳转指令完成函数调用,并需要在函数执行结束后返回到原先的位置继续执行;
int funcb()
{
return 0;
}
void funca(int num)
{
int k[10];
k[0] = funcb();
k[1] = num;
}
int main()
{
int a = 5;
funca(a);
}
在main()中,我们调用了funca, 在funca中,我们调用了funcb,看看在微观上(汇编的角度),需要做哪些事情才能完成;
在深入了解这个问题之前,我们需要学习一下arm架构中的几个重要概念;
在arm架构中,r0-r15寄存器是cpu的通用寄存器;
在函数调用的过程中,r0-r3用于传递函数参数,r4-r12 用于 存储临时数据和局部变量;
r13 是 sp, 栈指针寄存器,指向当前栈的顶部,arm架构中,栈是满减栈
r14是lr, 链接寄存器,用于存储函数返回地址,当函数调用子函数时,当前函数的返回地址会被存储在链接寄存器中,
r15是pc, 程序计数器,存储当前正在执行的指令的地址;
上述C代码对应的汇编代码如下:
.func main
main:
@函数入口
mov r0, #5
str r0, [sp, #0]
@调用函数funca
bl funca
@函数返回
mov r0, #0
bx lr
.endfunc
.func funca
funca:
@prologue
sub sp, sp, #40 @分配40字节的栈空间给k
@function body
bl funcb
str r1, [sp, #0] @将funcb的返回值存入k[0]
str r0, [sp, #4] @将参数num 存入k[1]
@epilogue
add sp, sp, #40
@函数返回
bx lr
.endfunc
.func funcb
funcb:
mov r1, #0
bx lr
.endfunc
.endfunc
从上面的汇编可以看出,为什么sp很重要,对一个函数而言,sp存放着这个函数的局部变量;
对一个任务而言,sp还要存放pc和寄存器组,当发生切换的时候,会从任务堆栈中把这些环境恢复出来;
关于函数调用约定
关于函数的调用有一些约定,以指导编译器的相关设计:
- 参数的传递方式:参数是通过寄存器,栈还是其他方式传递
- 寄存器的使用:哪些寄存器用于传递参数,保存临时数据等
- 返回值的传递:返回值是通过寄存器,栈还是其他方式传递
- 栈的管理,栈是如何分配,释放和维护的,栈帧的布局等
这些调用约定可以保证,不同模块之间的调用能够正确执行;
在ARM架构中,常见的函数调用约定有以下几种:
- ARM标准(AAPCS), 这是ARM架构的标准调用约定,全程ARM Architeture Procedure Call Stantard
- ARM 嵌入式ABI, 针对嵌入式的特定调用约定,如ARM EABI
AAPCS主要包括以下几个方面:
- 寄存器用途规定,见上面的描述
- 参数传递方式:优先使用寄存器传递,如果参数个数或者size超过了寄存器的容量,剩余的参数会压入栈中;参数的传递顺序是r0, r1,r2, r3, 然后是栈传递
- 返回值的传递方式:返回值通常放在r0寄存器,如果返回值是结构体或者浮点数,使用额外的寄存器
- 栈的管理:函数调用时,会在栈上创建一个栈帧(Stack Frame),用于保存局部变量,函数参数和返回地址信息,栈帧的布局和管理由编译器负责(这里是指如果是用C或者高级语言编程时,这些会由编译器帮用户完成,但如果是自己写汇编,则还是需要自己手动完成)
- 异常处理
如何切换任务
任务切换的核心就是这里的OS_TASK_SW()
首先是pc入栈,stmfd sp!, {pc} , 同时sp!的意思是sp的值更新;
这里需要注意:
虽然注释是PC入栈,但是实际上入栈的是lr, 这是因为当前调用了OSSched()函数,我们希望当任务切换回来后,从OSSChed()返回后的地址开始执行;
后面紧跟着是stmfd sp!, {r0-r12, lr} , r0-r12, lr入栈以及cpsr入栈;
stmfd 是连续压栈,stmfd sp!, {r0-r12, lr} 是 lr 先入栈,然后依次是r12, r11, ... r0
ldmfd是连续出栈,ldmfd sp!, {r0-r12, lr} 是r0先出栈,最后是lr出栈
综上看,从上往下代表从高地址到低地址,那么现在栈的布局就是
- PC --- 栈底
- LR
- R12
- R11
- ...
- R0
- CPSR <--- SP 栈顶
SP始终是栈顶,往下增长,PC则是在栈底;
完成这些后,通过SaveSPToCurTcb 把当前的sp给到p_OSTCBCur,完成任务栈的保存;
注意这里的入栈顺序,当出栈的时候,必须按照同样顺序出栈才能完成运行环境的重建;
拿到要运行任务的栈顶指针,执行POP_ALL, 把任务栈中恢复任务的运行环境
根据前面入栈的顺序,出栈就是
sp 指向 CPSR, 通过ldmfd sp!, {r4} 把这个cpsr给到r4; sp+=4
然后给把任务栈的R0到R12给到CPU寄存器R0到R12, lr给LR, PC给PC;
关于抢占式调度
标签:r0,函数,sp,任务,lr,寄存器,任务调度 From: https://www.cnblogs.com/Arnold-Zhang/p/18170639