调度表(ScheduleTables) 通过提供一组静态定义的到期点(expiry points)的封装来解决同步问题。每个到期点定义:
每个调度表都有一个持续时间(Duration),即调度表的一次运行时间,度量单位为计数器的计数单位 Tick。
从调度表的起点到初始终结点的距离被称为初始偏移(InitialOffset),相邻终结点间的距离被称为延迟(Delay),最后一个终结点到调度表终点的距离称之为最后延迟(FinalDelay)。
InitialOffset,Delay和FinalDelay的取值范围如公式3—1,3—2,3-3所示,其中OsCounterMinCycle是驱动计数器的最小循环值,OsCounterMaxAIIowedValue是驱动计数器的最大计数值。
除此之外,还要区分清楚几个概念:
驱动计数器:驱动计数器是用于驱动整个调度表运行的计数器,驱动计数器的计数周期通常是固定的,并且要大于调度表中所有任务的最小执行间隔时间,以确保每个任务都能够被执行到。驱动计数器用于控制任务执行的时间。
驱动计数器模数:就是驱动计数器的最大值,也就是一份调度表运行完,需要多少个counter。驱动计数器的模数被定义为OsCounterMaxAllowedValue + 1;
驱动计数器的模数通常不等于调度表的duration,而是大于调度表的duration。在实际的调度系统中,通常会将驱动计数器的模数设置为调度表的duration的整数倍,以确保调度表中的所有任务都能够被完全执行。例如,如果调度表的duration为10ms,那么可以将驱动计数器的模数设置为20ms或者30ms等,以确保每个任务都能够被执行到。
调度表的时间:调度表的持续时间通常是预先定义的,用于确定调度表中所有任务的执行时间范围。
驱动计数器与调度表持续时间关系:驱动计数器与调度表时间相等吗通常情况下,驱动计数器的计数周期不等于调度表的时间。驱动计数器是用于驱动整个调度表运行的计数器,它的计数周期通常是固定的,并且要大于调度表的时间,以确保调度表中的所有任务都能够被执行到。
例如:驱动计数器的计数周期为1,即每个计数周期的时间为1个单位。调度表中有3个任务,分别为Task A、Task B和Task C,它们的执行时间分别为3、5和2个计数周期。
注意:Task A在第一个周期中执行时间为3个计数周期,在第二个周期中执行时间为4个计数周期,在第三个周期中执行时间为2个计数周期,在第四个周期中执行时间为3个计数周期,在第五个周期中执行时间为5个计数周期。在调度表中,每个任务的执行时间和间隔时间是预先定义的。Task A的间隔时间是指Task A完成一次执行后需要等待的时间,在实际的调度系统中,任务的间隔时间可能会受到多种因素的影响,例如任务的优先级、资源的竞争等。在设计和实现调度系统时,需要根据具体的应用场景和需求进行合理的设计和实现。
同步计数器:同步计数器是一个软件计数器,用于同步任务的执行。显示同步中同步计数器等于驱动计数器,当驱动计数器的计数值等于同步计数器的计数值时,任务调度器会检查任务队列中是否有需要执行的任务。如果有,则执行该任务,否则继续等待下一个同步计数器的计数周期。这样,就可以实现任务的精确调度和同步,从而保证系统的稳定性和可靠性。
操作系统计数器:操作系统计数器通常由操作系统内核实现,用于记录系统运行时间和控制系统中各个模块和任务的执行时间和间隔时间。每个OS Counter都有一个最大计数值(OsCounterMaxAllowedValue),它表示OS Counter的最大计数范围。同时,每个OS Counter还有一个最小计数值(OsCounterMinCycle),它表示OS Counter的最小计数周期。OS Counter的最大计数值和最小计数周期可以用来确定驱动计数器的模数和调度表的持续时间。具体来说,驱动计数器的模数应该等于OS Counter的最大计数值+1,而调度表的持续时间应该等于OS Counter的最大计数值+1乘以OS Counter的最小计数周期。
假如tick是1ms,那么上面的调度表就是50ms要做的事情,做什么事情呢?
调度表,可以用OS Alarm、OS Counter来实现,相当于到时间了就去激活task、设置Event,那么为什么要搞一个调度表的概念呢?
因为要同步:使在OSEK OS中,可以利用一个OSEK计数器和一组附属于该计数器的自启动报警器来实现静态定义的任务激活机制。在简单情况下,可以通过设置报警器一旦启动就不能更改来实现。然而,如果在运行时对某个报警器进行修改,就很难保证报警器之间的相对同步。为此,AUTOSAR OS规范引入了调度表(ScheduleTable,ST)。
在AUTOSAR中,调度表是用于描述任务的执行顺序和时间间隔的一种数据结构。Alarm是一种定时器,用于在指定的时间点或时间间隔触发事件。当Alarm与调度表结合使用时,调度表可以保障Alarm的同步。
驱动计数器的一个Tick与调度表的一个Tick的分辨率一致;操作系统使用一个迭代器遍历调度表上的所有终结点,按照offset的大小从初始终结点(InitialExpiryPoint)开始遍历,直到最后一个终结点IFinaIExpiryPoint)为止。在处理终结点上的操作时,操作系统必须先激活完所有的任务后,才能设置事件。由于在一个终结点上很有可能对一个任务同时进行激活和设置事件的操作,如果先激活一个处于挂起态的任务,再对它设置事件,操作会成功执行,而如果顺序相反则会导致操作失败。因此,操作系统对终结点上操作的处理顺序进行了严格的限制。
单次执行是调度表处理完最后一个终结点后进入停止状态,不再继续执行,主要用于对某个触发进行响应:循环执行是调度表处理完最后一个终结点后再返回到第一个终结点,继续执行,主要用于执行重复操作的特定应用。
重复执行,由于调度表的持续时间不一定等于驱动计数器的模数,因此不能保证同一个终结点在不同次执行时都在同一个绝对计数值上执行,而在某些情况下这是必需的,例如校正发动机的角旋转度数,故需要同步机制来解决。
一个未使用同步策略的调度表在任意时刻总是处于下述三个状态中的一个,详细的状态转换模型如下图所示。在任何时刻,都可以使用系统服务GetScheduleTableStatus0来获取调度表的状态。 对于未使用同步策略的调度表来说,调度表的持续时间和驱动计数器的模数没有潜在的关联,但各终结点的偏移和延迟需要按照公式3-1、3-2、3-3设置。
3. 调度表有两种类型的启动方式:绝对启动 StartScheduleTableAbs()和相对启动StartScheduleTableRel0。调度表在被启动后,将进入运行态。
绝对启动是提供一个启动值(Start),当驱动计数器的计数值与Start匹配时,操作系统将启动调度表,该调度表的第一个终结点将在驱动计数器的计数值等于(Start+Initial Offset)时被处理。
相对启动是提供一个偏移(Offset),当驱动计数器的计数值相对于现在(Now)增加了Offset个Tick后,操作系统将启动调度表,该调度表上的第一个终结点将在驱动计数器的计数值与(Now +Offset+lnitial Offset)匹配时会被处理。
用户能在任意时刻调用系统服务StopScheduleTable()立即终止一个正在处于运行态的调度表,被终止后,调度表将进入停止态。无论调度表被终止时处理到了哪个终结点,下次启动时都将会从第一个终结点开始处理。此外,调度表在正常运行结束后,也会进入停止态。
当用户调用NextScheduleTable0发出切换调度表的请求时,操作系统将继续处理当前调度表,把被切换入的调度表的状态修改为下一个,直至处理完当前调度表的最后一个终结点后,才会去启动被切换入的调度表。
处理调度表上的初始终结点的绝对时间是用户可控的。然后,如果调度表的运行模式是重复执行,将不能保证每次执行初始终结点的时间是一样的。
该问题的产生,可能是由于调度表的持续时间不一定等于驱动计数器的模数导致的。
举例:注:驱动计数器通常是用于驱动整个调度表的计数器。它的作用是根据预定的时间间隔来触发调度表中各个任务的执行,从而实现任务的协调和调度。
假设调度表中有三个任务,任务A、任务B和任务C,它们的执行时间分别为2秒、3秒和4秒,也就是如果想要完整的执行三个任务需要9秒的时间。然而驱动计数器的模数为5秒,即当计数器达到5秒时会重新从0开始计数。也就是说任务C一直处于不执行的状态。
隐式同步:
在隐式同步的情况下,调度表的更新和OS模块的调度是隐式地同步的。即当调度表中的任务时间到达时,OS模块不会立即调度该任务,而是等待下一个系统节拍时再调度该任务。因此,在隐式同步的情况下,任务的执行时间可能存在一定的误差,并且无法保证任务的正确执行顺序和时间间隔。
对于隐式同步而言,调度表的驱动计数器就是同步计数器,即调度表上的时间和驱动计数器上的时间保持一致。
隐式同步的调度表不需要操作系统的额外支持,但为了使调度表上的时间和驱动计数器的计数值一致,做了一些限制:调度表的持续时间必须等于驱动计数器的模数。如公式3-4所示:
也就是隐式同步的操作系统模块的调度表(ScheduleTable)(每个调度表都有一个持续时间(Duration))应具有等于其关联 OSEK OS 计数器的 OsCounterMaxAllowedValue + 1 的持续时间。
注意:驱动计数器的模数被定义为OsCounterMaxAllowedValue + 1。其中,OsCounterMaxAllowedValue是一个常数,表示驱动计数器的最大计数值。而+1是因为计数器从0开始计数,因此最大计数值需要加上1才能得到模数。
为了使调度表达到同步,操作系统需要在一个己知的计数值启动调度表,这意味着调度表使用隐式同步策略时,只能使用绝对启动,不能使用相对启动。在调度表被绝对启动后,若终结点的偏移与驱动计数器的计数值匹配时,该终结点将会被处理。为了保证调度表和驱动计数器同步,通常的做法是在驱动计数器的计数值为0时启动调度表,即使用StartScheduleTableAbs(Tbl,0),如图所示:StartScheduleTableAbs(Tbl,0)是一个API函数,用绝对启动启动一个调度表,并将其绝对时间点设置为0。它的作用是将调度表的执行时间点设置为系统启动的时间点,从而让调度表从头开始执行。
其实说白了隐式同步就是在running和SYNC开始的节点,用绝对启动StartScheduleTableAbs(Tbl,0)使调度表 OS Counter的计数周期应该等于任务调度表的持续时间。让两者运行一致。从而实现调度表的周期性执行。
在显示同步的情况下,调度表的更新和OS模块的调度是显式地同步的。即当调度表中的任务时间到达时,OS模块会立即调度该任务,并执行该任务的任务函数。因此,在显示同步的情况下,任务的执行时间是准确的,并且可以保证任务的正确执行顺序和时间间隔。
显式同步是指调度表的驱动计数器和同步计数器不是同一个计数器,但是这 2 个计数器 tick 的周期是一样的。
显式同步的调度表(ScheduleTable)需要操作系统模块的额外支持。调度表(ScheduleTable)正常情况下,由操作系统模块的计数器驱动,被称为驱动计数器(drive Counter)。但处理过程需要与不属于操作系统模块的计数器对象的另一种计数器进行同步,此类进行同步的计数器被称为同步计数器(synchronization Counter)。
在调度表(ScheduleTable)、操作系统模块的计数器和同步计数器之间必须强制执行以下约束:
如上图所示:初始时调度表处于状态 STOPPED,调用函数 StartScheduleTableSync()后,进入状态 WAITING。WAITING 状态一直持续到用户调用 SyncScheduleTable(),
对每一个调度表初次调用 SyncScheduleTable()之后,调度表进入 RUNNING_AND_SYNCHRONOUS 状态。如果用户对一个周期调度表没有再次调用 SyncScheduleTable(),那么调度表将一直处在 RUNNING_AND_SYNCHRONOUS 状态,但是当用户由于某种原因再次调用 SyncScheduleTable()后,系统会进行调度表与同步计数器之间的偏移值计算。如果计算出的偏移值大于调度表偏移的容忍度,那么调度表就会进入 RUNNING 状态,并且系统会进行调度表同步,直到调度表的偏移值小于等于调度表的偏移容忍度之后停止同步,调度表再次进入 RUNNING_ AND_SYNCHRONOUS 状态。