调度器,极小资源单片机值得一用

调度器,极小资源单片机值得一用

本文转载于“调度器,极小资源单片机值得一用(转)”及“再出个调度器,极小资源单片机值得一用 ”

自认为有如下特点:

  1. 超级可以移植性,与CPU无关,几乎任何支持C语言编程的CPU都可以用!(本文仅仅以51单片机为例而已,但实际上可以任意移植)
  2. 小之又小, 原理很简单,一看就懂。
  3. 省之又省, 可以说对RAM和ROM省到极致。
  4. 取protothread之精华,将定时器与状态机和伪线程语法融合到一个框架,任务函数可以有两种写法。
  5. 基于定时器触发,调度效率高,最大化减少无效的代码运行时间。
/***********************************************************/  
#include 
#include 

/*****************小小调度器部分开始*************************/ 
#define  _SS		static char lc=0; switch(lc) { case 0: lc=0;
#define  _EE   	 	}; lc=0; 
#define  WaitX(a,b) settimer(&lc,__LINE__,a,b); return ; case __LINE__:

struct TASK {
	char td;
	void (*fp)();
};

#define MAXTASKS 5
struct TASK tasks[MAXTASKS];

//设置定时器
void settimer(char *lc, char line, char tmrid, int d) 
{
	*lc = line;
	tasks[tmrid].td = d;
}

//逻辑定时器处理,在定时器中断里调用
void dectimers() 
{ 
	unsigned char i;   
	for (i=0; i0) tasks[i].td--;  
	}
}

//任务调度函数,在main里面运行
void runtasks() 
{
	unsigned char i;    
	for(i=0; i < MAXTASKS; i++) {   
		if (tasks[i].fp != 0) {
			if (tasks[i].td == 0) {
				tasks[i].td = -1;  
				tasks[i].fp();
			}  
		}         
	}
}
/****************小小调度器部分结束*****************************/

sbit KEY = P3^2;
unsigned char code numtab[16]={0x24,0x6F,0xE0,0x62,0x2B,0x32,0x30,0x67,0x20,0x22,0x21,0x38,0xB4,0x68,0xB0,0xB1};

sfr IAP_CONTR = 0xC7;
sfr WDT_CONTR = 0xC1;

//清除看门狗
void clr_wdt()
{
	WDT_CONTR =0x3C; 
}

//初始化定时器
void InitT0()
{
	TMOD = 0x21;
	IE |= 0x82;  // 12t
	TL0=0Xff;
	TH0=0Xb7;
	TR0 = 1;
}

//定时器中断
void INTT0(void) interrupt 1 using 1
{
	TL0=0Xff;    //10ms 重装
	TH0=0Xb7;
	dectimers();
}

sbit LED1= P2^4;  

//任务一,状态机写法
void ontimer0()
{ 
	LED1=!LED1;  // LED1引脚接在发光管负极,LED1=0 为亮,LED1=1为灭。
	//重装定时器
	if (LED1) tasks[0].td=45;  //450mS 灭
	else tasks[0].td=5;  //50ms  亮
}

//任务二,状态机写法
char keycount=0;
void task1()
{
	if (KEY == 0) {
		keycount++;
		if (keycount>20) IAP_CONTR = 0x60;  //持续按下键1秒,将重启并进入固件升级
	} else {
		keycount=0;
	}
	//重装定时器
	tasks[1].td=5;
}

//任务三,伪线程写法
void  task2()
{
	static char i;
	_SS
	while(1) {
		for(i=0;i<=9;i++) { //从0--9快速显示,间隔200mS
			WaitX(2,20);	//等待200mS,实际是设置定时器2为200mS
			P1=numtab[i];
		}

		for(i=0;i<=9;i++) { //从0--9慢速显示,间隔500mS
			WaitX(2,50);	//等待500mS,实际是设置定时器2为500mS
			P1=numtab[i];
		}
	}
	_EE
}

void main()
{
	unsigned char         KeyNum;
	P3M0 = 0x00;
	P3M1 =0x00;
	//WDT_CONTR= 0x00; //关闭看门狗
	P1 = 0xff;         //关显示

	clr_wdt();
	InitT0();

	KEY =1;				//按键IO口
	KeyNum=0;			//按下次数

	//装载任务:
	tasks[0].fp=ontimer0;
	tasks[1].fp=task1;
	tasks[2].fp=task2;

	//循环调度
	while(1){
		runtasks();
		clr_wdt();
	}
}


作者对上面代码进行了再次优化(2楼)

/****小小调度器开始**********************************************/
#define MAXTASKS 2
static unsigned char timers[MAXTASKS];
unsigned char 	     currdt;

#define _SS 			static unsigned char _lc; switch(_lc){default: 
#define _EE 			;}; _lc=0; return 255;
#define WaitX(tickets)  do {_lc = __LINE__+((__LINE__%256)==0); return tickets;} while(0); case __LINE__+((__LINE__%256)==0): 

