从本章开始学习FreeRTOS, FreeRTOS 是一个 RTOS 类的嵌入式实时操作系统。 在学习和使用 FreeRTOS 之前, 需要先了解什么是 FreeRTOS? 为什么选择学习FreeRTOS? FreeRTOS 的特点以及FreeRTOS的编程风格。
        本章分为如下几部分:
1.1 初始 FreeRTOS
1.2 FreeRTOS资料说明
1.3 FreeRTOS 的编程风格

1.1 初识 FreeRTOS

1.1.1 什么是 FreeRTOS?

        首先看一下 FreeRTOS 的名字, 可以分为两部分:“Free” 和“RTOS”,“Free” 就是免费的、自由的、 不受约束的意思, “RTOS” 全称是 Real Time Operating System, 中文名是实时操作系统, 要注意的是,RTOS并不是值某一特定的操作系统, 而是指一类操作系统, 例如,µC/OS,FreeRTOS,RTX,RT-Thread 等这些都是 RTOS 类的操作系统。 因此,从FreeRTOS 的名字中就能看出, FreeROTS是一款免费的实时操作系统。
        本次学习的FreeRTOS 是众多 RTOS 类操作系统中的一种,FreeRTOS是一款​​免费开源的实时操作系统内核​​,专为资源受限的嵌入式微控制器设计。它的核心价值在于,通过​​多任务管理​​,让一个处理器能够“同时”处理多个任务,并确保关键任务能够在其截止时间前得到执行,从而满足嵌入式应用的实时性需求。
        为了快速抓住核心,下表概括了FreeRTOS的主要特征:

核心特征

说明

​​硬实时/软实时​​

采用​​抢占式调度​​,可满足严格时限(硬实时)或允许偶尔超时(软实时)的应用场景。

​​多任务并发​​

将复杂应用分解为多个独立​​任务​​,由调度器管理CPU时间,简化程序结构。

​​丰富的同步机制​​

提供​​队列、信号量、互斥量、事件组​​等,安全高效地实现任务间通信与同步。

​​高度可裁剪​​

内核轻量(最小可至约6KB ROM),功能模块可配置,适应不同资源约束的硬件平台。

​​多任务调度​​

支持基于优先级的​​抢占式调度​​(高优先级任务立即运行)和​​时间片轮转调度​​(同优先级任务轮流运行)。

​​强大的可移植性​​

使用C语言编写,已移植支持40多种处理器架构,包括ARM Cortex-M、RISC-V、ESP32等。

图 1.1.1.1 FreeRTOS的主要特征

1.1.2 为什么选择 FreeRTOS?

        FreeRTOS 操作系统是一个功能强大的 RTOS 操作系统, 并且能够根据需求进行功能裁剪,以满足各种环境的要求, FreeRTOS 的特点如下图所示:

图 1.1.2.1 FreeRTOS 特点

1.1.3 为什么要学习FreeRTOS?

        对于嵌入式开发者来说,学习FreeRTOS不是一道选择题,而是一门必修课。这主要是因为有的单片机开发环境默认集成FreeRTOS(比如ESP32的官方开发框架ESP-IDF构建在FreeRTOS之上)。为了让能快速把握全貌,下面这个表格总结了学习FreeRTOS为嵌入式开发带来的核心价值。

核心价值

说明

​​开发模式升级​​

从“裸机”的轮询架构升级为多任务并发的现代编程模型,使程序结构更清晰。

​​硬件潜力释放​​

充分利用处理器资源,实现真正的并行处理,提升系统效率和响应能力。

​​项目基石​​

有的单片机开发环境默认集成FreeRTOS(比如ESP-IDF及其所有组件和示例都基于FreeRTOS),要使用官方资源就必须掌握它。

​​资源管理专业化​​

提供队列、信号量、互斥锁等机制,能优雅地解决多任务环境下的资源共享和同步问题。

表1.1.3.1 学习FreeRTOS为嵌入式开发带来的核心价值说明

