本次实验我们来学习队列相关知识,本次实验我们使用队列进行两个进程间的通信,本次实验采用STM32F103ZET6主芯片的开发板,使用HAL库开发。
什么是队列
队列的本质是一个缓冲区,用于进程间或进程与ISR之间的少量的数据传递,所以也称为消息队列,一般可以采用先进先出(FIFO)或后进先出(LIFO)的方式。
FerrRTOS有针对于队列操作的函数,根据它们不同的功能可以分成:队列管理、获取队列信息、写入消息、读出消息四种,这些函数都可以在FreeRTOS的文档中查看,这里不再展开讲述,等下需要使用哪些我们会单独讲解。
程序实现
实验步骤:创建一个队列和两个进程,一个进程用于查询按键的状态并将其储存到队列中,另一个进程用于读取队列的状态并将其在LCD上展示。
CobeMX设置
我们先配置LCD相关配置:这里找到FSMC,选择NOR Flash/PSRAM/SRAM/ROM/LCD 4,然后具体配置如图所示:
图 1 LCD具体模式配置
注意:红色标注处为自己开发板设置的,可以查看自己开发板的原理图查看这一位。
图 2 FSMC参数设置
接下来我们需要4个按键,将这些按键设置为GPIO_OutPut,如图是我这块开发板的按键接口。
然后剩下的配置就是基本的打开FreeRTOS配置,设置接口为CMSIS_V2,剩下的保持初始设置不变;设置HAL时钟,这里设置为TIM6。因为之前没有使用FreeRTOS时HAL时钟默认是SysTick,但是现在FreeRTOS使用了SysTick,所以我们要为HAL时钟重新配置基础时钟。在NVIC中将TIM6的优先级配置为最高的0。
最后在FreeRTOS中添加两个进程和一个队列:
图 3 freeRTOS配置
这些配置完成后就可以生成代码了。
代码实现
这里我们需要先导入LCD的文件,这些文件一般是LCD厂商提供好的,只需要适配自己的开发板和编程工具即可,如果不会更改设置,可以翻看我之前的文章:LCD配置
如果没有LCD显示模块也可以使用串口进行调试,基本原理差不多。如果选择串口调试,就不需要配置之前CobeMX的LCD部分,而需要配置串口部分。
导入完成后在主函数中简单添加LCD初始化代码(删除了部分注释部分):
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_FSMC_Init();
/* USER CODE BEGIN 2 */
LCD_Init(); // LCD初始化
LCD_ShowString(10, 0, 280, 16, 16, (uint8_t *)"this is the damo Queue!"); // 在x = 10,y = 0位置处写入字符串,x最大为280个像素,字体为16号,高度为16
/* USER CODE END 2 */
osKernelInitialize(); /* Call init function for freertos objects (in freertos.c) */
MX_FREERTOS_Init();
osKernelStart();
while (1)
{
}
}
完成后测试LCD显示是否正常。
完成测试后定义一个枚举变量在main.h中,用于按键(KEY)的识别。
//KEY枚举类型
typedef enum
{
KEY0 = 0,
KEY1 = 1,
KEY2,
KEY3,
KEY4,
} KEY;
最后进入freertos.c文件,我们对进程的实现都在这个文件中。
在这个文件的开头添加两个头文件:
#include "lcd.h"
#include "queue.h"
然后在进程Task_Keys()中实现按键的捕捉和存入队列中:
void Task_Keys(void *argument)
{
KEY key;
/* USER CODE BEGIN Task_Keys */
/* Infinite loop */
for(;;)
{
key = KEY0; // 定义一个KEY的枚举变量
// 判断按下了哪个按键
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) == GPIO_PIN_RESET)
{
key = KEY1;
}
else if(HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin) == GPIO_PIN_RESET)
{
key = KEY2;
}
else if((HAL_GPIO_ReadPin(KEY3_GPIO_Port, KEY3_Pin) == GPIO_PIN_RESET))
{
key = KEY3;
}
else if((HAL_GPIO_ReadPin(KEY4_GPIO_Port, KEY4_Pin) == GPIO_PIN_SET))
{
key = KEY4;
}
// 将key的内容写入队列
if(key != KEY0)
{
BaseType_t err = xQueueSendToBack(myQueue_KeysHandle,&key,50);
if(err == errQUEUE_FULL) // 判断队列是否已经满了
{
xQueueReceive(myQueue_KeysHandle,&key,50); // 满了就复位队列
}
vTaskDelay(200); // 消除按键抖动
}
else
vTaskDelay(5);
}
/* USER CODE END Task_Keys */
}
这些具体操作在注释中已经详细解释了,这里就不再赘述。
在Task_Drow()进程中实现队列基本信息的展示和队列中储存的按键信息的提取识别:
void Task_Drow(void *argument)
{
uint16_t x = 0,y = 100;
UBaseType_t msgCount = 0,freeSpace = 0;
const char* qName = pcQueueGetName(myQueue_KeysHandle); // 获取队列名称
LCD_ShowString(10, 20, 280, 16, 16, (uint8_t *)"qName = ");
LCD_ShowString(75, 20, 280, 16, 16, (uint8_t *)qName);
// 获取队列初始剩余空间
UBaseType_t qSpaces = uxQueueSpacesAvailable(myQueue_KeysHandle);
LCD_ShowString(10, 40, 280, 16, 16, (uint8_t *)"QueueSpaces = ");
LCD_ShowxNum(120, 40, qSpaces, 2, 16, 0);
KEY keys; // 定义一个枚举变量
/* USER CODE BEGIN Task_Drow */
/* Infinite loop */
for(;;)
{
// 等待读取的消息个数
msgCount = uxQueueMessagesWaiting(myQueue_KeysHandle);
LCD_ShowString(10, 60, 280, 16, 16, (uint8_t *)"MessagesWaiting = ");
LCD_ShowxNum(145, 60, msgCount, 2, 16, 0);
// 读取剩余空间个数
freeSpace = uxQueueSpacesAvailable(myQueue_KeysHandle);
LCD_ShowString(10, 80, 280, 16, 16, (uint8_t *)"Spaces = ");
LCD_ShowxNum(80, 80, freeSpace, 2, 16, 0);
// 读取消息,阻塞式
UBaseType_t result = xQueueReceive(myQueue_KeysHandle,&keys,50);
// 判断是否成功读取数据
if(result != pdTRUE)
continue;
// 判断按键
if(keys == KEY2) // 按下按键2
{
LCD_ShowxNum(x, y, KEY2, 1, 16, 1);
// 在lcd上写2
}
else if(keys == KEY1) // 按下按键1
{
LCD_ShowxNum(x, y, KEY1, 1, 16, 1);
// 在lcd上写1
}
else if(keys == KEY3) // 按下按键3
{
LCD_ShowxNum(x, y, KEY3, 1, 16, 1);
// 在lcd上写3
}
if(keys == KEY4) // 按下按键4
{
LCD_ShowxNum(x, y, KEY4, 1, 16, 1);
// 在lcd上写4
}
x += 10;
if(x >= 240) // 判断是否超出一行了,如果超出就换行
{
x = 0;
y += 20;
}
vTaskDelay(200);
}
/* USER CODE END Task_Drow */
}
这样就完成了所有的配置,具体实验现象如图:
图 4 实验现象
第二行是队列的名称,第三行是队列初始剩余空间,第四行是等待读取的消息个数,第五行是剩余空间,第六行开始是每次按下的按键号。