第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层文件和内存管理方案消化一下,有问题随时交流。