写点什么

FreeRTOS 记录(六、FreeRTOS 消息队列—Enocean 模块串口通讯、RAM 空间不足问题分析)

作者:矜辰所致
  • 2022 年 9 月 13 日
    江苏
  • 本文字数:6498 字

    阅读完需:约 21 分钟

FreeRTOS记录(六、FreeRTOS消息队列—Enocean模块串口通讯、RAM空间不足问题分析)
本篇文章记录FreeRTOS消息队列的使用,不从理论开始介绍,直接用起来,然后从发现的问题分析记录解决。
复制代码


说明:FreeRTOS 专栏与我的 RT-Thread 专栏不同,我的 RT-Thread 专栏是从理论学习一步一步循序渐进,从 0 起步的 完整教学,而 FreeRTOS 更偏向于 我直接拿来使用,需要用到什么,然后引出知识点,在使用中发现问题,解然后再解决问题。


本 FreeRTOS 专栏记录的开发环境:

FreeRTOS 记录(一、熟悉开发环境以及 CubeMX 下 FreeRTOS 配置)

https://xie.infoq.cn/article/e82c7e10af242105b509bee2e

FreeRTOS 记录(二、FreeRTOS 任务 API 认识和源码简析)

https://xie.infoq.cn/article/250c67b1cc68b91241347f8e6

FreeRTOS 记录(三、RTOS 任务调度原理解析 _Systick、PendSV、SVC)

https://xie.infoq.cn/article/e0c671e5ca2d472ce40d272d4

FreeRTOS 记录(四、FreeRTOS 任务堆栈溢出问题和临界区)

https://xie.infoq.cn/article/d3f7d46aa262b2118a544f250

FreeRTOS 记录(五、FreeRTOS 任务通知)

https://xie.infoq.cn/article/bd3a7f06b5148b665893b6ac6


本文主要讲讲 FreeRTOS 消息队列的使用。

1、创建消息队列

在 CubemX 中,操作如下:



创建完毕生成代码,在代码中可以看到:


