一、消息队列
消息队列,一种用于任务与任务间、中断和任务间传递信息的数据结构,实现了任务接收来自其他任务或中断的不等长的消息。
消息队列能够接收来自线程或中断服务例程中不固定长度的消息,并把消息缓存在自己的内存空间中。其他线程也能够从消息队列中读取相应的消息,而当消息队列是空的时候,可以挂起读取线程。当有新的消息到达时,挂起的线程将被唤醒以接收并处理消息。消息队列是一种异步的通信方式。
创建消息队列,FreeRTOS系统会分配一块单个消息大小与消息队列长度乘积的空间;(创建成功后,每个消息的大小及消息队列长度无法更改,不能写入大于单个消息大小的数据,并且只有删除消息队列时,才能释放队列占用的内存) 。写入消息队列,当消息队列未满或允许覆盖入队时,FreeRTOS系统会直接将消息复制到队列末端;否则,程序会根据指定的阻塞时间进入阻塞状态,直到消息队列未满或者是阻塞时间超时,程序就会进入就绪状态;写入紧急消息,本质上与普通消息差不多,不同的是其将消息直接复制到消息队列队首;读取消息队列,在指定阻塞时间内,未读取到消息队列中的数据(消息队列为空),程序进入阻塞状态,等待消息队列中有数据;一旦阻塞时间超时,程序进入就绪态;
二、消息队列应用实例
函数解释
//创建队列
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,//队列长度
UBaseType_t uxItemSize );//队列中消息单元的大小,以字节为单位
//队列发送消息,队尾写入
BaseType_t xQueueSend(QueueHandle_t xQueue,//队列句柄
const void * pvItemToQueue,//指针,指向要发送到队列尾部的队列消息
TickType_t xTicksToWait);//等待时间
//中断服务程序中使用的发送函数
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken//为一个可选参数, 可以设置为 NULL。
);
//向队列首发送消息
BaseType_t xQueueSendToFront( QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait );
//从一个队列中接收消息并把消息从队列中删除
BaseType_t xQueueReceive(QueueHandle_t xQueue,
void *pvBuffer,
TickType_t xTicksToWait);
//在中断服务程序中接收消息队列的函数,总之就是在任务中调用和在中断服务程序中调用函数不同。
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void * const pvBuffer,
BaseType_t * const pxHigherPriorityTaskWoken
);
举例说明
QueueHandle_t Test_Queue =NULL;
#define QUEUE_LEN 4 /*消息队列长度*/
#define QUEUE_SIZE 4 /*每个消息大小 */
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;
·taskENTER_CRITICAL(); //进入临界区
/* 创建Test_Queue */
Test_Queue = xQueueCreate((UBaseType_t ) QUEUE_LEN,(UBaseType_t ) QUEUE_SIZE);
创建任务一:Send_Task
创建任务二:Receive_Task
vTaskDelete(AppTaskCreate_Handle);
taskEXIT_CRITICAL();
}
/
//任务一:发送
static void Send_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;
uint32_t send_data1 = 1;
uint32_t send_data2 = 2;
while (1)
{
xReturn = xQueueSend( Test_Queue,&send_data1,0 );
if(pdPASS == xReturn)
printf("send_data1 发送成功!\n\n");
vTaskDelay(20);
}
}
//任务二:接收
static void Receive_Task(void* parameter)
{
BaseType_t xReturn = pdTRUE;
uint32_t r_queue;
while (1)
{
xReturn = xQueueReceive( Test_Queue, &r_queue,portMAX_DELAY);
if(pdTRUE == xReturn)
printf("收到数据%d\n\n",r_queue);
else
printf("没有收到数据0x%lx\n",xReturn);
}
}
三、信号量
二值信号量任务之间同步或临界资源的互斥访问
同步:比如说,买包子
我要去买包子,如果包子店没有包子了,则需要等待卖包子的把包子做出来我才能买到包子,这个等待的过程就叫做同步。(在实际应用中:一个采集数据的传感器任务,一个处理数据的任务,则处理数据的任务需要等待传感器去采用数据,则在FreeRTOS系统中等待不能干等着,在该任务等待的过程中,CPU转而可以去执行其他任务,则就可以提高效率,则就是队列的阻塞机制)
互斥:比如说,抢厕所
只有一个厕所,一个人进去上了,另一个人也要上,则必须等待前人上完厕所才能上,等待的过程就是同步,而保护厕所只能一个人上的过程叫做互斥,厕所就是所谓的临界资源,同一时间只能一个人使用厕所。
以生活中的停车场为例来理解信号量的概念:
①当停车场空的时候,停车场的管理员发现有很多空车位,此时会让外面的车陆续进入停车场获得停车位;
②当停车场的车位满的时候,管理员发现已经没有空车位,将禁止外面的车进入停车场,车辆在外排队等候;
③当停车场内有车离开时,管理员发现有空的车位让出,允许外面的车进入停车场;待空车位填满后,又禁止外部车辆进入。
在此例子中,管理员就相当于信号量,管理员手中空车位的个数就是信号量的值(非负数,动态变化);停车位相当于公共资源(临界区),车辆相当于线程。车辆通过获得管理员的允许取得停车位,就类似于线程通过获得信号量访问公共资源。
四、信号量实例
- 二值信号量用于同步:在多任务系统中,经常会使用二值信号量来实现任务之间或者任务与中断之间的同步,比如,某个任务需要等待一个标记,那么任务可以在轮询中查询这个标记有没有被置位,则任务在等待的过程也会消耗CPU的资源,代码如下所示:
SemaphoreHandle_t BinarySem_Handle =NULL;
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;
taskENTER_CRITICAL();
/* 创建二值信号量*/
BinarySem_Handle = xSemaphoreCreateBinary();
if(NULL != BinarySem_Handle)
printf("BinarySem_Handle create successful!\r\n");
创建任务一:Receive_Task
创建任务二:Send_Task
vTaskDelete(AppTaskCreate_Handle);
taskEXIT_CRITICAL();
}
static void Receive_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;
while (1) {
//获取二值信号量,没有获取则一直等待
xReturn = xSemaphoreTake(BinarySem_Handle,portMAX_DELAY);
if(pdTRUE == xReturn)
printf("BinarySem_Handle get successful |!\n\n");
}
}
static void Send_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;
while (1)
{
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{
xReturn = xSemaphoreGive( BinarySem_Handle );//给出信号量
if( xReturn == pdTRUE )
printf("BinarySem_Handle 释放成功\r\n");
else
printf("BinarySem_Handle 释放失败\r\n");
}
vTaskDelay(20);
}
}
五、事件组
事件是一种实现任务/中断间通信的机制,主要用于实现多任务间的同步,但事件通信只能是事件类型的通信,无数据传输。其实事件组的本质就是一个整数(16/32位)。
事件组与队列/信号量的区别:
1.信号量/队列当事件发生时只会去唤醒一个任务,而事件组可以唤醒多个任务起到一个广播的作用。
2.信号量/队列是一个消耗性资源,即数据读走了则就减少,而事件组可以选择清除事件也可以选择保留事件。
3.事件组只能是起到一个同步的作用,并不能传递数据。
4.最重要的一点事件组可以实现多个任务之间的同步,队列/信号量则只能是两个任务之间的同步
事件组的特点:
1.一个 32 位的事件集合(EventBits_t 类型的变量,实际可用与表示事件的只有 24 位,还有8位用于管理事件),其中每一位表示一种事件类型(0 表示该事件类型未发生、1 表示该事件类型已经发生)。
2.事件仅用于同步,不提供数据传输功能。
3. 与信号量/队列不同设置事件组不会阻塞,即多次向任务设置同一事件等效于只设置一次。
4. 支持事件等待超时机制,即等待该事件类型(该事件还未发生)的任务会进入阻塞态。
5.事件获取的时候,有两个选择:1.逻辑或:任务所期望的事件中只要有任意一个事件发生,任务即可被唤醒。2.逻辑或:任务所期望的事件必须全部发生,任务才能被唤醒。
六、事件组实例
static EventGroupHandle_t Event_Handle =NULL;
#define KEY1_EVENT (0x01 << 0)
#define KEY2_EVENT (0x01 << 1)
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;
taskENTER_CRITICAL();
/* 创建事件 Event_Handle */
Event_Handle = xEventGroupCreate();
if(NULL != Event_Handle)
printf("Event_Handle创建成功\r\n");
创建任务一:LED_Task
创建任务二:KEY_Task
vTaskDelete(AppTaskCreate_Handle);
taskEXIT_CRITICAL();
}
static void LED_Task(void* parameter)
{
EventBits_t r_event;
while (1)
{
r_event = xEventGroupWaitBits(Event_Handle,
KEY1_EVENT|KEY2_EVENT,//事件
pdTRUE,pdTRUE, portMAX_DELAY);
if((r_event & (KEY1_EVENT|KEY2_EVENT)) == (KEY1_EVENT|KEY2_EVENT))
{
printf ( "收到事件\n");
}
else
printf ( "事件 错误\n");
}
}
static void KEY_Task(void* parameter)
{
while (1)
{
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) {
xEventGroupSetBits(Event_Handle,KEY1_EVENT); //触发事件一
}
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) {
xEventGroupSetBits(Event_Handle,KEY2_EVENT); 触发事件二
}
vTaskDelay(20);
}
}
七、任务通知
前言:任务间通信的机制,包括队列、事件组和各种不同类型的信号量。使用这些机制都需要创建一个通信对象。事件和数据不会直接发送到接收任务或接收ISR,而是发送到通信对象(也就是发送到队列、事件组、信号量)。同样,任务和ISR从通信对象接收事件和数据,而不是直接从发送事件或数据的任务或ISR接收事件和数据。
任务通知允许任务与其他任务交互,并与ISR同步,而不需要单独的通信对象。通过使用任务通知,任务或ISR可以直接向接收任务发送事件。
每个任务都有一个 32 位的通知值,在大多数情况下,任务通知可以替代二值信号量、计数信号量、事件组,也可以替代长度为 1 的队列;任务通知的使用无需创建队列;
(1)任务通知的缺点:按照FreeRTOS官方的说法使用任务通知比通过队列、事件标志组或信号量通信方式解除阻塞的任务要快 45%,并且更加省 RAM 内存空间,因为像队列、信号量、事件组这些通信方式使用前必须先创建,拿队列来说如下图所示,申请内存的时候至少需要下图这么多变量,而任务通知是任务结构体中自带的一个32位的无符号整数,一个8位的通知状态变量,一共就5个字节。
(2).任务通知的缺点
虽然说任务通知可以模拟这么多通信方式,但是肯定有限制、有缺点,不然还要这些队列、信号量、事件组干嘛。
1.不能发送通知到中断
原因很简单,任务通知、任务通知,人家通知的是任务,是修改任务控制块中那个32位无符号整数的值,中断并没有任务控制块这一说,但为什么队列、信号量、事件组这些就可以呢,说到底人家创建了一个独立的队列、信号量、事件组结构体当然谁都可以访问里面的内容,但是可以在中断中发送通知给其他任务,这个是没毛病的。
2.不能发送通知给多个任务
任务通知只能指定发送给某一个任务而不能广播,而队列、信号量、事件组任何中断和任务都能访问,不过很少出现多个任务或中断接收同一个通讯对象的情况
3.发送通知的任务不能进入阻塞
只有等待通知的任务可以被阻塞,发送通知的任务,在任何情况下都不会因为发送失败而进入阻塞态,像队列:写队列当队列满的时候,可以进入阻塞态
八、任务通知实例
获取任务通知
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, //任务句柄
uint32_t ulValue, //值
eNotifyAction eAction );
eNotifyAction 的取值
* eNoAction = 0//通知任务而不更新其通知值
* eSetBits//设置任务通知值中的值
* eIncrement//增加任务的通道值
* eSetvaluewithoverwrite//覆盖当前通知
* eSetValueWithoutoverwrite//不覆盖当前通知
等待任务通知
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,//将从任务的通知值中清除的位(Bit)。这个参数可以用来指定只关心特定的位。可以使用 0 来表示不清除任何位。
uint32_t ulBitsToClearOnExit,//从任务的通知值中清除的位。如果希望在任务等待结束时清除某些位,可以通过此参数指定。如果不需要清除,可以使用 0
uint32_t *pulNotificationValue,//函数将把接收到的通知值存储在这个指针指向的内存中。如果不需要接收到的通知值,可以传递 NULL
TickType_t xTicksToWait );//任务没有接收到通知值的情况下等待的时间(以滴答计时,毫秒为单位)。如果这个时间到了还没有收到通知,函数将返回。使用 portMAX_DELAY 可以让任务无限期等待,直到收到通知。
#define EVENTBIT_0 (1<<0) //CAN_0
#define EVENTBIT_1 (1<<1) //CAN_1
#define EVENTBIT_2 (1<<2) //USART_0
#define EVENTBIT_3 (1<<3) //USART_1
举例说明
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;
taskENTER_CRITICAL();
创建任务一: Receive1_Task
创建任务二 :Send_Task
vTaskDelete(AppTaskCreate_Handle);
taskEXIT_CRITICAL();
}
//发送任务
static void Send_Task(void* parameter)
{
BaseType_t xReturn = pdPASS;
uint32_t send1 = 1;
while (1)
{
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{
xReturn = xTaskNotify( Receive1_Task_Handle, /*任务句柄*/
(uint32_t)EVENTBIT_3, /*任务内容 */
eSetValueWithOverwrite );/*覆盖当前通知*/
if( xReturn == pdPASS )
printf("Receive1_Task_Handle ÈÎÎñ֪ͨÏûÏ¢·¢Ëͳɹ¦!\r\n");
}
vTaskDelay(20);
}
}
//接收任务
static void Receive1_Task(void* parameter)
{
BaseType_t xReturn = pdTRUE;
uint32_t r_num;
xReturn=xTaskNotifyWait(0x0, //进入函数的时候不清除任务bit
ULONG_MAX, //退出函数的时候清除所有bit
(uint32_t *)&r_num, //任务通知值
portMAX_DELAY); //阻塞事件
if( pdTRUE == xReturn )
printf("Receive1_Task 任务通知消息 %d \n",r_num);
}
九、互斥锁
信号量的一个特例,信号量可以实现一对一和一对多,而对于互斥锁只能实现一对一。互斥锁是一种用于保护共享资源的机制。当一个任务需要使用一个共享资源时,它必须首先获取互斥锁。如果互斥锁已经被另一个任务获取,那么这个任务就需要等待,直到互斥锁被释放。在FreeRTOS中,可以使用xSemaphoreCreateMutex()函数来创建一个互斥锁。
互斥量又叫相互排斥的信号量,是一种特殊的二值信号量。互斥量类似于只有一个车位的停车场:当有一辆车进入的时候,将停车场大门锁住,其他车辆在外面等候。当里面的车出来时,将停车场大门打开,下一辆车才可以进入。
SemaphoreHandle_t xMutex;
void vTask(void pvParameters)
{
for(;;)
{
if(xSemaphoreTake(xMutex, (TickType_t)10) == pdTRUE)
{
// The mutex was successfully taken, so the shared resource can be accessed.
printf("Task: Mutex taken!\n");
// ...
// Access the shared resource.
// ...
// Release the mutex.
xSemaphoreGive(xMutex);
}
else
{
// The mutex could not be taken.
printf("Task: Mutex not taken!\n");
}
}
}
信号量 vs 互斥锁区别:
- 互斥锁是一种所有权的概念,即一个任务获取了互斥锁后,只有它自己可以释放这个互斥锁,其他任务不能释放。而信号量没有所有权的概念,任何任务都可以释放信号量。
- 在FreeRTOS中,互斥锁有优先级翻转的解决机制,当一个低优先级的任务获取了互斥锁,而高优先级的任务需要这个互斥锁时,低优先级的任务的优先级会被提升,以减少优先级反转的问题。而信号量没有这个机制。
- 互斥锁通常用于保护共享资源,即在同一时间只能有一个任务访问某个资源。信号量则更多是用于任务同步,它可以被用来唤醒一个或多个等待的任务。
- 在FreeRTOS中,信号量可以有计数的概念,即可以被“给”多次,每次“给”都会增加一个计数,而互斥锁没有这个概念,它只有锁定和解锁两种状态。
- 信号量可以被用作二元信号量(即只有两种状态,0和1,类似互斥锁),而互斥锁不能被用作计数信号量。
十、总结
消息队列:不同任务或进程之间传递消息
互斥锁:侧重于保证同一时刻只有一个线程可以访问共享资源
信号量:控制对共享资源访问的同步机制,用于控制多个线程对共享资源的访问数量。
事件组:管理多个事件的同步机制,允许多个任务等待多个事件的发生
任务通知:任务之间直接发送通知,任务间通信
线程间同步:信号量,互斥锁,事件组
线程间通信:消息队列,任务通知