FreeRTOS快速入门:从零开始学习实时操作系统¶
学习目标¶
完成本教程后,你将能够:
- 理解FreeRTOS的基本概念和架构
- 搭建FreeRTOS开发环境
- 创建和运行第一个FreeRTOS任务
- 使用STM32CubeMX配置FreeRTOS
- 掌握基本的调试方法
- 理解任务调度的基本原理
前置要求¶
在开始本教程之前,你需要:
知识要求: - 了解C语言基础(指针、结构体、函数) - 熟悉基本的嵌入式开发概念 - 了解RTOS的基本概念(建议先阅读RTOS概述) - 掌握STM32的基本开发流程
技能要求: - 能够使用STM32CubeIDE或Keil MDK - 会使用基本的调试工具 - 了解如何下载程序到开发板
准备工作¶
硬件准备¶
| 名称 | 数量 | 说明 | 参考链接 |
|---|---|---|---|
| STM32开发板 | 1 | STM32F4 Discovery或类似 | - |
| ST-Link调试器 | 1 | 通常开发板自带 | - |
| USB数据线 | 1 | 用于连接开发板 | - |
| LED灯 | 2 | 用于演示任务(可选) | - |
软件准备¶
- 开发环境:STM32CubeIDE v1.10+ 或 Keil MDK v5.30+
- 配置工具:STM32CubeMX v6.0+(CubeIDE已集成)
- FreeRTOS版本:V10.3.1+(通过CubeMX自动集成)
- 辅助工具:串口调试助手(用于查看输出)
环境配置¶
- 安装STM32CubeIDE
- 从ST官网下载最新版本
- 按照安装向导完成安装
-
首次启动时选择工作空间
-
验证开发板连接
- 使用USB线连接开发板
- 打开设备管理器,确认ST-Link驱动正常
-
在CubeIDE中测试连接
-
准备串口工具
- 安装串口调试助手
- 配置波特率:115200
- 数据位:8,停止位:1,无校验
FreeRTOS基础概念¶
在开始实践之前,让我们快速了解FreeRTOS的核心概念:
什么是FreeRTOS?¶
FreeRTOS是一个开源的实时操作系统内核,专为嵌入式系统设计。它具有以下特点:
- 轻量级:内核代码小于10KB
- 可移植:支持40+种处理器架构
- 免费开源:MIT许可证
- 成熟稳定:广泛应用于商业产品
核心概念¶
任务(Task): - FreeRTOS中的基本执行单元 - 每个任务都有独立的栈空间 - 任务可以有不同的优先级
调度器(Scheduler): - 负责决定哪个任务运行 - 采用抢占式调度算法 - 高优先级任务优先执行
任务状态: - 运行态(Running):正在执行 - 就绪态(Ready):等待CPU - 阻塞态(Blocked):等待事件 - 挂起态(Suspended):被挂起
步骤1:创建FreeRTOS项目¶
1.1 新建STM32项目¶
- 打开STM32CubeIDE
- 选择 File → New → STM32 Project
- 在芯片选择界面:
- 选择你的目标芯片(如STM32F407VGT6)
- 或选择开发板型号(如STM32F4DISCOVERY)
- 点击 Next
- 输入项目名称:
FreeRTOS_FirstProject - 选择项目位置
- 点击 Finish
预期结果: - 项目创建成功 - 自动打开CubeMX配置界面
1.2 配置系统时钟¶
- 在左侧导航栏选择 System Core → RCC
- 配置时钟源:
- HSE:Crystal/Ceramic Resonator(外部晶振)
- LSE:Disable(暂不使用)
- 切换到 Clock Configuration 标签页
- 配置系统时钟为最大频率(如168MHz for STM32F4)
1.3 启用FreeRTOS¶
- 在左侧导航栏找到 Middleware → FREERTOS
- 将 Interface 设置为 CMSIS_V1 或 CMSIS_V2
- CMSIS_V1:经典API,简单易用
- CMSIS_V2:新版API,功能更强
-
本教程使用CMSIS_V1
-
在 Configuration 标签页配置:
-
Tasks and Queues 标签:
- 默认已有一个
defaultTask - 保持默认配置即可
- 默认已有一个
-
FreeRTOS 标签:
- Kernel settings:
- USE_PREEMPTION:Enabled(抢占式调度)
- CPU_CLOCK_HZ:自动设置
- TICK_RATE_HZ:1000(1ms时钟节拍)
-
MAX_PRIORITIES:5(最大优先级数)
-
Memory management settings:
- Memory Management scheme:heap_4(推荐)
- TOTAL_HEAP_SIZE:15360 bytes(根据需要调整)
1.4 配置调试输出(可选)¶
为了方便调试,我们配置串口输出:
- 在 Connectivity 中选择 USART2
- Mode:Asynchronous
- 配置参数:
- Baud Rate:115200
- Word Length:8 Bits
- Parity:None
-
Stop Bits:1
-
在 GPIO Settings 中确认引脚配置:
- PA2:USART2_TX
- PA3:USART2_RX
1.5 生成代码¶
- 点击右上角的 GENERATE CODE 按钮
- 或使用快捷键:Alt + K
- 等待代码生成完成
- 选择 Open Project 打开项目
预期结果:
- 代码生成成功
- 项目结构中包含FreeRTOS相关文件
- 可以看到 Middlewares/Third_Party/FreeRTOS 目录
步骤2:理解生成的代码¶
2.1 项目结构¶
生成的项目包含以下关键文件:
FreeRTOS_FirstProject/
├── Core/
│ ├── Src/
│ │ ├── main.c # 主程序文件
│ │ ├── freertos.c # FreeRTOS任务定义
│ │ └── ...
│ └── Inc/
│ ├── main.h
│ ├── FreeRTOSConfig.h # FreeRTOS配置文件
│ └── ...
├── Middlewares/
│ └── Third_Party/
│ └── FreeRTOS/
│ ├── Source/ # FreeRTOS源码
│ └── ...
└── ...
2.2 main.c 文件分析¶
打开 Core/Src/main.c,关键代码如下:
int main(void)
{
/* MCU配置 */
HAL_Init();
SystemClock_Config();
/* 初始化外设 */
MX_GPIO_Init();
MX_USART2_UART_Init();
/* 初始化调度器 */
osKernelInitialize();
/* 创建默认任务 */
MX_FREERTOS_Init();
/* 启动调度器 */
osKernelStart();
/* 永远不会执行到这里 */
while (1)
{
}
}
代码说明:
- osKernelInitialize():初始化FreeRTOS内核
- MX_FREERTOS_Init():创建任务和其他RTOS对象
- osKernelStart():启动调度器,开始任务调度
- 调度器启动后,main()函数不再返回
2.3 freertos.c 文件分析¶
打开 Core/Src/freertos.c,查看默认任务:
/* 默认任务函数 */
void StartDefaultTask(void *argument)
{
/* 用户代码开始 */
for(;;)
{
osDelay(1); // 延时1个时钟节拍(1ms)
}
/* 用户代码结束 */
}
代码说明:
- 任务函数是一个无限循环
- osDelay():任务延时,释放CPU给其他任务
- 任务函数永远不应该返回
2.4 FreeRTOSConfig.h 配置文件¶
这个文件包含FreeRTOS的所有配置选项:
#define configUSE_PREEMPTION 1 // 使用抢占式调度
#define configUSE_IDLE_HOOK 0 // 不使用空闲钩子
#define configUSE_TICK_HOOK 0 // 不使用时钟节拍钩子
#define configCPU_CLOCK_HZ 168000000 // CPU频率
#define configTICK_RATE_HZ 1000 // 时钟节拍频率(1ms)
#define configMAX_PRIORITIES 5 // 最大优先级数
#define configMINIMAL_STACK_SIZE 128 // 最小栈大小(字)
#define configTOTAL_HEAP_SIZE 15360 // 堆大小(字节)
步骤3:创建第一个任务¶
现在让我们创建一个简单的LED闪烁任务。
3.1 配置GPIO¶
首先在CubeMX中配置LED引脚:
- 重新打开
.ioc文件 - 找到LED引脚(如PD12 for STM32F4 Discovery)
- 设置为 GPIO_Output
- 在 GPIO Settings 中:
- GPIO output level:Low
- GPIO mode:Output Push Pull
- GPIO Pull-up/Pull-down:No pull-up and no pull-down
- Maximum output speed:Low
-
User Label:LED_GREEN
-
重新生成代码
3.2 添加LED任务¶
在 freertos.c 中添加新任务。找到 MX_FREERTOS_Init() 函数:
void MX_FREERTOS_Init(void) {
/* 用户代码开始 */
/* 创建LED任务 */
osThreadDef(ledTask, StartLedTask, osPriorityNormal, 0, 128);
ledTaskHandle = osThreadCreate(osThread(ledTask), NULL);
/* 用户代码结束 */
/* 创建默认任务 */
osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);
}
3.3 实现LED任务函数¶
在 freertos.c 中添加任务函数:
/* 任务句柄 */
osThreadId ledTaskHandle;
/* LED任务函数 */
void StartLedTask(void const * argument)
{
/* 用户代码开始 */
for(;;)
{
// 点亮LED
HAL_GPIO_WritePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin, GPIO_PIN_SET);
osDelay(500); // 延时500ms
// 熄灭LED
HAL_GPIO_WritePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin, GPIO_PIN_RESET);
osDelay(500); // 延时500ms
}
/* 用户代码结束 */
}
代码说明:
- osDelay(500):延时500个时钟节拍(500ms)
- 任务在延时期间会进入阻塞态,让出CPU
- 其他任务可以在此期间运行
3.4 添加串口输出任务¶
为了更好地理解任务调度,我们添加一个串口输出任务:
/* 串口任务句柄 */
osThreadId uartTaskHandle;
/* 串口任务函数 */
void StartUartTask(void const * argument)
{
/* 用户代码开始 */
uint32_t counter = 0;
char msg[50];
for(;;)
{
// 格式化消息
sprintf(msg, "Task running: %lu\r\n", counter++);
// 发送到串口
HAL_UART_Transmit(&huart2, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY);
// 延时1秒
osDelay(1000);
}
/* 用户代码结束 */
}
在 MX_FREERTOS_Init() 中创建任务:
/* 创建串口任务 */
osThreadDef(uartTask, StartUartTask, osPriorityNormal, 0, 128);
uartTaskHandle = osThreadCreate(osThread(uartTask), NULL);
注意:需要在文件开头添加头文件:
步骤4:编译和下载¶
4.1 编译项目¶
- 点击工具栏的 🔨 Build 按钮
- 或使用快捷键:Ctrl + B
- 查看控制台输出
预期输出:
常见编译错误:
如果出现 undefined reference to 'sprintf' 错误:
1. 右键项目 → Properties
2. C/C++ Build → Settings
3. MCU GCC Linker → Miscellaneous
4. 在 Other flags 中添加:-u _printf_float
4.2 下载程序¶
- 连接开发板到电脑
- 点击 ▶️ Run 按钮
- 或使用快捷键:F11(调试模式)
- 等待下载完成
预期结果: - 程序下载成功 - LED开始闪烁 - 串口输出计数信息
4.3 查看串口输出¶
- 打开串口调试助手
- 选择正确的COM口
- 配置波特率:115200
- 打开串口
预期输出:
步骤5:调试和验证¶
5.1 使用调试器¶
- 点击 🐞 Debug 按钮启动调试
- 程序会在
main()函数入口处暂停
设置断点:
- 在 StartLedTask() 函数中设置断点
- 在 StartUartTask() 函数中设置断点
观察任务切换: 1. 点击 ▶️ Resume 继续运行 2. 程序会在断点处停止 3. 观察哪个任务正在运行 4. 多次继续运行,观察任务切换
5.2 查看任务状态¶
在调试模式下,可以查看FreeRTOS的运行状态:
- 打开 Window → Show View → Other
- 选择 Debug → Variables
- 展开查看任务句柄和状态
关键变量:
- ledTaskHandle:LED任务句柄
- uartTaskHandle:串口任务句柄
- pxCurrentTCB:当前运行的任务控制块
5.3 性能分析¶
使用 SWV(Serial Wire Viewer) 进行性能分析:
- 在调试配置中启用SWV
- 配置ITM端口
- 查看任务执行时间和CPU占用率
步骤6:理解任务调度¶
6.1 任务优先级实验¶
修改任务优先级,观察调度行为:
/* 创建高优先级LED任务 */
osThreadDef(ledTask, StartLedTask, osPriorityHigh, 0, 128);
ledTaskHandle = osThreadCreate(osThread(ledTask), NULL);
/* 创建低优先级串口任务 */
osThreadDef(uartTask, StartUartTask, osPriorityLow, 0, 128);
uartTaskHandle = osThreadCreate(osThread(uartTask), NULL);
观察结果: - 高优先级任务优先执行 - 低优先级任务在高优先级任务阻塞时才运行
6.2 任务延时实验¶
修改延时时间,观察任务行为:
void StartLedTask(void const * argument)
{
for(;;)
{
HAL_GPIO_TogglePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin);
osDelay(100); // 改为100ms,LED闪烁更快
}
}
6.3 多任务并发实验¶
添加第三个任务,观察多任务并发:
/* 第二个LED任务 */
void StartLed2Task(void const * argument)
{
for(;;)
{
HAL_GPIO_TogglePin(LED_BLUE_GPIO_Port, LED_BLUE_Pin);
osDelay(300); // 不同的闪烁频率
}
}
观察结果: - 两个LED以不同频率闪烁 - 串口继续输出 - 所有任务并发执行
故障排除¶
问题1:程序无法启动¶
现象: - 程序下载成功,但LED不闪烁 - 串口无输出
可能原因: - 堆栈大小不足 - 时钟配置错误 - FreeRTOS配置错误
解决方法:
-
增加堆栈大小:
-
检查时钟配置:
- 确认
configCPU_CLOCK_HZ与实际CPU频率一致 -
检查
SystemClock_Config()函数 -
启用断言:
问题2:任务不执行¶
现象: - 某个任务从不运行 - 任务创建后无响应
可能原因: - 任务优先级设置不当 - 任务栈溢出 - 任务被阻塞
解决方法:
-
检查任务优先级:
-
启用栈溢出检测:
-
增加任务栈大小:
问题3:系统运行不稳定¶
现象: - 系统偶尔死机 - 任务执行异常 - 数据错乱
可能原因: - 中断优先级配置错误 - 共享资源未保护 - 内存分配失败
解决方法:
-
配置中断优先级:
-
保护共享资源:
问题4:串口输出乱码¶
现象: - 串口输出乱码或不完整
可能原因: - 波特率不匹配 - 多任务同时访问串口 - 缓冲区溢出
解决方法:
- 检查波特率配置
- 使用互斥量保护串口:
总结¶
通过本教程,你学习了:
- ✅ FreeRTOS的基本概念和架构
- ✅ 使用STM32CubeMX配置FreeRTOS项目
- ✅ 创建和管理FreeRTOS任务
- ✅ 理解任务调度和优先级
- ✅ 使用基本的调试方法
- ✅ 解决常见问题
关键要点¶
- 任务是FreeRTOS的基本执行单元
- 每个任务都有独立的栈空间
- 任务函数必须是无限循环
-
使用
osDelay()让出CPU -
调度器负责任务切换
- 采用抢占式调度
- 高优先级任务优先执行
-
相同优先级任务时间片轮转
-
合理配置是关键
- 堆栈大小要充足
- 优先级要合理分配
-
中断优先级要正确配置
-
调试技巧很重要
- 使用断点观察任务切换
- 启用栈溢出检测
- 使用断言捕获错误
进阶挑战¶
尝试以下挑战来巩固学习:
挑战1:多任务协作¶
创建3个任务,实现以下功能: - 任务1:每500ms读取一次按键状态 - 任务2:根据按键状态控制LED闪烁频率 - 任务3:每秒通过串口输出系统状态
提示:需要使用任务间通信机制(下一节课学习)
挑战2:优先级实验¶
创建不同优先级的任务,观察调度行为: - 高优先级任务(osPriorityHigh) - 普通优先级任务(osPriorityNormal) - 低优先级任务(osPriorityLow)
思考: - 低优先级任务什么时候能运行? - 如果高优先级任务不延时会怎样?
挑战3:资源监控¶
实现一个监控任务,定期输出: - 每个任务的栈使用情况 - 系统剩余堆内存 - 任务运行时间统计
提示:使用FreeRTOS的统计API
完整代码示例¶
freertos.c 完整代码¶
/* Includes */
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
#include <string.h>
#include <stdio.h>
/* Private variables */
osThreadId defaultTaskHandle;
osThreadId ledTaskHandle;
osThreadId uartTaskHandle;
/* Private function prototypes */
void StartDefaultTask(void const * argument);
void StartLedTask(void const * argument);
void StartUartTask(void const * argument);
/* External variables */
extern UART_HandleTypeDef huart2;
void MX_FREERTOS_Init(void) {
/* Create the thread(s) */
/* LED任务 */
osThreadDef(ledTask, StartLedTask, osPriorityNormal, 0, 128);
ledTaskHandle = osThreadCreate(osThread(ledTask), NULL);
/* 串口任务 */
osThreadDef(uartTask, StartUartTask, osPriorityNormal, 0, 128);
uartTaskHandle = osThreadCreate(osThread(uartTask), NULL);
/* 默认任务 */
osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);
}
/* StartDefaultTask function */
void StartDefaultTask(void const * argument)
{
for(;;)
{
osDelay(1);
}
}
/* StartLedTask function */
void StartLedTask(void const * argument)
{
for(;;)
{
HAL_GPIO_TogglePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin);
osDelay(500);
}
}
/* StartUartTask function */
void StartUartTask(void const * argument)
{
uint32_t counter = 0;
char msg[50];
for(;;)
{
sprintf(msg, "Task running: %lu\r\n", counter++);
HAL_UART_Transmit(&huart2, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY);
osDelay(1000);
}
}
测试环境¶
硬件环境: - 开发板:STM32F407 Discovery - 调试器:ST-Link V2 - LED:板载LED(PD12-PD15)
软件环境: - IDE:STM32CubeIDE v1.10.1 - HAL库版本:v1.27.1 - FreeRTOS版本:V10.3.1 - 编译器:GCC ARM 10.3
下一步学习¶
建议按以下顺序继续学习:
1. 深入任务管理¶
- FreeRTOS任务创建与管理
- 学习任务的创建、删除、挂起、恢复
- 理解任务优先级和调度策略
2. 任务间通信¶
- FreeRTOS队列通信实战
- 学习使用队列在任务间传递数据
- 理解阻塞和非阻塞通信
3. 同步机制¶
- FreeRTOS信号量应用
- 学习二值信号量和计数信号量
- 理解互斥量和临界区保护
4. 高级特性¶
- FreeRTOS软件定时器
- FreeRTOS事件标志组
- FreeRTOS任务通知
参考资料¶
官方文档¶
- FreeRTOS官方网站
- https://www.freertos.org/
-
包含完整的API文档和教程
-
FreeRTOS参考手册
- https://www.freertos.org/Documentation/RTOS_book.html
-
详细的概念和API说明
-
STM32 FreeRTOS移植指南
- ST官方应用笔记
- 包含移植细节和优化建议
推荐书籍¶
- 《Mastering the FreeRTOS Real Time Kernel》
- FreeRTOS作者编写
-
免费下载:https://www.freertos.org/Documentation/RTOS_book.html
-
《嵌入式实时操作系统FreeRTOS原理与实践》
- 中文书籍,适合入门
- 包含大量实例
在线资源¶
- FreeRTOS论坛
- https://forums.freertos.org/
-
活跃的社区支持
-
STM32社区
- https://community.st.com/
-
ST官方技术支持
-
GitHub示例代码
- https://github.com/FreeRTOS/FreeRTOS
- 官方示例和Demo
视频教程¶
- FreeRTOS官方YouTube频道
- 系列教程视频
-
实时操作系统概念讲解
-
STM32在线培训
- ST官方培训课程
- 包含FreeRTOS专题
常见问题解答¶
Q1: FreeRTOS和裸机编程有什么区别?¶
A: 主要区别在于: - 裸机:程序按顺序执行,需要手动管理任务切换 - FreeRTOS:多任务并发执行,调度器自动管理任务切换
优势: - 代码结构更清晰 - 更容易实现复杂功能 - 更好的实时性保证
Q2: 什么时候应该使用FreeRTOS?¶
A: 以下情况建议使用FreeRTOS: - 需要同时处理多个独立任务 - 对实时性有要求 - 系统功能复杂,需要模块化设计 - 需要使用中间件(如TCP/IP协议栈)
不适合的场景: - 简单的单一功能应用 - 资源极度受限(RAM < 8KB) - 对功耗要求极高的场景
Q3: 如何选择合适的堆内存大小?¶
A: 计算方法:
示例: - 3个任务,每个128字(512字节)= 1536字节 - 2个队列,每个100字节 = 200字节 - 余量20% = 347字节 - 总计约2KB,建议设置为3-4KB
Q4: 任务优先级如何分配?¶
A: 优先级分配原则: 1. 实时性要求高的任务:高优先级 2. 周期性任务:中等优先级 3. 后台任务:低优先级 4. 空闲任务:最低优先级(自动)
示例: - 中断后处理任务:osPriorityHigh - 数据采集任务:osPriorityAboveNormal - 显示更新任务:osPriorityNormal - 日志记录任务:osPriorityBelowNormal
Q5: 如何调试任务栈溢出?¶
A: 步骤: 1. 启用栈溢出检测 2. 实现钩子函数 3. 使用调试器查看栈使用情况 4. 增加栈大小或优化代码
工具:
- uxTaskGetStackHighWaterMark():查看栈剩余空间
- SystemView:可视化任务执行和栈使用
反馈与支持:
如果你在学习过程中遇到问题: - 💬 在评论区留言讨论 - 📧 发送邮件到:support@embedded-platform.com - 🐛 报告问题:GitHub Issues
贡献代码: 欢迎提交改进建议和示例代码!
版权声明:本教程采用 CC BY-SA 4.0 许可协议。
最后更新:2024-01-15
文档版本:1.0