FreeRTOS2
一、队列
1)什么是队列?
队列能够保存的数据项的最大数量称为队列的“长度”。在创建队列时,会设置其长度以及每个数据项的大小。队列通常用作先入先出(FIFO)缓冲区,其中数据被写入队列的末尾(尾部),并从队列的前端(头部)移除。下图展示了数据被写入和从一个用作 FIFO 的队列中读取的情况。同时,也有可能从队列的前端写入数据(比如当队列为空时,写入的一个数据既在队列的末端也在队列的前端)。当数据从队列的前端移除时,后面的数据自动移动到队列的前端,相当于覆盖已经在队列前端的数据。
2)介绍一下队列的主要特点:
◆ 多任务访问
队列本身就是对象,可以被任何知道该队列的任务或中断服务程序访问。任意数量的任务都可以向同一个队列写入数据,同样地,任意数量的任务也可以从同一个队列读取数据。在实践中,一个队列有多个写入者的情况非常普遍,但一个队列有多个读取者的情况则相对较少。
◆ 队列读取阻塞
当任务试图从队列中读取数据时,可以选择性地指定一个“阻塞”时间。如果队列已经清空,这个任务将保持在阻塞状态,以等待队列中有数据可用,这个等待的时间就是阻塞时间。当另一个任务或中断将数据放入队列时,处于阻塞状态等待队列中数据可用的任务将自动转移到就绪状态。如果在数据可用之前,指定的阻塞时间已到期,任务也将自动从阻塞状态转移到就绪状态。队列可以有多个读取者,因此单个队列上可能有多个任务因等待数据而被阻塞。当出现这种情况时,数据可用时只有一个任务会被解除阻塞。被解除阻塞的任务总是等待数据的最高优先级任务。如果有两个或更多被阻塞的任务具有相同的优先级,则解除阻塞的任务将是等待时间最长的那个任务。
◆ 队列写入阻塞
与从队列中读取时一样,任务向队列写入时也可以选择性地指定阻塞时间。在这种情况下,阻塞时间是指如果队列已经满了,任务在阻塞状态下等待队列空间可用的最长时间。队列可以有多个写入者,因此一个队列可能有多个任务阻塞在该队列上等待完成发送操作。当这种情况发生时,只有当队列上有空间可用时,才会解除一个任务的阻塞。被解除阻塞的任务始终是等待空间的最高优先级任务。如果两个或多个被阻塞的任务具有相同的优先级,那么被解除阻塞的任务是等待时间最长的任务。
3)CMSIS提供的接口:
4)使用CubeMX配置
5)常使用的函数接口
在生成的 app_freertos.c 文件中,MX_FREERTOS_Init()函数在启动调度器之前创建,其中 osMessageQueueNew() 函数用于创建队列,采用 CMSIS的 API函数,该函数的原型如下:
1.创建队列
osMessageQueueId_t osMessageQueueNew (uint32_t msg_count,uint32_t msg_size,const osMessageQueueAttr_t * attr
)
该函数创建并初始化一个消息队列对象。该函数的参数及返回值说明如下:msg_count :队列中消息的最大数量。msg_size :消息的最大大小(以字节为单位)。attr :消息队列属性。NULL:使用默认值。
返回值:队列的 ID,供其他函数引用;如果发生错误,则返回 NULL。
//注意:此函数不能从中断服务例程中调用
2.写入消息队列的函数
osStatus_t osMessageQueuePut (osMessageQueueId_t mq_id,const void *msg_ptr, uint8_t msg_prio, uint32_t timeout
);
osStatus_t osMessageQueueGet (osMessageQueueId_t mq_id, void *msg_ptr, uint8_t *msg_prio, uint32_t timeout
);
mq_id是队列的变量名,即我们定义的QueueUartByteHandle;
msg_ptr是要入队/出队的数据块指针;
msg_prio比较奇怪,入队和出队对应的变量类型不同,表示优先级,但一般都赋NULL;
最后的timeout,不等待就为0,永远等待用osWaitForever。这两个函数都可以在中断中运行(timeout写0)。
3.读取消息队列内容的函数
osStatus_t status = osMessageQueuePut(mq_id, &msg, 0, 0); // 超时设为0
注:如果想在中断中运行这两个函数,须遵循以下关键条件
设置超时时间为0调用时必须将timeout参数设为 0,以确保非阻塞行为。中断中不允许阻塞,否则可能导致未定义行为。
二、互斥与同步
1.同步
(1)介绍同步
同步:顺序执行
进程同步是指多个进程中发生的事件存在某种时序关系,必须协同动作共同完成一个任务。简单来讲同步是一种协作关系。
拿接力比赛举例子,所谓的同步就是当第一个人交接完第二个人才能跑,第二个人跑完第三个人才能跑,强调的是一种顺序
两个任务运行时,任务A需要获取任务B此时运行到某一步的运行结果或者信息,才能进行自己的下一步工作,这个时候就得等待任务B与自己通信(发送某一个消息或信号),任务A再继续执行。这种任务之间相互等待对方发送消息或信号的协作就叫做任务同步。
(2)实现同步的方式
也可以通过(全局变量)标志位实现
补充volatile:
变量i使用volatile关键字修饰的原因
1. 编译器优化的风险
代码如下:
for(int i=0;i<20000000;i++)
{
t1++;
}
编译器在优化代码时,可能会发现 for(i=0; i<20000000; i++) 的循环体(t1++)没有产生任何可观测的副作用(假设 t1 未被其他代码使用或未声明为 volatile)。
在这种情况下,编译器可能会直接跳过整个循环,或者将循环优化为 t1 +=20000000(直接计算结果,而无需执行循环)。这会导致循环失去其“时间消耗”的作用。
2. volatile 的作用
volatile 强制编译器每次访问变量时都从内存中读取,而非使用寄存器中的缓存值。这会禁止编译器对 i 的访问进行优化。
对于 for(i=0; i<20000000; i++),使用 volatile 确保:
循环计数器 i 的每次递增都是真实发生的。
循环体(t1++)会被执行完整的 20000000 次,不会被优化为更高效但不符合预期的形式。
2.互斥
(1)介绍互斥
互斥访问临界资源
多个任务在运行过程中,都需要某一个资源时,它们便产生了竞争关系,它们可能竞争某一块内存空间,也可能竞争某一个IO设备。当一方获取资源时,其他任务只能在该任务释放资源之后 才能去访问该资源,这就是任务互斥。简单来说,互斥是一种竞争关系。
举例:
假如多个任务同时申请一台打印机,而先申请打印机的一方先使用打印机,当它用完时在给其他进程使用。在一个任务使用打印机期间,其他任务对打印机的使用申请不予满足,这些任务必须等待
(2)实现互斥的方式
使用了全局变量(标志位)实现互斥;同时竞争临界资源(不同的任务的互斥代码相同!)
实现了互斥,但是会有个隐患 存在
假设当前的系统不止当前两个任务,会有很多其他任务。
当任务1运行到其函数的35行时被切换出去,等任务2运行,任务2 开始打印到一半被打断,任务1再次被调度时从36行开始,开始打印任务1的内容,此时互斥失效(任务1已经判断过了,因此可以继续向下执行36行代码,因此互斥就失效了),这种概率很低,可能需要程序跑上近千万次,但是这种隐患我们必须要考虑到。
三、信号量
1)介绍信号量
信号量代表某一类资源,其值表示系统中该资源的数量:
信号:起通知作用
量:还可以用来表示资源的数量
信号量的值>0, 表示有资源可以用, 可以申请到资源,
信号量的值=0, 表示没有资源可以用, 无法申请到资源, 阻塞.
当"量"没有限制时,它就是"计数型信号量"(Counting Semaphores)
当"量"只有 0、1 两个取值时,它就是"二进制信号量"(Binary Semaphores)
支持的动作:"give"给出资源,计数值加 1;"take"获得资源,计数值减 1计数型信号量的典型场景是:
计数:事件产生时"give"信号量,让计数值加 1;处理事件时要先"take"信号量,就是获得信号量,让计数值减 1。
资源管理:要想访问资源需要先"take"信号量,让计数值减 1;用完资源后"give"信号量,让计数值加 1。
2)两种信号量的对比:
信号量的计数值都有限制:限定了最大值。如果最大值被限定为 1,那么它就是二进制信号量;如果最大值不是 1,它就是计数型信号量。
3)常用到的函数接口(CMSIS提供的)
osSemaphoreId_t osSemaphoreNew(uint32_t max_count, uint32_t initial_count, const osSemaphoreAttr_t *attr);
参数说明
max_count:信号量的最大计数值(即信号量的上限)。
如果是二进制信号量,此值设为 1;如果是计数信号量,设为允许的最大计数(例如 10)。
initial_count:信号量的初始计数值。
必须满足 0 ≤ initial_count ≤ max_count。
例如:
二进制信号量初始为可用:initial_count = 1。
资源初始全部空闲:initial_count = max_count。
attr:指向信号量属性的指针(类型为 osSemaphoreAttr_t),可配置:
信号量的名称(调试用)。
自定义内存分配(不依赖动态分配)。
其他底层相关参数。
如果设为 NULL,使用默认属性(动态分配内存,无名信号量)。
返回值
成功:返回信号量的 ID(osSemaphoreId_t 类型),后续操作(如 osSemaphoreAcquire/osSemaphoreRelease)需使用此 ID。
失败:返回 NULL(例如内存不足或参数无效)。
1.该函数的可以用于创建信号量(不限定在二进制或计数信号量上)在CubeMX配置会自动生成上述代码
函数原型:
osStatus_t osSemaphoreAcquire(osSemaphoreId_t semaphore_id, uint32_t timeout);功能:该函数会减少信号量的计数。如果信号量的计数值大于0,则减1并立即返回。如果信号量计数值为0,则调用任务会阻塞,直到信号量被释放(或其他任务释放了信号量)或者超时。
参数:
- semaphore_id: 信号量的ID,由osSemaphoreNew创建时返回。
- timeout: 超时时间,单位是毫秒(ms)。可以设置为:
* 0:表示函数立即返回,如果无法获取信号量则返回错误。
* osWaitForever:表示无限等待,直到获取到信号量。
* 具体数值:表示等待的毫秒数。
返回值:
- osOK: 成功获取信号量。
- osErrorTimeout: 在指定的超时时间内未获取到信号量。
- osErrorResource: 如果timeout=0时信号量不可用。
- osErrorParameter: 参数错误,比如semaphore_id无效。
- osErrorISR: 在中断服务程序中调用,但该信号量不允许在ISR中使用(注意:有些实现可能允许在ISR中使用,但通常需要特定的函数版本)。
函数原型:
osStatus_t osSemaphoreRelease(osSemaphoreId_t semaphore_id);
功能:该函数会增加信号量的计数。如果有任务正在等待该信号量,那么等待的任务会被唤醒(一个任务,如果有多个任务在等待,则根据优先级或等待顺序唤醒一个)。信号量的计数有一个最大值,由创建信号量时指定,如果当前计数已经达到最大值,则释放操作失败,返回osErrorResource。
参数:
- semaphore_id: 信号量的ID。返回值:
- osOK: 成功释放信号量。
- osErrorResource: 信号量计数已经达到最大值(无法再增加)。
- osErrorParameter: 参数错误,semaphore_id无效。
- osErrorISR: 在中断服务程序中调用,但该信号量不允许在ISR中释放(同样,有些实现可能有ISR安全版本)。注意:
信号量通常用于资源管理(计数信号量)或任务同步(二值信号量,即最大计数为1的信号量)。
在中断服务程序(ISR)中使用时,需要使用特定的函数版本(如以FromISR结尾的函数)或者确保该信号量支持在ISR中使用。但CMSIS-RTOS2标准中,这两个函数可以在ISR中使用,前提是系统支持。然而,在ISR中调用时,timeout参数必须为0(因为ISR不能阻塞)。
4)在CubeMX上配置信号量
5)实例:
题目:
目标:理解计数型信号量如何管理资源池
场景:模拟有5个车位的停车场,10辆车随机进出
#include "cmsis_os2.h"#define PARKING_SPOTS 5
osSemaphoreId_t parking_sem; // 停车位信号量//在FreeRTOS实现时,下面代码就是每个任务的执行函数实现的内容
//任务名:有属性以及任务函数等;需要句柄,方便对任务进行操作
//任务函数:一般可能需要先声明后定义;
void car_task(void *arg) {char *car_id = (char *)arg;uint32_t park_time = 100 + (osRandom() % 400); // 随机停车时间while(1) {// 尝试进入停车场printf("\n%s waiting for parking...", car_id);if (osSemaphoreAcquire(parking_sem, osRandom() % 1000) == osOK) {// 成功获取车位printf("\n[%s PARKED] Spots left: %d", car_id, osSemaphoreGetCount(parking_sem));// 停车中...osDelay(park_time);// 离开停车场osSemaphoreRelease(parking_sem);printf("\n[%s LEFT] Free spots: %d", car_id, osSemaphoreGetCount(parking_sem) );} else {printf("\n%s gave up waiting!", car_id);}osDelay(500 + (osRandom() % 1500)); // 等待下次出行}
}int main(void) {// 创建计数型信号量(初始5个车位,最大5个)parking_sem = osSemaphoreNew(PARKING_SPOTS, PARKING_SPOTS, NULL);// 创建10辆车const char *cars[] = {"Car1","Car2","Car3","Car4","Car5","Car6","Car7","Car8","Car9","Car10"};for(int i=0; i<10; i++) {osThreadNew(car_task, (void*)cars[i], NULL);}osKernelStart();
}
上述代码仅仅使用信号量能实现基本要求,但是考虑到模拟时需要在串口看效果,串口是临界资源,因此需要使用互斥量来完善。
四、互斥量
互斥量和二进制的信号量都是来实现对单一资源的互斥访问的,那有了二进制信号量了为什么还需要互斥量呢,这其实是用来解决一个问题。
信号量可以由任何任务释放,这会有问题,我们想要实现的是谁上锁谁来解锁,还有优先级反转的问题
.CMSIS提供常用函数接口
osMutexId_t osMutexNew(const osMutexAttr_t *attr);
功能:创建新的互斥锁对象
参数:
attr:指向互斥锁属性的指针(可为 NULL 使用默认属性)
返回值:成功:互斥锁 ID失败:NULL
属性结构体 osMutexAttr_t:typedef struct {const char *name; // 互斥锁名称(调试用)uint32_t attr_bits; // 属性位(见下方标志)void *cb_mem; // 控制块内存(通常为NULL)uint32_t cb_size; // 控制块大小} osMutexAttr_t;
属性位标志:osMutexRecursive:创建递归互斥锁osMutexPrioInherit:启用优先级继承(防止优先级反转)osMutexRobust:强健互斥锁(FreeRTOS 不支持此特性)
osStatus_t osMutexAcquire(osMutexId_t mutex_id, uint32_t timeout);
功能:获取互斥锁所有权
参数:
mutex_id:互斥锁 ID
timeout:超时时间(单位:tick)
0:不等待,立即返回
osWaitForever:无限期等待
n:等待 n 个 tick
返回值:
osOK:成功获取锁
osErrorTimeout:超时未获取
osErrorResource:锁不可用(timeout=0时)
osErrorParameter:参数错误
osStatus_t osMutexRelease(osMutexId_t mutex_id);
功能:释放互斥锁所有权
参数:互斥锁 ID
返回值:
osOK:成功释放
osErrorResource:当前任务不持有锁
osErrorParameter:参数错误
osStatus_t osMutexDelete(osMutexId_t mutex_id);
功能:删除互斥锁并释放资源
参数:互斥锁 ID
返回值:
osOK:成功删除
osErrorParameter:参数错误
osErrorResource:锁仍被占用
osThreadId_t osMutexGetOwner(osMutexId_t mutex_id);
功能:获取当前持有互斥锁的任务 ID
参数:互斥锁 ID
返回值:
持有锁的任务 ID
NULL:无任务持有或参数错误
3.优先级反转与优先级继承
使用互斥锁存在优先级反转问题,优先级继承是 FreeRTOS 互斥量(Mutex)内置的一种解决优先级反转问题的机制。
1)介绍什么是优先级反转问题
优先级反转是一种不符合预期调度逻辑的现象。低优先级任务 无意中(或间接地)阻塞了 高优先级任务 的执行,导致高优先级任务被迫等待低优先级任务,甚至可能被中等优先级任务抢占,从而极大地增加高优先级任务的延迟。
为什么会发生? 当任务通过互斥量(Mutex)共享资源时:
1. 低优先级任务(L) 获取了互斥量(进入临界区,访问共享资源)。
2. 高优先级任务(H) 准备就绪,抢占 L。H 也需要访问同一个共享资源,尝试获取互斥量。
3. 互斥量被 L 持有,所以 H 被阻塞(进入阻塞态)。调度器切换回 L 继续执行(因为它现在是就绪的最高优先级任务)。
4. 此时,中等优先级任务(M) 准备就绪。由于 M 的优先级高于 L 但低于 H,它会抢占 L 开始执行。
5. 问题出现: M 不需要那个共享资源,它只是执行自己的代码。H(最高优先级)本应最快执行完,但现在不仅被 L(低优先级)阻塞,还被 M(中等优先级)无限期地延迟!因为 M 只要处于就绪态就能一直运行(或者直到被更高优先级的任务抢占,但此时 H 被阻塞了),而 L 无法继续运行释放互斥量。H 的延迟时间变成了 L 执行临界区的时间 加上 M 可能运行的时间,这完全违背了优先级调度的原则。
后果: 高优先级任务的响应时间变得不可预测,甚至可能错过其截止期限(deadline),导致系统实时性丧失。
时间轴 | 任务状态
-------------------
t1 | L (低) 获取 Mutex, 运行
t2 | H (高) 就绪,抢占 L,尝试获取 Mutex -> 阻塞 (等待 L 释放)
t3 | L (低) 恢复运行 (最高就绪任务)
t4 | M (中) 就绪,抢占 L (因为 M > L), 运行
t5 | M (中) 继续运行... (H 在等 L, L 无法运行因为 M 在运行)
t6 | M (中) 完成或阻塞
t7 | L (低) 恢复运行
t8 | L (低) 释放 Mutex
t9 | H (高) 获取 Mutex, 终于运行! (被严重延迟)
2)介绍优先级继承
优先级继承是 FreeRTOS 互斥量(Mutex)内置的一种解决优先级反转问题的机制。
● 如何工作?
1. 当高优先级任务(H) 尝试获取一个已被低优先级任务(L) 持有的互斥量而被阻塞时。
2. FreeRTOS 内核临时将低优先级任务(L)的优先级提升到与阻塞它的最高优先级任务(H)相同的优先级。
3. 提升后的 L 运行: 现在,L 以 H 的高优先级运行。
● L 会尽快执行完它的临界区代码并且 L 不会被中等优先级任务(M)抢占(因为现在 L 的临时优先级等于 H,高于 M)。
●L 释放互斥量: 当 L 释放互斥量时:
1. FreeRTOS 自动将 L 的优先级恢复到它原来的低优先级。
2. H(正在等待该互斥量)立即解除阻塞。由于 H 的优先级最高(现在和 L 的原始优先级一样),它立刻抢占 L 开始执行。
● 效果:
1. 防止了中等优先级任务(M)的干扰: 因为 L 在持有互斥量期间被临时提升到了高优先级,M 无法抢占 L。
2. 限制了高优先级任务(H)的最大阻塞时间: H 的阻塞时间被限制在低优先级任务(L)执行其临界区代码所需的时间内。不会再包含任何 M 的执行时间。
3. 解决了优先级反转: 通过让 L 临时“继承” H 的优先级,确保了 L 能不受干扰地尽快释放资源,让 H 继续执行。
时间轴 | 任务状态 | 优先级变化
--------------------------------
t1 | L (低) 获取 Mutex, 运行 | L: Low
t2 | H (高) 就绪,抢占 L,尝试获取 Mutex -> 阻塞 | L: Low, H: 阻塞
t3 | **内核提升 L 优先级到 High!** | **L: High (临时)**
t4 | L (临时高) 恢复运行 | L: High
t5 | M (中) 就绪 -> **无法抢占 L!** (因为 L 现在 High > M) | L: High, M: 就绪但未运行
t6 | L (临时高) 继续运行... | L: High
t7 | L (临时高) 释放 Mutex | L: High
t8 | **内核恢复 L 优先级到 Low** | **L: Low**
t8 | H (高) 获取 Mutex, 立即抢占 L 并运行 | H: High
t9 | H (高) 完成运行...
注意:
1. 仅限互斥量: 优先级继承是 FreeRTOS 互斥量(Mutex) 特有的特性。
2. 自动触发: 优先级继承是由内核在检测到高优先级任务因互斥量阻塞在低优先级任务上时自动触发的。开发者不需要(也无法)在应用代码中显式调用优先级继承。
3. 临时提升: 优先级的提升是临时的,仅在低优先级任务持有互斥量且阻塞了更高优先级任务期间有效。一旦互斥量被释放,低优先级任务的优先级立即恢复原状。