一、前言
下载平台:STM32F407VGT6
代码使用平台:VSCode
编译器:arm-none-aebi-gcc
程序下载工具:STlink
批处理工具:make
移植的FreeRTOS版本:V11.2.0
其实此方法并不局限在arm-none-aebi-gcc中,此方法对于Keil5也是可以使用的,
只不过复制的一些文件不同而已。
欢迎来我的仓库下载我的代码,FreeRTOS项目在freertos分支内:
https://gitee.com/timing_matlab/stm32-f407-vgt6-project.git
这里分享一下在移植过程中需要注意的点!
二、移植FreeRTOS的文件
1、不管三七二十一,先拿源码
来到FreeRTOS的github代码仓库,下载源码或者直接克隆仓库
shell
https://github.com/FreeRTOS/FreeRTOS-Kernel/archive/refs/tags/V11.2.0.tar.gz
或者
克隆的话一定要加--recurse-submodules,不然得到的源码不完整!
shell
git clone https://github.com/FreeRTOS/FreeRTOS.git --recurse-submodules

2、创建一个文件夹保存移植过来的源码
创建一个文件夹ThirdParty,表示这是第三方库,然后再里面创建FreeRTOS文件夹,然后创建inc与src,保存头文件与源文件。
然后我们来到FreeRTOS的源文件开始移植!
第一步来到源码的./FreeRTOS/FreeRTOS/Source
复制当前目录下的全部的c语言源文件,到我们创建的目录下的src里面了
复制完成之后的src文件夹下
第二步来到源码的./FreeRTOS/FreeRTOS/Source/include
复制当前目录下全部的头文件,到我们创建目录的inc里面
复制完成之后的inc文件夹下
第三步来到源码的./FreeRTOS/FreeRTOS/Source/portable

此处需要移植两处地方的源文件与头文件,这里与前两步不太一样。前两步无论在什么平台都是通用的,但是这一步不同平台有不同平台的移植方式。
1、移植平台文件夹--gcc或者RVDS
这里我使用的是STMF40732VGT6,这个芯片使用的是M4架构的,所以要选择CM4结尾或者特征的目录文件,并且如果不使用浮点运算直接使用CM3也是可以的。
我这里因为使用的是arm-none-aebi-gcc,所以我选择gcc。如果是keil,可以去RVDS找芯片对应的文件!
这里很有趣的点是Keil MDK移植居然不是在keil,而是在RVDS中,这里简单提一嘴:
!提一嘴\] 提一嘴 Keil-MDK 的 **编译器** 就是 ARM 当年 **RVDS 套装里的 RVCT(RealView Compiler Toolchain)** , 只是 ARM 在 2005 年收购 Keil 后,把: **RVCT 编译器** **uVision IDE** (原来 Keil 的) 重新打包成 **"Keil-MDK"** 并沿用 **"RealView"** 品牌。 因此 MDK 的 **portable/RVDS/...** 目录仍叫 RVDS,实质就是 **Keil-MDK 用的 ARMCC/ARMCLANG 编译器**
我这里演示stm32F407VGT6移植gcc里面文件的过程:
来到gcc目录下,找到与我芯片符合的ARM-CM4F符合的文件夹
将里面的c源文件与头文件分别复制到src与inc里面
然后就没了。keil平台的话,就是来到RVDS文件夹下,找到对应的芯片型号的文件夹
复制对应的c源文件与头文件到src与inc里面即可
对了还有一个比较重要的头文件,这个也是我们后面交互比较多的文件,用于开启freertos的某些钩子(hook)函数,这个头文件也需要根据各自的芯片架构来选择!
2、来到./FreeRTOS/FreeRTOS/Demo选择对应芯片的FreeRTOSConfig.h


没了,就那么简单!
完成之后,文件夹的内容:

