一、书接上回
上回写了一个测试程序,可以直观的体会PC指针和堆栈指针的变化和影响。这章写下参考程序的过程原理。
源码我已上传,免积分,贴在第一章末尾
上回链接:
【深入学习51单片机】一、基于8051的RTOS内核任务切换堆栈过程剖析
二、初始化过程
main函数:
int main(void)
{
system_init();
os_init();
os_task_create(1,task_1,(unsigned char)os_tsak_stack[1]);
os_task_create(0,task_0,(unsigned char)os_tsak_stack[0]);
os_start();
return 0;
}
- system_init()
- 是空的,用来初始化用户配置
- os_init()
- 初始化了用来切换任务堆栈的定时器,上章提到了用定时器切换任务,就是指利用中断函数调用过程中PC指针自动入栈,修改栈地址后PC指针弹出到目标地址。
- 初始化了系统空闲任务,因为系统在空闲的时候总要有段程序跑,就是这个
- 源码:
/* 初始化系统 */
void os_init(void)
{
EA = 0;
ET0 = 1; //定时器0开中断
AUXR &= 0x7f; //12T模式
TMOD &= 0xf0;
TMOD |= 0x01; //16位计数器
TH0 = 0xee;
TL0 = 0x00; //5ms中断
os_int_count = 0; //嵌套层数初始化
os_task_rdy_tab = 0; //任务就绪表初始化
os_task_create(MAX_TASK-1,task_idle,(unsigned char)os_tsak_stack[MAX_TASK-1]); //系统idle任务初始化
}
- os_task_create(1,task_1,(unsigned char)os_tsak_stack[1]);
- 初始化用户任务task_1
- 指定优先级为1
- 指定任务堆栈
- os_task_create(0,task_0,(unsigned char)os_tsak_stack[0]);
- 初始化用户任务task_0
- 指定优先级为0
- 指定任务堆栈
- os_start();
- 查找优先级最高的就绪态任务
- 切换任务堆栈到优先级最高的就绪态任务,也就是调度器
- 开始运行内核
- 源码:
/* 任务开始运行 */
void os_start(void)
{
unsigned char i;
for(i=0; i<MAX_TASK; i++)
{
if(os_task_rdy_tab & os_map_tab[i]) //查找任务就续表
{
break;
}
}
os_task_running_id = i; //优先级最高的先运行
EA = 1;
SP = os_tcb[os_task_running_id].os_task_stcak_bottom + 1; //弹出是任务地址
TR0 = 1;
os_running = os_true;
}
三、任务的创建
源码:
/* 创建任务 */
void os_task_create(unsigned char task_id,void (*task)(void),unsigned char stack_point)
{
char cpu_sr;
os_enter_critical();
((unsigned char idata*)stack_point)[0] = (unsigned int)task;
((unsigned char idata*)stack_point)[1] = (unsigned int)task >> 8; //任务地址放在栈底两个字节
os_tcb[task_id].os_task_stcak_bottom = stack_point; //栈底
// os_tcb[task_id].os_task_stcak_top = stack_point + 14; //栈顶(起始这里应该,对任务堆栈初始化后的指针)
//为什么要加1?(因为栈中已经有数据了task)当然初不初始化都可以,用上面的也行(只不过好多人对为什么14有疑惑)
os_tcb[task_id].os_task_stcak_top = os_tesk_stack_init(stack_point + 1);
os_task_rdy_tab |= os_map_tab[task_id]; //更新任务就绪表
os_tcb[task_id].os_tsak_wait_tick = 0; //无等待
os_tcb[task_id].suspend = 0; //任务以就绪
os_exit_critical();
}
任务创建放在临界段(就是失能中断和使能恢复中断的操作之间的代码)处理,中断状态在临时变量cpu_sr中保存,有几个重要的数据结构:
- 任务控制表,记录堆栈操作关键位置
/*任务控制表 */
typedef struct os_task_control_table
{
unsigned char os_tsak_wait_tick;
unsigned char os_task_stcak_top;
unsigned char os_task_stcak_bottom;
unsigned char suspend;
}TCB;
- 优先级映射表,任务优先级调度用的就是这个表
const unsigned char os_map_tab[] = {0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};
如果理解了上一章讲的东西,那这里就较好理解了。
- 先把用户定义的任务地址压到栈底,然后初始化栈底和栈顶
- 任务优先级和os_map_tab会绑定,然后更新就绪态到任务就续表os_task_rdy_tab
- 最后进行任务状态设置(等待和挂起)
四、任务的切换
源码:
/* 任务切换心跳 */
void timer0_isr(void) interrupt 1 //using 0 默认用,所有的寄存器会自动入栈,
{ // 用using 1 2 3 则需要手动对r0-r7入栈,出栈(请查看寄存器组选择(看任意一本讲51的书))
unsigned char i;
char cpu_sr;
if(os_true == os_running)
{
os_enter_critical();
/* 寄存器入栈(注释的部分是在进入中断函数前已经压入栈中
(函数调用会让PC压栈,中断函数不但会让PC压栈,还会对下面五个寄存器压栈)
,就是我写的这个顺序) */
// __asm PUSH ACC
// __asm PUSH B
// __asm PUSH DPH
// __asm PUSH DPL
// __asm PUSH PSW
// __asm PUSH 0
// __asm PUSH 1
// __asm PUSH 2
// __asm PUSH 3
// __asm PUSH 4
// __asm PUSH 5
// __asm PUSH 6
// __asm PUSH 7
os_int_enter();
os_time_tick();
if(1 == os_int_count) //当然,51单片机最多只能有一次中断嵌套,os_int_count最大为2(+本次)
{
os_tcb[os_task_running_id].os_task_stcak_top = SP;
for(i=0; i<MAX_TASK; i++) //找到已经就绪,未被挂起,且优先级最高的任务(任务调度器)
{
if(os_task_rdy_tab & os_map_tab[i])
{
if(0 == os_tcb[i].suspend)
{
break;
}
}
}
if(os_task_running_id != i) //现在执行的任务就是优先级最高的,所以不需要任务切换
{
os_task_running_id = i; //可执行的最高优先级任务
SP = os_tcb[os_task_running_id].os_task_stcak_top; //最高优先级任务的栈顶
}
}
TF0 = 0; //清除中断标志
TH0 = 0xee; //时间重装载
TL0 = 0x00;
os_int_exit();
// __asm POP 7
// __asm POP 6
// __asm POP 5
// __asm POP 4
// __asm POP 3
// __asm POP 2
// __asm POP 1
// __asm POP 0
os_exit_critical();
/*和前面的道理相同(既然压栈了,当然也要出栈)(说明,后面手动加的__asm RETI,则下面的这几跳POP是必须要的,
这是为什么呢? 哈哈,因为默认情况下,RETI指令是要在,POP的后面,不然就不能执行POP,这几个寄存器值就恢复不了
,最重要的是SP也就乱套了 ,,,,,,,)*/
// __asm POP PSW
// __asm POP DPL
// __asm POP DPH
// __asm POP B
// __asm POP ACC
/*写不写都一样(写了,这条语句执行完会进行上面寄存器和PC出栈,
不写的话,C语言写的中断函数,编译器汇编过后,会在后面加一条reti指令,和上面执行相同的功能)
【写到这里,同学我突然有这么一个想法,能不能用ret(reti对中断标志位有清零作用,这里我们手动清除标志位就可以了)
,函数返回指令来返回中断函数,当然这些POP之类的指令就需要收到写了,程序是玩出来的,大家可以尽情的尝试哈^_^】*/
// __asm RETI
// __asm ret //好像不可以,为什么呢?
}
}
- os操作同样放在了临界段处理
- 更新中断层数,因为只有这个中断,所以os_int_count只会是1
- 更新系统心跳,用于系统延时
- 找到已经就绪,未被挂起,且优先级最高的任务(任务调度器)
for(i=0; i<MAX_TASK; i++) //找到已经就绪,未被挂起,且优先级最高的任务(任务调度器)
{
if(os_task_rdy_tab & os_map_tab[i])
{
if(0 == os_tcb[i].suspend)
{
break;
}
}
}
- 运行态任务id更新,并切换堆栈指针到其栈顶,恢复现场后,SP会弹出到栈底,也就是要返回的PC指针位置
五、任务的等待(系统延时)
源码:
/* 系统延时 */
void os_delay(unsigned char tisks)
{
unsigned char i;
// char cpu_sr;
if(tisks > 0)
{
//os_enter_critical();
EA = 0; //直接操作,而不用临界段的方法主要是为了任务切换更快
__asm PUSH ACC //寄存器入栈(在此之前不能有任何运算操作,不然会改变寄存器值)
__asm PUSH B
__asm PUSH DPH
__asm PUSH DPL
__asm PUSH PSW
__asm PUSH 0
__asm PUSH 1
__asm PUSH 2
__asm PUSH 3
__asm PUSH 4
__asm PUSH 5
__asm PUSH 6
__asm PUSH 7
os_tcb[os_task_running_id].os_task_stcak_top = SP;
os_tcb[os_task_running_id].os_tsak_wait_tick = tisks; //延时时间
os_tcb[os_task_running_id].suspend = 1; //因为有延时,所以先挂起本任务
for(i=0; i<MAX_TASK; i++) //找到已经就绪,未被挂起,且优先级最高的任务
{
if(os_task_rdy_tab & os_map_tab[i])
{
if(0 == os_tcb[i].suspend)
{
break;
}
}
}
os_task_running_id = i; //可执行的最高优先级任务
SP = os_tcb[os_task_running_id].os_task_stcak_top; //最高优先级任务的栈顶
__asm POP 7
__asm POP 6
__asm POP 5
__asm POP 4
__asm POP 3
__asm POP 2
__asm POP 1
__asm POP 0
__asm POP PSW
__asm POP DPL
__asm POP DPH
__asm POP B
__asm POP ACC
EA = 1;
//os_exit_critical();
//__asm RET //后面是函数返回,即 ret
//__asm reti
}
}
- 压栈,保存现场,更新栈顶
- 保存延时时间,然后挂起任务
- 找到已经就绪,未被挂起,且优先级最高的任务
- 切换正在运行的任务id:os_task_running_id
- 堆栈指向该id的任务
- 现场恢复,SP会弹出到栈底,也就是要返回的PC指针位置
#五、小结
因为有现场保存和恢复,看起来复杂了很多,最好是仿真单步跟着走,否则SP还是很绕的。。。