本文参考自[野火EmbedFire]《RT-Thread内核实现与应用开发实战——基于STM32》,仅作为个人学习笔记。更详细的内容和步骤请查看原文(可到野火资料下载中心下载)
文章目录
事件的基本概念
事件主要用于线程间的同步,与信号量不同,它的特点是可以实现一对多,多对多的同步。即一个线程可等待多个事件的触发:可以是其中任意一个事件唤醒线程进行事件处理的操作;也可以是几个事件都到达后才唤醒线程进行后续的处理;同样,事件也可以是多个线程同步多个事件,这种多个事件的集合可以用一个32位无符号整型变量来表示,变量的每一位代表一个事件,线程通过“逻辑与”或“逻辑或”与一个或多个事件建立关联,形成一个事件集。事件的“逻辑或”也称为是独立型同步,指的是线程与任何事件之一发生同步;事件“逻辑与”也称为是关联型同步,指的是线程与若干事件都发生同步。
——RT-Thread官方中文手册
事件的应用场景
事件主要用来同步,不能进行数据传输,所以我们可以将事件作为标志位。在裸机系统中,我们常常使用全局变量作为标志位,但在实时操作系统中,使用全局标志极容易造成程序可读性差,代码管理困难等问题。
事件使用的场合比信号量更广,既能实现一对一,还能一对多,多对多控制。
事件的运作机制
在RT-Thread实现中,每个线程都拥有一个事件信息标记,它有三个属性,分别是
RT_EVENT_FLAG_AND
(逻辑与),RT_EVENT_FLAG_OR
(逻辑或)以及RT_EVENT_FLAG_CLEAR
(清除标记)。当线程等待事件同步时,可以通过32个事件标志和这个事件信息标记来判断当前接收的事件是否满足同步调节。
如上图事件工作示意图,线程#1的事件标志中第2位和第29位被置位,如果事件信息标记位设为逻辑与,则表示线程#1只有在事件1和事件29都发生以后才会被触发唤醒,如果事件信息标记位设为逻辑或,则事件1或事件29中任意一个发生都会触发唤醒线程#1。
如果信息标记同时设置了清除标记位,则当线程#1唤醒后将主动把事件1和事件29请为0,否则事件标志将依然存在(为1)。——RT-Thread官方中文手册
事件控制块
事件的控制块只有一个主要成员,即32位的事件集合。
struct rt_event
{
struct rt_ipc_object parent; /**< 继承自ipc_object类 */
rt_uint32_t set; /**< 事件集合 */
};
事件函数接口
创建事件 rt_event_create()
rt_event_t
rt_event_create
(const char* name, rt_uint8_t flag);
调用该函数接口时,系统会从动态内存堆中分配事件对象,然后进行对象的初始化,IPC对象初始化,并把set设置成0。使用 RT_IPC_FLAG_PRIO 优先级 flag 创建的 IPC 对象,在多个线程等待资源时,将由优先级高的线程优先获得资源。而使用 RT_IPC_FLAG_FIFO 先进先出 flag 创建的IPC 对象,在多个线程等待资源时,将按照先来先得的顺序获得资源。
参数 | 描述 |
---|---|
name | 事件的名称 |
flag | 事件的标志 |
删除事件 rt_event_delete()
rt_err_t
rt_event_delete
(rt_event_t event);
在调用rt_event_delete函数删除一个事件对象时,应该确保该事件不再被使用。在删除前会唤醒所有挂起在该事件上的线程(线程的返回值是-RT_ERROR),然后释放事件对象占用的内存块。
参数 | 描述 |
---|---|
event | 事件对象的句柄 |
初始化事件 rt_event_init()
rt_err_t
rt_event_init
(rt_event_t event, const char* name, rt_uint8_t flag);
调用该接口时,需指定静态事件对象的句柄(即指向事件控制块的指针),然后系统会初始化事件对象,并加入到系统对象容器中进行管理。使用 RT_IPC_FLAG_PRIO 优先级 flag 创建的 IPC 对象,在多个线程等待资源时,将由优先级高的线程优先获得资源。而使用 RT_IPC_FLAG_FIFO 先进先出 flag 创建的IPC 对象,在多个线程等待资源时,将按照先来先得的顺序获得资源。
参数 | 描述 |
---|---|
event | 事件对象的句柄 |
name | 事件的名称 |
flag | 事件的标志 |
事件接收 rt_event_recv()
rt_err_t
rt_event_recv
(rt_event_t event, rt_uint32_t set, rt_uint8_t option,
rt_int32_t timeout, rt_uint32_t* recved);
当用户调用这个接口时,系统首先根据set参数和接收选项来判断它要接收的事件是否发生,如果已经发生,则根据参数option上是否设置有RT_EVENT_FLAG_CLEAR来决定是否重置事件的相应标志位,然后返回(其中recved参数返回收到的事件); 如果没有发生,则把等待的set和option参数填入线程本身的结构中,然后把线程挂起在此事件对象上,直到其等待的事件满足条件或等待时间超过指定的超时时间。如果超时时间设置为零,则表示当线程要接受的事件没有满足其要求时就不等待,而直接返回-RT_TIMEOUT。
参数 | 描述 |
---|---|
event | 事件对象的句柄 |
set | 接收线程感兴趣的事件 |
option | 接收选项 |
timeout | 指定超时时间 |
recved | 指向收到的事件 |
事件发送 rt_event_send()
rt_err_t
rt_event_send
(rt_event_t event, rt_uint32_t set);
使用该函数接口时,通过参数set指定的事件标志来设定event对象的事件标志值,然后遍历等待在event事件对象上的等待线程链表,判断是否有线程的事件激活要求与当前event对象事件标志值匹配,如果有,则唤醒该线程。
参数 | 描述 |
---|---|
event | 事件对象的句柄 |
set | 发送的事件集 |
事件实验
要想在RT-Thread中使用事件通信,需要先修改rtconfigh
配置文件。可以使用下面两种方法:
- 取消注释,开启宏定义,
- 或者使用
Configuration Wizard
向导进行图形化配置,
本实验需要创建两个线程,分别是接收线程和发送线程,
接收线程负责接收 KEY1_EVENT 和 KEY2_EVENT 两个事件( KEY0 按下和 WK_UP 按下),RT_EVENT_FLAG_AND 表示当两个事件都发生时,才算接收到事件,RT_EVENT_FLAG_CLEAR 表示事件发生后,自动清除两个事件的标志位。
发送线程负责扫描按键,检测到按键按下,将对应事件标志位置1。
#include "board.h"
#include "rtthread.h"
// 定义线程控制块指针
static rt_thread_t recv_thread = RT_NULL;
static rt_thread_t send_thread = RT_NULL;
// 定义事件控制块
static rt_event_t test_event = RT_NULL;
#define KEY1_EVENT (0x01 << 0) //设置事件掩码的位0
#define KEY2_EVENT (0x01 << 1) //设置事件掩码的位1
/******************************************************************************
* @ 函数名 : recv_thread_entry
* @ 功 能 : 接收线程入口函数
* @ 参 数 : parameter 外部传入的参数
* @ 返回值 : 无
******************************************************************************/
static void recv_thread_entry(void *parameter)
{
rt_uint32_t recved;
while(1)
{
// 等待接收事件标志
rt_event_recv(test_event, // 事件对象句柄
KEY1_EVENT | KEY2_EVENT, // 接收线程感兴趣的事件
RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR, // 接收选项
RT_WAITING_FOREVER, // 超时事件一直等
&recved); // 接收到的事件
if(recved == (KEY1_EVENT | KEY2_EVENT))
{
rt_kprintf("recv:KEY0与WK_UP都被按下\n");
LED0_TOGGLE; // LED0 反转
}
else
{
rt_kprintf("事件错误!\n");
}
}
}
/******************************************************************************
* @ 函数名 : send_thread_entry
* @ 功 能 : 发送线程入口函数
* @ 参 数 : parameter 外部传入的参数
* @ 返回值 : 无
******************************************************************************/
static void send_thread_entry(void *parameter)
{
while(1)
{
// KEY0 被按下
if(Key_Scan(KEY0_GPIO_PORT, KEY0_GPIO_PIN) == KEY_ON)
{
rt_kprintf("send:KEY0被单击\n");
// 发送事件1
rt_event_send(test_event, KEY1_EVENT);
}
// WK_UP 被按下
if(Key_Scan(WK_UP_GPIO_PORT, WK_UP_GPIO_PIN) == KEY_ON)
{
rt_kprintf("send:WK_UP被单击\n");
// 发送事件1
rt_event_send(test_event, KEY2_EVENT);
}
rt_thread_delay(20); //每20ms扫描一次
}
}
int main(void)
{
// 硬件初始化和RTT的初始化已经在component.c中的rtthread_startup()完成
// 创建一个事件
test_event = // 事件控制块指针
rt_event_create("test_event", // 事件初始值
RT_IPC_FLAG_FIFO); // FIFO队列模式(先进先出)
if(test_event != RT_NULL)
rt_kprintf("事件创建成功!\n");
// 创建一个动态线程
recv_thread = // 线程控制块指针
rt_thread_create("recv", // 线程名字
recv_thread_entry, // 线程入口函数
RT_NULL, // 入口函数参数
255, // 线程栈大小
5, // 线程优先级
10); // 线程时间片
// 开启线程调度
if(recv_thread != RT_NULL)
rt_thread_startup(recv_thread);
else
return -1;
// 创建一个动态线程
send_thread = // 线程控制块指针
rt_thread_create("send", // 线程名字
send_thread_entry, // 线程入口函数
RT_NULL, // 入口函数参数
255, // 线程栈大小
5, // 线程优先级
10); // 线程时间片
// 开启线程调度
if(send_thread != RT_NULL)
rt_thread_startup(send_thread);
else
return -1;
}
实验现象
当 KEY0 和 WK_UP 都被按下后,接收端打印两个“按键都被按下“。
本实验中检测两个按键被依次按下这种场景并不常见,只是为了演示事件的运作机制。