Logo

郎哥编程

搭建FreeRTOS实时操作系统开发环境

2026-11-11 11

1.什么是实时操作系统

在高峰时段,交通调度系统需要快速响应各种突发情况,如交通事故、道路拥堵等,及时调整信号灯时长、引导车辆分流,确保交通流畅。实时操作系统,就如同这个交通调度系统,只不过它管理的是计算机系统中的任务,需要在规定的时间限制内对各种事件做出响应和处理,确保系统的正常运行。​

实时操作系统的英文简称是RTOS(Real-Time Operating System,RTOS),是指当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统作出快速响应,并控制所有实时任务协调一致运行的操作系统 。与一般的操作系统相比,实时操作系统更加注重 “实时性”,强调在规定时间内完成特定任务,对响应时间、处理精度等有着严格要求。图1为实时操作系统示意图。

图 1 实时操作系统示意图

图中黄色模块是不同优先级的任务,从左到右优先级从高到低。这些任务是系统要执行的具体程序代码,高优先级任务会获得更优先的CPU执行权,确保关键任务能及时响应。

实时内核(Real-Time Kernel)是整个系统的大脑,负责接收来自任务或中断服务程序的信号、消息等事件,再通过任务选择器(Task Selector)来调度CPU资源,决定哪个任务可以占用CPU执行。

当外部事件(比如传感器数据输入、用户指令)触发时,信号会传递给实时内核,内核的任务选择器会根据任务的优先级,快速挑选出最需要执行的任务,分配CPU资源让其运行,以此保障高优先级任务能在规定时间内完成,实现系统的实时响应。

在工业自动化领域,实时操作系统用于控制生产线上的各种设备,如机器人手臂的精确运动控制。一旦出现指令延迟,可能导致生产的产品出现偏差,影响产品质量甚至导致设备损坏。在航空航天领域,飞行控制系统依赖实时操作系统确保飞机的飞行姿态、发动机运行等关键参数的实时监控和调整,任何延迟都可能引发严重后果。

2.当前主流的嵌入式实时操作系统

当前主流的嵌入式实时操作系统有FreeRTOS、RT-Thread、uC/OS、鸿蒙等操作系统。

FreeRTOS(‌Free Real-Time Operating System‌)是一款开源的实时操作系统内核,主要为嵌入式系统和微控制器设计。主要特点是轻量、高效、可移植性强,被广泛应用于资源受限的硬件环境,如物联网设备、传感器、工业控制器等。

FreeRTOS的代码量极小(通常仅数KB),内存占用低(最低仅几百字节),适合运行在RAM和Flash资源有限的微控制器(如ARM Cortex-M、ESP32、RISC-V等)。

uC/OS(MicroC/OS)是由Micrium公司(现被Silicon Labs收购)开发的一款‌商业实时操作系统‌(RTOS),广泛应用于工业控制、汽车电子、医疗设备等安全关键领域。

RT-Thread 是一款国产开源‌嵌入式实时操作系统‌,广泛应用于智能家居、工业控制、可穿戴设备等领域。

鸿蒙也是一款国产化开源‌嵌入式实时操作系统‌,提供DevEco Studio开发环境,支持Java/JS/C++多语言开发,覆盖从L0(极小内存设备)到L5(标准系统)的全场景嵌入式设备‌。

3.如何选择合适的实时操作系统

选择适合自己项目需求的实时操作系统,需要综合考虑项目的性能要求、硬件兼容性、开发成本等多个因素。

3.1.性能要求​

不同的应用场景对实时操作系统的性能要求差异很大。在工业自动化领域,对于电机控制、机器人操作等任务,通常要求实时操作系统具备毫秒级甚至微秒级的响应时间,以确保设备的精确运行。而在智能家居系统中,虽然也需要一定的实时性,但对于响应时间的要求相对宽松,一般在几十毫秒内即可满足需求。​

任务处理能力也是一个重要的考量因素。如果项目涉及大量复杂的任务,如航空航天领域中的飞行器导航与控制系统,需要实时操作系统能够高效地调度和管理多个任务,确保各个任务都能按时完成。在这种情况下,应选择具备较强任务处理能力和高效调度算法的实时操作系统,如 RT-Thread、FreeRTOS 等。​

3.2.硬件兼容性​

硬件兼容性能够确保实时操作系统能够在目标硬件平台上稳定运行。不同的硬件平台,如 ARM、x86、PowerPC 等,具有不同的架构和特性,需要与之兼容的实时操作系统来运行,以便其性能得到充分优化。​

