第1章:FreeRTOS移植——从源码到跑起来
大家好,我是老张。做嵌入式十几年了,从裸机一路折腾到RTOS。今天咱们聊聊FreeRTOS在STM32上的移植。说实话,这活儿看着简单,但坑不少。我当年第一次移植时,愣是折腾了两天才把系统时钟调对。
好,不废话了。咱们直接开干。
1.1 FreeRTOS源码结构
先看看FreeRTOS的源码长什么样。你从官网下载后,解压出来大概是这样:
FreeRTOS/
├── Source/
│ ├── include/ // 头文件,别乱动
│ ├── portable/ // 移植层,重点在这里
│ ├── tasks.c // 任务管理核心
│ ├── queue.c // 队列
│ ├── timers.c // 软件定时器
│ └── event_groups.c // 事件组
├── Demo/
│ └── CORTEX_STM32F103_Keil/ // 官方示例
└── License/
我个人习惯把Source文件夹整个拷到项目里。但注意,portable文件夹里东西很多,你只需要保留跟MCU相关的部分。比如STM32F103,就保留portable/GCC/ARM_CM3(如果你用GCC)或者portable/RVDS/ARM_CM3(如果用Keil)。
1.2 在STM32上的移植步骤
移植说白了就三步:拷文件、改配置、调中断。嗯,听起来简单,但每一步都有讲究。
第一步:准备基础工程
你得先有一个能跑起来的STM32裸机工程。GPIO能点灯,串口能打印。这个我就不多说了,大家都会。
第二步:添加FreeRTOS源码
把下面这些文件加到工程里:
Source/tasks.cSource/queue.cSource/list.cSource/timers.c(如果你要用软件定时器)Source/portable/[你的编译器]/[你的MCU架构]/port.cSource/portable/[你的编译器]/[你的MCU架构]/portmacro.h
举个例子,我用Keil + STM32F103,就加:
Source/tasks.c
Source/queue.c
Source/list.c
Source/portable/RVDS/ARM_CM3/port.c
Source/portable/RVDS/ARM_CM3/portmacro.h
第三步:配置FreeRTOSConfig.h
这个文件是FreeRTOS的“大脑”。你需要自己创建一个,放在工程里。核心配置项如下:
| 配置项 | 说明 | 推荐值 |
|---|---|---|
| configUSE_PREEMPTION | 是否使用抢占式调度 | 1(抢占式更实用) |
| configCPU_CLOCK_HZ | CPU主频,单位Hz | 根据你的晶振设置,比如72000000 |
| configTICK_RATE_HZ | 系统时钟节拍频率 | 1000(1ms一个tick) |
| configMAX_PRIORITIES | 最大任务优先级数 | 5(够用,别设太大浪费RAM) |
| configMINIMAL_STACK_SIZE | 空闲任务栈大小 | 128(单位是字,不是字节) |
| configTOTAL_HEAP_SIZE | 总堆大小 | 根据你RAM大小,比如4096 |
这里有个坑:configCPU_CLOCK_HZ一定要跟你的实际主频一致。我见过有人用72MHz的晶振,但这里写成了72000(少写了三个零),结果系统时钟全乱套了。
1.3 配置系统时钟和中断
好,到了最关键的环节。系统时钟和中断配置不好,FreeRTOS根本跑不起来。
系统时钟配置
FreeRTOS需要一个硬件定时器来产生系统节拍(tick)。在STM32上,通常用SysTick定时器。嗯,就是那个24位的系统滴答定时器。
在port.c文件里,你会看到这个函数:
void vPortSetupTimerInterrupt(void)
{
// 配置SysTick
// 重装载值 = 主频 / 节拍频率 - 1
// 比如72MHz主频,1000Hz节拍:
// 72000000 / 1000 - 1 = 71999
SysTick->LOAD = (configCPU_CLOCK_HZ / configTICK_RATE_HZ) - 1;
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
}
说白了,就是告诉SysTick:每1ms给我产生一次中断。这个中断就是FreeRTOS的“心跳”。
中断优先级配置
这是新手最容易翻车的地方。STM32的中断优先级分为抢占优先级和子优先级。FreeRTOS要求:
- 所有中断的抢占优先级必须相同(或者至少SysTick和PendSV相同)
- SysTick和PendSV的优先级必须是最低的
在FreeRTOSConfig.h里,有个配置项:
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
这里解释一下:
configLIBRARY_LOWEST_INTERRUPT_PRIORITY:最低优先级,给SysTick和PendSV用configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY:能调用FreeRTOS API的最高中断优先级。比这个高的中断,不能调用任何FreeRTOS函数
我曾经在一个项目中,把某个外设中断优先级设得比SysTick还高,然后在里面调用了xQueueSendFromISR。结果呢?系统时不时死机,查了两天才发现是优先级冲突。
xQueueSendFromISR而不是xQueueSend。否则,系统会进入断言失败。
PendSV中断
PendSV是FreeRTOS实现任务切换的关键。它是个可挂起的异常,优先级最低。当系统需要切换任务时,会触发PendSV,然后在PendSV的处理函数里完成上下文切换。
在port.c里,PendSV的处理函数是:
void xPortPendSVHandler(void)
{
// 这里保存当前任务的寄存器
// 然后加载下一个任务的寄存器
// 最后返回,CPU就跑到新任务里去了
__asm volatile (
" mrs r0, psp \n"
" isb \n"
" ldr r3, =pxCurrentTCB \n"
" ldr r2, [r3] \n"
// ... 省略中间代码
" bx r14 \n"
);
}
嗯,这段汇编代码看着挺吓人。但你不用改它,FreeRTOS已经帮你写好了。你只需要确保:
- PendSV的中断向量表里有这个函数名
- PendSV的优先级是最低的
1.4 验证移植是否成功
移植完了,怎么知道对不对?写个最简单的测试:
void vTask1(void *pvParameters)
{
while(1)
{
GPIO_ToggleBits(GPIOA, GPIO_Pin_0); // 翻转LED
vTaskDelay(500); // 延时500ms
}
}
int main(void)
{
// 硬件初始化
GPIO_Init();
// 创建任务
xTaskCreate(vTask1, "Task1", 128, NULL, 1, NULL);
// 启动调度器
vTaskStartScheduler();
// 正常情况下不会跑到这里
while(1);
}
如果LED以1秒的频率闪烁(亮500ms,灭500ms),恭喜你,移植成功了!
好了,这一章就到这里。下一章咱们聊聊任务创建和调度,那才是RTOS真正好玩的地方。