FreeRTOS学习笔记—— 系列文章目录
同步:就是:我(A)正在用会议室,你(B)等会再用。
互斥:就是:我(A)正在用会议室,你(B)不能进来用。
互斥可以使用同步来实现,你(B)等我(A)用完会议室,你(B)再用会议室。这就是使用同步来实现互斥。
在这个过程中,A、B是互斥地访问会议室,会议室被称之为临界资源。这里使用了同步机制实现了“临界资源”的“互斥访问”。
任务A、B都要使用串口来打印,串口就是临界资源。如果A、B同时使用串口,那么打印出来的信息就是A、B混杂,无法分辨。所以使用串口时,应该:A用完,B再用;B用完,A再用。
能实现同步、互斥的内核方法有:队列(queue)、信号量(semaphoe)、互斥量(mutex)、事件组(event group)、任务通知(task notification)。
队列包含若干个数据:队列中有若干项,这被称为"长度"(length),每个数据大小固定,创建队列时就要指定长度、数据大小。
队列的创建有两种方法:动态分配内存、静态分配内存
动态创建队列函数:xQueueCreate,在函数内部动态分配内存。函数说明如下:
/* 函数原型
* 参数:uxQueueLength:队列长度,最多能存放多少个数据(item)
* 参数:uxItemSize :每个数据(item)的大小,以字节为单位。
* 返回值:非 0 :创建成功,返回句柄,使用这个句柄操作这个队列。
* NULL:创建失败,内存不足
*/
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
静态创建队列函数:xQueueCreateStatic,队列的内存事先分配好,函数说明如下:
/* 函数原型
* 参数1:uxQueueLength:队列长度,最多能存放多少个数据(item)
* 参数2:uxItemSize :每个数据(item)的大小,以字节为单位。
* 参数3:pucQueueStorageBuffer:必须指向一个 uint8_t 数组,此数组大小至少为 uxQueueLength * uxItemSize
* 参数4:pxQueueBuffer:必须定义一个StaticQueue_t结构体,用来保存队列的数据结构
* 返回值:非 0 :创建成功,返回句柄,使用这个句柄操作这个队列。
* NULL:创建失败,pxQueueBuffer 为 NULL
*/
QueueHandle_t xQueueCreateStatic(
UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t *pucQueueStorageBuffer,
StaticQueue_t *pxQueueBuffer
);
复位队列函数: xQueueReset,把队列恢复为初始状态
/* 参数:pxQueue : 复位队列的句柄
* 返回值: pdPASS(必定成功)
*/
BaseType_t xQueueReset( QueueHandle_t pxQueue);
删除队列函数: vQueueDelete,只能删除动态方法创建的队列,释放内存
/* 参数:xQueue : 要删除队列的句柄
*/
void vQueueDelete( QueueHandle_t xQueue );
可以把数据写到队列的头部,也可以写到尾部,写队列函数有两个版本:在任务中使用、在中断中使用(函数后缀为 ISR 的是在中断中使用)
/*
* 参数:xQueue :队列句柄,要写哪个队列
*
* 参数:pvItemToQueue :数据指针,这个数据的值会被复制进队列
*
* 参数:xTicksToWait :如果队列满则无法写入新数据,可以让任务进入阻塞状态,xTicksToWait表示阻塞的最大时间(Tick Count)。
如果被设为0,无法写入数据时函数会立刻返回;
如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写
* 返回值:pdPASS :数据成功写入了队列
* errQUEUE_FULL:写入失败,因为队列满了。
*/
/*
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSend( //等同于xQueueSendToBack
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
/*
* 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
读到一个数据后,队列中该数据会被移除。读队列函数有两个版本:在任务中使用、在中断中使用
/*
* 参数:xQueue :队列句柄,要读哪个队列
*
* 参数:pvBuffer :bufer指针,队列的数据会被复制到这个buffer
*
* 参数:xTicksToWait :如果队列空则无法读出数据,可以让任务进入阻塞状态,xTicksToWait表示阻塞的最大时间(Tick Count)。
如果被设为0,无法读出数据时函数会立刻返回;
如果被设为portMAX_DELAY,则会一直阻塞直到有数据可读
* 返回值:pdPASS :数据成功读出
* errQUEUE_EMPTY:读取失败,因为队列空了。
*/
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait
);
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken
);
可以查询队列中有多少个数据、有多少空余的空间。
// 返回队列中可用数据的个数
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
// 返回队列中可用空间的个数
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );
覆盖:当队列长度为1时,可以使用 xQueueOverwrite() 或 xQueueOverwriteFromISR() 来覆盖数据。队列长度必须为1。
/* 覆盖队列
* xQueue: 写队列
* pvItemToQueue: 数据地址
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueueOverwrite(
QueueHandle_t xQueue,
const void * pvItemToQueue
);
BaseType_t xQueueOverwriteFromISR(
QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
偷看:队列中的数据供多方读取,也就是读取队列中的数据但不移除队列中的数据。从队列中复制出数据,但不移除,如果队列中没有数据,那么偷看时会导致阻塞;一旦队列中有数据,以后每次偷看都会成功。
/* 偷看队列
* xQueue: 偷看的队列
* pvItemToQueue: 数据地址, 用来保存复制出来的数据
* xTicksToWait: 如果没有数据,阻塞一会
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueuePeek(
QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait
);
BaseType_t xQueuePeekFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
);
在main函数中创建了队列、创建了发送任务、接收任务
QueueHandle_t xQueue; //队列句柄
int main( void )
{
prvSetupHardware();
xQueue = xQueueCreate(1, sizeof( char * )); // 创建队列: 长度为1,数据大小为4字节(存放一个char指针)
if(xQueue == NULL)
{
printf("can not create queue\r\n");
}
printf( "Runing.\r\n" );
/* 创建2个任务用于写队列
* 优先级为1
*/
xTaskCreate(vSenderTask, "Sender1", 100, "Task1 is Runing!\r\n", 1, NULL);
xTaskCreate(vSenderTask, "Sender2", 100, "Task2 is Runing!\r\n", 1, NULL);
/* 创建1个任务用于读队列
* 优先级为2,高于写队列的任务,队列里一有数据就被读走
*/
xTaskCreate(vReceiverTask, "Recevier", 100, NULL, 2, NULL);
/* 启动调度器. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
写队列函数:将字符串地址写入队列
static void vSenderTask(void * param)
{
char * ValToSend; //接收到的字符存入 ValToSend
BaseType_t xStatus; // 写队列函数的返回值,用来判断是否写入成功
ValToSend = ( char * )param;
while (1)
{
xStatus = xQueueSend(xQueue, &ValToSend, portMAX_DELAY);
if(xStatus != pdTRUE)
{
printf( "Could not send to the queue.\r\n" );
}
}
}
读队列函数:
static void vReceiverTask(void * param)
{
BaseType_t xStatus;
char * ValToReceived;
const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );
while(1)
{
xStatus = xQueueReceive( xQueue, &ValToReceived, xTicksToWait );
if(xStatus == pdTRUE)
{
printf("%s", ValToReceived);
}
else
{
printf( "Could not receive from the queue.\r\n" );
}
}
}
当有多个发送任务,通过同一个队列发出数据,这就要区分数据的来源。可以通过写入队列结构体,在结构体中定义成员表明数据来源。接下来上代码。
main 函数中
typedef enum
{
Source01,
Source02
} ID_t; // 定义 枚举 类型,数据来源
typedef struct
{
ID_t eDataID;
char * lDataValue;
uint32_t val;
} Data_t; //定义队列中的传输格式 结构体
// 定义结构体 数组,不同的数据来源,不同的数据
static const Data_t xStructToSend[ 2 ] = { {Source01, "Hello world", 6},
{Source02, "Welcome", 8}
};
QueueHandle_t xQueue; //队列句柄
int main( void )
{
prvSetupHardware();
xQueue = xQueueCreate(5, sizeof( Data_t )); // 创建队列: 长度为5,数据大小为结构体的大小
if(xQueue == NULL)
{
printf("can not create queue\r\n");
}
/* 创建2个任务用于写队列,传入的参数是 结构体的地址
* 优先级为2
*/
xTaskCreate(vSenderTask, "Source01 Task", 1000, (void *) &(xStructToSend[0]), 2, NULL);
xTaskCreate(vSenderTask, "Source02 Task", 1000, (void *) &(xStructToSend[1]), 2, NULL);
/* 创建1个任务用于读队列
* 优先级为1,低于写队列的任务,发送任务优先执行,队列是满的
*/
xTaskCreate(vReceiverTask, "Recevier", 1000, NULL, 1, NULL);
/* Start the scheduler. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
写队列函数:将结构体地址写入队列
static void vSenderTask(void * param)
{
BaseType_t xStatus; // 写队列函数的返回值,用来判断是否写入成功
while (1)
{
xStatus = xQueueSend(xQueue, param, portMAX_DELAY);
if(xStatus != pdTRUE)
{
printf( "Could not send to the queue.\r\n" );
}
}
}
读队列函数:
static void vReceiverTask(void * param)
{
BaseType_t xStatus;
Data_t DateToReceived; /* 读取队列时, 用这个变量来存放数据 */
while(1)
{
xStatus = xQueueReceive( xQueue, &DateToReceived, 0 );
if(xStatus == pdTRUE)
{
//读到数据
if(DateToReceived.eDataID == Source01) //判断数据源,做不同的事情
{
printf("From Source01 \r\nval = %d \r\n%s \r\n\r\n", DateToReceived.val, DateToReceived.lDataValue);
}
else if(DateToReceived.eDataID == Source02)
{
printf("From Source02 \r\nval = %d \r\n%s \r\n\r\n", DateToReceived.val, DateToReceived.lDataValue);
}
}
else
{
printf( "Could not receive from the queue.\r\n" );
}
}
}
如果某系统支持鼠标输入、按键输入、触摸屏输入等,每种输入设备都会产生数据,产生的数据就可以放到对应的队列中,在应用程序中要去读取这三个队列,任意一个队列有数据都可以唤醒应用,这时就需要队列集
队列集可以监测多个队列,从多个队列中挑出有数据的队列,去读队列。
队列集也是队列,队列集中放的是队列。队列集的大小是检测队列的长度和,例如:要检测三个队列A、B、C,则队列集的长度为:队列A的长度 + 队列B的长度 + 队列C的长度。
程序
创建两个队列,使用队列集监测这两个队列,队列中有数据就读出来。
main函数
static QueueHandle_t xQueueHandle1; //队列的句柄
static QueueHandle_t xQueueHandle2; //队列的句柄
static QueueSetHandle_t xQueueHandle_Set; //队列集的句柄
int main( void )
{
prvSetupHardware();
//1、创建2个队列
xQueueHandle1 = xQueueCreate(2, sizeof( int ));
if(xQueueHandle1 == NULL)
{
printf("can not create queue\r\n");
}
xQueueHandle2 = xQueueCreate(2, sizeof( int ));
if(xQueueHandle2 == NULL)
{
printf("can not create queue\r\n");
}
//2、创建队列集
// xQueueCreateSet();是创建队列集函数,参数为队列集的大小(4) = 队列1的大小(2) + 队列2的大小(2)
xQueueHandle_Set = xQueueCreateSet(4);
//3、队列与队列集建立联系
xQueueAddToSet(xQueueHandle1, xQueueHandle_Set); //利用xQueueAddToSet(); 将 xQueueHandle1(队列1) 添加到 xQueueHandle_Set(队列集)中
xQueueAddToSet(xQueueHandle2, xQueueHandle_Set); //利用xQueueAddToSet(); 将 xQueueHandle2(队列2) 添加到 xQueueHandle_Set(队列集)中
//4、创建3个任务
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, NULL);
xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
xTaskCreate(Task3Function, "Task3", 100, NULL, 1, NULL);
/* Start the scheduler. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
三个任务函数:
任务 1:隔一段时间就往队列 1 中写数据
static void Task1Function(void * param)
{
BaseType_t xStatus;
int i = 0;
while (1)
{
// 任务 1:隔一段时间就往队列 1 中写数据
xStatus = xQueueSend(xQueueHandle1, &i, portMAX_DELAY);
if(xStatus != pdTRUE)
{
printf( "Could not send to the queue.\r\n" );
}
i++;
vTaskDelay(100);
}
}
任务 2:隔一段时间就往队列 2 中写数据
static void Task2Function(void * param)
{
BaseType_t xStatus;
int i = -1;
while (1)
{
// 任务 2:隔一段时间就往队列 2 中写数据
xStatus = xQueueSend(xQueueHandle2, &i, portMAX_DELAY);
if(xStatus != pdTRUE)
{
printf( "Could not send to the queue.\r\n" );
}
i--;
vTaskDelay(200);
}
}
任务 3:监测队列 1 和 队列 2 ,两个队列中有数据就读出来
static void Task3Function(void * param)
{
QueueSetMemberHandle_t handler;
int i;
//任务 3:监测队列 1 和 队列 2 ,两个队列中有数据就读出来
while (1)
{
//读队列集函数xQueueSelectFromSet(); 看哪个队列有数据,返回值为有数据的队列句柄,参数为队列集的句柄和等待时间
handler = xQueueSelectFromSet(xQueueHandle_Set, portMAX_DELAY);
//读有数据的队列
xQueueReceive(handler, &i, 0);
//打印出来
printf("get data : %d\r\n", i);
}
}
注意: 在使用函数来创建队列集,使用队列集的相关函数时要配置下面的宏:
#define configUSE_QUEUE_SETS 1
如果要使用 xQueueCreateSet(); 来创建队列集要配置两个宏,如下图:
其中第二个宏在FreeRTOS.h中被自动配置为 1 :
其中第一个宏在FreeRTOS.h中被自动配置为 0 :
所以需要增加配置宏
#define configUSE_QUEUE_SETS 1