#define RunTask(TaskName,TaskID) do { if (timers[TaskID]==0) timers[TaskID]=TaskName(); } while(0);
#define CallSub(SubTaskName)  	 do {_lc = _LINE__ + ((__LINE__%256)==0); return 0; case __LINE__+((__LINE__%256)==0): currdt=SubTaskName(); if(currdt != 255) return currdt;} while(0);
#define UpdateTimers() 		     unsigned char i; for(i=MAXTASKS;i>0 ;i--) {if((timers[i-1]!=0)&&(timers[i-1]!=255)) timers[i-1]--;}

#define SEM unsigned int 
//初始化信号量
#define InitSem(sem) sem=0;
//等待信号量
#define WaitSem(sem) do{ sem=1; WaitX(0); if (sem>0) return 1;} while(0);
//等待信号量或定时器溢出, 定时器tickets 最大为0xFFFE
#define WaitSemX(sem,tickets)  do {sem=tickets+1; WaitX(0); if(sem>1){ sem--;  return 1;}} while(0);
//发送信号量
#define SendSem(sem)  do {sem=0;} while(0);
/*****小小调度器结束*************************************************/

sbit LED1 = P2^1;
sbit LED2 = P2^2;

void InitT0()
{
	TMOD = 0x21;
	IE |= 0x82; // 12t
	TL0=0Xff;
	TH0=0XDB; //22M---b7;
	TR0 = 1;
}

void INTT0(void) interrupt 1 using 1
{
	UpdateTimers();

	TL0=0Xff; //10ms 重装
	TH0=0XDB; //b7;    
}


void  task1()
{
	_SS
	while(1) {
		WaitX(50);
		LED1=!LED1;   
	}
	_EE
}

void  task2()
{
	_SS
	while(1) {
		WaitX(100);
		LED2=!LED2;   
	}
	_EE
}


void main()
{
	InitT0();
	while(1) {
		RunTask(task1,0);
		RunTask(task2,1);
	}
}

在keil下编译,又减少了18字节的ROM(超过10%了)。应该运行效率会更高。


// 以下为说明

小小调度器任务函数的写法主要注意的,主要有三点:

  1. 任务函数内部变量,建议都用静态局部变量来定义。
  2. 任务函数内不能用switch语句。
  3. 任务函数内,不能用return语句。 因为return已经被赋予任务延时的特定意义。(这是返回型任务函数版本的一个强制要求)

这三点,并不会明显造成写程序的不方便。


从裸奔到使用OS操作系统或调度系统的代价主要有:

硬件资源代价(对RAM和ROM的消耗),学习代价(学会其原理,并掌握其用法),移植代价(往不同cpu上移植的工作量),效率代价(使用调度系统后带来的额外cpu负担),商业代价(版权费用),稳定性代价(是否引入潜在不稳定因素,或者增大bug跟踪调试工作量)。

从这几方面来讲,应用小小调度器的代价,都是非常小的。

  1. 硬件资源代价: 前面的优化版本已经说明问题。keil下,本例程ram消耗 : 22字节,rom消耗126字节.
  2. 学习代价: 小小调度器总共只有十多行代码,如果我们做一个简单的解释说明,理解起来其实是很快的。我相信学习时间比其他调度系统要短。
  3. 移植代价: 几乎没有什么移植工作量,对于各种cpu,几乎是通吃。
  4. 效率代价: 我们一直在努力优化,相信调度效率已经不低了。比如任务切换时间,应该是可以做到uS级别,甚至亚uS级别。
  5. 商业代价: 小小本调度器为免费使用,无需支付任何费用。
  6. 稳定性代价:小小调度器本质上仅仅是几个宏而已,未涉及任何对内部寄存器或堆栈的操作,避免了引入不稳定风险因素,所有操作都在可预见,可把控的前提下进行。

本调度器的宗旨是:以最小的代价,实现基于自然语法的多任务并行处理机制。并具备代码的高度可以移植性。
每个任务占用3个字节RAM。任务数量没有限制。
其工作原理很简单,大家可在此基础上任意DIY自己的调度方法。


102及144楼修改后结果:

在这里插入代码片
#include 

/**************小小调度器开始啦 *********************************/
#define MAXTASKS	3 //顶层任务数量,可按需增加,最大为255个。
char  				timers[MAXTASKS];
char 				currid; //当前运行的任务号

#define _SS   		 static char lc=0; switch(lc){ case 0: //跳转开始
#define _EE   		 }; lc=0; //跳转结束
#define DelayX(b)  	 settimer(&lc,__LINE__,b); return ; case __LINE__: //任务内延时等待“函数”
#define RunTask(a,b) currid=b; if (timers[b] == 0) {timers[b] = -1; a();} //运行顶层任务
#define CallSub(x)   DelayX(0);x(); if (timers[currid]!=-1) return; //调用子任务

void settimer(char *lc, char line,char d) //设置定时器
{
	*lc=line; timers[currid]=d;
}
/****************小小调度器结束啦*********************************/

sbit KEY = P3^2;
unsigned char code numtab[16]={0x24,0x6F,0xE0,0x62,0x2B,0x32,0x30,0x67,0x20,0x22,0x21,0x38,0xB4,0x68,0xB0,0xB1};

