一、前言
学习 FreeRTOS 时,很多初学者容易陷入一个误区:
学了很多 API,但是不知道什么时候使用。
例如:
- 什么时候创建 Task?
- 什么时候使用 Queue?
- 什么时候使用 Semaphore?
- 什么时候使用 Mutex?
真正的工程开发流程不是:
看到功能 → 创建 Task而应该是:
分析需求 ↓ 设计系统结构 ↓ 划分 Task ↓ 设计任务通信 ↓ 编写代码二、第一步:需求分析
假设一个 STM32 系统需要完成:
- 读取传感器数据
- 执行控制算法
- 控制输出设备
- 接收通信数据
- 输出调试信息
首先不要考虑代码。
先分析:每个功能有什么特点?
三、功能特点分析
| 功能 | 运行方式 | 实时性 |
|---|---|---|
| 传感器采集 | 周期运行 | 较高 |
| 控制算法 | 严格周期 | 最高 |
| 通信处理 | 事件驱动 | 中高 |
| 调试输出 | 低频运行 | 低 |
可以发现:它们并不是同一种运行模式。
四、第二步:确定 Task
根据特点设计 Task。
1. 控制任务 ControlTask
负责:
读取状态 ↓ 计算控制算法 ↓ 输出控制量特点:
- 周期固定
- 实时性高
因此:独立 Task。
2. 通信任务 CommunicationTask
负责:
接收数据 ↓ 解析数据 ↓ 发送数据特点:数据什么时候到达未知。
因此:使用阻塞等待。
3. 调试任务 DebugTask
负责:
打印状态 ↓ 输出调试信息特点:低频运行。
最终系统:
FreeRTOS | |-----------------| | | | ↓ ↓ ↓ ControlTask CommTask DebugTask五、第三步:确定任务周期
不同任务周期不同。
例如:
控制任务
周期 1ms,使用:
vTaskDelayUntil();保证周期。
调试任务
周期 100ms,使用:
vTaskDelay();即可。
通信任务
没有固定周期,使用:
- Queue
- Semaphore
等待事件。
六、第四步:确定任务优先级
优先级设计原则:
实时性越高,优先级越高。
例如:
| 优先级 | 任务类型 |
|---|---|
| 10 | 控制任务 |
| 8 | 通信任务 |
| 3 | 调试任务 |
不要所有任务都设为Priority = 10,因为系统会失去层次。
七、第五步:设计任务之间的数据流
这是系统设计中非常重要的一步。
例如传感器产生数据:
Sensor ↓ 数据 ↓ ControlTask这个属于数据传递,应该使用 Queue。
例如:
SensorTask ↓ Queue ↓ ControlTask八、第六步:判断使用 Queue、Semaphore 还是 Mutex
情况 1:传递数据
例如:
- 角度
- 速度
- 温度
- 状态
使用:Queue
结构:
Task A ↓ Queue ↓ Task B情况 2:通知事件
例如:
- 数据接收完成
- DMA 完成
- 外部中断发生
使用:Semaphore
结构:
ISR ↓ Semaphore ↓ Task情况 3:保护共享资源
例如多个任务访问:
- UART
- SPI
- CAN
- 共享变量
使用:Mutex
结构:
Task A ↓ Mutex ↓ 资源 Task B 等待九、一个完整系统框架示例
FreeRTOS | |---------------------------------| | | | ↓ ↓ ↓ SensorTask ControlTask CommTask | | | | ↓ | | Control Algorithm | | | | -------- Queue/Semaphore --------- | ↓ DebugTask十、代码框架设计
创建 Task
xTaskCreate( ControlTask, "Control", 512, NULL, 10, NULL );控制任务
void ControlTask(void *argument) { TickType_t lastWakeTime; lastWakeTime = xTaskGetTickCount(); while(1) { Read_Data(); Control(); Output(); vTaskDelayUntil( &lastWakeTime, pdMS_TO_TICKS(1) ); } }通信任务
void CommunicationTask(void *argument) { while(1) { xQueueReceive( Queue, &data, portMAX_DELAY ); Process(data); } }调试任务
void DebugTask(void *argument) { while(1) { Print_Status(); vTaskDelay( pdMS_TO_TICKS(100) ); } }十一、为什么不能所有代码放在一个 Task?
例如:
void MainTask(void *argument) { Read_Sensor(); Control(); Communication(); Print(); }问题:
周期难保证
通信可能阻塞,打印可能耗时,导致控制周期变化。代码难维护
所有功能耦合,修改一个地方可能影响整个系统。无法发挥 RTOS 优势
FreeRTOS 最大的意义就是多个任务并行管理。
十二、优秀系统设计思想
一个好的 FreeRTOS 系统应该:
任务职责明确
例如:
- ControlTask:只负责控制
- CommunicationTask:只负责通信
- DebugTask:只负责调试
数据流清晰
例如:
Sensor ↓ Queue ↓ Control ↓ Output阻塞合理
例如:没有数据时 Task 等待,而不是while不断查询。
十三、本节总结
设计 FreeRTOS 系统时,不要从 API 开始。
应该:
- 分析需求
- 划分功能
- 设计 Task
- 设计通信方式
- 设置优先级
- 编写代码
核心思想:
FreeRTOS 不是让程序变复杂,而是帮助我们把复杂系统拆成多个清晰、可管理的任务。
到这里,我们已经完成STM32 + FreeRTOS 基础篇,包括:
- Task
- Delay
- Queue
- Semaphore
- Mutex
- Priority
- Task 架构设计
下一阶段将进入STM32 + FreeRTOS 实战篇,开始学习:
- 多任务启动流程
- 文件结构设计
- Task 初始化管理
- 任务间数据管理
- 中断与任务结合
- 实际控制系统框架设计
下一节预告
STM32 + FreeRTOS 课程(6):工程文件结构设计与任务初始化管理
main.c应该放什么?- Task 文件怎么分?
freertos.c怎么组织?- 初始化顺序是什么?
- 为什么大型项目不会把所有代码写在
main.c?
这一步会从“会写 FreeRTOS 示例”进入“会搭建工程”。