ARM 架构在嵌入式领域应用很广。许多实时操作系统都针对 ARM 架构进行了优化,如 FreeRTOS等,能够很好地支持 ARM 系列处理器。而 x86 架构则多用在工业控制、通信设备等领域,一些实时操作系统,如 Windows CE、VxWorks 等,对 x86 架构提供了良好的兼容性 。​

为了评估实时操作系统与硬件的兼容性,可以参考操作系统的官方文档,了解其支持的硬件平台和设备列表。还可以进行硬件兼容性测试,通过实际运行操作系统在目标硬件上,观察系统的稳定性、性能表现以及对硬件设备的驱动支持情况,确保操作系统与硬件能够无缝配合 。​

3.3.开发成本​

开发成本也是选择实时操作系统时不可忽视的一个因素,开发成本主要包括软件授权费用、开发工具成本、学习成本等多个方面。​

一些商业实时操作系统,如 VxWorks、QNX 等,一般需要支付较高的软件授权费用,这对于预算有限的项目来说可能是一个较大的负担。而开源实时操作系统,如 FreeRTOS、RT-Thread 等,由于其开源免费的特性,可以有效降低软件授权成本 。​

开发工具的成本也需要考虑,一些实时操作系统可能需要特定的开发工具和环境,这些工具可能需要购买许可证或支付使用费用;一些实时操作系统提供了免费的开发工具,或者支持使用开源的开发工具,如 GCC 编译器、Eclipse 开发环境等,可以有效降低开发工具成本。

团队的学习成本也需要考虑,如果开发团队对某个实时操作系统不熟悉,需要花费大量的时间和精力去学习和掌握其使用方法,这将增加项目的开发周期和成本。

4.关于FreeRTOS

FreeRTOS是一个专为嵌入式系统设计的开源实时操作系统内核。它提供了一套用于管理任务、内存和队列的API,让开发者可以高效地开发嵌入式多任务程序,缩短系统开发时间,并更好地保证系统的实时性和可靠性‌。

FreeRTOS作为一款流行的、免费的、开源的实时操作系统(RTOS),在嵌入式系统开发领域有广泛的应用,学好FreeRTOS是是嵌入式工程师提升核心竞争力的关键一步。

从智能家居到工业4.0,从可穿戴设备到汽车电子,FreeRTOS凭借其轻量级、可裁剪、实时性的特性,成为众多嵌入式开发项目的首选。掌握FreeRTOS,意味着开发者能够轻松驾驭多任务调度,优化系统资源分配,确保关键任务在严格的时间约束内完成。这可不只是让你技术更牛,更是让你学会怎么搞定那些头疼的工程难题。

嵌入式多任务开发为什么一定要从FreeRTOS开始呢?因为FreeRTOS是创建多任务的开源套件,而在工程的实际应用中多任务是项目的基础(除非项目很小,功能少代码量不大,去跑裸机,否则都要先创建多任务的工程)。

5.Keil5集成FreeRTOS开发环境

在使用STM32 HAL库时,通常会发现其包含的软件包中已经集成了FreeRTOS实时操作系统。这意味着开发者可以直接利用这些预集成的组件来构建实时应用,而无需单独下载和配置FreeRTOS。

你需要在 Keil5 环境中,从 STM32Cube_FW_F1_V1.8.0 固件包中(或其他对应芯片系列的固件包)提取 FreeRTOS 代码并完成完整集成,下面是详细操作流程,涵盖工程创建、代码提取、配置、编译调试全环节,确保没接触FreeRTOS开发的新手也能一步步完成。

5.1 前期准备

1、确认环境与资源

已安装 Keil5(MDK-ARM),并添加 STM32F1 系列芯片的器件库;

下载并解压STM32Cube_FW_F1_V1.8.0固件包(或其他对应芯片系列的固件包);

目标芯片:以 STM32F103R6 为例(其他 F1 系列操作一致)。

5.2 获取FreeRTOS源代码

STM32Cube_FW_F1_V1.8.0 中 FreeRTOS 相关代码路径:

FreeRTOS 相关代码路径示意图

图 2 FreeRTOS 相关代码路径示意图

5.3 详细集成步骤

步骤 1:创建 Keil5 基础工程(无 RTOS)

在《在裸机系统上执行多任务》课程中,我们已经创建了一个多任务工程。该工程实现两个独立任务 ——LED1 每 500ms 闪一次,LED2 每 1000ms 闪一次,并利用SysTick定时器实现多任务调度。现在我们用FreeRTOS来调度这两个任务。