...osThreadId enoecantaskHandle;osMessageQId EnoceanQueueHandle;...void MX_FREERTOS_Init(void) {  /* USER CODE BEGIN Init */         /* USER CODE END Init */
/* USER CODE BEGIN RTOS_MUTEX */ /* add mutexes, ... */ /* USER CODE END RTOS_MUTEX */
/* USER CODE BEGIN RTOS_SEMAPHORES */ /* add semaphores, ... */ /* USER CODE END RTOS_SEMAPHORES */
/* USER CODE BEGIN RTOS_TIMERS */ /* start timers, add new ones, ... */ /* USER CODE END RTOS_TIMERS */
/* Create the queue(s) */ /* definition and creation of EnoceanQueue */ osMessageQDef(EnoceanQueue, 100, uint8_t); EnoceanQueueHandle = osMessageCreate(osMessageQ(EnoceanQueue), NULL); ... osThreadDef(enoecantask, StartenoecanTask, osPriorityIdle, 0, 192); enoecantaskHandle = osThreadCreate(osThread(enoecantask), NULL); /* USER CODE BEGIN RTOS_THREADS */ /* add threads, ... */ __HAL_UART_ENABLE_IT(&hlpuart1,UART_IT_RXNE); /* USER CODE END RTOS_THREADS */
复制代码


在上面代码void MX_FREERTOS_Init(void)最后一部分我加入了__HAL_UART_ENABLE_IT(&hlpuart1,UART_IT_RXNE);串口中断使能,开启串口接收。


为什么在MX_FREERTOS_Init最后加入串口中断使能,是为了防止先使能了串口中断,如果操作系统还没有开始调度之前有中断发送,在中断中有消息队列的入队处理,那么是有可能出问题的。

2、中断中发送消息

我们使用消息队列接收串口的数据,那么需要在stm32l0xx_it.c文件中相关串口的中断处理函数进行消息队列的入队操作:stm32l0xx_it.c


...#include "cmsis_os.h".../* USER CODE BEGIN EV */extern osMessageQId EnoceanQueueHandle;/* USER CODE END EV */
.../** * @brief This function handles LPUART1 global interrupt / LPUART1 wake-up interrupt through EXTI line 28. */void LPUART1_IRQHandler(void){ u8 res; /* USER CODE BEGIN LPUART1_IRQn 0 */ if(__HAL_UART_GET_FLAG(&hlpuart1,UART_FLAG_RXNE) == SET){ res = hlpuart1.Instance->RDR;
xQueueSendFromISR(EnoceanQueueHandle,&res,NULL); } /* USER CODE END LPUART1_IRQn 0 */ HAL_UART_IRQHandler(&hlpuart1); /* USER CODE BEGIN LPUART1_IRQn 1 */
/* USER CODE END LPUART1_IRQn 1 */}
复制代码

2.1 操作寄存器接收串口数据

上面代码中可以看到,使用的是 M0 的内核,操作 ISR 和 RDR 寄存器:



如果是 M3、M4 的内核,操作 SR 和 DR 寄存器:



2.1 中断中入队

在串口中断中,使用了xQueueSendFromISR(EnoceanQueueHandle,&res,NULL);向消息队列中发送数据;



<font color=#0033FF>为什么使用xQueueSendFromISR而不用osMessagePut。</font>


在 CubeMX 中,封装好的消息发送函数为osMessagePut,和其他一样,封装好的会自动判断是否在中断中发送,自动引用xQueueSendFromISR或者xQueueSend函数,源码如下:


/*** @brief Put a Message to a Queue.* @param  queue_id  message queue ID obtained with \ref osMessageCreate.* @param  info      message information.* @param  millisec  timeout value or 0 in case of no time-out.* @retval status code that indicates the execution status of the function.* @note   MUST REMAIN UNCHANGED: \b osMessagePut shall be consistent in every CMSIS-RTOS.*/osStatus osMessagePut (osMessageQId queue_id, uint32_t info, uint32_t millisec){  portBASE_TYPE taskWoken = pdFALSE;  TickType_t ticks;    ticks = millisec / portTICK_PERIOD_MS;  if (ticks == 0) {    ticks = 1;  }    if (inHandlerMode()) {    if (xQueueSendFromISR(queue_id, &info, &taskWoken) != pdTRUE) {      return osErrorOS;    }    portEND_SWITCHING_ISR(taskWoken);  }  else {    if (xQueueSend(queue_id, &info, ticks) != pdTRUE) {      return osErrorOS;    }  }    return osOK;}
复制代码


但是注意!!!osMessagePut 的第二个参数为uint32_t 类型,所以只有当定义的消息Item Sizeuint32_t 时候才能使用,否则消息会出错!

3、消息队列函数形参分析

3.1 关于 void *p

从源码可知,消息队列接收中定义了一个osEvent event;


osEvent osMessageGet (osMessageQId queue_id, uint32_t millisec){  portBASE_TYPE taskWoken;  TickType_t ticks;  osEvent event;
复制代码


我们在上一篇文章FreeRTOS记录(五、FreeRTOS任务通知) 中的二、任务通知使用 章节的 3、接收通知 小节 用到过osEvent类型,给出了结构体的定义。


其中结构体中关于 value 是一个联合体,比如如果是 uint32_t 类型的数据,直接使用 value.v 读取,但如果是一个地址,而且可能是不同的数据类型,就得使用到 void *p :



同样的,我们在xQueueReceivexQueueGenericSend里面,第二个形参也使用了void *类型:


BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait )......BaseType_t xQueueGenericSend( QueueHandle_t xQueue, const void * const pvItemToQueue, TickType_t xTicksToWait, const BaseType_t xCopyPosition )
复制代码


这里要说明的是,void *可以指向任何类型的数据!


来看看我们经常使用的memset原型,是不是能更好的理解:


void *memset(void *s, int ch, size_t n);//将s中当前位置后面的n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s 
复制代码


所以消息队列这里使用表示他可以发送和接收任意类型的数据,也就是消息类型,从简单的数据,到结构体,二维数组等消息都是可以传递的。

3.2 关于 void * const pvBuffer 和 const void * const pvItemToQueue

对于第一个void * const pvBuffer:const 后面紧跟的是 pvBuffer , pvBuffer const 类型的,不可变(这里是指的某个地址不可变)但是 *pvBuffer 可变(该地址的数据是可变的)消息队列接收的时候使用这个定义的形参;


对应的举个例子,如果是const void *pvBuffer:*pvBuffer 是 const ,const 后面紧跟的是 void,所以 *pvBuffer 可能是任意类型,但是这个数据不可变。


对于第二个const void * const pvItemToQueue:pvItemToQueue 和 *pvItemToQueue 都是不可变的(这个地址不可变,这个地址上的数据不可变)消息队列发送的时候使用这个定义实参;

4、数据接收处理

4.1 在任务中接收

在任务中接收消息,因为要保存到数组里面,使用了xQueueReceive函数:


...uint8 USART_Enocean_BUF[100];uint8 Enocean_Data = 0;       //数据长度记录   .../* USER CODE END Header_StartenoecanTask */void StartenoecanTask(void const * argument){  /* USER CODE BEGIN StartenoecanTask */  /* Infinite loop */  for(;;)  {    if(xQueueReceive(EnoceanQueueHandle,&USART_Enocean_BUF[Enocean_Data++],portMAX_DELAY) == pdPASS){      while(xQueueReceive(EnoceanQueueHandle,&USART_Enocean_BUF[Enocean_Data++],15));      HAL_UART_Transmit(&huart1,USART_Enocean_BUF, Enocean_Data,0xFFFF); //将串口3接收到的数据通过串口1传出       memset(USART_Enocean_BUF,0,sizeof(USART_Enocean_BUF));      Enocean_Data=0;    }    // osDelay(1);  }
复制代码


在 CubeMX 中,封装好的消息发送函数为osMessageGet ,和其他一样,封装好的会自动判断是否在中断中,自动引用xQueueReceiveFromISR或者xQueueReceive函数,源码如下:


/*** @brief Get a Message or Wait for a Message from a Queue.* @param  queue_id  message queue ID obtained with \ref osMessageCreate.* @param  millisec  timeout value or 0 in case of no time-out.* @retval event information that includes status code.* @note   MUST REMAIN UNCHANGED: \b osMessageGet shall be consistent in every CMSIS-RTOS.*/osEvent osMessageGet (osMessageQId queue_id, uint32_t millisec){  portBASE_TYPE taskWoken;  TickType_t ticks;  osEvent event;    event.def.message_id = queue_id;  event.value.v = 0;    if (queue_id == NULL) {    event.status = osErrorParameter;    return event;  }    taskWoken = pdFALSE;    ticks = 0;  if (millisec == osWaitForever) {    ticks = portMAX_DELAY;  }  else if (millisec != 0) {    ticks = millisec / portTICK_PERIOD_MS;    if (ticks == 0) {      ticks = 1;    }  }    if (inHandlerMode()) {    if (xQueueReceiveFromISR(queue_id, &event.value.v, &taskWoken) == pdTRUE) {      /* We have mail */      event.status = osEventMessage;    }    else {      event.status = osOK;    }    portEND_SWITCHING_ISR(taskWoken);  }  else {    if (xQueueReceive(queue_id, &event.value.v, ticks) == pdTRUE) {      /* We have mail */      event.status = osEventMessage;    }    else {      event.status = (ticks == 0) ? osOK : osEventTimeout;    }  }    return event;}
复制代码


需要把数据保存至我们自己定义的数组中,所以使用了xQueueReceive函数。


然后等待 15ms,确保收到的是一帧完整的数据。接收完一帧数据,通过串口 1 打印出来,然后清空数据。

4.2 数据解析

根据上面的代码,通过串口助手测试看看效果,发现有下面的问题,最后会多出来一位, 但是每次都是一帧数据正常发送(很简单的问题,仔细看一下代码就知道问题所在了):



问题的原因很简单,如下图:



然后数据解析函数直接用以前的驱动包,需要稍微修改一下函数:



测试效果,成功:



至此,使用消息队列 串口接收 不定长度的数据测试完成,结果也比较理想。


另外提一下,Enocean 除了接收,发送可以直接用以前写好的函数,直接在需要的任务中调用:


4.3 缓存大小问题

在测试中,本来为了解决 RAM 使用空间,想把USART_Enocean_BUF[100] 数组大小设置为 50,因为最大的一帧数据也只有在读 ID 的时候 44 个字节,每次读取完成都会把数组清 0。所以觉得数组 大小 50 足够使用了,但是实际上测试下来发现,50,甚至是 80 90 的数组大小都不够使用,接收任务会出问题,具体原因一下子还不明白。


这个缓存问题,按理来说,80、90 字节 应该也够的,我试着把消息队列的大小也定义成缓存一样大大小,还是不行,这个问题后面如果发现再来补充!(未解决)

5 、RAM 空间不足问题

内存空间不足 小结写在前面:


  • FreeRTOS 定义的 TOTAL_HEAP_SIZE,直接在内存占用这么大的空间,编译过后的.bss 段直接增加对应大小;


  • 系统的 Minimum Heap Size 和 Minimum Stack Size ,(也可以认为)直接需要占用这么大的空间;


  • 如果 FreeRTOS 使能了定时器,定时器启动以后会有一个任务:


  • 虽然前面申请了 TOTAL_HEAP_SIZE 空间,图中这个任务Tmr Svc会占用 TOTAL_HEAP_SIZE 空间内容

  • 注意!!除了任务所占用的申请过的空间不用计算内存,开启软件定时器之后还会额外占用.bss 段


  • Include definitions里面使能了需要的定义,需要占用一点.bss 段


  • 消息队列肯定也是要占用额外的.bss 段的(这里已经使用上了,我就不重新去改掉,后面遇到再来维护)

5.1 问题的出现

这次测试使用的是 STM32L051C8,8KB 的 RAM,64KB 的 Flash,Flash 还是够用的,但是 8KB 的 RAM 使用起来就有点捉襟见肘,在中途编译的时候就已经发现:




具体如何计算我有一篇博文单独介绍 内存问题:

STM32 的内存管理相关(内存架构,内存管理,map 文件分析)

https://xie.infoq.cn/article/625b2009e810086435349be30

包括本文后面的一些分析内存部分,也需要参考上面这篇博文的内容。


果然,在接下来 RAM 空间不够了:



原因是由于我发现 FreeRTOS 内存可用字节数不够了:



于是我把 FreeRTOS 可用的内存空间TOTAL_HEAP_SIZE修改大了:



我改大了 1KB, 开始我们编译已经看到用了大概 7.7KB,所以这么一加上去,RAM 空间不够用,提示溢出 616 bytes,


那么我试一下,如果我减少 200 bytes,那么他编译溢出是不是就只有 416 bytes 了:



确实如此,但是注意!!!这个值需要是 1024 的倍数,这是只是单纯分配大小测试 RAM 的占用情况


那么在小容量的 MCU 上运行 RTOS,我们就得非常注意这个内存空间的使用,应该合理的划分了,那么如何能够合理的划分内存空间,除了需要知道 STM32 的内存管理相关 的内容,还需要对 FreeRTOS 任务如何占用内存空间需要一定的了解。

5.2 FreeRTOS 任务占用的 RAM 空间

通过上面我们知道,FreeRTOS TOTAL_HEAP_SIZE 是直接在 RAM 里面划分空间的,那么这个TOTAL_HEAP_SIZE 占用的空间是在 RAM 的什么位置呢?


如果了解 FreeRTOS 任务创建原理相关的知识,这点不难回答,我们这里可以用一种简单的办法告诉答案,通过.map 文件,还是在 《STM32 的内存管理相关(内存架构,内存管理,map 文件分析)》文章中最后一节,GCC 下.map 文件中有相关解释,这里再次说明一遍:



在 Cubemx 中设置的TOTAL_HEAP_SIZE大小,直接占用的是上图部分 RAM 空间的大小,结尾的地方是.bss.heap_end.4167 0x200010c0 。FreeRTOS 每一个任务都在这个开辟 ,我们选中的是heap_4.c内存管理方案,在heap_4.c文件里面是有申请内存的操作:




我们这里需要说明的是:

FreeRTOS 申请的内存空间在 RAM 的位置属于.bss 段,FreeRTOS 创建的每个任务都是在开始申请的这段内存空间中创建的,每个 FreeRTOS 任务在这段空间中都有自己的栈空间。

在创建任务的时候我们申请的任务大小就是申请任务的栈空间,但是这个栈空间与系统的栈空间是不一样的,在.map 文件的最后:



系统的栈空间是用来处理中断发生,系统函数调用的现场保存的,而 FreeRTOS 的任务有每个任务单独的自己的栈空间来保存任务的现场,不要搞混淆了。


简单画了一张图:


5.3 问题的解决办法

那既然遇到这个问题,该如何解决呢?

5.3.1 修改系统 Heap Size

首先我们得知道内存分配的相关知识,我们知道 系统的堆空间 Heap Size 默认 0x200,而且在程序中如果基本不用 malloc 动态分配空间,那么这个 Heap Size 是可以不用的,但是不能粗暴的设置为 0,部分 C 库函数还要用,一定要分配一定大小。



5.3.2 了解 FreeRTOS 任务占用大小计算

除了修改Heap Size ,FreeRTOS 任务的大小和任务的分配需要好好考虑一下,可以在产品测试的时候先通过vTaskList查看每个任务需要使用的内存大小,再确定分配多少内存,如何查看,请参考另一篇博文:


FreeRTOS 记录(四、FreeRTOS 任务堆栈溢出问题和临界区)

https://xie.infoq.cn/article/d3f7d46aa262b2118a544f250


这里直接给个说明:


FreeRTOS 任务占用内存大小 取决于 局部变量 和 调用深度。


学会合理的分配内存空间是很有必要的,FreeRTOS 消息队列 用作串口通讯的测试就到这里,后续有关系消息队列的知识再来补充。

发布于: 刚刚阅读数: 5
用户头像

矜辰所致

关注

CSDN、知乎、微信公众号: 矜辰所致 2022.08.02 加入

不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开! 为了活下去的嵌入式工程师,画画板子,敲敲代码,玩玩RTOS,搞搞Linux ...

评论

发布
暂无评论
FreeRTOS记录(六、FreeRTOS消息队列—Enocean模块串口通讯、RAM空间不足问题分析)_内存管理_矜辰所致_InfoQ写作社区