写在前面:
由于时间的不足与学习的碎片化,写博客变得有些奢侈。
但是对于记录学习(忘了以后能快速复习)的渴望一天天变得强烈。
既然如此
不如以天为单位,以时间为顺序,仅仅将博客当做一个知识学习的目录,记录笔者认为最通俗、最有帮助的资料,并尽量总结几句话指明本质,以便于日后搜索起来更加容易。
标题的结构如下:“类型”:“知识点”——“简短的解释”
部分内容由于保密协议无法上传。
点击此处进入学习日记的总目录
2024.04.03:UCOSIII第三十一节:信号量函数接口讲解
- 四十五、UCOSIII:信号量函数接口讲解
- 1、创建信号量函数OSSemCreate()
- 2、信号量删除函数OSSemDel()
- 3、信号量释放函数OSSemPost()
- 1. OSSemPost()
- 2. OS_SemPost()源码
- 4、信号量获取函数OSSemPend()
- 5、使用信号量的注意事项
四十五、UCOSIII:信号量函数接口讲解
1、创建信号量函数OSSemCreate()
在定义完信号量结构体变量后就可以调用 OSSemCreate()函数创建一个信号量,跟消息队列的创建差不多。
其实这里的“创建信号量”指的就是对内核对象(信号量)的一些初始化。
要特别注意的是内核对象使用之前一定要先创建, 这个创建过程必须要保证在所有可能使用内核对象的任务之前,所以一般我们都是在创建任务之前就创建好系统需要的内核对象(如信号量等)。
创建信号量函数OSSemCreate()源码具体如下
void OSSemCreate (OS_SEM *p_sem, //(1) //信号量控制块指针
CPU_CHAR *p_name, //(2) //信号量名称
OS_SEM_CTR cnt, //(3) //资源数目或事件是否发生标志
OS_ERR *p_err) //(4) //返回错误类型
{
CPU_SR_ALLOC();
//使用到临界段(在关/开中断时)时必须用到该宏,该宏声明和定义一个局部变
//量,用于保存关中断前的 CPU 状态寄存器 SR(临界段关中断只需保存SR)
//,开中断时将该值还原。
#ifdef OS_SAFETY_CRITICAL//(5)//如果启用(默认禁用)了安全检测
if (p_err == (OS_ERR *)0) //如果错误类型实参为空
{
OS_SAFETY_CRITICAL_EXCEPTION(); //执行安全检测异常函数
return; //返回,不继续执行
}
#endif
#ifdef OS_SAFETY_CRITICAL_IEC61508//(6)//如果启用(默认禁用)了安全关键
//如果是在调用OSSafetyCriticalStart()后创建该信号量
if (OSSafetyCriticalStartFlag == DEF_TRUE)
{
*p_err = OS_ERR_ILLEGAL_CREATE_RUN_TIME; //错误类型为“非法创建内核对象”
return; //返回,不继续执行
}
#endif
#if OS_CFG_CALLED_FROM_ISR_CHK_EN > 0u //(7)
//如果启用(默认启用)了中断中非法调用检测
if (OSIntNestingCtr > (OS_NESTING_CTR)0) //如果该函数是在中断中被调用
{
*p_err = OS_ERR_CREATE_ISR; //错误类型为“在中断函数中创建对象”
return; //返回,不继续执行
}
#endif
#if OS_CFG_ARG_CHK_EN > 0u//(8)//如果启用(默认启用)了参数检测
if (p_sem == (OS_SEM *)0) //如果参数 p_sem 为空
{
*p_err = OS_ERR_OBJ_PTR_NULL; //错误类型为“信号量对象为空”
return; //返回,不继续执行
}
#endif
OS_CRITICAL_ENTER(); //进入临界段
p_sem->Type = OS_OBJ_TYPE_SEM; //(9) //初始化信号量指标
p_sem->Ctr = cnt;
p_sem->TS = (CPU_TS)0;
p_sem->NamePtr = p_name;
OS_PendListInit(&p_sem->PendList); //(10)//初始化该信号量的等待列表
#if OS_CFG_DBG_EN > 0u//如果启用(默认启用)了调试代码和变量
OS_SemDbgListAdd(p_sem); //将该定时添加到信号量双向调试链表
#endif
OSSemQty++; //(11)//信号量个数加1
OS_CRITICAL_EXIT_NO_SCHED(); //退出临界段(无调度)
*p_err = OS_ERR_NONE; //错误类型为“无错误”
}
- (1):信号量控制块指针,指向我们定义的信号量控制块结构体变量, 所以在创建之前我们需要先定义一个信号量控制块变量。
- (2):信号量名称,字符串形式。
- (3):这个值表示初始化时候资源的个数或事件是否发生标志, 一般信号量是二值信号量的时候,这个值一般为0或者为1,而如果信号量作为计数信号量的时候,这个值一般定义为初始资源的个数。
- (4):用于保存返回错误类型。
- (5):如果启用了安全检测(默认禁用),在编译时则会包含安全检测相关的代码, 如果错误类型实参为空,系统会执行安全检测异常函数,然后返回,不执行创建信号量操作。
- (6):如果启用(默认禁用)了安全关键检测,在编译时则会包含安全关键检测相关的代码, 如果是在调用OSSafetyCriticalStart()后创建该信号量,则是非法的,返回错误类型为“非法创建内核对象”错误代码,并且退出,不执行创建信号量操作。
- (7):如果启用了中断中非法调用检测(默认启用), 在编译时则会包含中断非法调用检测相关的代码,如果该函数是在中断中被调用,则是非法的,返回错误类型为“在中断中创建对象”的错误代码, 并且退出,不执行创建信号量操作。
- (8):如果启用了参数检测(默认启用), 在编译时则会包含参数检测相关的代码,如果p_sem参数为空,返回错误类型为“创建对象为空”的错误代码,并且退出,不执行创建信号量操作。
- (9):进入临界段,然后进行初始化信号量相关信息, 如初始化信号量的类型、名字、可用信号量Ctr、记录时间戳的变量TS等。
- (10):调用OS_PendListInit()函数初始化该信号量的等待列表。
- (11):系统信号量个数加1。
如果我们创建一个初始可用信号量个数为5的信号量,那么信号量创建成功的示意图具体见图
创建信号量函数OSSemCreate()的使用实例具体如下:
OS_SEM SemOfKey; //标志KEY1是否被按下的信号量
/* 创建信号量 SemOfKey */
OSSemCreate((OS_SEM *)&SemOfKey, //指向信号量变量的指针
(CPU_CHAR *)"SemOfKey", //信号量的名字
(OS_SEM_CTR )0,
//信号量这里是指示事件发生,所以赋值为0,表示事件还没有发生
(OS_ERR *)&err); //错误类型
2、信号量删除函数OSSemDel()
OSSemDel()用于删除一个信号量,信号量删除函数是根据信号量结构(信号量句柄)直接删除的,删除之后这个信号量的所有信息都会被系统清空, 而且不能再次使用这个信号量了。
但是需要注意的是,如果某个信号量没有被定义,那也是无法被删除的,如果有任务阻塞在该信号量上, 那么尽量不要删除该信号量。
想要使用互斥量删除函数就必须将OS_CFG_SEM_DEL_EN宏定义配置为1,其函数源码具体如下
#if OS_CFG_SEM_DEL_EN > 0u //如果启用了 OSSemDel() 函数
OS_OBJ_QTY OSSemDel (OS_SEM *p_sem, //(1) //信号量指针
OS_OPT opt, //(2) //选项
OS_ERR *p_err) //(3) //返回错误类型
{
OS_OBJ_QTY cnt;
OS_OBJ_QTY nbr_tasks;
OS_PEND_DATA *p_pend_data;
OS_PEND_LIST *p_pend_list;
OS_TCB *p_tcb;
CPU_TS ts;
CPU_SR_ALLOC();
#ifdef OS_SAFETY_CRITICAL//(4)//如果启用(默认禁用)了安全检测
if (p_err == (OS_ERR *)0) //如果错误类型实参为空
{
OS_SAFETY_CRITICAL_EXCEPTION(); //执行安全检测异常函数
return ((OS_OBJ_QTY)0); //返回0(有错误),不继续执行
}
#endif
#if OS_CFG_CALLED_FROM_ISR_CHK_EN > 0u//(5)//如果启用了中断中非法调用检测
if (OSIntNestingCtr > (OS_NESTING_CTR)0) //如果该函数在中断中被调用
{
*p_err = OS_ERR_DEL_ISR; //返回错误类型为“在中断中删除”
return ((OS_OBJ_QTY)0); //返回0(有错误),不继续执行
}
#endif
#if OS_CFG_ARG_CHK_EN > 0u//(6)//如果启用了参数检测
if (p_sem == (OS_SEM *)0) //如果 p_sem 为空
{
*p_err = OS_ERR_OBJ_PTR_NULL; //返回错误类型为“内核对象为空”
return ((OS_OBJ_QTY)0); //返回0(有错误),不继续执行
}
switch (opt) //(7)//根据选项分类处理
{
case OS_OPT_DEL_NO_PEND: //如果选项在预期之内
case OS_OPT_DEL_ALWAYS:
break; //直接跳出
default: //(8)//如果选项超出预期
*p_err = OS_ERR_OPT_INVALID; //返回错误类型为“选项非法”
return ((OS_OBJ_QTY)0); //返回0(有错误),不继续执行
}
#endif
#if OS_CFG_OBJ_TYPE_CHK_EN > 0u//(9)//如果启用了对象类型检测
if (p_sem->Type != OS_OBJ_TYPE_SEM) //如果 p_sem 不是信号量类型
{
*p_err = OS_ERR_OBJ_TYPE; //返回错误类型为“内核对象类型错误”
return ((OS_OBJ_QTY)0); //返回0(有错误),不继续执行
}
#endif
CPU_CRITICAL_ENTER(); //关中断
p_pend_list = &p_sem->PendList; //(10)//获取信号量的等待列表到p_pend_list
cnt = p_pend_list->NbrEntries; //获取等待该信号量的任务数
nbr_tasks = cnt;
switch (opt) //(11)//根据选项分类处理
{
case OS_OPT_DEL_NO_PEND: //(12)
//如果只在没有任务等待的情况下删除信号量
if (nbr_tasks == (OS_OBJ_QTY)0) //如果没有任务在等待该信号量
{
#if OS_CFG_DBG_EN > 0u//如果启用了调试代码和变量
OS_SemDbgListRemove(p_sem); //将该信号量从信号量调试列表移除
#endif
OSSemQty--; //(13)//信号量数目减1
OS_SemClr(p_sem); //(14)//清除信号量内容
CPU_CRITICAL_EXIT(); //开中断
*p_err = OS_ERR_NONE; //(15)//返回错误类型为“无错误”
}
else//(16)//如果有任务在等待该信号量
{
CPU_CRITICAL_EXIT(); //开中断
*p_err = OS_ERR_TASK_WAITING;
//返回错误类型为“有任务在等待该信号量”
}
break;
case OS_OPT_DEL_ALWAYS: //(17)//如果必须删除信号量
OS_CRITICAL_ENTER_CPU_EXIT(); //锁调度器,并开中断
ts = OS_TS_GET(); //(18)//获取时间戳
while (cnt > 0u) //(19)
//逐个移除该信号量等待列表中的任务
{
p_pend_data = p_pend_list->HeadPtr;
p_tcb = p_pend_data->TCBPtr;
OS_PendObjDel((OS_PEND_OBJ *)((void *)p_sem),
p_tcb,
ts); //(20)
cnt--;
}
#if OS_CFG_DBG_EN > 0u//如果启用了调试代码和变量
OS_SemDbgListRemove(p_sem);
//将该信号量从信号量调试列表移除
#endif
OSSemQty--; //(21)//信号量数目减1
OS_SemClr(p_sem); //(22)//清除信号量内容
OS_CRITICAL_EXIT_NO_SCHED(); //减锁调度器,但不进行调度
OSSched(); //(23)
//任务调度,执行最高优先级的就绪任务
*p_err = OS_ERR_NONE; //返回错误类型为“无错误”
break;
default: //(24)//如果选项超出预期
CPU_CRITICAL_EXIT(); //开中断
*p_err = OS_ERR_OPT_INVALID; //返回错误类型为“选项非法”
break;
}
return ((OS_OBJ_QTY)nbr_tasks); //(25)
//返回删除信号量前等待其的任务数
}
#endif
- (1):信号量控制块指针,指向我们定义的信号量控制块结构体变量, 所以在删除之前我们需要先定义一个信号量控制块变量,并且成功创建信号量后再进行删除操作。
- (2):删除的选项。
- (3):用于保存返回的错误类型。
- (4):如果启用了安全检测(默认),在编译时则会包含安全检测相关的代码, 如果错误类型实参为空,系统会执行安全检测异常函数,然后返回,不执行删除信号量操作。
- (5):如果启用了中断中非法调用检测(默认启用), 在编译时则会包含中断非法调用检测相关的代码,如果该函数是在中断中被调用,则是非法的, 返回错误类型为“在中断中删除对象”的错误代码,并且退出,不执行删除信号量操作。
- (6):如果启用了参数检测(默认启用),在编译时则会包含参数检测相关的代码, 如果p_sem参数为空,返回错误类型为“内核对象为空”的错误代码,并且退出,不执行删除信号量操作。
- (7):判断opt选项是否合理,该选项有两个, OS_OPT_DEL_ALWAYS与OS_OPT_DEL_NO_PEND,在os.h文件中定义。此处是判断一下选项是否在预期之内,如果在则跳出switch语句。
- (8):如果选项超出预期,则返回错误类型为“选项非法”的错误代码,退出,不继续执行。
- (9):如果启用了对象类型检测,在编译时则会包含对象类型检测相关的代码, 如果p_sem不是信号量类型,返回错误类型为“内核对象类型错误”的错误代码,并且退出,不执行删除信号量操作。
- (10):程序执行到这里,表示可以删除信号量了, 系统首先获取信号量的等待列表保存到p_pend_list变量中。然后再获取等待该信号量的任务数。
- (11):根据选项分类处理
- (12):如果opt是OS_OPT_DEL_NO_PEND,则表示只在没有任务等待的情况下删除信号量, 如果当前系统中有任务阻塞在该信号量上,则不能删除,反之,则可以删除信号量。
- (13):如果没有任务在等待该信号量,信号量数目减1。
- (14):清除信号量内容。
- (15):删除成功,返回错误类型为“无错误”的错误代码。
- (16):如果有任务在等待该信号量,则返回错误类型为“有任务在等待该信号量”错误代码。
- (17):如果opt是OS_OPT_DEL_ALWAYS,则表示无论如何都必须删除信号量 ,那么在删除之前,系统会把所有阻塞在该信号量上的任务恢复。
- (18):获取时间戳,记录一下删除的时间。
- (19):根据前面cnt记录阻塞在该信号量上的任务个数,逐个移除该信号量等待列表中的任务。
- (20):调用OS_PendObjDel()函数将阻塞在内核对象(如信号量)上的任务从阻塞态恢复, 此时系统在删除内核对象,删除之后,这些等待事件的任务需要被恢复,其源码具体见 代码清单:消息队列-8。
- (21):执行到这里,表示已经删除了信号量了,系统信号量个数减1。
- (22):清除信号量内容。
- (23):进行一次任务调度。
- (24):如果选项超出预期,返回错误类型为“选项非法”的错误代码,退出。
- (25):返回删除信号量前阻塞在该信号量上的任务个数。
信号量删除函数OSSemDel()的使用也是很简单的,只需要传入要删除的信号量的句柄与选项还有保存返回的错误类型即可,调用函数时, 系统将删除这个信号量。
需要注意的是在调用删除信号量函数前,系统应存在已创建的信号量。
如果删除信号量时,系统中有任务正在等待该信号量, 则不应该进行删除操作,因为删除之后的信号量就不可用了。
删除信号量函数OSSemDel()的使用实例具体如下:
OS_SEM SemOfKey;; //声明信号量
OS_ERR err;
/* 删除信号量sem*/
OSSemDel ((OS_SEM *)&SemOfKey, //指向信号量的指针
OS_OPT_DEL_NO_PEND,
(OS_ERR *)&err); //返回错误类型
3、信号量释放函数OSSemPost()
与消息队列的操作一样,信号量的释放可以在任务、中断中使用。
在前面的讲解中,我们知道当信号量有效的时候,任务才能获取信号量,那么,是什么函数使得信号量变得有效?
其实有两个方式:
- 一个是在创建的时候进行初始化,将它可用的信号量个数设置一个初始值;
如果该信号量用作二值信号量,那么我们在创建信号量的时候其初始值的范围是0~1, 假如初始值为1个可用的信号量的话,被获取一次就变得无效了。 - 另一个就是释放信号量,μC/OS提供了信号量释放函数, 每调用一次该函数就释放一个信号量。
但是有个问题,能不能一直释放?
很显然如果用作二值信号量的话,一直释放信号量就达不到同步或者互斥访问的效果。
虽然μC/OS的信号量是允许一直释放的,但是,信号量的范围还需我们用户自己根据需求进行决定,当用作二值信号量的时候, 必须确保其可用值在0~1范围内;
而用作计数信号量的话,其范围是由用户根据实际情况来决定的,在写代码的时候,我们要注意代码的严谨性罢了
1. OSSemPost()
信号量释放函数的源码具体如下:
OS_SEM_CTR OSSemPost (OS_SEM *p_sem, //(1) //信号量控制块指针
OS_OPT opt, //(2) //选项
OS_ERR *p_err) //(3) //返回错误类型
{
OS_SEM_CTR ctr;
CPU_TS ts;
#ifdef OS_SAFETY_CRITICAL//如果启用(默认禁用)了安全检测
if (p_err == (OS_ERR *)0) //如果错误类型实参为空
{
OS_SAFETY_CRITICAL_EXCEPTION(); //执行安全检测异常函数
return ((OS_SEM_CTR)0); //返回0(有错误),不继续执行
}
#endif
#if OS_CFG_ARG_CHK_EN > 0u//如果启用(默认启用)了参数检测功能
if (p_sem == (OS_SEM *)0) //如果 p_sem 为空
{
*p_err = OS_ERR_OBJ_PTR_NULL; //返回错误类型为“内核对象指针为空”
return ((OS_SEM_CTR)0); //返回0(有错误),不继续执行
}
switch (opt) //根据选项情况分类处理
{
case OS_OPT_POST_1: //如果选项在预期内,不处理
case OS_OPT_POST_ALL:
case OS_OPT_POST_1 | OS_OPT_POST_NO_SCHED:
case OS_OPT_POST_ALL | OS_OPT_POST_NO_SCHED:
break;
default: //如果选项超出预期
*p_err = OS_ERR_OPT_INVALID; //返回错误类型为“选项非法”
return ((OS_SEM_CTR)0u); //返回0(有错误),不继续执行
}
#endif
#if OS_CFG_OBJ_TYPE_CHK_EN > 0u//如果启用了对象类型检测
if (p_sem->Type != OS_OBJ_TYPE_SEM) //如果 p_sem 的类型不是信号量类型
{
*p_err = OS_ERR_OBJ_TYPE; //返回错误类型为“对象类型错误”
return ((OS_SEM_CTR)0); //返回0(有错误),不继续执行
}
#endif
ts = OS_TS_GET(); //获取时间戳
#if OS_CFG_ISR_POST_DEFERRED_EN > 0u//如果启用了中断延迟发布
if (OSIntNestingCtr > (OS_NESTING_CTR)0) //如果该函数是在中断中被调用
{
OS_IntQPost((OS_OBJ_TYPE)OS_OBJ_TYPE_SEM,//将该信号量发布到中断消息队列
(void *)p_sem,
(void *)0,
(OS_MSG_SIZE)0,
(OS_FLAGS )0,
(OS_OPT )opt,
(CPU_TS )ts,
(OS_ERR *)p_err); //(4)
return ((OS_SEM_CTR)0); //返回0(尚未发布),不继续执行
}
#endif
ctr = OS_SemPost(p_sem, //将信号量按照普通方式处理
opt,
ts,
p_err); //(5)
return (ctr); //返回信号的当前计数值
}
- (1):信号量控制块指针。
- (2):释放信号量的选项,该选项在os.h中定义,具体见
#define OS_OPT_POST_FIFO (OS_OPT)(0x0000u)
//默认采用FIFO方式发布信号量
#define OS_OPT_POST_LIFO (OS_OPT)(0x0010u)
//μC/OS也支持采用FIFO方式发布信号量。
#define OS_OPT_POST_1 (OS_OPT)(0x0000u)
//发布给一个任务。
#define OS_OPT_POST_ALL (OS_OPT)(0x0200u)
//发布给所有等待的任务,也叫广播信号量。
- (3):用于保存返回错误类型。
- (4):如果启用了中断延迟发布,并且该函数在中断中被调用, 则使用OS_IntQPost()函数将信号量发布到中断消息队列中,OS_IntQPost()函数源码我们在前面已经将近过,就不再重复赘述。
- (5):将信号量按照普通方式处理
2. OS_SemPost()源码
OS_SEM_CTR OS_SemPost (OS_SEM *p_sem, //信号量指针
OS_OPT opt, //选项
CPU_TS ts, //时间戳
OS_ERR *p_err) //返回错误类型
{
OS_OBJ_QTY cnt;
OS_SEM_CTR ctr;
OS_PEND_LIST *p_pend_list;
OS_PEND_DATA *p_pend_data;
OS_PEND_DATA *p_pend_data_next;
OS_TCB *p_tcb;
CPU_SR_ALLOC();
CPU_CRITICAL_ENTER(); //关中断
p_pend_list = &p_sem->PendList; //(1)//取出该信号量的等待列表
//如果没有任务在等待该信号量
if (p_pend_list->NbrEntries == (OS_OBJ_QTY)0) //(2)
{
//判断是否将导致该信号量计数值溢出,
switch (sizeof(OS_SEM_CTR)) //(3)
{
case 1u: //(4)
//如果溢出,则开中断,返回错误类型为
if (p_sem->Ctr == DEF_INT_08U_MAX_VAL)
//“计数值溢出”,返回0(有错误),
{
CPU_CRITICAL_EXIT(); //不继续执行。
*p_err = OS_ERR_SEM_OVF;
return ((OS_SEM_CTR)0);
}
break;
case 2u:
if (p_sem->Ctr == DEF_INT_16U_MAX_VAL)
{
CPU_CRITICAL_EXIT();
*p_err = OS_ERR_SEM_OVF;
return ((OS_SEM_CTR)0);
}
break;
case 4u:
if (p_sem->Ctr == DEF_INT_32U_MAX_VAL)
{
CPU_CRITICAL_EXIT();
*p_err = OS_ERR_SEM_OVF;
return ((OS_SEM_CTR)0);
}
break;
default:
break;
}
p_sem->Ctr++; //(5)//信号量计数值不溢出则加1
ctr = p_sem->Ctr; //获取信号量计数值到 ctr
p_sem->TS = ts; //(6)//保存时间戳
CPU_CRITICAL_EXIT(); //则开中断
*p_err = OS_ERR_NONE; //返回错误类型为“无错误”
return (ctr); //(7)
//返回信号量的计数值,不继续执行
}
OS_CRITICAL_ENTER_CPU_EXIT(); //(8)//加锁调度器,但开中断
if ((opt & OS_OPT_POST_ALL) != (OS_OPT)0)
//如果要将信号量发布给所有等待任务
{
cnt = p_pend_list->NbrEntries; //(9)//获取等待任务数目到 cnt
}
else
//如果要将信号量发布给优先级最高的等待任务
{
cnt = (OS_OBJ_QTY)1; //(10)//将要操作的任务数为1,cnt置1
}
p_pend_data = p_pend_list->HeadPtr; //获取等待列表的首个任务到p_pend_data
while (cnt > 0u) //(11)//逐个处理要发布的任务
{
p_tcb = p_pend_data->TCBPtr; //取出当前任务
p_pend_data_next = p_pend_data->NextPtr; //取出下一个任务
OS_Post((OS_PEND_OBJ *)((void *)p_sem), //发布信号量给当前任务
p_tcb,
(void *)0,
(OS_MSG_SIZE)0,
ts); //(12)
p_pend_data = p_pend_data_next; //处理下一个任务
cnt--; //(13)
}
ctr = p_sem->Ctr; //获取信号量计数值到 ctr
OS_CRITICAL_EXIT_NO_SCHED(); //(14)
//减锁调度器,但不执行任务调度
//如果 opt没选择“发布时不调度任务”
if ((opt & OS_OPT_POST_NO_SCHED) == (OS_OPT)0)
{
OSSched(); //(15)//任务调度
}
*p_err = OS_ERR_NONE; //(16)//返回错误类型为“无错误”
return (ctr); //返回信号量的当前计数值
}
- (1):取出该信号量的等待列表保存在p_pend_list变量中。
- (2):判断一下有没有任务在等待该信号量, 如果没有任务在等待该信号量,则要先看看信号量的信号量计数值是否即将溢出。
- (3):怎么判断计数值是否溢出呢?μC/OS支持多个数据类型的信号量计数值, 可以是8位的,16位的,32位的,具体是多少位是由我们自己定义的。
- (4):先看看OS_SEM_CTR的大小是个字节,如果是1个字节,表示Ctr计数值是8位的, 则判断一下Ctr是否到达了DEF_INT_08U_MAX_VAL,如果到达了,再释放信号量就会溢出,那么就会返回错误类型为“计数值溢出”的错误代码。 而对于OS_SEM_CTR是2字节、4字节的也是同样的判断操作。
- (5):程序能执行到这里,说明信号量的计数值不溢出,此时释放信号量则需要将Ctr加1。
- (6):保存释放信号量时的时间戳。
- (7):返回错误类型为“无错误”的错误代码,然后返回信号量的计数值,不继续执行。
- (8):而程序能执行到这里,说明系统中有任务阻塞在该信号量上, 此时我们释放了一个信号量,就要将等待的任务进行恢复,但是恢复一个还是恢复所有任务得看用户自定义的释放信号量选项。 所以此时不管三七二十一将调度器锁定,但开中断,因为接下来的操作是需要操作任务与信号量的列表,系统不希望其他任务来打扰。
- (9):如果要将信号量释放给所有等待任务, 首先获取等待该信号量的任务个数到变量cnt中,用来记录即将要进行释放信号量操作的任务个数。
- (10):如果要将信号量释放给优先级最高的等待任务,将要操作的任务数为1,所以将cnt置1。
- (11):逐个处理要释放信号量的任务。
- (12):调用OS_Post()函数进行对应的任务释放信号量, 因为该源码在前面就讲解过了,此处就不再重复赘述,具体见 代码清单:消息队列-14。
- (13):处理下一个任务。
- (14):减锁调度器,但不执行任务调度.
- (15):如果 opt 没选择“发布时不调度任务”,那么就进行任务调度。
- (16):操作完成,返回错误类型为“无错误”的错误代码,并且返回信号量的当前计数值。
如果可用信号量未满,信号量控制块结构体成员变量Ctr就会加1,然后判断是否有阻塞的任务。
如果有的话就会恢复阻塞的任务,然后返回成功信息。
用户可以选择只释放(发布)给一个任务或者是释放(发布)给所有在等待信号量的任务(广播信号量), 并且用户可以选择在释放(发布)完成的时候要不要进行任务调度。
如果信号量在中断中释放,用户可以选择是否需要延迟释放(发布)。
信号量的释放函数的使用很简单,具体如下:
OS_SEM SemOfKey; //标志KEY1是否被按下的信号量
OSSemPost((OS_SEM *)&SemOfKey, //发布SemOfKey
(OS_OPT )OS_OPT_POST_ALL, //发布给所有等待任务
(OS_ERR *)&err); //返回错误类型
4、信号量获取函数OSSemPend()
与消息队列的操作一样,信号量的获取可以在任务中使用。
与释放信号量对应的是获取信号量,我们知道,当信号量有效的时候,任务才能获取信号量。
当任务获取了某个信号量的时候, 该信号量的可用个数就减一,当它减到0的时候,任务就无法再获取了,并且获取的任务会进入阻塞态(假如用户指定了阻塞超时时间的话)。
如果某个信号量中当前拥有1个可用的信号量的话,被获取一次就变得无效了,那么此时另外一个任务获取该信号量的时候,就会无法获取成功, 该任务便会进入阻塞态,阻塞时间由用户指定。
μC/OS支持系统中多个任务获取同一个信号量,假如信号量中已有多个任务在等待,那么这些任务会按照优先级顺序进行排列。
如果信号量在释放的时候选择只释放给一个任务,那么在所有等待任务中最高优先级的任务优先获得信号量, 而如果信号量在释放的时候选择释放给所有任务,则所有等待的任务都会获取到信号量。
信号量获取函数OSSemPend()源码具体如下:
OS_SEM_CTR OSSemPend (OS_SEM *p_sem, (1) //信号量指针
OS_TICK timeout, (2) //等待超时时间
OS_OPT opt, (3) //选项
CPU_TS *p_ts, (4) //等到信号量时的时间戳
OS_ERR *p_err) (5) //返回错误类型
{
OS_SEM_CTR ctr;
OS_PEND_DATA pend_data;
CPU_SR_ALLOC();
#ifdef OS_SAFETY_CRITICAL(6)//如果启用(默认禁用)了安全检测
if (p_err == (OS_ERR *)0) //如果错误类型实参为空
{
OS_SAFETY_CRITICAL_EXCEPTION();//执行安全检测异常函数
return ((OS_SEM_CTR)0); //返回0(有错误),不继续执行
}
#endif
#if OS_CFG_CALLED_FROM_ISR_CHK_EN > 0u(7)//如果启用了中断中非法调用检测
if (OSIntNestingCtr > (OS_NESTING_CTR)0) //如果该函数在中断中被调用
{
*p_err = OS_ERR_PEND_ISR; //返回错误类型为“在中断中等待”
return ((OS_SEM_CTR)0); //返回0(有错误),不继续执行
}
#endif
#if OS_CFG_ARG_CHK_EN > 0u(8)//如果启用了参数检测
if (p_sem == (OS_SEM *)0) //如果 p_sem 为空
{
*p_err = OS_ERR_OBJ_PTR_NULL; //返回错误类型为“内核对象为空”
return ((OS_SEM_CTR)0); //返回0(有错误),不继续执行
}
switch (opt) (9)//根据选项分类处理
{
case OS_OPT_PEND_BLOCKING: //如果选择“等待不到对象进行阻塞”
case OS_OPT_PEND_NON_BLOCKING: //如果选择“等待不到对象不进行阻塞”
break; //直接跳出,不处理
default: (10)//如果选项超出预期
*p_err = OS_ERR_OPT_INVALID; //返回错误类型为“选项非法”
return ((OS_SEM_CTR)0); //返回0(有错误),不继续执行
}
#endif
#if OS_CFG_OBJ_TYPE_CHK_EN > 0u(11)//如果启用了对象类型检测
if (p_sem->Type != OS_OBJ_TYPE_SEM) //如果 p_sem 不是信号量类型
{
*p_err = OS_ERR_OBJ_TYPE; //返回错误类型为“内核对象类型错误”
return ((OS_SEM_CTR)0); //返回0(有错误),不继续执行
}
#endif
if (p_ts != (CPU_TS *)0) (12)//如果 p_ts 非空
{
*p_ts = (CPU_TS)0;
//初始化(清零)p_ts,待用于返回时间戳
}
CPU_CRITICAL_ENTER(); //关中断
if (p_sem->Ctr > (OS_SEM_CTR)0) (13)//如果资源可用
{
p_sem->Ctr--; (14)//资源数目减1
if (p_ts != (CPU_TS *)0) (15)//如果 p_ts 非空
{
*p_ts = p_sem->TS; //获取该信号量最后一次发布的时间戳
}
ctr = p_sem->Ctr; (16)//获取信号量的当前资源数目
CPU_CRITICAL_EXIT(); //开中断
*p_err = OS_ERR_NONE; //返回错误类型为“无错误”
return (ctr);
//返回信号量的当前资源数目,不继续执行
}
if ((opt & OS_OPT_PEND_NON_BLOCKING) != (OS_OPT)0) (17)
//如果没有资源可用,而且选择了不阻塞任务
{
ctr = p_sem->Ctr; //获取信号量的资源数目到 ctr
CPU_CRITICAL_EXIT(); //开中断
*p_err = OS_ERR_PEND_WOULD_BLOCK;
//返回错误类型为“等待渴求阻塞”
return (ctr);
//返回信号量的当前资源数目,不继续执行
}
else
//如果没有资源可用,但选择了阻塞任务 (18)
{
if (OSSchedLockNestingCtr > (OS_NESTING_CTR)0)(19)//如果调度器被锁
{
CPU_CRITICAL_EXIT(); //开中断
*p_err = OS_ERR_SCHED_LOCKED;
//返回错误类型为“调度器被锁”
return ((OS_SEM_CTR)0);
//返回0(有错误),不继续执行
}
}
OS_CRITICAL_ENTER_CPU_EXIT(); (20) //锁调度器,并重开中断
OS_Pend(&pend_data,
//阻塞等待任务,将当前任务脱离就绪列表,
(OS_PEND_OBJ *)((void *)p_sem),
//并插入节拍列表和等待列表。
OS_TASK_PEND_ON_SEM,
timeout); (21)
OS_CRITICAL_EXIT_NO_SCHED(); //开调度器,但不进行调度
OSSched(); (22)
//找到并调度最高优先级就绪任务
/* 当前任务(获得信号量)得以继续运行 */
CPU_CRITICAL_ENTER(); //关中断
switch (OSTCBCurPtr->PendStatus) (23)
//根据当前运行任务的等待状态分类处理
{
case OS_STATUS_PEND_OK: (24)//如果等待状态正常
if (p_ts != (CPU_TS *)0) //如果 p_ts 非空
{
*p_ts = OSTCBCurPtr->TS; //获取信号被发布的时间戳
}
*p_err = OS_ERR_NONE; //返回错误类型为“无错误”
break;
case OS_STATUS_PEND_ABORT: (25)//如果等待被终止中止
if (p_ts != (CPU_TS *)0) //如果 p_ts 非空
{
*p_ts = OSTCBCurPtr->TS; //获取等待被中止的时间戳
}
*p_err = OS_ERR_PEND_ABORT; //返回错误类型为“等待被中止”
break;
case OS_STATUS_PEND_TIMEOUT: (26)//如果等待超时
if (p_ts != (CPU_TS *)0) //如果 p_ts 非空
{
*p_ts = (CPU_TS )0; //清零 p_ts
}
*p_err = OS_ERR_TIMEOUT; //返回错误类型为“等待超时”
break;
case OS_STATUS_PEND_DEL: (27)//如果等待的内核对象被删除
if (p_ts != (CPU_TS *)0) //如果 p_ts 非空
{
*p_ts = OSTCBCurPtr->TS; //获取内核对象被删除的时间戳
}
*p_err = OS_ERR_OBJ_DEL;
//返回错误类型为“等待对象被删除”
break;
default: (28)//如果等待状态超出预期
*p_err = OS_ERR_STATUS_INVALID;
//返回错误类型为“等待状态非法”
CPU_CRITICAL_EXIT(); //开中断
return ((OS_SEM_CTR)0); //返回0(有错误),不继续执行
}
ctr = p_sem->Ctr; //获取信号量的当前资源数目
CPU_CRITICAL_EXIT(); //开中断
return (ctr); (29)//返回信号量的当前资源数目
}
- (1):信号量指针。
- (2):用户自定义的阻塞超时时间
- (3):获取信号量的选项,当信号量不可用的时候,用户可以选择阻塞或者不阻塞。
- (4):用于保存返回等到信号量时的时间戳。
- (5):用于保存返回的错误类型,用户可以根据此变量得知错误的原因。
- (6):如果启用(默认禁用)了安全检测,在编译时则会包含安全检测相关的代码, 如果错误类型实参为空,系统会执行安全检测异常函数,然后返回,停止执行。
- (7):如果启用了中断中非法调用检测,并且如果该函数在中断中被调用, 则返回错误类型为“在中断获取信号量”的错误代码,然后退出,停止执行。
- (8):如果启用了参数检测,在编译时则会包含参数检测相关的代码, 如果 p_sem参数为空,返回错误类型为“内核对象为空”的错误代码,并且退出,不执行获取消息操作。
- (9):判断一下opt选项是否合理,如果选择“等待不到对象进行阻塞” (OS_OPT_PEND_BLOCKING)或者选择“等待不到对象不进行阻塞”(OS_OPT_PEND_NON_BLOCKING),则是合理的,跳出switch语句。
- (10):如果选项超出预期,则返回错误类型为“选项非法”的错误代码,并且推出。
- (11):如果启用了对象类型检测,在编译时则会包含对象类型检测相关代码, 如果 p_sem不是信号量类型,那么返回错误类型为“对象类型有误”的错误代码,并且退出,不执行获取信号量操作。
- (12):如果 p_ts 非空,就初始化(清零)p_ts,待用于返回时间戳。
- (13):如果当前信号量资源可用。
- (14):那么被获取的信号量资源中的Ctr成员变量个数就要减一。
- (15):如果 p_ts 非空,获取该信号量最后一次发布的时间戳。
- (16):获取信号量的当前资源数目用于返回,执行完成,那么返回错误类型为“无错误”的错误代码,退出。
- (17):如果没有资源可用,而且用户选择了不阻塞任务, 获取信号量的资源数目到ctr变量用于返回,然后返回错误类型为“等待渴求组塞”的错误代码,退出操作。
- (18):如果没有资源可用,但用户选择了阻塞任务,则需要判断一下调度器是否被锁。
- (19):如果调度器被锁,返回错误类型为“调度器被锁”的错误代码,然后退出,不执行信号量获取操作。
- (20):如果调度器未被锁,就锁定调度器,重新打开中断。 此次可能有同学就会问了,为什么刚刚调度器被锁就错误的呢,而现在又要锁定调度器?那是因为之前锁定的调度器不是由这个函数进行锁定的, 这是不允许的,因为现在要阻塞当前任务,而调度器锁定了就表示无法进行任务调度,这也是不允许的。那为什么又要关闭调度器呢, 因为接下来的操作是需要操作队列与任务的列表,这个时间就不会很短,系统不希望有其他任务来操作任务列表, 因为可能引起其他任务解除阻塞,这可能会发生优先级翻转。比如任务A的优先级低于当前任务,但是在当前任务进入阻塞的过程中, 任务A却因为其他原因解除阻塞了,那系统肯定是会去运行任务A,这显然是要绝对禁止的, 因为挂起调度器意味着任务不能切换并且不准调用可能引起任务切换的API函数,所以,锁定调度器,打开中断这样的处理, 既不会影响中断的响应,又避免了其他任务来操作队列与任务的列表。
- (21):调用OS_Pend()函数将当前任务脱离就绪列表, 并根据用户指定的阻塞时间插入节拍列表和队列等待列表,然后打开调度器,但不进行调度。
- (22):当前任务阻塞了,就要进行一次任务的调度。
- (23):当程序能执行到这里,就说明大体上有两种情况, 要么是信号量中有可以的信号量了,任务获取到信号量了;要么任务还没获取到信号量(任务没获取到信号量的情况有很多种), 无论是哪种情况,都先把中断关掉再说,再根据当前运行任务的等待状态分类处理。
- (24):如果任务状态是OS_STATUS_PEND_OK,则表示任务获取到信号量了, 获取信号被释放时候的时间戳,返回错误类型为“无错误”的错误代码。
- (25):如果任务在等待(阻塞)被中止,则表示任务没有获取到信号量, 如果p_ts非空,获取等待被中止时的时间戳,返回错误类型为“等待被中止”的错误代码,跳出switch语句。
- (26):如果等待(阻塞)超时,说明等待的时间过去了, 任务也没获取到信号量,如果p_ts非空,将p_ts清零,返回错误类型为“等待超时”的错误代码,跳出switch语句。
- (27):如果等待的内核对象被删除, 如果p_ts非空,获取对象被删时的时间戳,返回错误类型为“等待对象被删”的错误代码,跳出switch语句。
- (28):如果等待状态超出预期,则返回错误类型为“状态非法”的错误代码,跳出switch语句。
- (29):打开中断,返回信号量的当前资源数目。
当有任务试图获取信号量的时候,当且仅当信号量有效的时候,任务才能获取到信号量。
如果信号量无效,在用户指定的阻塞超时时间中, 该任务将保持阻塞状态以等待信号量有效。
当其他任务或中断释放了有效的信号量,该任务将自动由阻塞态转移为就绪态。
当任务等待的时间超过了指定的阻塞时间,即使信号量中还是没有可用信号量,任务也会自动从阻塞态转移为就绪态。
信号量获取函数OSSemPend()的使用实例具体如下:
OSSemPend ((OS_SEM *)&SemOfKey, //等待该信号量被发布
(OS_TICK )0, //无期限等待
(OS_OPT )OS_OPT_PEND_BLOCKING, //如果没有信号量可用就等待
(CPU_TS *)&ts_sem_post, //获取信号量最后一次被发布的时间戳
(OS_ERR *)&err); //返回错误类型
5、使用信号量的注意事项
- 信号量访问共享资源不会导致中断延迟。当任务在执行信号量所保护的共享资源时, ISR或高优先级任务可以抢占该任务。
- 应用中可以有任意个信号量用于保护共享资源。然而,推荐将信号量用于I/O端口的保护,而不是内存地址。
- 信号量经常会被过度使用。很多情况下,访问一个简短的共享资源时不推荐使用信号量,请求和释放信号量会消耗CPU时间。
通过关/开中断能更有效地执行这些操作。假设两个任务共享一个32位的整数变量。第一个任务将这个整数变量加1,第二个任务将这个变量清零。
考虑到执行这些操作用时很短,不需要使用信号量。执行这个操作前任务只需关中断,执行完毕后再开中断。
但是若操作浮点数变量且处理器不支持硬件浮点操作时,就需要用到信号量。
因为在这种情况下处理浮点数变量需较长时间。 - 信号量会导致一种严重的问题:优先级翻转。