FreeRTOS利用队列实现同步和互斥
前言:最近也是在学习韦东山老师的FreeRTOS的快速入门课程,希望学完之后能将之前的项目优化一下。学到了队列也算踏入了操作系统真正的大门,发一篇帖子来记录一下FreeRTOS的学习过程
一、同步和互斥的概念
很多大佬的博客将这两个概念写的很清楚。通俗点讲:
同步是在互斥的基础上进行有序的资源访问,例如串口的发送和接收,要等发送完数据之后才能接收数据。
互斥是同一个资源只允许一个资源对其进行访问,好比我和好朋友打电话,他说话的时候我就要倾听,我说话的时候他就要倾听。
二、在FreeRTOS中的队列
1.FreeRTOS中的消息队列或信号量的核心数据结构
typedef struct QueueDefinition /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
int8_t * pcHead; /*< Points to the beginning of the queue storage area. */
int8_t * pcWriteTo; /*< Points to the free next place in the storage area. */
union
{
QueuePointers_t xQueue; /*< Data required exclusively when this structure is used as a queue. */
SemaphoreData_t xSemaphore; /*< Data required exclusively when this structure is used as a semaphore. */
} u;
List_t xTasksWaitingToSend; /*< List of tasks that are blocked waiting to post onto this queue. Stored in priority order. */
List_t xTasksWaitingToReceive; /*< List of tasks that are blocked waiting to read from this queue. Stored in priority order. */
volatile UBaseType_t uxMessagesWaiting; /*< The number of items currently in the queue. */
UBaseType_t uxLength; /*< The length of the queue defined as the number of items it will hold, not the number of bytes. */
UBaseType_t uxItemSize; /*< The size of each items that the queue will hold. */
volatile int8_t cRxLock; /*< Stores the number of items received from the queue (removed from the queue) while the queue was locked. Set to queueUNLOCKED when the queue is not locked. */
volatile int8_t cTxLock; /*< Stores the number of items transmitted to the queue (added to the queue) while the queue was locked. Set to queueUNLOCKED when the queue is not locked. */
#if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated; /*< Set to pdTRUE if the memory used by the queue was statically allocated to ensure no attempt is made to free the memory. */
#endif
#if ( configUSE_QUEUE_SETS == 1 )
struct QueueDefinition * pxQueueSetContainer;
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxQueueNumber;
uint8_t ucQueueType;
#endif
} xQUEUE;
2、队列实现同步
通过比较两段程序中的flagCalcEnd变1的时间来判断是否实现同步
(1).程序1
void Task1Function(void * param)
{
volatile int i = 0;
while (1)
{
for (i = 0; i < 10000000; i++)
sum++;
//printf("1");
flagCalcEnd = 1;
vTaskDelete(NULL);
}
}
void Task2Function(void * param)
{
while (1)
{
if (flagCalcEnd)
printf("sum = %d\r\n", sum);
}
}
在主函数中创建两个任务、将flagCalcEnd放入逻辑分析仪中观察当从0变1所需要的时间
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
可以看到变量从0变化到1需要近乎4s,这是因为任务一和任务二同时抢占资源,但是我们设置的是当变量变成1的的时候任务2开始打印sum。这时我们引入队列来进行操作。
(2).程序2
void Task1Function(void * param)
{
volatile int i = 0;
while (1)
{
for (i = 0; i < 10000000; i++)
sum++;
//printf("1");
//flagCalcEnd = 1;
//vTaskDelete(NULL);
xQueueSend(xQueueCalcHandle, &sum, portMAX_DELAY);
sum = 1;
}
}
void Task2Function(void * param)
{
int val;
while (1)
{
//if (flagCalcEnd)
flagCalcEnd = 0;
xQueueReceive(xQueueCalcHandle, &val, portMAX_DELAY);
flagCalcEnd = 1;
printf("sum = %d\r\n", val);
}
}
这个时候我们来看看这段程序的逻辑,任务1中我们不断的对sum进行自加,当for循环执行完之后再把sum的值写入队列
。来看看xQueueSend()函数。
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait
);
对这个函数的各个参数进行解释
xQueueHandle xQueue: 这是队列句柄,它是之前通过调用xQueueCreate创建的队列的唯一标识符。传入正确的队列句柄以便将数据发送到对应的队列中。
const void * pvItemToQueue: 这是指向待发送数据的指针。pvItemToQueue指向的数据将会复制到队列中。数据的大小应该与创建队列时指定的uxItemSize相匹配。
TickType_t xTicksToWait:个参数决定了任务在试图发送数据到队列时可以阻塞等待的最长时间(以滴答计数)。如果队列已满并且无法立即发送数据:
- 如果xTicksToWait为0,则函数会尝试非阻塞地发送数据,若队列满则立即返回错误状态(一般为errQUEUE_FULL)。
- 如果xTicksToWait为portMAX_DELAY,则函数会一直等待直到队列有足够的空间来接收数据。
- 其他正值则代表最多等待指定滴答数后,无论队列是否为空都会返回。
再来看看xQueueReceive()函数
BaseType_t xQueueReceive(
QueueHandle_t xQueue,
void *pvBuffer,
TickType_t xTicksToWait
);
同样对这个函数进行分析
QueueHandle_t xQueue: 这是队列句柄,是在调用 xQueueCreate 成功创建队列后返回的句柄,用于标识你想要从中接收消息的队列。
void pvBuffer: 这是指向一个缓冲区的指针,当从队列中成功接收到一个消息时,队列中的数据将被复制到这个缓冲区内。因此,调用者需要提供一块足够大的内存区域来存放队列中存储的消息。
TickType_t xTicksToWait: 这个参数指定了在没有可用消息时,调用任务愿意等待的滴答数(ticks)。滴答是RTOS内部的时间基本单位,每个滴答通常与系统时钟周期相关联。
- 如果 xTicksToWait 设置为 0,函数将以非阻塞的方式尝试从队列接收数据,如果没有数据可接收则立即返回错误(通常为
ERRQUEUEEMPTY 或类似错误码)。 - 如果 xTicksToWait 设置为 portMAX_DELAY,那么函数将无限期等待,直到有消息到达队列。
- 对于其他正值,函数将在指定的滴答数内等待消息,超时后仍无消息则返回错误。
可以看出,将参数设置成 portMAX_DELAY时,只有队列中有消息才会读取,不然会无限期等待。所以这段程序的逻辑就是,只有等任务1中的for循环结束将sum写入队列中,任务2才开始执行,不然就会一直处于等待状态。同样我们把flagCalcEnd放入逻辑分析仪中观察时间
可以看到此时的时间来到了2s左右,缩短了一倍的时间,所以达到了我们原本的目的,利用队列来实现两个任务的同步。
3、队列实现互斥
利用锁来实现互斥,执行前看看可不可以,执行后告诉别人,我用完了,你们可以用了。
程序
/*初始化*/
int InitUARTLock(void)
{
int val;
xQueueUARTcHandle = xQueueCreate(1, sizeof(int));
if (xQueueUARTcHandle == NULL)
{
printf("can not create queue\r\n");
return -1;
}
xQueueSend(xQueueUARTcHandle, &val, portMAX_DELAY);
return 0;
}
/*读取队列消息*/
void GetUARTLock(void)
{
int val;
xQueueReceive(xQueueUARTcHandle, &val, portMAX_DELAY);
}
/*释放队列消息*/
void PutUARTLock(void)
{
int val;
xQueueSend(xQueueUARTcHandle, &val, portMAX_DELAY);
}
/*任务函数*/
void TaskGenericFunction(void * param)
{
while (1)
{
GetUARTLock();
printf("%s\r\n", (char *)param);
PutUARTLock();
vTaskDelay(1);
}
}
Task4获取UART的访问权,输出一条信息,然后释放UART访问权,Task3获取UART的访问权,输出一条信息,然后释放UART访问权,就构成了简单的互斥控制机制。
最后我们来看看串口是否是我们想象的那样。
总结
本文介绍了FreeRTOS中如何利用队列来实现简单的同步和互斥。