ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FreeRTOS内核(三)队列的核心机制

FreeRTOS内核(三)队列的核心机制 一、队列的引入队列解决两大问题1.g_Count的灾难需要互斥性:临界资源 (Critical Resource)一句话定义临界资源一次只允许一个“执行流”任务或中断访问的共享资源。通俗理解图书馆的“孤本藏书”图书馆里有本珍贵的“孤本藏书”临界资源。它不能被两个人同时翻阅否则书会被撕坏或信息混乱。任何想读它的人任务都必须排队等上一个人完全读完释放资源并放回原处后下一个人才能开始。在单片机中具体指什么全局变量如uint32_t g_Count。硬件外设寄存器如 UART 的发送数据寄存器、GPIO 的输出状态寄存器。动态内存管理中的堆ucHeap[]。RTOS 系统 (多任务)任务看似“同时”运行实际是毫秒级切换。问题如果任务 A 和任务 B 同时修改同一个全局变量int g_Count会发生数据竞争 (Race Condition)。int fun_A() { while(1) { A; } } int fun_B() { while(1) { A; } } //相同的底层汇编 LDR R0, [A] ; 1. 读内存中A的值到寄存器R0 ADD R0, R0, #1; 2. 寄存器内加1 STR R0, [A] ; 3. 写回内存A任务在运行中被tick中断执行任务B时间轴 | v [任务 A] 读取 A (R00) 这里的A是变量不是任务A | (发生任务切换A 被挂起R00 被保存) v [任务 B] 读取 A (R00) 修改 (R01) 写回 (A 1) |(B 运行结束或切换) v [任务 A] 恢复现场 (R0 恢复为 0) 修改 (R01) 写回 (A 1) -- 错误期望是 2结果是 12. “轮询等待” 的效率问题任务需要有休眠唤醒机制任务 B 需要轮询判断flag是否置位哪怕flag很久才置位B 也要判断百万次浪费 CPU 资源而 RTOS 进一步引入 “事件驱动” —— 数据就绪时主动唤醒任务而非轮询。int fun_A() { while(1) { ... ... flag 1; } } int fun_B() { while(1) { if(flag 1) { ... ... } } }二、队列的核心原理1.核心一关中断实现互斥在裸机中我们可能用cli/sti关中断。在 FreeRTOS 队列操作中API 内部自动完成了这一步。原理任务切换依赖于SysTick 中断。如果在操作队列的关键代码段如拷贝数据、修改指针关闭中断SysTick 就无法触发任务切换就不会发生。注意其余硬件中断还能执行串口GPIO等此时当前任务独享 CPU就像在裸机环境中一样安全。如下写队列进入临界区执行操作最后退出临界区读队列同理2.核心二环形缓冲区 (Ring Buffer)队列的数据存储区是一个连续的内存块通过读索引和写索引实现循环使用。参考环形缓冲区的使用-CSDN博客static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, uint8_t * pucQueueStorage, const uint8_t ucQueueType, Queue_t * pxNewQueue )3.核心三链表实现阻塞与唤醒这是队列优于普通全局变量的地方无数据时不轮询直接休眠让出 CPU。A. 两个等待链表队列结构体内部维护了两个链表xTasksWaitingToReceive:等待接收的任务链表队列空时读者挂在这里。xTasksWaitingToSend:等待发送的任务链表队列满时写者挂在这里。B. 阻塞 (Block) 流程当任务 B 读队列但队列为空如xQueueReceive(q, val, 100)表示最多等 100ms。只要等待时间不等于 0一旦队列空都会调用阻塞流程记录将任务 B 的 TCB 插入到xTasksWaitingToReceive链表中。这样将来有数据时写任务知道要唤醒谁。状态变更将任务 B 从就绪链表 (Ready List)移除放入延迟链表 (Delay List)(状态变为 Blocked)。调度触发任务切换CPU 去运行其他就绪任务。结果任务 B 停止运行不浪费任何 CPU 周期。C. 唤醒 (Unblock) 流程当任务 A 向队列写入数据检查发现xTasksWaitingToReceive链表不为空有人在等。移除从等待链表中取出任务 B 的 TCB。状态变更将任务 B 从延迟链表移回就绪链表(状态变为 Ready)。调度如果任务 B 优先级高于当前任务 A立即触发切换任务 B 马上运行并读到数据。D.超时机制 (Timeout)流程当你调用xQueueReceive(q, val, 100)表示最多等 100ms计算唤醒时间当前 Tick 计数 xTickCount(例如 500)。超时时间 500 100 600。插入延迟链表任务阻塞时不仅加入队列的等待链表还会根据600这个时间点按时间顺序插入到内核的全局延迟链表 (Delayed Task List)中。延迟链表是按唤醒时间排序的50, 100, 600, 800...。SysTick 中断处理每 1msSysTick 中断执行xTaskIncrementTick()。检查延迟链表头部如果头部任务.唤醒时间 当前Tick说明超时了动作将该任务从延迟链表移除加入就绪链表。注意此时任务虽然就绪了但从队列角度看它还没拿到数据。任务恢复运行任务被调度运行后从阻塞点醒来。代码会再次检查队列发现有数据了吗如果有刚好被别的任务写了拷贝数据返回成功。如果没有真的是超时了从队列的等待链表中移除自己返回errQUEUE_TIMEOUT。总结一下就是任务读队列无数据→将自己加入xTasksWaitingToReceive→从就绪链表移到延迟链表休眠其他任务写队列→唤醒xTasksWaitingToReceive中的任务→任务移回就绪链表任务写队列满→将自己加入xTasksWaitingToSend→休眠其他任务读队列→唤醒xTasksWaitingToSend中的任务。以下为例子任务 B 设置等待 100ms。 动作 1把自己从就绪链表拔掉扔进延迟链表设定‘闹钟’为 100ms 后。 动作 2立即让出 CPU触发任务切换。 结果任务 B 彻底停止运行CPU 去跑任务 C、D、E 或者进入低功耗模式。 情况 一有人写数据了 比如在第 5ms任务 A 写了数据。内核发现任务 B 在等 立刻把 B 从延迟链表拽回就绪链表。B 醒来读到数据返回成功。 耗时仅 5ms且中间 B 没占 CPU。 情况 二没人写数据 时间一分一秒过去B 一直在睡觉。直到第 100ms SysTick 中断发现 B 的‘闹钟’响了。内核把 B 从延迟链表拽回就绪链表。 B 醒来发现还是没数据或数据被更高优先级抢了返回超时错误。以下为队列结构体源码/* * 调度器使用的队列结构体定义。 * 队列中的数据以拷贝方式入队而非引用/指针设计原因参考 * https://www.FreeRTOS.org/Embedded-RTOS-Queues.html */ typedef struct QueueDefinition /* 保留旧命名规则避免内核调试器兼容问题 */ { int8_t * pcHead; /* 指向队列存储区的起始地址环形缓冲区的头。 注队列内存布局 队列结构体 连续的存储缓冲区 此字段指向缓冲区第一个字节是环形Buffer的基准地址 */ int8_t * pcWriteTo; /* 指向缓冲区中「下一个可写入数据」的位置写指针。 每次写入数据后该指针会按 uxItemSize 偏移 到缓冲区末尾时绕回头部环形特性 */ union { QueuePointers_t xQueue; /* 当结构体用作「队列」时的专属数据 包含读指针pcReadFrom等核心字段 是环形缓冲区读操作的关键 */ SemaphoreData_t xSemaphore; /* 当结构体用作「信号量」时的专属数据 FreeRTOS中信号量是队列的特例长度1的队列 此字段复用队列结构体实现信号量功能 */ } u; List_t xTasksWaitingToSend; /* 因「写队列失败队列满」而阻塞的任务链表。 特性 1. 按任务优先级排序高优先级任务优先被唤醒 2. 当队列有空闲空间时从链表头部唤醒任务 */ List_t xTasksWaitingToReceive; /* 因「读队列失败队列空」而阻塞的任务链表。 特性 1. 按任务优先级排序 2. 当队列有新数据时从链表头部唤醒任务 */ volatile UBaseType_t uxMessagesWaiting; /* 队列中当前有效数据的个数已写入未读取。 核心作用 1. 判断队列是否为空0/满uxLength 2. 读写操作时原子更新关中断保护 */ UBaseType_t uxLength; /* 队列长度指队列能容纳的「元素个数」非字节数。 例uxLength5 表示队列最多存5个int/char等元素 */ UBaseType_t uxItemSize; /* 队列中单个元素的字节大小。 例uxItemSize4 表示每个元素是int类型4字节 写入/读取时按此大小拷贝数据 */ volatile int8_t cRxLock; /* 队列「读锁」计数器 作用场景中断中读队列时为避免嵌套问题会先锁队列 取值规则 - 等于 queueUNLOCKED-1队列未锁定 - 大于0表示队列锁定期间成功读取的元素个数 解锁时统一处理这些读取操作 */ volatile int8_t cTxLock; /* 队列「写锁」计数器 作用场景中断中写队列时的锁机制 取值规则同cRxLock记录锁定期间写入的元素个数 */ #if ( ( configSUPPORT_STATIC_ALLOCATION 1 ) ( configSUPPORT_DYNAMIC_ALLOCATION 1 ) ) uint8_t ucStaticallyAllocated; /* 标记队列内存是否为「静态分配」 - pdTRUE静态分配不允许free内存 - pdFALSE动态分配可调用vQueueDelete释放 */ #endif #if ( configUSE_QUEUE_SETS 1 ) struct QueueDefinition * pxQueueSetContainer; /* 指向队列所属的「队列集」Queue Set。 队列集用于监听多个队列/信号量 此字段标记当前队列归属哪个队列集 */ #endif #if ( configUSE_TRACE_FACILITY 1 ) UBaseType_t uxQueueNumber; /* 队列唯一编号调试用 跟踪工具如FreeRTOSTrace通过此编号识别不同队列 */ uint8_t ucQueueType; /* 队列类型标记调试用 区分普通队列、信号量、互斥量等不同类型 */ #endif } xQUEUE;三、读写流程图四、总结队列的本质带互斥保护 休眠唤醒机制的环形缓冲区解决多任务数据传输的 “数据破坏” 和 “轮询低效” 问题核心保障关中断实现读写互斥链表管理等待任务环形 Buffer 存储数据核心操作读队列无数据则休眠有数据 / 超时则唤醒读成功后唤醒等待写的任务写队列无空间则休眠有空间 / 超时则唤醒写成功后唤醒等待读的任务关键特性FreeRTOS 队列是 “线程安全” 的支持任务间 / 中断间读写无需用户手动处理互斥。
返回列表