一、引言:开启FreeRTOS之旅
在上一章节中,我们共同探讨了RTOS系统在嵌入式开发中的核心价值,也初步领略了实时操作系统的独特魅力。那么从本章开始,我们算是正式进入RTOS的学习阶段啦!我将以FreeRTOS为例为大家深入挖掘其底层内核的运行机制,一步一步揭开他的神秘面纱,那么在这一切的开始之前我们必须先要“利其器”——完成关于FreeRTOS的移植工作。
二、何为FreeRTOS?
可能有些小伙伴是第一次接触FreeRTOS,心中难免会有疑问:为什么选择FreeRTOS?它到底好在哪儿?哈哈别急,在动手移植之前,我们先花几分钟认识一下这位即将与我们长期并肩作战的“伙伴”。
简单来说,FreeRTOS是一个专为微控制器和小型嵌入式系统设计的实时操作系统内核。它的名字就揭示了它的核心特质:Free(自由/免费) + RTOS(实时操作系统)。凭借其开源免费、高度可裁剪、稳定可靠的特点,它已成为全球嵌入式开发者中最受欢迎的RTOS之一,从工业控制到智能家居,从穿戴设备到物联网终端,处处都有它的身影。
那么他到底好在哪了呢?这就不得不提到他的三大核心优势了:轻量、可移植性、强大的生态社区。
- 轻量——仅需几KB的ROM和几百字节的RAM即可运行,这代表着它能够在资源极其有限的MCU上”挥洒自如“。
- 可移植性——这是本章“移植”的核心基础,FreeRTOS的架构设计极为清晰,将核心调度逻辑与硬件底层(如时钟、中断、任务栈)完全解耦。这意味着我们只需关注并实现少量的硬件接口函数,就能将它轻松“嫁接”到新的芯片平台上。
- 强大的生态与社区——FreeRTOS拥有异常丰富的文档,在学习过程遇到问题可以很方便的找到答案。
理解FreeRTOS的价值,不仅在于掌握一个工具,更在于领悟其背后的设计哲学——如何在有限的资源中实现最大的确定性。这种对实时性、可靠性和效率的追求,正是嵌入式开发的精髓所在。正是基于这些特性,FreeRTOS成为了我们学习RTOS的绝佳起点。好了,我们话不多说,接下来就开始步入我们移植的正题啦!
三、FreeRTOS源码组织结构
3.1 FreeRTOS源码获取
移植源码的第一步肯定是要获取到一份可靠的源码,给大家提供一下FreeRTOS的官网,可以到官网上下载到最新的源码资源。FreeRTOS 官网不仅提供了源码,还有非常全面的文档和教程,非常适合新手学习和查阅。在后续学习 API 或进行开发时,建议大家多借助官网资源,效率会更高。

可以看到目前的最新版本是2024.06.04发行的,但是本博客主要是以2022.12.01发布的版本为例,展开移植的讲解,我也会在文章末尾附上我使用环境资源,便于随步骤操作,大家自行选择源码版本即可。
3.2 FreeRTOS源码结构
解压后的FreeRTOS源码如下,这里面最关键移植的目录是FreeRTOS以及FreeRTOS-Plus。

FreeRTOS-Plus可以理解为是“升级版”,里面提供了更为丰富的组件。例如TCP、命令行接口、 I/O 抽象层和文件系统组件等等。我们在学习移植阶段只关注FreeRTOS的最基础的功能。所以FreeRTOS-Plus这里面的源码结构我们可以不过多关注。
进入到FreeRTOS下面,Source就是我们接下来所要移植的关键部分了,里面是FreeRTOS的核心源码

接下来,我将具体介绍一下Source层,每一个文件的具体所代表的是什么:
- include:为头文件目录,可以看到里面全都是一些头文件:
- 红色标注:核心必备头文件
task.h– 任务管理API(如xTaskCreate())queue.h– 队列通信APIsemphr.h– 信号量API- 这些是移植时必须关注的关键组件
- 蓝色标注:可选功能头文件
timers.h– 软件定时器event_groups.h– 事件组
- 绿色标注:特殊功能与兼容性
- 红色标注:核心必备头文件

- portable:为对接硬件平台的目录,包含了 FreeRTOS 针对不同编译器和处理器架构的移植层代码。
- 编译器相关移植目录
- GCC:用于 GNU GCC 编译器(STM32 CubeIDE、Eclipse 等常用)
- Keil:用于 ARM Keil MDK 编译器
- ARMClang:用于 ARM Compiler 6(ARMv8 架构)
- MSVC-MingW:用于 Windows 模拟器开发
- CCS:用于 Texas Instruments Code Composer Studio
- SDCC:用于 8051 等 8 位单片机
- RVDS:Keil软件下的MCU内核,包括M3、M4、M0等移植内核(我们后面移植的平台也主要是Keil,这个目录需要留意一下)
- MemMang:内存管理实现(很重要!我们接下来会选择下面的一种方案进行移植)
- heap_1.c – 最简单的分配,不支持释放
- heap_2.c – 最佳匹配算法,支持释放但会产生碎片
- heap_3.c – 包装标准 malloc/free(需要编译器支持)
- heap_4.c – 推荐方案,支持释放、碎片合并
- heap_5.c – heap_4 的增强版,支持非连续内存区域
- 第三方和特殊平台
- ThirdParty:社区贡献的移植代码
- Rowley:CrossWorks for ARM 编译器
- Tasking:Tasking 编译器
- Renesas:瑞萨单片机
- MPLAB:Microchip PIC 单片机
- MikroC:mikroC 编译器(PIC、AVR 等)
- 编译器相关移植目录