此时,工程结构如下:

工程目录结构

步骤 2:从 Cube 固件包提取 FreeRTOS 核心代码

1、在工程根目录创建FreeRTOS目录;

2、复制固件包的FreeRTOS源代码到工程,分为两种方式:

方式一:把固件包的source目录完整复制到工程的FreeRTOS目录。该方式操作简单,适合快速搭建项目。可能包含不必要的文件,增加项目体积;

方式二:可选择性复制相关source目录下的代码到工程(建议在FreeRTOS目录下建立与固件包相同的目录),该方式精简项目结构,便于维护和理解核心逻辑。需要手动管理文件依赖关系,容易遗漏关键文件:

必选文件(核心源码):

croutine.c、event_groups.c、list.c、queue.c、tasks.c、timers.c

必选文件夹:

include/ (所有头文件,如FreeRTOS.h、task.h、queue.h等)、

内存管理算法(选其一,新手推荐heap_4.c):

复制MemMang/heap_4.c到工程目录FreeRTOS/Portable/MemMang;

Cortex-M3 移植层(适配 Keil ARMCC):

复制RVDS/ARM_CM3/下的port.c和portmacro.h到工程目录

FreeRTOS/Portable/ARM_CM3;

步骤 3:修改FreeRTOS配置模板文件

FreeRTOSConfig.h是一个用于配置 FreeRTOS 实时操作系统行为和功能的核心头文件,它定义了系统运行时的各种参数和选项,允许开发者根据具体应用的需求对 FreeRTOS 内核进行裁剪、优化和定制。

在下载的Cube固件包中已经提供了该配置的模板文件FreeRTOSConfig_template.h,该模板文件在固件包Source/include目录,可直接将复制到工程的FreeRTOSConfig_template.h重新命名为FreeRTOSConfig.h即可。

重新命名后,‌通常需要进行一些修改‌,因为模板文件中的某些配置可能需要根据实际硬件平台、系统时钟频率、内存分配策略等进行调整。例如,需要设置系统时钟频率、启用或禁用特定功能(如互斥量、事件组等)、配置内存分配策略等。

STM32Cube_FW_F1_V1.8.0 固件包自带的 FreeRTOS 配置模板文件针对 STM32F103 系列微控制器进行了基础配置,可直接用于基本应用场景。但通常需要根据具体应用需求进行适当调整以确保最佳性能和稳定性。

步骤 4:将 FreeRTOS 代码添加到 Keil 工程

1、启动Keil5并打开工程,在Project窗口创建FreeRTOS分组;

2、将工程FreeRTOS/Souce目录下的所有c实现文件添加到FreeRTOS分组;

3、将工程FreeRTOS/Souce/portable/MemMang目录下的heap_4.c添加到FreeRTOS分组;

4、将工程FreeRTOS/Souce/portable/RVDS\ARM_CM3目录下的port.c添加到FreeRTOS分组;

此时,工程结构如下:

步骤 5:添加头文件路径

打开工程属性对话框,在C++标签页Include Paths域添加以下路径(根据你的工程实际路径调整):

./FreeRTOS/Core/include(FreeRTOS 核心头文件);

./FreeRTOS/Portable/ARM_CM3(移植层头文件 portmacro.h);

图 添加头文件路径示意图

步骤 6:编译过程中可能出现的问题

问题1:Symbol SysTick_Handler multiply defined (by port.o and main.o)

原因:FreeRTOS 的 port.c 已经自带了 SysTick_Handler中断函数 的实现,在当前工程的main.c文件中也实现了SysTick_Handle中断函数,导致函数实现冲突。

解决方案:删除在main.c文件中的SysTick_Handler中断函数代码。

问题2:编译可以通过,但Keil编辑器的语法检查报“PRESERVE8”未定义

原因:当前工程采用的是ARMCC 5.0.6编译器,完全支持 PRESERVE8 伪指令的(所以编译能通过),但 Keil MDK 的编辑器内置的语法检查器(静态代码分析)和编译器的解析规则不一致:

编译器(ARMCC):能正确识别 PRESERVE8 作为汇编伪指令,编译无问题;

编辑器语法检查:误将 PRESERVE8 当作普通符号(变量 / 函数),认为其未定义,从而标红提示。

这种情况不影响代码编译和运行,只是编辑器的 “误报”,常见于 Keil MDK 5.x 搭配 ARMCC 5 的环境。

步骤 7:修改main.c代码

