1. 任务的基础知识
1.1 前后台和多任务系统
- 在裸机程序中,一般方式是在main()函数的while(1)中循环执行所有的程序,有时,使用中断去执行一些紧急的功能,中断中执行的程序称为前台,main()函数中执行的程序称为后台。
- 对于前后台框架的程序,必须当前函数执行完,才会去执行下一个函数,其实时性比较差,但它耗用的资源少。
- 在实际项目中,main()中会有一个周期(如1ms)轮询去执行main()函数中所有函数,同时专门使用一个定时器以一定周期(如500us)执行比较重要的函数,对于实时性要求不是很精确的项目,这种框架还是可以满足。
- FreeRTOS是一个可剥夺性内核的多任务系统。在多任务系统中,高优先级可以打断低优先级,能让更高优先级的任务得到CPU资源,完成这种功能的是任务调度器,其任务调度器也是可剥夺性任务调度器。
1.2 协程(Co-routine)的特性
协程是为那些资源很少的MCU而做的,FreeRTOS 存在协程功能,但不再更新和维护。协程和任务在概念上是相似的,但有如下不同:
- 堆栈使用方面: 所有的协程使用同一个堆栈,这样消耗比较少的RAM,而使用FreeRTOS任务中,每个任务都有自己的堆栈。
- 调度器和优先级: 协程使用合作式的调度器,但是可以在使用抢占式的调度器中使用协程。
- 宏实现: 协程是通过宏定义来实现的。
- 使用限制: 为了降低对RAM 的消耗做了很多的限制。
1.3 任务和任务状态
在FreeRTOS中,可以将任务理解为进程。任务由FreeRTOS调度器调度,有优先级,有独立堆栈空间等。每个时间只能执行一个任务,具体执行那个任务,由FreeRTOS调度器决定,每个任务必须有一个任务状态,如下:
- 运行态: 当前正在运行的任务称为运行态,对于单核微控制器,运行态的任务有且仅有一个。
- 就绪态: 就绪态的任务在就绪列表中,表示当前无阻塞和挂起,只是有更高优先级的任务在执行,随时等待调度器调度。刚刚创建的任务处于就绪态。
- 阻塞态: 阻塞态的任务不在就绪列表中,不能被调度器调用。它表示正在等待一个外部事件,如队列,信号量,互斥量、事件标志组或通知等。阻塞态的任务一般会设置一个超时事件,外部事情不满足但超出时间,会退出阻塞态。
- 挂起态: 挂起态的任务不能被调度器调用,无超时时间。唯一退出挂起态的方法就是调用vTaskResume()函数或vTaskResumeFromISR()函数。
1.4 任务优先级
- 优先级数字越低,任务优先级越低,优先级处于0 ~ (configMAX_PRIORITIES-1)。
- 空闲任务的优先级最低,在启动调度器的时候被创建,任务优先级为0。
- 任务调度器确保处于就绪态和运行态的高优先级任务获取CPU所有权。
- 就绪态中,优先级相同的任务会使用时间片轮转调度器获取运行时间。
- configUSE_PORT_OPTIMISED_TASK_SELECTION设置为1时,通过硬件平台支持类似计算前导零指定获取下一个要运行的任务,优先级数量不能超过32级,即configMAX_PRIORITIES不能超过32。
1.5 任务控制块
每个任务都有一个任务控制块,本质是定义了一个结构体,包含了任务的所有信息,如堆栈的储存地址,优先级,状态列表和事件列表等等属性。在创建任务时,会给任务分配一个任务控制块TCB_t,用来储存任务的所有属性。该任务控制块结构体在task.c中定义,如下:
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* 栈顶指针,中断或任务切换时,会对任务压栈或入栈,该指针指向SP */
/* 启用MPU的情况下设置 */
#if ( portUSING_MPU_WRAPPERS == 1 )
/* 设置任务访问的内存权限 */
xMPU_SETTINGS xMPUSettings;
#endif
ListItem_t xStateListItem; /* 状态列表任务会处于不同的状态,该项会被插入到对应的链表,如就绪列表、延时列表和挂起列表 */
ListItem_t xEventListItem; /* 事件列表,表示等待的具体事件,如等待queue资源 */
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
/* trace或debug时候用到 */
#if ( configUSE_TRACE_FACILITY == 1 )
/* 用于调试,表示本任务是第几个创建,每创建一个任务,系统有一个全局变量加1,并将该值赋给新任务 */
UBaseType_t uxTCBNumber;
/* 调试用,用户通过API函数vTaskSetTaskNumber()来设置,数值由函数参数指定 */
UBaseType_t uxTaskNumber;
#endif
/* 如果使用任务互斥量信号 */
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority; /* 优先级提升前,保存原优先级,优先级反转时候用到 */
UBaseType_t uxMutexesHeld; /* 任务获取的互斥信号量个数,为0时,将其优先级还原到初始优先级*/
#endif
/* 如果使能任务标签功能 */
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif
/*
* 与本地储存有关
* 有pvTaskGetThreadLocalStoragePointer和vTaskSetThreadLocalStoragePointer配合使用
*/
#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
void *pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
#endif
#if( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter; /* 记录任务运行的总时间,实现获取任务cpu占用率信息*/
#endif
/*
* 如果使用Newlib运行库
* 为了保证重入性,就不得不为每个任务实现一套struct _reent xNewLib_reent,默认使用的glibc
* 你的操作系统资源不够,而不得不选择newlib,就必须打开该宏
*/
#if ( configUSE_NEWLIB_REENTRANT == 1 )
struct _reent xNewLib_reent; /* 定义一个newlib结构体变量 */
#endif
/* 与任务通知相关 */
#if( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue; /* 任务通知值 */
volatile uint8_t ucNotifyState; /* 任务通知状态 */
#endif
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
/*
* 标记任务是动态创建还是静态创建
* 动态创建时,为pdTURE;静态创建时,为pdFALSE。
*/
uint8_t ucStaticallyAllocated;
#endif
#if( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif
} tskTCB;
/* 新版本将任务控制块重命名为TCB_t,旧版本定义为takTCB,兼容旧版本 */
typedef tskTCB TCB_t;
1.6 任务堆栈
- 在创建任务时,需要指定任务堆栈深度ulStackDepth,该值是uint32_t类型,即实际定义的堆栈大小为 4 * ulStackDepth。内存申请函数参数为size_t类型,即堆栈能分配的最大字节数为类型size_t能表示的最大数字。
- 函数xTaskCreate()动态创建任务时,只需要定义任务堆栈深度ulStackDepth,堆栈空间函数自动创建,函数xTaskCreateStatic()静态方法创建需要自定义任务堆栈,然后将首地址作为参数参数带入。
- 在任务切换时,栈用来保存任务现场,如CPU 寄存器值。如下次任务恢复时,从栈中恢复现场值。
1.7 任务相关的全局变量
/* 指向当前正在运行的任务 */
PRIVILEGED_DATA TCB_t * volatile pxCurrentTCB = NULL;
/*
* 定义了configMAX_PRIORITIES 个就绪列表,按照优先级进行排序
* 不同优先级的任务挂在不同的就绪列表中
* 在调度时就很方便从优先级高的就绪表中先进行调度
*/
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
/* 定义了两个延时列表,任务延时功能用了两个延时列表 */
PRIVILEGED_DATA static List_t xDelayedTaskList1;
PRIVILEGED_DATA static List_t xDelayedTaskList2;
/*
* 为两个延时列表定义了两个指针
* pxDelayedTaskList 指向 xDelayedTaskList1
* pxOverflowDelayedTaskList 指向 xDelayedTaskList2
*/
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList;
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList;
/* 任务准备就绪,但是调度器挂起,则将该任务放入列表,在xTaskResumeAll时,重新放入就绪表中*/
PRIVILEGED_DATA static List_t xPendingReadyList;
#if( INCLUDE_vTaskDelete == 1 )
/* 等待结束的任务,对于一些特殊删除的任务,如任务删除本身,因为还没有执行完,不能立即释放空间,则会先放在此列表中 */
PRIVILEGED_DATA static List_t xTasksWaitingTermination;
PRIVILEGED_DATA static volatile UBaseType_t uxDeletedTasksWaitingCleanUp = ( UBaseType_t ) 0U;
#endif
#if ( INCLUDE_vTaskSuspend == 1 )
/* 任务挂起列表 */
PRIVILEGED_DATA static List_t xSuspendedTaskList;
#endif
/* 当前任务的总数 */
PRIVILEGED_DATA static volatile UBaseType_t uxCurrentNumberOfTasks = ( UBaseType_t ) 0U;
/* 系统开始后所有的tick计数 */
PRIVILEGED_DATA static volatile TickType_t xTickCount = ( TickType_t ) 0U;
/*
* 记录当前就绪表中优先级最高的任务,32位的无符号整形数据,表示最高32个优先级
* 需要将configUSE_PORT_OPTIMISED_TASK_SELECTION置1,才有效
*/
PRIVILEGED_DATA static volatile UBaseType_t uxTopReadyPriority = tskIDLE_PRIORITY;
/* 表示调度器是否运行 */
PRIVILEGED_DATA static volatile BaseType_t xSchedulerRunning = pdFALSE;
/* 暂停调度器期间,记录未被处理的ticks个数 */
PRIVILEGED_DATA static volatile UBaseType_t uxPendedTicks = ( UBaseType_t ) 0U;
/* 在某种临界状态下,任务状态发生改变,需要等待从新调度 */
PRIVILEGED_DATA static volatile BaseType_t xYieldPending = pdFALSE;
/* 记录tick计数翻转的次数 */
PRIVILEGED_DATA static volatile BaseType_t xNumOfOverflows = ( BaseType_t ) 0;
/* 记录全局任务数,为新建的任务分配一个TaskNumber */
PRIVILEGED_DATA static UBaseType_t uxTaskNumber = ( UBaseType_t ) 0U;
/* 记录了延时链表中,第一个需要被唤醒的任务时间点 */
PRIVILEGED_DATA static volatile TickType_t xNextTaskUnblockTime = ( TickType_t ) 0U;
/* 表示空闲任务的句柄 */
PRIVILEGED_DATA static TaskHandle_t xIdleTaskHandle = NULL;
/* 调度器暂定标志 */
PRIVILEGED_DATA static volatile UBaseType_t uxSchedulerSuspended = ( UBaseType_t ) pdFALSE;
#if ( configGENERATE_RUN_TIME_STATS == 1 )
PRIVILEGED_DATA static uint32_t ulTaskSwitchedInTime = 0UL; /* 用于记住任务切换的时间 */
PRIVILEGED_DATA static uint32_t ulTotalRunTime = 0UL; /* 系统的总运行时间 */
#endif