3、MemMang文件夹
这里的MemMang是和内存管理有关的文件,这个直接复制对应的头文件与源文件到我们创建文件src即可
通常只需要复制heap_4.c即可,这里提一嘴它们之间的关系,并且它们只能选择一个!
!补充\] 补充 一句话总结 heap_1 \~ heap_5 是 **同一个接口(pvPortMalloc / vPortFree)下的五种实现** ,差异只在 **"能否 free、能否合并碎片、能否跨非连续内存"** ------ 选哪个文件,就决定了 FreeRTOS 堆的形态。
文件 | 能否 free | 合并相邻碎片 | 支持多块不连续内存 | 典型场景 |
---|---|---|---|---|
heap_1.c | ❌ | ❌ | ❌ | 只分配、不释放的简单应用 |
heap_2.c | ✅ | ❌ | ❌ | 早期版本,碎片严重,已不推荐 |
heap_3.c | ✅ | 依赖 C 库 | ❌ | 直接包装 malloc/free ,加线程安全 |
heap_4.c | ✅ | ✅ | ❌ | 官方默认,碎片最少,单块 RAM |
heap_5.c | ✅ | ✅ | ✅ | heap_4 功能 + 可跨多块 RAM/SDRAM |
使用规则
-
同一工程 只选 1 个
heap_x.c
放进 src中。 -
heap_5 必须先调用
cvPortDefineHeapRegions(xHeapRegions); // 传入各段首地址+长度
才能
pvPortMalloc
,否则第一次创建任务/队列就挂。 -
推荐顺序 :
普通 MCU → heap_4 ;
需要外扩 RAM → heap_5 ;
极简、永不释放 → heap_1。
目前全部文件都移植完成!
三、修改或者增添部分文件的函数
1、修改部分文件的函数
修改FreeRTOSConfig.h部分内容:
这里提供一部分有些重要,有些不重要的内容
🔴 必须(动就炸)
宏 说明 configCPU_CLOCK_HZ
必须与 MCU 主频一致(SystemCoreClock) configTICK_RATE_HZ
系统心跳,1 kHz 通用,> 10 kHz 会吃 CPU configPRIO_BITS
与芯片手册匹配(F1/F4 为 4) configKERNEL_INTERRUPT_PRIORITY
&configMAX_SYSCALL_INTERRUPT_PRIORITY
中断优先级位偏移,错一位就 HardFault vPortSVCHandler
/xPortPendSVHandler
/xPortSysTickHandler
向量映射,名字必须对齐 configASSERT
调试开关,关闭就失去定位能力
🟡 重要(根据应用调)
宏 推荐值 提示 configTOTAL_HEAP_SIZE
75 kB 调大/调小,看剩余 RAM configMINIMAL_STACK_SIZE
130 以 字 为单位,F4 建议 128-256 configMAX_PRIORITIES
5-8 够用即可,越大 RAM 越多 configCHECK_FOR_STACK_OVERFLOW
2 开发期开,量产关 configUSE_MALLOC_FAILED_HOOK
1 开发期开,方便抓内存泄漏 configUSE_TIMERS
1 用软件定时器就开 configUSE_MUTEXES
/configUSE_RECURSIVE_MUTEXES
1 多任务共享资源必开
🟢 可选(可关可开)
宏 典型场景 configUSE_IDLE_HOOK
/configUSE_TICK_HOOK
低功耗/统计 configUSE_TRACE_FACILITY
开 TRACE 时才用 configGENERATE_RUN_TIME_STATS
性能分析 configUSE_CO_ROUTINES
几乎没人用
红色不能改,黄色按需改,绿色随意改。
所以我们可以关闭,configUSE_IDLE_HOOK / configUSE_TICK_HOOK功能、configUSE_TRACE_FACILITY、configGENERATE_RUN_TIME_STATS、configUSE_CO_ROUTINES
不过最后在FreeRTOSConfig.h文件中加入头文件

