一、概述

    无论是新手还是大佬,基于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与其他常见的嵌入式操作系统对比如下:

维度 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配置

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模式规则通道配置

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