事件标志与信号量一样属于任务间同步的一种机制,但是信号量一般用于任务间的单事件同步,而对于任务间的多事件同步,仅仅使用信号量就显得力不从心了。为此 µC/OS-III 提供了事件标志这一机制,事件标志就能够很好地处理任务间的多任务同步。
1 µC/OS-III 事件标志简介
1.1 事件标志
事件标志是一个用于指示事件是否发生的比特位,因为一个事件是否发生只有两种情况,分别为事件发生和事件未发生,因此只需一个比特位就能够表示事件是否发生,µC/OS-III
用
1表示事件发生,用 0
表示事件未发生。
1.2 事件标志组
事件表示组是多个事件的集合,下面来看一下事件标志组的结构体,事件标志组的结构体定义在文件 os.h
中,具体的代码如下所示:
struct os_flag_grp{
/* 此宏用于使能检查内核对象的类型 */
#if (OS_OBJ_TYPE_REQ > 0u)
/* 对象类型,
* 在信号量初始化时,
* 信号量的类型会被初始化为 OS_OBJ_TYPE_FLAG
*/
OS_OBJ_TYPE Type;
#endif
/* 此宏用于使能代码调试功能 */
#if (OS_CFG_DBG_EN > 0u)
/* 如果使能了代码调试功能,
* 那么就会为每一个信号量赋一个名称,
* 方便调试
*/
CPU_CHAR* NamePtr;
#endif
/* 挂起等待任务链表
* 当任务获取的事件还未发生时,
* 就可以选择被挂起到该链表,
* 等待事件发生
*/
OS_PEND_LISTPendList;
/* 此宏用于使能代码调试功能 */
#if (OS_CFG_DBG_EN > 0u)
/* 一些与代码调试相关的成员变量 */
OS_FLAG_GRP*DbgPrevPtr;
OS_FLAG_GRP*DbgNextPtr;
CPU_CHAR* DbgNamePtr;
#endif
/* 事件表示的集合
* 对于 32 位 MCU,
* 这一成员变量是一个 32 位的无符号数,
* 每一比特位表示一个事件标志,
* 因此一个事件标志组最多容纳 32 个事件标志
*/
OS_FLAGS Flags;
/* 以下的成员变量用于第三方的调试工具 */
#if (OS_CFG_TS_EN > 0u)
CPU_TS TS;
#endif
#if (defined(OS_CFG_TRACE_EN) && (OS_CFG_TRACE_EN > 0u))
CPU_INT16U FlagID;
#endif
};
从上面的代码中可以看到,事件标志组的结构体中包含了一个数据类型为 OS_FLAGS
的成员变量 Flags
,对于
32
位的
MCU
而言,数据类型
OS_FLAGS
实际上就是
32
位的无符号整型,因此成员变量 Flags
就包含了
32
比特,而
Flags
的每一个比特就能够用来存储一个事件标志。当成员变量 Flags
的某一比特位被置一时,就说明这一比特位对应的事件发生了。
1.3 事件标志操作简介
事件标志组由多个事件标志组成,因此任务就能够很方便地通过多个事件的发生进行任务间的同步。通过事件标志,任务还能够通过多个事件之间的逻辑关系(与、或关系)进行任务间的同步,如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/648a445fe910f04bf30e3dc7ee7bd87d.png)
2 µC/OS-III 事件标志相关 API 函数
µC/OS-III 提供了一些列操作事件标志的
API
函数,这些操作函数如下表所示:
函数
| 描述 |
OSFlagCreate()
|
创建一个事件标志组
|
OSFlagDel()
|
删除一个事件标志组
|
OSFlagPend()
|
等待事件标志组中的事件
|
OSFlagPendAbort()
|
终止挂起等待事件标志组中的事件
|
OSFlagPendGetFlagRdy()
|
获取任务等待到的事件
|
OSFlagPost()
|
设置事件标志组中的事件
|
3 µC/OS-III 事件标志实验
本实验的程序流程图,如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/f09375c4a145ee26fed54ccd19af0532.png)
(1) start_task
任务
start_task 任务的入口函数代码如下所示:
/**
* @brief start_task
* @param p_arg : 传入参数(未用到)
* @retval 无
*/
void start_task(void *p_arg)
{
OS_ERR err;
CPU_INT32U cnts;
/* 初始化 CPU 库 */
CPU_Init();
/* 根据配置的节拍频率配置 SysTick */
cnts = (CPU_INT32U)(HAL_RCC_GetSysClockFreq() / OSCfg_TickRate_Hz);
OS_CPU_SysTickInit(cnts);
/* 开启时间片调度,时间片设为默认值 */
OSSchedRoundRobinCfg(OS_TRUE, 0, &err);
/* 创建事件标志 */
OSFlagCreate( (OS_FLAG_GRP* )&flag,
(CPU_CHAR* )"flag",
(OS_FLAGS )0,
(OS_ERR* )&err);
/* 创建 Task1 */
Task1Task_STK = (CPU_STK *)mymalloc( SRAMIN,
TASK1_STK_SIZE * sizeof(CPU_STK));
OSTaskCreate( (OS_TCB* )&Task1Task_TCB,
(CPU_CHAR* )"task1",
(OS_TASK_PTR )task1,
(void* )0,
(OS_PRIO )TASK1_PRIO,
(CPU_STK *)Task1Task_STK,
(CPU_STK_SIZE )TASK1_STK_SIZE / 10,
(CPU_STK_SIZE )TASK1_STK_SIZE,
(OS_MSG_QTY )0,
(OS_TICK )0,
(void* )0,
(OS_OPT )(OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR),
(OS_ERR* )&err);
/* 创建 Task2 */
Task2Task_STK = (CPU_STK *)mymalloc( SRAMIN,
TASK2_STK_SIZE * sizeof(CPU_STK));
OSTaskCreate( (OS_TCB* )&Task2Task_TCB,
(CPU_CHAR* )"task2",
(OS_TASK_PTR )task2,
(void* )0,
(OS_PRIO )TASK2_PRIO,
(CPU_STK* )Task2Task_STK,
(CPU_STK_SIZE )TASK2_STK_SIZE / 10,
(CPU_STK_SIZE )TASK2_STK_SIZE,
(OS_MSG_QTY )0,
(OS_TICK )0,
(void* )0,
(OS_OPT )(OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR),
(OS_ERR* )&err);
/* 创建 Task3 */
Task3Task_STK = (CPU_STK *)mymalloc( SRAMIN,
TASK3_STK_SIZE * sizeof(CPU_STK));
OSTaskCreate( (OS_TCB* )&Task3Task_TCB,
(CPU_CHAR* )"task3",
(OS_TASK_PTR )task3,
(void* )0,
(OS_PRIO )TASK3_PRIO,
(CPU_STK* )Task3Task_STK,
(CPU_STK_SIZE )TASK3_STK_SIZE / 10,
(CPU_STK_SIZE )TASK3_STK_SIZE,
(OS_MSG_QTY )0,
(OS_TICK )0,
(void* )0,
(OS_OPT )(OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR),
(OS_ERR* )&err);
/* 删除 Start Task */
OSTaskDel((OS_TCB *)0, &err);
}
从上面的代码中可以看出,start_task
任务使用函数
OSTaskCreate()
创建了
task1
任务、
task2任务和 task3 任务,并且 task1 任务、task2 任务和 task3 任务的任务栈都是动态分配的,当然,
也可以静态地定任务栈,就像定义一个数组一样,同时还调用函数 OSFlagCreate()
创建了一个用于本次实验的事件标志组。
(2) task1
任务
/**
* @brief task1
* @param p_arg : 传入参数(未用到)
* @retval 无
*/
void task1(void *p_arg)
{
uint8_t key;
OS_ERR err;
while (1)
{
key = key_scan(0);
switch (key)
{
case KEY0_PRES:
{
OSFlagPost( (OS_FLAG_GRP* )&flag,
(OS_FLAGS )FLAGBIT_0,
(OS_OPT )OS_OPT_POST_FLAG_SET,
(OS_ERR* )&err);
break;
}
case KEY1_PRES:
{
OSFlagPost( (OS_FLAG_GRP* )&flag,
(OS_FLAGS )FLAGBIT_1,
(OS_OPT )OS_OPT_POST_FLAG_SET,
(OS_ERR* )&err);
break;
}
default:
{
break;
}
}
OSTimeDly(10, OS_OPT_TIME_DLY, &err);
}
}
可以看到 task1
任务就是用于扫描按键,当检测到按键
0
被按下时,就设置事件标志组中的事件标志 0
,当检测到按键
1
被按下时,就设置事件标志组中的事件标志
1
,每间隔
10ticks就扫描一次按键。
(3) task2
任务
/**
* @brief task2
* @param p_arg : 传入参数(未用到)
* @retval 无
*/
void task2(void *p_arg)
{
uint32_t task2_num = 0;
OS_ERR err;
CPU_SR_ALLOC();
while (1)
{
OSFlagPend( (OS_FLAG_GRP* )&flag,
(OS_FLAGS )FLAGBIT_ALL,
(OS_TICK )0,
(OS_OPT )OS_OPT_PEND_FLAG_SET_ALL |
OS_OPT_PEND_FLAG_CONSUME |
OS_OPT_PEND_BLOCKING,
(CPU_TS *)0,
(OS_ERR *)&err);
CPU_CRITICAL_ENTER();
lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]);
CPU_CRITICAL_EXIT();
}
}
从上面的代码中可以看出,task2
任务会等待事件标志组中的事件标志
0
和事件标志
1
,只有当事件标志 0
和事件标志
1
同时被置
1
时,
task2
任务才能够执行,并且在
task2
任务成功等待到事件标志 0
和事件标志
1
后,会清零事件标志
0
和事件标志
1
。
(4) task3
任务
/**
* @brief task3
* @param p_arg : 传入参数(未用到)
* @retval 无
*/
void task3(void *p_arg)
{
OS_FLAGS flags;
OS_ERR err;
CPU_SR_ALLOC();
while (1)
{
flags = flag.Flags;
CPU_CRITICAL_ENTER();
lcd_show_xnum(163, 110, flags, 1, 16, 0, BLUE);
CPU_CRITICAL_EXIT();
OSTimeDly(10, OS_OPT_TIME_DLY, &err);
}
}
task3 任务比较简单,就是不断地获取事件标志组中事件标志的值,然后实时地将事件标志组中事件标志的值显示在 LCD
上,但值得一提的是,从上面的代码中可以看出,操作
LCD
的代码是处于临界区的,这是因为在本次实验中,task2
任务和
task3
任务都会操作
LCD
,在临界区中操作 LCD
就是为了避免任务在访问
LCD
这一共享资源的时候,产生访问冲突,因此在task2 任务的任务代码中,也能够看到操作
LCD
的代码被放在了临界区中。
编译并下载代码,复位后可以看到 LCD 屏幕上显示了本次实验的相关信息,接着按下按键 0,使得 task1 任务将事件标志组中的事件标志 0 置 1,随后立马就能看到task3 任务在 LCD 上显示了事件标志组中事件标志的值为 1。但此时 task2 任务还没有刷新 LCD,因为 task2 任务等待的时事件标志 0 和事件标志 1。接下来按下按键 1,使得 task1 任务设置事件标志组中的事件标志 1。
可以看到,task2 任务刷新了 LCD
,同时因为
task2
任务等待到了事件标志
0
和事件标志
1
,清 0
了事件标志组中的事件标志
0
和事件标志
1
,因此
task3
任务在
LCD
显示的事件标志组中事件标志的值变回了 0
。