当前位置: 首页 > java >正文

【工具使用】STM32CubeMX-FreeRTOS操作系统-内存池、消息队列、邮箱篇

一、概述

    无论是新手还是大佬,基于STM32单片机的开发,使用STM32CubeMX都是可以极大提升开发效率的,并且其界面化的开发,也大大降低了新手对STM32单片机的开发门槛。
    本文主要讲述STM32芯片FreeRTOS内存池、消息队列、邮箱功能的配置及其相关知识。

二、软件说明

    STM32CubeMX是ST官方出的一款针对ST的MCU/MPU跨平台的图形化工具,支持在Linux、MacOS、Window系统下开发,其对接的底层接口是HAL库,另外习惯于寄存器开发的同学们,也可以使用LL库。STM32CubeMX除了集成MCU/MPU的硬件抽象层,另外还集成了像RTOS,文件系统,USB,网络,显示,嵌入式AI等中间件,这样开发者就能够很轻松的完成MCU/MPU的底层驱动的配置,留出更多精力开发上层功能逻辑,能够更进一步提高了嵌入式开发效率。
    演示版本 6.1.0

三、FreeRTOS功能简介

    嵌入式初学者一般使用的是裸机开发,而大多数所谓的进阶课程,就是使用操作系统开发。其实两者并不存在很大的差距,使用操作系统,更多是在裸机开发的基础上,限制在操作系统要求的框架下进行开发,同时需要留意操作系统的一些特性,以防止出现问题。具体操作系统开发与裸机开发的区别如下:

维度FreeRTOS(RTOS 开发)裸机开发(轮询 / 中断驱动)
任务管理多任务并行(抢占式 / 协作式),自动调度单线程轮询 + 中断处理,手动协调任务优先级
实时性高(可精确控制任务执行顺序和响应时间)依赖中断优先级和轮询顺序,复杂场景易卡顿
系统复杂度适合复杂逻辑(如多外设、通信协议、用户界面)适合简单逻辑(如单一传感器采集、LED 控制)
代码结构模块化(任务独立,通过 IPC 通信)线性代码 + 全局变量,耦合度高
资源占用需额外内存(栈空间、内核数据结构)资源占用极小(仅代码和必要数据)
开发成本学习成本较高(需理解 RTOS 概念)门槛低,适合快速实现简单功能
可维护性任务隔离性好,扩展新功能更方便功能扩展可能需修改全局逻辑,维护困难

    提到操作系统,第一反应也是大家最早接触的,应该就是Windows系统了(当然新生代可能第一接触的是苹果的IOS系统或华为的鸿蒙系统),但由于Windows的交互友好性,让大家很难感知到它的存在;而学习了嵌入式后,又知道了Linux这个天花板般的操作系统存在,虽然是个开源系统,但其体量也让大多数人忘而生畏,从而使操作系统蒙上一层神秘的面纱。而今天的主角FreeRTOS,作为小体量且开源的操作系统,正是敲开操作系统神秘大门的砖头,带着我们了解其中的奥妙。

    FreeRTOS 是一款开源实时操作系统(RTOS),专为嵌入式系统设计,尤其适用于资源受限的微控制器(MCU)。由 Richard Barry 开发并发布首个版本(V1.0),最初名为 FreeRTOS Kernel,旨在提供轻量级、可移植的多任务处理能力。早期版本以代码简洁、易于移植为特点,迅速在嵌入式社区流行。2012 年,成立 Real Time Engineers Ltd 公司,推动 FreeRTOS 商业化,推出付费技术支持和扩展组件(如文件系统、TCP/IP 栈)。逐步支持更多硬件平台(如 ARM Cortex-M、ESP32、RISC-V 等),并构建生态系统,包括中间件和工具链。2016 年,亚马逊(AWS)收购 FreeRTOS,将其纳入 IoT 战略,推出 AWS IoT Greengrass for FreeRTOS,强化物联网(IoT)连接能力(如 MQTT、OTA 升级)。最新版本(截至 2025 年)为 V202212.00,持续优化实时性能、安全性和云集成,并提供免费的认证服务(如功能安全认证 IEC 61508)。

    FreeRTOS 以实时性、轻量级、可配置性为核心,功能模块包括:

  1. 任务调度器
  • 抢占式调度:支持多任务按优先级运行,高优先级任务可中断低优先级任务,确保实时响应。
  • 协作式调度(可选):任务主动释放控制权,适合对实时性要求不高的场景。
  • 支持 任务优先级(最多 32 级,可配置)和 时间片轮转(同优先级任务分时执行)。
  1. 任务间通信(IPC)
  • 队列(Queue):任务 / 中断间传递数据,支持先进先出(FIFO)或优先级队列。
  • 信号量(Semaphore):包括二进制信号量、计数信号量,用于资源同步与互斥(如 mutex 互斥信号量)。
  • 事件组(Event Group):实现任务间多事件同步。
  1. 内存管理
  • 提供多种内存分配策略:
    – 静态分配:编译时分配固定内存,适合关键任务,避免内存碎片。
    – 动态分配:运行时动态申请内存(类似 C 语言 malloc),需注意碎片问题。
  • 支持用户自定义内存管理方案。
  1. 定时器与中断管理
  • 软件定时器:基于系统时钟的周期性或一次性定时器,支持回调函数。
  • 中断安全接口:允许在中断服务程序(ISR)中安全访问 RTOS 资源(如队列、信号量)。
  1. 可配置性与移植性
  • 通过头文件(FreeRTOSConfig.h)配置内核参数(如任务数量、栈大小、调度器行为),灵活适配不同硬件。
  • 提供标准接口,移植到新平台只需实现少量汇编代码(如上下文切换)。
  1. 生态与扩展组件
  • 中间件:集成文件系统(如 FATFS)、TCP/IP 栈(LwIP)、USB 协议栈、图形界面(GUI)等。
  • 物联网支持:通过 AWS IoT 组件实现设备与云端通信,支持 MQTT、TLS 加密、设备管理等。
  • 安全与认证:提供功能安全版本(如 FreeRTOS-Safety),通过 IEC 61508、ISO 26262 等认证,适合工业、汽车电子等场景。

    除了FreeRTOS,其他还有很多其他优秀的嵌入式操作系统,其中就包括很多人学校里会学到的uC/OS-II,FreeRTOS与其他常见的嵌入式操作系统对比如下:

