第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跑起来。哪怕只是闪个灯,也能帮你快速验证硬件和软件环境是否正常。

下一章,我们会深入任务管理的细节。到时候我会分享一个我在暖通空调项目中遇到的「任务饿死」案例——挺有意思的,你看了就知道。