1.1.3.1 告别“裸机”限制

        在传统的“裸机”编程中,所有功能通常都塞在一个大的 while(1)循环里。这会导致一个严重问题:如果某个任务(比如读取传感器)需要等待,整个系统都会被阻塞,其他任务(如更新显示屏)也无法执行。
        FreeRTOS通过​​多任务并发​​解决这个问题。可以为每个独立的功能(如网络连接、传感器采集、用户界面更新)创建一个独立的​​任务​​。每个任务都有自己的运行上下文,操作系统内核(调度器)负责在多个任务之间快速切换,让它们看起来像是在同时运行。这带来了几个显著好处:

  • 模块化与团队协作​​:每个功能模块可以独立开发和测试,代码更易维护和复用;
  • ​​实时性保证​​:可以为不同任务设置​​优先级​​。例如,处理紧急指令的任务可以设为高优先级,确保它能立即抢占低优先级任务(如日志记录)的CPU时间,从而满足实时性要求。
1.1.3.2 发挥双核威力

        以ESP32单片机为例,是一款功能强大的​​双核​​处理器。FreeRTOS的SMP(对称多处理)支持允许将任务精确地分配到指定的CPU核心上运行。
        通过使用 xTaskCreatePinnedToCore()这个API,可以实现精细的任务分配。例如,将需要快速响应的Wi-Fi/蓝牙任务绑定到核心0,将复杂的后台数据处理任务绑定到核心1。这样就能真正发挥ESP32的硬件潜力,大幅提升整体处理能力,避免单个核心忙死而另一个核心闲置的情况。

1.1.3.3 使用官方生态的前提

        乐鑫为ESP32提供的​​ESP-IDF​​开发框架,其内核就是FreeRTOS。这意味着:

  • ​​官方示例基于FreeRTOS​​:几乎所有ESP-IDF的示例代码都是通过创建FreeRTOS任务来组织的;
  • ​​系统组件依赖FreeRTOS​​:Wi-Fi、蓝牙、文件系统等高级功能,其底层驱动和事件处理都依赖于FreeRTOS的任务调度和通信机制。

        因此,不学习FreeRTOS,就很难深入理解和灵活运用ESP-IDF提供的强大功能。

1.1.3.4 优雅处理多任务协作

        当多个任务需要访问同一资源(如全局变量、串口)时,如果没有保护机制,会导致数据混乱。FreeRTOS提供了一整套成熟的​​同步与互斥​​机制来优雅地解决这些问题:

  • 队列​​:任务间安全传递数据的管道,实现解耦;
  • ​​信号量和互斥锁​​:互斥锁带有优先级继承机制,能有效防止优先级反转问题,安全地保护共享资源;
  • ​​事件组​​:允许任务等待多个事件中的一件或多件发生后才继续执行。
1.1.3.5 如何开始学习
  • 搭建环境​​:按照ESP-IDF官方文档安装开发环境;
  • ​​理解核心概念​​:重点理解​​任务​​、​​状态​​(运行、就绪、阻塞、挂起)、优先级调度和上述的通信机制;
  • ​​从示例入手​​:从最简单的“hello_world”任务创建示例开始,编译、烧录、观察串口日志,体会任务的创建、延时和删除;
  • 逐步实践​​:尝试创建多个不同优先级的任务,观察调度行为;然后使用队列在任务间传递数据;最后在共享资源访问中引入互斥锁。

1.2 FreeRTOS资料说明

        获取FreeRTOS最权威、最实时的资料,FreeRTOS官网是最好的地方,FreeRTOS的官网网址是https://www.freertos.org/,打开后如下图所示:

图 1.2.1.1 FreeRTOS 官网

        FreeRTOS 的官网是全英文的,打开后分别是“Download FreeRTOS” 和“FreeRTOS Documentation”, 通过“Download FreeRTOS” 就能够下载到最新发布的 FreeRTOS, 而右侧的“FreeRTOS Documentation” 就是在 FreeRTOS 官网查看在线资料的入口,点击进入“FreeRTOS Documentation” 可以看到FreeRTOS相关介绍。

