【STM32F4】【银杏科技ARM+FPGA】iCore3移植RT-Thread--内核之事件集
一、事件集的引入
以某人坐公交车为例,坐公交车难免有一下三种情况的发生:
1、 张三需要去商场买蔬菜,但是只有一班公交车到商场,张三需要等待公交车的到来。
2、 张三需要到高铁站出行,有三班公交车到高铁站,张三等到其中的一班即可出发。
3、 张三约李四一起到公园游玩,张三需要等到李四到达了公交站牌后,等到公交车的到来方可出发。
我们将公交车的到来、等到同伴李四的到来视为事件,将到商场、高铁站、公园的行为视为线程。案例1为一个事件触发一个线程;案例2为任意一个事件触发一个线程;案例3为多个事件同时发生触发一个线程。
二、事件工作机制
由事件集的引入我们大概了解事件的工作机制了。事件集主要用于线程间的同步,与信号量不同,事件集的特点是可以实现一对多,多对多的同步。多个事件集的集合可以用一个32位无符号整型变量来表示,变量的每一位代表一个事件,线程通过“逻辑与”、“逻辑或”将一个或多个事件关联起来,形成事件组合。事件的“逻辑或”成为独立型同步,线程与任一事件发生同步;事件的“逻辑与”成为关联型同步,线程与若干事件共同发生同步。
事件集特点:
1、 事件只与线程相关,事件集相互独立:每个线程拥有32个事件标志,采用一个32位无符号整型数记录,每个位代表一个事件。
2、 事件仅用于同步,不提供数据传输功能。
3、 事件无排队性,即多次向线程发送同一个事件(如果线程未及时读走),其效果等同于一次。
在RT-Thread实现中,每个线程都拥有一个事件信息标记,它有三个属性,分别是RT_EVENT_FLAG_AND(逻辑与),RT_EVENT_FLAG_OR(逻辑或)和RT_EVENT_FLAG_CLEAR(清除标记)。当线程等待事件同步时,可以通过32个事件标志和这个事件信息标记来判断当前接收的事件是否满足同步条件。如上图所示,线程 #1 的事件标志中第 1 位和第 30 位被置位,如果事件信息标记位设为逻辑与,则表示线程 #1 只有在事件 1 和事件 30 都发生以后才会被触发唤醒,如果事件信息标记位设为逻辑或,则事件 1 或事件 30 中的任意一个发生都会触发唤醒线程 #1。如果信息标记同时设置了清除标记位,则当线程 #1 唤醒后将主动把事件 1 和事件 30 清为零,否则事件标志将依然存在(即置 1)。
三、事件集的优点
到此为止,我们对事件集有了全面了了解,为了全面掌握RT-Thread操作系统,同时有人也会有一些疑惑,例如可以设定标志位去触发线程,下面我们分两层讲解:一,事件集与信号量。RTT为了保护公共资源设定了信号量,到获取信号量以后方可启动线程,RTT事件集为当某事件或几个事件发生了才触发线程。看是相似,我们来具体分析:事件适应性更广,当信号量用于线程间同步的时候,与事件等同;事件与信号量不同在于事件的的发送不可累加(无论在什么条件下,多次发送的效果等同于一次),信号量的释放是可以累加的;事件的主要特性在于多个事件到来可触发一个或多个线程,这是信号量望尘莫及的功能,信号量只能辨别一个同步动作;二、事件与标志位。我们跑裸机系统时,为了触发某一程序会设定全局变量为标志位,但是在操作系统中,设定标志位就没那么好用了。我们会议一下信号量为了保护公共资源才设定了信号量,在“一对多”、“多对多”线程同步时,无法保护全局变量。另外,设定了标志位就需要设定专门的线程轮询访问事件是否发生,不能保证操作系统的实时性。
四、事件集控制块
事件集控制块是操作系统用于管理事件的一个数据结构,由结构体struct rt_event表示:
struct rt_event
{
struct rt_ipc_object parent; /* 继 承 自 ipc_object 类 */
/* 事 件 集 合, 每 一 bit 表 示 1 个 事 件, bit 位 的 值 可 以 标 记 某 事 件 是 否 发 生 */
rt_uint32_t set;
};
/* rt_event_t 是 指 向 事 件 结 构 体 的 指 针 类 型 */
typedef struct rt_event* rt_event_t;
事件集控制块含有事件集相关的重要参数,相关接口函数如下,对一个事件集的操作包含:创建/初始化事件集、发送事件集、接收事件集、删除/脱离事件集。
五、事件集的接口函数
1、创建动态事件集函数
当创建一个事件集时,内核首先创建一个事件集控制块,然后对该事件集控制块进行基本的初始化。
rt_event_t rt_event_create(const char* name, rt_uint8_t flag);
(1)入口参数:
name:事件集的名称。
flag:事件集的标志,它可以取如下数值:RT_IPC_FLAG_FIFO 或RT_IPC_FLAG_PRIO。
(2)返回值:
RT_NULL:创建失败。
事件对象的句柄:创建成功。
2、删除动态事件函数
系统不再使用 rt_event_create() 创建的事件集对象时,通过删除事件集对象控制块来释放系统资源。在删除一个事件集对象时,应该确保该事件集不再被使用。在删除前会唤醒所有挂起在该事件集上的线程(线程的返回值是RT_ERROR),然后释放事件集对象占用的内存块。
rt_err_t rt_event_delete(rt_event_t event);
(1)入口参数:
event:事件集对象的句柄。
(2)返回值:
RT_EOK:成功。
3、创建静态事件集函数
这里所说的创建静态事件集和《RT-Thread编程指南》所说的初始化事件集是一样的,静态事件集对象的内存是在系统编译时由编译器分配的,一般放于读写数据段或未初始化数据段中。
rt_err_t rt_event_init(rt_event_t event, const char* name, rt_uint8_t flag);
(1)入口参数:
event:事件集对象的句柄。
name:事件集的名称。
flag:事件集的标志,它可以取如下数值:RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO。
(2)返回值:
RT_EOK:成功。
4、删除静态事件集函数
当系统不再使用 rt_event_init() 初始化的事件集对象时,通过脱离事件集对象控制块来释放系统资源,系统首先唤醒所有挂在该事件集等待队列上的线程(线程的返回值是RT_ERROR),然后将该事件集从内核对象管理器中脱离。
rt_err_t rt_event_detach(rt_event_t event);
(1)入口参数:
event:事件集对象的句柄。
(2)返回值:
RT_EOK:成功。
5、发送事件函数
发送事件函数可以发送事件集中的一个或多个事件,使用该函数接口时,通过参数 set 指定的事件标志来设定 event 事件集对象的事件标志值,然后遍历等待在 event 事件集对象上的等待线程链表,判断是否有线程的事件激活要求与当前 event 对象事件标志值匹配,如果有,则唤醒该线程。
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set);
(1)入口参数:
event:事件集对象的句柄。
set:发送的一个或多个事件的标志值。
(2)返回值:
RT_EOK:成功。
6、接收事件函数
这里是引用内核使用 32 位的无符号整数来标识事件集,它的每一位代表一个事件,因此一个事件集对象可同时等待接收 32 个事件,内核可以通过指定选择参数 “逻辑与” 或 “逻辑或” 来选择如何激活线程,使用 “逻辑与” 参数表示只有当所有等待的事件都发生时才激活线程,而使用 “逻辑或” 参数则表示只要有一个等待的事件发生就激活线程。
系统首先根据 set 参数和接收选项 option 来判断它要接收的事件是否发生,如果已经发生,则根据参数 option 上是否设置有 RT_EVENT_FLAG_CLEAR 来决定是否重置事件的相应标志位,然后返回(其中 recved 参数返回接收到的事件);如果没有发生,则把等待的 set 和 option 参数填入线程本身的结构中,然后把线程挂起在此事件上,直到其等待的事件满足条件或等待时间超过指定的超时时间。如果超时时间设置为零,则表示当线程要接受的事件没有满足其要求时就不等待,而直接返回RT_ETIMEOUT。
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);
(1)入口参数:
event:事件集对象的句柄。
set:接收线程感兴趣的事件。
option:接收选项。
timeout:指定超时时间。
recved:指向接收到的事件。
(2)返回值:
RT_EOK:成功。
RT_ETIMEOUT:超时。
RT_ERROR:错误。
六、事件集例程及实验现象
#include <rtthread.h> #define THREAD_PRIORITY 9 #define THREAD_TIMESLICE 5 #define EVENT_FLAG3 (1 << 3) #define EVENT_FLAG5 (1 << 5) /* 事件控制块 */ static struct rt_event event; ALIGN(RT_ALIGN_SIZE) static char thread1_stack[1024]; static struct rt_thread thread1; /* 线程1入口函数 */ static void thread1_recv_event(void *param) { rt_uint32_t e; /* 第一次接收事件,事件3或事件5任意一个可以触发线程1,接收完后清除事件标志 */ if (rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5), RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR, RT_WAITING_FOREVER, &e) == RT_EOK) { rt_kprintf("thread1: OR recv event 0x%x\n", e); } rt_kprintf("thread1: delay 1s to prepare the second event\n"); rt_thread_mdelay(1000); /* 第二次接收事件,事件3和事件5均发生时才可以触发线程1,接收完后清除事件标志 */ if (rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5), RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR, RT_WAITING_FOREVER, &e) == RT_EOK) { rt_kprintf("thread1: AND recv event 0x%x\n", e); } rt_kprintf("thread1 leave.\n"); } ALIGN(RT_ALIGN_SIZE) static char thread2_stack[1024]; static struct rt_thread thread2; /* 线程2入口 */ static void thread2_send_event(void *param) { rt_kprintf("thread2: send event3\n"); rt_event_send(&event, EVENT_FLAG3); rt_thread_mdelay(200); rt_kprintf("thread2: send event5\n"); rt_event_send(&event, EVENT_FLAG5); rt_thread_mdelay(200); rt_kprintf("thread2: send event3\n"); rt_event_send(&event, EVENT_FLAG3); rt_kprintf("thread2 leave.\n"); } int event_sample(void) { rt_err_t result; /* 初始化事件对象 */ result = rt_event_init(&event, "event", RT_IPC_FLAG_FIFO); if (result != RT_EOK) { rt_kprintf("init event failed.\n"); return -1; } rt_thread_init(&thread1, "thread1", thread1_recv_event, RT_NULL, &thread1_stack[0], sizeof(thread1_stack), THREAD_PRIORITY - 1, THREAD_TIMESLICE); rt_thread_startup(&thread1); rt_thread_init(&thread2, "thread2", thread2_send_event, RT_NULL, &thread2_stack[0], sizeof(thread2_stack), THREAD_PRIORITY, THREAD_TIMESLICE); rt_thread_startup(&thread2); return 0; } /* 导出到 msh 命令列表中 */ MSH_CMD_EXPORT(event_sample, event sample);
打开putty,选择正确的端口和波特率,编译成功后下载程序,可以看到ARM_LED等循环闪烁,输入event_sample命令: