细说STM32单片机FreeRTOS消息缓冲区及其应用实例
目录
一、消息缓冲区功能概述
二、消息缓冲区操作相关函数
1、相关函数概述
2、部分函数详解
(1)创建消息缓冲区
(2)写入消息
(3)读取消息
(4)消息缓冲区状态查询
三、消息缓冲区使用示例
1、示例功能与CubeMX项目设置
(1)RCC、SYS、Code Generator、USART3、TIM6
(2)RTC的设置
(3)FreeRTOS的设置
(4)NVIC
2、程序功能实现
(1)主程序
(2)FreeRTOS对象初始化
(3)RTC的唤醒中断
(4)任务Task_Show的功能
3、运行调试
一、消息缓冲区功能概述
消息缓冲区(message buffer)是基于流缓冲区实现的,也就是它的实现使用了流缓冲区的技术,如同信号量是基于队列实现的。与流缓冲区的差异在于:消息缓冲区传输的是可变长度的消息,如10字节、20字节或35字节的消息。写入者向消息缓冲区写入一个10字节的消息,读取者也必须以10字节的消息读出,而不是像流缓冲区那样,按字节流读出。
每个消息都有一个消息头,就是消息数据的字节数。在STM32 MCU上,消息头就是一个uint32_t类型的整数。消息头的写入和读取是由FreeRTOS的API函数自动处理的,例如,向消息缓冲区写入一个长度为20字节的消息,实际占用空间是24字节。
消息缓冲区没有触发水平,写入和读取都是以一条消息为单位的,操作要么成功,要么失败。
消息缓冲区的其他特性与流缓冲区一样。例如:在只有一个写入者和一个读取者的情况下,可以安全操作消息缓冲区;如果有多个写入者或多个读取者,读写消息缓冲区的代码必须在临界代码段内,且等待时间必须设置为0。
二、消息缓冲区操作相关函数
1、相关函数概述
消息缓冲区相关函数的头文件message_buffer.h,源程序都在文件stream_buffer.c里,因为消息缓冲区是基于流缓冲区实现的,要在程序中使用消息缓冲区,需包含头文件message_buffer.h。
分组 | 函数 | 功能 |
创建 和 | xMessageBufferCreate() | 创建一个消息缓冲区,只需设置缓冲区大小 |
xMessageBufferCreateStatic() | 创建一个消息缓冲区,静态分配内存 | |
vMessageBufferDelete() | 删除一个消息缓冲区 | |
xMessageBufferReset() | 复位一个消息缓冲区,清空数据。只有没有任务在阻塞状态下读或写消息缓冲区时,才可以复位消息缓冲区 | |
写入 | xMessageBufferSend() | 向消息缓冲区发送一个消息 |
xMessageBufferSendFromISR() | xMessageBufferSend()的ISR版本 | |
读取 | xMessageBufferReceive() | 从消息缓冲区接收一条消息 |
xMessageBufferReceiveFromISR() | xMessageBufferReceive()的ISR版本 | |
状态 查询 | xMessageBufferIsEmpty() | 查询消息缓冲区是否为空,返回值pdTRUE表示无任何消息 |
xMessageBufferIsFull() | 查询消息缓冲区是否满了,返回值pdTRUE表示不能 | |
xMessageBufferSpacesAvailable() | 查询消息缓冲区的剩余存储空间 |
与流缓冲区不同的是:消息缓冲区无须设置触发水平,在写入或读取消息超时的时候,实际写入或读取的数据字节数为0,不会只写入或读取部分数据。
2、部分函数详解
(1)创建消息缓冲区
用于创建消息缓冲区的函数是xMessageBufferCreate(),这是个宏函数,其原型定义如下:
/**
* \defgroup xMessageBufferCreate xMessageBufferCreate
* \ingroup MessageBufferManagement
*/
#define xMessageBufferCreate( xBufferSizeBytes ) ( MessageBufferHandle_t ) xStreamBufferGenericCreate( xBufferSizeBytes, ( size_t ) 0, pdTRUE )
调用函数xMessageBufferCreate()时,只需传递缓冲区大小xBufferSizeBytes。这个函数实际上调用了函数xStreamBufferGenericCreate(),传递的触发水平参数为0,因为消息缓冲区没有触发水平,最后的参数pdTRUE表示要创建的是消息缓冲区。
函数xMessageBufferCreate()的返回值是MessageBufferHandle_t类型的,就是所创建的消息缓冲区对象指针。
(2)写入消息
用于向消息缓冲区写入消息的函数是xMessageBufferSend(),这是个宏函数,其原型定义如下:
/**
* \defgroup xMessageBufferSend xMessageBufferSend
* \ingroup MessageBufferManagement
*/
#define xMessageBufferSend( xMessageBuffer, pvTxData, xDataLengthBytes, xTicksToWait ) xStreamBufferSend( ( StreamBufferHandle_t ) xMessageBuffer, pvTxData, xDataLengthBytes, xTicksToWait )
实际上,它是执行了流缓冲区写入数据的函数xStreamBufferSend()。函数中各参数的意义如下。
- xMessageBuffer,所操作的消息缓冲区的句柄。
- pvTxData,准备写入的数据缓冲区指针。
- xDataLengthBytes,消息数据的字节数,不包括消息头的4字节。
- xTicksToWait,等待的节拍数,如果消息缓冲区没有足够的空间用于写入这条消息,任务可以进入阻塞状态等待。若设置为0,则表示不等待;若设置为portMAX_DELAY,则表示一直等待。
函数xStreamBufferSend()内部会判断传递来的缓冲区对象的类型。如果是消息缓冲区,就在实际写入数据前面加上一个uint32_t类型的整数,表示消息的字节数;如果是流缓冲区,就直接写入数据。
函数xMessageBufferSend()的返回值是实际写入消息的字节数,不包括消息头的4字节。如果函数是因为等待超时而退出的,则返回值为0;如果写入成功,返回值就是写入的消息数据的字节数。这是与流缓冲区不同的一个地方,使用函数xStreamBufferSend()向流缓冲区写入数据时,如果因等待超时而退出,仍然可能向流缓冲区写入了一些数据。
在ISR中,向消息缓冲区写入消息的函数是xMessageBufferSendFromISR(),它是个宏函数,实际就是执行了函数xStreamBufferSendFromISR(),其原型定义如下:
/**
* \defgroup xMessageBufferSendFromISR xMessageBufferSendFromISR
* \ingroup MessageBufferManagement
*/
#define xMessageBufferSendFromISR( xMessageBuffer, pvTxData, xDataLengthBytes, pxHigherPriorityTaskWoken ) xStreamBufferSendFromISR( ( StreamBufferHandle_t ) xMessageBuffer, pvTxData, xDataLengthBytes, pxHigherPriorityTaskWoken )
(3)读取消息
用于从消息缓冲区读取消息的函数是xMessageBufferReceive(),其原型定义如下:
/**
* \defgroup xMessageBufferReceive xMessageBufferReceive
* \ingroup MessageBufferManagement
*/
#define xMessageBufferReceive( xMessageBuffer, pvRxData, xBufferLengthBytes, xTicksToWait ) xStreamBufferReceive( ( StreamBufferHandle_t ) xMessageBuffer, pvRxData, xBufferLengthBytes, xTicksToWait )
它就是执行了函数xStreamBufferReceive()。函数中各参数的意义如下。
- xMessageBuffer,所操作的消息缓冲区的句柄。
- pvRxData,保存读出数据的缓冲区指针。
- xBufferLengthBytes,缓冲区pvRxData的长度,也就是最大能读取的字节数。
- xTicksToWait,等待的节拍数。如果消息缓冲区里没有消息,任务可以进入阻塞状态等待。若设置为0,则表示不等待;若设置为portMAX_DELAY,则表示一直等待。
函数xStreamBufferReceive()会自动区分参数xMessageBuffer是流缓冲区,还是消息缓冲区。如果是消息缓冲区,它会先读取表示消息长度的4字节消息头,然后按照长度读取后面的消息数据。
函数xMessageBufferReceive()返回的是实际读取的消息的字节数,不包括消息头的4字节。如果函数是因为等待超时而退出的,则返回值为0。
在ISR中从消息缓冲区读取消息的函数是xMessageBufferReceiveFromISR(),它是个宏函数,实际就是执行了函数xStreamBufferReceiveFromISR(),其原型定义如下:
/**
* \defgroup xMessageBufferReceiveFromISR xMessageBufferReceiveFromISR
* \ingroup MessageBufferManagement
*/
#define xMessageBufferReceiveFromISR( xMessageBuffer, pvRxData, xBufferLengthBytes, pxHigherPriorityTaskWoken ) xStreamBufferReceiveFromISR( ( StreamBufferHandle_t ) xMessageBuffer, pvRxData, xBufferLengthBytes, pxHigherPriorityTaskWoken )
(4)消息缓冲区状态查询
以下几个查询消息缓冲区状态的函数,只需使用消息缓冲区的句柄作为函数的输入参数。
- xMessageBufferIsEmpty()查询一个消息缓冲区是否为空,若返回pdTRUE,则表示缓冲区不包含任何消息。
- xMessageBufferIsFull()查询一个消息缓冲区是否已满,若返回pdTRUE,则表示不能再写入任何消息。
- xMessageBufferSpacesAvailable()查询一个消息缓冲区剩余的存储空间字节数,返回值类型为uint32_t。
三、消息缓冲区使用示例
1、示例功能与CubeMX项目设置
本示例演示消息缓冲区的使用,实例的功能和使用流程如下。
- 创建一个消息缓冲区和一个任务Task_Show。
- 使用RTC的唤醒中断,唤醒周期为1s。在RTC的唤醒中断里读取当前时间,转化为字符串后,作为消息写入消息缓冲区,每次写入的消息长度不一样。
- 在任务Task_Show里读取消息缓冲区的消息,并在串口助手上显示。
-
继续使用旺宝红龙开发板STM32F407ZGT6 KIT V1.0。
-
一些设置可以参考本文作者写的其他文章:
细说STM32单片机FreeRTOS流缓冲区及其应用实例-CSDN博客 https://wenchm.blog.csdn.net/article/details/148168854?spm=1011.2415.3001.5331
(1)RCC、SYS、Code Generator、USART3、TIM6
配置时钟树,将APB1定时器时钟频率设置为84MHz,APB2定时器时钟频率设置为168MHz ;设置TIM6作为基础时钟源;其它设置可见参考文章。
(2)RTC的设置
启用LSE,启用RTC,在时钟树上将LSE作为RTC的时钟源。启用周期唤醒功能,设置唤醒周期为1s,其他参数用默认值即可。
(3)FreeRTOS的设置
设置FreeRTOS接口为CMSIS_V2,所有“Config”和“Include”参数保持默认值。在FreeRTOS里创建一个任务Task_Show,其主要参数如图所示。
(4)NVIC
在NVIC里开启RTC唤醒中断,设置其中断优先级为5,因为要在其ISR里使用FreeRTOS API函数。
2、程序功能实现
(1)主程序
完成设置后,CubeMX自动生成代码。在CubeIDE中打开项目,添加用户功能代码后,主程序代码如下:
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "cmsis_os.h"
#include "rtc.h"
#include "usart.h"
#include "gpio.h"/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
void MX_FREERTOS_Init(void);/*** @brief The application entry point.* @retval int*/
int main(void)
{/* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* Configure the system clock */SystemClock_Config();/* Initialize all configured peripherals */MX_GPIO_Init();MX_RTC_Init();MX_USART3_UART_Init();/* USER CODE BEGIN 2 *///Start Menuuint8_t startstr[] = "Demo9_2:Using Message Buffer.\r\n\r\n";HAL_UART_Transmit(&huart3,startstr,sizeof(startstr),0xFFFF);/* USER CODE END 2 *//* Init scheduler */osKernelInitialize();/* Call init function for freertos objects (in cmsis_os2.c) */MX_FREERTOS_Init();/* Start scheduler */osKernelStart();/* We should never get here as control is now taken by the scheduler *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}
(2)FreeRTOS对象初始化
自动生成的函数MX_FREERTOS_Init()只创建了任务,在CubeMX里不能可视化地创建消息缓冲区,需要在CubeMX生成的CubeIDE初始代码的基础上,编程创建消息缓冲区。在文件freertos.c中定义两个常量和消息缓冲区对象,在函数MX_FREERTOS_Init()中增加创建消息缓冲区对象的代码。完成后的代码如下:
自动生成includes,并手动添加私有includes:
/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "message_buffer.h"
#include "usart.h"
#include <stdio.h> //用到函数sprintf()
#include <string.h> //用到函数strlen()
/* USER CODE END Includes */
手动添加私有宏定义:
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define MSG_BUFFER_LEN 50 //消息缓存区长度,单位:字节
#define MSG_MAX_LEN 20 //消息最大长度,单位:字节
/* USER CODE END PD */
手动添加创建消息缓冲区句柄变量代码;
自动生成任务函数句柄变量代码:
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */
MessageBufferHandle_t msgBuffer; //消息缓存区句柄变量
/* USER CODE END Variables */
/* Definitions for Task_Show */
osThreadId_t Task_ShowHandle;
const osThreadAttr_t Task_Show_attributes = {.name = "Task_Show",.stack_size = 256 * 4,.priority = (osPriority_t) osPriorityNormal,
};/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes *//* USER CODE END FunctionPrototypes */void AppTask_Show(void *argument);void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) *//*** @brief FreeRTOS initialization* @param None* @retval None*/
void MX_FREERTOS_Init(void)
{/* Create the thread(s) *//* creation of Task_Show */Task_ShowHandle = osThreadNew(AppTask_Show, NULL, &Task_Show_attributes);/* USER CODE BEGIN RTOS_THREADS *//* add threads, ... */msgBuffer=xMessageBufferCreate(MSG_BUFFER_LEN); //创建消息缓存区/* USER CODE END RTOS_THREADS */
}
(3)RTC的唤醒中断
在RTC的唤醒中断里读取当前时间,将其转换为字符串后写入消息缓冲区。RTC唤醒中断的回调函数是HAL_RTCEx_WakeUpTimerEventCallback()。为便于使用消息缓冲区句柄变量msgBuffer,在文件freertos.c中重新实现这个回调函数:
/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
{RTC_TimeTypeDef sTime;RTC_DateTypeDef sDate;if (HAL_RTC_GetTime(hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK)return;if (HAL_RTC_GetDate(hrtc, &sDate, RTC_FORMAT_BIN) !=HAL_OK)return;char dtArray[MSG_MAX_LEN]; //存储消息的数组, MSG_MAX_LEN=20if ((sTime.Seconds % 2)==0) //分奇偶秒,发送不同长度的消息字符串siprintf(dtArray,"Seconds = %u",sTime.Seconds); //转换为字符串,自动加'\0'elsesiprintf(dtArray,"Minute= %u",sTime.Minutes); //转换为字符串,自动加'\0'uint8_t bytesCount=strlen(dtArray); //字符串长度,不带最后的结束符BaseType_t highTaskWoken=pdFALSE;if (msgBuffer != NULL){uint16_t realCnt=xMessageBufferSendFromISR(msgBuffer,dtArray, bytesCount+1, &highTaskWoken); // bytesCount+1,带结束符'\0'printf("Write bytes= %d\r\n", realCnt); //实际写入消息长度portYIELD_FROM_ISR(highTaskWoken); //申请进行一次任务调度}
}int __io_putchar(int ch)
{HAL_UART_Transmit(&huart3,(uint8_t*)&ch,1,0xFFFF);return ch;
}
/* USER CODE END Application */
上述程序首先读取RTC的时间和日期,根据当前时间的秒数是奇数还是偶数,生成不同长度的字符串数据并保存到数组dtArray里。这里用到了C语言标准库中的两个函数siprintf()和strlen()。siprintf()与printf()类似,只是把字符串写入一个数组,并且在字符串最后自动添加结束符\0。strlen()用于得到字符串的长度,但是不包括最后的结束符。
在使用函数xMessageBufferSendFromISR()向消息缓冲区写入消息时,执行的代码如下:
uint16_t realCnt = xMessageBufferSendFromISR(msgBuffer,dtArray,bytesCount+1,&highTaskwoken);
这里传递的第3个参数值是bytesCount+1,也就是加上了字符串的结束符,否则,读取者读出的消息字符串将不带结束符,串口助手将无法正常显示字符串。bytesCount+1的值必须小于或等于MSG_MAX_LEN。
函数的返回值realCnt是实际写入的消息长度,不带消息头的4个字节。如果消息写入成功,那么realCnt等于bytesCount+1。
这里写入消息的数据是字符串,这只是为了演示方便,实际写入消息的数据可以是任意类型的数据,而不一定是字符串。
(4)任务Task_Show的功能
在任务Task_Show里读取消息缓冲区里的消息,并在串口助手上显示,其任务函数代码如下:
/* USER CODE BEGIN Header_AppTask_Show */
/*** @brief Function implementing the Task_Show thread.* @param argument: Not used* @retval None*/
/* USER CODE END Header_AppTask_Show */
void AppTask_Show(void *argument)
{/* USER CODE BEGIN AppTask_Show *//* Infinite loop */uint8_t dtArray[MSG_MAX_LEN]; //读出的数据临时保存数组for(;;){uint16_t realCnt=xMessageBufferReceive(msgBuffer, dtArray,MSG_MAX_LEN, portMAX_DELAY); //读取消息printf("Read message bytes = %d\r\n", realCnt); //实际读出字节数printf("message string Read = %s\r\n", dtArray); //显示读出的消息字符串}/* USER CODE END AppTask_Show */
}
上述程序用函数xMessageBufferReceive()读取消息缓冲区里的消息,然后在串口助手上显示实际读取的消息长度和消息字符串。调用函数xMessageBufferReceive()的代码如下:
uint16_t realCnt = xMessageBufferReceive(msgBuffer,dtArray,MSG_MAX_LEN,portMAX_DELAY);
其中,dtArray是用于存储读出数据的uint8_t类型数组,传递的第3个参数是MSG_MAX_LEN,也就是最大可以读取的消息的长度。函数返回值realCnt是实际读取的消息的长度,不包括消息头的4个字节。MSG_MAX_LEN应该大于或等于realCnt,否则,会导致无法读出一条完整的消息。
3、运行调试
构建项目后,下载到开发板上并运行测试,会发现显示的写入消息长度和读出消息长度是一致 的,串口助手上显示的消息字符串也是正确的,说明可以写入和读出不同长度的消息。在实际使用消息缓冲区时,写入者和读取者之间应该定义好消息的格式,如同串口通信一样定义通信协议。