3. FreeRTOS移植与配置:在STM32平台上移植FreeRTOS
好,咱们进入实战环节。这一章我带你手把手把FreeRTOS移植到STM32上。说实话,移植这事儿看着复杂,其实套路固定。你只要摸清几个关键点,半小时就能搞定。
3.1 移植前的准备工作
先说说我个人的习惯。我一般会从FreeRTOS官网下载最新的源码包,或者直接用STM32CubeMX生成。两种方式我都用过,但如果你刚开始接触,我建议用CubeMX——省事,不容易漏文件。
你需要准备这些东西:
- 一个STM32开发板(我用的是STM32F407,但F1/F4系列都行)
- Keil MDK或STM32CubeIDE(我习惯用Keil)
- FreeRTOS v10.x源码(别用太老的版本,坑多)
- 一个串口调试工具(方便看打印信息)
3.2 核心文件结构
FreeRTOS的源码结构其实很清晰。我把它分成三块:
| 目录/文件 | 作用 | 是否需要修改 |
|---|---|---|
| Source/tasks.c | 任务管理核心 | 否 |
| Source/list.c | 链表操作 | 否 |
| Source/queue.c | 队列与信号量 | 否 |
| Source/portable/ | 平台相关代码 | 是(选对架构) |
| Source/include/ | 头文件 | 否 |
| FreeRTOSConfig.h | 配置文件 | 是(必须改) |
嗯,这里要注意。portable目录下有很多子目录,你得选对架构。STM32F4是Cortex-M4,所以选portable/GCC/ARM_CM4F或者portable/RVDS/ARM_CM4F,看你用什么编译器。
3.3 配置时钟节拍
时钟节拍是RTOS的心跳。说白了,就是系统每隔固定时间产生一次中断,让内核有机会检查要不要切换任务。
在FreeRTOS里,时钟节拍由configTICK_RATE_HZ这个宏控制。我一般设成1000,也就是1ms一个tick。
// FreeRTOSConfig.h 中的关键配置
#define configTICK_RATE_HZ ((TickType_t)1000) // 1ms一个tick
#define configCPU_CLOCK_HZ ((unsigned long)168000000) // STM32F407主频168MHz
为什么会是1000?你想想看,如果设成100,那10ms才切换一次任务,响应太慢了。如果设成10000,中断太频繁,CPU光切换任务了,正经事干不了。1000是个平衡点。
时钟节拍的实现依赖硬件定时器。在STM32上,通常用SysTick定时器。FreeRTOS的移植层已经帮我们写好了vPortSetupTimerInterrupt()这个函数,它会自动配置SysTick。
3.4 任务堆栈大小设置
任务堆栈大小,这是个容易出问题的地方。我见过太多人因为堆栈设小了,程序跑着跑着就莫名其妙地死机。
每个任务都有自己的堆栈,用于保存局部变量、函数调用上下文等。FreeRTOS用configMINIMAL_STACK_SIZE定义最小堆栈,通常给空闲任务用。
// 堆栈大小配置
#define configMINIMAL_STACK_SIZE ((unsigned short)128) // 最小堆栈,单位是字(4字节)
#define configTOTAL_HEAP_SIZE ((size_t)(20 * 1024)) // 总堆大小20KB
// 创建任务时指定堆栈
xTaskCreate(
vTaskFunction, // 任务函数
"Task1", // 任务名
256, // 堆栈大小,256字 = 1KB
NULL, // 参数
1, // 优先级
NULL // 任务句柄
);
堆栈大小怎么估算?我有个土办法:先设一个较大的值(比如512字),跑起来后用uxTaskGetStackHighWaterMark()查看实际使用量,再调整到1.5倍左右。
3.5 调度策略选择
FreeRTOS支持两种调度策略:抢占式和协作式。默认是抢占式,这也是绝大多数场景的选择。
| 策略 | 宏定义 | 特点 | 适用场景 |
|---|---|---|---|
| 抢占式 | configUSE_PREEMPTION = 1 | 高优先级任务随时可以打断低优先级任务 | 大多数实时系统 |
| 协作式 | configUSE_PREEMPTION = 0 | 任务主动让出CPU(调用taskYIELD()) | 简单轮询、低功耗场景 |
我个人几乎只用抢占式。为什么?因为售货机这种系统,你想想看,用户投币后必须立刻响应,不能等当前任务慢慢执行完。抢占式能保证紧急任务第一时间拿到CPU。
// FreeRTOSConfig.h 中的调度策略配置
#define configUSE_PREEMPTION 1 // 1:抢占式 0:协作式
#define configUSE_TIME_SLICING 1 // 时间片轮转,同优先级任务轮流执行
#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1 // 使用硬件优化任务选择
这里还有个configUSE_TIME_SLICING,它控制同优先级任务是否按时间片轮转。我建议打开,否则同优先级的任务里,如果有一个不主动让出CPU,其他任务就永远没机会运行。
3.6 完整的FreeRTOSConfig.h示例
下面是我常用的配置模板,你可以直接拿来用:
#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 )
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
#define configMAX_PRIORITIES ( 5 )
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 20 * 1024 ) )
#define configMAX_TASK_NAME_LEN ( 16 )
#define configUSE_TRACE_FACILITY 1
#define configUSE_16_BIT_TICKS 0
#define configIDLE_SHOULD_YIELD 1
#define configUSE_MUTEXES 1
#define configUSE_RECURSIVE_MUTEXES 1
#define configUSE_COUNTING_SEMAPHORES 1
#define configUSE_ALTERNATIVE_API 0
#define configQUEUE_REGISTRY_SIZE 8
#define configUSE_QUEUE_SETS 0
#define configUSE_TIME_SLICING 1
#define configUSE_NEWLIB_REENTRANT 0
#define configENABLE_BACKWARD_COMPATIBILITY 0
#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 5
// 内存管理
#define configSUPPORT_STATIC_ALLOCATION 0
#define configSUPPORT_DYNAMIC_ALLOCATION 1
#define configAPPLICATION_ALLOCATED_HEAP 0
// 协程(一般不用)
#define configUSE_CO_ROUTINES 0
#define configMAX_CO_ROUTINE_PRIORITIES 2
// 中断优先级
#define configKERNEL_INTERRUPT_PRIORITY 255
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 191
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
// 断言
#define configASSERT( x ) if( ( x ) == 0 ) { taskDISABLE_INTERRUPTS(); for( ;; ); }
#endif /* FREERTOS_CONFIG_H */
这个配置我用了好几年,在多个项目里验证过。你根据自己的芯片资源调整configTOTAL_HEAP_SIZE和configMAX_PRIORITIES就行。
3.7 移植后的验证
移植完别急着写业务代码。先跑个简单的测试,确认系统能正常工作:
void vTestTask(void *pvParameters)
{
while(1)
{
printf("Task is running, tick = %d\n", xTaskGetTickCount());
vTaskDelay(pdMS_TO_TICKS(1000)); // 延时1秒
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
xTaskCreate(vTestTask, "Test", 256, NULL, 1, NULL);
vTaskStartScheduler(); // 启动调度器
// 正常情况下不会执行到这里
while(1);
}
如果串口能每秒打印一次tick值,恭喜你,移植成功了。如果没反应,先检查SysTick中断有没有使能,再检查vApplicationTickHook有没有定义(如果开启了tick hook)。