嵌入式实时操作系统 μC/OSII和μC/OSIII 读书笔记

虽然之前有读过,当时对很多概念都是没有概念的,现在接触过一些rtos,现在有时间再读一次这本书。本书只针对本书的部分部分笔记。

嵌入式实时操作系统 μC/OSII(第二版)邵贝贝
野火uC/OS-III 内核实现与应用开发实战指南

在这里插入图片描述
在这里插入图片描述
这就是目录,最上面的是II,下面的是III。
我最喜欢的是图,图非常形象,也比文字好理解的多,操作系统很多,但是结构都是类似的。

初识μC/OS

μC/OS通过了各种认证,上火星也是用的它,之前是收费的(现在开源了,晚了),干不过freertos,特点就是通过认证了,安全。

实时操作系统概念

在这里插入图片描述
分为两种:软实时和硬实时

  • 硬实时任务(Hard Real-time Task):系统必须满足任务对截止时间的要求,否则可能出现难以预测的后果。
    如:工业和武器控制系统中
  • 软实时任务(Soft Real-time Task):偶尔错过了任务的截止时间,对系统产生的影响也不会太大。 如:信息查询和多媒体系统中

内核结构

临界段

临界段代码,也称作临界域,是一段不可分割的代码。uCOS 中包含了很多临界段代
码。如果临界段可能被中断,那么就需要关中断以保护临界段。如果临界段可能被任务级
代码打断,那么需要锁调度器保护临界段。
临界段用一句话概括就是一段在执行的时候不能被中断的代码段。在uCOS 里面,这
个临界段最常出现的就是对全局变量的操作,全局变量就好像是一个枪把子,谁都可以对
他开枪,但是我开枪的时候,你就不能开枪,否则就不知道是谁命中了靶子。可能有人会
说我可以在子弹上面做个标记,我说你能不能不要瞎扯淡。
那么什么情况下临界段会被打断?一个是系统调度,还有一个就是外部中断。在
uCOS 的系统调度,最终也是产生PendSV 中断,在PendSV Handler 里面实现任务的切换,
所以还是可以归结为中断。既然这样,uCOS 对临界段的保护最终还是回到对中断的开和
关的控制。
uCOS 中定义了一个进入临界段的宏和两个出临界段的宏,用户可以通过这些宏定义
进入临界段和退出临界段。

  • OS_CRITICAL_ENTER()
  • OS_CRITICAL_EXIT()
  • OS_CRITICAL_EXIT_NO_SCHED()
    此外还有一个开中断但是锁定调度器的宏定义OS_CRITICAL_ENTER_CPU_EXIT()。

野火 68页

任务控制块(OS_TCB)

在这里插入图片描述

就绪表

正常使用是用不到的,这个属于源码的实现,这个是为了省内存快速找到任务节的一种方法,当然我还是觉得rtt 那种实现更好一点。

参考文章

在这里插入图片描述

任务调度

任务调度的时间是常数,和任务多少没关系,可以主动调度。

给调度器上锁和开锁

和上面临界区差不多,上锁是系统不调度了(前提是系统之前已经运行),任务A里面给调度上锁,其他的任务和IPC通信(线程之间通信)都停止,中断依然有效,只是不响应而已。

μC/OS中的中断

在这里插入图片描述
ARM Cortex-M 系列内核的中断是由硬件管理的,而uCOS 是软件,它并不接管由硬
件管理的相关中断(接管简单来说就是,所有的中断都由RTOS 的软件管理,硬件来了中
断时,由软件决定是否响应,可以挂起中断,延迟响应或者不响应)

时钟节拍

实现时间延迟和确认延迟的。比如,任务A事情做完了,需要延迟20毫秒再做一次,这里的时间就需要一个时基(时间基准)当作标准。现在是2021年,频率都达到非常的地步,也不是说时基这个时间设置越小越好,如果非常小,会经常调度,每次调度都是花费一些时间,但是也不能设置太大,比如设置20s,对汽车安全气囊,飞机弹射,这个时间该发生的都已经结束了。一般都设置为1ms,性能更强的会设置短些而已。

μC/OS 初始化