维度FreeRTOSuC/OS-IIRTX(ARM)RIOT OS
许可证开源(GPLv2,修改需开源)/ 商业许可开源(需购买商业许可用于产品)商业许可(需授权)开源(BSD-2-Clause)
实时性抢占式调度,微秒级响应抢占式调度,支持优先级继承抢占式调度,支持 CMSIS-RTOS 标准抢占式 + 协作式,适合 IoT 低功耗
代码复杂度简洁,核心代码约 10k 行 C 语言模块化设计,代码量较大集成于 Keil 工具链,抽象层完善面向 IoT,轻量级(<10KB 内存)
生态系统丰富(AWS IoT、中间件、社区支持)成熟(工业、汽车领域案例多)与 ARM 工具链深度整合(Keil、IAR)专注 IoT,支持传感器网络和低功耗协议
资源占用极小(ROM: ~4KB,RAM: ~1KB)中等(ROM: ~10KB,RAM: ~2KB)中等(依赖组件数量)极轻量(适合 8/16 位 MCU)
典型应用IoT 设备、消费电子、工业控制医疗设备、航空航天、汽车电子嵌入式系统开发(ARM Cortex-M 系列)物联网边缘设备、传感器节点

    FreeRTOS 凭借轻量、开源、易移植的特性,成为嵌入式领域最流行的 RTOS 之一,尤其适合 IoT 和中小型实时系统。其与 AWS 的深度整合进一步强化了物联网能力,而功能安全认证版本则拓展了工业和汽车电子市场。相比裸机开发,它能显著提升复杂系统的设计效率和实时性,但需权衡资源占用和学习成本。对于开发者而言,若项目需要多任务调度、实时响应或未来扩展,FreeRTOS 是理想选择;若需求简单或资源受限,裸机开发仍具优势。

