在实时操作系统中,当为了协调中断与线程之间或线程与线程之间同步,但不需要传送数据时,常以事件为手段。本节主要介绍事件的含义及应用场合、事件常用函数及事件的编程举例。关于事件所涉及的结构体、事件等待函数和事件置位函数将在11.1节进行深入剖析。
当某个线程需要等待另一线程(或中断)的信号才能继续工作,或需要将两个及以上的信号进行某种逻辑运算,并用逻辑运算的结果作为同步控制信号时,可采用事件字来实现,而这个信号或运算结果可以看作一个事件。例如,在串行中断服务程序中,将接收的数据放入接收缓冲区,当缓冲区数据是一个完整的数据帧时,可以把数据帧放入全局变量区,随后使用一个事件来通知其他线程及时对该数据帧进行剖析,这样就把两件事情交由不同主体完成:中断处理程序负责接收数据,并负责初步识别,而比较费时的数据处理交由线程函数完成。中断处理程序“短小精悍”是程序设计的基本要求。
一个事件用一位二进制数(0、1)表达,每一位称为一个事件位。在Mbed OS中,通常用一个字(如32位)来表达事件,这个字被称为事件字(用变量set表示) 。事件字的每一位可以记录一个事件,且事件之间相互独立,互不干扰。
事件字可以实现多个线程(或中断)协同控制一个线程。当各个相关线程(或中断)先后发出自己的信号后(使事件字的对应事件位有效),预定的逻辑运算结果有效,触发被控制的线程,使其脱离阻塞态,进入就绪态。
在Mbed OS中,事件的常用函数有事件等待函数(wait)、事件设置函数(set)、事件清除函数(clear)。
事件等待函数有两种形式,一种是wait_any,另一种是wait_all。当调用事件等待函数时,线程进入阻塞态。在GEC架构下,事件等待函数wait_any和wait_all被封装成event_recv函数。
(1)wait_any。等待32位事件字指定的一位或几位置位,就退出阻塞态。
(2)wait_all。等待32位事件字指定的几位事件位全部置位,才退出阻塞态。
事件设置函数用来设置事件字的指定事件位。该函数运行后(即事件位被置位后),因为执行事件等待函数而进入阻塞列表的线程,则会退出阻塞态,进入就绪列表,接受调度。一般编程过程,可以认为从事件等待函数之后的语句开始执行。在GEC架构下,事件设置函数set被封装成event_send函数。
事件清除函数用来清除指定的事件位。在GEC架构下,事件清除函数clear被封装成event_clear函数。
在“...\04-Softwareware\CH05\CH5.2.3-ISR_Event_mbedOS_STM32L431”文件夹下,可见具体通过事件实现中断与线程的通信实例,其功能为当串口接收到一帧数据(帧头3A+四位数据+帧尾0D 0A)时可控制红灯的亮暗。
在线程间使用事件进行同步时,一般编程步骤分为准备阶段与应用阶段。
(1)声明事件字全局变量并创建事件字:在使用事件之前,需要先确定程序中需要使用哪些事件字,可以通过event_create函数创建事件字。例如,在本节样例程序中,先在include.h中声明事件字全局变量EventWord(G_VAR_PREFIX就是extern),代码如下:
然后,在07_AppPrg\threadauto_appinit.c文件中创建事件字实例,代码如下:
(2)给事件位取名:在线程所包含的预定义头文件中对相应事件的事件位屏蔽字进行宏定义,以方便之后的识别与使用,即给事件位“取名字”。例如,在本节样例程序中红灯线程等待RED_LIGHT_EVENT置1,即事件字的第3位置1;若中断对RED_LIGHT_TASK置1,则红灯线程会收到这个信号,然后实现红灯反转操作。对应的宏定义在线程包含的预定义头文件07_AppPrg\includes.h中,代码如下:
(3)模块初始化与中断使能:这样产生中断之后才能进入中断服务程序。在工程的07_AppPrg\threadauto_appinit.c文件中添加以下代码:
(4)中断服务程序重定向:为了确保串口接收中断服务程序的可移植性和可复用性,将不同MCU的串口名称进行重新定义,以保证串口接收中断服务程序的通用性。在工程的05_UserBoard\user.h文件中添加以下代码:
在初始化结束事件变量后,就可以使用结束事件了。
(1)等待事件位置位:这一步是在等待事件触发的线程中进行的,在等待事件的红灯线程中需要同步的代码前通过event_recv等待函数获取符合条件的事件位。
等待事件位置位有两种参数选项,一是等待指定事件位“逻辑与”选项,即等待屏蔽字中逻辑值为1的所有事件位都被置位,选项名为“EVENT_FLAG_AND”;二是等待事件位“逻辑或”的选项,即等待屏蔽字中逻辑值为1的任意一个事件位被置位,选项名为“EVENT_FLAG_OR”。例如,在本节样例程序中,在线程thread_redlight里等待“红灯闪烁”事件位置位,代码如下:
(2)设置事件位:这一步是在触发事件的中断或线程中进行的,在中断的相应位置使用event_send函数对事件位置位,用来表示某个特定事件发生。例如,在本节样例程序中,在中断处理程序UART_User_Handler中设置了“红灯闪烁事件”的事件位,代码如下:
1)红灯线程
2)串口接收中断服务程序
3)程序执行流程分析
红灯线程的执行流程需要等待串口接收一个完整的数据帧(帧头3A+4位数据+帧尾0D 0A)之后,设置红灯事件位(事件字的第3位)。当红灯线程执行event_recv()这个语句时,红灯线程会被放入事件阻塞列表和等待列表中,状态由激活态变为阻塞态。直到收到串口中断中设置红灯事件位信号后,红灯线程才会从事件阻塞列表和等待列表中移出,红灯线程状态由阻塞态变为就绪态,并放入就绪列表中,由实时操作系统内核进行调度,才会执行后续语句(切换红灯亮暗)。
通过事件实现中断与线程间通信示例的运行结果如图5-1所示。通过串口输出的数据可以清晰地看出,在中断中设置红灯事件位,从而实现中断与线程之间的通信,实际效果是在发送完一帧数据后红灯的状态发生反转。
图5-1 通过事件实现中断与线程间通信示例的运行结果
在“...\04-Softwareware\CH05\CH5.2.4-Event_mbedOS_STM32L431”文件夹下,有通过事件实现线程间的通信实例,其功能为蓝灯线程控制绿灯事件,从而实现线程之间的通信。
前面已经详细阐述了使用事件之前所要做的准备工作,由于这里没有用到中断,所以就不需要对中断进行声明、使能和重定向了。
1)绿灯线程
2)蓝灯线程
3)程序执行流程分析
绿灯线程的执行流程需要等待蓝灯线程设置绿灯事件位(事件字的第2位)。当绿灯线程执行event_recv()这个语句时,绿灯线程会被放入事件阻塞列表和等待列表中,状态由激活态变为阻塞态。直到收到蓝灯线程设置绿灯事件位信号后,绿灯线程才会从事件阻塞列表和等待列表中移出,状态由阻塞态变为就绪态,并放入就绪列表中,由实时操作系统内核进行调度,才会执行后续语句(切换绿灯亮暗)。
当蓝灯线程执行event_send(EventWord,GREEN_LIGHT_EVENT)这个语句时,会向绿灯线程发送绿灯事件位已被设置信号,绿灯线程收到这个绿灯事件位信号后,才会执行后续语句(切换绿灯亮暗)。事件调度过程将在11.1节进行深入剖析。
通过事件实现线程间通信示例的运行结果如图5-2所示。通过串口输出可以看见在蓝灯线程中设置绿灯事件位,从而实现蓝灯线程与绿灯线程之间的通信,实际效果是绿灯亮暗交替。
图5-2 通过事件实现线程间通信示例的运行结果