4、FreeRTOS移植:源码获取、移植步骤、任务创建与调度机制
好,咱们进入第四讲。这一章要聊的是FreeRTOS移植。说实话,很多初学者一听到「移植」两个字就头大,觉得那是大神才干的事。其实没那么玄乎。我当年第一次移植FreeRTOS到STM32上,也就花了一个下午。关键是你要知道每一步在干什么,而不是机械地复制粘贴。
4.1 FreeRTOS源码获取
先说说源码从哪来。我个人习惯直接从FreeRTOS官网下载,或者去GitHub上拉官方仓库。网址我就不写了,你搜一下「FreeRTOS official」就能找到。
下载下来后,你会看到一个叫 FreeRTOSv202212.01 这样的文件夹(版本号会变)。里面主要有两部分:
- FreeRTOS:核心源码,包括
tasks.c、queue.c、list.c这些文件 - FreeRTOS-Plus:一些扩展组件,比如TCP/IP协议栈、FAT文件系统等
我们做Modbus网关,只需要核心部分就够了。别贪多,先把基础跑起来。
4.2 移植步骤:从零到跑起来
移植说白了就是让FreeRTOS认识你的硬件。我把它拆成四个步骤,你照着做就行。
4.2.1 准备一个裸机工程
你得先有一个能正常跑起来的裸机工程。比如用STM32CubeMX生成一个,点个LED、串口打印个「Hello World」。这一步是为了确认你的硬件和编译器没问题。
嗯,这里要注意:时钟配置一定要对。FreeRTOS的心跳(SysTick)依赖系统时钟,时钟错了,任务调度就全乱套了。
4.2.2 添加FreeRTOS源码
把下载好的源码复制到你的工程里。通常需要这几个文件:
tasks.c、queue.c、list.c、timers.c、event_groups.cportable文件夹下的移植层代码,比如port.c和portmacro.hFreeRTOSConfig.h配置文件
我习惯把源码放在一个叫 FreeRTOS 的文件夹里,和用户代码分开。这样看着清爽,也好维护。
4.2.3 配置FreeRTOSConfig.h
这个文件是FreeRTOS的灵魂。里面定义了系统能用多少任务、堆栈多大、时钟频率多少等等。我贴一个我常用的配置模板:
#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 ) 72000000 )
#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_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_QUEUE_SETS 0
/* 钩子函数 */
#define configCHECK_FOR_STACK_OVERFLOW 2
#define configUSE_MALLOC_FAILED_HOOK 1
#endif /* FREERTOS_CONFIG_H */
重点: configTOTAL_HEAP_SIZE 决定了系统可用内存。Modbus网关通常需要处理多个串口和网络连接,我建议至少给20KB。太小了任务一多就崩。
4.2.4 实现移植接口
FreeRTOS需要几个底层接口才能跑起来。主要是这三个:
- vPortSetupTimerInterrupt():配置系统心跳定时器。通常用SysTick,也可以用一个通用定时器。
- vPortYield():触发任务切换。一般通过PendSV中断实现。
- prvStartFirstTask():启动第一个任务。这个函数会初始化栈指针,然后跳转到第一个任务。
如果你用的是Cortex-M3/M4内核,FreeRTOS的portable层已经帮你写好了大部分代码。你只需要在启动文件里把 PendSV_Handler 和 SysTick_Handler 的中断处理函数替换成FreeRTOS的版本就行。
configMAX_SYSCALL_INTERRUPT_PRIORITY。我一开始没注意,结果串口中断老是抢任务切换的优先级,导致系统卡死。查了整整一天才找到原因。
4.3 任务创建:让代码「活」起来
移植完成后,第一件事就是创建任务。任务说白了就是一个无限循环的函数。FreeRTOS会帮你管理这些函数的执行顺序。
创建任务用 xTaskCreate() 函数。原型是这样的:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 堆栈大小(单位是字,不是字节)
void *pvParameters, // 任务参数
UBaseType_t uxPriority, // 优先级(0最低,configMAX_PRIORITIES-1最高)
TaskHandle_t *pxCreatedTask // 任务句柄(可以传NULL)
);
举个例子,创建一个LED闪烁任务:
void vLEDTask(void *pvParameters)
{
(void)pvParameters; // 防止编译器警告
while(1)
{
GPIO_ToggleBits(GPIOA, GPIO_Pin_5);
vTaskDelay(pdMS_TO_TICKS(500)); // 延时500ms
}
}
// 在main函数中创建任务
int main(void)
{
// 硬件初始化
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
// 创建任务
xTaskCreate(vLEDTask, "LED", 128, NULL, 1, NULL);
// 启动调度器
vTaskStartScheduler();
// 正常情况下不会执行到这里
while(1);
}
你看,就这么简单。一个任务就创建好了。你想想看,如果不用RTOS,你得自己写一个延时循环,还得处理各种中断嵌套。有了FreeRTOS,你只需要关心每个任务自己的逻辑就行。
4.4 调度机制:任务是怎么切换的
任务创建好了,那它们是怎么轮流执行的?这就涉及到FreeRTOS的调度机制了。
FreeRTOS用的是抢占式调度。什么意思?就是高优先级的任务可以随时打断低优先级的任务。系统每过一个tick(比如1ms),就会检查一次有没有更高优先级的任务需要运行。
调度流程大致是这样的:
- SysTick中断触发
- 中断服务函数调用
xTaskIncrementTick() - 这个函数检查所有任务的时间片是否用完
- 如果当前任务的时间片到了,就触发PendSV中断
- PendSV中断里执行
vTaskSwitchContext(),选出下一个要运行的任务 - 恢复新任务的上下文,继续执行
整个过程对用户来说是透明的。你只需要设置好优先级,剩下的交给FreeRTOS。
4.5 任务状态与状态切换
任务在运行过程中,会在几个状态之间切换。理解这个对调试很有帮助。
| 状态 | 说明 | 如何进入 |
|---|---|---|
| 运行态 | 正在使用CPU | 被调度器选中 |
| 就绪态 | 可以运行,但CPU被别的任务占着 | 创建任务、从阻塞态恢复 |
| 阻塞态 | 等待某个事件(延时、信号量、队列等) | 调用vTaskDelay()、xQueueReceive()等 |
| 挂起态 | 不参与调度 | 调用vTaskSuspend() |
我刚开始学的时候,总搞不清阻塞态和挂起态的区别。后来在项目里调试一个串口接收超时的问题,才真正理解了:阻塞态是「我暂时不跑,但随时准备回来」,挂起态是「别叫我,我自己会醒」。
4.6 常见问题与调试技巧
移植过程中难免遇到问题。我总结几个最常见的:
- 任务不运行:检查
vTaskStartScheduler()是否调用了,以及SysTick中断是否使能。 - 堆栈溢出:在
FreeRTOSConfig.h里把configCHECK_FOR_STACK_OVERFLOW设为2,然后实现vApplicationStackOverflowHook()函数。一旦溢出,这个函数会被调用。 - 内存不足:检查
configTOTAL_HEAP_SIZE是否够用。可以用xPortGetFreeHeapSize()查看剩余堆内存。
调试利器: FreeRTOS提供了 vTaskList() 和 vTaskGetRunTimeStats() 两个函数,可以打印所有任务的状态和CPU占用率。我每次调试都会在串口终端里跑一下这两个函数,问题一目了然。
好了,这一章的内容就到这。移植FreeRTOS其实没那么难,关键是动手做一遍。下一章我们会讲Modbus协议栈的移植,到时候这些任务就会真正派上用场了。