4. FreeRTOS核心概念
好,咱们正式开始动手移植。这一章我带你走一遍完整的移植流程,重点放在 FreeRTOSConfig.h 这个配置文件的修改上。说实话,很多初学者卡就卡在这个文件上——配置不对,系统要么跑不起来,要么功耗高得吓人。
我个人习惯把移植分成两大块:复制文件 和 修改配置。复制文件是体力活,改配置才是技术活。咱们先干体力活,再啃硬骨头。
4.1 移植前的准备工作
动手之前,你得先准备好三样东西:
- 一个能正常运行的STM32裸机工程(我用的是STM32F103和STM32F407各一套)
- FreeRTOS官方源码包(去官网下载V10.4.6或更新版本)
- 你的开发板原理图(主要是为了确认晶振频率和中断优先级分组)
4.2 移植步骤详解
移植说白了就三步:复制 → 包含 → 修改。我画了张流程图,你先看个全貌。
4.3 复制文件到工程目录
打开你下载的FreeRTOS源码包,找到 FreeRTOS/Source 目录。你需要复制以下文件:
| 源文件/文件夹 | 复制到工程的位置 | 说明 |
|---|---|---|
tasks.c |
FreeRTOS/Source/ |
任务管理核心 |
list.c |
FreeRTOS/Source/ |
链表操作 |
queue.c |
FreeRTOS/Source/ |
队列和信号量 |
timers.c |
FreeRTOS/Source/ |
软件定时器 |
event_groups.c |
FreeRTOS/Source/ |
事件组 |
portable/MemMang/heap_4.c |
FreeRTOS/Source/portable/MemMang/ |
内存管理(推荐heap_4) |
portable/RVDS/ARM_CM3/ 或 ARM_CM4F/ |
FreeRTOS/Source/portable/ |
移植层代码(F1选CM3,F4选CM4F) |
include/ 文件夹 |
FreeRTOS/Source/include/ |
所有头文件 |
ARM_CM4F 目录下的port.c和portmacro.h。用错了会导致任务切换时浮点寄存器没保存,计算结果全乱套。我曾经帮一个客户排查了三天,最后发现就是port文件选错了。
4.4 添加头文件路径
文件复制完后,打开你的IDE(我习惯用Keil,但MDK、IAR、STM32CubeIDE都一样),在工程设置里添加以下头文件路径:
FreeRTOS/Source/includeFreeRTOS/Source/portable/RVDS/ARM_CM3(F1)或ARM_CM4F(F4)- 你的工程主目录(放FreeRTOSConfig.h的地方)
嗯,这一步没什么技术含量,但漏掉一个路径编译就会报一堆"file not found"。我建议你加完路径后先编译一次,确认所有头文件都能找到。
4.5 修改FreeRTOSConfig.h——核心中的核心
好了,重头戏来了。FreeRTOSConfig.h 是FreeRTOS的"性格"配置文件。你想想看,系统用多大的堆栈、支持多少个优先级、要不要用钩子函数——全在这里定义。
FreeRTOS官方提供了一个模板文件,在 FreeRTOS/Demo/CORTEX_STM32F103_Keil 目录下。我建议你拿这个模板改,别自己从头写。
下面是我针对STM32F1/F4系列整理的关键配置项,你直接照着改就行:
| 配置宏 | F1推荐值 | F4推荐值 | 说明 |
|---|---|---|---|
configCPU_CLOCK_HZ |
72000000 | 168000000 | 必须与你的系统时钟一致 |
configTICK_RATE_HZ |
1000 | 1000 | 1ms一个tick,够用 |
configMAX_PRIORITIES |
5 | 5 | 够用就行,别贪多 |
configMINIMAL_STACK_SIZE |
128 | 128 | 单位是word,不是byte |
configTOTAL_HEAP_SIZE |
10240 | 20480 | 根据你的RAM大小调整 |
configUSE_PREEMPTION |
1 | 1 | 抢占式调度 |
configUSE_IDLE_HOOK |
0 | 0 | 低功耗时才需要 |
configUSE_TICK_HOOK |
0 | 0 | 一般用不到 |
configLIBRARY_LOWEST_INTERRUPT_PRIORITY |
15 | 15 | STM32用4位优先级,最低是15 |
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY |
5 | 5 | 优先级高于5的中断不能调用FreeRTOS API |
configCPU_CLOCK_HZ 必须和你工程里的系统时钟一致。F1默认是72MHz,F4默认是168MHz。如果你用了外部晶振或者改了PLL倍频,这里一定要同步修改。我见过有人F4配了168MHz,但实际跑在180MHz,结果tick时间全乱套。
下面是我常用的一个配置片段,你直接复制到你的 FreeRTOSConfig.h 里:
/* 系统时钟和Tick配置 */
#define configCPU_CLOCK_HZ ((unsigned long)72000000) // F1用72MHz,F4改成168000000
#define configTICK_RATE_HZ ((TickType_t)1000)
#define configUSE_PREEMPTION 1
#define configUSE_IDLE_HOOK 0
#define configUSE_TICK_HOOK 0
/* 任务和堆栈配置 */
#define configMAX_PRIORITIES (5)
#define configMINIMAL_STACK_SIZE ((unsigned short)128)
#define configTOTAL_HEAP_SIZE ((size_t)(10 * 1024)) // F4建议20KB
#define configMAX_TASK_NAME_LEN (16)
/* 中断优先级配置(STM32专用) */
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
#define configKERNEL_INTERRUPT_PRIORITY configLIBRARY_LOWEST_INTERRUPT_PRIORITY
#define configMAX_SYSCALL_INTERRUPT_PRIORITY configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
/* 功能开关 */
#define configUSE_MUTEXES 1
#define configUSE_RECURSIVE_MUTEXES 1
#define configUSE_COUNTING_SEMAPHORES 1
#define configUSE_QUEUE_SETS 0
#define configUSE_TASK_NOTIFICATIONS 1
4.6 中断优先级配置——最容易踩坑的地方
STM32的中断优先级分组和FreeRTOS的配合,是移植中最容易出问题的地方。我专门说一下。
STM32F1/F4默认使用4位优先级分组(NVIC_PriorityGroup_4),也就是4位全部用于抢占优先级,没有子优先级。FreeRTOS要求中断优先级分组必须是4位抢占优先级模式。
你在系统初始化时一定要调用:
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
为什么?因为FreeRTOS内部用 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 来判断一个中断能不能调用FreeRTOS API。如果优先级分组不对,这个判断逻辑就全错了。
4.7 编写中断处理函数
FreeRTOS接管了SysTick和PendSV两个中断。你需要把裸机工程里的 SysTick_Handler 替换成FreeRTOS的版本:
#include "FreeRTOS.h"
#include "task.h"
void SysTick_Handler(void)
{
if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
{
xPortSysTickHandler();
}
}
void PendSV_Handler(void)
{
vPortPendSVHandler();
}
void SVC_Handler(void)
{
vPortSVCHandler();
}
注意:如果你的工程里原来有 SysTick_Handler 函数(比如HAL库生成的),一定要把它注释掉,否则会重复定义。
4.8 编译验证
所有文件加好、配置改好之后,编译一次。如果报错,99%是以下三个原因:
- 头文件路径没加全——检查include路径
- 中断处理函数重复定义——检查startup文件里的弱定义
- configCPU_CLOCK_HZ和实际时钟不匹配——用示波器或者调试器确认系统时钟
编译通过后,写一个简单的测试任务:
void vTestTask(void *pvParameters)
{
while(1)
{
GPIO_ToggleBits(GPIOA, GPIO_Pin_5); // 翻转LED
vTaskDelay(pdMS_TO_TICKS(500)); // 延时500ms
}
}
int main(void)
{
// 硬件初始化...
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
xTaskCreate(vTestTask, "Test", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
vTaskStartScheduler();
while(1); // 正常情况下不会跑到这里
}
如果LED以500ms的间隔闪烁,恭喜你,移植成功了!
vTaskDelay 的延时时间。把 pdMS_TO_TICKS(500) 改成 500(假设tick是1ms),排除宏定义的问题。如果还不闪,用调试器在 vTaskStartScheduler 之后设断点,看看程序是不是卡在启动调度器那一步。