在这里插入图片描述
记住这个模板就行了,其实os都是这样的。
在这里插入图片描述
这是伪代码!!!这只是其中一种方式,还有一种方式是,先创建一个任务,在这个任务中创建其他的任务。
在这里插入图片描述
在这个系统初始化中,我们主要看两个地方,一个是空闲任务的初始化,一个是时钟节拍任务的初始化,这两个任务是必须存在的任务,否则系统无法正常运行。

任务管理

任务的运行打断之类的,优先级高的会打断优先级低的,同优先级使能时间片会相互运行。
uCOS 内核中采用两种方法寻找最高优先级的任务,第一种是通用的方法,因为uCOS
防止CPU 平台不支持前导零指令,就采用C 语言模仿前导零指令的效果实现了快速查找到
最高优先级任务的方法。而第二种方法则是特殊方法,利用硬件计算前导零指令CLZ,这
样子一次就能知道哪一个优先级任务能够运行,这种调度算法比普通方法更快捷,但受限
于平台(在STM32 中我们就使用这种方法)。
这种方法(rtt也是类似)从数中找到第一个1这个代表任务的优先级等,这里只是单纯的算法。
在这里插入图片描述
uCOS 的任务状态通常分为以下几种:

  • 就绪(OS_TASK_STATE_RDY):该任务在就绪列表中,就绪的任务已经具备
    执行的能力,只等待调度器进行调度,新创建的任务会初始化为就绪态。
  • 延时(OS_TASK_STATE_DLY):该任务处于延时调度状态。
  • 等待(OS_TASK_STATE_PEND):任务调用OSQPend()、OSSemPend()这类等
    待函数,系统就会设置一个超时时间让该任务处于等待状态,如果超时时间设置
    为0,任务的状态,无限期等下去,直到事件发生。如果超时时间为N(N>0),在
    N 个时间内任务等待的事件或信号都没发生,就退出等待状态转为就绪状态。
  • 运行(Running):该状态表明任务正在执行,此时它占用处理器,UCOS 调度器
    选择运行的永远是处于最高优先级的就绪态任务,当任务被运行的一刻,它的任
    务状态就变成了运行态,其实运行态的任务也是处于就绪列表中的。
  • 挂起(OS_TASK_STATE_SUSPENDED):任务通过调用OSTaskSuspend()函数
    能够挂起自己或其他任务,调用OSTaskResume()是使被挂起的任务回复运行的唯
    一的方法。挂起一任务意味着该任务再被恢复运行以前不能够取得CPU 的使用权,
    类似强行暂停一个任务。
  • 延时+挂起(OS_TASK_STATE_DLY_SUSPENDED):任务先产生一个延时,延
    时没结束的时候被其他任务挂起,挂起的效果叠加,当且仅当延时结束并且挂起
    被恢复了,该任务才能够再次运行。
  • 等待+挂起(OS_TASK_STATE_PEND_SUSPENDED):任务先等待一个事件或
    信号的发生(无限期等待),还没等待到就被其他任务挂起,挂起的效果叠加,
    当且仅当任务等待到事件或信号并且挂起被恢复了,该任务才能够再次运行。
  • 超时等待+挂起(OS_TASK_STATE_PEND_TIMEOUT_SUSPENDED):任务在
    指定时间内等待事件或信号的产生,但是任务已经被其他任务挂起。
  • 删除(OS_TASK_STATE_DEL):任务被删除后的状态,任务被删除后将不再运
    行,除非重新创建任务。

时间管理

就是延时函数的实现和时间的获取,举例,任务A做完了需要延迟一会,这时就是延迟函数,这段时间把cpu释放出来给其他任务使用。时间的获取,就像一天24小时,现在几点了,获取时间。

事件控制块

事件控制块不是事件标志,可能会比较疑惑,这个控制块是用来控制 线程之间通信 的,比如 消息队列,邮箱,事件标志,互斥量之类的,管理方式也是类似任务的优先级。

信号量

重头戏终于到了,从这里到下面都是 IPC通信,线程之间通信用的。
在这里插入图片描述
二值信号量则是只有两个位置,比方,停车位,别人(任务获取信号量)你就不能停车,只能等待。和互斥量也相似。