四、FreeRTOS配置及应用

4.1 FreeRTOS配置

FreeRTOS配置
    具体参考《【工具使用】STM32CubeMX-FreeRTOS操作系统-任务、延时、定时器篇》,这里就不再赘述了。

4.2 接口说明

    使用CubeMX生成的工程,会将FreeRTOS的接口再封装一层统一接口CMSIS-RTOS,这是 ARM 定义的一套 RTOS 抽象层标准,旨在通过统一接口屏蔽不同 RTOS 的差异。这里我们先来认识几个比较常用的接口。

4.2.1 内存池

1. 内存池定义宏 (osPoolDef)

#define osPoolDef(name, no, type) ...

    此宏用于定义一个内存池对象。
参数:
name:内存池名称(用于生成唯一标识符)
no:内存池中可分配的最大块数
type:每个内存块的数据类型(决定块大小)
宏展开后会创建一个osPoolDef_t类型的结构体,包含块数量、块大小等信息。例如:

osPoolDef(myPool, 10, uint32_t);  // 定义一个包含10个uint32_t大小块的内存池

2. 内存池访问宏 (osPool)

#define osPool(name) &os_pool_def_##name

    该宏用于获取内存池定义的指针,返回osPoolDef_t类型的结构体指针,后续可用于创建内存池。例如:

osPoolId poolId = osPoolCreate(osPool(myPool));  // 创建内存池

3. 创建内存池 (osPoolCreate)

osPoolId osPoolCreate (const osPoolDef_t *pool_def);

    创建并初始化一个内存池。参数pool_def是通过osPool宏获取的内存池定义指针。成功时返回内存池 ID(非 NULL),失败时返回 NULL。

4. 分配内存块 (osPoolAlloc)

void *osPoolAlloc (osPoolId pool_id);

    从指定内存池中分配一个内存块。参数pool_id是内存池 ID。成功时返回分配的内存块地址,失败(如内存池已满)时返回 NULL。分配的内存块内容是未初始化的。

5. 分配并清零内存块 (osPoolCAlloc)

void *osPoolCAlloc (osPoolId pool_id);

    功能与osPoolAlloc类似,但分配后会将内存块内容清零。这在需要干净内存的场景中很有用,例如初始化结构体或缓冲区。

6. 释放内存块 (osPoolFree)

osStatus osPoolFree (osPoolId pool_id, void *block);

    将之前分配的内存块归还给内存池。
参数:
pool_id:内存池 ID
block:要释放的内存块地址
返回osStatus类型的状态码,表示操作成功或失败。注意:必须释放由同一内存池分配的有效内存块,否则会导致错误。

4.2.2 消息队列

    消息队列是 RTOS 中实现 任务间异步通信 的核心机制,主要特点包括:
FIFO(先进先出):消息按发送顺序存储和读取。
固定大小:每个消息通常为固定长度(如 uint32_t),适合传递简单数据或状态标志。
阻塞机制:发送 / 接收消息时可设置超时,无数据或队列满时自动阻塞。

1. 消息队列定义宏(osMessageQDef 和 osMessageQ)

#if defined (osObjectsExternal)  // 外部声明(如在头文件中)
#define osMessageQDef(name, queue_sz, type)   \
extern const osMessageQDef_t os_messageQ_def_##name
#else  // 本地定义#if (configSUPPORT_STATIC_ALLOCATION == 1)  // 静态内存分配#define osMessageQDef(name, queue_sz, type)   \const osMessageQDef_t os_messageQ_def_##name = { (queue_sz), sizeof(type), NULL, NULL }#define osMessageQStaticDef(name, queue_sz, type, buffer, control)   \const osMessageQDef_t os_messageQ_def_##name = { (queue_sz), sizeof(type), (buffer), (control) }#else  // 动态内存分配(默认)#define osMessageQDef(name, queue_sz, type)   \const osMessageQDef_t os_messageQ_def_##name = { (queue_sz), sizeof(type) }#endif
#endif#define osMessageQ(name)  \
&os_messageQ_def_##name  // 获取队列定义指针

    声明或定义消息队列对象的结构体(osMessageQDef_t)。
