TODO(未完待续)
- 核心调度器的调度实现部分介绍完成
- 时间片的处理介绍完成
- 任务切换处理介绍完成
- 空闲任务未完成
- 定时器任务未介绍完成
- 通信方式实现未介绍完成
freertos概述
freertos属于小系统实时操作系统,支持多任务实时操作系统。多任务通过链表实现连接存储,通过时间片完成任务切换。
freertos关键变量说明
PRIVILEGED_DATA static volatile UBaseType_t uxCurrentNumberOfTasks = ( UBaseType_t ) 0U;
PRIVILEGED_DATA static volatile TickType_t xTickCount = ( TickType_t ) 0U;
PRIVILEGED_DATA static volatile UBaseType_t uxTopReadyPriority = tskIDLE_PRIORITY;
PRIVILEGED_DATA static volatile BaseType_t xSchedulerRunning = pdFALSE;
PRIVILEGED_DATA static volatile UBaseType_t uxPendedTicks = ( UBaseType_t ) 0U;
PRIVILEGED_DATA static volatile BaseType_t xYieldPending = pdFALSE;
PRIVILEGED_DATA static volatile BaseType_t xNumOfOverflows = ( BaseType_t ) 0;
PRIVILEGED_DATA static UBaseType_t uxTaskNumber = ( UBaseType_t ) 0U;
PRIVILEGED_DATA static volatile TickType_t xNextTaskUnblockTime = ( TickType_t ) 0U; /* Initialised to portMAX_DELAY before the scheduler starts. */
PRIVILEGED_DATA static TaskHandle_t xIdleTaskHandle = NULL; /*< Holds the handle of the idle task. The idle task is created automatically when the scheduler is started. */
- pxCurrentTCB : 当前任务TCB描述符
- uxCurrentNumberOfTasks:当前就绪列表任务数量
- xTickCount: systick的中断计数
- uxTopReadyPriority: 当前运行就绪任务的优先级,即是目前最高的优先级。
- xSchedulerRunning: 调度器运行标志位
- uxPendedTicks: 悬挂时的ticks统计(也是通过systick的中断计数统计)
- xYieldPending:
- xNumOfOverflows:溢出列表对应的任务数
- uxTaskNumber:记录所有任务数
- xNextTaskUnblockTime:阻塞任务的下次到达时间,也对应延时任务列表中任务最小触发时间。
- xIdleTaskHandle:空闲任务句柄
任务类型
- idle任务
- 定时器任务
- 线程任务
任务管理列表
PRIVILEGED_DATA TCB_t * volatile pxCurrentTCB = NULL;
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
PRIVILEGED_DATA static List_t xDelayedTaskList1;
PRIVILEGED_DATA static List_t xDelayedTaskList2;
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList;
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList;
PRIVILEGED_DATA static List_t xPendingReadyList;
任务存储链表类型:list_t
- 就绪任务链表:用于存储当前系统可以抢占和调度的任务。每个优先级存在一个链表,一个链表用于存储同一优先级的所有任务,通过xtaskCreate创建,在插入链表是不分先后,默认插入到链表末尾。
就绪任务链表:pxReadyTasksLists[configMAX_PRIORITIES]
最大优先级配置,最大优先级通过配置宏configMAX_PRIORITIES实现。 - 延时任务队列链表:用于存储执行延时调度的任务。通过vTaskDelay实现,即是osDealy函数。任务延时。延时任务队列链表分为两个,一个为延时任务队列链表,另外一个为溢出延时队列链表。
延时任务队列链表:xDelayedTaskList1 --> pxDelayedTaskList
溢出延时队列链表:xDelayedTaskList2 --> pxOverflowDelayedTaskList
设置延时时,会通过延时使得当前任务被加入到延时列表中。使用唤醒时间(链表的xItemValue=当前时间计数 + 延时时间)作为key来决定插入链表的位置。最开始位置保持为延时时间节点最小的任务,链表开始到结束任务需要等待的时间依次增加。当任务的唤醒时间小于等于当前时间计数时,则任务添加到溢出延时任务列表,否则任务放入延时任务列表;当系统时间计数溢出时,延时任务列表和溢出延时任务列表交换。
通过xNextTaskUnblockTime及链表延时任务的xItemValue决定延时任务是否执行。xNextTaskUnblockTime表示当前最近的延时任务触发的时间计数。而链表延时任务的xItemValue表示了对应任务的延时触发时间计数。 - xPendingReadyList
- xTasksWaitingTermination
- xSuspendedTaskList
任务内存分配
每个任务存在两片内存,一片内存为任务描述符,代码中使用TCB_t表示; 另外一片内存为任务栈pxStack。
freertos中根据内存的生长方向,将内存分为两种分配方式。
- 向下生长
- 向上生长
任务描述符
任务描述符:TCB_t --> tskTCB
- stack
- 默认填充内容:0xa5UL
- 4 * stack_size, 4字节对齐
- 向上生长或向下生长(默认生长方向)
- xStateListItem :状态链表项:调度器通过把任务TCB中的状态列表项xStateListItem作为链表节点将任务添加到就绪、阻塞、挂起列表
- 初始化0x5a5a5a5a
- pvOwner = pxNewTCB
- xEventListItem:事件链表项:用作多任务建通信方式,如事件,队列等
- 初始化0x5a5a5a5a
- xItemValue = configMAX_PRIORITIES - uxPriority
- pvOwner = pxNewTCB
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack;/*指向任务栈的栈顶位置*/
ListItem_t xStateListItem;/*任务的状态列表项,用于表示任务状态(Ready, Blocked, Suspended ). */
ListItem_t xEventListItem; /*任务的时间列表项,用于描述任务的触发事件*/
UBaseType_t uxPriority; /* 当前任务的优先级 */
StackType_t *pxStack; /* 任务栈内存指针 */
char pcTaskName[ configMAX_TASK_NAME_LEN ];/* 任务名字 */
#if ( portSTACK_GROWTH > 0 )
StackType_t *pxEndOfStack; /* 当栈内存正向生长时,该指针指向栈底位置,用于判断是否栈溢出 */
#endif
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting; /*保存临界区嵌套深度*/
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber; /* tcb的数量 */
UBaseType_t uxTaskNumber; /* 任务数量 */
#endif
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority; /* 存任务的基础优先级 */
UBaseType_t uxMutexesHeld;
#endif
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif
#if( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter; /*< Stores the amount of time the task has spent in the Running state. */
#endif
} tskTCB;
任务栈
栈内存必须按4字节对齐。根据宏定义配置可通过pvPortMalloc申请,也可静态指定。
xTaskCreate函数动态申请内存,而其形参的栈大小usStackDepth对应的实际内存为4*usStackDepth字节,同时初始化将栈内存中的内容按字节全部填充为0xa5UL。当任务切换时,使用任务的栈顶的内存存储芯片寄存器用于保存现场,除去现场保存部分,剩余的栈内存用于任务应用的栈内存所需。
指针pxStack指向堆栈的起始位置,任务创建时会分配指定数目的任务堆栈,申请堆栈内存函数返回的指针就被赋给该变量。pxTopOfStack指向当任务栈栈顶,随着进栈出栈,pxTopOfStack指向的位置是会变化的。随着任务的运行,堆栈可能会溢出,在堆栈向下增长的系统中,使用pxStack变量和pxTopOfStack检查堆栈是否溢出;如果在堆栈向上增长的系统中,使用pxEndOfStack和pxTopOfStack来诊断是否堆栈溢出。
任务寄存器的存储位置
任务创建申请内存后,从栈顶位置(pxTopOfStack)开始依次向下存储芯片寄存器内容用于保存任务切换现场。参考如下表。
栈位置 | 对应寄存器 | 初始数据 | 说明 |
---|---|---|---|
0x0 | NULL | ||
-0x4 | XPSR | 0x01000000 | |
-0x8 | PC | pxCode | 任务函数指针用于给PC寄存器 |
-0xc | LR | prvTaskExitError | |
-0x10 | R12 | ||
-0x14 | R3 | ||
-0x18 | R2 | ||
-0x1c | R1 | ||
-0x20 | R0 | pvParameters | 任务参数 |
-0x24 | portINITIAL_EXEC_RETURN | 0xfffffffd | |
-0x28 | R11 | ||
-0x2c | R10 | ||
-0x30 | R9 | ||
-0x34 | R8 | ||
-0x38 | R7 | ||
-0x3c | R6 | ||
-0x40 | R5 | ||
-0x44 | R4 | ||
-0x48 | R7 | ||
其他 | 任务应用栈区 |
- 上述表中数据空的位置表示为无具体含义
- 栈顶内存对于现场存储的具体分配取决于实际的MCU控制芯片,上述表中数据参考于ARM_CM4芯片
优先级处理
在任务描述符中使用uxPriority表示任务优先级,该值越大,则表示任务优先级越高。任务的最高优先级是通过FreeRTOSConfig.h文件中的configMAX_PRIORITIES配置,可以使用的任务优先级范围是0~configMAX_PRIORITIES-1,当设置任务优先级大于等于configMAX_PRIORITIES时,则任务优先级被设置为configMAX_PRIORITIES-1。其中0优先级任务有系统设置为idle任务,即是空闲任务。由于每一个优先级会定义一个任务就绪列表,故configMAX_PRIORITIES的值越大,也就意味着任务就绪列表数组越大,占用资源越多。
uxTopReadyPriority用于统计就绪列表最高任务优先级,在将任务加入就绪列表是根据加入任务优先级调整该值,并在后续任务切换时选择最高优先级使用。
freertos内核使用了arm芯片提供的3个中断,分别是penSV、SVC和systick中断,这禅个硬件的中断优先级会设置为芯片最低的任务优先级,以确保其他硬件中断能够的到完整执行不被打断,同时也避免中断中发生任务切换导致终端中出现宕机。
freertos任务调度中断的函数
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
- penSV : 用于实现任务切换
- SVC : 用于发起任务切换,但并不是绝对使用。只有当在中断中手动调度时使用该调度。
- systick中断 :通过时间片实现最高优先级任务切换,或者osDelay时间到达触发任务切换。
中断的实现将在下面具体介绍。
freertos调用流程
系统启动初始化
- 时钟初始化:分系统时钟初始化,滴答定时器初始化,定时器初始化。这里存在两个定时器初始化,一个用于freertos,一个用于芯片外设库。
外设时钟定时器的时钟由外设需求决定,如stm32的时钟为1ms
freertos使用定时器作为系统操作时基和节拍。决定系统响应的速率(实时的颗粒度)以及任务切换的最小时间片- 创建任务时并不会立刻执行相应的任务,只会将任务加入到就序列表,在调用vTaskStartScheduler之前或者在vTaskEndScheduler之后都不会执行创建任务。任务是否执行由xSchedulerRunning变量决定,该变量默认false,只有当vTaskStartScheduler运行时才调整为true,当vTaskEndScheduler运行时将其调整为false。在vTaskStartScheduler到vTaskEndScheduler之间创建任务,会将任务直接添加到就绪列表,并判断当前优先级是否高于uxTopReadyPriority,来决定任务是否立马调度执行。
- vTaskStartScheduler会初始空闲任务(prvIdleTask),定时器任务(prvTimerTask,通过configUSE_TIMERS配置使用),初始化xNextTaskUnblockTime和xTickCount的值,初始化svc中断、penSV中断、定时器中断,并通过"svc 0"触发svc中断完成第一个任务调度。只要存在任务,且不停止调度器,则该函数不会继续向下执行,永久停留在这里,并该函数不会退出。
- 设置penSV和systick同样优先级,svc更高一级优先级
- uxCriticalNesting表示临界区嵌套层数
通过汇编指令svc 0是的程序触发svc中断。
任务创建
taskYIELD_IF_USING_PREEMPTION即是portYIELD_WITHIN_API的宏定义。主要目的用于触发pendSV中断。
#ifndef portYIELD_WITHIN_API
#define portYIELD_WITHIN_API portYIELD
#endif
#define portSVC_YIELD 1
#define portYIELD() __asm{ SVC portSVC_YIELD } /* portYIELD_FROM_ISR使用 */
#define portSY_FULL_READ_WRITE ( 15 )
#define portYIELD_WITHIN_API() \
{ \
/* Set a PendSV to request a context switch. */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
\
/* Barriers are normally not required but do ensure the code is completely \
within the specified behaviour for the architecture. */ \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
任务的时间片处理
触发penSV中断的实现:portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT
关闭中断(portDISABLE_INTERRUPTS):__asm volatile( “cpsid i” )
开启中断 (portENABLE_INTERRUPTS) : __asm volatile( “cpsie i” )
xTaskIncrementTick函数用于判断是否需要发生任务调度,主要判断3部分功能是否调度。
uxSchedulerSuspended
延时调度 : xNextTaskUnblockTime和xTickCount
同优先级任务调度 : configUSE_PREEMPTION和configUSE_TIME_SLICING
执行tick_hook函数 : configUSE_TICK_HOOK
抢占调度:configUSE_PREEMPTION xYieldPending
函数返回值表示是否需要发生任务切换
通过宏configUSE_TICK_HOOK控制
taskSWITCH_DELAYED_LISTS: 实现延时任务列表和溢出延时任务列表指针交互,同时复位xNextTaskUnblockTime值
编译条件:configUSE_PREEMPTION == 1 ,configUSE_TIME_SLICING==1
–> f{configUSE_PREEMPTION & xYieldPending} --true–> 抢占调度
xTickCount : 表示进入systick的节拍数,不表示具体的时间,只是一个相对的时间。如果systick中断触发为1ms,则对应xTickCount的值就是毫秒计数。但如果systick中断的触发不是1ms,则xTickCount只表示对应进入中断次数,不再对应毫秒数。同理,由于freertos中所有与时间有关的动作都以此计数作为时间判断,故所有时间都不一定为毫秒级,都是通过该值作为可控时间粒度进行系统度量。
又由于systick是硬件中断优先级配置为最低,也就意味着当发生其他硬件中断时,其他中断执行的时间越短,这里时间片的精度也就越高。同样则也意味着系统的时间片永远不是精确的,始终会受到应用层代码的影响。
由于freertos更加偏向于小型控制器操作系统,常规使用systick即为1ms一次进入中断,这并不是对所有系统都是友好的。当前控制器芯片的主频范围可能从十几兆到上G,对于一些成本很低的产品,则控制器的主频很低,这时systick的1ms中断对于系统将变成很大的开销,这并不是开发中希望的;对于一些特殊应用要求也是类似,为了追求更好的性能,则不希望在系统的切换上花费过多的开销。
任务delay的实现
vTaskSuspendAll:表示暂停任务调度器,通过uxSchedulerSuspended=1来实现
任务的切换过程
freertos中任务切换使用的是penSV和svc。
其中调度器在需要时直接发起pendSV中断请求,然后再penSV中完成栈的pop和push,以及前一个任务的现场保存。而svc更多用于现场用户主动调度。
pendSV
vTaskSwitchContext之前代码在进行即将运行结束的任务进行现场保存,包含FPU寄存器,pc等控制器寄存器,栈指针的寄存器保存,用于下次再运行该任务时恢复现场使用,即是vTaskSwitchContext之后的动作。vTaskSwitchContext之后的代码与之前代码执行是对称的,执行内容相同,但操作方向和时序相反,主要作用用于取出先前该任务执行结束时保存的现场,并将其赋予相应的寄存器,让控制器能够继续按上次运行结束的状态运行。
需要注意的是vTaskSwitchContext前后的pxCurrentTCB值是不同的,vTaskSwitchContext前的pxCurrentTCB指向任务切换前运行的任务,该任务即将切出不运行;vTaskSwitchContext后的pxCurrentTCB指向任务切换后运行的任务,该任务即是将要运行的任务。vTaskSwitchContext主要作用即是切换任务,将现在运行的任务关闭,找到即将要运行的任务并将任务tcb赋予pxCurrentTCB变量。
isb : 指令执行屏障,确保先前指令执行完成
- taskSELECT_HIGHEST_PRIORITY_TASK用于获取就绪列表数组中最高优先级任务,同时更新最高优先级记录(uxTopReadyPriority)
- 计算总运行时间(ulTotalRunTime): 通过定义更精确的时间基准来统计运行时间,该时间可后续用于计算CPU使用率和其他一些状态。通过宏configGENERATE_RUN_TIME_STATS和portALT_GET_RUN_TIME_COUNTER_VALUE控制选择具体的接口,且接口portALT_GET_RUN_TIME_COUNTER_VALUE和portALT_GET_RUN_TIME_COUNTER_VALUE由用户自定义如何计算时间。
svc
上述过程与pendSV中断处理函数中vTaskSwitchContext执行的内容基本相同都是用于现场恢复。svc中断时通过软件触发的,通常用于开始运行任务调度器时才会调度该函数,因为调度器开始前本就没必要进行现场数据保存。
定时器任务(后续添加)
xTimerCreateTimerTask
configTIMER_TASK_PRIORITY