互斥量管理

互斥量又称互斥锁,一般用于共享资源的互斥排他性访问保护。

互斥量在任意时刻处于且仅会处于解锁或锁定状态,当一个任务获取到一把锁后(互斥量锁定),其他任务再尝试获得这把锁时会失败或进入阻塞状态,当该任务释放持有的锁时(互斥量解锁),会唤醒一个正阻塞等待此互斥量的任务,被唤醒的任务将会获取这把锁。
在多任务运行环境中,有些共享资源不具有多线程可重入性,对于这类不希望被多任务同时访问的资源(临界资源),可以采用互斥量来进行保护。

互斥量又称互斥信号量(本质也是一种信号量,不具备传递数据功能),是一种特殊的二值信号量,它和信号量不同的是,它支持互斥量所有权、递归访问以及防止优先级翻转的特性,用于实现对临界资源的独占式处理。任意时刻互斥量的状态只有两种,开锁或闭锁。当互斥量被任务持有时,该互斥量处于闭锁状态,这个任务获得互斥量的所有权。当该任务释放这个互斥量时,该互斥量处于开锁状态,任务失去该互斥量的所有权。当一个任务持有互斥量时,其他任务将不能再对该互斥量进行开锁或持有。持有该互斥量的任务也能够再次获得这个锁而不被挂起,这就是递归访问,也就是递归互斥量的特性,这个特性与一般的信号量有很大的不同,在信号量中,由于已经不存在可用的信号量,任务递归获取信号量时会发生主动挂起任务最终形成死锁。
如果想要用于实现同步(任务之间或者任务与中断之间),二值信号量或许是更好的选择,虽然互斥量也可以用于任务与任务间同步,但是互斥量更多的是用于保护资源的互锁。
用于互锁的互斥量可以充当保护资源的令牌,当一个任务希望访问某个资源时,它必须先获取令牌。当任务使用完资源后,必须还回令牌,以便其它任务可以访问该资源。是不是很熟悉,在我们的二值信号量里面也是一样的,用于保护临界资源,保证多任务的访问井然有序。当任务获取到信号量的时候才能开始使用被保护的资源,使用完就释放信号量,下一个任务才能获取到信号量从而可用使用被保护的资源。但是信号量会导致的另一个潜在问题,那就是任务优先级翻转(具体会在下文讲解)。而uCOS 提供的互斥量可以
通过优先级继承算法,可以降低优先级翻转问题产生的影响,所以,用于临界资源的保护
一般建议使用互斥量。
在uCOS 操作系统中为了降低优先级翻转问题利用了优先级继承算法。优先级继承算
法是指,暂时提高某个占有某种资源的低优先级任务的优先级,使之与在所有等待该资源
的任务中优先级最高那个任务的优先级相等,而当这个低优先级任务执行完毕释放该资源
时,优先级重新回到初始设定值。因此,继承优先级的任务避免了系统资源被任何中间优
先级的任务抢占。
互斥量与二值信号量最大的不同是:互斥量具有优先级继承机制,而信号量没有。
在这里插入图片描述
在这过程中,H 任务的等待时间过长,这对系统来说这是很致命的,所以这种情况不
允许出现,而互斥量就是用来降低优先级翻转的产生的危害。

在这里插入图片描述
但是使用互斥量的时候一定需要注意:在获得互斥量后,请尽快释放互斥量,同时需要注意的是在任务持有互斥量的这段时间,不得更改任务的优先级。UCOS 的优先级继承
机制不能解决优先级反转,只能将这种情况的影响降低到最小,硬实时系统在一开始设计
时就要避免优先级反转发生。

事件标志组管理

事件集也是线程间同步的机制之一,一个事件集可以包含多个事件,利用事件集可以完成一对多,多对多的线程间同步。下面以坐公交为例说明事件,在公交站等公交时可能有以下几种情况:

①P1 坐公交去某地,只有一种公交可以到达目的地,等到此公交即可出发。

②P1 坐公交去某地,有 3 种公交都可以到达目的地,等到其中任意一辆即可出发。

③P1 约另一人 P2 一起去某地,则 P1 必须要等到 “同伴 P2 到达公交站” 与“公交到达公交站”两个条件都满足后,才能出发。

