(1)FreeRTOS是我一天过完的,由此回忆并且记录一下。个人认为,如果只是入门,利用STM32CubeMX是一个非常好的选择。学习完本系列课程之后,再去学习网上的一些其他课程也许会简单很多。
(2)本系列课程是使用的keil软件仿真平台,所以对于没有开发板的同学也可也进行学习。
(3)叠甲,再次强调,本系列课程仅仅用于入门。学习完之后建议还要再去寻找其他课程加深理解。
(4)本系列博客对应代码仓库:gitee仓库
(1)在前文的 同步与互斥的缺陷中,我们讲解了当两个任务都需要调用
printf()
函数时候,会存在内容覆盖的情况。为了防止出现这类问题,FreeRTOS
提出了一个解决办法,就是队列。
(2)我们举个例子,假如老板树懒有两个员工,一个兔子和一个乌龟。
<1>兔子和乌龟都需要在网络上提交自己的工作内容,而老板树懒工作速度慢,而且他们公司的网络系统只能接受一个工作内容。那么因为兔子的工作效率高,频繁的提交工作内容,这就会导致兔子提交的内容会覆盖掉乌龟的工作内容。因此,全公司似乎就只有兔子在干活,乌龟是一个可有可无的员工。
<2>为了解决这个问题,公司的网络系统升级了,可以接受多个工作内容提交。那么当乌龟提交完内容之后的瞬间,兔子再去提交内容,就会自动的放在乌龟提交的内容后面。可以理解为一个传送带,每次提交数据都是放在传送带上。最终传送带将内容运送给树懒。
(3)从上面的例子中,我们就可以明白为什么需要存在队列,以及队列的作用了。
(1)将3.0章节的工程复制一份。
(2)在
freertos.c
中包含头文件queue.h
。
(3)本次实验将会使用到两个按键,因此将
PA0
和PA1
设置为下拉输入。
(1)按照下图方式创建动态创建一个队列
(1)按
Ctrl+F
搜索BEGIN Variables
即可找到如下代码块,进行补充。
/* USER CODE BEGIN Variables */
QueueHandle_t KeilQueueHandle; //Keil端创建的队列句柄
/* USER CODE END Variables */
(2)按
Ctrl+F
搜索BEGIN RTOS_QUEUES
即可找到如下代码块,进行补充。
/* USER CODE BEGIN RTOS_QUEUES */
/* add queues, ... */
KeilQueueHandle = xQueueCreate(10, sizeof(uint16_t));
/* USER CODE END RTOS_QUEUES */
(1)因为
STM32CubeMX
和Keil
所创建的队列最终都是只需要操作一个队列的句柄,所以我这里就只以Keil
所创建的队列句柄举例子了。(注意,如果像以STM32CubeMX
所创建的队列举例,只需要将KeilQueueHandle
修改为CubemxQueueHandle
)
(2)按Ctrl+F
搜索StartCubemxTask
即可找到如下代码块,进行补充。
(3)如下代码需要做的是,介绍:
<1>当K0按下,插入数据。当K1按下,读取数据。
<2>我们通过修改宏,设置要进行的实验操作
/* USER CODE END Header_StartCubemxTask */
#define Test_xQueueSendToFront 1
#define Test_xQueueSendToBack 0
#define Test_xQueueOverwrite 0
#define Test_xQueueReset 0
void StartCubemxTask(void *argument)
{
/* USER CODE BEGIN StartCubemxTask */
char *CubemxTaskPrintf = (char *)argument;
uint16_t Buf = 0;
BaseType_t status;
/* Infinite loop */
for(;;)
{
if (HAL_GPIO_ReadPin(Key_0_GPIO_Port, Key_0_Pin) == GPIO_PIN_SET)
{
Buf++;
#if Test_xQueueSendToFront
// 写实验1:测试头插xQueueSendToFront()函数
status = xQueueSendToFront(KeilQueueHandle, &Buf, 0);
if (status == pdTRUE)
{
printf("xQueueSendToFront writes data successfully : %d\r\n", Buf);
}
else
{
printf("xQueueSendToFront failed to write data\r\n");
}
#elif Test_xQueueSendToBack
// 写实验2:测试尾插xQueueSendToBack()函数
status = xQueueSendToBack(KeilQueueHandle, &Buf, 0);
if (status == pdTRUE)
{
printf("xQueueSendToBack writes data successfully : %d\r\n", Buf);
}
else
{
printf("xQueueSendToBack failed to write data\r\n");
}
#elif Test_xQueueOverwrite
// 写实验3:测试xQueueOverwrite()函数,这个只能用于队列大小为1的情况。不为1的队列使用这个,程序会崩溃
xQueueOverwrite(KeilQueueHandle, &Buf);
printf("xQueueOverwrite writes data successfully : %d\r\n", Buf);
#elif Test_xQueueReset
// 写实验4:测试尾插xQueueReset()函数
status = xQueueSendToBack(KeilQueueHandle, &Buf, 0);
if (status == pdTRUE)
{
printf("xQueueSendToBack writes data successfully : %d\r\n", Buf);
}
else
{
printf("xQueueSendToBack failed to write data\r\n");
}
xQueueReset(KeilQueueHandle);
#endif
//查询队列中存储的消息数
status = uxQueueMessagesWaiting(KeilQueueHandle);
printf("The number of data stored in the queue : %d\r\n",status);
//注意,在RTOS中,还使用阻塞的方式判断事件,无疑是非常愚蠢的。但是因为这个涉及后面内容,因此暂时使用这个做例子
while (HAL_GPIO_ReadPin(Key_0_GPIO_Port, Key_0_Pin) == GPIO_PIN_SET );
}
}
/* USER CODE END StartCubemxTask */
}
/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */
int fputc(int ch, FILE *f)
{
unsigned char temp[1]={ch};
HAL_UART_Transmit(&huart1,temp,1,0xffff);
return ch;
}
#define Test_xQueueReceive 1
#define Test_xQueueSendToBack 0
void StartKeilTask(void *argument)
{
uint16_t Buf = 0;
BaseType_t status;
while(1)
{
#if Test_xQueueReceive
// 按下K1读取数据
if (HAL_GPIO_ReadPin(Key_1_GPIO_Port, Key_1_Pin) == GPIO_PIN_SET )
{
// 读实验1:从队列头部读取消息,并删除消息
status = xQueueReceive(KeilQueueHandle, &Buf, 0);
if (status == pdTRUE)
{
printf("KeilQueueHandle data read successfully :%d\r\n", Buf);
}
else
{
printf("KeilQueueHandle data read failed\r\n");
}
#elif Test_xQueuePeek
// 读实验2:从队列头部读取消息,但是不删除消息
status = xQueuePeek(KeilQueueHandle, &Buf, 0);
if (status == pdTRUE)
{
printf("xQueuePeek data read successfully :%d\r\n", Buf);
}
else
{
printf("xQueuePeek data read failed\r\n");
}
#endif
//查询队列中的可用空间数
status = uxQueueSpacesAvailable(KeilQueueHandle);
printf("There is space left in the queue : %d\r\n",status);
//注意,在RTOS中,还使用阻塞的方式判断事件,无疑是非常愚蠢的。但是因为这个涉及后面内容,因此暂时使用这个做例子
while (HAL_GPIO_ReadPin(Key_1_GPIO_Port, Key_1_Pin) == GPIO_PIN_SET );
}
}
}
/* USER CODE END Application */
(1)因为这个读函数就有2个,写函数就有4个,一共8种组合。全写出来太费劲了。对具体任务感兴趣的,可以直接拿我写好的工程代码实测。
(2)如下为仿真器相关配置
<1>打开微库,因为我们需要使用printf()
函数。
<2>配置为软件仿真
DARMSTM.DLL
pSTM32F103C8
(3)按照如下方法进行模拟按键按下和松开。然后你根据你想测试的实验,打开相关注释即可。
(1)动态创建队列。
(2)参数介绍:
<1>队列的长度。通俗来说,就是这条队伍最大你想要排多少人。
<2>队列的数据所占字节。通俗点说,就是这个队伍中,每个人之间的间距是多少。
(3)从上面的解释我们就可以得出,队列最终的所占字节为uxQueueLength * uxItemSize
。
(4)uxItemSize
参数注意事项:
<1>这里需要注意一点的是,uxItemSize
是数据的所占字节数,因此当你传入sizof(float)
和sizof(int)
本质上是一个效果。(假设是32位系统)至于最终队列解析为float还是int,不由队列决定。
<2>这就理解为,这个队列中有两个体形硕大的人,队列中的uxItemSize
只需要让这两个人能够站进来,至于这两个人,是男是女,长多高,年龄多大,都和队列无关。只是和最终处理这个队列数据的人有关系。
/**
* @brief 动态创建队列
*
* @param uxQueueLength 队列的长度
* -uxItemSize 队列中每个数据的所占字节
*
* @return 队列创建成功,返回所创建队列的句柄。创建队列创建失败,返回 NULL
*/
QueueHandle_t xQueueCreate( uxQueueLength, uxItemSize );
(1) 释放分配用于存储放置在队列中的项目的所有内存
/**
* @brief 释放分配用于存储放置在队列中的项目的所有内存
*
* @param xQueue 要删除的队列的句柄
*
* @return 无
*/
void vQueueDelete( QueueHandle_t xQueue );
(1)
xQueueSend()
和xQueueSendToBack()
是同一个东西,至于为什么要弄两个一模一样的函数,可能是一开始只有尾插xQueueSend()
。然后后来又推出了头插xQueueSendToFront()
函数,为了做对比就再弄了一个尾插xQueueSendToBack()
。
(2)这两个函数,就是在队列中末尾加入数据。这个可以理解为一个遵守纪律的人,自觉排到队尾。
(3)传入参数解释:
<1>xQueue:队列的句柄。
<2>pvItemToQueue:指向待入队数据项的指针。
- 这里需要注意,传入的数据项内容大小,不应当超出创建队列时候指定的数据所占字节大小。否则会出现数据截断情况。
- 如果你创建队列时候,使用的是
sizof(float)
,而你这里传入一个int
型数据。对于xQueueSend()
和xQueueSendToBack()
函数而言,是可行的,因为他们都是4字节。- 但是对于整个工程项目来说,是危险的。因为最终读取数据的时候,
int
型数据和float
型数据混搭在队列中,我们很难知道这个取出来的到底是int
型数据还是float
型数据。- 如果不理解,举个医院看病的例子。我们排队的时候,人与人之间的间隔距离(队列的数据所占字节)只和人的体形大小(传入队列的数据所占字节)有关。至于这个人是男是女(
int
还是float
),和队列是没有关系的。那么,这样就会出现一个问题,一个男性胖子,排队的时候排到了妇产科。坐在里面的医生是不知道的,因此就会出问题。<3>xTicksToWait: 如果队列已满,则任务应进入阻塞态等待队列上出现可用空间的最大时间。
- 如果设置为 0,调用将立即返回。如果队列已满,将直接返回errQUEUE_FULL,并不会将任务进如阻塞态。
- 时间以滴答周期为单位定义,因此一般用常量
portTICK_PERIOD_MS
转换为实时。例如,队列已满,我们等待100ms,那么就传入100/portTICK_PERIOD_MS
。- 如果想要一直阻塞,直到队列有空位置才结束,那么传入
portMAX_DELAY
。这里需要注意,需要在FreeRTOSConfig.h
中将INCLUDE_vTaskSuspend
设置为 “1”
/**
* @brief 队列数据尾插
*
* @param xQueue 队列的句柄
* -pvItemToQueue 指向待入队数据项的指针,每次只能插入一个数据
* -xTicksToWait 如果队列已满,则任务应进入阻塞态等待队列上出现可用空间的最大时间。如果设置为 0,调用将立即返回。时间以滴答周期为单位定义,因此如果需要,应使用常量 portTICK_PERIOD_MS 转换为实时。将阻塞时间指定为 portMAX_DELAY 会导致任务无限期地阻塞(没有超时)。
*
* @return 如果数据尾插成功,返回 pdTRUE,否则返回 errQUEUE_FULL。
*/
BaseType_t xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait ) ;
(1)这个函数与
xQueueSendToBack()
相反的,xQueueSendToBack()
是当数据需要进入队列的时候,自觉的排到最后面。而xQueueSendToFront()
却是不知羞耻的插队到最前面。
(2)传入的参数解释同上,返回值同上。
/**
* @brief 队列数据头插
*
* @param xQueue 队列的句柄
* -pvItemToQueue 指向待入队数据项的指针,每次只能插入一个数据
* -xTicksToWait 如果队列已满,则任务应进入阻塞态等待队列上出现可用空间的最大时间。如果设置为 0,调用将立即返回。时间以滴答周期为单位定义,因此如果需要,应使用常量 portTICK_PERIOD_MS 转换为实时。将阻塞时间指定为 portMAX_DELAY 会导致任务无限期地阻塞(没有超时)。
*
* @return 如果数据头插成功,返回 pdTRUE,否则返回 errQUEUE_FULL。
*/
BaseType_t xQueueSendToFront( QueueHandle_t xQueue,const void * pvItemToQueue,TickType_t xTicksToWait );
(1)这个函数旨在用于长度为 1 的队列, 这意味着队列要么为空,要么为满。这个长度为1的队列还有一种专业术语,叫做邮箱。
(2)如果队列长度大于1,程序就会卡死。因为我现在手上没有开发板,所以无法上机实测具体是卡死在哪里了。软件仿真的结果只是知道程序崩了。
/**
* @brief 以覆盖的方式将数据传入队列,旨在用于长度为 1 的队列, 这意味着队列要么为空,要么为满
*
* @param xQueue 队列的句柄
* -pvItemToQueue 指向待入队数据项的指针
*
* @return 如果数据头插成功,返回 pdTRUE,否则返回 errQUEUE_FULL
*/
BaseType_t xQueueOverwrite(QueueHandle_t xQueue,const void * pvItemToQueue);
(1)将队列重置为其原始的空状态,队列中的所有数据都将会被清除。
/**
* @brief 将队列重置为其原始的空状态
*
* @param xQueue 队列的句柄
*
* @return 总是返回 pdPASS
*/
BaseType_t xQueueReset( QueueHandle_t xQueue );
(1)从队列头部读取消息,并删除消息。
(2)需要注意的是,pvItemToQueue
的数据类型要和队列中的数据类型可以不一致,只要数据大小一致即可,否则可能会出现数据截断情况。但是不建议这么用,因为xQueueReceive()
就是上面xQueueSendToBack()
函数介绍中举例的医生。你一个妇产科医生,想整花活,诊断男科。主打一个猝不及防。
/**
* @brief 从队列头部读取消息,并删除消息
*
* @param xQueue 队列的句柄
* -pvItemToQueue 指向缓冲区的指针
* -xTicksToWait 如果队列已满,则任务应进入阻塞态等待队列上出现可用空间的最大时间。如果设置为 0,调用将立即返回。时间以滴答周期为单位定义,因此如果需要,应使用常量 portTICK_PERIOD_MS 转换为实时。将阻塞时间指定为 portMAX_DELAY 会导致任务无限期地阻塞(没有超时)
*
* @return 如果从队列成功接收到项目,返回 pdTRUE,否则返回 pdFALSE
*/
BaseType_t xQueueReceive(QueueHandle_t xQueue,void *pvBuffer,TickType_t xTicksToWait);
(1)从队列头部读取消息,但是不删除消息。这个函数和
xQueueReceive()
的唯一区别在于,xQueueReceive()
读取数据的同时,还会把数据从队列中删除。而xQueuePeek()
仅仅只会读数据,数据并不会被删除。
/**
* @brief 从队列头部读取消息,但是不删除消息
*
* @param xQueue 队列的句柄
* -pvItemToQueue 指向缓冲区的指针
* -xTicksToWait 如果队列已满,则任务应进入阻塞态等待队列上出现可用空间的最大时间。如果设置为 0,调用将立即返回。时间以滴答周期为单位定义,因此如果需要,应使用常量 portTICK_PERIOD_MS 转换为实时。将阻塞时间指定为 portMAX_DELAY 会导致任务无限期地阻塞(没有超时)
*
* @return 如果从队列成功接收到项目,返回 pdTRUE,否则返回 pdFALSE
*/
BaseType_t xQueuePeek(QueueHandle_t xQueue,void *pvBuffer,TickType_t xTicksToWait);
(1)查询队列中的可用空间数。
/**
* @brief 查询队列中的可用空间数
*
* @param xQueue 队列的句柄
*
* @return 返回队列中的可用空间数
*/
UBaseType_t uxQueueSpacesAvailable( QueueHandle_t xQueue );
(1)查询队列中存储的消息数
/**
* @brief 查询队列中存储的消息数
*
* @param xQueue 队列的句柄
*
* @return 返回队列中存储的消息数
*/
UBaseType_t uxQueueSpacesAvailable( QueueHandle_t xQueue );
(1)FreeRTOS官方文档:队列管理