FreeRTOS 基础
FreeRTOS 一定会有一个空闲任务(Idle Task)。空闲任务是 FreeRTOS 自动创建的任务,具有最低优先级(优先级为 0),并且是系统运行时的“默认任务”。
调度器
操作系统(如 FreeRTOS)的核心组件之一,负责管理任务的执行顺序和资源分配。它的主要作用是决定在任意时刻哪个任务可以运行,并确保系统的资源被高效、公平地使用。
调度器的主要职责包括:
- 任务切换:在多个任务之间切换执行,确保每个任务都能获得 CPU 时间。
- 优先级管理:根据任务的优先级决定执行顺序,高优先级任务优先执行。
- 资源分配:管理任务对 CPU、内存等系统资源的使用。
- 状态管理:跟踪任务的状态(如运行、就绪、阻塞等),并根据状态决定任务的调度。
任务的状态
在 FreeRTOS 中,任务可以处于以下几种状态:
- 运行(Running):任务正在占用 CPU 执行。
- 就绪(Ready):任务已准备好运行,但尚未获得 CPU 时间。
- 阻塞(Blocked):任务正在等待某个事件(如信号量、队列、延时等),暂时无法运行。
- 挂起(Suspended):任务被显式挂起,不会被调度器调度。
- 删除(Deleted):任务已被删除,不再参与调度。
调度器根据任务的状态决定是否将其调度到 CPU 上运行。
断言
在 FreeRTOS 中,断言(Assertion)是一种调试工具,用于在运行时检查程序中的某些条件是否为真。如果条件为假,则断言会触发,通常意味着程序中存在错误或异常情况。断言的主要目的是帮助开发者在开发和调试阶段快速发现和定位问题。
断言的作用
- 检查关键条件:确保程序在运行时满足某些假设或条件。
- 快速定位错误:当条件不满足时,断言会触发,并记录错误信息(如文件名、行号等)。
- 提高代码可靠性:通过断言,可以在开发阶段发现潜在的错误,避免这些错误在生产环境中引发更严重的问题。
FreeRTOS 中的断言实现
在 FreeRTOS 中,断言通常通过宏 configASSERT
实现。这个宏是 FreeRTOS 配置的一部分,开发者可以根据需要自定义其行为。例如:
1 |
- **
configASSERT(x)
**:这是一个宏,接受一个参数x
。 - **
if((x)==0)
**:检查参数x
的值是否为0
(即条件为假)。 - **
vAssertCalled(__FILE__,__LINE__)
**:如果x
为0
,则调用函数vAssertCalled
,并传入当前文件名(__FILE__
)和行号(__LINE__
)。
任务堆栈
在嵌入式系统中,任务堆栈是每个任务独立使用的内存区域,用于存储任务的局部变量、函数调用信息(如返回地址、寄存器值)以及任务切换时的上下文信息。设置任务堆栈大小是确保任务能够正常运行的关键步骤。
1. 存储局部变量
- 局部变量:每个任务在执行过程中会使用局部变量,这些变量存储在任务的堆栈中。
- 动态内存需求:任务的局部变量数量和大小可能因任务功能的不同而不同,因此需要为每个任务分配足够的堆栈空间。
2. 防止堆栈溢出
- 堆栈溢出:如果堆栈空间不足,可能导致堆栈溢出,破坏其他内存区域或导致系统崩溃。
- 调试困难:堆栈溢出通常难以调试,因为其症状可能是不可预测的(如数据损坏、程序崩溃等)。
1 |
|
任务挂起与恢复
有时候我们需要暂停某个任务的运行,过一段时间以后在重新运行。这个时候要是使用任务删除和重建的方法的话那么任务中变量保存的值肯定丢失了!FreeRTOS 给我们提供了解决这种问题的方法,那就是任务挂起和恢复,当某个任务要停止运行一段时间的话就将这个任务挂起,当要重新运行这个任务的话就恢复这个任务的运行。
vTaskSuspend()
作用
- 将指定任务挂起(暂停执行),任务将不再占用 CPU 时间片。
- 挂起的任务不会被调度器调度,直到它被恢复。
vTaskResume()
作用
- 恢复被挂起的任务,使其重新进入就绪状态,可以被调度器调度。
- 只能恢复被
vTaskSuspend()
挂起的任务,不能恢复被其他方式(如信号量阻塞)挂起的任务。
xTaskResumeFromISR()
作用
- 在 中断服务程序(ISR) 中恢复被挂起的任务。
- 与
vTaskResume()
类似,但专用于 ISR 环境。
列表和列表项
在 FreeRTOS 中,列表(List)和列表项(List Item) 是内核实现任务调度、任务通信和同步机制的核心数据结构。它们用于管理和组织任务、事件、队列等内核对象。
列表(List)
- 列表是一个双向链表数据结构,用于存储和管理多个列表项。
- 在 FreeRTOS 中,列表用于组织和管理任务、事件、队列等对象。
- 例如:
- 就绪任务列表:存储所有处于就绪状态的任务。
- 阻塞任务列表:存储所有因等待资源或事件而被阻塞的任务。
- 延时任务列表:存储所有因延时而被挂起的任务。
列表项(List Item)
- 列表项是列表中的节点,每个列表项可以链接到其他列表项,形成链表。
- 每个任务、事件或队列等内核对象都包含一个或多个列表项,用于将其插入到相应的列表中。
- 列表项通常包含以下信息:
- 指向下一个列表项的指针。
- 指向上一个列表项的指针。
- 列表项的值(用于排序或优先级管理)。
时间片
在 FreeRTOS 中,任务的运行顺序主要由 任务优先级 决定,同时也可以配置 时间片轮转(Time Slicing) 来实现相同优先级任务的公平调度。
- 如果多个任务具有 相同的优先级,调度器会为每个任务分配一个固定的时间片(Time Slice)。
- 任务在运行一段时间(时间片)后,会被强制切换,让下一个任务运行。
- 时间片的长度由系统节拍(Tick)决定,通常为
configTICK_RATE_HZ
的倒数。
FreeRTOS 使用 固定优先级调度算法,高优先级任务总是优先于低优先级任务运行。如果某个高优先级任务发生阻塞,但系统中没有其他 相同或更高优先级 的任务处于就绪态,调度器可能会切换到空闲任务。
句柄
句柄(Handle) 本质上是一个 标识符 或 引用,用于间接访问和管理资源(如文件、内存、设备、对象等)。句柄本身通常是一个整数或指针,但它并不是资源的直接地址,而是通过操作系统或库来映射到实际的资源。
1. 句柄的作用
句柄的主要作用是 抽象和管理资源,具体包括:
- 隐藏底层细节:
- 句柄封装了资源的底层实现细节,开发者无需关心资源的具体存储位置或管理方式。
- 提高安全性:
- 句柄是间接访问资源的,操作系统或库可以通过句柄验证权限,防止非法访问。
- 简化资源管理:
- 句柄可以统一管理不同类型的资源(如文件、内存、网络连接等),简化开发者的工作。
- 跨平台兼容:
- 句柄的抽象特性使得代码可以更容易地移植到不同的平台或操作系统。
2. 句柄的常见应用场景
句柄在计算机系统中广泛应用,以下是一些典型的例子:
(1) 文件句柄
- 在操作系统中,文件句柄用于标识和管理打开的文件。
- 例如,在 Windows 中,
HANDLE
类型用于表示文件句柄;在 Linux 中,文件描述符(File Descriptor)就是一种句柄。
(2) 窗口句柄
- 在图形用户界面(GUI)编程中,窗口句柄用于标识和管理窗口。
- 例如,在 Windows 中,
HWND
类型表示窗口句柄。
(3) 内存句柄
- 在操作系统中,内存句柄用于标识和管理分配的内存块。
- 例如,在 Windows 中,
HGLOBAL
表示全局内存句柄。
(4) 任务句柄
- 在实时操作系统(RTOS)中,任务句柄用于标识和管理任务。
- 例如,在 FreeRTOS 中,
TaskHandle_t
表示任务句柄。
(5) 数据库连接句柄
- 在数据库编程中,连接句柄用于标识和管理数据库连接。
- 例如,在 ODBC 中,
SQLHDBC
表示数据库连接句柄。
3. 句柄的实现原理
句柄的实现通常依赖于 句柄表(Handle Table),句柄表是操作系统或库内部维护的一个数据结构,用于将句柄映射到实际的资源。以下是句柄的基本工作原理:
- 资源申请:
- 当申请资源(如打开文件、创建任务)时,操作系统或库会分配一个句柄,并将句柄与资源关联。
- 句柄使用:
- 开发者通过句柄访问资源,操作系统或库会根据句柄表找到实际的资源。
- 资源释放:
- 当资源不再需要时,操作系统或库会释放资源,并回收句柄。
4. 句柄与指针的区别
句柄和指针都是用于访问资源的标识符,但它们有以下区别:
特性 | 句柄 | 指针 |
---|---|---|
直接性 | 间接访问资源,通过句柄表映射 | 直接访问资源,指向内存地址 |
安全性 | 更安全,操作系统可以验证权限 | 直接访问内存,可能存在安全隐患 |
抽象性 | 高度抽象,隐藏底层实现 | 具体,直接指向内存地址 |
跨平台性 | 更容易移植到不同平台 | 依赖于具体的内存地址,移植性较差 |
管理方式 | 由操作系统或库管理 | 由开发者管理 |
5. 示例
以下是一个简单的句柄使用示例(以 FreeRTOS 任务句柄为例):
1 |
|
在这个例子中:
xTaskHandle
是一个任务句柄,用于标识和管理创建的任务。- 开发者通过
xTaskHandle
可以操作任务(例如删除任务、修改任务优先级等),而无需关心任务的具体实现细节。
消息队列
用于任务间通信的重要机制。它允许任务之间安全地传递数据,从而实现任务解耦和同步。
作用:
- 用于任务之间传递数据。
- 支持任务与任务、任务与中断服务程序(ISR)之间的通信。
特点:
- 队列是线程安全的,FreeRTOS 内部会处理并发访问的问题。
- 队列可以存储固定大小的数据项,每个数据项的大小和队列的长度由用户定义。
- 数据项可以是任意类型(如整数、结构体、指针等)。
工作方式:
- 任务可以向队列发送数据(写操作)。
- 任务可以从队列接收数据(读操作)。
- 如果队列已满,发送操作可以阻塞任务,直到队列有可用空间。
- 如果队列为空,接收操作可以阻塞任务,直到队列有数据。
创建队列
1 | QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize); |
KEYMSG_Q_NUM
:- 队列的长度,即队列中最多可以存储的数据项数量。
- 例如,如果
KEYMSG_Q_NUM
定义为10
,则队列最多可以存储 10 个数据项。
sizeof(u8)
:- 每个数据项的大小(以字节为单位)。
u8
通常是无符号 8 位整数(即unsigned char
),因此sizeof(u8)
返回 1。
发送数据到队列
1 | BaseType_t xQueueSend(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait); |
从队列接收数据
1 | BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait); |
查询队列状态
1 | CUBaseType_t uxQueueMessagesWaiting(QueueHandle_t xQueue); // 获取队列中当前的数据项数量 |
信号量
用于控制对共享资源的访问和任务同步,
二值信号
只有一个队列项的队列,队列要么是满的要么是空的,但是不同于队列,只传递状态不传递数据,并且同一时刻只能由一个任务访问。任务能否直接获取二值信号量,取决于信号量的当前值:
- 值为 1:任务可以直接获取信号量,信号量的值会变为 0。
- 值为 0:任务会被阻塞(如果指定了阻塞时间)或返回失败(非阻塞模式)。
在嵌入式开发中(尤其是使用 FreeRTOS 等实时操作系统时),变量名前面的 x
是一种命名约定,通常用于表示该变量是一个 句柄(Handle) 或 对象(Object),而不是普通的变量。除了 x
前缀,FreeRTOS 中还有其他常见的命名约定:
pv
前缀:表示指针(Pointer to Void),例如pvTaskParameter
。ul
前缀:表示无符号长整型(Unsigned Long),例如ulTaskCounter
。uc
前缀:表示无符号字符型(Unsigned Char),例如ucQueueStorage
。pc
前缀:表示指针字符型(Pointer to Char),例如pcTaskName
。
在 FreeRTOS 中,函数名前缀 x
通常表示函数的返回值是一个 有符号整数 或 枚举类型,而不是 void
。这种命名约定是为了让开发者通过函数名就能快速了解函数的返回值类型。Generic
通常表示该函数是一个 通用实现,可以处理多种不同的场景或类型。这种设计是为了避免代码重复,提高代码的复用性。
在任务或中断中释放信号量:
1 |
|
计数信号量
长度大于1的队列,依然无需关心队列中存储了什么数据,只需要关心队列是否为空即可。
创建计数型信号量:
1 |
|
释放和获取计数信号量:
1 |
|
计数型信号每次释放都会增加1,每次获取会减少1,但是无法直接更改计数型信号的值,而是通过释放和获取改变的。在获取信号量时,如果信号量的值大于 0,则将其减 1;如果信号量的值为 0,则阻塞(或返回错误),直到信号量的值大于 0。如下所示,当信号量为0时在xSemaphoreTake()
函数处发生阻塞,导致后面的程序不执行。
1 | //获取计数型信号量任务函数 |
互斥信号量和递归互斥信号量
当低优先级任务Task1长时间占用信号量时,同时高优先级任务Task2也要获取信号量时,高优先级任务就会发生阻塞,导致高优先级等待低优先级,同时比Task1优先级高但比Task2优先级低的任务Task3却会执行,导致任务执行的优先级混乱。为此提出互斥信号量,将Task1的优先级提升到和Task2的一样。
不同于二值信号,创建互斥信号量时,其初始值默认为 1。在创建后是直接可以进行获取的。任务在获取互斥信号后需要再释放信号。
递归互斥信号量与互斥信号量不同在于,可以多次获取信号量,同时也要释放同样次数的信号量。
内存管理
FRTOS函数、代码介绍
NVIC_PriorityGroupConfig()函数
用于 ARM Cortex-M 系列微控制器 中配置 嵌套向量中断控制器(NVIC,Nested Vectored Interrupt Controller) 的优先级分组。
1. 函数作用
该函数用于设置 NVIC 的优先级分组,即如何将 中断优先级寄存器 的位数分配给 抢占优先级 和 子优先级。在 ARM Cortex-M 系列中,每个中断的优先级由 抢占优先级 和 子优先级 组成:
- 抢占优先级:决定中断是否可以打断正在执行的中断。
- 子优先级:当多个中断具有相同的抢占优先级时,子优先级决定它们的执行顺序。
优先级分组决定了 抢占优先级 和 子优先级 在中断优先级寄存器中的位数分配。例如:
- 如果选择
NVIC_PriorityGroup_2
,则抢占优先级占 2 位,子优先级占 2 位。 - 如果选择
NVIC_PriorityGroup_4
,则抢占优先级占 4 位,子优先级占 0 位(即没有子优先级)。
NVIC_PriorityGroup_4
注意是占四位,而不是四个优先级,抢占优先级 可以取值为 0
到 15
(即 2^4 = 1624=16 个值)。
TaskHandle_t
1 | typedef void * TaskHandle_t; |
typedef
:用于定义类型别名。void *
:是一个 指向 void 类型的指针,即通用指针(可以指向任意类型的数据)。TaskHandle_t
:是新定义的类型别名。
TickType_t
是 FreeRTOS 中定义的一个数据类型,通常用于表示系统时钟节拍(Tick)的计数。它的具体定义取决于 FreeRTOS 的配置:
- 如果
configUSE_16_BIT_TICKS
被定义为1
,则TickType_t
是uint16_t
(16 位无符号整数)。 - 否则,
TickType_t
是uint32_t
(32 位无符号整数)。
uxSchedulerSuspended
通常用于 FreeRTOS 的调度器实现中,用于跟踪调度器的挂起状态。
1 | PRIVILEGED_DATA static volatile UBaseType_t uxSchedulerSuspended = ( UBaseType_t ) pdFALSE; |
PRIVILEGED_DATA
:- 这是 FreeRTOS 中定义的一个宏,用于将变量放置在特权数据段中。特权数据段通常只能由特权模式(如操作系统内核)访问,而不能由用户任务直接访问,以提高系统的安全性和稳定性。
static
:- 表示该变量具有静态存储期,仅在当前文件(或当前作用域)内可见,不会被其他文件访问。
volatile
:- 表示该变量是易变的,可能会被外部因素(如硬件或中断)修改。编译器在优化时不会对该变量进行假设,每次访问都会从内存中读取其值,而不是使用缓存的值。
UBaseType_t
:- 这是 FreeRTOS 中定义的一个无符号整型数据类型,通常用于表示无符号的基础数据,具体实现可能是
unsigned int
或unsigned long
,取决于目标平台。
- 这是 FreeRTOS 中定义的一个无符号整型数据类型,通常用于表示无符号的基础数据,具体实现可能是
uxSchedulerSuspended
:- 变量名,表示“调度器是否被挂起”。在 FreeRTOS 中,调度器挂起意味着任务切换被暂停,所有任务将无法切换,直到调度器恢复。
pdFALSE
:- 这是 FreeRTOS 中定义的一个常量,表示“假”或“否”,通常值为
0
。
- 这是 FreeRTOS 中定义的一个常量,表示“假”或“否”,通常值为
( UBaseType_t ) pdFALSE
:- 将
pdFALSE
强制转换为UBaseType_t
类型,以确保类型匹配。
- 将
vTaskDelay()
函数:
让当前任务挂起一段时间(以系统时钟节拍 Tick
为单位)。在这段时间内,任务不会占用 CPU,调度器会将 CPU 分配给其他任务。
参数
xTicksToDelay
:需要延迟的时间,以系统时钟节拍(Tick)为单位。如果传入0
,则任务会立即让出 CPU。
1 | void vTaskDelay( const TickType_t xTicksToDelay ) |
vTaskStartScheduler()
函数
FreeRTOS 中的一个核心函数,用于 启动任务调度器。调用这个函数后,FreeRTOS 会开始调度任务,系统正式进入多任务运行状态。
- 初始化调度器所需的数据结构和资源。
- 启动 SysTick 定时器(或其他硬件定时器),用于生成系统节拍(tick)。
- 启动空闲任务(Idle Task),这是一个优先级最低的任务,当没有其他任务运行时,空闲任务会运行。
- 如果启用了软件定时器功能,还会启动定时器服务任务(Timer Service Task)。
- 开始调度任务,切换到最高优先级的就绪任务。
taskENTER_CRITICAL()
和taskEXIT_CRITICAL()
FreeRTOS 提供的宏,用于实现 临界区保护。它们的作用是确保在多任务环境下,某些关键代码段不会被中断或其他任务打断,从而保证数据的完整性和一致性。
- 临界区 是指一段代码,在执行过程中不能被中断或其他任务打断。
- 在多任务系统中,如果多个任务或中断服务程序(ISR)同时访问共享资源(如全局变量、硬件寄存器等),可能会导致数据竞争(Race Condition)或数据不一致。
- 临界区保护机制用于避免这种问题。
临界区内的代码应尽可能短,因为禁用中断会影响系统的实时性。长时间禁用中断可能导致任务调度延迟或中断丢失。
printf()
在 PC 上,printf
默认输出到控制台(如终端或命令行窗口)。但在 STM32 等嵌入式系统中,没有默认的控制台,因此需要将 printf
的输出重定向到具体的硬件外设(如串口)。在printf内部会调用fputc函数进行输出。
fputc
的作用
fputc
是 C 标准库中的一个函数,用于将一个字符写入指定的文件流(FILE *f
)。printf
函数在内部会调用fputc
,将格式化后的字符逐个输出到标准输出流(通常是控制台)。
重写 fputc
通过串口1输出到电脑的XCOM:
1 | //重定义fputc函数 |
sprintf
的使用
1 | sprintf((char*)p, "Total Size:%d", msgq_total_size); |
- 将
msgq_total_size
的值插入到格式化字符串"Total Size:%d"
中,生成类似"Total Size:20"
的字符串。 - 结果存储在
p
指向的内存中。
portDISABLE_INTERRUPTS()
和portENABLE_INTERRUPTS()
1. 作用
portDISABLE_INTERRUPTS()
:禁用中断。portENABLE_INTERRUPTS()
:启用中断。
这些宏通常用于临界区保护,即在执行关键代码时,防止被中断打断,以确保操作的原子性。
configMAX_SYSCALL_INTERRUPT_PRIORITY
FreeRTOS 实时操作系统中的一个配置宏,用于定义 系统调用中断的最高优先级。它的作用是确保 FreeRTOS 内核能够安全地管理中断,并防止高优先级中断干扰系统的正常运行。
- 优先级 高于
configMAX_SYSCALL_INTERRUPT_PRIORITY
的中断被称为 不可屏蔽中断,FreeRTOS 无法管理这些中断。 - 优先级 低于或等于
configMAX_SYSCALL_INTERRUPT_PRIORITY
的中断被称为 可屏蔽中断,FreeRTOS 可以管理这些中断。
1 |
vTaskDelete()
vTaskDelete()
是 FreeRTOS 实时操作系统中的一个 API 函数,用于 删除一个任务。当一个任务不再需要运行时,可以通过调用 vTaskDelete()
将其从系统中移除,并释放其占用的资源。
删除开始任务(如 vTaskDelete(StartTask_Handler);
)是一种常见的编程模式。它的目的是在完成初始化工作后,释放开始任务占用的资源,使系统更加高效。
1 | //开始任务任务函数 |