第2章:FreeRTOS入门:FreeRTOS简介、源码结构、移植到STM32平台
2.1 为什么是FreeRTOS?
做暖通空调控制这些年,我接触过不少实时操作系统。坦白讲,FreeRTOS是我用得最顺手的一个。为什么?因为它够轻、够稳、够开放。
你想想看,一个空调控制器,既要处理温度传感器采样,又要跑PID算法,还得响应遥控器指令、控制风机转速、管理定时开关机……如果全靠裸机轮询,代码写起来那叫一个痛苦。我记得早期做一款多联机控制器时,主循环里塞了十几个任务,稍微改一个功能就得重新梳理时序,调试起来简直要命。
FreeRTOS的出现,说白了就是帮我们把「什么时候该干什么事」这个难题交给调度器去管。我们只需要把每个功能拆成独立的任务,剩下的——优先级、时间片、同步机制——操作系统全包了。
2.2 FreeRTOS源码结构
我第一次下载FreeRTOS源码时,说实话有点懵。文件夹不少,但真正核心的东西其实就那几块。我习惯把它的源码结构分成三部分来看:
| 目录/文件 | 作用 | 我的备注 |
|---|---|---|
| Source/tasks.c | 任务管理核心 | 任务创建、删除、挂起、恢复都在这里 |
| Source/queue.c | 队列与消息传递 | 任务间通信的命脉,我几乎每个项目都用 |
| Source/timers.c | 软件定时器 | 做周期性采样时特别好用 |
| Source/event_groups.c | 事件组 | 多条件触发场景,比如同时满足温度和湿度条件才动作 |
| Source/portable/ | 移植层代码 | 不同MCU的适配代码,我们STM32用ARM Cortex-M系列 |
| Source/include/ | 头文件 | FreeRTOS.h是总入口,其他API声明都在这里 |
嗯,这里要注意一点:portable目录下的代码是跟硬件绑定的。你换一个MCU,这部分就得换。但好消息是,FreeRTOS官方已经帮我们做好了STM32全系列的移植文件,基本不用自己动手改。
核心文件速记口诀:
任务队列定时器,事件组里传消息。
移植层里配MCU,头文件里找API。
2.3 移植到STM32平台
移植FreeRTOS到STM32,我做过不下十次了。每次换芯片型号,流程几乎一模一样。我总结了一个「三步走」的方法,你照着做基本不会出问题。
第一步:准备基础工程
先建一个能跑起来的裸机工程。用STM32CubeMX生成也好,自己手写也行。关键是要确认:时钟配置正确、SysTick定时器能用、串口能打印信息。我曾经跳过这一步,直接往一个没调通时钟的工程里塞FreeRTOS,结果折腾了两天才发现是HSE没起振……
第二步:复制FreeRTOS源码
从官方源码包里,把以下文件复制到你的工程目录:
FreeRTOS/Source/
├── tasks.c
├── queue.c
├── timers.c
├── event_groups.c
├── croutine.c(可选,协程,我基本不用)
├── portable/
│ ├── MemMang/
│ │ └── heap_4.c(推荐,防碎片化)
│ └── RVDS/
│ └── ARM_CM4F/
│ ├── port.c
│ └── portmacro.h
└── include/
└── (全部头文件)
我个人习惯用heap_4.c。它支持合并相邻空闲内存块,长时间运行不容易出现碎片。暖通空调设备一开就是几个月甚至几年,内存碎片问题必须提前考虑。
第三步:配置FreeRTOSConfig.h
这个文件是FreeRTOS的「总开关」。我每次移植都会仔细过一遍里面的宏定义。下面是一个针对STM32F4系列的典型配置:
#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H
/* 基础配置 */
#define configUSE_PREEMPTION 1 // 抢占式调度
#define configUSE_IDLE_HOOK 0 // 空闲钩子,一般不用
#define configUSE_TICK_HOOK 0 // 时钟钩子,调试时偶尔开
#define configCPU_CLOCK_HZ ( ( unsigned long ) 168000000 ) // STM32F407主频168MHz
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 ) // 1ms一个tick
#define configMAX_PRIORITIES ( 5 ) // 优先级数量,够用就行
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) // 最小栈大小,单位word
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 20 * 1024 ) ) // 堆大小20KB
#define configMAX_TASK_NAME_LEN ( 16 ) // 任务名最大长度
/* 功能开关 */
#define configUSE_16_BIT_TICKS 0 // 32位tick计数器
#define configIDLE_SHOULD_YIELD 1 // 空闲任务让出CPU
#define configUSE_MUTEXES 1 // 使用互斥量
#define configUSE_RECURSIVE_MUTEXES 1 // 使用递归互斥量
#define configUSE_COUNTING_SEMAPHORES 1 // 使用计数信号量
#define configUSE_QUEUE_SETS 0 // 队列集,复杂场景才用
/* 中断配置 */
#define configKERNEL_INTERRUPT_PRIORITY 255
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 191
/* 断言,调试时打开 */
#define configASSERT( x ) if( ( x ) == 0 ) { taskDISABLE_INTERRUPTS(); for( ;; ); }
#endif /* FREERTOS_CONFIG_H */
我的经验:
堆大小configTOTAL_HEAP_SIZE别设太大。STM32F407的RAM有192KB,但你要留给应用代码、全局变量、中断栈。我一般先设20KB,跑起来后用uxTaskGetSystemState()查看实际使用量,再微调。多了浪费,少了崩溃。
2.4 验证移植是否成功
移植完别急着写业务代码。先跑一个最简单的测试:创建两个任务,一个闪灯,一个打印信息。
void vTaskLED(void *pvParameters)
{
for( ;; )
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
vTaskDelay(pdMS_TO_TICKS(500)); // 延时500ms
}
}
void vTaskPrint(void *pvParameters)
{
uint32_t count = 0;
for( ;; )
{
printf("系统运行中... 计数: %lu\r\n", count++);
vTaskDelay(pdMS_TO_TICKS(1000)); // 延时1000ms
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
xTaskCreate(vTaskLED, "LED", 128, NULL, 1, NULL);
xTaskCreate(vTaskPrint, "Print", 256, NULL, 2, NULL);
vTaskStartScheduler(); // 启动调度器
// 正常情况下不会跑到这里
for( ;; );
}
如果LED以500ms间隔闪烁,串口每秒打印一次信息,恭喜你——移植成功了。
避坑指南:
我曾经在移植后遇到任务不调度的问题。查了半天,发现是HAL_Init()里把SysTick的中断优先级设成了0,而FreeRTOS要求SysTick优先级必须是最低的。解决办法:在HAL_Init()之后、vTaskStartScheduler()之前,手动调用HAL_NVIC_SetPriority(SysTick_IRQn, 15, 0)。嗯,这种细节最容易忽略。
2.5 小结
FreeRTOS的移植,说白了就是三步:准备工程、复制源码、配置头文件。真正花时间的不是移植本身,而是理解每个配置项背后的含义。我建议你拿到一块新的STM32板子时,第一件事就是先把FreeRTOS跑起来。哪怕只是闪个灯,也能帮你快速验证硬件和软件环境是否正常。
下一章,我们会深入任务管理的细节。到时候我会分享一个我在暖通空调项目中遇到的「任务饿死」案例——挺有意思的,你看了就知道。