先给自己打个广告,本人的微信公众号正式上线了,搜索:张笑生的地盘,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题,二维码如下:
一 相关概念
任务:嵌入式系统是为了实现某个功能(任务)而专门设计的一套系统,但是这个功能很复杂,我们需要将这些功能划分为一个个小的任务来实现每个小功能,这些小功能组合成整个系统的功能。我们这里的任务主要指实现这些小功能的任务。比如对于我们的网络视频监控系统,我们可以大致分为监控和网络通信两个部分,监控是实现实时检测的目的,网络通信实现将检测到的数据信息传递到服务器上。因此,我们可以划分为这样两个任务:任务1,实现监控任务;任务2,实现数据传递任务(当然这样的划分还是比较笼统,还是很很多细节没考虑到)。
任务控制块:TCB(task control block)
同步/通信:为实现各个任务之间的合作,在各个任务之间必须建立一种制约关系,这种制约性的合作运行机制叫做任务间的同步。这种制约关系需要解决2个问题:一是各任务之间需要有一种互斥关系,即对于某个共享资源,如果一个任务正在使用,另一个任务不能使用,必须等待;二是任务的执行存在先后顺序关系,一个任务需要等待另外一个任务的通知才能运行,或是建立了某个条件才能运行。
事件:实现任务间同步(通信)的方式,我们称之为事件, 实现一个任务向另一个任务发出信号;一个任务还可以等待另外一个任务或中断服务子程序给它发出信号,这里需要注意到是,只有任务可以等待事件的发生,中断服务子程序是不能这样做的。UCOS中的事件有信号量、邮箱(消息邮箱)和消息队列
事件控制块:ECB(event control block)
发送事件:发信方将事件发送到事件上
请求事件:接收方读取事件操作
二 分析
- 任务间为什么需要同步
随着我们的系统越来越复杂,实现的功能越来越多,不可避免会出现不同任务需要访问同一个资源,这个资源我们称为共享资源。但是在访问过程中,我们需要保证每个任务需要的资源是开始时它本身需要的,考虑如下的一种情况,该函数是一个会被两个任务同时调用
static u8 g_byVal;
int func(int a)
{
a = a + 1;
g_byVal = g_byVal + 1;
return (g_byVal * a);
}
分析,显然对于全局变量g_byVal,当某个任务执行完 g_byVal = g_byVal + 1时,由于系统调度,发生了一次任务调度,别的任务开始执行,也需要调用这个函数,这样就再次执行g_byVal = g_byVal + 1,因此全局变量的值就会被改变,当高优先级任务运行结束之后,再次返回原来的任务时候,就会导致g_byVal不是它开始的值,因此计算的结果就不是我们所期望的值。当然这只是一个例子,在实际项目代码中不一定是这么使用,主要是为了便于我们理解
2. 如何管理事件
作为功能完善的事件,应该有队这些等待任务具有两方面的管理功能,一是要对等待事件的所有任务进行记录并排序;二是应该允许等待有一个等待时限,即当任务认为等不及时可以退出对事件的请求。
ucos采用了和任务就绪表类似的位图方式来记录等待事件任务,即定义了一个u8类型的数组OSEventTbl[]作为等待事件任务的记录表,等待任务表以任务优先级为顺序为每个任务分配一个二进制位,并用该位为"1"来表示这一位对应的任务为事件的等待任务,否则不是等待任务。
同样,为了加快对该表的访问速度,也定义了一个u8类型的变量OSEventGrp来表示等待任务表中的任务组,等待任务表OSEventTbl[]与变量OSEventGrp的示意图如下:
ucos中使用事件控制块数据结构来描述事件,数据结构如下:
typedef struct os_event {
INT8U OSEventType; /* 事件类型 */
void *OSEventPtr; /* 消息或者消息队列指针 */
INT16U OSEventCnt; /* 信号量计数器 */
OS_PRIO OSEventGrp; /* 等待事件任务组 */
OS_PRIO OSEventTbl[OS_EVENT_TBL_SIZE]; /* 任务等待表*/
#if OS_EVENT_NAME_EN > 0u
INT8U *OSEventName;
#endif
} OS_EVENT;
该结构体示意图如下:
三 任务间通信同步—信号量
A. 信号量的基本概念
信号量设置的最初目的:是为了给共享资源设立一个标志,该标志表示该共享资源被占用情况。当一个任务在访问共享区域之前,需要先查询这个信号量,以了解共享资源被占用的情况,来决定自己是否访问这段共享区域
考虑我们日常生活中的一个示例:
- 母子二人围在一起吃水果,母亲削苹果网盘里放,孩子则从盘里去苹果吃,盘里最多放N个苹果,用信号量方式表示应该如下:
假设S=N,S1=0
母
while(1)
{
削苹果
wait(s)
放苹果
signal(s1)
}
孩子
while(1)
{
wait(s1)
吃苹果
signal(s)
}
- 公用电话亭:假设有一个公用电话亭,外面有一个红绿灯,红灯亮表示里面有人正在用,外面的人必须等待才能用;绿灯亮,表示里面没有人在用,外面的人可以使用
B. 信号量的使用
当事件控制块描述一个信号量时,事件控制成员OSEventType的值为OS_EVENT_TYPE_SEM,信号量事件使用控制块中的OSEventCnt作为计数器,用数组OSEventTbl[]作为等待任务表。
示例:计数器当前值为3,且有4个等待任务,等待信号量任务优先级分别是4,7,10,19
使用信号量的步骤: - 创建信号量
当OSEventType= OS_EVENT_TYPE_SEM时,即创建了一个信号量,使用API:OSSemCreate()来实现,如:
test = OSSemCreate(10); //创建一个计数器为10的信号量;
创建完成之后,事件列表数组如下:
源码如下:
OS_EVENT *OSSemCreate (INT16U cnt)
{
OS_EVENT *pevent;
#if OS_CRITICAL_METHOD == 3u /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0u;
#endif
#ifdef OS_SAFETY_CRITICAL_IEC61508
if (OSSafetyCriticalStartFlag == OS_TRUE) {
OS_SAFETY_CRITICAL_EXCEPTION();
return ((OS_EVENT *)0);
}
#endif
if (OSIntNesting > 0u) { /* See if called from ISR ... */
return ((OS_EVENT *)0); /* ... can't CREATE from an ISR */
}
OS_ENTER_CRITICAL();
pevent = OSEventFreeList; /* Get next free event control block */
if (OSEventFreeList != (OS_EVENT *)0) { /* See if pool of free ECB pool was empty */
OSEventFreeList = (OS_EVENT *)OSEventFreeList->OSEventPtr;
}
OS_EXIT_CRITICAL();
if (pevent != (OS_EVENT *)0) { /* Get an event control block */
pevent->OSEventType = OS_EVENT_TYPE_SEM;
pevent->OSEventCnt = cnt; /* Set semaphore value */
pevent->OSEventPtr = (void *)0; /* Unlink from ECB free list */
#if OS_EVENT_NAME_EN > 0u
pevent->OSEventName = (INT8U *)(void *)"?";
#endif
OS_EventWaitListInit(pevent); /* Initialize to 'nobody waiting' on sem. */
}
return (pevent);
}
根据宏开关,简写代码如下:
OS_EVENT *OSSemCreate (INT16U cnt)
{
OS_EVENT *pevent;
if (OSIntNesting > 0u) { /* 中断服务程序中,不能创建信号量 */
return ((OS_EVENT *)0);
}
OS_ENTER_CRITICAL();
pevent = OSEventFreeList; /* 占领一个空事件列表 */
if (OSEventFreeList != (OS_EVENT *)0) { /* 让空事件列表指向下一个事件链表 */
OSEventFreeList = (OS_EVENT *)OSEventFreeList->OSEventPtr;
}
OS_EXIT_CRITICAL();
if (pevent != (OS_EVENT *)0) { /* 非控事件表才能创建新事件 */
pevent->OSEventType = OS_EVENT_TYPE_SEM;
pevent->OSEventCnt = cnt; /* 设置信号量计数器 */
pevent->OSEventPtr = (void *)0; /* 初始化事件指针 */
OS_EventWaitListInit(pevent); /* 初始化信号量等待事件列表 */
}
return (pevent);
}
- 请求信号量
用OSSemPend()来实现
OSSemPend(test,0,&err1); //0表示请求等待时间无限长
当任务需要访问一个共享资源时,先要请求管理该资源的信号量,这样就可以根据信号量是否有效来决定该任务是否可以继续运行。
比如对于任务A和任务B,都需要调用同一个函数(共享资源C),那么当每个任务分别可以调用这个函数之前,就需要先请求信号量,确认可否调用
- 发送信号量
用OSSemPost()来实现
OSSemPost(test);
- 信号量使用实例
有一个func函数、一个My_task任务,一个Your_Task任务,他们都需要调用func函数,但是不能同时调用
OS_EVENT *test;
INT32U err1 = 0;
int main(void)
{
brd_init();
OSInit();
PRINTF("Hello,World!\n\r");
test = OSSemCreate(1); //创建信号量
OSTaskCreate(My_Task,NULL,
&my_task[TASK1_STK_SIZE-1], MY_TASK_PRIO);
OSTaskCreate(Your_Task,NULL,
&you_task[TASK1_STK_SIZE-1], YOUR_TASK_PRIO);
OSStart();
return 0;
}
/* 函数中增加了信号量访问操作,显然这个函数不能被两个不同的任务同时调用 */
void Func(char *str)
{
OSSemPend(test,0,&err1);
PRINTF("This is %s working-2018-07-01!\n\r", str);
OSSemPost(test);
}
/*
* My_Task和Your_Task用于调试信号量
*/
static void My_Task(void)
{
char *my_s = "my task";
while(1)
{
Func(my_s);
OSTimeDlyHMSM(0,0,1,0);
}
}
static void Your_Task(void)
{
char *you_s = "you task";
while(1)
{
Func(you_s);
OSTimeDlyHMSM(0,0,2,0);
}
}