1).单次执行的任务
2).周期性执行的任务
3).事件触发执行的任务
1)ucos ii最多有64个优先级,(最高0~最低63)
2)在
例: #define OS_LOWEST_PRIO 18 //即最低优先级为18,(最高0~最低18,共19个)。
3)其中:
0 ------------------------------- ucos ii系统保留了对该优先级的使用权
1 ------------------------------- ucos ii系统保留了对该优先级的使用权
2 ------------------------------- ucos ii系统保留了对该优先级的使用权
3 ------------------------------- ucos ii系统保留了对该优先级的使用权
OS_LOWEST_PRIO-3 ------ ucos ii系统保留了对该优先级的使用权
OS_LOWEST_PRIO-2 ------ ucos ii系统保留了对该优先级的使用权
OS_LOWEST_PRIO-1 ------为“统计任务”
OS_LOWEST_PRIO ---------为“空闲任务”
即:最低两个优先级为ucos ii系统所调用,分别为OS_TaskIdle“空闲任务”和OS_TaskStat“统计任务”。其他(0,1,2,3,4,OS_LOWEST_PRIO-3,OS_LOWEST_PRIO-2,OS_LOWEST_PRIO-1,OS_LOWEST_PRIO)均被系统保留了使用权,或许ucos ii系统升级之后,这些优先级有可能被系统所调用。
故,用户能放心使用的优先级为(最高4~最低OS_LOWEST_PRIO-4)
4)任务优先级分配时,宜宽松安排。
例:安排优先级为(5,7,9,13),各优先级之间留有间隙,方便日后项目的修改维护升级。
当一个任务正在调用某个公共函数时,被另一个高优先级的任务抢占,当这个高优先级任务也调用了同一个公共函数时,极有可能破坏原任务的数据。
措施:
1)互斥调用:
以互斥方式调用公共函数:
A、采取关中断方式(函数简单,运行时间短):先关中断再调用公共函数,再开中断;
B、使用互斥信号量(函数比较复杂,运行时间比较长)。
2)可重入设计
设计关键:函数内及其子函数内不使用全局资源(全局变量,静态变量)。
一个任务的代码设计是从上到下的过程,先分析系统总体任务关联图,明确每个任务在系统整体中的位置和角色,就在将一个一个地进行详细关联分析,然后画出任务的程序流程图,最后按程序流程图编写程序代码。
程序清单L3-1
例:
void xxx_ISR(void)
{
OSIntEnter(); //使系统掌握当前中断的嵌套深度等
//...中断程序
OSIntExit(); //实现抢占式调度功能
}
1)触发ISR的事件不包括数据
使用信号量与关联任务通讯。
2)触发ISR的事件包括数据
a.低频事件:将采集数据的工作放在关联任务中完成,ISR使用信号量与关联任务通讯;
b.中高频事件:采集数据工作放在ISR中完成,使用消息邮箱与关联任务通讯;
c.高频事件:数据采集放在ISR中完成,使用消息队列与关联任务通讯。
使用范围:被控制方总能够及时响应控制方发出的信号,完成相应处理任务,并在下一次信号来到之前进入等待状态。
ucos-II的信号量实际上就是计数信号量
使用范围:被控制方不能保证在下一次信号来到之前处理完本次控制方发出的信号,但总体上可以响应所有信号。
若被控制方总能及时响应控制方发出的信号,完成相应的处理,并在下一次信号来到之前进入等待状态,则此时计数信号量实际上等同于二值信号量。
原理:计数信号量初始值为0,控制方发出同步信号,使计数信号量加1。被控制方在“同步点”调用等待一个信号量的服务函数,若计数信号量的值不为0,则将计数信号量减1,并继续运行下去;若计数信号量的值为0,则将自己挂起,等待控制方信号。
能实现多个任务(包括ISR)协同控制一个任务。
由于消息邮箱里只能存放一条消息,在用消息邮箱进行同步控制时,必须满足:任何时候消息的产生速度都比消息的消费速度慢,即被控制方任务总是在等待消息,这和二值信号的情况类似。
1)发送消息函数:INT8U OSMboxPost(OS_EVENT *pevent, void *msg);
2)分发消息函数:INT8U OSMboxPostOpt(OS_EVENT *pevent, void *msg, INT8U opt);
a.该函数第三个参数opt为消息分发方式,
b.通常为OS_POST_OPT_BROADCAST (分发给所有正在等待该消息的任务)
消息队列可以存放多个消息,能够有效解决消息的临时堆积问题。和计数信号量类似。但必须满足:消息的平均生产时间比消息的平均消费时间长(即生产速度慢,消费速度快);否则,再长的消息队列也会溢出。
a、当同步过程不需要传输具体内容时,可选择信号量类手段(二值信号量、计数信号量、事件标志组)。
b、当同步过程需要传输具体内容时,可选择消息类手段(消息邮箱、消息队列)。
c、当满足“任何时候同步信息的生产速度都比消费速度慢”时,可选择简单的通讯手段(二值信号量,事件标志组,消息邮箱)。
d、对于非周期性同步信息,不能保证“任何时候同步信息的生产速度都比同步信息的消费速度慢”时,可选择有缓冲功能的通信手段(计数信号量、消息队列)。
e、当同步信号为多个信号的逻辑运算结果时,采用事件标志组作为通讯手段。
1)、两个任务之间的单向同步:实际同步效果受到两个任务之间的优先级关系影响。
a.当控制方任务的优先级低于被控制方任务的优先级时,控制方任务发出信息后使被控制方任务进入就绪状态,并立即发生任务切换,使被控制方任务直接进入运行状态,瞬时同步效果较好。
b.当控制方任务的优先级高于被控制方任务的优先级时,控制方任务发出信息后虽然使被控制方任务进入就绪状态,但并不发生任务切换,只有当控制方再次调用系统服务函数(如延时函数)并使自己挂起,被控制方任务才有运行机会,瞬时同步效果较差。当控制方任务的实时性要求不高,或者采取有缓冲功能的通信手段时,这是可以接受的。常用做法是采用首尾触发方式(控制方任务的尾部触发被控制方的首部),控制方在发出信息后立即调用延时函数(延时时间大于被控制方任务的处理时间),主动使自己挂起来,让低优先级的被控制方任务尽快得到运行机会,从而改善同步效果。
2)、当一个事情触发两个以上任务时,按照任务划分原则可以将它们合并为一个任务。如果这些任务因为其他原因不能合并(不能的功能部件),则可以采用具有消息分发功能的通信机制,以减少通信工具的个数。
可以通过协调生产者和消费者的关系来建立一个产销平衡的理想状态。
通信的双方相互制约,生产者通过提供消息来同步消费者,消费者通过回复消息来同步生产者。
即生产者必须得到消费者的回复后才能进行下一个消息的生产。
一种简单的双向同步过程称为“交汇”:
任务A在交汇点向任务B发消息,并等待任务B回复消息;任务B在交汇点等待任务A发来的消息,在获得消息后再向任务A回复消息。
使用事件标志组来实现。
多个任务相互同步可以将若干相关任务的运行频度保持一致。(每一轮运行的先后快慢可以动态变化)。每个相关任务在运行到同步点时都必须等待其他任务,只有全部相关任务都到达同步点,才可以按优先级顺序依次离开同步点,从而达到相关任务的运行频度保持一致的目的。
它的操作:用一个初始值为零的全局变量作为“签到”计数器,每个到达同步点的任务使用“加1”操作来“签到”。显然,“签到”计数器的数字反映了已经到达同步点的任务数。每个任务首先从“签到”计数器的当前值判断自己是否是最后一个到达同步点的任务。如果不是最后一个到达的任务,就进行“签到”,然后等待同步信号(挂起);如果是最后一个到达的任务,就先清除“签到”计数器,然后再向其他任务分发同步信号,使它们全部处于就绪状态,而自己已经是就绪状态,不需要触发。至此,全部任务均通过了同步点,“签到”计数器也在被清除,从而可靠的完成了一次多任务同步。
程序清单L4-1
例:
#define total 3//需要相互同步的任务数量
OS_EVENT *Mybox;
INT8U count;//签到计数器
void A_Task(void *pdata)
{
INT8U err;
pdata = pdata;
while(1)
{
//....code
OS_ENTER_CRITICAL();//进入临界状态,关中断,保护共享资源count
if(count == total - 1)//是否为最后一个到达的任务
{
//是最后一个到达
count = 0;//清除签到计数器
OS_EXIT_CRITICAL();//退出临界状态,开中断
OSMboxPostOpt(Mybox, (void *)1,
OS_POST_OPT_BROADCAST);//发送同步消息,进行消息分发
}
else//不是最后一个
{
count++;//签到
OS_EXIT_CRITICAL();//退出临界状态,开中断
OSMboxPend(Mybox, 0, &err)//等待同步消息
}
//....code
}
}
可将同步代码精简成一个函数:
void synch(void)
{
INT8U err;
OS_ENTER_CRITICAL();//进入临界状态,关中断,保护共享资源count
if(count == total - 1)//是否为最后一个?
{
//是最后一个到达
count = 0;//清除签到计数器
OS_EXIT_CRITICAL();//退出临界状态,开中断
OSMboxPostOpt(Mybox, (void *)1,
OS_POST_OPT_BROADCAST);//发送同步消息,进行消息分发
}
else//不是最后一个
{
count++;//签到
OS_EXIT_CRITICAL();//退出临界状态,开中断
OSMboxPend(Mybox, 0, &err)//等待同步消息
}
}
void A_Task(void *pdata)
{
INT8U err;
pdata = pdata;
while(1)
{
//....code
synch();//同步
//....code
}
}
void B_Task(void *pdata)
{
INT8U err;
pdata = pdata;
while(1)
{
//....code
synch();//同步
//....code
}
}
void C_Task(void *pdata)
{
INT8U err;
pdata = pdata;
while(1)
{
//....code
synch();//同步
//....code
}
}
被两个或两个以上的任务或ISR访问的资源称为共享资源,共享资源一定是全局变量,但全局变量不一定是共享资源。
任务对共享资源进行访问的代码段落称为关键段落,各个任务访问同一资源的关键段落必须互斥,才能保障共享资源信息的可靠性和完整性,这种使得不同任务访问共享资源时能够确保共享资源信息可靠和完整的措施称为“资源同步”。
实现资源同步的手段为:将各个任务访问同一资源的关键段落进行互斥处理。
资源同步手段:
1)关中断
2)关调度
3)使用互斥信号量
4)使用计数信号量
如程序清单L4-1所示,通过关中断保护共享资源count。防止任务1在访问共享资源时,发生异步中断ISR,然后在ISR结束后再跳到优先级更高的任务2中,改变count值,最后返回到原来的任务1时,count值已经改变,从而影响程序的正常运行。
故应在对共享资源访问的关键段落之前,进入临界状态,关中断,访问完成后再开中断。
观察程序清单L4-1,synch()函数内含有全局变量,使其成为不可重入函数,这和公共资源的要求似乎矛盾,但公共资源并不要求一定是可重入函数,能够做到互斥访问也是可以的。
1)当参与访问共享资源的并发程序单元中包含ISR时(即该共享资源的使用者包含ISR),任务极程序单元只能用个“关中断”的措施来访问共享资源。
2)关中断会影响系统的实时性,为了减轻对系统实时性的不利影响,访问共享资源的关键代码必须精良简短,决不允许在关键代码中包含有可能使自己挂起的系统服务函数,否则将使系统死机。
3)由于关中断直接影响系统的实时性,因此只能用于简单共享资源的短暂访问,故关中断方法常用于对全局变量和小规模全局数据结构的访问。
关调度OSSchedLock()和开调度OSSchedUnlock();
使用前提:共享资源的使用者全部是任务(即不包含ISR)
如果在关调度期间调用系统服务函数而被挂起,其他任务又不能运行,则系统将崩溃。
关调度的资源同步方法优点不多,缺点不少,尽可能不要使用,最好改用互斥信号量来进行资源同步。
若需要访问的共享资源比较复杂,且访问时间较长,则关中断措施肯定是不行的,关调度措施也不可取,对其系统实时性的影响超过了能接受的范围。若改共享资源的使用者全部都是任务(即不包括ISR),就可以采用互斥信号量来访问这个资源。
互斥信号量的使用前提条件:该共享资源的所有使用者全部都是任务(即不包括ISR)。
若共享资源的访问过程比较费时,则这种访问过程实际上变成“使用”过程,容易出现优先级翻转现象,而互斥信号量具有避免优先级翻转的功能,特别适合对共享资源的互斥访问。
互斥信号量也是二值的,是专门用于资源同步的信号量,与行为同步的二值信号量不同。互斥信号量的初始值为1(共0xFF表示),表示共享资源有效(尚未被使用)。一个任务需要访问某共享资源时,首先获取该共享资源对应的互斥信号量,如果获取成功,则说明该共享资源尚未被其他任务占用,就可以对该共享资源进行使用;使用结束后必须及时发送互斥信号量,解除对该共享资源的占用,以便供其他任务使用。
使用互斥信号量来进行共享资源访问对系统实时性影响最小。
1)OS_EVENT *OSMutexCreate (INT8U prio, INT8U *perr);//创建互斥信号量
第一个参数prio :在创建时设置的优先级继承值,须比所有访问该共享资源的任务优先级高。在某个任务获取该互斥信号量时,任务优先级会临时提升至prio,使正在访问该共享资源的任务的优先级高于其他可能会访问该共享资源的任务,避免发生优先级翻转现象。
2)void OSMutexPend (OS_EVENT *pevent, INT32U timeout, INT8U *perr)//获取互斥信号量
3)INT8U OSMutexPost (OS_EVENT *pevent)//释放互斥信号量
使用范例:
程序清单L5-1
void A_Task(void * pdata)
{
INT8U err;
pdata = pdata;
while(1)
{
//code...
OSMutexPend(Sem, 0, &err);//获取互斥信号量
//访问共享资源的关键段落
OSMutexPost(Sem);//释放互斥信号量
//code...
}
}
当同一类型共享资源有多个实体(如多台打印机)时,就允许多个任务同时使用这列共享资源,但每个任务所使用的共向实体是不同的,即对每个实体的使用仍然是互斥的。
与用于行为同步的计数信号量不同,用于资源同步的计数信号量的初始化值为共享资源的实体总数。在系统运行过程中,计数信号量的值动态反映了当前限制共享资源实体的个数。
消息邮箱与消息队列传递的是 (void *)型指针。
消息邮箱与消息队列所传递的是 (void *)型指针。如果此指针是某变量的地址,设
OSMboxPost(Mbox, (void *)Message) 或
OSQPost(MQ, (void *)Message)
则此变量Message必须在其内容未被改变之前通过OSMboxPend( )或OSQPend()获取。有两种情况下会在执行OSMboxPend()时此变量的内容不会被改变:
A. 在此变量的生存周期内执行OSMboxPend(),如上图所示的case1,2,3;
B. 在此变量的生存周期结束后立即执行OSMboxPend(),使程序在未改变其内容之前获取到消息指针,如case 4.1。注意,此方法并不可靠,如果程序还有异步中断,如果在执行OSMboxPend()或OSQPend()之前或之后执行了异步中断,则消息的内容将被改变,从而将得到错误的消息内容。
如果消息内容大小不大于(void *)大小,则可将消息内容冒充指针进行发送,即将消息内容强制转换为指针进行发送。此时则不需要考虑生存周期:
OSMboxPost(Mbox, (void*)Message);此时通过OSMboxPend()得到的即为消息内容。例:
程序清单L6-1
void fun1(void)
{
INT8U temp;
//...code get temp
OSMboxPost(Mbox, (void *)temp);
}
void fun2(void)
{
INT8U temp, err;
temp = (INT8U)(INT8U*)OSMboxPend(Mbox, 0, &err);
//....code
}
1)控制任务的执行周期
2)控制任务的运行节奏
3)状态查询
4)终止周期性任务
程序清单L6-1
void MyTask(void *pdata)
{
//....code 进行准备工作的代码
while(1)
{
//....code 任务实体代码
//....code 调用系统延时函数OSTimeDly()或OSTimeDlyHMSM()
}
}
程序清单L6-2
void MyTask(void *pdata)
{
//....code 进行准备工作的代码
while(1)
{
//....code 调用获取事件的函数
//....code 第一部分操作代码
//....code 调用系统延时函数(间隔1) OSTimeDly()或OSTimeDlyHMSM()
//....code 第二部分操作代码
//....code 调用系统延时函数(间隔2) OSTimeDly()或OSTimeDlyHMSM()
//....code 第三部分操作代码
}
}
查询过程是个无限循环的过程,只有当希望的状态出现以后才能退出这个无限循环,这种情况下在实时操作系统是不允许的,它将剥夺其他任务的运行机会,并影响系统的实时性。解决办法是“用定时查询代替连续查询”,即在查询过程中插入延时。
程序清单L6-3
void MyTask(void *pdata)
{
//....code 进行准备工作的代码
while(1)
{
//....code 任务实体代码
while(/*状态查询*/)
{
OSTimeDly(2);
}
}
}
如果某种状态必然在很短时间内(微妙数量级)之内发生,就不需插入延时函数了,因不会对系统产生较大影响。
调用超时限制的等待信号量(或消息),在没有按时获得信号量(或消息)时,其超时效果起到了控制运行周期的作用(与OSTimeDly()效果相同);当成功获取信号量(或消息)时,可以及时退出循环,终止周期性任务。
程序清单L6-4
void MyTask(void *pdata)
{
//....code 进行准备工作的代码
while(1)
{
//....code 任务实体代码
OSSemPend(Sem, 20, &err);
if(err == OS_ERR_NONE)
break;
}
OSTaskDel(OS_PRIO_SELF);
}
当采样对象是一个低频信号时,采用周期频率就可设置的比较低,即采样周期比系统节拍周期长的多,将采样周期设置为系统节拍周期的整数倍,就可以用系统提供的延时函数来控制采样周期。这时,采样功能就可以由一个独立的采样任务来完成,而不需要ISR配合。
当采样周期与系统节拍周期在同一个数量级时,如果任然用系统函数来控制采样周期,其采样周期的时间抖动将比较明显(相邻两次采样的时间间隔误差不能忽略),会严重影响采样结果的质量。这时,可以另外使用一个定时器,由定时中断产生稳定的采样周期。
当某种功能的运行周期与系统节拍周期相同时,使用系统节拍函数的钩子函数来完成此功能是非常有利的。
系统节拍钩子函数是系统节拍函数的一部分,具有ISR性质,应当尽可能简洁,不允许调用任何可能使自己挂起来的系统服务函数。
void OSTimeTickHook(void);//系统节拍钩子函数
由外部信号启动的的采样过程的方式称为被动采样。在被动采样中,启动采样的时刻是随机的,故没有采样周期的概念。
1)起始码:有一个或若干个包含特定内容的字节组成,表示一个通讯帧的开始。当通讯帧的总长度比较短、信道质量比较高(没有干扰)时,为了提高效率,可以不设置起始码。
2)地址码:在主从结构的多机组网通信系统中,用来指明分机号。当分机总数在255台之内时,地址码可以用一个字节表示。可以定义一个特殊的地址码作为广播地址,代表所有分机。在多主多从(无主从)结构的多机组网通信系统中,地址码包含收发双方的地址。如果仅仅是两台设备之间进行点对点的通信,则帧结构中就不需要包含地址码。
3)长度吗:在数据长度不固定的通信场合(变长帧)中,表示本帧数据内容的字节数。如果每次通信数据内容的长度为双方约定的固定值(定长帧),则帧结构中就不需要包含长度码。
4)数据段:通讯的实质内容。
5)校验码:由于通信信道存在干扰,为了判断接收的内容是否收到干扰,就需要在帧结构中加入校验码。可以采用简单的异或校验码,也可以像HEX文件那样采用算术加法校验,甚至采用检错能力极强的CRC校验。当接收方校验出错时,可请求发送方重新发送,直到接收正确为止。当信道干扰比较严重时,可能多次重发也得不到正确的内容,必须将全帧内容进行纠错编码,使接收方能够从收到干扰的数据中将正确内容还原回来,而不需要发送方重新发送。当通信环境非常好(如室内设备之间的通信)时,贞结构中可以不包含校验码。
在进行串行通信时,双方遵循相同的通信协议,由于波特率不变,故相邻两次串行口中断的间隔时间基本固定。有两种情况会使接受过程出现差错:
1)系统关中断的最长时间大于相邻两次中断的间隔时间,这时有可能遗漏一次中断,造成数据丢失。
2)有一个比串行口中断优先级更高的ISR,其运行时间比相邻两次串行接收中断的间隔时间长,同样可能遗漏一次中断,造成数据丢失。
把不能响应串行接收中断的时间称为串行接收中断的“死区”。解决问题的最低条件是死区时间不能比相邻两次串行中断的间隔时间长。这要求任务在访问比较耗时的共享资源时不要采用关中断方式(改用互斥信号量),ISR尽量简短,将可以剥离的工作转交给关联任务完成。
数据发送为主动行为,没有什么风险。当一帧数据需要分若干次发送时,由于实时操作系统的介入,串行发送任务不宜采用查询方式完成发送过程,以免浪费宝贵的CPU机时,最好由ISR和串行发送任务配合完成。
以下为转载内容:
在嵌入式应用中,使用RTOS的主要原因是为了提高系统的可靠性,其次是提高开发效率、缩短开发周期。uCOS-II是一个占先式实时多任务内核,使用对象是嵌入式系统,对源代码适当裁减,很容易移植到8~32位不同框架的微处理器上。但uCOS-II仅是一个实时内核,它不像其他实时操作系统(如嵌入式Linux)那样提供给用户一些API函数接口。在uCOS-II实时内核下,对外设的访问接口没有统一完善,有很多工作需要用户自己去完成。串口通信是单片机测控系统的重要组成部分,异步串行口是一个比较简单又很具代表性的中断驱动外设。本文以单片机中的串口为例,介绍uCOS—II下编写中断服务程序以及外设驱动程序的一般思路。
1 uCOS-II的中断处理及51系列单片机中断系统分析
uCOS-II中断服务程序(ISR)一般用汇编语言编写。以下是中断服务程序的步骤。
保存全部CPU寄存器;调用OSIntEnter()或OSIntNesting(全局变量)直接加1;
执行用户代码做中断服务;
调用OSIntExit();
恢复所有CPU寄存器;
执行中断返回指令。
uCOS-II提供两个ISR与内核接口函数;OSIntEnter()和OSIntExit()。OSIntEnter()通知uCOS-II核,中断服务程序开始了。事实上,此函数做的工作是把一个全局变量OSIntNesting加1,此中断嵌套计数器可以确保所有中断处理完成后再做任务调度。另一个接口函数OSIntExit()则通知内核,中断服务已结束。根据相应情况,退回被中断点(可能是一个任务或者是被嵌套的中断服务程序)或由内核作任务调度。
用户编写的ISR必须被安装到某一位置,以便中断发生后,CPU根据相应的中断号运行准确的服务程序。许多实时操作系统都提供了安装和卸载中断服务程序的API接口函数,但uCOS-II内核没有提供类似的接口函数,需要用户在对CPU的移植中自己实现。这些接口函数与具体的硬件环境有关,接下来以51单片机下的中断处理对此详细说明。
51单片机的中断基本过程如下:CPU在每个机器周期的S5P2时刻采样中断标志,而在下一指令周期将对采样的中断进行查询。如果有中断请求,则按照优先级高低的原则进行处理。响应中断时,先置相应的优先级激活触发器于相应位,封锁同级或低级中断,然后根据中断源类别,在硬件控制下,将中断地址压入堆栈,并转向相应的中断向量入口单元。通常在入口单元处放一跳转指令,转向执行中断服务程序.当执行中断返回指令RETI时,把响应中断时所置位的优先级激活触发器清零后,从堆栈中弹出被保护的断点地址,装入程序计数器PC,CPU返回原来被中断处继续执行程序。
在移植的过程中,采用Keil C51作为编译环境。Keil C5l集成C编译和汇编器。中断子程序用汇编语言编写,放到移植uCOS-II后的OS_CPU_A.ASM汇编文件中。下面是以串行口中断为例的移植中断服务子程序代码。
CSEGAT0023H ;串口中断响应入口地址
LJMPSerialISR;转移到串口中断子程序入口地址
RSEG?PR?SeriallSR?OS_CPU_A
SerialISR:
USINGO
CLR EA ;先关中断,以防中断嵌套
PUSHALL ;已定义的压栈宏,用于将
;CPU寄存器的值压入堆栈
LCALL_?OSIntEnter ;监视中断嵌套
LCALL_?Serial ;串口中断服务程序
LCALL_?OSintExlt
SETBEA
POPALL;已定义的出栈宏,将CPU寄存器的值出栈
RETI
2 串口驱动程序
笔者已在5l单片机上成功移植了uCOS-II内核,移植过程在此不再讨论。这里重点分析uC0S—II内核下串口驱动程序编写。
由于串行设备存在外设处理速度和CPU速度不匹配的问题,所以需要一个缓冲区.向串口发送数据时,只要把数据写到缓冲区中,然后由串口逐个取出往外发。从串口接收数据时,往往等收到若干个字节后才需要CPU进行处理,所以这些预收的数据可以先存于缓冲区中。实际上,单片机的异步串口中只有两个相互独立、地址相同的接收、发送缓冲寄存器SBUF。在实际应用中,需要从内存中开辟两个缓冲区,分别为接收缓冲区和发送缓冲区。这里把缓冲区定义为环形队列的数据结构。
uCOS-II内核提供了信号量作为通信和同步的机制,引入数据接收信号量、数据发送信号量分别对缓冲区两端的操作进行同步。串口的操作模式如下:用户任务想写,但缓冲区满时,在信号量上睡眠,让CPU运行别的任务,待ISR从缓冲区读走数据后唤醒此睡眠的任务;同样,用户任务想读,但缓冲区空时,也可以在信号量上睡眠,待外部设备有数据来了再唤醒。由于uCOS-II的信号量提供了超时等待机制,串口当然也具有超时读写能力。
图1是带缓冲区和信号量的串口接收示意图。数据接收信号量初始化为0,表示在环形缓冲区中无数据。
3 串口通信模块的设计
每个串行端口有两个环状队列缓冲区,同时有两个信号量:一个用来指示接收字节,另一个用来指示发送字节。每个环状缓冲区有以下四个要素:
存储数据(INT8U数组);
包含环状缓冲区字节数的计数器;
环状缓冲区中指向将被放置的下一字节的指针;
环状缓冲区中指向被取出的下一字节的指针。
图3是接收数据软件模块的流程图。SerialGetehar()用来获取接收到的数据,如果缓冲区已空时将任务挂起,接收到字节时,任务将被唤醒,同时从串行口接收字节。SerialPutRxChar()用来将接收的字节放到缓冲区中,如果接收缓冲区已满,则该字节被丢弃。当字节插入到缓冲区中,SerialPutRxChar()通知数据接收信号量,使之将数据己到的消息传达给所有等待的任务。为防止挂起应用任务,可以通过调用SceiallsEmPty()去发现环状队列中是否有字节。
图4是发送数据模块的流程图。当需要发送数据给串行端口时,SerialPurChar()等待信号量在初始化发送信号量时应该初始为缓冲区的大小。因此,当缓冲区中没有更多空间时,SerialPutChar()就挂起任务,只要UART再次发送字节,挂起任务就将恢复。SerialGctChar()被中断服务程序调用,如果发送缓冲区至少还有一个字节,Seria1GetChar()就返回一个从缓冲区发送的字节。如果缓冲区己空,则SerialGetChar()返回Null,这将使调用停止进一步的发送中断,一直到有数据发送为止。
4 异步串行通信的接口函数
应用任务可以通过如下的几个函数来控制和访问UART:SerialCfgPort()、SerialGetChar()、SerialInit()、SerialIsEmpty()、SerialIsFull()和SerialPutChar()。
SerialCfgPort()用于建立串行端口的特征,在为指定端口调用其他服务前,必须先调用该函数,包括确定波特率、比特数、奇偶校验和停止位等。
SerialGetChar()使应用程序从接收数据的环状缓冲区中取出数据。
SerialInit()用于初始化整个串口软件模块,且必须在该模块提供的其他任何服务前调用。SeriallInit()将环状缓冲区计数器的字节数清零,并初始化每个环状缓冲区的IN和OUT指针,指向数据存储区的开始处。数据接收信号量初始化为0,表示在环状缓冲区无数据。用传送缓冲区大小初始化数据传送信号量,表示缓冲区已空。
SerialIsEmpty()允许应用程序确定是否有字节从串口接收进来。本函数允许在无数据时避免将任务挂起。
SerialIsFull()允许应用程序确定传送环状缓冲区的状态,本函数可以在缓冲区已满时避免将任务挂起。
SerialPutChar()允许应用程序向一个串行端口发送数据。
结 语
该串口通信模块充分利用了实时内核的任务调度功能和信号量机制,系统软件模块化,可读性增强,便于修改和移植,其设计思路和方法可以很好的应用在多种情况下的测控系统中,系统的扩展方便,具有一定的借鉴作用。该串口通信模块已作为某铁路供水远程控制终端的一部分,运行稳定,提高了整个系统的运行效率和实时性。
显示任务是一个位于最下游的任务(通常也是用户任务中优先级较低的任务),它接收其他任务的操作指令,完成显示画面内容的更新。
如果要满足多个任务对画面更新的需求,可创建一个显示任务,他接受各个任务提出的要去,逐个完成画面内容的更新。为了同时为多个任务服务,就需要为显示任务配置一个消息队列,各个任务都可以将显示要求放到消息队列中。