参数:
name:队列名称(用于生成唯一标识符,如 myQueue)。
queue_sz:队列最大消息数(如 10 表示最多存储 10 条消息)。
type:消息数据类型(仅用于调试,实际存储为 uint32_t)。
buffer/control(静态分配):预分配的缓冲区和控制块地址,适用于内存受限场景。
示例:

// 定义一个最大存储 5 条消息、消息类型为 uint32_t 的队列
osMessageQDef(myQueue, 5, uint32_t);  

2. 创建消息队列(osMessageCreate)

osMessageQId osMessageCreate(const osMessageQDef_t *queue_def, osThreadId thread_id);

    初始化消息队列,分配内存并设置队列参数。
参数:
queue_def:队列定义指针(通过 osMessageQ(name) 获取)。
thread_id:接收消息的目标线程 ID(可选,通常设为 NULL,由任意线程接收)。
返回值:
成功:队列 ID(非 NULL)。
失败:NULL(如内存不足或参数无效)。
示例:

osMessageQId queue_id = osMessageCreate(osMessageQ(myQueue), NULL);

3. 发送消息(osMessagePut)

osStatus osMessagePut(osMessageQId queue_id, uint32_t info, uint32_t millisec);

    向队列中发送一条消息(拷贝 info 的值到队列)。
参数:
queue_id:队列 ID(由 osMessageCreate 返回)。
info:消息内容(uint32_t 类型,如状态码、数值等)。
millisec:超时时间(毫秒):
0:非阻塞发送,立即返回。
osWaitForever:无限等待直到队列有空间(需 RTOS 支持)。
返回值:
osOK:发送成功。
osErrorTimeout:超时未发送(队列满且等待超时)。
osErrorParameter:参数无效(如 queue_id 为 NULL)。
逻辑示例:

// 发送消息 0x1234 到队列,最多等待 100 毫秒
if (osMessagePut(queue_id, 0x1234, 100) == osOK) {// 消息发送成功
} else {// 处理发送失败(如队列满)
}

4. 接收消息(osMessageGet)

osEvent osMessageGet(osMessageQId queue_id, uint32_t millisec);

    从队列中接收一条消息,若无消息则阻塞等待。
参数:
queue_id:队列 ID。
millisec:超时时间(同上)。
返回值:
osEvent 结构体:

  • status:状态码(osEventMessage 表示成功接收,osEventTimeout 表示超时)。
  • value.v:消息内容(uint32_t 类型)。
特性消息队列邮箱(Mailbox)信号量
数据类型固定长度(uint32_t)指针(通常传递结构体地址)无数据(仅计数 / 互斥)
队列特性FIFO通常 FIFO无队列
典型用途传递简单状态、数值传递大块数据或复杂结构体资源计数、互斥访问
阻塞机制发送 / 接收均支持超时接收支持超时等待支持超时

4.2.3 邮箱

1. 邮件队列定义宏(osMailQDef 和 osMailQ)