- croutine.c:协程功能源文件
- event_groups.c:事件标志组源文件
- list.c:列表和列表项源文件
- queue.c:消息队列源文件
- stream_buffer.c:流缓存
- task.c:任务管理源文件
- timer.c:软件定时器源文件
需要在这里提一嘴,在我们移植FreeRTOS到工程的时候,同样需要我们自己创建一个FreeRTOSConfig.h文件。他是 FreeRTOS 的用户配置文件,也是整个 FreeRTOS 移植和定制的核心文件。比如我们我们想要开启FreeRTOS的定时器功能,可以通过改变这个文件里的宏来控制,大家在此有个印象先,为我们后面的移植打下基础。
四、FreeRTOS移植
好了,铺垫到这里就可以正式开启我们的任务啦!我将从创建Keil工程开始,一步一步带你完成FreeRTOS的移植,我们话不多说,开始干活!
1.Keil工程创建







“万里长征”从这里迈出第一步!我们将创建一个基础的STM32工程框架。
2.HAL库移植
HAL库是我们能够让我们硬件运行起来的基石,因此这一步也是重中之重。我们在安装Cortex-M4内核软件包的时候,里面就会附带有HAL库的源码。我的软件包版本选择的是2.12.0,因此HAL库的版本同样是2.12.0,我会把我使用到的所以环境版本附到百度网盘上,大家可以跟我的环境保持一致进行移植,同样大家也可以自行选择版本。
HAL库的源码在我们安装完软件包之后,就会集成到我们keil的安装目录下面,
















在移植过程中,由于编译器环境、HAL库版本等差异,可能会出现与我不同的编译错误。遇到报错不要慌张,这完全是移植过程中的正常现象。我们需要耐心地逐一解决每个问题。相信我,当你最终看到”0 Errors, 0 Warnings”时,那种成就感绝对会让你觉得一切努力都值得!
3.FreeRTOS源码移植
我们前面已经完成了所有准备工作,接下来终于到了我们的重头戏!FreeRTOS的源码移植!

















到这里我们的全部代码就算移植完成啦!接下来就是上板测试了,我会给到大家一个demo,可以实际烧录到我们的STM32F407IGT6开发板上面,看看能不能成功运行!
#include "stdio.h"
#include "stm32f4xx.h"
#include "stm32f4xx_hal.h"
//初始化系统时钟配置
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 25;
RCC_OscInitStruct.PLL.PLLN = 336;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
// Error_Handler();
}
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
// Error_Handler();
}
}
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
/*新版开发板为 PF8、PF9、PF10、PE2,根据自己板子原理图进行引脚配置*/
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_2, GPIO_PIN_SET);
GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);
}
void USART1_Init(void)
{
//串口引脚初始化
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
//串口配置初始化
UART_HandleTypeDef huart1;
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
// Error_Handler();
}
}
void Key_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOI_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
/*Configure GPIO pin : PI9 */
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOI, &GPIO_InitStruct);
/*Configure GPIO pin : PF11 */
GPIO_InitStruct.Pin = GPIO_PIN_11;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);
/* EXTI interrupt init*/
HAL_NVIC_SetPriority(EXTI9_5_IRQn, 5, 1);
HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);
HAL_NVIC_SetPriority(EXTI15_10_IRQn, 5, 1);
HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
}
//printf重定向
int fputc(int ch,FILE* F)
{
while(!(USART1->SR&(1<<7)));
USART1->DR = (uint8_t)ch;
return ch;
}
int main(void)
{
uint32_t cnt = 0;
/* HAL_Init()调用的是 stm32f4xx_hal_timebase_tim_template.c 文件里的 HAL_InitTick()
内部初始化的是TIM6,初始化以后可以调用 HAL_Delay 函数 */
HAL_Init();
/* SystemClock_Config() 初始化时钟以后也调用了 stm32f4xx_hal_timebase_tim_template.c 文件里的 HAL_InitTick()
但并没有初始化和开启systick中断*/
SystemClock_Config();
LED_Init();
USART1_Init();
while(1)
{
HAL_GPIO_TogglePin(GPIOF,GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10);
HAL_GPIO_TogglePin(GPIOE,GPIO_PIN_2);
printf("cnt = %u\r\n",cnt++);
HAL_Delay(1000);
}
}
//因为上边调用开启了SysTick中断,所以要写一个实现函数
//另外将操作系统实现的systick中断函数给注释掉,避免多重定义
void SysTick_Handler(void)
{
}
void EXTI9_5_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_9);
}
void EXTI15_10_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_11);
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_9)
{
if(HAL_GPIO_ReadPin(GPIOI, GPIO_PIN_9) == GPIO_PIN_RESET)
{
HAL_Delay(20); //如果在中断中使用 HAL_Delay 它的优先级一定要比此中断优先级高
if(HAL_GPIO_ReadPin(GPIOI, GPIO_PIN_9) == GPIO_PIN_RESET)
{
printf("scan : key1\r\n");
}
}
}
else if(GPIO_Pin == GPIO_PIN_11)
{
if(HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_11) == GPIO_PIN_RESET)
{
HAL_Delay(20); //如果在中断中使用 HAL_Delay 它的优先级一定要比此中断优先级高
if(HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_11) == GPIO_PIN_RESET)
{
printf("scan : key2\r\n");
}
}
}
}
我们在移植的工程中一定要有着不断试错的心态,很少情况下是可以一次性很顺利的移植成功的,都会出现各种各样的报错,但请记住,每一次解决错误都是一次宝贵的学习机会。,在此过程中我们不但能够获取到移植的经验,同样可以也可以锻炼一个平稳的心性。
如果在移植过程中遇到与我不同的报错或问题,非常欢迎大家与我交流讨论!后面我会将本文章所有用到的软件环境提到网盘。
