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