sfr IAP_CONTR = 0xC7; 
sfr WDT_CONTR = 0xC1;

//清除看门狗
void clr_wdt()
{
	WDT_CONTR =0x3C; 
}

//初始化定时器,产生10ms中断
void InitT0()
{
	TMOD = 0x21;
	IE |= 0x82; // 12t
	TL0=0Xff;
	TH0=0Xb7;
	TR0 = 1;
}

//定时器中断处理函数
void INTT0(void) interrupt 1 using 1
{
	unsigned char i;
	TL0=0Xff;    //10ms 重装
	TH0=0Xb7;
	//逻辑定时器处理
	for (i=0;i0) {
			timers[i]--;
		}
	}        
}

sbit LED1= P2^4;  

//任务一,状态机写法
void ontimer0()
{
	LED1=!LED1; // LED1引脚接在发光管负极,LED1=0 为亮,LED1=1为灭  
	if (LED1) timers[currid]=45; //450mS 灭
	else timers[currid]=5; //50ms  亮
}

//任务二,状态机写法
char keycount=0;
void task1()
{
	if(KEY==0) {
	   keycount++;
	   if (keycount>20) IAP_CONTR = 0x60;
	} else {
		keycount=0;
	}
	timers[currid]=5;         //重装定时器
}

//这是一个子任务函数
void  subtask()
{
	static char i;
	_SS
	for(i=0;i<=5;i++) {
		DelayX(20);
		P1=numtab[i];
	}
	_EE
}

//任务三,“线程”写法,复杂任务请用这种方式来写,比传统状态机写法更简单,更自然,而且更省ROM!
void  task2()
{
	static char i;         //变量请申明为静态的。
	_SS

	while(1) {
		for(i=0;i<=9;i++) {
			//先从0--9快速显示,间隔200mS
			DelayX(20);
			P1=numtab[i];
		}
		
		for(i=0;i<=9;i++) {
			//然后从0--9慢速显示,间隔500mS
			DelayX(50);
			P1=numtab[i];
			CallSub(subtask); //顶层任务的任何地方都可以调用子任务
		}
		//CallSub(subtask); //顶层任务的任何地方都可以调用子任务
	}

	_EE
}
/* task2分解
void  task2()
{
	static char i;

	static char lc=0; 
	switch(lc){
		case 0:

	while(1) {
		for(i=0;i<=9;i++) {
			//先从0--9快速显示,间隔200mS
			//DelayX(20);
			settimer(&lc,__LINE__,20); // *lc=line; timers[currid]=20;
			return ;
		case __LINE__:
			P1=numtab[i];
		}

		for(i=0;i<=9;i++) {
			//然后从0--9慢速显示,间隔500mS
			//DelayX(50);
			settimer(&lc,__LINE__,50); // *lc=line; timers[currid]=50;
			return ; 
		case __LINE__:
			P1=numtab[i];
			//CallSub(subtask); //顶层任务的任何地方都可以调用子任务
			DelayX(0);
			x(); 
			if (timers[currid]!=-1)
				return; 
		}
		//CallSub(subtask); //顶层任务的任何地方都可以调用子任务
	}

	}; lc=0; //跳转结束
}
*/
void main()
{
	P3M0 = 0x00;
	P3M1 =0x00;
	P1 = 0xff; //关LED数码管显示

	InitT0(); //初始化定时器,以便产生10ms中断
	KEY =1; //按键IO口        

	while(1) {
		clr_wdt(); //清除看门狗

		//这里,只需运行顶层任务即可
		RunTask(ontimer0,0); //ontimer0任务放在第0个任务槽
		RunTask(task1,1); //task1任务放在第1个任务槽
		RunTask(task2,2); //task2任务放在第2个任务槽
		//...还可以增加更多任务
	}
}

protothread

protothread 是经典的状态机,只要看函数开头的switch(pt-﹥lc) 进行散转,就一清二楚了。
因此说protothread是协程,实际很不准确, 准确地说法是: protothread 就是状态机!

protothread使用行号代替人为设置的状态,如果使用1个Byte记忆状态,那么一个任务函数内代码行数不能超过255行,所以用了1个word来记忆,因此占用了两个字节.一个任务函数内代码行数不超过65535行(这完全够了)。

这就是一个protothread任务消耗2个字节的原因:就是状态变量的消耗。只要使用状态机机制,谁能省掉这个呢?

protothread具备状态机的所有特性,只要能使用状态机的地方,就可以使用protothread。即可以单独使用,也可以结合其他多任务系统机制使用,也可以和时间调度触发联合使用。

而protothread主要好处是: protothread任务函数的代码语法,无需人工设计各个状态, 非常类似于多线程环境下的任务代码,大大降低了代码的复杂度.

附带说下:
很多地方有说到protothread的,总提到protothread是协程,好像有很多问题,但protothread根本不是协程,而是典型的状态机!
状态机的实时性本身不是问题,问题在于如何使用状态机这种机,因此,protothread的实时性本身也根本不是问题,问题在于如何使用protothread。

参考“protothread, 就是状态机!”及“Protothread 机制”!

你可能感兴趣的:(C)