1. 项目概述
作为一名嵌入式开发者,我最近在跟随keysking大佬学习FreeRTOS。虽然教程基于STM32F103开发板,但我手头只有一块蓝桥杯的STM32G431RBT6开发板。本文将详细记录我将代码移植到G431的过程,重点解决三个核心问题:消息队列传输结构体指针、串口不定长数据接收,以及在CLion开发环境下的CMake配置问题。
这个项目的主要目标是实现一个基于FreeRTOS的多任务系统,其中:
- 按键任务(Key_Task)负责检测按键输入
- LED任务(Led_Task)负责控制LED状态
- 命令任务(CommandTask)负责解析串口接收的数据
- 通过消息队列实现任务间的通信
2. 核心原理:为什么使用消息队列?
在裸机开发中,我们习惯使用全局变量来传递标志位。但在RTOS多任务系统中,这种做法就像没有红绿灯的十字路口,容易发生"撞车"(数据竞争)。消息队列(Queue)则提供了一个安全高效的通信机制。
2.1 消息队列的工作原理
消息队列就像一个传送带系统:
- 生产者(如按键任务或串口中断)将数据打包放入队列
- 消费者(如LED任务)从队列中取出数据处理
- 解耦:发送方只管发送,接收方只管处理,双方不需要直接交互
2.2 消息队列的优势
- 线程安全:队列内部实现了互斥机制,防止多任务同时访问导致的数据竞争
- 阻塞机制:当队列为空时,接收任务可以自动阻塞,不占用CPU资源
- 优先级继承:FreeRTOS的队列支持优先级继承,防止优先级反转问题
- 灵活的数据传输:可以传输简单数据类型或复杂数据结构
3. 硬件配置与开发环境搭建
3.1 硬件准备
本项目使用STM32G431RBT6开发板,需要配置以下外设:
- LED控制引脚
- 按键输入引脚(配置为上拉输入)
- USART1用于串口通信
- 特别注意PD2锁存器引脚
3.2 CubeMX配置
-
引脚配置:
- LED:PC8、PC9、PC10
- 按键:PB0、PB1、PB2、PA0(上拉输入)
- 串口:USART1
-
NVIC配置:
- 必须开启USART RX中断,用于接收串口数据
-
FreeRTOS配置:
- 创建两个任务:Key_Task(高优先级)和Led_Task(低优先级)
- 创建一个队列:Led_Queue,用于任务间通信
注意:输入/数据采集任务应设置为高优先级,显示/执行类任务设置为低优先级
3.3 CLion开发环境配置
-
工程结构管理:
- 在Core目录下新建APP文件夹
- APP下创建Task(存放任务逻辑)和Type(存放公共结构体定义)子目录
-
CMakeLists.txt配置:
- CLion会自动更新CMakeLists.txt,但建议手动整理
- 确保所有源文件都正确添加到target_sources中
- 设置正确的头文件包含路径
4. 消息队列实现细节
4.1 数据结构定义
在Type/LedType.h中定义LED控制相关的数据结构:
c复制typedef enum {
led_off = 0,
led_on = 1,
} Led_state;
typedef enum {
led_1 = 0,
led_2 = 1,
led_3 = 2,
} led_now_light;
typedef struct {
Led_state state;
led_now_light led_now;
} Led_message;
4.2 队列创建与配置
在CubeMX中配置队列时需要注意:
- Item Size:sizeof(Led_message*)(传递指针而非结构体本身)
- Queue Size:根据实际需求设置,本项目设置为16
4.3 按键任务实现
KeyTask.c的核心逻辑:
c复制void StartKeyTask(void *argument) {
for (;;) {
static uint8_t key_down = 0, key_old = 0, key_up = 0, key_val = 0;
uint8_t temp = 0;
static Led_state state = led_off;
// 读取按键状态
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET)
temp |= KEY1;
// 其他按键读取类似...
// 按键状态处理
key_val = temp;
key_down = key_val & (key_val ^ key_old);
key_up = ~key_val & (key_val ^ key_old);
key_old = key_val;
// 按键1按下处理
if (key_down & KEY1) {
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_10);
// 控制PD2锁存器
GPIOD->BSRR |= 0x01 << 2;
GPIOD->BRR |= 0x01 << 2;
state = !state;
// 分配消息内存
Led_message* message = pvPortMalloc(sizeof(Led_message));
message->state = state;
message->led_now = led_1;
// 发送消息到队列
osMessageQueuePut(Led_QueueHandle, &message, 0, osWaitForever);
}
osDelay(10);
}
}
4.4 LED任务实现
LedTask.c的核心逻辑:
c复制void StartLedTask(void *argument) {
for (;;) {
Led_message* message;
// 从队列获取消息
osMessageQueueGet(Led_QueueHandle, &message, 0, osWaitForever);
// 根据消息控制LED
switch (message->led_now) {
case led_1:
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8, message->state ? GPIO_PIN_RESET : GPIO_PIN_SET);
// 控制PD2锁存器
GPIOD->BSRR |= 0x01 << 2;
GPIOD->BRR |= 0x01 << 2;
break;
// 其他LED控制类似...
}
// 释放内存
vPortFree(message);
}
}
5. 串口通信与协议解析
5.1 串口中断接收实现
在usart.c中添加以下代码:
c复制uint8_t rxData;
void Usart_Start_Receive_Data(void) {
HAL_UART_Receive_IT(&huart1, &rxData, 1);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 将接收到的字节放入队列
osMessageQueuePut(CommandQueueHandle, &rxData, 1, 0);
// 重新启动接收
HAL_UART_Receive_IT(&huart1, &rxData, 1);
}
}
重要原则:中断中不可阻塞,osMessageQueuePut的timeout必须为0
5.2 命令协议设计
为了防止数据错乱,设计简单的协议帧:
code复制包头(0xAA) + 长度(N) + [数据...] + 校验和
其中:
- 包头:1字节,固定为0xAA
- 长度:1字节,表示整个帧的长度
- 数据区:N-3字节(减去包头、长度和校验和)
- 校验和:1字节,前面所有字节的和的最低字节
5.3 命令任务实现
CommandTask.c的核心逻辑:
c复制void StartCommandTask(void *argument) {
uint8_t recv;
uint8_t command[32];
uint8_t commandLength = 0;
uint8_t commandIndex = 0;
for (;;) {
// 从队列获取一个字节
osMessageQueueGet(CommandQueueHandle, &recv, 0, osWaitForever);
if (commandIndex == 0) {
// 状态1:等待包头
if (recv == 0xAA) {
command[commandIndex++] = recv;
}
} else if (commandIndex == 1) {
// 状态2:获取长度
commandLength = recv;
// 长度检查
if (commandLength < 4 || commandLength > sizeof(command)) {
// 长度非法,重置状态
commandIndex = 0;
commandLength = 0;
memset(command, 0, sizeof(command));
continue;
}
command[commandIndex++] = recv;
} else {
// 状态3:接收数据
command[commandIndex++] = recv;
// 检查是否接收完整帧
if (commandIndex == commandLength) {
// 计算校验和
uint16_t sum = 0;
for (int i = 0; i < commandLength - 1; i++) {
sum += command[i];
}
uint8_t human_sum = (uint8_t)sum & 0xff;
// 校验通过
if (human_sum == command[commandLength - 1]) {
// 解析命令
for (int i = 2; i < commandLength - 2; i += 2) {
Led_message *message = pvPortMalloc(sizeof(Led_message));
if (message != NULL) {
message->led_now = command[i];
message->state = command[i + 1];
// 发送LED控制消息
osMessageQueuePut(Led_QueueHandle, &message, 0, 0);
}
}
}
// 重置接收状态
commandIndex = 0;
commandLength = 0;
memset(command, 0, sizeof(command));
}
}
}
}
6. 关键技术与经验分享
6.1 指针传递 vs 值传递
在FreeRTOS队列中传递数据时,有两种方式:
-
值传递:直接传递结构体,FreeRTOS会拷贝整个结构体到队列
- 优点:简单直接
- 缺点:当结构体较大时,拷贝开销大
-
指针传递:传递结构体的指针(地址)
- 优点:只传递4字节指针,效率高
- 缺点:需要手动管理内存,必须确保接收方释放内存
本项目采用指针传递,需要注意:
- 使用
pvPortMalloc分配内存(FreeRTOS专用内存分配函数) - 使用
vPortFree释放内存 - 必须成对使用,不能混用标准C的malloc/free
6.2 内存管理注意事项
-
谁分配谁释放原则:
- 如果发送方分配内存,接收方负责释放
- 确保每个
pvPortMalloc都有对应的vPortFree
-
内存泄漏检查:
- 使用FreeRTOS的内存统计功能检查内存泄漏
- 在开发阶段可以添加调试输出,跟踪内存分配/释放
-
内存碎片问题:
- 频繁分配释放小内存可能导致碎片
- 可以考虑使用内存池技术优化
6.3 中断处理最佳实践
-
中断要短:
- 只做最必要的操作(如接收数据)
- 复杂处理(如协议解析)交给任务处理
-
中断中不可阻塞:
- 不能使用带有等待时间的API
- osMessageQueuePut的timeout必须为0
-
中断优先级:
- 合理设置中断优先级
- 确保不会影响关键系统功能
6.4 CLion开发技巧
-
CMakeLists.txt管理:
- 虽然CLion会自动更新CMakeLists.txt,但建议手动整理
- 确保所有源文件都正确添加到target_sources中
-
代码补全:
- 使用Alt+Enter快速补全缺失的头文件
- 利用CLion强大的代码分析功能
-
调试技巧:
- 配置OpenOCD进行硬件调试
- 使用SEGGER RTT进行日志输出
7. 常见问题与解决方案
7.1 队列操作失败
问题现象:
- osMessageQueuePut返回错误
- 队列无法正常传递数据
可能原因及解决方案:
-
队列已满:
- 检查队列大小是否足够
- 增加队列长度或优化生产者速率
-
内存不足:
- 检查堆空间是否足够
- 调整FreeRTOS的堆大小
-
优先级问题:
- 确保接收任务的优先级合理
- 考虑使用任务通知替代队列
7.2 串口数据丢失
问题现象:
- 部分串口数据丢失
- 协议解析错误
可能原因及解决方案:
-
接收缓冲区溢出:
- 提高串口中断优先级
- 增加队列大小
-
校验失败:
- 检查硬件连接是否稳定
- 增加协议重传机制
-
波特率不匹配:
- 确认发送端和接收端波特率一致
- 使用示波器检查实际波特率
7.3 系统稳定性问题
问题现象:
- 系统运行一段时间后崩溃
- 内存泄漏
可能原因及解决方案:
-
内存泄漏:
- 检查所有pvPortMalloc都有对应的vPortFree
- 使用FreeRTOS的内存统计功能
-
堆栈溢出:
- 增加任务堆栈大小
- 使用uxTaskGetStackHighWaterMark监控堆栈使用
-
优先级反转:
- 合理设置任务优先级
- 对共享资源使用互斥锁
8. 项目总结与扩展建议
通过这个项目,我们实现了一个基于FreeRTOS的多任务系统,使用消息队列实现了任务间通信,并通过串口实现了外部控制。以下是几个可以进一步优化的方向:
-
协议增强:
- 增加更完善的错误处理机制
- 支持更复杂的命令集
-
性能优化:
- 使用内存池技术优化内存分配
- 考虑使用任务通知替代队列提高性能
-
功能扩展:
- 增加更多的外设控制
- 实现无线通信功能
-
调试工具:
- 添加更完善的日志系统
- 实现远程调试功能
在实际开发中,我发现FreeRTOS的消息队列是一个非常强大的工具,但需要特别注意内存管理和任务优先级的设计。合理使用这些机制可以构建出既高效又稳定的嵌入式系统。