这里,可以将 P1 去某地视为线程,将 “公交到达公交站”、“同伴 P2 到达公交站” 视为事件的发生,情况①是特定事件唤醒线程;情况②是任意单个事件唤醒线程;情况③是多个事件同时发生才唤醒线程。
时间组一般可以是8位、16位、32位、64位。(比如:8位 8 bit = 1 byte 很节省空间)。
在这里插入图片描述
可以把事件定义,这里只占1bit

消息邮箱管理

邮箱服务是实时操作系统中一种典型的线程间通信方法。举一个简单的例子,有两个线程,线程 1 检测按键状态并发送,线程 2 读取按键状态并根据按键的状态相应地改变 LED 的亮灭。这里就可以使用邮箱的方式进行通信,线程 1 将按键的状态作为邮件发送到邮箱,线程 2 在邮箱中读取邮件获得按键状态并对 LED 执行亮灭操作。
这里的线程 1 也可以扩展为多个线程。例如,共有三个线程,线程 1 检测并发送按键状态,线程 2 检测并发送 ADC 采样信息,线程 3 则根据接收的信息类型不同,执行不同的操作。
操作系统的邮箱用于线程间通信,特点是开销比较低,效率较高。邮箱中的每一封邮件只能容纳固定的 4 字节内容(针对 32 位处理系统,指针的大小即为 4 个字节,所以一封邮件恰好能够容纳一个指针)。典型的邮箱也称作交换消息,如下图所示,线程或中断服务例程把一封 4 字节长度的邮件发送到邮箱中,而一个或多个线程可以从邮箱中接收这些邮件并进行处理。
在这里插入图片描述
切记:邮箱只是传输地址,比如数组Index[10],将Index数组首地址传出,接收的地方就可以凭借这个首地址获取到整个数组。前提必须要自己定义好结构。

消息队列

消息队列是另一种常用的线程间通讯方式,是邮箱的扩展。可以应用在多种场合:线程间的消息交换、使用串口接收不定长数据等。
消息队列能够接收来自线程或中断服务例程中不固定长度的消息,并把消息缓存在自己的内存空间中。其他线程也能够从消息队列中读取相应的消息,而当消息队列是空的时候,可以挂起读取线程。当有新的消息到达时,挂起的线程将被唤醒以接收并处理消息。消息队列是一种异步的通信方式。
如下图所示,线程或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个线程也可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常将先进入消息队列的消息先传给线程,也就是说,线程先得到的是最先进入消息队列的消息,即先进先出原则 (FIFO)。
在这里插入图片描述
消息队列是将想传输的数据(地址也算数据一种)复制到预先申请好的内存里,接收方将内存取出。

软件定时器

软件定时器是以系统时间片为时基再封装而成的,对于硬件了解的同学会知道,单片机内部有几个硬件定时器,但是数量有限,并且使用也是麻烦。
定时器提供两类定时器机制:第一类是单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动停止。第二类是周期触发定时器,这类定时器会周期性的触发定时器事件,直到用户手动的停止,否则将永远持续执行下去。
另外,根据超时函数执行时所处的上下文环境,定时器可以分为 HARD_TIMER 模式与 SOFT_TIMER 模式
算法用的是哈希算法,其他os有用跳表的,不过这些都不重要,只要会用就可以了。

任务信号量

