【深入学习51单片机】二、一个极简RTOS源码分析

目录

  • 一、书接上回
  • 二、初始化过程
  • 三、任务的创建
  • 四、任务的切换
  • 五、任务的等待(系统延时)

一、书接上回

上回写了一个测试程序,可以直观的体会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;
}
  1. system_init()
    • 是空的,用来初始化用户配置
  2. 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任务初始化
}
  1. os_task_create(1,task_1,(unsigned char)os_tsak_stack[1]);
    • 初始化用户任务task_1
    • 指定优先级为1
    • 指定任务堆栈
  2. os_task_create(0,task_0,(unsigned char)os_tsak_stack[0]);
    • 初始化用户任务task_0
    • 指定优先级为0
    • 指定任务堆栈
  3. 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还是很绕的。。。

你可能感兴趣的:(单片机,51单片机,RTOS,任务调度)