第4章 FreeRTOS内核移植:源码目录结构分析、port层文件修改、内存管理方案选择

好,咱们进入正题。上一章我们把FreeRTOS的源码包下载下来了,解压后一堆文件夹,很多人第一眼就懵了。别急,我带你捋一遍。

4.1 源码目录结构分析

FreeRTOS的源码,说白了就两大块:核心源码移植层代码。核心源码是通用的,不管你在什么芯片上跑,这部分基本不用动。移植层代码,就是咱们今天要动刀的地方。

打开源码包,你会看到这样的结构:

FreeRTOS/
├── FreeRTOS-Plus/        # 扩展组件(TCP/IP、CLI等)
├── FreeRTOS/             # 核心源码
│   ├── Source/           # 内核源码
│   │   ├── include/      # 头文件
│   │   ├── portable/     # 移植层(重点!)
│   │   └── tasks.c, queue.c ...  # 核心文件
│   └── Demo/             # 官方示例工程
└── tools/                # 辅助工具

我个人习惯,先把Source目录下的核心文件过一遍。tasks.c、queue.c、timers.c这些,是内核的骨架,一般不动。真正需要改的,都在portable目录里。

核心原则:移植FreeRTOS,本质上就是告诉内核「你的CPU长什么样、怎么切换任务、怎么处理中断」。这些信息,全部写在port层文件里。

4.2 port层文件修改

port层,全称是portable,意思是「可移植的」。它里面按编译器分文件夹,比如GCC、IAR、Keil。再往下,按架构分,比如ARM Cortex-M3、M4、RISC-V等等。

举个例子,如果你用的是STM32F103(Cortex-M3内核),Keil编译器,那你要找的路径是:

FreeRTOS/Source/portable/Keil/ARM_CM3/
├── port.c
├── portmacro.h
└── portasm.s

这三个文件,就是移植的核心。我简单说一下每个文件的作用:

文件名 作用
port.c 任务切换、中断处理、系统时钟配置的C语言实现
portmacro.h 数据类型重定义、临界区宏、栈增长方向等
portasm.s 汇编实现的 PendSV 异常处理(任务切换核心)

portmacro.h 修改要点

这个文件里,最关键的几行是:

/* 数据类型重定义 */
#define portSTACK_TYPE    uint32_t
#define portBASE_TYPE     long

/* 栈增长方向:向下增长 */
#define portSTACK_GROWTH  ( -1 )

/* 临界区宏 */
#define portDISABLE_INTERRUPTS()    __disable_irq()
#define portENABLE_INTERRUPTS()     __enable_irq()

嗯,这里要注意:portSTACK_GROWTH的值,ARM Cortex-M是-1(向下增长),但有些架构比如AVR,是1(向上增长)。搞反了,任务一跑就崩。我曾经在这个坑里趴了一整天,最后发现是宏定义写反了。

port.c 修改要点

port.c里最重要的函数是vPortSetupTimerInterrupt()。这个函数配置系统滴答定时器(SysTick),决定每个tick多长时间。比如你想让系统时钟节拍为1ms:

void vPortSetupTimerInterrupt( void )
{
    /* 配置SysTick,1ms中断一次 */
    SysTick_Config( SystemCoreClock / 1000 );
}

这里SystemCoreClock是你芯片的主频。如果主频是72MHz,那SysTick_Config(72000)就是1ms。我建议你把这个值做成宏定义,方便以后改。

我的经验:刚开始移植时,先把tick设成10ms(100Hz),跑起来稳定了再改回1ms。这样万一有问题,调试时不会因为中断太频繁而手忙脚乱。

4.3 内存管理方案选择(heap_1~heap_5)

内存管理,说白了就是FreeRTOS怎么给你分配任务栈、队列、信号量这些内存。官方提供了5种方案,放在Source/portable/MemMang/目录下。

我直接给你一个选择指南:

方案 特点 适用场景
heap_1 最简单,只分配不释放 任务栈、队列等一次性分配,永不删除
heap_2 支持释放,但可能碎片 简单场景,不频繁创建删除
heap_3 包装了malloc/free 你已经有标准库,想用现成的
heap_4 合并相邻空闲块,防碎片 推荐!大多数场景的首选
heap_5 支持多个不连续内存区域 外部RAM + 内部RAM混合使用

我个人最推荐heap_4。为什么?因为它能合并相邻的空闲内存块,碎片问题比heap_2好得多。我在一个项目里,一开始用了heap_2,跑了三天后系统莫名其妙挂掉,查了半天发现是内存碎片导致分配失败。换成heap_4后,再也没出过问题。

避坑指南:我曾经在一个产品里用了heap_1,后来需求变更要动态删除任务,结果heap_1不支持释放,只能重启。所以,除非你100%确定不会动态创建删除,否则别用heap_1。

heap_4 的配置

使用heap_4,你需要在FreeRTOSConfig.h里定义堆大小:

#define configTOTAL_HEAP_SIZE    ( ( size_t ) 20 * 1024 )  /* 20KB */

这个值怎么定?我一般先给个保守值,比如20KB。编译下载后,通过uxTaskGetStackHighWaterMark()查看每个任务的栈使用峰值,再调整。说白了,就是「先跑起来,再优化」。

heap_5 的特殊用法

如果你的芯片既有内部SRAM,又接了外部SDRAM,heap_5就派上用场了。你需要先定义内存区域:

/* 定义两个内存区域 */
const HeapRegion_t xHeapRegions[] = 
{
    { ( uint8_t * ) 0x20000000, 0x10000 },  /* 内部SRAM,64KB */
    { ( uint8_t * ) 0x60000000, 0x80000 },  /* 外部SDRAM,512KB */
    { NULL, 0 }                              /* 结束标志 */
};

/* 初始化时调用 */
vPortDefineHeapRegions( xHeapRegions );

嗯,这里要注意:heap_5必须在创建任何内核对象之前调用vPortDefineHeapRegions()。我见过有人把它放在main函数最后,结果任务创建时直接硬故障。

4.4 移植后的验证

移植完,怎么知道对不对?我一般写一个最简单的测试:

void vTask1( void *pvParameters )
{
    while(1)
    {
        /* 让LED闪烁,证明任务在跑 */
        GPIO_ToggleBits( GPIOA, GPIO_Pin_0 );
        vTaskDelay( 500 );
    }
}

int main( void )
{
    /* 硬件初始化 */
    SystemInit();
    
    /* 创建任务 */
    xTaskCreate( vTask1, "Task1", 128, NULL, 1, NULL );
    
    /* 启动调度器 */
    vTaskStartScheduler();
    
    /* 正常情况下不会跑到这里 */
    while(1);
}

如果LED以500ms的间隔闪烁,恭喜你,移植成功了。如果不闪,先检查SysTick配置,再检查中断向量表里PendSV和SysTick的优先级设置。这两个中断的优先级,必须设为最低(数值最大),否则FreeRTOS无法正常工作。

我的调试习惯:移植完成后,先跑一个任务,确认调度器能正常工作。再加第二个任务,测试任务切换。最后再加中断,测试临界区保护。一步一步来,别想一口吃成胖子。

好了,这一章的内容就到这里。下一章,我们会把移植好的FreeRTOS跑在具体的电子标签硬件上,到时候会涉及更多硬件相关的细节。你先把今天讲的port层文件和内存管理方案消化一下,有问题随时交流。