在嵌入式应用领域,很多场合对系统的实时性要求严格,因此操作系统的选择要基于实时系统。实时多任务操作系统(Real Time Operating System,简称RTOS)是根据操作系统的工作特性而言的,实时是指物理进程的真实时间。
1、RTOS的特点
操作系统是计算机中最重要的软件,类似管家,把不同的软件任务安排给硬件资源去执行。为保证服务质量,需要对任务进行合理安排,访问硬件也进行一定的安全维护,保证硬件资源不闲置,不争抢冲突,但允许合理的插队。
2、任务设计
嵌入式系统的设计都是从需求分析开始,在软件角度,对具体功能进行任务划分,是实时操作系统应用软件设计的关键。任务(task或thread)划分是否合理将直接影响软件设计的质量、执行效率和可扩展性。
2.1 任务的特性
“任务”的状态是动态变化的,就是有运行和等待,而且阻塞等待是必须的。多个任务宏观上是并发运行,其实对单CPU是分时复用,每个任务运行时独立的占有系统资,这也导致任务间数据传输的异步性。
好比三口之家只有一个卫生间,多人分时使用,不需要三人就配三个卫生间,每人使用时完全享有卫生间的设施,其他人有需要时,得排队或者看家庭地位强迫插队。
在对一个具体的嵌入式系统进行任务划分时,不同的角度有不同的方案,一般先考虑需求指标和硬件资源限制。
2.2 任务划分方法
系统任务必定是为了实现某些功能,这个功能是主动执行,如读取信息、控制外设,还是被动执行,如接收按键事件。
根据这个特性,任务可分为“主动型”和“被动型”两种。主动型任务与其他任务通过通信机制向该任务发出执行请求执行,被动型任务通常是一个中断服务程序(ISR) 和一个与之关联的任务。在这个基础上,也可以把多个同类型外设的检测合并到一个任务。
“关键性”是指某种功能在系统中的重要性,如果该功能异常将会产生严重后果,这种关键任务必须优先得到运行机会,而且其检测事件、消息队列不能遗漏,特殊情况下控制时间也要及时响应。这种任务得提高优先级来保证运行,而且其任务功能必须尽可能与其他功能剥离,独立成为一个任务,不能因为任务里面的其他功能阻塞或者异常,影响关键功能的实现。
“紧迫性”是指某种功能必须在规定的时间内得到运行权(及时运行),这类功能有严格的实时性要求。大多数紧迫任务是由异步事件触发的,这种紧迫任务安排在相应的中断服务中,或者优先级尽可能髙的任务。要达到“按时完成”的目的,必须使“紧迫任务”需要的执行时间尽可能短。尽可能剥离“不太紧迫”的操作,只剩下“必须立刻执行”的操作,将被剥离的“不太紧迫”的操作另外封装为一个任务。
用户应用程序中消耗机时最多的一般是各种数据处理单元,而且通常不只一个,它们分别为不同的功能服务,这些单元划分出来,分别包装成不同的任务。由于这类任务需要消耗较多机时,所以优先级必须较低。多个不同的数据处理任务时,可安排相同的优先级,采用时间片轮转方式运行。
如果若干功能由相同的事件触发,就可以将这些功能合并为一个任务,从而减少事件分发给多个任务的工作量。如检测到按键中断后要进行相应数据处理、界面显示、外设控制。在任务内部,各功能的执行顺序需要尽可能按规则设定,如果各个功能之间有因果关系,则按因果关系的顺序执行;如果各个功能之间完全独立,则按实时性要求的强弱顺序执行。
对于需要重复执行的操作,即具有周期性。这类功能组合在一起封装为一个任务,避免一个时间事件触发多个任务,省去事件分发操作与它们之间的通信。
如果若干功能按固定顺序进行流水作业,相互之间完全没有"并发性",尽量将这些功能组合为一个任务。
2.3 任务的可调度性分析
任务划分后,需要评估CPU占有率,以便确定这些任务是否可以在操作系统的调度下正常运行。简单的说就是计算在一定周期内,各任务运行的最长时间长和是否小于周期,也可以粗略确认最低优先级的任务(待机任务)是否有周期运行的机会,否则系统的正常运行可能难以保证。
类似生产流水线,如果某个环节超时,整个产线就没法持续运行。这种情况需要提高系统时钟频率或者换硬件方案,或者优化软件算法。
2.4 任务类型
任务设计是整个应用程序的基础,按任务的执行方式可以分为三类:单次执行类、周期性执行类和事件触发类。
这类任务创建后只执行一次,执行结束后即自行删除。优点一个功能运行完了,立刻销毁释放资源,但也正是这样,缺点也明显,释放不干净,或者信号量处理不当导致关联任务永久阻塞。仅适合不与其他任务进行通信(除ISR外)的孤立任务。
不过有些RTOS或者二次封装,可以提供工作队列(work queue),多个这样的功能需求,统一到一个固定的任务执行,该固定任务并不销毁,一直存在,接收其他任务的执行请求。类似共享单车,大家都是用完就还,由指定人负责维护执行。
周期性执行的任务创建一次,使用while 1永久存在,执行任务实体后休眠,再按时唤醒或者超时触发,再运行再休眠的循环操作。需要注意休眠时间和任务执行时间比值越大越好,否则时间间隔会累积误差。适用于对周期稳定性要求不高的任务,否则只能采用独立于操作系统的定时中断来触发。
这类任务实体代码的执行需要等待某种事件的发生,在相关事件发生之前,如获取队列消息或信号量阻塞中,该任务被挂起等待。相关事件发生一次,该任务实体代码就执行一次。这种任务可以与其他任务进行通信,也可被动等待时间触发,一般情况下使用最多。
2.5 任务优先级
不同RTOS任务优先级定义的范围和意义不同,比如有的是数值越小表示优先级越高,有的相反;最大数值的范围也与系统配置有关。
3、公共函数的设计
基于RTOS系统的全局公共函数,需要考虑多任务访问的冲突。好比你在家如厕,使用私有资源随心所欲;出门在外上公厕,使用共享资源就得遵守一定规则。如果某个任务调用某个公共函数时,被另一个髙优先级的任务抢占,且高优先级任务也要调用该公共函数,为防止破坏原任务的数据,确保这段代码即使在多个任务同时且多次调用,运行结果表现都是一样,就必须按一定规则限制访问,一般采用两种措施,互斥调用和可重入设计。
3.1 互斥调用
将公共函数作为一种共享资源看待,以互斥方式调用公共函数。如果公共函数比较简单,运行时间很短,则可采用先关中断( 或关调度) 再调用公共函数,调用结束后再开中断(或开调度),从而避免其他任务打扰。如果公共函数比较复杂,运行时间较长,以上方案严重影响系统的实时性,最好为这个公共函数配备一个互斥信号量,任何任务在调用这个公共函数前必须首先取得对应的互斥信号量,否则就会被挂起。
3.2 可重入设计
可重入函数允许多个任务嵌套调用,各任务的数据相互独立,互不干扰,这种方式比采用互斥调用更直接。
将公共函数设计成为可重入函数的关键是不使用全局资源(如全局变量),只能使用临时的局部变量,局部变量是调用时临时分配储存空间,所以不同的任务在不同时刻调用该函数时,同一个局部变量分配的存储空间是不重叠的,互不干扰。可重入函数调用的其他函数也必须是可重入的。
3.3 运行效率
为保证运行效率和实时性要求,函数尽量不要使用轮询等待机制,可使用任务休眠等待。
4、中断服务程序的设计
中断服务程序(ISR ) 是嵌入式应用系统获取各种事件的基本手段,ISR的设计质量直接影响到系统的实时性指标和工作效率。
4.1 中断优先级
为不同的中断服务程序安排不同的优先级,在允许中断嵌套的情况下,高优先级的中断总是能够得到及时响应。只要没有关闭中断,中断服务程序可以中断任何任务的运行,故可以将中断服务程序看成比最高优先级任务还要优先的特殊任务。
4.2 中断与关联任务的通信
中断的主要功能是响应异步事件,ISR只是触发事件,本身不做过多处理,将获取的异步事件通信给关联任务处理。
ISR 与关联任务的通信方式有两种方式,信号量和消息:当使用信号量进行通信时,ISR 只发送信号量,表示事件发生,通过信号量的同步功能触发关联任务,所有具体工作均由关联任务完成。当使用数据消息进行通信时,ISR 需要完成对异步事件的信息采集,然后使用消息邮箱(或消息队列) 将数据发送给关联任务,由关联任务完成后续数据处理工作。
哪种方式更合适,需根据实际情况而定:
触发 ISR 的亊件不包含数据:不需要对事件进行数据处理,仅仅是一个特殊标记,这种情况 ISR 使用信号量与关联任务进行通信。
触发 ISR 的事件是包含数据的低频事件:将数据采集的工作放在关联任务中完成,产生的时刻延误与采样周期相比可以忽略不计,这种情况下,ISR 使用信号量与关联任务进行通信,从而简化了ISR。
触发 ISR 的事件是包含数据的中髙频事件:数据采集的工作放在关联任务中完成,可能因为时刻延误对采样数据的质量有影响,在这种情况下,数据采集的工作应该放在 ISR 中完成,再使用消息邮箱(或消息队列)与关联任务进行通信,关联任务从消息邮箱(或消息队列)中得到消息的数据,并完成后续处理。
触发 ISR 的事件是包含数据的非周期“高频”事件:对于非周期“高频”事件,其最短事件间隔可能小于事件数据处理的耗时,如果在关联任务采集数据就可能出现数据丢失问题,在这种情况下,数据采集的工作应该放在 ISR 中完成,由 ISR 使用具有数据缓冲功能的消息队列与关联任务进行通信,关联任务从消息队列中得到消息的数据,并完成后续处理工作。
有些芯片平台的中断服务不能使用操作系统接口函数,不能直接发送消息或者信号量,需要进行特殊处理。
5、行为同步
在实时操作系统的支持下,系统的整体功能是通过各个任务( 包括 ISR )的协同运行来实现的,其中运行步骤的协调就是行为同步。
一个任务的运行过程需要和其他任务的运行配合,才能得到预定的效果,任务之间的协调关系称为“行为同步”。行为同步的结果体现为任务之间的运行按某种预定的顺序来进行。一般实时操作系统提供了各种的通信方式来适应不同场合的需求,实现任务之间的行为同步。这里只是简要描述,不同RTOS只是接口不同,其作用都一样。也可以参考《FreeRTOS及其应用,万字长文,基础入门》。
可以简单理解为一个全局变量只能为0或者1,但其实是基于系统接口实现,支持跨任务读写。二值信号量初始值为0,控制方发出同步信号(put/give),二值信号量的值为 1,被控制方在任务的同步点调用“等待一个信号量”的服务函数(get/take),如果二值信号量的值已经为 1,就将二值信号量复位( 清零) 并继续运行下去;如果二值信号量的值为 0,便使自己挂起等待控制方的信号。
如果被控制方总能够及时响应控制方发出的信号,完成相应处理任务,并在下一次信号来到之前进入等待状态,这样才能保证稳定。如果被控制方获取信号量过慢,控制方可能已经发出多个信号,导致信号事件被丢失。
把同步信息视为一个产品,控制方是产品的生产者,被控制方就是产品的消费者。当产品的“最短生产时间”比产品的“最长消费时间”还长时,产品永远是“供不应求 ”,消费者总是处于等待状态”,生产出来的产品立即被消费者取走,生产者和消费者达到完全同步,生产一个就立即消费一个。如果生产过快,消费来不及,出现产品积压,则表示任务可能需要调整优先级,或者说二值信号量不合适,需要使用计数信号量。
只要产品的平均生产时间比产品的平均消费时间长,所有的产品就都会被消费掉。但仍然有可能在某个时间段内出现临时的“产品积压”。如产品的生产时间为 20-40 秒(平均30秒),产品的消费时间为 10-30秒(平均 20 秒),总体上看是生产慢、消费快,但在某个时间段,生产速度维持在高水平状态(每20秒生产一个),而消费速度偏偏维持在低水平状态(每30秒消费一个),这时就会临时出现产品积压现象。
二值信号量不能处理“信号积压”现象,未及时响应的信号将会被遗弃,有效响应次数少于实际发出的信号次数。在这种情况下,采用计数信号是一个有效的选择,在总体上能够使控制方对被控制方进行同步控制,在特殊情况下也不失去控制,保证每次控制信号都能够得到响应,尽管响应时间偶尔会有延误。
计数信号最初始值为 0,控制方需要发出同步信号时,就调用“发出一个信号量”的服务函数,使计数信号量的值加 1,被控制方在任务的“同步点”调用“等待一个信号量”的服务函数。如果计数信号量的值不为 0,就将计数信号量减 1,并继续运行下去;如果计数信号量的值为0,便使自己挂起,等待控制方的信号。
计数信号量适用于被控制方不能保证在下一次信号到来之前处理完本次控制方发出的信号,但总体上可以响应所有信号。
需要将两个以上的信号进行某种逻辑运算,且用逻辑运算结果作为同步控制信号时,简单的通信方式难以实现,可采用“事件标志组”来实现,事件标志组是若干二值信号的组合,可以实现多个任务(包括 ISR)协同控制一个任务,当各个相关任务(包括ISR)先后发出自己的信号后,使事件标志组的对应标志有效,预定的逻辑运算结果有效,触发被控制的任务。
事件标志组的使用方法很灵活,可以将标志定义为“1 有效”,也可以将标志定义为“0 有效”,逻辑关系可以为“逻辑与”(全部标志均有效),也可以为“逻辑或”( 只要任何一个标志有效)。可以简单理解为某个变量,多个控制任务共同控制变量的对应的位,被控制任务在全部或者某一位满足要求时触发运行。
用信号量进行行为同步时,只能提供同步的时刻信息,不能提供内容信息,当控制方对被控制方进行控制,需要向被控制方提供内容信息时,消息邮箱是一种有效的方案。
关于邮箱,不同的RTOS其功能定义有一定差异。大多数RTOS的邮箱不支持缓存,后面发送的会覆盖前面;但有的类似简化版的消息队列,支持缓存,只是消息内容固定为一个指针,可以多次发送;有的RTOS不支持邮箱。
消息队列是应用比较多,队列可以存放多个消息,能够有效解决消息的临时堆积问題,但仍然需要满足一个条件,消息的平均生产时间比消息的平均消费时间长,否则,再长的消息队列也会溢出。
特别注意,当需要传输的数据量较大时,传输其内容指针,而不是内容本身。
以上用于行为同步的通信方式,根据实际情况来选择:
当同步过程不需要传输具体内容时,可选择二值信号量、计数信号量和事件标志组。
当同步过程需要传输具体内容时,可选择消息邮箱、消息队列。
当“任何时候同步信息的生产速度都比同步信息的消费速度慢”时,可选择二值信号量 、事件标志组。
对于非周期性同步信息,当不能保证“任何时候同步信息的生产速度都比同步信息的消费速度慢"时,可选择有缓冲功能的计数信号量、消息队列。
当同步信号为多个信号的逻辑运算结果时,采用事件标志组作为同步手段
合理安排同步点和任务的优先级是获得预期同步效果的关键。
一个任务(或 ISR)为控制方,发出控制信息,另一个任务为被控制方,获取控制信息后即进入就绪状态,若优先级足够高就可以很快进入运行状态,必要时可在ISR退出前进行一次任务切换。
一个由异步亊件触发的ISR 通常与一个任务关联,它们之间就是单向同步关系,与ISR关联的任务总是处于等待状态,每当ISR发出信息就被触发,其任务结构为事件触发型。
如果单向同步发生在两个任务之间,同步效果与两个任务的优先级有关。当控制方任务的优先级低于被控制方任务的优先级时,控制方任务发出信息,被控制方任务进入就绪状态,并立即发生任务切换进人运行状态,瞬时同步效果较好。当控制方任务的优先级高于被控制方任务的优先级时,控制方任务发出信息后,虽然被控制方任务进入就绪状态,但并不发生任务切换,只有当控制方调用系统服务休眠或其它使自己挂起时,被控制方任务才有运行机会,瞬时同步效果较差。不过,控制方在发出信息后立即调用延时函数( 延时时间大于被控制方任务的处理时间),主动使自己挂起来,让低优先级的被控制方任务尽快得到运行机会,可改善同步效果。
在单向同步过程中,必须保证消息的平均生产时间比消息的平均消费时间长,否则再长的消息队列也会溢出。如果消息的生产者是外部物理世界,其消息生产速度是客观的,为了不遗漏消息,只能提高系统处理速度来应对。
如果消息的生产者是系统内部的某个任务,则可以通过协调生产者与消费者的关系来建立一个产销平衡的理想状态。通信的双方相互制约,生产者通过“提供消息”来同步消费者,消费者通过“回复消息”来同步生产者,即生产者必须得到消费者的回复后,才进行下一个消息的生产,这种运行方式称为“双向同步”,这和UART串口的流控比较类似,其实现方式是单向同步操作的两倍。
当需要两个以上任务同步一个任务时,简单的通信方式难以实现,可采用“事件标志组”来实现。只要被同步任务的执行速度足够快,其执行次数就可以“等于”各个同步任务发出信号次数的总和,否则也会遗漏个别消息。
多个任务的运行频度保持一致,即一组关联任务在特定点互相等待,每个相关任务在运行到同步点时都必须等待其他任务,只有相关任务都到达同步点,才可以按优先级顺序依次离开同步点,从而达到相关任务的运行频度保持一致的目的。类似一组人各自独立的到达指定集合点,都到齐统一签到,再各回各家。
多个任务相互同步,理论上事件标志组可用来实现多任务相互同步,每个相关任务运行到同步点时都要“签到”,即调用“发送标志函数”将对应标志置位;然后调用“获取标志组标志函数”进入等待状态,当最后一个相关任务运行到同步点并“签到”时,通过逻辑运算产生“全部到齐”的信号(多任务同步标志),并将这个同步信号分发给各个相关任务( 进入就绪状态)后,相关任务按优先级次序先后运行。但是,因为各个任务的运行情况是动态变化的,每个任务都有可能最后一个到达同步点,只使用事件标志组无法判断在最后一个到达同步点的任务清除标志,导致后续会不断误触发。
其实使用最简单的全局变量,记录当前已经到达同步点任务的数量,这样数值累加到一定程度,即可判断出最后一个到达同步点的任务,每个任务再同步点判断计数值,再将同步信号分发给各个相关任务。这个全局变量在多个任务操作,也需进行特殊处理,也就是下一章的资源同步。
被两个以上并发程序单元(任务或1SR )访问的资源称为共享资源,共享资源一定是全局资源。但全局资源不一定都是共享资源,固定只被一个任务(或 ISR ) 使用的全局资源并不是共享资源,而是这个任务( 或 ISR )的私有资源,对私有资源的读写操作是不受限制的。
任务对共享资源进行访问的代码段落称为关键段落,各个任务访问同一共享资源的关键段落必须互斥,才能保障共享资源信息的可靠性和完整性。这种使得不同任务访问共享资源时能够确保共享资源信息可靠和完整的措施称为资源同步。
资源同步有关中断、关调度、使用互斥信号量、使用计数信号量几种方法,它们都能够在访问共享资源时,保障共享资源信息的可靠性和完整性。
并不是访问所有共享资源都要采取资源同步措施,如共享资源具有“只读”特性,其信息只能读出,不能改写。这类共享资源具有天然的完整性和可靠性,各个任务可以任意读取,如系统内固化的硬件参数信息。需要采取资源同步措施进行访问的必定是动态共享资源,至少存一个任务( 或ISR)可以对其进行“写”操作。
一个任务在对共享资源进行访问前将中断关闭,然后执行访问共享资源的关键段落代码,操作结束后再打开中断。
当中断被关闭后,系统失去了对所有事件的反映能力,不能进行任务切换,从而保证了对共享资源的独占式访问。(优先级非常高的硬件中断不受操作系统关中断控制,具体与RTOS实现及其配置有关)
当参与访问共享资源的并发程序单元中包含ISR 时,关中断是任务访问共享资源的最简单方法,也是唯一的方法。
关中断的优点是简单,缺点是影响系统的实时性。为了减轻对系统实时性的不利影响,访问共享资源的关键段落代码必须尽量简短,绝对不允许在关键段落代码中包含有可能使自己挂起的系统服务函数,否则将使系统死机。
当需要对共享资源进行“写”访问时,先将需要“写入”的信息提前准备好,存放在自己的局部数据结构内,这个局部数据结构与共享资源的被访问部分的数据结构相同,然后关中断,在关键段落代码中只需要完成将局部数据结构的内容复制到共享资源中的操作即可,复制完毕就可以开中断;由于两者数据结构相同,所以执行复制功能的关键段落代码自然非常简单、 快捷。
当需要对共享资源进行“读”访问时,先准备一个与共享资源被访问部分的数据结构相同的局部数据结构,然后关中断,在关键段落代码中只需要完成将共享资源的有关内容复制到局部数据结构中的操作即可,复制完就可以开中断。退出关键段落代码后,再对复制到局部数据结构中的信息进行相关处理。
由于关中断直接影响系统的实时性,因此只能用于对简单共享资源的短暂访问,一般用于对全局变量或小规模全局数据结构的访问。
当共享资源比较复杂或者规模比较大时,关中断的方式不可取。如果该共享资源的使用者全部是任务( 即不包含 ISR ),就可以采用关调度的方法来访问共享资源。
关调度的方法使操作系统的任务调度器停止工作,不能进行任务切换,从而保障关键段落代码的执行不会受到其他任务的干扰。
由于关调度的方法没有将中断关闭,故系统对各种异步事件仍然可以及时响应,并使相关任务进入就绪状态,但不会立即进入运行状态。这也是关调度的缺点,导致所有任务受到牵连,即使它们的优先级足够高并被ISR 触发到就绪状态也无法运行。
在关调度期间,必须尽可能快速完成访问共享资源的工作,以缩短关调度的时间。如果在关调度期间调用系统服务函数而被挂起,其他任务又不能运行,那么系统将崩溃。关调度的资源同步方法优点不多,缺点不少,尽可能不要使用。
当需要访问的共享资源比较复杂,且访问过程比较费时时,关中断、关凋度的措施不可取,它对系统实时性产生了严重影响。如果该共享资源的使用者全部是任务( 即不包含 ISR),可采用互斥信号量的方法来访问。
但使用信号量存在一定风险,可能出现优先级反转现象(优先级反转是指一个低优先级的任务持有一个高优先级任务所需要的共享资源,高优先任务必须等到低优先级任务释放资源才能访问。如果此时有个优先级处于两者之间的任务,并且不需要那个共享资源,则该中优先级的任务实际运行效果优先级最高。如果高优先级任务等待资源时不是阻塞等待,而是循环忙检测,则低优先级任务无法执行,一直占用共享资源,造成的后果就是高优先级任务无法获得资源一直处于等待状态,系统进入假死状态)。
解决方案有两种,设置优先级上限,给临界区一个高优先级,进入临界区的任务都将获得这个高优先级,其他试图进入临界区的任务的优先级都低于这个高优先级,也就不存在优先级反转;另一个方案是优先级继承,当一个高优先级任务等待一个低优先级任务持有的资源时,低优先级任务将暂时获得高优先级进程的优先级别,在释放共享资源后,低优先级任务回到原来的优先级别。这块不需要应用程序关注或实现,因为RTOS自带的互斥信号量(mutex)具有处理优先级反转的功能,特别适合对共享资源的互斥访问。
互斥信号量与用于行为同步的二值信号量不同,互斥信号量的初始值为 1(unlock),表示共享资源有效( 尚未被使用),一个任务需要访问某共享资源时,首先获取该共享资源对应的互斥信号量(lock),如果已经被其它任务占用则等待;若获取成功,则说明该共享资源尚未被其他任务占用,就可以对该共享资源进行使用,使用结束后必须及时发送互斥信号量,解除对该共享资源的占用,以便供其他任务使用。可以类比于进公厕,必须确保门未锁,进入后立刻锁门,事毕出来再开锁,其他人才可再进入使用。
使用互斥信号量访问共享资源时,对中断和任务调度没有限制,系统可以正常响应各种异步事件,其他与该共享资源无关的高优先级任务仍然可以及时运行( 即使任务正在运行关键段落代码),因此,使用互斥信号量进行共享资源访问对系统实时性影响最小。
在使用互斥信号量进行资源同步时,任何任务一旦获得共享资源就可以一直使用到不需要为止,其他任务优先级再高也不能夺去使用权。其原因是获得共享资源的任务具有临时的高优先级(优先级继承值),其他需要访问同一个共享资源的任务均达不到这样高的优先级。优先级继承值必须高于所有需要访问这个共享资源的任务的优先级。
需要注意的是,优先级继承值不能与其他任务的优先级相冋,必须使用一个空闲的、高于全部使用该共享资源的任务的优先级值,初始定义优先级时不要将任务的优先级安排得太紧密,中间留些间隔较好。
当同一类型共享资源有多个实体,就允许多个任务同时使用这类共享资源,但每个任务所使用的共享资源实体是不同的,即对每一个实体的使用仍然是互斥的。
在嵌入式应用系统中,多个实体,比如多路ADC,多路UART,其实对应不同的硬件资源,并不是完全相同的,不能随意选择性使用,也就是感觉计数信号量管理多实体共享资源没有实际意义,对于多路ADC或UART,一般是使用多个互斥信号量或者二值信号量各自管理其中一路。
系统运行过程中,ISR与任务之间、 任务与任务之间必然伴随数据通信,可根据实际情况来选择最合适的通信方式。
全局变量(包括全局数组和全局结构体)可以充当一种共享资源,用来在任务之间传输数据,提供数据的任务或ISR对全局变量进行写操作,使用数据的任务或ISR对全局变量进行读操作,从而实现了数据的的传输。这种情况下全局变量作为共享资源,对其进行的访问必须遵循资源同步的规则。
全局变量虽然可以实现数据传输,但不能实现行为同步,即新的数据产生之后并不能自动通知相关使用者,使用者也不知道当前数据是何时更新的,因此,全局变鼂只能用于没有行为同步要求的任务之间,即每次产生的新数据不要求立即使用,甚至可以不被使用。
在没有行为同步要求的前提下,且传输的数据量不大,采用全局变量并配合关中断的资源同步措施是一种最有效的方法。
也有种特殊情况,当对共享资源进行写操作的任务只有一个,且其优先级高于所有其他进 行读操作的任务,可以在进行写操作时不必关中断,因为低优先级执行读操作任务不可能获取运行权,而低优先级执行读操作任务必须使用关中断措施来访问全局变量。
若需要传输的数据量很大时,采用内存块来存放数据是最合适的选择,也就是内存动态管理函数,操作系统一般会适配动态内存申请、释放的接口,且支持重入。
如果申请的内存空间地址指针作为全局变量,其用法和全局变量一样,必须遵循资源同步的规则。实际它少独立使用,一般情况下是用于消息队列通信方式的内容缓冲区,将申请的内存块地址作为临时变量,作为消息内容的参数传输。
当发送的数据要求接收方及时接收和处理,在数据通信的同时发生行为同步,当通信双方的执行均具有周期性(且周期相同)时,消息邮箱是合适的通信工具。(前提是RTOS支持邮箱,且不带缓存,一次只能发送一条)。
使用消息邮箱发送消息时,实际上只发送指针( 消息的地址〉,不管消息本身的数据类型,发送的指针都按 void *处理,接收方得到这个消息指针后,将指针强制类型转换获取真正的消息内容。
邮箱内容可以直接发送常量(或者数据强制转为指针类型,因为变量100和指针100,本质都是100这个数字)。如果发送的是变量的地址,务必确保变量本身不会销毁,一般是全局变量或者静态局部变量的地址。
由于消息邮箱里只能存放一条消息(部分RTOS邮箱消息功能不同,但消息队列,所有RTOS都表现一致)。通信双方至少有一方没有固定的执行周期,无法保证消息总能在下一个消息产生之前处理完毕,可能丢失消息,这种情况应该使用具有缓冲功能的消息队列,事实上消息队列最常用。
队列是一种标准线性结构,消息的发送、接收就是消息内容的入队或出队。在用户层面,发送消息就是将消息内容放入队列,接收消息时就是从队列中取出一条消息。
消息内容的结构和消息队列的长度由用户自行设置,消息长度按消息积压的最坏情况决定,太短了不保险,太长了浪费内存资源。队列是先进先出,但入队时可以选择放入队列头部还是尾部,对重要消息可以小范围的实现优先处理。
消息内容的要求和消息邮箱一样,必须确保接收消息的任务还能取出数据,不能使用临时变量;也不能使用大量数据块,可以动态申请内存,消息传递的是其指针,实现大数据的传输。
操作系统可以多任务间进行切换,就是靠一个系统定时器以一定频率中断,为系统提供调度(上下文切换)实现任务切换。而这个定时器,就是系统节拍(tick),任务调度、休眠延时都是基于节拍,一个节拍对应的时间各不相同,一般配置1-10ms。
节拍对应时间数值越小,系统实时性越高,但过小则会导致频繁切换任务反而影响任务执行效率。
时间管理服务函数以系统节拍为处理单位,最坏的情况下误差接近一个系统节拍,因此,时间管理服务函数只能用在对时间精度要求不高的场合,或者时间间隔较长的场合。