#include "main.h"
#include "stm32f1xx_hal.h"
#include "FreeRTOS.h"
#include "task.h"
void SystemClock_Config(void);
void Error_Handler(void);
void vTaskLED1(void *pvParameters);
void vTaskLED2(void *pvParameters);
/* 任务句柄 */
TaskHandle_t xTaskLED1Handle = NULL;
TaskHandle_t xTaskLED2Handle = NULL;
/* 主函数入口 */
int main(void)
{
  /* HAL库初始化 */
  HAL_Init();
/* 配置系统时钟 */
  SystemClock_Config();
  /* 初始化GPIO(PA1、PA2推挽输出,控制LED) */
  MX_GPIO_Init();
/* 创建任务 */
    
xTaskCreate(vTaskLED1, "LED1_Task", 128, NULL, 1, &xTaskLED1Handle);
    xTaskCreate(vTaskLED2, "LED2_Task", 128, NULL, 2, &xTaskLED2Handle);
    
    /* 启动调度器 */
  vTaskStartScheduler();
  /* 主循环:多任务调度核心 */
  while (1)
  {
    
  }
}
/**
  * @brief 系统时钟配置:72MHz
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  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_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}
/******* 硬件初始化函数 ******/
/**
  * @brief  GPIO初始化:PA1、PA2 推挽输出,无上拉下拉,速度2MHz
  * @note   LED为阴极接GPIO,初始置高(LED熄灭)
  */
void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  /* 使能GPIOA时钟(STM32F1必须先使能外设时钟) */
  __HAL_RCC_GPIOA_CLK_ENABLE();
  /* 配置PA1:LED1 */
  GPIO_InitStruct.Pin = LED1_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;  // 推挽输出
  GPIO_InitStruct.Pull = GPIO_NOPULL;          // 无上拉下拉
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 低速(2MHz)
  HAL_GPIO_Init(LED1_PORT, &GPIO_InitStruct);
  /* 配置PA2:LED2 */
  GPIO_InitStruct.Pin = LED2_PIN;
  HAL_GPIO_Init(LED2_PORT, &GPIO_InitStruct);
  /* 初始状态:GPIO置高 → LED熄灭(阴极接GPIO,高电平无电流) */
  HAL_GPIO_WritePin(LED1_PORT, LED1_PIN, GPIO_PIN_SET);
  HAL_GPIO_WritePin(LED2_PORT, LED2_PIN, GPIO_PIN_SET);
}
/************************** 任务执行函数 **************************/
/* LED1任务函数 */
void vTaskLED1(void *pvParameters)
{
    while(1)
    {
HAL_GPIO_TogglePin(LED1_PORT, LED1_PIN); // 翻转引脚电平
// 延时1000毫秒
    vTaskDelay(pdMS_TO_TICKS(500));
    }
}
/* LED2任务函数(演示优先级) */
void vTaskLED2(void *pvParameters)
{
    while(1)
    {
  HAL_GPIO_TogglePin(LED2_PORT, LED2_PIN); // 翻转引脚电平
   // 延时1000毫秒
       vTaskDelay(pdMS_TO_TICKS(1000));
    }
}
/**** 错误处理函数(HAL库规范) *******/
void Error_Handler(void)
{
  __disable_irq();
  while (1)
  {
  }
}
#ifdef USE_FULL_ASSERT
void assert_failed(uint8_t *file, uint32_t line)
{
}
#endif /* USE_FULL_ASSERT */

关键代码解释

vTaskDelay() 函数:

FreeRTOS 核心延时函数,作用是让当前任务进入阻塞状态,让出 CPU 给其他任务执行,直到延时时间结束。

这是实现任务周期执行的关键,避免任务空循环占用全部 CPU 资源。

pdMS_TO_TICKS() 宏:

把毫秒数转换为 FreeRTOS 的系统节拍数(tick),是 FreeRTOS 推荐的写法,兼容性更好。

比如系统节拍频率为 1000Hz(1ms/tick)时,pdMS_TO_TICKS(500) 就等于 500;若频率为 100Hz(10ms/tick),则等于 50,无需手动计算。

步骤 8:编译执行

1、编译工程代码,此时应无报错(若有报错,优先检查头文件路径和宏定义);

2、Proteus仿真软件对FreeRTOS的仿真支持尚不完善,编译完成的程序需在STM32嵌入式开发板上实际运行。目前市场上常见的STM32嵌入式开发板品牌包括普中电子、正点原子、野火等,其中基于STM32F103系列的开发板价格普遍在百十元人民币左右,非常适合新手用于嵌入式开发学习。

评论区

请 登录 后发表评论
暂无评论