图2.9显示了相同任务集的执行,具有与图2.8相同的到达模式,但这次任务是非抢先调度的。您可以看到,高优先级任务的响应时间比t时要长得多,但总体堆栈消耗要低得多。
单堆栈模型还显著简化了链接时的堆栈空间分配,因为您只需要为整个系统堆栈分配一个内存部分,就像您完全不使用操作系统一样。
RTA-OS独特地扩展了单堆栈模型,以提供对扩展任务的支持,而对基本任务的性能没有任何影响。
在RTA-OS中,扩展任务的生命周期如下:
Suspended ➔ Ready 该任务将被添加到已准备就绪的队列中。
Ready ➔ Running 任务被分派了,但与基本任务不同的是,上下文被放置在堆栈的顶部,上下文被放置在堆栈空间中,位于所有低优先级任务的预先计算的最坏情况抢占深度。
Running ➔ Ready 扩展任务被抢占。如果抢占任务是一个基本任务,那么它将正常地在堆栈顶部分派。如果抢占任务是一个扩展任务,那么它将按照所有低优先级任务预先计算的最坏情况抢占深度进行分派。
Running ➔ Waiting 任务的等待事件堆栈上下文,包括操作系统上下文、本地数据、函数调用的堆栈帧等,被保存到一个内部操作系统缓冲区。
图2.9 具有非抢占式任务的单堆栈行为
Waiting ➔ Ready 该任务将被添加到已准备就绪的队列中。
Ready ➔ Running 任务的“等待事件堆栈”上下文从内部操作系统缓冲区复制到堆栈中,并以所有低优先级任务的预先计算的最坏情况抢占深度进行计算。
此流程允许管理扩展任务的额外成本仅应用于扩展任务本身。包含扩展任务的系统中的基本任务与仅包含基本任务的系统具有相同的性能。
这个生命周期的关键部分是在最坏情况下的分派/恢复,以及在堆栈上和从堆栈上复制。在最坏情况抢占点的分派可以保证,无论何时扩展任务在等待后恢复,它都可以在内存中完全相同的位置恢复其局部变量。它保证了低优先级任务的每种可能的抢占模式都不会超过扩展任务的分派点。扩展任务D的调度-等待-恢复周期如图2.10所示。
复制和打开允许恢复扩展任务堆栈上下文。这是必要的,因为高优先级任务和/或isr可能在扩展任务等待时发生。这些可能会消耗比最坏情况抢占点更大的堆栈空间(记住,最坏情况抢占点只适用于较低优先级的对象),因此会超过写入扩展任务的上下文。但是,固定优先级抢占调度。
图2.10 具有扩展任务的单堆栈管理
保证在扩展任务恢复时没有更高优先级的任务可以准备好运行(如果是这种情况,它就不能恢复)。
扩展任务管理需要告诉RTA-OS任务和ISR使用了多少堆栈。下面几节描述各种配置参数。
计算出的最坏情况分派点定义了字节数,相对于调用StartOS()时堆栈指针的地址,扩展任务需要从该地址开始。这些偏移量作为ROM数据存储在扩展任务控制块中,并在运行时添加到堆栈的基址中。
这意味着需要告诉RTA-OS有关堆栈使用的各种参数。捕获的值是特定于端口的,您应该阅读您的端口的目标/编译器端口指南以获得额外的指导。
RTA-OS提供了运行时特性,用于测量任务和ISR的最坏情况下的堆栈值。
通常,所有端口都将允许您指定以下值。所有数字均以字节为单位:
用于C启动的堆栈(Stack used for C-startup, SpPreStartOS)在调用StartOS()时已经在使用的堆栈量。
该值被简单地添加到操作系统在运行时支持所有任务和中断所需的总堆栈大小中。
通常,您使用它来获得链接器必须分配的堆栈数量。
在计算最坏情况分派点时不需要这个值,可以安全地将其设置为零,除非您希望在堆栈使用情况报告中计算最坏情况的总体堆栈使用情况。
正常情况下,当操作系统配置发生变化时,该值不会改变。
空闲时使用的堆栈(Stack used when idle, SpStartOS)
操作系统处于空闲状态(通常在Os_Cbk_Idle()内部)时使用的最大堆栈量。这只是调用StartOS()时使用的堆栈与没有任务或中断运行时使用的堆栈之间的区别。如果没有使用Os_Cbk_Idle(),该值可以为零。它必须包含在空闲状态下被调用的任何函数所使用的堆栈。
正常情况下,当操作系统配置发生变化时,该值不会改变。
ISR激活的堆栈开销(Stack overheads for ISR activation, SpIDisp)
与从task中激活任务相比,从ISR中激活任务所需的额外堆栈量。如果一个任务在类别2 ISR中被激活,并且该任务比任何当前运行的任务具有更高的优先级,那么对于一些与激活优先级较低的任务相比,目标操作系统可能需要使用更多的堆栈。这个值说明了这一点。在大多数目标上,这个值为零。
该值用于最坏情况下的堆栈大小计算。
当操作系统配置有较大变化时,该值可能会发生变化。
ECC任务的堆栈开销(Stack overheads for ECC tasks, SpECC)
启动ECC任务所需的额外堆栈量。ECC任务在启动时需要比BCC任务在堆栈上保存更多的状态。此值包含差值。
这个值可以通过测量堆栈值得到:
l在基本任务(向上)激活之前,立即在被激活任务的入口函数中。
l在扩展任务(向上)激活之前,立即在激活任务的入口函数中。
然后用第一个值减去第二个值。
当操作系统配置有较大变化时,该值可能会发生变化。还需要注意的是,如果你正在使用堆栈重定位(将不受信任的代码堆栈对齐以适应MPU),那么你将需要减少调整量的值。
ISR的堆栈开销(Stack overheads for ISR SpPreemption)
图2.11 基本堆栈值
用于服务第二类ISR的堆栈数量。当二类ISR中断一个任务时,它通常会在堆栈上放置一些数据。如果ISR测量堆栈以确定被抢占的任务是否超过了它的堆栈预算,那么它将高估堆栈使用情况,除非从测量的大小中减去这个值。
该值也用于计算系统最坏情况下的堆栈使用情况。该值可以通过在中断之前测量堆栈值,并在测试期间立即在类别2 ISR的输入函数中获得。注意要准确地设置这个值。如果它的值太高,那么当发生减法时,就会发生32位下溢,并导致操作系统认为已经检测到预算超支。
当操作系统配置有较大变化时,该值可能会发生变化。
Note: 除了ISR (SpPreemption)的堆栈开销外,所有的强制堆栈值都会在检查期间添加到堆栈值中。这意味着指定比实际发生的更大的值是安全的。但是,ISR的堆栈开销(SpPreemption)将从进入ISR时的堆栈指针的当前值中减去,以检查被抢占的任务或ISR是否已经超过了它的堆栈使用量。因此,在这里指定一个大的值可能会导致报告一个没有发生的错误(即RTA-OS堆栈管理将报告一个“false positive”。
在只包含基本任务的系统中,没有必要告诉RTA-OS任何堆栈分配,除非正在进行堆栈监控。只需要在链接器/定位器中为应用程序分配一个足够大的堆栈部分。这是单栈架构的好处之一。
对于使用扩展任务的应用程序,可以像以前一样分配链接器部分,但是还必须告诉RTA-OS配置中优先级低于最高优先级扩展任务的每个任务的堆栈分配,即使它们是基本任务。RTA-OS使用堆栈分配信息为每个离线扩展任务计算最坏情况下的抢占点。
指定的堆栈分配是用于该任务的整个堆栈,包括:
•操作系统上下文
•任务主体中局部变量的空间
•任务体中调用的任何函数(以及它们的局部函数)所需的空间
可以使用RTA-OS的堆栈度量特性来获取堆栈分配的准确值。
Note: RTA-OS仅使用提供的堆栈信息来计算最坏情况下的抢占点。RTA-OS不保留任何堆栈空间。必须按照与普通应用程序相同的方式指定堆栈应用程序堆栈空间。
图2.12显示了如何配置堆栈分配。
图2.12 堆栈分配配置Stack Allocation Configuration
虽然RTA-OS使用单堆栈模型,但在某些端口上,这并不一定意味着只使用一个物理堆栈。
可能是编译器或硬件自动将数据强制放到不同的堆栈上。例如,TriCore设备使用CSA内存快速保存呼叫上下文。RTA-OS将其视为等同于堆栈。当在你的配置中输入堆栈值时,你应该提供两个逗号分隔的值;第一个用于Supervisor堆栈,第二个用于Context堆栈。
即使有多个物理堆栈,RTA-OS仍然提供了单一堆栈架构的好处——当任务和/或isr共享优先级时,每个物理堆栈上所需的堆栈空间可以叠加。但是,为了使堆栈分配正确工作,您需要指定每个堆栈上所需的空间。
如果您配置了需要此信息的目标,RTA-OS将要求您提供多个堆栈值。图2.13显示了这样一个配置的对话框,其中有两个堆栈:' Supervisor '和' Context '。
图2.13针对多个堆栈的堆栈分配配置Stack Allocation Configuration for multiple stacks
图2.14 指定WaitEvent()堆栈分配
回顾第2.6.1节,每当扩展任务进入等待状态时,RTA-OS保存任务的“等待事件堆栈”上下文,当任务重新进入运行状态时,上下文将恢复。
RTA-OS将“等待事件堆栈”上下文保存在内部缓冲区中。默认情况下,RTAOS分配的缓冲区等于您为该任务指定的最坏情况堆栈分配。假设您的堆栈分配是正确的,这应该总是足以在调用WaitEvent()时保持最坏情况下的堆栈使用情况。
不过,大多数使用扩展任务的应用程序通常只从任务的入口函数调用WaitEvent()。此时,堆栈上只有少量本地数据(可能为零)。在使用扩展任务时,您可以通过仅分配足够的缓冲区空间来保存最坏情况下的“等待事件堆栈”上下文,而不是任务所需的绝对最坏情况空间,从而最小化RTA-OS预留的RAM大小。
可以通过在WaitEvent()执行时给出RTA-OS最差的值(图4.14)来指定为这个缓冲区保留多少字节。请记住,最坏情况的值可能包括WaitEvent()调用本身使用的一些堆栈。获得正确值的一种方法是从一个小值开始,一直增加它,直到在Os_Cbk_StackOverrunHook()调用中不再看到OS_ECC_WAIT错误。如果无法保存堆栈上下文,RTA-OS将不会将ECC TASK置于等待状态。
Note: 如果保留等待事件()堆栈分配为“未定义的”,那么RTA-OS将默认使用您为堆栈分配指定的字节数。
使用默认值
虽然您应该为每个任务设置一个堆栈值,但RTA-OS允许您设置一个被所有任务使用的全局默认值。这可以在常规➔默认堆栈值中找到。
如果没有为任务配置堆栈分配,则RTA-OS将使用默认值:
l计算最坏情况下的堆栈偏移量
l配置等待事件(WaitEvent())保存/恢复区域
l堆栈监控(当配置时)
特定于任务/ ISR的堆栈分配的规范将覆盖默认值。
如果您提供给RTA-OS的堆栈分配数字是错误的(即它们太小),那么在运行时这是一个潜在的错误来源。
有三件事可能会出错:
1. 当RTA-OS试图分发补丁扩展任务时,由于堆栈指针的当前值高于计算出的最坏情况分派点,因此无法启动扩展任务。这意味着堆栈上的一个(或多个)低优先级任务消耗了太多空间。
后面章节中描述的堆栈监视可用于确定哪个任务出现故障。
2. 扩展任务不能从等待状态恢复,因为堆栈指针高于它应有的位置。当为扩展任务正在等待的事件调用SetEvent()并且扩展任务现在是系统中最高优先级的任务时,可能会发生这种情况。
3.扩展任务不能进入等待状态,因为该任务当前使用的堆栈数量大于配置的' WaitEvent()堆栈'的大小。
当RTA-OS检测到扩展任务堆栈管理的问题时,它将调用ShutdownOS(),错误代码为E_OS_STACKFAULT。
如果你想调试这个问题,那么你可以启用堆栈故障钩子,如图2.15所示。
图2.15 使能Os_Cbk_StackOverrunHook()
FUNC(void, OS_CALLOUT_CODE) Os_Cbk_StackOverrunHook(Os_StackSizeType
Overrun, Os_StackOverrunType Reason) {
/* Identify problem */
for(;;) {
/* Do not return! */
}
}
Example 2.1: Minimum recommended Os_Cbk_StackOverrunHook()
当配置了Os_Cbk_StackOverrunHook(),当发生堆栈故障时,RTA-OS将调用用户提供的回调Os_Cbk_StackOverrunHook(),而不是关闭OS()。该回调被传递了两个参数:
1.溢出数的字节数
2.溢出原因
对于未启用堆栈监视的扩展任务系统,溢出可以是以下情况之一:
•OS_ECC_START—扩展任务无法启动,因为当前堆栈指针超过了构建时计算的最坏情况分派点。此故障的原因是一个(或多个)低优先级任务超过了配置的堆栈分配。要解决这个问题,您需要确定哪个任务出错了。栈监控和测量章节解释了如何使用RTA-OS的堆栈监视特性来做到这一点。
•OS_ECC_RESUME—扩展任务不能从等待中恢复,因为当前堆栈指针超过了构建时计算的最坏情况分派点。此故障的原因是一个(或多个)低优先级任务超过了配置的堆栈分配。
要解决这个问题,您需要确定哪个任务出错了。栈监控和测量章节解释了如何使用RTA-OS的堆栈监视特性来做到这一点。
•OS_ECC_WAIT—扩展任务使用的堆栈空间超过WaitEvent()设置的堆栈大小,无法进入等待状态。要解决这个问题,您应该至少将WaitEvent()堆栈大小增加到溢出参数所指示的字节数。
任务类似于C函数,当它们被RTA-OS调用时,它会实现某种形式的系统功能。
TASK(task_identifier)
{
/* Your code */
}
Note: 不需要为任务输入功能提供任何C函数原型。这些都是通过RTA-OS生成的Os.h头文件提供的。
当任务开始运行时,将从任务输入函数开始执行。任务输入函数是使用示例2.2中的C语法编写的。
请记住,基本的任务是一次性的。这意味着它们从固定的任务入口点执行,并在完成后终止。
示例2.3显示了一个名为BCC_Task的基本任务的代码。
Example 2.3: A Basic Task
现在,将示例2.3中的示例与示例2.4进行比较。示例2.4显示,扩展的任务不一定需要终止,并且可以保持在一个循环中等待事件。
Example 2.4: Extended Task Waiting for Events
2.8 激活任务Activating Tasks
任务只有激活后才能运行。激活可以将任务从挂起状态移动到就绪状态,也可以将另一个条目添加到就绪任务队列(如果任务支持多个激活)。该任务将为每个激活运行一次。
超过激活计数是一个错误,当这种情况发生时,应用程序将生成E_OS_LIMIT错误(即使在标准构建状态下)。
任务可以从两个任务和(第2类)ISRs中激活。
激活任务并不会使任务立即开始执行,它只是使任务准备好运行。然而,RTA-OS需要检查激活的任务是否比当前运行的任务具有更高的优先级,如果是,则导致上下文切换,以便新任务可以抢占当前运行的任务。
当您从另一个任务激活一个任务RTA-OS时,确切的行为取决于相对的任务优先级。如果激活的任务优先级高于当前运行的任务,则新激活的任务将抢占当前任务。否则,该任务将保持在就绪队列中,直到它成为最高优先级的就绪任务。在一个设计良好的实时系统中,一个任务激活一个更高优先级的任务是不寻常的。通常isr捕获系统触发器,然后激活任务以执行任何相关处理。反过来,这些任务可能激活较低优先级的任务,以实现具有较长截止日期的触发器响应。
观察这一事实导致了RTA-OS中的一个主要优化。如果指定任务从不激活高优先级任务,RTA-OS可以消除测试每次激活后是否需要上下文切换的内部代码。这是通过选择“禁止向上激活”优化来配置的。
这类似于从ISR激活任务时的行为。所有ISR都有一个严格高于最高任务优先级的优先级。
当一个任务从ISR激活时,它永远不会立即进入运行状态,因此不需要检查上下文切换。
只有在离开ISR时才需要这样的检查。
2.8.1 直接激活Direct Activation
任务可以通过许多不同的方式激活。任务激活的基本机制是ActivateTask() API调用,它直接激活任务。ActivateTask(TaskID)调用将指定任务置于就绪状态。ChainTask(TaskID)调用终止了调用任务(见2.11节),并将命名任务置于就绪状态。
2.8.2 间接激活Indirect Activation
除了直接激活任务外,还可以使用其他AUTOSAR OS机制间接激活任务。这些方法在后面的章节中有更详细的描述。
报警激活(Activation by an Alarm)。对于系统中的每个告警,您可以指定每次告警过期时激活的任务。
Example 2.5: Using Direct Activation Chains
通过调度表激活(Activation by a Schedule Table)。对于系统中的每个调度表,可以指定在表上的一个或多个到期点上激活的任务。
2.9 控制任务执行顺序Controlling Task Execution Ordering
在许多情况下,您需要限制特定任务的执行顺序。在基于数据流的设计中尤其如此,其中一个任务需要在另一个任务使用计算值之前执行一些计算。如果执行顺序不受约束,则可能出现竞态条件,应用程序行为将不可预知。可以通过以下方式控制任务的执行顺序:
•直接激活链(见2.9.1节)。
•优先级(见章节2.9.2)。
•非抢占任务
2.9.1 直接激活链Direct Activation Chains
当使用直接激活链来控制执行顺序时,任务对必须在进行调用的任务之后执行的任务执行ActivateTask()调用。
有三个任务Task1、Task2和Task3,它们必须按照Task1、Task2、Task3的顺序执行。
示例2.5给出了任务体示例。
图2.16显示了假设Task1优先级最高,Task 3优先级最低,这些任务将如何执行。
图2.16 控制任务执行顺序的直接激活
2.9.2 使用优先级Using Priority Levels
约束任务执行顺序的优先级级方法可以用来利用抢占调度策略的性质来控制激活顺序。
回顾第2.1节,在固定优先级抢占调度下,调度器总是运行最高优先级的任务。如果许多任务被释放到就绪队列中,它们将按优先级顺序执行。这意味着您可以使用任务优先级来控制执行顺序。
从前面的例子来看,在例2.5中,让我们假设Task1的优先级最高,Task3的优先级最低。这意味着可以重写任务主体以利用优先级控制的激活。这可以在例2.6中看到。
图2.17显示了如何执行这些任务。
Example 2.6: Using Priority Level Controlled Activation
图2.17 使用优先级来控制任务的执行顺序
2.10 RTA-OS中的合作调度Co-operative Scheduling in RTA-OS
当一个任务是非抢占式运行时,它会阻止任何任务(包括那些高优先级的任务)的执行。然而,有时对于非抢占式任务来说,提供可以进行重调度的显式位置是有用的。这比简单地非抢先运行更有效,因为高优先级任务对系统刺激的响应时间更短。在一个系统中,任务以非抢先的方式运行,并为重新调度提供点,这种系统被称为协作调度系统。
Schedule() API调用可用于暂时消除非抢占任务和使用内部资源的任务施加的抢占约束。
当调用Schedule()时,允许运行任何优先级高于调用任务的就绪任务。Schedule()直到所有高优先级任务结束才返回。
例2.7展示了一个非抢占任务Cooperative,它包括一系列函数调用。一旦启动,每个函数运行到完成时不会抢占,但是任务本身可以在每个函数调用之间被抢占。
图2.18显示了Task1和Task2这两个相互协作的任务在战时如何发挥作用。白色部分表示不可抢占的代码部分。
Example 2.7: Making a task run co-operatively
图2.18 合作任务Co-operative tasks
2.10.1 优化Schedule() API Optimizing out the Schedule() API
Schedule()在完全抢占式系统中没有用处。如果不打算使用它,可以使用“优化,RTA-OS, disallow Schedule()”来禁止在rtaoscfg中调用Schedule()。如果不允许对Schedule()的调用,那么将看到系统的最差情况堆栈需求降低了。
2.11 终止任务Terminating Tasks
在AUTOSAR操作系统中终止的任务必须通过API调用来告诉操作系统正在发生这种情况。AUTOSAR OS标准为任务终止定义了两个API调用。必须使用其中一个来终止任何任务。这些API调用是:
•TerminateTask ()
•ChainTask (TaskID)
当一个任务完成时,它必须调用这些API中的一个。这确保RTA-OS可以正确地调度下一个准备运行的任务。
TerminateTask()强制调用任务进入挂起状态。然后RTA-OS将在就绪状态下运行下一个优先级最高的任务。
ChainTask(TaskID)终止调用任务,激活任务TaskID。因此,该API就像执行一个TerminateTask(),然后立即执行ActivateTask(TaskID)。链接任务将指定的任务置于就绪状态。
2.11.1 优化RTA-OS中的任务终止Optimizing Termination in RTA-OS
AUTOSAR OS标准允许任务在任何时候调用任务终止API调用,包括在嵌套很深的函数调用集中。
这是一种糟糕的编程实践——相当于goto的使用。
在运行时,RTA-OS必须存储允许它在任务终止时清除堆栈的信息,而不是入口函数。这通常使用setjmp/longjmp对来完成。
例2.8显示了对其他函数进行嵌套调用的任务。Task1运行时,它调用Function1()。
Function1()然后调用Function2()。Function2()包含可以终止调用任务的代码(在本例中是Task1)。
然而,单堆栈架构的一个关键好处是,在其入口函数中终止的任务可以简单地返回- TerminateTask()不需要做任何事情。如果所有的任务都没有终止,或者只是在它们的入口函数中终止,那么RTA-OS保存的允许从任何地方返回的上下文都不需要存储。
RTA-OS允许您使用快速终止优化(Optimizations➔fast Terminate)来开发良好的应用程序设计。当所有执行TerminateTask()或ChainTask() api的任务只在它们的entry函数中执行此优化时,您可以启用此优化。优化告诉RTA-OS不生成代码以节省不必要的上下文,从而节省堆栈空间。
#include void Function1(void) {
...
Function2();
...
}
void Function2(void) {
if (SomeCondition) {
TerminateTask();
}
}
TASK(Task1) {
/* Make a nested function call. */
Function1();
/* Terminate the task in the entry function*/
TerminateTask();
}
Example 2.8: Terminating a Task
2.12 延迟任务Delayed Tasks
OS选项“支持延迟任务执行”可用于添加对api Os_SetDelayedTasks()、Os_AddDelayedTasks()和Os_RemoveDelayedTasks()的支持。
这些api允许您告诉RTA-OS延迟一组任务的执行。延迟任务可以被激活,但直到从集合中删除它们才会实际运行。
Os_SetDelayedTasks()用于指定需要延迟哪些任务。如果一个任务在调用之前被延迟,但它不在新的延迟任务集中,那么如果它的优先级高于调用方,那么它将在此调用返回之前执行。
您必须只设置在调用核心上运行的任务。
Os_AddDelayedTasks()用于向已有的延迟任务集中添加任务。多次添加任务是允许的,但没有效果。必须只添加在调用核心上运行的任务。
Os_RemoveDelayedTasks()用于从去袒护的任务集中移除任务。如果被删除的任务的优先级高于调用方,则它们将在此调用返回之前执行。
注意,如果某个特定核心上的任务共享优先级,则必须指定共享优先级的所有任务或不指定优先级。当启用延迟任务时,任务状态模型变得有点复杂。下表试图解释可能的转换。
2.13 空闲机制The Idle Mechanism
当没有任务或ISR要运行时,任何抢占式操作系统都必须有事可做。在AUTOSAR OS中,这是通过空闲机制实现的。在RTA-OS中,当没有任务或ISR要运行时,操作系统将处于繁忙等待循环中,什么也不做。
但是,可以通过声明一个名为Os_Cbk_Idle的回调来提供您自己的空闲机制实现,从而覆盖默认行为。
Os_Cbk_Idle的行为与任务相同,除了:
•无法激活
•不能终止
•它不能等待事件
•它不能被束缚
•不能使用内部资源
Os_Cbk_Idle的优先级比系统中的任何任务都低,因此它只在没有准备运行的任务(或ISR)时运行。
因此,空闲机制为您提供了一个几乎完全不受系统开销影响的“额外任务”。
例2.9显示了Os_Cbk_Idle用于控制RTA的实现。
Os_Cbk_Idle在退出时返回一个布尔值,告诉RTA-OS是否再次调用Os_Cbk_Idle。当返回TRUE时,RTA-OS立即再次调用Os_Cbk_Idle。当返回FALSE时,RTA-OS停止调用Os_Cbk_Idle,并进入繁忙等待循环的默认行为。
#include FUNC(boolean, OS_CALLOUT_CODE) Os_Cbk_Idle(void) {
#ifdef OS_TRACE
CheckTraceOutput();
UploadTraceData();
#endif /* OS_TRACE */
return TRUE;
}
OS_MAIN() {
/* System hardware initialization. */
StartOS(OSDEFAULTAPPMODE);
/* The call never returns */
}
2.14 任务开始和结束的构子函数Pre and Post Task Hooks
假设需要在每个任务开始之前和/或在每个任务结束之后执行一些代码,例如分析执行的跟踪。可以使用AUTOSAR OS提供的PreTask和PostTask钩子来实现这一点。
当任务进入运行状态时,RTA-OS会调用PreTask钩子。
这意味着当一个任务在抢占后恢复时,PreTask钩子也将被调用。
当任务移出运行状态时,RTA-OS会调用PostTask钩子。
PostTask钩子将在任务终止时调用,并且每次任务被抢占时调用。
图2.19显示了相对于任务抢占,PreTask和PostTask钩子被调用的位置。
这两个钩子只有在配置时才被调用。图2.20显示了如何启用钩子。
图2.19 PreTaskHook()和PostTaskHook()相对于任务抢占
图2.20 Enabling the PreTaskHook() and PostTaskHook()
FUNC(void, OS_CALLOUT_CODE) PreTaskHook(void) {
/* PreTask hook code. */
}
FUNC(void, OS_CALLOUT_CODE) PostTaskHook(void) {
/* PostTask hook code. */
}
Example 4.10: The PreTaskHook and PostTaskHook
例2.10展示了钩子应该如何出现在代码中。
在任务进入和退出时以及每次抢占/恢复时调用PreTask和PostTask钩子。这意味着可以使用这些钩子记录应用程序的执行跟踪。由于应用程序中的所有任务都必须使用相同的PreTask和PostTask钩子,因此有必要使用GetTaskID() API调用来确定在进入钩子例程时哪个任务已经或将要运行。
RTA-OS定义了一组宏,这些宏仅在对应的钩子被启用时才被定义。这些宏被称为:
•OS_PRETASKHOOK
•OS_POSTTASKHOOK
FUNC(void, OS_CALLOUT_CODE) PreTaskHook (void)
{
/* Your code */
}
Example 2.11: Conditional Compilation of PreTaskHook
这允许编写代码,其中可以有条件地编译钩子,如示例2.11所示。
2.15 通过抢占保存硬件寄存器Saving Hardware Registers across Preemption
RTA-OS在上下文切换时尽可能少地保存上下文—只保存操作系统正确操作的上下文。但是,可能会发现需要在运行时保存和恢复附加的依赖于应用程序的上下文。例如,可能有使用浮点寄存器的任务,因此需要通过上下文切换保存微控制器的浮点上下文。
可以选择使用PreTask和PostTask钩子和应用程序管理的堆栈手动实现这一点。但是,很难在不改变操作系统配置的情况下优化这种类型的实现。可以:
•始终保存每个交换机上的上下文到一个任务,然后在每个交换机上恢复。这个模型意味着你可能会做不必要的保存和恢复(例如,当切换到一个不使用它的任务时,保存一个寄存器集);或
•离线计算所需的保存,然后编写一个更复杂的钩子对,使用GetTaskID()/GetISRID()来计算是否需要保存/恢复。这个模型是脆弱的,因为对配置的更改,例如添加新的任务/ isr或修改优先级,将意味着需要重新工作。为了避免这些问题,RTA-OS提供了一种简单的通用机制,用于保存特定于用户的上下文和操作系统上下文。RTA-OS能够利用其优先级空间的knowl edge来精确计算哪些任务需要在运行时保存寄存器集,从而优化掉不必要的保存,节省上下文切换所需的时间和堆栈。例如:
•如果你只有一个任务或二类ISR使用一个给定的寄存器集,那么不需要保存或恢复。
•如果多个任务使用相同的寄存器集,但不能同时执行(因为它们不可抢占,共享内部资源或共享优先级),那么RTA-OS不需要保存寄存器集。
图2.21 Register saving in action
•上下文切换到使用寄存器集的最低优先级任务不需要进行保存,因为可以保证没有其他任务可以使用该集(因为如果高优先级任务正在使用寄存器集,则最低优先级任务不可能运行)。
•类似地,从使用寄存器集的最高优先级任务进行上下文切换不需要进行保存,因为没有更高优先级的任务使用寄存器集,因此不会破坏上下文。
图2.21显示了由任务1、3和5共享的寄存器集。可以看到,当不需要保存时(当切换到不使用寄存器集的任务时),就不会进行上下文保存。
需要保存的每个寄存器集都需要在配置时声明给RTA-OS。Rtaosgen使用声明定义两个回调函数,必须提供它们来保存和恢复寄存器集。图2.22显示了三个寄存器集的定义。
使用寄存器集的每个任务都需要在运行时声明这一点,以便rtaosgen可以计算需要保存的最大集数。图2.23显示了如何为任务执行此操作。
RTA-OS不知道如何或在哪里保存和恢复寄存器集——它只知道需要保存多少次以及何时保存和重新存储它们。对于定义的每个寄存器集,RTA-OS生成一个宏OS_REGSET_
图2.22 寄存器集定义
图23. 在一个任务中使用寄存器集
typedef volatile uint32 RegType;
uint32 VolatileRegisterSaveArea[OS_REGSET_VolatileRegister_SIZE];
FUNC(void, OS_CALLOUT_CODE)
Os_Cbk_RegSetSave_VolatileRegister(Os_RegSetDepthType Depth) {
VolatileRegisterSaveArea[Depth] = VOLATILEREGISTER;
}
FUNC(void, OS_CALLOUT_CODE)
Os_Cbk_RegSetRestore_VolatileRegister(Os_RegSetDepthType Depth) {
VOLATILEREGISTER = VolatileRegisterSaveArea[Depth];
}
Example 2.12: Register Set Save And Restore
你还需要为保存和恢复操作提供回调函数:
•Os_Cbk_RegSetSave_
•Os_Cbk_RegSetRestore_
两个回调函数都传递了一个Depth值,该值指示要保存或恢复的寄存器集。
例2.12显示了回调函数应该如何出现在你的代码中。
2.16 小结
• 任务是一个并发活动。
• 有两类任务:基本任务和扩展任务。
• 任务可以共享优先级,但建议不要这样做。
• 根据优先级安排任务。
• 当一个高优先级的任务准备好运行时,它将抢占低优先级的任务,但它不会抢占任何已配置为非抢占的任务。
• 任务以就绪、运行、挂起或等待状态存在(但是,只有已扩展的任务可以进入等待状态)。
• 如果一个任务终止,它必须调用TerminateTask()或ChainTask(TaskID)来终止。
• 所有任务在其入口函数中终止的系统可以使用“快速终止”优化来最小化堆栈使用和上下文切换时间。
• 任务只能在处于挂起状态时被激活,除非指定了多个激活。• PreTask和PostTask钩子允许你在任务开始前和结束后执行代码。这可用于在运行时分析应用程序。