FreeRTOS(实时操作系统)

张开发
2026/4/16 5:37:05 15 分钟阅读

分享文章

FreeRTOS(实时操作系统)
FreeRTOS是一个迷你的实时操作系统内核。作为一个轻量级的操作系统功能包括任务管理、时间管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等可基本满足较小系统的需要。RTOS 的核心目标是“确定性”和“实时性”。它保证某个任务必须在严格的时间限制内得到执行。RTOS保证任务在严格的时间限制内得到响应和执行而不是追求最高的平均吞吐量。硬实时超时即失败后果严重。例如安全气囊必须在碰撞后几毫秒内打开否则无效。软实时超时会导致性能下降但不会 catastrophic failure。例如播放音乐卡顿RTOS通过任务调度器实现它根据优先级来决定当前哪个任务可以运行CPU。高优先级的任务可以“抢占”低优先级的任务。为什么需要RTOS嵌入式项目变得复杂需要同时处理多个任务如读取传感器、控制电机、显示界面、联网通信并且任务间有严格的时间要求时用一个大循环while(1)会变得非常复杂和难以维护。RTOS 允许你将程序分成多个独立的任务让调度器来管理CPU时间使系统设计更清晰、可靠。由于RTOS需占用一定的RAM资源只有μC/OS-II、embOS、salvo、FreeRTOS等少数实时操作系统能在小RAM单片机上运行。相对μC/OS-II、embOS等商业操作系统FreeRTOS操作系统是完全免费的操作系统具有源码公开、可移植、可裁减、调度策略灵活的特点。比喻没有RTOS的while(1)循环像一个单线程序的厨师必须做完一道菜再做下一道忙不过来。μC/OS-II / embOS (抢占式)像一個严格的厨房总管。总管会命令“停你低优先级任务的汤先熬着现在立刻去处理客人催的牛排高优先级任务”这才是真正意义上的实时系统。中断的优先级永远是最高的他是属于硬件级的优先级高优先级任务优先执行当高优先级任务阻塞之后低优先级任务执行动态调整优先级 FreeRTOS允许在运行时动态调整任务的优先级。通过使用vTaskPrioritySet函数可以根据实时需求在任务执行过程中调整其优先级。pvParameters是一个void指针它的设计目的是允许你在创建任务时向该任务传递一个任意类型的参数。一个结构体最常见和有用的用法关键注意事项生命周期你传递给pvParameters的指针所指向的内存必须在任务的整个生命周期内有效。通常这意味着你应该使用全局变量静态变量动态分配的内存如pvPortMalloc分配的绝对不要传递一个局部变量的地址除非你 100% 确定该任务在局部变量离开作用域之前会使用完这个参数但这非常危险且不推荐。类型转换在任务函数内部你必须将void*类型的pvParameters强制转换回它原本的数据类型指针然后才能使用它。复用任务函数这是pvParameters最大的优势。你可以用同一个任务函数创建多个实例每个实例通过不同的参数来拥有不同的行为极大地提高了代码的复用性和可维护性。pxCreatedTask是一个输出参数。它不是一个用来输入数据的参数而是函数用来向你返回一个东西的参数。xTaskCreate()函数会创建一个任务而这个新任务需要一个唯一的“身份证””才能在未来被系统和其他任务识别和操作。pxCreatedTask就是用来接收这个新任务的“身份证”的变量地址。这个“身份证/遥控器”的正式名称就叫任务句柄。前面是什么返回类型xTaskCreate中x就是什么类型各状态详解1. 运行态 (Running)含义任务正在微处理器的核心上实际执行。此时此刻CPU 正在运行这个任务的代码。细节在单核单片机中任何时刻都只有一个任务处于运行态。调度器负责决定2. 就绪态 (Ready)含义任务已经准备就绪完全有能力运行既没有被挂起也没在等待事件但它现在没有运行因为有一个更高优先级的任务正在运行或者一个同等优先级的任务正在运行且还没有用完它的时间片。细节调度器维护着一个或多个就绪列表Ready List所有处于就绪态的任务都按优先级排在这些列表中。一旦 CPU 空闲调度器就会从就绪列表中选择最高优先级的任务来执行。3.阻塞态 (Blocked)含义任务正在等待某个事件发生在这个事件发生之前它无法继续执行。处于阻塞态的任务不会被调度器选择去运行。如何进入任务通过调用“阻塞”API 函数主动进入阻塞态。例如vTaskDelay(pdMS_TO_TICKS(100))等待一个时间事件延迟100ms。xQueueReceive()等待从一个队列中接收到数据。xSemaphoreTake()等待一个信号量可用。如何退出当任务等待的事件发生时延迟时间到、队列收到数据、信号量可用它会被自动移动到就绪态等待被调度。4. 挂起态 (Suspended)含义任务被无限期地暂停不再参与调度。它既不会被运行也不会因为任何事件而就绪除非明确地命令它恢复。它就像被“冻住”了一样。如何进入由另一个任务或中断通过调用vTaskSuspend()函数将其挂起。一个任务也可以挂起自己。如何退出只能由另一个任务或中断通过调用vTaskResume()或xTaskResumeFromISR()函数来将其解救出来使其恢复到就绪态。FreeRTOS队列和信号量一、队列队列是 FreeRTOS 中最主要的任务间通信方式。关键特性在于它是先进先出的1. 工作原理数据传递传递的是数据的拷贝而不是原始数据的指针除非你拷贝的就是指针。这意味着生产者任务和消费者任务操作的是各自的数据副本互不干扰非常安全。队列传输数据时有两种方法1、直接拷贝数据2、拷贝数据的地址然后根据地址读取数据。 第二种方法适合传输大数据比如一个大数组 或者一个结构体变量。阻塞机制当一个任务尝试从空队列读取数据时它可以选择阻塞一段时间等待有数据到来。当一个任务尝试往满队列写入数据时它也可以选择阻塞一段时间等待有空间空出来。这种阻塞机制能高效地让出 CPU 给其他任务而不是傻等忙等待。当在中断中读写队列时如果队列空或满不会进行阻塞直接返回队列空或队列满错误因为中断要的就是快进快出。2. 核心 APIxQueueCreate(): 创建队列指定队列能容纳的项目数量和每个项目的大小。xQueueSend()/xQueueSendToBack(): 向队列尾部发送数据。xQueueSendToFront(): 向队列头部发送数据插队。xQueueReceive(): 从队列头部接收数据并移除该项目。xQueuePeek(): 从队列头部读取数据但不移除该项目。3. 典型用途生产者-消费者模型一个任务如传感器采样任务不断产生数据通过xQueueSend()发送到队列。另一个任务如数据处理任务通过xQueueReceive()从队列取数据来处理。命令队列一个任务如用户界面任务将用户的操作如“播放”、“暂停”作为命令发送到队列。另一个任务如播放控制任务从队列接收并执行这些命令。二、信号量信号量主要用于任务同步和资源管理。它不像队列那样传递具体的数据而是传递一种信号或令牌。在 FreeRTOS 中信号量是基于队列构建的是一种特殊的队列其队列项大小为 0不存储数据只用来管理计数值。1、二值信号量相当于一个令牌或钥匙只有一把。其值只能是 0 或 1。工作原理“给出”信号量xSemaphoreGive()- 将令牌放回信号量值变为 1。“获取”信号量xSemaphoreTake()- 取走令牌信号量值变为 0。如果尝试获取时令牌已被取走值为 0任务可以选择阻塞等待。典型用途同步用于任务与任务、任务与中断之间的同步。示例中断服务程序ISR收到一个按键后xSemaphoreGiveFromISR()给出信号量。一个高优先级任务一直在xSemaphoreTake()等待这个信号量一旦收到就立刻去处理按键事件。这被称为二值信号量同步模型。互斥锁保护共享资源如全局变量、外设确保同一时刻只有一个任务能访问。但请注意FreeRTOS 有专门的互斥信号量来做这件事更合适。二值信号量API2、计数信号量是二值信号量的扩展可以看作是一串多个相同的令牌。其值是一个计数器可以大于 1。工作原理“给出”信号量计数器 1。“获取”信号量计数器 -1。如果计数器为 0则任务阻塞等待令牌典型用途管理多个 identical相同的 资源示例你有 3 个可用的串口。初始化时创建一个计数值为 3 的信号量。任何任务要使用一个串口前必须先Take一个信号量获取令牌。用完后再Give回去。如果三个串口都被占用第四个任务尝试Take时就会阻塞直到有任务释放一个串口Give。这确保了资源不会被超量使用。3、互斥信号量一种特殊的二值信号量具有优先级继承机制。为什么需要它用于解决优先级反转问题。优先级反转低优先级任务持有一个共享资源的锁中优先级任务抢占了CPU导致高优先级任务虽然就绪却因为拿不到锁而无法运行仿佛优先级比中优先级任务还低。优先级继承当高优先级任务尝试获取一个已被低优先级任务占有的互斥锁时系统会临时提升低优先级任务的优先级到与高优先级任务相同让它能尽快执行完并释放锁从而让高优先级任务能尽快运行。释放锁后低优先级任务恢复其原有优先级。典型用途保护共享资源。这是保护临界区、实现线程安全访问的最佳实践。互斥信号量的API互斥信号量不能用于中断服务函数中原因如下 (1) 互斥信号量有任务优先级继承的机制但是中断不是任务没有任务优先级所以互斥信号量只能用与任务中不能用于中断服务函数。 (2) 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态4、递归互斥信号量在 FreeRTOS 中递归互斥信号量Recursive Mutex是一种特殊的互斥锁允许同一任务多次获取同一把锁而不会导致死锁适用于任务中存在嵌套访问共享资源的场景。1、核心特性同一任务可多次调用xSemaphoreTake()获取锁每次获取会使 “递归计数” 加 1。必须调用相同次数的xSemaphoreGive()释放锁直到计数归 0其他任务才能获取该锁。仍具备优先级继承机制解决优先级反转问题。2、递归互斥量的API3、应用场景三、任务间的互斥和同步任务间的互斥和同步是保障系统有序运行的核心机制用于解决多个任务对共享资源的访问冲突及任务间的协作问题。1、任务间的互斥Mutual Exclusion定义防止多个任务同时访问同一共享资源如全局变量、硬件外设等避免数据混乱或操作冲突。核心问题解决 “临界区” 竞争确保同一时间只有一个任务访问共享资源。常用实现方式互斥锁Mutex示例场景两个任务同时操作 UART 发送数据通过互斥锁确保同一时间只有一个任务使用 UART 外设。是 FreeRTOS 中专门用于互斥的内核对象具有 “所有权” 特性任务获取锁后只有该任务能释放锁其他任务需等待锁释放。支持 “优先级继承” 机制当低优先级任务持有锁时若高优先级任务请求该锁低优先级任务会临时继承高优先级避免 “优先级反转”高优先级任务因等待低优先级任务而阻塞。临界区Critical Section通过关闭 / 开启中断或任务调度强制禁止其他任务进入临界区适用于短时间的资源访问。缺点过度使用会影响系统实时性不适合长时间占用资源的场景。2、任务间的同步Synchronization定义协调多个任务的执行顺序确保任务按预期逻辑协作如 “先生产后消费”。核心问题解决任务间的依赖关系使任务在特定条件下触发或等待。常用实现方式信号量Semaphore示例场景传感器采集任务生产者将数据放入缓冲区后释放信号量数据处理任务消费者等待信号量后读取数据。二进制信号量类似 “开关”初始值为 0 或 1用于触发任务执行如任务 A 完成后释放信号量任务 B 等待信号量后启动。计数信号量初始值为 N用于限制同时访问资源的任务数量如限制 3 个任务同时使用某资源。队列Queue通过传递数据实现同步一个任务发送数据到队列另一个任务从队列接收数据接收方会阻塞直到队列有数据天然实现 “生产 - 消费” 同步。事件标志组Event Group示例场景任务 C 需等待任务 A 完成事件 A且任务 B 完成事件 B后才执行可通过事件标志组实现。多个任务可等待特定事件的组合如 “事件 A 和事件 B 都发生” 或 “事件 A 或事件 B 发生”适用于复杂条件的同步。四、FreeRTOS信号量互斥量代码举例讲解1、信号量在这段代码中的具体意义在该串口 DMA 发送流程中这条语句的作用是当任务调用uart_write()函数发送数据时会先启动 DMA 传输然后通过这条语句让当前任务进入阻塞状态等待 DMA 发送完成只有当 DMA 发送完成在中断服务程序中释放了uart_tx_done_semphr信号量该任务才会被唤醒继续执行2、互斥量代码中创建了多个uart_send_task任务优先级 5、6、7这些任务会并发调用uart_write()函数发送数据。而串口 1 的 DMA 发送硬件资源如 DMA 控制器、USART 寄存器是全局共享的如果多个任务同时操作可能导致 DMA 参数如内存地址、传输长度被多个任务交替修改最终发送的数据错乱比如 A 任务的参数被 B 任务覆盖。可能出现前一次 DMA 发送尚未完成后一次发送就强行启动导致数据帧重叠、校验错误等问题。互斥量uart_tx_busy_mux通过独占访问控制确保同一时间只有一个任务能进入uart_write()的临界区配置 DMA 并启动发送从根本上避免了并发冲突。保证 DMA 发送的原子性art_write()函数的操作流程配置 DMA 参数→启动发送→等待完成是一个不可分割的原子操作必须完整执行中间不能被其他任务打断修改参数。必须等待前一次发送完成后才能允许下一次发送。

更多文章