uCOS 提供任务信号量这个功能,每个任务都有一个32 位(用户可以自定义位宽,我
们使用32 位的CPU,此处就是32 位)的信号量值SemCtr,这个信号量值是在任务控制块
中包含的,是任务独有的一个信号量通知值,在大多数情况下,任务信号量可以替代内核
对象的二值信号量、计数信号量等。
注:本章主要讲解任务信号量,而非内核对象信号量,如非特别说明,本章中的信号量都指的是内核对象信号量。前面所讲的信号量是单独的内核对象,是独立于任务存在的;本章要讲述的任务信号量是任务特有的属性,紧紧依赖于一个特定任务。
相对于前面使用uCOS 内核通信的资源,必须创建二进制信号量、计数信号量等情况,
使用任务信号量显然更灵活。因为使用任务信号量比通过内核对象信号量通信方式解除阻
塞的任务的速度要快,并且更加节省RAM 内存空间,任务信号量的使用无需单独创建信
号量。
通过对任务信号量的合理使用,可以在一定场合下替代uCOS 的信号量,用户只需向
任务内部的信号量发送一个信号而不用通过外部的信号量进行发送,这样子处理就会很方
便并且更加高效,当然,凡事都有利弊,不然的话uCOS 还要内核的IPC 通信机制干嘛,
任务信号量虽然处理更快,RAM 开销更小,但也有限制:只能有一个任务接收任务信号
量,因为必须指定接收信号量的任务,才能正确发送信号量;而内核对象的信号量则没有
这个限制,用户在释放信号量,可以采用广播的方式,让所有等待信号量的任务都获取到
信号量。
在实际任务间的通信中,一个或多个任务发送一个信号量给另一个任务是非常常见的,而一个任务给多个任务发送信号量的情况相对比较少。这种情况就很适合采用任务信号量进行传递信号,如果任务信号量可以满足设计需求,那么尽量不要使用普通信号量,这样子设计的系统会更加高效。
任务信号量的运作机制与普通信号量一样,没什么差别。

任务消息队列

任务消息队列跟任务信号量一样,均隶属于某一个特定任务,不需单独创建,任务在则任务消息队列在,只有该任务才可以获取(接收)这个任务消息队列的消息,其他任务只能给这个任务消息队列发送消息,却不能获取。任务消息队列与前面讲解的(普通)消息队列极其相似,只是任务消息队列已隶属于一个特定任务,所以它不具有等待列表,在操作的过程中省去了等待任务插入和移除列表的动作,所以工作原理相对更简单一点,效率也比较高一些。
注意:本书所提的“消息队列”,若无特别说明,均指前面的(普通)消息队列(属于内核对象),而非任务消息队列。
通过对任务消息队列的合理使用,可以在一定场合下替代uCOS 的消息队列,用户只需向任务内部的消息队列发送一个消息而不用通过外部的消息队列进行发送,这样子处理就会很方便并且更加高效,当然,凡事都有利弊,任务消息队列虽然处理更快,RAM 开销更小,但也有限制:只能指定消息发送的对象,有且只有一个任务接收消息;而内核对象的消息队列则没有这个限制,用户在发送消息的时候,可以采用广播消息的方式,让所有等待该消息的任务都获取到消息。
在实际任务间的通信中,一个或多个任务发送一个消息给另一个任务是非常常见的,而一个任务给多个任务发送消息的情况相对比较少,前者就很适合采用任务消息队列进行传递消息,如果任务消息队列可以满足设计需求,那么尽量不要使用普通消息队列,这样子设计的系统会更加高效。

内存管理

在计算系统中,变量、中间数据一般存放在系统存储空间中,只有在实际使用时才将它们从存储空间调入到中央处理器内部进行运算。通常存储空间可以分为两种:内部存储空间和外部存储空间。内部存储空间访问速度比较快,能够按照变量地址随机地访问,也就是我们通常所说的RAM(随机存储器),或电脑的内存;而外部存储空间内所保存的内
容相对来说比较固定,即使掉电后数据也不会丢失,可以把它理解为电脑的硬盘。在这一
章中我们主要讨论内部存储空间(RAM)的管理——内存管理。
在嵌入式系统设计中,内存分配应该是根据所设计系统的特点来决定选择使用动态内存分配还是静态内存分配算法,一些可靠性要求非常高的系统应选择使用静态的,而普通的业务系统可以使用动态来提高内存使用效率。静态可以保证设备的可靠性但是需要考虑内存上限,内存使用效率低,而动态则是相反。
uCOS 的内存管理是采用内存池的方式进行管理,也就是创建一个内存池,静态划分
一大块连续空间作为内存管理的空间,里面划分为很多个内存块,我们在使用的时候就从
这个内存池中获取一个内存块,使用完毕的时候用户可以将其放回内存池中,这样子就不
会导致内存碎片的产生。
uCOS 内存管理模块管理用于系统中内存资源,它是操作系统的核心模块之一,主要
包括内存池的创建、分配以及释放。
很多人会有疑问,什么不直接使用C 标准库中的内存管理函数呢?在电脑中我们可以
用malloc()和free()这两个函数动态的分配内存和释放内存。但是,在嵌入式实时操作系统
中,调用malloc()和free()却是危险的。
由于实时系统中对时间的要求非常严格,内存管理往往要比通用操作系统要求苛刻得多:

1)分配内存的时间必须是确定的。一般内存管理算法是根据需要存储的数据的长度在内存中去寻找一个与这段数据相适应的空闲内存块,然后将数据存储在里面。而寻找这样一个空闲内存块所耗费的时间是不确定的,因此对于实时系统来说,这就是不可接受的,实时系统必须要保证内存块的分配过程在可预测的确定时间内完成,否则实时任务对外部事件的响应也将变得不可确定。

2)随着内存不断被分配和释放,整个内存区域会产生越来越多的碎片(因为在使用过程中,申请了一些内存,其中一些释放了,导致内存空间中存在一些小的内存块,它们地址不连续,不能够作为一整块的大内存分配出去),系统中还有足够的空闲内存,但因为它们地址并非连续,不能组成一块连续的完整内存块,会使得程序不能申请到大的内存。对于通用系统而言,这种不恰当的内存分配算法可以通过重新启动系统来解决 (每个月或者数个月进行一次),但是对于那些需要常年不间断地工作于野外的嵌入式系统来说,就变得让人无法接受了。

3)嵌入式系统的资源环境也是不尽相同,有些系统的资源比较紧张,只有数十 KB 的内存可供分配,而有些系统则存在数 MB 的内存,如何为这些不同的系统,选择适合它们的高效率的内存分配算法,就将变得复杂化。

RT-Thread 操作系统在内存管理上,根据上层应用及系统资源的不同,有针对性地提供了不同的内存分配管理算法。总体上可分为两类:内存堆管理与内存池管理,而内存堆管理又根据具体内存设备划分为三种情况:

第一种是针对小内存块的分配管理(小内存管理算法);
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

第二种是针对大内存块的分配管理(slab 管理算法);
在这里插入图片描述

第三种是针对多内存堆的分配情况(memheap 管理算法)
在这里插入图片描述

在这里插入图片描述
rtt 是把剩余的RAM全部当做内存,算法虽然有多种,对用户来说只需要关心如何使用。
这里提出了一个新的概念内存池

内存池

内存堆管理器可以分配任意大小的内存块,非常灵活和方便。但其也存在明显的缺点:一是分配效率不高,在每次分配时,都要空闲内存块查找;二是容易产生内存碎片。为了提高内存分配的效率,并且避免内存碎片,RT-Thread 提供了另外一种内存管理方法:内存池(Memory Pool)。

内存池是一种内存分配方式,用于分配大量大小相同的小内存块,它可以极大地加快内存分配与释放的速度,且能尽量避免内存碎片化。此外,RT-Thread 的内存池支持线程挂起功能,当内存池中无空闲内存块时,申请线程会被挂起,直到内存池中有新的可用内存块,再将挂起的申请线程唤醒。

内存池的线程挂起功能非常适合需要通过内存资源进行同步的场景,例如播放音乐时,播放器线程会对音乐文件进行解码,然后发送到声卡驱动,从而驱动硬件播放音乐。
在这里插入图片描述
如上图所示,当播放器线程需要解码数据时,就会向内存池请求内存块,如果内存块已经用完,线程将被挂起,否则它将获得内存块以放置解码的数据;

而后播放器线程把包含解码数据的内存块写入到声卡抽象设备中 (线程会立刻返回,继续解码出更多的数据);

当声卡设备写入完成后,将调用播放器线程设置的回调函数,释放写入的内存块,如果在此之前,播放器线程因为把内存池里的内存块都用完而被挂起的话,那么这时它将被将唤醒,并继续进行解码。
注意:这里的内存池函数一般和上面的内存函数分开的,因为上面内存是给任务和整体用的,这里的内存池是给专项的常用的专用的
到此,本文接近尾声。移植之类的这里不啰嗦了。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值