图 1.2.1.2 FreeRTOS 官网查看在线资料网页

        FreeRTOS 官方对相关相关API有详细说明,并且提供了两份 PDF 文档和一份文档配套的源代码, 其中一份PDF是FreeRTOS的教程指南, 另一份PDF是FreeRTOS的参考手。后续会参开着两份手册进行编程, 如下图所示:

图 1.2.1.3 FreeRTOS 编程参考书籍

1.3 FreeRTOS 的编程风格

      学习一个 RTOS, 搞懂它的编程的风格很重要,这可以大大提供我们阅读代码的效率。下面以 FreeRTOS中的数据类型、变量名、 函数名和宏这几个方面做简单介绍。

1.3.1 数据类型

       在 FreeRTOS 中, 对标准 C 的数据类型又进行了重定义,给它们取了一个新的名字, 比如 char 重新定义了一个名字 portCHAR, 这里面的 port 表示接口的意思,就是 FreeRTOS 要移植到这些处理器上需要这些接口文件来把它们连接在一起。我们在写程序的时候并非一定要遵循 FreeRTOS 的风格, 我们还是可以直接用 C 语言的标准类型。在 Cortex-M 内核的 MCU 中, short 为 16 位, long 为 32位。
       FreeRTOS 中详细的数据类型重定义在 portmacro.h 这个头文件中实现, 具体汇总见下表:

新定义的数据类型 实际的数据类型(标准类型)
portCHAR char
portSHORT short
portLONG long
portTickType unsigned short int 用于定义系统时基计数器的值和阻塞时间的值。当 FreeRTOSConfig.h 头文件中的宏configUSE_16_BIT_TICKS 为 1 时则为 16位。
unsigned int 用于定义系统时基计数器的值和阻塞时间的值。FreeRTOSConfig.h头文件中的宏configUSE_16_BIT_TICKS 为 0 时则为 32 位。
portBASE_TYPE long 根据处理器的架构来决定是多少位的, 如果是 32/16/8bit 的处理器则是 32/16/8bit 的数据类型。一般用于定义函数的返回值或者布尔类型。

表1.3.1.1 FreeRTOS 中的数据类型重定义

      portmacro.h 头文件中,FreeRTOS 数据类型重定义:

/* Type definitions. */
#define portCHAR		char
#define portFLOAT		float
#define portDOUBLE		double
#define portLONG		long
#define portSHORT		short
#define portSTACK_TYPE	uint32_t
#define portBASE_TYPE	long

typedef portSTACK_TYPE StackType_t;
typedef long BaseType_t;
typedef unsigned long UBaseType_t;

#if( configUSE_16_BIT_TICKS == 1 )
	typedef uint16_t TickType_t;
	#define portMAX_DELAY ( TickType_t ) 0xffff
#else
	typedef uint32_t TickType_t;
	#define portMAX_DELAY ( TickType_t ) 0xffffffffUL

        在编程的时候,如果用户没有明确指定 char 的符号类型, 那么编译器会默认的指定 char 型的变量为无符号或者有符号。正是因为这个原因,在 FreeRTOS 中,我们都需要明确的指定变量 char 是有符号的还是无符号的。 在 keil 中, 默认 char 是无符号的,但是也可以配置为有符号的,具体配套过程见下图。

图1.3.1.2 char 型变量的符号配置( KEIL)

1.3.2 变量名

        FreeRTOS 中采用了一套变量命名约定,类似于匈牙利命名法,通过前缀直观地表示变量的类型和属性,从而提高代码可读性。其规则总结如下:

(1)基础类型前缀

  • c:char 类型
  • s:short 类型
  • l:long 类型
  • x:portBASE_TYPE 类型(该类型为 FreeRTOS 中跨平台定义的整型)
  • x:也用于 FreeRTOS 中的结构体、任务句柄、队列句柄等自定义类型

(2)属性修饰前缀

  • u:表示无符号(unsigned)
  • p:表示指针(pointer)

(3)组合前缀规则
        前缀按顺序组合:指针 p + 无符号 u + 基础类型,例如:

  • uc:无符号 char 变量(unsigned char)
  • pc:char 指针变量(char *)
  • puc:无符号 char 指针(unsigned char *)
  • us:无符号 short 变量
  • px:portBASE_TYPE 指针(或任务/队列句柄指针)

