从系统的角度看,任务是竞争系统资源的最小运行单元。FreeRTOS 是一个支持多任务的操作系统。在 FreeRTOS 中,任务可以使用或等待CPU、使用内存空间等系统资源,并独 立 于 其 它 任 务 运 行 , 任 何 数 量 的 任 务 可 以 共 享 同 一 个 优 先 级 , 如 果 宏configUSE_TIME_SLICING 定义为 1,处于就绪态的多个相同优先级任务将会以时间片切换的方式共享处理器。
简而言之: FreeRTOS 的任务可认为是一系列独立任务的集合。每个任务在自己的环境中运行。在任何时刻,只有一个任务得到运行,FreeRTOS 调度器决定运行哪个任务。调度器会不断的启动、停止每一个任务,宏观看上去所有的任务都在同时在执行。作为任务,不需要对调度器的活动有所了解,在任务切入切出时保存上下文环境(寄存器值、堆栈内容)是调度器主要的职责。为了实现这点,每个 FreeRTOS 任务都需要有自己的栈空间。
当任务切出时,它的执行环境会被保存在该任务的栈空间中,这样当任务再次运行时,就能从堆栈中正确的恢复上次的运行环境,任务越多,需要的堆栈空间就越大,而一个系统能运行多少个任务,取决于系统的可用的 SRAM。
FreeRTOS 的可以给用户提供多个任务单独享有独立的堆栈空间,系统可用决定任务的状态,决定任务是否可以运行,同时还能运用内核的 IPC 通信资源,实现了任务之间的通信,帮助用户管理业务程序流程。这样用户可以将更多的精力投入到业务功能的实现中。
FreeRTOS 中的任务是抢占式调度机制,高优先级的任务可打断低优先级任务,低优先级任务必须在高优先级任务阻塞或结束后才能得到调度。同时 FreeRTOS 也支持时间片轮转调度方式,只不过时间片的调度是不允许抢占任务的 CPU 使用权。
任务通常会运行在一个死循环中,也不会退出,如果一个任务不再需要,可以调用FreeRTOS 中的任务删除 API 函数接口显式地将其删除。
FreeRTOS 中提供的任务调度器是基于优先级的全抢占式调度:在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的。系统理论上可以支持无数个优先级(0 ~ N,优先级数值越小的任务优先级越低,0 为最低优先级,分配给空闲任务使用,一般不建议用户来使用这个优先级。假如使能了 configUSE_PORT_OPTIMISED_TASK_SELECTION 这个宏(在 FreeRTOSConfig.h 文件定义),一般强制限定最大可用优先级数目为 32。在一些资源比较紧张的系统中,可以根据实际情况选择只支持 8 个或 32 个优先级的系统配置。在系统中,当有比当前任务优先级更高的任务就绪时,当前任务将立刻被换出,高优先级任务抢占处理器运行。
一个操作系统如果只是具备了高优先级任务能够“立即”获得处理器并得到执行的特点,那么它仍然不算是实时操作系统。因为这个查找最高优先级任务的过程决定了调度时间是否具有确定性,例如一个包含 n 个就绪任务的系统中,如果仅仅从头找到尾,那么这个时间将直接和 n 相关,而下一个就绪任务抉择时间的长短将会极大的影响系统的实时性。
FreeRTOS 内核中采用两种方法寻找最高优先级的任务,第一种是通用的方法,在就绪链表中查找从高优先级往低查找 uxTopPriority,因为在创建任务的时候已经将优先级进行排序,查找到的第一个 uxTopPriority 就是我们需要的任务,然后通过 uxTopPriority 获取对应的任务控制块。
第二种方法则是特殊方法,利用计算前导零指令 CLZ,直接在uxTopReadyPriority 这个 32 位的变量中直接得出 uxTopPriority,这样子就知道哪一个优先级任务能够运行,这种调度算法比普通方法更快捷,但受限于平台
FreeRTOS 内核中也允许创建相同优先级的任务。相同优先级的任务采用时间片轮转方式进行调度(也就是通常说的分时调度器),时间片轮转调度仅在当前系统中无更高优先级就绪任务存在的情况下才有效。为了保证系统的实时性,系统尽最大可能地保证高优先级的任务得以运行。任务调度的原则是一旦任务状态发生了改变,并且当前运行的任务优先级小于优先级队列组中任务最高优先级时,立刻进行任务切换(除非当前系统处于中断处理程序中或禁止任务切换的状态)。
FreeRTOS 系统中的每一个任务都有多种运行状态
(1):创建任务→就绪态(Ready):任务创建完成后进入就绪态,表明任务已准备就绪,随时可以运行,只等待调度器进行调度。
(2):就绪态→运行态(Running):发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运行态。
(3):运行态→就绪态:有更高优先级任务创建或者恢复后,会发生任务调度,此刻就绪列表中最高优先级任务变为运行态,那么原先运行的任务由运行态变为就绪态,依然在就绪列表中,等待最高优先级的任务运行完毕继续运行原来的任务(此处可以看做是 CPU 使用权被更高优先级的任务抢占了)。
(4):运行态→阻塞态(Blocked):正在运行的任务发生阻塞(挂起、延时、读信号量等待)时,该任务会从就绪列表中删除,任务状态由运行态变成阻塞态,然后发生任务切换,运行就绪列表中当前最高优先级任务。
(5):阻塞态→就绪态:阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态;如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。
(6) (7) (8):就绪态、阻塞态、运行态→挂起态(Suspended):任务可以通过调用 vTaskSuspend() API 函数都可以将处于任何状态的任务挂起,被挂起的任务得不到CPU 的使用权,也不会参与调度,除非它从挂起态中解除。
(9):挂起态→就绪态:把 一 个 挂 起 状态 的 任 务 恢复的 唯 一 途 径 就 是调 用 vTaskResume() 或 vTaskResumeFromISR() API 函数,如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。
FreeRTOS 系统中的每一任务都有多种运行状态。系统初始化完成后,创建的任务就可以在系统中竞争一定的资源,由内核进行调度。
任务状态通常分为以下四种:
就绪(Ready):该任务在就绪列表中,就绪的任务已经具备执行的能力,只等待调度器进行调度,新创建的任务会初始化为就绪态。
运行(Running):该状态表明任务正在执行,此时它占用处理器,FreeRTOS 调度器选择运行的永远是处于最高优先级的就绪态任务,当任务被运行的一刻,它的任务状态就变成了运行态。
阻塞(Blocked):如果任务当前正在等待某个时序或外部中断,我们就说这个任务处于阻塞状态,该任务不在就绪列表中。包含任务被挂起、任务被延时、任务正在等待信号量、读写队列或者等待读写事件等。
挂起态(Suspended):处于挂起态的任务对调度器而言是不可见的,让一个任务进入挂起状态的唯一办法就是调用 vTaskSuspend()函数;而 把 一 个 挂 起 状态 的任 务 恢复的 唯 一 途 径 就 是 调 用 vTaskResume() 或 vTaskResumeFromISR()函数,我们可以这么理解挂起态与阻塞态的区别,当任务有较长的时间不允许运行的时候,我们可以挂起任务,这样子调度器就不会管这个任务的任何信息,直到我们调用恢复任务的 API 函数;而任务处于阻塞态的时候,系统还需要判断阻塞态的任务是否超时,是否可以解除阻塞。
任务挂起函数
vTaskSuspend()
任务可以通过调用 vTaskSuspend()函数都可以将处于任何状态的任务挂起,被挂起的任务得不到 CPU 的使用权,也不会参与调度,它相对于调度器而言是不可见的,除非它从挂起态中解除。
vTaskSuspendAll()
将所有的任务都挂起,就是挂起任务调度器。调度器被挂起后则不能进行上下文切换,但是中断还是使能的。 当调度器被挂起的时候,如果有中断需要进行上下文切换, 那么这个中断将会被挂起,在调度器恢复之后才响应这个中断。
任务恢复函数
任务恢复就是让挂起的任务重新进入就绪状态,恢复的任务会保留挂起前的状态信息,在恢复的时候根据挂起时的状态继续运行。
xTaskResumeFromISR()与 vTaskResume()一样都是用于恢复被挂起的任务,不一样的是 xTaskResumeFromISR() 专 门 用 在 中 断 服 务 程 序 中 。 无 论 通 过 调 用 一 次 或 多 次vTaskSuspend()函数而被挂起的任务,也只需调用一次 xTaskResumeFromISR()函数即可解挂 。 要 想 使 用 该 函 数 必 须 在 FreeRTOSConfig.h 中 把 INCLUDE_vTaskSuspend 和
INCLUDE_vTaskResumeFromISR 都定义为 1 才有效。任务还没有处于挂起态的时候,调用xTaskResumeFromISR()函数是没有任何意义的
任务删除函数 vTaskDelete()
vTaskDelete()用于删除一个任务。当一个任务删除另外一个任务时,形参为要删除任务创建时返回的任务句柄,如果是删除自身, 则形参为 NULL。删除的任务将从所有就绪,阻塞,挂起和事件列表中删除.
任务延时函数
void vTaskDelay( const TickType_t xTicksToDelay )
vTaskDelay()在我们任务中用得非常之多,每个任务都必须是死循环,并且是必须要有阻塞的情况,否则低优先级的任务就无法被运行了。
延时的时长由形参 xTicksToDelay 决定,单位为系统节拍周期。
1 #if ( INCLUDE_vTaskDelayUntil == 1 )
2
3 void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
4 const TickType_t xTimeIncrement );
在 FreeRTOS 中,除了相对延时函数,还有绝对延时函数vTaskDelayUntil(),这个绝对延时常用于较精确的周期运行任务,比如我有一个任务,希望它以固定频率定期执行,而不受外部的影响,任务从上一次运行开始到下一次运行开始的时间间隔是绝对的,而不是相对的
vTaskDelayUntil() 与 vTaskDelay () 一 样 都 是 用 来 实 现 任 务 的 周 期 性 延 时 。 但vTaskDelay ()的延时是相对的,是不确定的,它的延时是等 vTaskDelay ()调用完毕后开始计算的。并且 vTaskDelay ()延时的时间到了之后,如果有高优先级的任务或者中断正在执行,被延时阻塞的任务并不会马上解除阻塞,所有每次执行任务的周期并不完全确定。
而vTaskDelayUntil()延时是绝对的,适用于周期性执行的任务。当(*pxPreviousWakeTime +xTimeIncrement)时间到达后,vTaskDelayUntil()函数立刻返回,如果任务是最高优先级的,那么任务会立马解除阻塞,所以说 vTaskDelayUntil()函数的延时是绝对性的.
在设计之初就应该考虑下面几点因素:任务运行的上下文环境、任务的执行时间合理设计。
FreeRTOS 中程序运行的上下文包括:
中断服务函数。
普通任务。
空闲任务。
中断服务函数是一种需要特别注意的上下文环境,它运行在非任务的执行环境下(一般为芯片的一种特殊运行模式(也被称作特权模式)),在这个上下文环境中不能使用挂起当前任务的操作,不允许调用任何会阻塞运行的 API 函数接口。另外需要注意的是,中断服务程序最好保持精简短小,快进快出,一般在中断服务函数中只做标记事件的发生,然后通知任务,让对应任务去执行相关处理,因为中断服务函数的优先级高于任何优先级的任务,如果中断处理时间过长,将会导致整个系统的任务无法正常运行。所以在设计的时候必须考虑中断的频率、中断的处理时间等重要因素,以便配合对应中断处理任务的工作。
任务看似没有什么限制程序执行的因素,似乎所有的操作都可以执行。但是做为一个优先级明确的实时系统,如果一个任务中的程序出现了死循环操作(此处的死循环是指没有阻塞机制的任务循环体),那么比这个任务优先级低的任务都将无法执行,当然也包括了空闲任务,因为死循环的时候,任务不会主动让出 CPU,低优先级的任务是不可能得到CPU 的使用权的.
所以在进行任务设计时,就应该保证任务在不活跃的时候,任务可以进入阻塞态以交出 CPU 使用权,这就需要我们自己明确知道什么情况下让任务进入阻塞态,保证低优先级任务可以正常运行。在实际设计中,一般会将紧急的处理事件的任务优先级设置得高一些。
空闲任务(idle 任务)是 FreeRTOS 系统中没有其他工作进行时自动进入的系统任务。因为处理器总是需要代码来执行——所以至少要有一个任务处于运行态。FreeRTOS 为了保证这一点,当调用 vTaskStartScheduler()时,调度器会自动创建一个空闲任务,空闲任务是一个非常短小的循环。用户可以通过空闲任务钩子方式,在空闲任务上钩入自己的功能函数。通常这个空闲任务钩子能够完成一些额外的特殊功能,例如系统运行状态的指示,系统省电模式等。除了空闲任务钩子,FreeRTOS 系统还把空闲任务用于一些其他的功能,比如当系统删除一个任务或一个动态任务运行结束时,在执行删除任务的时候,并不会释放任务的内存空间,只会将任务添加到结束列表中,真正的系统资源回收工作在空闲任务完成,空闲任务是唯一一个不允许出现阻塞情况的任务,因为 FreeRTOS 需要保证系统永远都有一个可运行的任务。
对于空闲任务钩子上挂接的空闲钩子函数,它应该满足以下的条件:
永远不会挂起空闲任务;
不应该陷入死循环,需要留出部分时间用于系统处理系统资源回收。
任务的执行时间一般是指两个方面,一是任务从开始到结束的时间,二是任务的周期。
在系统设计的时候这两个时间候我们都需要考虑,例如,对于事件 A 对应的服务任务Ta,系统要求的实时响应指标是 10ms,而 Ta 的最大运行时间是 1ms,那么 10ms 就是任务Ta 的周期了,1ms 则是任务的运行时间,简单来说任务 Ta 在 10ms 内完成对事件 A 的响应即可。此时,系统中还存在着以 50ms 为周期的另一任务 Tb,它每次运行的最大时间长度是 100us。在这种情况下,即使把任务 Tb 的优先级抬到比 Ta 更高的位置,对系统的实时性指标也没什么影响,因为即使在 Ta 的运行过程中,Tb 抢占了 Ta 的资源,等到 Tb 执行完毕,消耗的时间也只不过是 100us,还是在事件 A 规定的响应时间内(10ms),Ta 能够安全完成对事件 A 的响应。但是假如系统中还存在任务 Tc,其运行时间为 20ms,假如将 Tc的优先级设置比 Ta 更高,那么在 Ta 运行的时候,突然间被 Tc 打断,等到 Tc 执行完毕,那 Ta 已经错过对事件 A(10ms)的响应了,这是不允许的。所以在我们设计的时候,必须考虑任务的时间,一般来说处理时间更短的任务优先级应设置更高一些。