#if defined (osObjectsExternal)  // 外部声明
#define osMailQDef(name, queue_sz, type) \
extern struct os_mailQ_cb *os_mailQ_cb_##name \
extern osMailQDef_t os_mailQ_def_##name
#else  // 本地定义
#define osMailQDef(name, queue_sz, type) \
struct os_mailQ_cb *os_mailQ_cb_##name; \
const osMailQDef_t os_mailQ_def_##name =  \
{ (queue_sz), sizeof(type), (&os_mailQ_cb_##name) }
#endif#define osMailQ(name)  \
&os_mailQ_def_##name  // 获取队列定义指针

    定义邮件队列的元数据(如队列大小、单个邮件数据类型)和内部控制块(os_mailQ_cb)。
参数:
name:队列名称(如 myMailQ)。
queue_sz:队列最大邮件数(即同时可存储的指针数量)。
type:邮件数据类型(如 struct MyData),用于计算内存块大小。
内部机制:每个邮件队列关联一个 内存池,用于分配 / 回收内存块(os_mailQ_cb 指向内存池控制块)。
示例:

// 定义一个最大存储 3 封邮件、邮件类型为 struct SensorData 的队列
typedef struct {int temp;float humi;
} SensorData;osMailQDef(sensorMailQ, 3, SensorData);  

2. 创建邮件队列(osMailCreate)

osMailQId osMailCreate(const osMailQDef_t *queue_def, osThreadId thread_id);

    初始化邮件队列及其关联的内存池,分配所需内存。
参数:
queue_def:队列定义指针(通过 osMailQ(name) 获取)。
thread_id:目标线程 ID(可选,通常设为 NULL,允许任意线程接收)。
返回值:
成功:邮件队列 ID(非 NULL)。
失败:NULL(如内存不足或参数无效)。
示例:

osMailQId mail_id = osMailCreate(osMailQ(sensorMailQ), NULL);

3. 分配邮件内存块(osMailAlloc 和 osMailCAlloc)

void *osMailAlloc(osMailQId queue_id, uint32_t millisec);
void *osMailCAlloc(osMailQId queue_id, uint32_t millisec);

    从邮件队列关联的内存池中分配一个内存块,用于填充邮件数据。
参数:
queue_id:邮件队列 ID。
millisec:超时时间(毫秒),用于等待内存块可用(类似信号量等待)。
返回值:
成功:内存块指针(void*,可强制转换为目标类型)。
失败:NULL(如内存池无空闲块或超时)。
区别:
osMailAlloc:分配的内存块内容未初始化。
osMailCAlloc:分配后自动清零内存块(适用于结构体初始化)。
示例:

SensorData *mail_data = (SensorData*)osMailAlloc(mail_id, osWaitForever);
if (mail_data != NULL) {mail_data->temp = 25;mail_data->humi = 60.5f;
}

4. 发送邮件(osMailPut)

osStatus osMailPut(osMailQId queue_id, void *mail);

    将填充好数据的内存块放入邮件队列,供其他线程接收。
参数:
queue_id:邮件队列 ID。
mail:待发送的内存块指针(必须是通过 osMailAlloc/osMailCAlloc 分配的)。
返回值:
osOK:发送成功(内存块被加入队列)。
osError:失败(如队列满或指针无效)。
注意:发送后,内存块所有权转移至队列,发送方不再拥有该指针,需等待接收方处理并释放。

5. 接收邮件(osMailGet)

osEvent osMailGet(osMailQId queue_id, uint32_t millisec);

    从邮件队列中获取一封邮件(内存块指针)。
参数:
queue_id:邮件队列 ID。
millisec:超时时间(毫秒)。
返回值:
osEvent 结构体:
.status:状态码(osEventMail 表示成功接收,osEventTimeout 表示超时)。
.value.p:内存块指针(void*,需强制转换为目标类型)。
示例:

osEvent evt = osMailGet(mail_id, osWaitForever);
if (evt.status == osEventMail) {SensorData *received_data = (SensorData*)evt.value.p;process_data(received_data);  // 处理数据osMailFree(mail_id, received_data);  // 处理完成后释放内存块
}

6. 释放邮件内存块(osMailFree)

osStatus osMailFree(osMailQId queue_id, void *mail);

    将使用完毕的内存块归还给邮件队列的内存池,供后续重复使用。
参数:
queue_id:邮件队列 ID。
mail:待释放的内存块指针(必须是通过 osMailGet 获取的)。
返回值:
osOK:释放成功。
osError:失败(如指针不属于该队列或已释放)。

特性消息队列(Message Queue)邮件队列(Mail Queue)
数据类型固定长度值(uint32_t)指针(指向动态分配的内存块)
典型用途传递简单状态、数值(如命令码)传递复杂数据结构(如结构体、缓冲区)
内存管理内部存储值,无需手动分配内存结合内存池,需手动分配 / 释放内存块
数据大小限制受限于 uint32_t(4 字节)仅受内存池块大小限制(灵活)

4.3 应用配置

    首先需要配置ADC功能,用来计算单片机内部温度,具体配置及说明可以参考《【工具使用】STM32CubeMX-单ADC模式规则通道配置》。然后是配置FreeRTOS,全部都按默认配置即可,这里为了演示线程间的交互,需要再创建一个线程。
在这里插入图片描述

4.4 代码实现

4.4.1 邮箱+内存池

    在两个线程运行周期不对等的情况下,比如串口收发数据,收得快,发得慢,可以用消息队列或邮箱来做缓冲池,把待发送的数据先缓存下来。这里就用两个线程来模拟这种效果,一个是ADC采集的线程,用来采集并计算单片机内部温度,并通过邮箱发送至另一个线程,这里接收的线程是无限等待的,而采集的线程设定1s采集一次。刚好可以同时用上内存池和邮箱。

/* USER CODE BEGIN 4 */
/* 邮箱定义(用于ADC数据传递) */
typedef struct {uint32_t adc_raw;      // ADC原始值float temperature;     // 计算后的温度值(℃)uint32_t timestamp;    // 采样时间戳
} AdcData_t;osMailQDef(AdcMailQ, 5, AdcData_t);  // 邮箱队列:最多5条消息
osMailQId AdcMailQHandle;/* 内存池定义(用于循环存储) */
#define STORAGE_POOL_SIZE 10        // 内存池大小(存储10个数据点)
osPoolDef(StoragePool, STORAGE_POOL_SIZE, AdcData_t);  // 内存池定义
osPoolId StoragePoolHandle;/* 循环缓冲区控制结构 */
typedef struct {uint32_t head;                  // 写入位置uint32_t tail;                  // 读取位置uint32_t count;                 // 当前存储数量AdcData_t* buffer[STORAGE_POOL_SIZE];  // 指向内存块的指针数组
} CircularBuffer_t;CircularBuffer_t storageBuffer;     // 循环缓冲区实例/* USER CODE END 4 *//* USER CODE BEGIN Header_StartDefaultTask */
/*** @brief  Function implementing the defaultTask thread.* @param  argument: Not used* @retval None*/
uint32_t adc_value;
float temperature;/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void const * argument)
{/* USER CODE BEGIN 5 *//* Infinite loop */for(;;){/* 1. 启动ADC转换 */HAL_ADC_Start(&hadc1);if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK){adc_value = HAL_ADC_GetValue(&hadc1);/* 2. 计算温度值 (STM32F103 温度计算公式) */// Vref=3.3V, ADC分辨率=12位, 温度传感器参数: 25℃时1.43V, 灵敏度4.3mV/℃temperature = (1.43f - (float)adc_value * 3.3f / 4096.0f) / 0.0043f + 25.0f;/* 3. 分配邮箱内存块并填充数据 */AdcData_t* mail = (AdcData_t*)osMailAlloc(AdcMailQHandle, osWaitForever);if (mail != NULL){mail->adc_raw = adc_value;mail->temperature = temperature;mail->timestamp = osKernelSysTick();/* 4. 发送到邮箱 */osMailPut(AdcMailQHandle, mail);}}osDelay(1000);  // 每秒采集一次}/* USER CODE END 5 */
}/* USER CODE BEGIN Header_StartTask02 */
/**
* @brief Function implementing the myTask02 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTask02 */
void StartTask02(void const * argument)
{/* USER CODE BEGIN StartTask02 */osEvent evt;/* 创建邮箱 */AdcMailQHandle = osMailCreate(osMailQ(AdcMailQ), NULL);/* 创建内存池 */StoragePoolHandle = osPoolCreate(osPool(StoragePool));/* Infinite loop */for(;;){/* 1. 从邮箱获取数据(阻塞等待) */evt = osMailGet(AdcMailQHandle, osWaitForever);if (evt.status == osEventMail){AdcData_t* received_data = (AdcData_t*)evt.value.p;/* 2. 从内存池分配空间存储数据 */AdcData_t* storage_slot = (AdcData_t*)osPoolAlloc(StoragePoolHandle);if (storage_slot != NULL){/* 3. 复制数据到内存池 */*storage_slot = *received_data;/* 写入新数据(覆盖最旧数据,如果已满) */storageBuffer.buffer[storageBuffer.head] = storage_slot;storageBuffer.head = (storageBuffer.head + 1) % STORAGE_POOL_SIZE;/* 如果缓冲区已满,移动tail指针(丢弃最旧数据) */if (storageBuffer.count >= STORAGE_POOL_SIZE){storageBuffer.tail = (storageBuffer.tail + 1) % STORAGE_POOL_SIZE;}else{storageBuffer.count++;}}/* 5. 释放邮箱内存块 */osMailFree(AdcMailQHandle, received_data);}}/* USER CODE END StartTask02 */
}

在这里插入图片描述

4.4.2 消息队列+内存池

    功能跟邮箱一样,只是消息队列没办法一次性传输大量数量,所以这里在一个线程中申请内存池,并通过消息队列传递内存池的首地址,另一个线程接收到此地址后,将数据从内存池中拷贝出来,然后将申请的内存释放掉,完成数据的传递。

/* USER CODE BEGIN 4 */
#define TEMP_POOL_BLOCK_NUM   10  // 内存池块数量
#define MSG_QUEUE_LEN         5   // 消息队列长度// 温度数据结构体
typedef struct {uint32_t adc_raw;      // ADC 原始值float    temperature;  // 计算后的温度值 (℃)uint32_t timestamp;    // 采样时间戳 (系统滴答数)
} TempData_t;// 循环缓冲区控制结构体
typedef struct {uint32_t  head;        // 写入索引uint32_t  tail;        // 读取索引uint32_t  count;       // 当前存储数据数量TempData_t buffer[TEMP_POOL_BLOCK_NUM]; // 实际存储温度的缓存池
} CircularBuffer_t;// FreeRTOS 句柄
osMessageQDef(MsgQueue, MSG_QUEUE_LEN, uint32_t);  // 邮箱队列:最多5条消息
osMessageQId MsgQueueHandle;      // 消息队列句柄osPoolDef(TempPool, TEMP_POOL_BLOCK_NUM, TempData_t);  // 内存池定义
osPoolId     TempPoolHandle;      // 内存池句柄// 循环缓冲区实例
CircularBuffer_t circular_buf = {0};/* USER CODE END 4 *//* USER CODE BEGIN Header_StartDefaultTask */
/*** @brief  Function implementing the defaultTask thread.* @param  argument: Not used* @retval None*/
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void const * argument)
{/* USER CODE BEGIN 5 */// 内存池句柄(CubeMX 自动生成,名称需与配置一致)TempPoolHandle = osPoolCreate(osPool(TempPool));// 消息队列句柄(CubeMX 自动生成,名称需与配置一致)MsgQueueHandle = osMessageCreate(osMessageQ(MsgQueue), NULL);uint32_t adc_raw = 0;float    temperature = 0.0f;/* Infinite loop */for(;;){// 1. 启动 ADC 转换HAL_ADC_Start(&hadc1);if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK){adc_raw = HAL_ADC_GetValue(&hadc1);// Vref=3.3V, ADC分辨率=12位, 温度传感器参数: 25℃时1.43V, 灵敏度4.3mV/℃temperature = (1.43f - (float)adc_raw * 3.3f / 4096.0f) / 0.0043f + 25.0f;// 3. 从内存池申请内存块TempData_t* data_block = (TempData_t*)osPoolAlloc(TempPoolHandle);if (data_block != NULL){// 4. 填充温度数据到内存块data_block->adc_raw     = adc_raw;data_block->temperature = temperature;data_block->timestamp   = osKernelSysTick();// 5. 发送内存块指针到消息队列osStatus status = osMessagePut(MsgQueueHandle, (uint32_t)data_block, osWaitForever);if (status != osOK){// 发送失败,释放内存块osPoolFree(TempPoolHandle, data_block);// 可在此处添加错误处理逻辑}}}// 1 秒采集一次osDelay(1000);}/* USER CODE END 5 */
}/* USER CODE BEGIN Header_StartTask02 */
/**
* @brief Function implementing the myTask02 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTask02 */
void StartTask02(void const * argument)
{/* USER CODE BEGIN StartTask02 */osEvent event;/* Infinite loop */for(;;){// 1. 从消息队列接收数据(阻塞等待)event = osMessageGet(MsgQueueHandle, osWaitForever);if (event.status == osEventMessage){// 2. 提取内存块指针TempData_t* data_block = (TempData_t*)event.value.v;// 写入新数据circular_buf.buffer[circular_buf.head].adc_raw = data_block->adc_raw;circular_buf.buffer[circular_buf.head].temperature = data_block->temperature;circular_buf.buffer[circular_buf.head].timestamp = data_block->timestamp;circular_buf.head = (circular_buf.head + 1) % TEMP_POOL_BLOCK_NUM;// 如果缓冲区已满,移动 tail 指针(覆盖最旧数据)if (circular_buf.count >= TEMP_POOL_BLOCK_NUM){circular_buf.tail = (circular_buf.tail + 1) % TEMP_POOL_BLOCK_NUM;}else{circular_buf.count++;}// 把数据拷出来后就可以释放掉内存osPoolFree(TempPoolHandle, data_block);}// 任务延时(可根据需求调整)osDelay(100);}/* USER CODE END StartTask02 */
}

在这里插入图片描述

五、注意事项

1、邮箱与消息队列最大区别在于邮箱可以承载更多的数据,而消息队列只能传递一个32位的数据,所以一般邮箱可用在通信领域,比如需要传递一个串口数据,或以太网数据报文;而消息队列则适用于各种生产者-消费者架构的事件模型中,比如用户通过话费充值充了多少次,后面运营商就要给你的手机卡号增加多少次话费,两个动作是异步进行的,但次数是需要保持一致的。
2、内存池申请完,在使用过后需要释放掉,两者需要成对出现,防止内存泄漏。

六、相关链接

对于刚入门的小伙伴可以先看下STM32CubeMX的基础使用及Keil的基础使用。
【工具使用】STM32CubeMX-基础使用篇
【工具使用】Keil5软件使用-基础使用篇
【工具使用】STM32CubeMX-FreeRTOS操作系统-任务、延时、定时器篇
【工具使用】STM32CubeMX-单ADC模式规则通道配置

http://www.xdnf.cn/news/14473.html

相关文章:

  • 时间序列分析
  • Django中使用流式响应,自己也能实现ChatGPT的效果
  • CGAL 快速构建三维凸包
  • 20年架构师视角:SpringAI如何重塑Java技术栈?
  • 进程和线程区别、管道和套接字、共享变量、TCP三次握手,是否可以少一次握手、子进程和主进程区别和API——Nodejs
  • 206. 反转链表
  • ArkUI-X框架LogInterface使用指南
  • C++题解(36) 2025年顺德区中小学生程序设计展示活动(初中组C++)换位(二)
  • BeckHoff <---> Mitsubishi RH-20FR(三菱)水平关节机械手通过网桥(EL6692)通讯
  • C++队列的那些事儿
  • db2主从同步 逻辑复制 APPLY_THROTTLE参数
  • LangGraph AI 系统测试与高可用保障体系
  • SwiftHub 项目分析
  • Linux之Python定制篇——新版Ubuntu24.04安装
  • to avoid naming wrong index webpage for one website
  • DrissionPage如何通过截图的方式获取图片
  • 水果商城管理系统笔记
  • 零基础上手Conda:安装、创建环境、管理依赖的完整指南
  • 计算机硬件——主板
  • 架构设计的核心原则与基础理论
  • 什么是java jdk?
  • Eclise中Lombck配置
  • DC8靶机渗透
  • 数据赋能(259)——数据赋能业务——数据驱动业务转型
  • DAY 54 Inception网络及其思考
  • 进程上下文与中断上下文详解
  • Spring AI的ChatClient和ChatModel接口
  • YOLOv3 正负样本划分详解
  • OpenIPC-aviateur上位机程序编译环境配置
  • 【AI大模型】Elasticsearch9 + 通义大模型实现语义检索操作详解