(4)优点

  • 代码自注释:开发者能快速识别变量类型,无需查找定义;
  • 减少类型错误:在嵌入式开发中,明确类型有助于避免数据溢出、误用等问题;
  • 统一风格:提升 FreeRTOS 代码的可维护性和可移植性。

        注意:此约定是 FreeRTOS 的代码风格建议,并非强制语法要求,但遵循它能使代码更符合项目规范。

1.3.3 函数名

        函数名遵循一个信息丰富的格式,通常包含三部分:<返回值前缀> + <所属模块/文件名> + <动作描述>
        
函数名各部分的明确含义:

(1)返回值前缀

  • v:返回 void;
  • x:返回 portBASE_TYPE(或 BaseType_t)、句柄等;
  • (其他如 pv, ux, pc 等同上文)

(2)文件名/模块名
        函数名中紧随返回值前缀的部分(如 Task, Queue, Semaphore)直接指明了该函数在哪个源文件中定义。
        目的:极大地帮助开发者在代码库中快速定位函数定义,并立即了解其所属的功能模块。
        特别,如果一个函数是模块内部的私有函数(不对外公开的API),则会加上 prv(private)前缀。例如:prvSampleFunction(),表示这是一个内部使用的私有函数。

        具体示例如下:

函数名 解析 对应文件

vTaskPrioritySet

v (void返回) + Task (属于任务模块) + PrioritySet (设置优先级) task.c

xQueueReceive

x (portBASE_TYPE返回) + Queue (属于队列模块) + Receive (接收) queue.c

vSemaphoreCreateBinary

v (void返回) + Semaphore (属于信号量模块) + CreateBinary (创建二进制信号量) semphr.h(注:其实现通常在 semphr.c 中)

表1.3.2.1 示例说明

        注:关于 vSemaphoreCreateBinary,它是一个历史API(现已被 xSemaphoreCreateBinary 取代),但其命名规则依然符合上述约定。

        设计优势总结:

        这种命名约定的核心优势在于 “自解释性” 和 “可追溯性”:

  • 快速定位:看到函数名即可知道去哪个源文件(task.c, queue.c 等)查找其定义或实现,无需全局搜索;
  • 明确作用域:通过 prv 前缀清晰区分公共API与私有实现,增强了模块的封装性;
  • 理解功能:结合模块名和动作描述,能直观推断函数的大致作用;
  • 提高效率:显著降低了在大型项目(如RTOS内核)中阅读、维护和调试代码的成本。

        总而言之,FreeRTOS 通过将返回值类型、所属文件/模块和功能描述三者编码进函数名,构建了一套极其高效、清晰的代码导航和阅读理解体系。

1.3.4 宏

        宏均是由大写字母表示,并配有小写字母的前缀, 前缀用于表示该宏在哪个头文件定义,部分举例具体见下表:

前缀 宏定义的文件
port (举例, portMAX_DELAY) portable.h
task (举例, taskENTER_CRITICAL()) task.h
pd (举例, pdTRUE) projdefs.h
config(举例, configUSE_PREEMPTION) FreeRTOSConfig.h
err (举例, errQUEUE_FULL) projdefs.h

表1.3.2.2 宏定义举例

        需要注意是信号量的函数都是一个宏定义,但是它的函数的命名方法是遵循函数的命名方法而不是宏定义的方法。
        在贯穿 FreeRTOS 的整个代码中,还有几个通用的宏定义我们也要注意下,都是表示 0 和 1 的宏,具体见下表:

实际的值
pdTRUE 1
pdFALSE 0
pdPASS 1
pdFAIL 0

表1.3.2.3 通用宏定义

1.3.5 格式

        一个 tab 键等于四个空格键。我们在编程的时候最好使用空格键而不是使用 tab 键,当两个编译器的 tab 键设置的大小不一样的时候,代码移植的时候代码的格式就会变乱,而使用空格键则不会出现这种问题。具体在MDK中设置方法如下:

图1.3.5.1 设置对话框

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