第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)。

小提示: 别把整个portable文件夹都加进工程,编译会报一堆重复定义。我见过有人这么干,然后跑来问我为什么编译不过...

1.2 在STM32上的移植步骤

移植说白了就三步:拷文件、改配置、调中断。嗯,听起来简单,但每一步都有讲究。

第一步:准备基础工程

你得先有一个能跑起来的STM32裸机工程。GPIO能点灯,串口能打印。这个我就不多说了,大家都会。

第二步:添加FreeRTOS源码

把下面这些文件加到工程里:

  • Source/tasks.c
  • Source/queue.c
  • Source/list.c
  • Source/timers.c(如果你要用软件定时器)
  • Source/portable/[你的编译器]/[你的MCU架构]/port.c
  • Source/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
注意: 不同编译器用的port文件不一样。GCC用GCC文件夹下的,Keil用RVDS或ARMCC文件夹下的。我曾经帮人排查问题,发现他Keil工程里加的是GCC的port文件...那肯定跑不起来啊。

第三步:配置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的“心跳”。

核心要点: SysTick的优先级一定要设置为最低!因为FreeRTOS在临界区时会屏蔽中断,如果SysTick优先级太高,会导致系统卡死。

中断优先级配置

这是新手最容易翻车的地方。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。结果呢?系统时不时死机,查了两天才发现是优先级冲突。

避坑指南: 在中断服务函数里调用FreeRTOS API时,一定要用带“FromISR”后缀的版本。比如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),恭喜你,移植成功了!

调试技巧: 如果LED不闪,先检查SysTick有没有产生中断。可以在SysTick_Handler里加个断点。如果断点没触发,说明时钟配置有问题。如果触发了但任务不切换,检查PendSV的优先级是不是最低。

好了,这一章就到这里。下一章咱们聊聊任务创建和调度,那才是RTOS真正好玩的地方。