从 FreeRTOS 到 Zephyr
前言
- 当我们进入嵌入式这个领域的时候,往往首先接触的都是单片机编程,单片机编程又首选 51 单片机来入门。大家说的单片机编程通常都是指裸机编程,即不加入任何 RTOS(Real Time Operating System 实时操作系统)的程序。在裸机系统中,所有的操作都是在一个main 函数的无限的大循环里轮询处理,更进进一步就是使用中断响应外部事件,组成一个前后台系统,这样也足以满足很多中小型的电子产品的需求。随着产品的功能越来越多,单纯的裸机编程已经不能够完美地解决问题,各种功能耦合在一起反而会使编程变得更加复杂,还会影响系统响应实时性,引入 RTOS 实现多任务管理,可以有效解决这些问题,这是使用RTOS 的最大优势。
- 目前国际上使用 RTOS 很多,常用的 RTOS 有国外的 FreeRTOS、μC/OS 和国内的 RT-thread、Huawei LiteOS 和 AliOS-Things 等,目前来讲应该是以国外开源且免费的 FreeRTOS的市场占有率最高。本篇文章从 FreeRTOS 入手,总结一些 RTOS 的通用机制、原理与应用,过渡到 Zephyr RTOS。
什么是RTOS?
RTOS 全称 Real Time Operating System,实时操作系统,相比较于传统操作系统,(windows 、Linux、安卓、Mac OS),RTOS 弱爆了,它管理的软硬件资源都非常少,例如内存都要精确到字节来使用,平时都是能省一个是一个,能提供的功能也不多(相较于OS);但是它的反应时间非常快,通常是在us(微秒)级别,这也是 RTOS 中 RT 的由来,在需要实时响应的系统中,RTOS可以保证系统及时响应。而且 RTOS 的体积通常非常小,通过裁剪可以达到 kB 级别,所以在资源紧张的场景中(嵌入式场景,单片机的flash和内存都以kB计),RTOS也可以很好的跑起来,典型的短小精悍。
RTOS 到底有什么?
- RTOS 的核心部分,应该说是调度器和同步通信机制,几乎所有 RTOS 内核都会提供这部分的支持。
- RTOS中,负责任务的现场保存、恢复,以及任务切换的模块,称为调度器。调度器决定在任何时间点允许哪个任务执行,此任务称为当前任务。而同步机制,可以让一个单核的MCU更好地“同时”做多件事情,也就是所谓的“并发”执行。
- 在实际应用中,往往某些任务之间会存在依赖关系,例如任务A需要依赖任务B的一些状态,如只有B走到了状态S1,A任务才能继续运行。这时候就需要一些同步机制,让任务B执行到状态S1的时候,发送一个“通知”给任务A,然后MCU就可以先把B的现场保存起来,然后继续去运行任务A。这个发送通知的功能,就是这些同步机制。
上述两个功能,调度器是 RTOS 的核心功能。RTOS 没有了同步机制,它还能称作 RTOS;而如果它没有调度器,那它就不是一个 RTOS。
早期的 RTOS 一般只提供调度器和同步机制,如 uC/OS-II,而现在随着 MCU 的资源日渐丰富,性能越来越高,RTOS 所支持的功能和组件也越来越多,例如:
- 内存管理
- 外设驱动
- 网络协议栈
- 文件系统
- etc…
为什么要用RTOS?
上一节简单介绍了 RTOS 的功能,可以知道,在嵌入式 MCU 场景,有 RTOS 会比无 RTOS 能做更多事情。但是,从另一方面看,多了 RTOS,MCU 就需要做真实应用之外的事情。例如保存当前任务现场,恢复另一任务的现场。所以在应用比较简单的场景下,其实用 RTOS 并不会给工作效率带来提升。
那我们什么时候需要用 RTOS 呢?
- 逻辑业务可以划分为几个任务,并且耦合度较低,可以并行执行
- 应用本身就存在需要并行执行的逻辑
具体到我们的机器人业务来讲,需要 RTOS 的原因
- 机器人有很多传感器和控制器业务,对于嵌入式,这些传感器和控制器业务相互之间往往比较独立,使用多线程一方面可以简化业务逻辑设计,一方面可以进一步解耦,并且大量传感器和控制器对系统实时响应都有要求,很适合用上 RTOS。
FreeRTOS framework
- Hardware(HW): MCU上的各种外设,flash,sram,timer,gpio,etc.
- Scheduler: 调度器
- Task:任务管理和控制,以及自带的软件定时器,和idle task, 在 Zephtyr 里就是线程 thread
- IPC:所有同步机制, Zephyr 有类似的同步机制, Zephyr 里 IPC 指的是处理器间的通信
- Memory Manager(后简称MM):内存管理
- Application(APP):用户写的应用
箭头含义:
- scheduler需要一个硬件定时来驱动
- 任务由调度器来调度
- IPC机制中的等待队列,需要调度器来管理
- 需要分配MCU上的一段内存给MM,MM所管理的就是这段内存
- APP需要调用 RTOS 的接口实现自己的逻辑
- FreeRTOS 没有device驱动框架,APP需要直接控制HW; Zephyr 有自己的设备驱动框架
基于上述框架,用一段话简单概括包含 RTOS 固件的工作情景:
- 应用中,MCU的硬件定时器中断会定时的调用 Scheduler,Scheduler 负责将就绪的,可以投入运行的任务,切换为运行,同时也会把之前运行的任务现场保存起来,使之变为等待运行的状态。
- 通常 RTOS 本身自带两个任务:软件定时器任务(用户可以选择是否存在)和 idle 任务(必须存在),idle 任务回收 delete 任务之后需要释放的资源。
- APP通过任务管理接口创建自己的任务,任务本身可以通过内存管理接口申请和释放内存,任务之间可以通过同步机制安全的协作;在无法满足任务请求时,同步机制通过调度器将任务挂起(类似暂停),同时调度器也会让其他任务进入运行态。
下面详细介绍 RTOS 各个通用组件:
调度器
上文了解到,调度器要做的事情就是保存任务现场和恢复任务现场,这样才能实现它的目标:调度任务。所以还得从以下3个要素来了解调度器:
- 什么是任务现场
- 如何保存和恢复现场,即如何切换任务
- 什么时候需要保存/恢复现场
任务现场
嵌入式场景下,我们通常用C/C++来开发我们的APP的,对于C/C++来说,“现场”就是具体到某一时刻,当时的局部变量,和代码地址。如果在那一刻暂停了 CPU,只要恢复时,这些现场没有改变,那 CPU 就可以继续执行刚才的任务。
-
因为每个任务的栈都是相互独立的保存在内存中,所以只要保存了当前的栈指针值,就可以相当于保存了任务栈现场了。
实际上在将 C/C++ 代码丢到 MCU 上运行前,都需要先编译为 MCU 核心体系结构相关的二进制文件,然后才能烧录到 MCU 上运行,而这个时候的“任务现场”,已经不像 C/C++ 所看到的那么简单了。并且,不同的体系结构所包含的“任务现场”信息,往往各不相同,这里以 Cortex-M3/M4系列相关的“任务现场”信息为例。 -
Cortex-M3/M4 核心的执行现场是保存在CPU通用寄存器中,以及状态寄存器中。如果开启了 FPU 单元(Cortex-M4F内核),还会涉及到浮点寄存器组。
未开启 FPU 的场景下的现场:
通用寄存器组
因为线程中使用的 PSP,所以我们恢复现场的时候,恢复的是 PSP
MSP/PSP
- MSP 和 PSP 的含义是 Main_Stack_Pointer 和 Process_Stack_Pointer,在逻辑地址上他们都是 R13,这意味着同一个逻辑地址,实际上有两个物理寄存器,一个为 MSP,一个为 PSP,在不同的工作模式调用不同的物理寄存器
- 这样设计是为了在进行模式转换的时候,减少堆栈的保存工作,也就是说可以为不同权限的工作模式设置不同的堆栈而不用考虑它有一些额外的执行消耗。
- 典型的 OS 环境中,MSP 和 PSP 的用法如下:
- MSP 用于 OS 内核和异常处理。
- PSP 用于应用任务。
- 也就是说中断多级嵌套,寄存器压栈到系统堆栈,不是任务堆栈,这样不用考虑中断嵌套对任务堆栈的影响,节省RAM,特别是像Cortex-M4带FPU,需要额外增加136 bytes
启用 FPU 时,任务现场还包括以下浮点寄存器组
因为每个任务的栈都是相互独立的保存在内存中,所以只要保存了当前的栈指针值,就可以相当于保存了任务栈现场了。
因为 Cortex-M3/M4 还具有中断时自动保存现场,退出中断时自动恢复现场的机制,从任务栈中,恢复数据到寄存器列表的寄存器中,因为 r0 - r3, r12, LR, PC, xPSR 的内容是退出中断时,CPU自动从 SP 指向的栈中恢复,无需手动恢复。所以实际上我们只需要手动保存一部分寄存器就可以了。
任务切换
从上述场景中,我们可以知道,任务切换,其实就是将当前任务的执行现场保存起来。然后将下一个将要运行的任务执行现场恢复到CPU的以上寄存器中。这样就完成了任务切换,CPU继续运行新的任务,而保存起来的任务则在下一次合适的时机再切换回来,继续运行。
运行时机
实时操作系统之所以实时的原因之一,就是确保高优先级的就绪态任务尽快投入运行。
就绪态其实是任务的一种状态,RTOS在调度多任务时,也需要记录任务的状态,每个任务都有一个地方记录自己的状态。
RTOS中的任务有以下几个状态,它们的转换关系如下:
这里我们重点关注就绪态和运行态,运行态就是当前正在运行的任务的状态,就绪态就是等待进入运行前的状态。
为了确保高优先级的任务尽快投入运行,需要通过两件事来确保:
- 实时检测是否有比当前任务优先级更高的任务进入就绪态
- 尽快将就绪态的最高优先级的任务切换为运行态
以上两件事也是调度器要做的事情,所以调度器的运行时机对系统的实时影响非常大。RTOS中,任务切换可能发生在以下几处: - 主动执行一个系统任务切换,置位相关寄存器位产生一个 PendSV 中断
- 在 Arm Cortex-M 中,除了上电启动的第一个任务外,后续任务的切换最终都是在 PendSV 中断服务函数中完成的,所以在需要做任务切换时,只需触发 PendSV 即可
- 任务从运行状态转换为挂起或等待状态时,例如等待获取信号量或调用了系统延时函数
- 任务从非就绪状态转换为就绪状态时,例如获取到了信号量或系统延时结束
- 处理中断后返回任务上下文
调度算法
一般 RTOS 都支持抢占式调度和协作式调度,并且分为有无时间片两种情况,一个时间片周期就是系统滴答定时器的周期
- 抢占式时间片调度,高优先级抢占低优先级,每个时间片调度一次,若最高优先级任务有多个,则每个任务各占有一个时间片周期,轮流交替执行。
- 抢占式无时间片调度,高优先级抢占低优先级,若最高优先级任务有多个,则只有本次任务阻塞时才会切换执行。
- 协作式调度,当前执行任务将会一直运行,高优先级任务不会抢占低优先级任务。内核调度只会在当前执行任务进入阻塞状态的时候才会执行,选择处于就绪状态的任务中优先级最高的任务进行执行。也就是需要我们再合适的时机主动让出 CPU 使用权,让其他任务能够运行。协作线程可以实现互斥,而不需要内核对象,例如互斥体。
- 通常我们是选择这几种中的一种来使用,不过 Zephyr 支持同时使用协作和抢占,在 Zephyr 里区分了协作线程和抢占线程,协作线程优先级为负,抢占线程优先级为正,所以一旦协作线程就绪就会一直执行该线程,直到我们主动让出。官方建议使用协作线程用于设备驱动程序和其他性能关键性的工作,使用抢占式线程使时间敏感的处理优先于时间敏感的处理。
任务/线程
从系统的角度看,任务是竞争系统资源的最小运行单元。在 RTOS 中,任务可以使用或等待 CPU、使用内存空间、硬件寄存器等系统资源,并独立于其他任务运行, 任何数量的任务可以共享同一优先级,如果支持时间片轮询, 处于就绪态的多个相同优先级任务将会以时间片切换的方式共享处理器。多任务系统依赖于调度器,作为任务,不需要对调度器的活动有所了解,在任务切入切出时保存上下文环境(寄存器值、堆栈内容)是调度器主要的职责。但是为了实现这点,每个 RTOS 任务都需要有自己的栈空间。 当任务切出时,它的执行环境会被保存在该任务的栈空间中,这样当任务再次运行时,就 能从堆栈中正确的恢复上次的运行环境,任务越多,需要的堆栈空间就越大,而一个系统 能运行多少个任务,取决于系统的可用的 SRAM。
任务组成
- 任务控制块
- 任务栈
- 任务函数
任务优先级
- RTOS 任务优先级一般没有上限,优先级上限受限于内存,对于传统的就绪列表,实际就是链表类型的数组,数组的下标对应了任务的优先级,同一优先级的任务统一插入到就绪列表的同一条链表中。所以优先级越多越大,就绪列表相对应的也会变大。
- 某些架构芯片支持汇编前导0指令,可以快速查找到当前系统的最高优先级,但只支持32个优先级。因为对32位 MCU 来讲,前导0指令使用了一个 u32 变量实现这种机制,u32 变量的每一位代表一个优先级,如果当前优先级存在任务就将对应位置0,而前导0指令可以直接找到第一个 bit 为 0 的位置,通过这个也即是找到了最高优先级的位置。
任务栈
- 栈我们都知道,平时我们用的函数变量,形参用的都是栈内存,在裸机里,栈是单片机 RAM 里面一段连续的内存空间,栈的大小一般在启动文件或者链接脚本里面指定,最后由 C 库函数_main 进行初始化
- 多任务系统中,每个任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于 RAM 中
- 对于ARM Cortex-M 系列来说,线程堆栈最少为64字节(保存16个32位的寄存器),如果启用了FPU,还需要额外增加136 bytes。至于线程最多需要多少内存,要根据线程变量数、调用函数的层数等来定。
- 对于任务栈大小,通常都是先分配比较大的内存,再根据运行使用情况去做调整
线程安全
在多线程环境里,程序的设计往往都需要兼具线程安全,防止出现数据污染等意外情况。
这里总结了一些使用经验:
- 对于函数,检查函数是否需要多线程调用场景,建议优先将其设计成可重入函数,也就是函数内部全部使用局部变量
- 对于非线程安全的函数,比如说有全局变量,或者涉及操作硬件资源,需要加锁(只读的应该不用)
- 对于和中断共享的变量,可以使用临界区操作,或者互斥锁(中断的互斥锁使用非阻塞)
- 临界区操作指的是共享资源只在当前区域内被操作,处于临界段区的代码,不允许管控内的中断发生,包括任务切换(PendSV 中断和滴答定时器中断是最低优先级中断)
- 以下场景也可通过临界操作保证线程安全
- 读取或修改变量,尤其是任务间通信的全局变量
- 调用公共函数,特别是不可重入函数
- 涉及硬件寄存器的操作,特别是硬件层次操作的寄存器,如读ADC寄存器值等
- 临界段执行越短越好,否则影响系统实时性
- 假如有一个线程,处理的时间较长,又不想被其他线程打断,关中断可能影响系统的正常运行,怎么办呢?其实很简单,在OS中一般可以直接挂起调度器,系统正常运行,但是不会切换线程,当处理完再把调度器解除即可
- Zephyr 可以通过 irq_lock() 和 irq_unlock(key) 进入退出临界区
通信机制
RTOS 内核除了多任务调度之外,往往还会提供一些线程间的通信机制,这些机制通常支持超时机制,可以替代以往我们使用的全局变量、全局数组的场景。
数据的同步机制:信号量
信号量(Semaphore)是一种实现任务间通信的机制,常用于任务与任务,或任务与中断中的同步,通常又分为二值信号量和计数信号量
运作机制
- 创建信号量时,系统会为创建的信号量对象分配内存,并把可用信号量初始化为用户自定义的个数, 二值信号量的最大可用信号量个数为 1。
- 任何任务都可以从创建的二值信号量资源中获取一个二值信号量, 获取成功则返回正确,否则任务会根据用户指定的阻塞超时时间来等待其它任务/中断释放信号量。在等待这段时间,系统将任务变成阻塞态,任务将被挂到该信号量的阻塞等待列中。
- 也就是说在二值信号量无效的时候,假如此时有任务获取该信号量的话,那么任务将进入阻塞状态;假如某个时间中断/任务释放了信号量,那么,由于获取无效信号量而进入阻塞态的任务将获得信号量并且恢复为就绪态。
- 计数信号量可以用于资源管理,允许多个任务获取信号量访问共享资源,但会限制任务的最大数目。访问的任务数达到可支持的最大数目时,会阻塞其他试图获取该信号量的任务,直到有任务释放了信号量。
应用
- 二值信号量:适合任务或中断之间一对一同步,一边释放信号量,一边获取信号量,替代标志位的用法。比如某个任务使用信号量在等中断的标记的发生,在这之前任务已经进入了阻塞态,在等待着中断的发生,当在中断发生之后,释放一个信号量,也就是我们常说的标记,当它退出中断之后,操作系统会进行任务的调度,如果这个任务能够运行,系统就会把等待这个任务运行起来,这样子就大大提高了我们的效率。
- 二进制信号量适用于在数据产生的频率比较低的场合,如果数据产生的频率较高,因为信号量最多只能保存一个信号,更多产生的数据将会直接被忽略抛弃。对此,我们需要使用计数信号量。
- 计数信号量:适合任务或中断之间多对一同步,多边释放信号量,一边获取信号量,或者说可以防止错过一些处理,比如说串口接收完一帧数据后产生一个信号量通知解析线程解析数据,使用计数信号量可以防止系统解析慢于接收时错过了数据。
资源的保护机制:互斥量
多任务环境下会存在多个任务访问同一临界资源的场景,为了防止出现数据污染,通常该资源会被任务独占处理, 其他任务在资源被占用的情况下不允许对该临界资源进行访问。实现这种临界访问的机制就是互斥量。
临界资源通常是指全局变量或硬件寄存器如串口发送等对多个任务共享的一类资源,想想以下这些场景,如果没有资源保护会怎样?
- 全局变量在操作过程中被更高优先级任务抢占并处理
- 硬件资源如串口在发送过程被抢占并发送
运作机制
- 用互斥量处理不同任务对临界资源的同步访问时,任务想要获得互斥量才能进行资源访问,如果一旦有任务成功获得了互斥量,则互斥量立即变为闭锁状态,此时其他任务会因为获取不到互斥量而不能访问这个资源,任务会根据用户自定义的等待时间进行等待,直到互斥量被持有的任务释放后,其他任务才能获取互斥量从而得以访问该临界资源,此时互斥量再次上锁,如此一来就可以确保每个时刻只有一个任务正在访问这个临界资源,保证了临界资源操作的安全性。
应用
- 一般有全局变量,或者涉及操作硬件资源的地方,都要考虑使用互斥锁保护,比如在一个可能被多个线程调用的函数里,如串口的发送函数,发送数据会通过改函数写入环形数组里,在串口中断里发送出去,对于环形数组的操作就要加互斥锁,避免一个线程在写入时被另一个线程抢占同时写入,这样会导致环形数组的数据是不连续的。
信号量也可以用于资源保护,但存在优先级倒置问题 - 低优先级获取信号量和资源的操作权,被中优先级抢占,中优先级又被高优先级抢占,高优先级获取不到信号量阻塞,等中优先级处理完后继续处理低优先级并给出信号量后高优先级才能继续执行
- 互斥量与二值信号量最大的不同是:互斥量具有优先级继承机制,而信号量没有。优先级继承算法是指,暂时提高某个占有某种资源的低优先级任务的优先级,使之与在所有等待该资源的任务中优先级最高那个任务的优先级相等,而当这个低优先级任务执行完毕释放该资源时,优先级重新回到初始设定值。因此,继承优先级的任务避免了系统资源被任何中间优先级的任务抢占。也就是说,某个临界资源受到一个互斥量保护,如果这个资源正在被一个低优先级任务使用,那么此时的互斥量是闭锁状态,也代表了没有任务能申请到这个互斥量,如果此时一个高优先级任务想要对这个资源进行访问,去申请这个互斥量,那么高优先级任务会因为申请不到互斥量而进入阻塞态,那么系统会将现在持有该互斥量的任务的优先级临时提升到与高优先级任务的优先级相同,这个优先级提升的过程叫做优先级继承。这个优先级继承机制确保高优先级任务进入阻塞状态的时间尽可能短,以及将已经出现的“优先级翻转”危害降低到最小。
互斥量的使用需要注意避免出现死锁:两个任务获取对方拥有的锁,各自进入挂起列表,无法释放互斥锁
多任务同步机制:事件标志
事件是一种实现任务间通信的机制,主要用于实现多任务间的同步,但事件通信只能是事件类型的通信,无数据传输。与信号量不同的是,它可以实现一对多,多对多的同步。 即一个任务可以等待多个事件的发生:可以是任意一个事件发生时唤醒任务进行事件处理; 也可以是几个事件都发生后才唤醒任务进行事件处理。同样,也可以是多个任务同步多个事件。
运行机制
- 每个事件获取的时候,用户可以选择感兴趣的事件,并且选择读取事件信息标记,它有三个属性,分别是逻辑与,逻辑或以及是否清除标记。当任务等待事件同步时,可以通过任务感兴趣的事件位和事件信息标记来判断当前接收的事件是否满足要求,如果满足则说明任务等待到对应的事件,系统将唤醒等待的任务;否则,任务会根据用户指定的阻塞超时时间继续等待下去。
- 事件可使用于多种场合,它能够在一定程度上替代信号量,用于任务与任务间,中断与任务间的同步。一个任务或中断服务例程发送一个事件给事件对象,而后等待的任务被唤醒并对相应的事件进行处理。但是它与信号量不同的是,事件的发送操作是不可累计的, 而信号量的释放动作是可累计的。事件另外一个特性是,接收任务可等待多种事件,即多个事件对应一个任务或多个任务。同时按照任务等待的参数,可选择是“逻辑或”触发还 是“逻辑与”触发。这个特性也是信号量等所不具备的,信号量只能识别单一同步动作, 而不能同时等待多个事件的同步。 各个事件可分别发送或一起发送给事件对象,而任务可以等待多个事件,任务仅对感 兴趣的事件进行关注。当有它们感兴趣的事件发生时并且符合感兴趣的条件,任务将被唤 醒并进行后续的处理动作。
应用
- 适合系统需要多对一的同步的场景,比如同时满足5个按键按下时,任务才启动
任务或中断间的数据传递
单读单写是安全的,多读或多写通常是不安全的,需要临界
- 消息队列,允许线程和 ISR 异步发送和接收固定大小的数据项,数据项可以是指针。
- 消息缓冲区,允许线程和 ISR 添加和删除任意大小的数据项,常用于传递字节流数据。
- 邮箱,提供了超出消息队列对象功能的增强消息队列功能,邮箱允许线程同步或异步发送和接收任何大小的消息。
- 管道,允许一个线程向另一个线程发送字节流。管道可用于同步传输全部或部分数据块。
系统任务
软件定时器任务
定时器有软硬之分,硬件定时器是芯片本身提供的定时功能,精度一般都很高,而且支持中断触发,但是资源有限;实际工作中,往往有很多处理需要定周期执行,这类执行并不需要很高的精度,为了周期执行而使用硬件定时器未免太过浪费,所以 RTOS 为我们了无数量限制的软件定时器,它使用了系统的一个队列和一个任务资源,为了更好响应,软件定时器的优先级应设置为所有任 务中最高的优先级。
运作机制,以 FreeRTOS 为例
- 当用户创建并启动一个软件定时器时, FreeRTOS 会根据当前系统时间及用户设置的定时确定该定时器唤醒时间,并将该定时器控制块挂入软件定时器列表,FreeRTOS 中采用两个定时器列表维护软件定时器,pxCurrentTimerList 与 pxOverflowTimerList是列表指针,系统新创建并激活的定时器都会以超时时间升序的方式插入到 pxCurrentTimerList 列表中。系统在定时器任务中扫描 pxCurrentTimerList 中的第一个定时器,看是否已超时,若已经超时了则调用软件定时器回调函数。否则将定时器任务挂起,因为定时时间是升序插入软件定时器列表的,列表中第一个定时器的定时时间都还没到的话,那后面的定时器定时时间自然没到。
- pxOverflowTimerList 列表是在软件定时器溢出的时候使用,作用与 pxCurrentTimerList 一致。同时,FreeRTOS 的软件定时器还采用消息队列进行通信,利用“定时器命令队列” 向软件定时器任务发送一些命令,任务在接收到命令就会去处理命令对应的程序,比如启动定时器,停止定时器等。假如定时器任务处于阻塞状态,我们又需要马上再添加一个软 件定时器的话,就是采用这种消息队列命令的方式进行添加,才能唤醒处于等待状态的定时器任务,并且在任务中将新添加的软件定时器添加到软件定时器列表中,所以,在定时器启动函数中,FreeRTOS 是采用队列的方式发送一个消息给软件定时器任务,任务被唤醒从而执行接收到的命令。
- 定时器节拍随着 SysTick 的触发一直在增长(每一次硬件定时器中断来临,节拍变量会加 1),在软件定时器任务运行的时候会获取下一个要唤醒的定时器,比较当前系统时间节拍是否大于或等于下一个定时器唤醒时间,若大于则表示已经超时,定时器任务将会调用对应定时器的回调函数,否则将软件定时器任务挂起,直至下一个要唤醒的软件定时器时间到来或者接收到命令消息。
- 在Zephyr里,每次定时器到期时执行的到期函数由系统时钟中断处理程序执行。
空闲任务
空闲任务也是系统自带一个任务,其优先级一般都是最低的,当其他系统任务都进入阻塞时,保证系统至少有一个任务在运行;往往大家都会在空闲任务里设置微处理器进入低功耗模式来达到省电的目的。基于此,RTOS 一般支持 Tickless 低功耗机制。
Tickless 低功耗机制
- 大多数 RTOS 内核都是使用硬件计时器来生成周期性的滴答中断,以用于测量时间。常规硬件计时器实施的节能受限于必须定期退出然后重新进入低功耗状态来处理滴答中断。如果滴答中断的频率太高,则为每次滴答中断进入和退出低功耗状态所消耗的能量和时间,会超过除最轻节能模式之外其他任何可能的节能收益。
- 为解决此限制问题,RTOS 为低功耗应用程序提供了非滴答计时器模式。RTOS 非滴答空闲模式在空闲时间段(即不存在可执行的应用程序任务的时间段)内将停止周期性的滴答中断,然后在重新启动滴答中断时对 RTOS 滴答计数值进行校正调整。通过停止滴答中断,微控制器可以维持在深度节能状态,直到中断发生,或者到了 RTOS 内核将任务转换为就绪状态的时间。
工作队列线程(zephyr),卸载 ISR 工作
工作队列是一个内核对象,它使用专用线程以先进先出的方式处理工作项。通过调用工作项指定的函数来处理每个工作项。工作队列通常由 ISR 或高优先级线程使用,以将非紧急的处理卸载到低优先级线程,因此它不会影响时间敏感的处理。
中断管理
- ARM Cortex-M 系列内核的中断是由硬件管理的,而 RTOS 是软件,它并不接管由硬件管理的相关中断(接管简单来说就是,所有的中断都由 RTOS 的软件管理,硬件来了中断时,由软件决定是否响应,可以挂起中断,延迟响应或者不响应),只支持简单的开关中断等,所以 RTOS 中的中断使用其实跟裸机差不多的,需要我们自己配置中断,并且使能中断,编写中断服务函数,在中断服务函数中使用内核通信机制,一般建议使用信号量、消息或事件标志组等标志事件的发生,将事件发布给处理任务,等退出中断后再由相关处理任务具体处理中断。当然在 Zephyr 里我们可以通过在中断注册工作队列,将中断处理延时到系统的工作队列线程里(类似 linux 中断下半部的概念)。
- Cortex-M 系列内核中,有一个 basepri 寄存器的,当 basepri 设置为某个值的时候,NVIC 不会响应比该优先级低的中断,而优先级比之更高的中断则不受影响。前面说的临界区就是利用这个中断特性,因为 SysTick 和 PendSV 都会涉及到系统调度,而系统调度的优先级要低于系统的其它硬件中断优先级,即优先相应系统中的外部硬件中断,所以 SysTick 和 PendSV 的中断优先级通常都是配置为最低,并且是可以被屏蔽的,所以我们进入临界区的时候才不会产生任务切换。受屏蔽的中断可以安全的调用 RTOS 提供的 API 函数接口,而不受屏蔽的中断也不能调用 RTOS 提供的 API 函数接口。
- 在 Zephyr 里,默认所有中断都可以通过 irq_lock() 屏蔽,对于不期望被屏蔽的中断,需要在中断初始化时(IRQ_CONNECT或IRQ_DIRECT_CONNECT)配置为零延迟中断
内存管理
…