2、stm32f4xx_it.c文件
我们可以在FreeRTOSConfig.h查看
![[Pasted image 20250824094014.png]]
所以我们需要去stm32f4xx_it.c中注释掉对应的函数,因为这部分的函数实现在port.c中已经实现了。
SVC_Handler
PendSV_Handler
SysTick_Handler
3、delay函数
增加一个比较通用的delay文件。
在调度器启动前,使用堵塞式的delay函数;
在调度器启动后,使用FreeRTOS的delay函数。
可以确保一些初始化函数可以正确运行。
初始化调用:
c
delay_init(168); //168是STM32VGT6支持的时钟频率,这里请修改相应的时钟频率
函数使用:
c
delay_ms(nms); /* 延时nms */
delay_us(nus); /* 延时nus */
delay.h
c
#ifndef __DELAY_H
#define __DELAY_H
#include "stm32f4xx.h" //若用 F1/F7/H7 改成对应头文件
#include <misc.h>
void delay_init(uint16_t sysclk); /* 初始化延迟函数 */
void delay_ms(uint16_t nms); /* 延时nms */
void delay_us(uint32_t nus); /* 延时nus */
#endif
delay.c
c
#include "delay.h"
#include "stm32f4xx.h" // 若用 F1/F7/H7 改成对应头文件
#include "FreeRTOS.h"
#include "task.h"
static uint32_t fac_us = 0;
/* 初始化时只算系数,不碰 SysTick 寄存器 */
void delay_init(uint16_t sysclk)
{
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
fac_us = sysclk / 8; // 时钟 = HCLK/8
}
/*------------------------------------------
* 微秒延时:调度器启动前 → 纯空循环
* 调度器启动后 → SysTick 计数
*------------------------------------------*/
void delay_us(uint32_t nus)
{
if (xTaskGetSchedulerState() == taskSCHEDULER_RUNNING)
{
/* FreeRTOS 已运行:用 SysTick 忙等 */
volatile uint32_t ticks, told, tnow, tcnt = 0, reload;
UBaseType_t original_priority = uxTaskPriorityGet(NULL);
vTaskPrioritySet(NULL, configMAX_PRIORITIES - 1);
reload = SysTick->LOAD;
ticks = nus * fac_us;
told = SysTick->VAL;
while (1)
{
tnow = SysTick->VAL;
if (tnow != told)
{
if (tnow < told)
tcnt += told - tnow;
else
tcnt += reload - tnow + told;
told = tnow;
if (tcnt >= ticks)
break;
}
}
vTaskPrioritySet(NULL, original_priority);
}
else
{
/* 调度器未启动:纯空循环,不依赖 SysTick */
uint32_t ticks = nus * fac_us;
while (ticks--)
__NOP();
}
}
/*------------------------------------------
* 毫秒延时:调度器启动前 → 空循环
* 调度器启动后 → vTaskDelay
*------------------------------------------*/
void delay_ms(uint16_t nms)
{
if (xTaskGetSchedulerState() == taskSCHEDULER_RUNNING)
{
vTaskDelay(pdMS_TO_TICKS(nms));
}
else
{
/* 调度器未启动:空循环 1 ms × nms */
for (uint32_t i = 0; i < nms; i++)
delay_us(1000);
}
}
4、定义两个个钩子函数
此钩子函数可以作为一个调试接口。这里可以点灯 / 打印 / 断点,方便调试
vApplicationStackOverflowHook
vApplicationMallocFailedHook
c
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
/* 这里可以点灯 / 打印 / 断点,方便调试 */
(void)xTask;
(void)pcTaskName;
/* 死循环,方便调试器停住 */
taskDISABLE_INTERRUPTS();
for (;;)
;
}
void vApplicationMallocFailedHook(void)
{
/* 可以点灯、打印或断点 */
taskDISABLE_INTERRUPTS();
for (;;)
;
}
随便在一个包含
#include "FreeRTOS.h"
#include "task.h"
文件下定义即可。
随后只要在Makefile或者Keil里面添加对应的头文件与源文件路径即可
这里就不提供keil的添加方式了,这个流程在创建工程的时候就应该知道了。
四、后记
我们简单使用一下,这个迁移过来的FreeRTOS工程。
1、包含头文件
c
#include "FreeRTOS.h"
#include "task.h"
2、初始化延时函数(此步可选)
c
delay_init(168);
3、创建任务
500ms -- LED翻转
1s -- LCD的显示变化
c
//任务实现
void LED_Task(void *arg)
{
for(;;)
{
GPIO_ToggleBits(GPIOA, GPIO_Pin_0);
vTaskDelay(500);
}
}
void LCD_Task(void *arg)
{
ST_7735S_Clear(0xffff);
int num = 0;
char buff[20] = {0};
for (;;)
{
bzero(buff,sizeof(buff));
sprintf(buff,"%d",num++);
ST_7735S_ShowString(10, 20, "Hello!World!", RGB888to565(0xff, 0x00, 0xff), 0xffff);
ST_7735S_ShowString(10, 40, buff, RGB888to565(0xff, 0x00, 0xff), 0xffff);
vTaskDelay(500);
}
}
//任务创建
xTaskCreate(LED_Task,"LED",128,NULL,1,NULL);
xTaskCreate(LCD_Task, "LCD", 128, NULL, 2, NULL);
值得一提的是任务创建不局限xTaskCreate 函数,这是动态创建的任务,还有创建静态任务xTaskCreateStatic的方式。这里我们分析分析这个动态创建任务的函数:
cBaseType_t xTaskCreate( TaskFunction_t pxTaskCode, const char * const pcName, const configSTACK_DEPTH_TYPE uxStackDepth, void * const pvParameters, UBaseType_t uxPriority, TaskHandle_t * const pxCreatedTask )
参数名 类型 含义 pxTaskCode
TaskFunction_t
任务入口函数指针 (必须是一个 void func(void *pv)
形式的函数)pcName
const char *
任务名字 (调试/Trace 用,长度受 configMAX_TASK_NAME_LEN
限制)uxStackDepth
configSTACK_DEPTH_TYPE
任务栈大小 ,单位是 "字" (不是字节)。 例如 128 → 512 B(32 位 MCU) pvParameters
void *
传给任务的参数 ,可为 NULL
uxPriority
UBaseType_t
任务优先级 ,数值越大越优先;0 为空闲任务 pxCreatedTask
TaskHandle_t *
返回的任务句柄 ,之后可用它 vTaskDelete/ vTaskPrioritySet
等; 不需要句柄就填NULL
返回值
pdPASS
:创建成功errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY
:内存不足
4、开始任务
c
vTaskStartScheduler();
5、任务现象
正确的实现!

这里总结一下移植的流程
1.获取FreeRTOS源码
2.移植文件
3.修改部分FreeRTOSConfig.h文件
4.修改stm32f407xx_it.c注释掉SVC_Handler、PendSV_Handler、SysTick_Handler函数实现
5.增加一个比较通用的delay函数
6.实现调试的钩子函数
vApplicationStackOverflowHook、vApplicationMallocFailedHook
7.创建任务与开启任务
以上为本文的内容。享受FreeRTOS吧!