此函数就是判断什么时候该执行那一个任务了,实现任务的管理操作,应用者只需要在main()函数中调用此函数就可以了,并不需要去分别调用和处理任务函数。
到此,一个时间片轮询应用程序的架构就建好了,大家看看是不是非常简单呢?此架构只需要两个函数,一个结构体,为了应用方面下面将再建立一个枚举型变量。
下面就说说怎样应用吧,假设我们有三个任务:时钟显示,按键扫描,和工作状态显示。
1.定义一个上面定义的那种结构体变量:
代码/***************************************************************************************Variabledefinition**************************************************************************************/staticTASK_COMPONENTSTaskComps[]={{0,60,60,TaskDisplayClock},//显示时钟{0,0,0,TaskKeySan},//按键扫描{0,30,30,TaskDispStatus},//显示工作状态//这里添加你的任务。。。。};在定义变量时,我们已经初始化了值,这些值的初始化,非常重要,跟具体的执行时间优先级等都有关系,这个需要自己掌握。①大概意思是,我们有三个任务,没1s执行以下时钟显示,因为我们的时钟最小单位是1s,所以在秒变化后才显示一次就够了。②由于按键在按下时会参数抖动,而我们知道一般按键的抖动大概是0ms,那么我们在顺序执行的函数中一般是延伸0ms,而这里我们每0ms扫描一次,是非常不错的出来,即达到了消抖的目的,也不会漏掉按键输入。③为了能够显示按键后的其他提示和工作界面,我们这里设计每30ms显示一次,如果你觉得反应慢了,你可以让这些值小一点。后面的名称是对应的函数名,你必须在应用程序中编写这函数名称和这三个一样的任务。.任务列表:代码//任务清单typedefenum_TASK_LIST{TAST_DISP_CLOCK,//显示时钟TAST_KEY_SAN,//按键扫描TASK_DISP_WS,//工作状态显示//这里添加你的任务。。。。TASKS_MAX//总的可供分配的定时任务数目}TASK_LIST;好好看看,我们这里定义这个任务清单的目的其实就是参数TASKS_MAX的值,其他值是没有具体的意义的,只是为了清晰的表面任务的关系而已。3.编写任务函数:代码/***************************************************************************************FunctionName:TaskDisplayClock()*Description:显示任务*EntryParameter:None*ReturnValue:None**************************************************************************************/voidTaskDisplayClock(void){}/***************************************************************************************FunctionName:TaskKeySan()*Description:扫描任务*EntryParameter:None*ReturnValue:None**************************************************************************************/voidTaskKeySan(void){}/***************************************************************************************FunctionName:TaskDispStatus()*Description:工作状态显示*EntryParameter:None*ReturnValue:None**************************************************************************************/voidTaskDispStatus(void){}//这里添加其他任务。。。。。。。。。现在你就可以根据自己的需要编写任务了。4.主函数:代码/***************************************************************************************FunctionName:main()*Description:主函数*EntryParameter:None*ReturnValue:None**************************************************************************************/intmain(void){InitSys();//初始化while(1){TaskProcess();//任务处理}}到此我们的时间片轮询这个应用程序的架构就完成了,你只需要在我们提示的地方添加你自己的任务函数就可以了。是不是很简单啊,有没有点操作系统的感觉在里面?不防试试把,看看任务之间是不是相互并不干扰?并行运行呢?当然重要的是,还需要,注意任务之间进行数据传递时,需要采用全局变量,除此之外还需要注意划分任务以及任务的执行时间,在编写任务时,尽量让任务尽快执行完成。。。。。。。。三、操作系统操作系统的本身是一个比较复杂的东西,任务的管理,执行本事并不需要我们去了解。但是光是移植都是一件非常困难的是,虽然有人说过“你如果使用过系统,将不会在去使用前后台程序”。但是真正能使用操作系统的人并不多,不仅是因为系统的使用本身很复杂,而且还需要购买许可证(ucos也不例外,如果商用的话)。这里本人并不想过多的介绍操作系统本身,因为不是一两句话能过说明白的,下面列出UCOS下编写应该程序的模型。大家可以对比一下,这三种方式下的各自的优缺点。代码/***************************************************************************************FunctionName:main()*Description:主函数*EntryParameter:None*ReturnValue:None**************************************************************************************/intmain(void){OSInit();//初始化uCOS-IIOSTaskCreate((void(*)(void*))TaskStart,//任务指针(void*)0,//参数(OS_STK*)TaskStartStk[TASK_START_STK_SIZE-1],//堆栈指针(INT8U)TASK_START_PRIO);//任务优先级OSStart();//启动多任务环境return(0);}代码/***************************************************************************************FunctionName:TaskStart()*Description:任务创建,只创建任务,不完成其他工作*EntryParameter:None*ReturnValue:None**************************************************************************************/voidTaskStart(void*p_arg){OS_CPU_SysTickInit();//InitializetheSysTick.#if(OS_TASK_STAT_EN0)OSStatInit();//这东西可以测量CPU使用量#endifOSTaskCreate((void(*)(void*))TaskLed,//任务1(void*)0,//不带参数(OS_STK*)TaskLedStk[TASK_LED_STK_SIZE-1],//堆栈指针(INT8U)TASK_LED_PRIO);//优先级//Herethetaskofcreatingyourwhile(1){OSTimeDlyHMSM(0,0,0,);}}不难看出,时间片轮询法优势还是比较大的,即由顺序执行法的优点,也有操作系统的优点。结构清晰,简单,非常容易理解。延伸阅读:你是单片机高手,还是菜鸟?看看程序框架就知道了从大学参加电子设计大赛到现在,在单片机学习的道路上也有几年的摸索了,把自己的一些心得体会分享给大家。
初学单片机时,往往都会纠结于其各个模块功能的应用,如串口(3,)对各种功能IC的控制,电机控制PWM,中断应用,定时器应用,人机界面应用,CAN总线等.这是一个学习过程中必需的阶段,是基本功。很庆幸,在参加电子设计大赛赛前培训时,MCU周围的控制都训练的很扎实。经过这个阶段后,后来接触不同的MCU就会发现,都大同小异,各有各的优势而已,学任何一种新的MCU都很容易入手包括一些复杂的处理器。而且对MCU的编程控制会提升一个高度概况——就是对各种外围进行控制(如果是对复杂算法的运算就会用DSP了),而外围与MCU的通信方式一般也就几种时序:IIC,SPI,intel,M。这样看来MCU周围的编程就是一个很简单的东西了。
然而这只是嵌入式开发中的一点皮毛而已,在接触过多种MCU,接触过复杂设计要求,跑过操作系统等等后,我们在回到单片机的裸机开发时,就不知不觉的就会考虑到整个程序设计的架构问题;一个好的程序架构,是一个有经验的工程师和一个初学者的分水岭。
以下是我对单片机程序框架以及开发中一些常用部分的认识总结:
任何对时间要求苛刻的需求都是我们的敌人,在必要的时候我们只有增加硬件成本来消灭它;比如你要8个数码管来显示,我们在没有相关的硬件支持的时候必须用MCU以动态扫描的方式来使其工作良好;而动态扫描将或多或少的阻止了MCU处理其他的事情。在MCU负担很重的场合,我会选择选用一个类似max外围ic来解决这个困扰;
然而庆幸的是,有着许多不是对时间要求苛刻的事情:
例如键盘的扫描,人们敲击键盘的速率是有限的,我们无需实时扫描着键盘,甚至可以每隔几十ms才去扫描一下;然而这个几十ms的间隔,我们的MCU还可以完成许多的事情;
单片机虽然是裸机奔跑,但是往往现实的需要决定了我们必须跑出操作系统的姿态——多任务程序;
比如一个常用的情况有4个任务:
1、键盘扫描;
、led数码管显示;
3、串口数据需要接受和处理;
4、串口需要发送数据;
如何来构架这个单片机的程序将是我们的重点;
读书时代的我会把键盘扫描用查询的方式放在主循环中,而串口接收数据用中断,在中断服务函数中组成相应的帧格式后置位相应的标志位,在主函数的循环中进行数据的处理,串口发送数据以及led的显示也放在主循环中;
这样整个程序就以标志变量的通信方式,相互配合的在主循环和后台中断中执行;
然而必须指出其不妥之处:
每个任务的时间片可能过长,这将导致程序的实时性能差。如果以这样的方式在多加几个任务,使得一个循环的时间过长,可能键盘扫描将很不灵敏。所以若要建立一个良好的通用编程模型,我们必须想办法,消去每个任务中费时间的部分以及把每个任务再次分解;下面来细谈每个任务的具体措施:
1、键盘扫描
键盘扫描是单片机的常用函数,以下指出常用的键盘扫描程序中,严重阻碍系统实时性能的地方;
众所周知,一个键按下之后的波形是这样的(假定低有效):
在有键按下后,数据线上的信号出现一段时间的抖动,然后为低,然后当按键释放时,信号抖动一段时间后变高。当然,在数据线为低或者为高的过程中,都有可能出现一些很窄的干扰信号。
unsignedcharkbscan(void)
{
unsignedcharsccode,recode;
P=0xf8;
if((P0xf8)!=0xf8)
{
delay();//延时0ms去抖--------这里太费时了,很糟糕
if((P0xf8)!=0xf8)
{
sccode=0xfe;
while((sccode0x08)!=0)
{
P=sccode;
if((P0xf8)!=0xf8)
break;
sccode=(sccode1)
0x01;
}
recode=(P0xf8)
0x0f;
return(sccoderecode);
}
}
return(KEY_NONE);
}
键盘扫描是需要软件去抖的,这没有争议,然而该函数中用软件延时来去抖(ms级别的延时),这是一个维持系统实时性能的一个大忌讳;
一般还有一个判断按键释放的代码:
While(kbscan()!=KEY_NONE)
;//死循环等待
这样很糟糕,如果把键盘按下一直不放,这将导致整个系统其它的任务也不能执行,这将是个很严重的bug。
有人会这样进行处理:
While(kbsan()!=KEY_NONE)
{
Delay(10);
If(Num++10)
Break;
}
即在一定得时间内,如果键盘一直按下,将作为有效键处理。这样虽然不导致整个系统其它任务不能运行,但也很大程度上,削弱了系统的实时性能,因为他用了延时函数;
我们用两种有效的方法来解决此问题:
1、在按键功能比较简单的情况下,我们仍然用上面的kbscan()函数进行扫描,只是把其中去抖用的软件延时去了,把去抖以及判断按键的释放用一个函数来处理,它不用软件延时,而是用定时器的计时(用一般的计时也行)来完成;代码如下
voidClearKeyFlag(void)
{
KeyDebounceFlg=0;
KeyReleaseFlg=0;
}
voidScanKey(void)
{
++KeyDebounceCnt;//去抖计时(这个计时也可以放在后台定时器计时函数中处理)
KeyCode=kbscan();
if(KeyCode!=KEY_NONE)
{
if(KeyDebounceFlg)//进入去抖状态的标志位
{
if(KeyDebounceCntDEBOUNCE_TIME)//大于了去抖规定的时间
{
if(KeyCode==KeyOldCode)//按键依然存在,则返回键值
{
KeyDebounceFlg=0;
KeyReleaseFlg=1;//释放标志
return;//Hereexitwithkeycode
}
ClearKeyFlag();//KeyCode!=KeyOldCode,只是抖动而已
}
}else{
if(KeyReleaseFlg==0)
{
KeyOldCode=KeyCode;
KeyDebounceFlg=1;
KeyDebounceCnt=0;
}else{
if(KeyCode!=KeyOldCode)
ClearKeyFlag();
}
}
}else{
ClearKeyFlag();//没有按键则清零标志
}
KeyCode=KEY_NONE;
}
在按键情况较复杂的情况,如有长按键,组合键,连键等一些复杂功能的按键时候,我们跟倾向于用状态机来实现键盘的扫描;
//
avr单片机中4*3扫描状态机实现
charread_keyboard_FUN()
{
staticcharkey_state=0,key_value,key_line,key_time;
charkey_return=No_key,i;
switch(key_state)
{
case0://最初的状态,进行3*4的键盘扫描
key_line=0b00000;
for(i=1;i=4;i++)//扫描键盘
{
PORTD=~key_line;//输出行线电平
PORTD=~key_line;//必须送次!!!(注1)
key_value=Key_maskPIND;//读列电平
if(key_value==Key_mask)
key_line=1;//没有按键,继续扫描
else
{
key_state++;//有按键,停止扫描
break;//转消抖确认状态
}
}
break;
case1://此状态来判断按键是不是抖动引起的
if(key_value==(Key_maskPIND))//再次读列电平,
{
key_state++;//转入等待按键释放状态
key_time=0;
}
else
key_state--;//两次列电平不同返回状态0,(消抖处理)
break;
case://等待按键释放状态
PORTD=0b;//行线全部输出低电平
PORTD=0b;//重复送一次
if((Key_maskPIND)==Key_mask)
{
key_state=0;//列线全部为高电平返回状态0
key_return=(key_line
key_value);//获得了键值
}
elseif(++key_time=)//如果长时间没有释放
{
key_time=0;
key_state=3;//进入连键状态
key_return=(key_line
key_value);
}
break;
case3://对于连键,每隔50ms就得到一次键值,windowsxp系统就是这样做的
PORTD=0b;//行线全部输出低电平
PORTD=0b;//重复送一次
if((Key_maskPIND)==Key_mask)
key_state=0;//列线全部为高电平返回状态0
elseif(++key_time=5)//每隔50MS为一次连击的按键
{
key_time=0;
key_return=(key_line
key_value);
}
break;
}
returnkey_return;
}
以上用了4个状态,一般的键盘扫描只用前面3个状态就可以了,后面一个状态是为增加“连键”功能设计的。连键——即如果按下某个键不放,则迅速的多次响应该键值,直到其释放。在主循环中每隔10ms让该键盘扫描函数执行一次即可;我们定其时限为10ms,当然要求并不严格。
、数码管的显示
一般情况下我们用的八位一体的数码管,采用动态扫描的方法来完成显示;非常庆幸人眼在高于50hz以上的闪烁时发现不了的。所以我们在动态扫描数码管的间隔时间是充裕的。这里我们定其时限为4ms(50HZ),用定时器定时为ms,在定时中断程序中进行扫描的显示,每次只显示其中的一位;当然时限也可以弄长一些,更推荐的方法是把显示函数放入主循环中,而定时中断中置位相应的标志位即可;
//Timer0比较匹配中断服务,4ms定时
interrupt[TIM0_COMP]voidtimer0_