一,一些前置知识
1.什么是任务?
想象你的电脑就像一家餐厅,每个顾客点的菜就是一个任务。操作系统就像是餐厅的经理,负责安排厨师做菜、上菜,以及管理顾客的等候时间,确保每个顾客都能及时得到自己点的菜。 实际上任务在代码上的表现就是一个函数,不过这个函数不会返回,但可以通过中断被切换到别的函数,完成后又切换回来。这个解释起来比较麻烦,不理解没关系,看到下面的源码讲解你自能了解。
2.ARM-Cortex-M3的CPU寄存器情况
CM3 拥有通用寄存器 R0‐R15 以及一些特殊功能寄存器。R0‐R12 是最“通用目的”的,特殊功能寄存器有预定义的功能,而且必须通过专用的指令来访问。这里只做简单的介绍,需要更加详细的解读可以看这个网址FreeRTOS-ARM架构深入理解_freertos架构-CSDN博客的博客,该部分也摘自该博客,讲得十分详细。
R0-R12是通用的寄存器你可以将它们当中间变量使用,但我们有一些约定,一般用R0-R3来保存函数的形参,R3-R11是用来存一些函数内的局部变量或临时数据,R12用于暂存子程序之间传递的参数或临时数据。R13用来放栈指针,R14用来放返回地址,即调用指令的下一条指令的地址(发生函数的调用与中断靠这个回到原处),R15用来放正在执行指令的地址。除去以上的不同,R3-R11还有一个十分重要的特性,它们需要手动装载,其余皆可以自动装载,这在任务调度的现场保护和还原十分重要。
3.特权级?线程模式?
ARM内核提供了两个级别分别是特权级和用户级,特权级下可以访问所有寄存器,而用户级有限制。除了两个级别还有两个模式分别是Handle模式和线程模式,通俗的说Handle模式就是发生中断时所处的模式,线程即是一般情况。中断时一定为特权级。
4.什么是MSP主栈指针?什么是PSP进程指针?
MSP(Main Stack Pointer)主栈指针是指在嵌入式系统中用于管理主线程的栈空间的指针。主栈指针指向主线程的栈空间,用于存储主线程执行时所需的局部变量、函数调用信息等数据。裸机一般就只会使用该指针。
PSP(Process Stack Pointer)进程指针是指在嵌入式系统中用于管理多线程或多进程的栈空间的指针。每个线程或进程都有自己的进程指针,用于指向该线程或进程的栈空间,以便存储该线程或进程执行时所需的局部变量、函数调用信息等数据。在操作系统的情况下,任务会使用PSP指针,而中断中会使用MSP指针。
通俗的说MSP就是主线任务用的栈指针,PSP是分线任务用的指针,而无操作系统时是没有分线任务的,只会使用MSP指针。主线任务即餐厅经理的调度过程,分线任务即要上菜的炒菜,端菜等的运行过程。
5.函数是如何被执行的?函数运行中被打断会发生什么?
我们用keil5中的一段C代码通过反汇编对其进行调试,就能非常清楚的看到函数的执行过程。
void func2( void )
{
int b = 2;
}
void func1( void )
{
int a = 1;
func2();
a++;
}
int main()
{
func1();
}
我们打开汇编,找到"最开始"运行的地方,在这里我们可以得到很多信息,如当前使用的指针为MSP,为线程模式,r15即PC存储真的是当前运行指令的地址,r13存储了MSP指针的地址,回归正题我们可以看到在进入main函数后,准备调用func1()函数前,出现了一个压栈的操作,PUSH {lr}即将lr的值压入栈(每一个函数在被调用时系统都会给它分配一块内存,它是一般是向下生长的,即入栈时栈指针会减小,如:当栈为一个数组a[ 12 ]时,初始栈顶指针为&a[11],入栈一次栈顶指针就变为了&a[10],栈一般用来当函数被打断时保存函数的运行环境,说白了就是将打断前CPU各个必要的寄存器的值压入栈中保存【在os中“所有”寄存器的值都是必要的】,当打断的程序运行完返回时,再从栈中取出放回CPU中,如果此时打断程序又被另一个程序打断,那将会继续压栈形成一个个栈帧,最后随着函数的运行结束栈也会被释放),好,我们再具体点lr的值是多少?压入了主栈的哪里?红圈不难发现lr = 0x0000014B,压入了0x20001064(因为sp-4),0x0000014B其实就是上次调用指令的下一个指令的地址,这样做是为了正确的返回,?????我们不急往下看一个好观察的,你就明白了。
我们可以看到压栈指令后是一个调用指令 BL.W func1(0x00000228) ,将返回地址存入lr中,在下面你可以看到lr寄存器的值变成了0x0000023F,然后跳转到该地址开始执行。看红圈也可以验证上面所说的压栈相关值变化。蓝色是该调用指令的下一个指令,留意有用。
接着往下走。。。。又发生了压栈,为什么?因为下面func2的调用要打断func1的执行,这次压的是什么,压的是上面蓝色指令的地址0x0000023E,但实际它压入的是0x0000023F,你会发现它们的地址有点不一样,笔者猜测是内存对齐的问题,0x0000023E~0x00000240为一整体使用,0x0000023F经过对齐后变为0x0000023E,如理解错误,望指正。
往下走。。。r2寄存器写入1
又发生调用,并更新lr为0x00000231,可以在下面验证,为什么这次没有再压栈,因为这是最后一层调用了,注意蓝色部分。
r0寄存器写入2,完成func2函数,是不是该返回func1中调用func2的下一个指令的位置了
将lr的值赋给pc,后跳转到该处开始执行,它会返回到0x00000230,就是第二个蓝条位置。
r2实现自增,到这里是不是根据lr的值开始返回了,前面说的将lr的值压入栈中为了正确返回是不是明白了,虽然这个lr没有压入栈中。
到此func1也执行完了是不是该回到main发生func1调用的下一个指令位置了,从栈顶指针(第三张图有栈的情况)处取出值赋给pc并跳转到该处执行。
我们又回来了,还记得那个起始断点吗?第一个蓝色的地方吗?就是跳到该处执行。
下面还有些收尾,栈并未空,因为篇幅原因不再提及。
总结:栈就是函数运行环境的缓存空间。跳转回PC和跳转回LR的区别在于它们的用途不同:跳转回PC用于通用的跳转和分支操作,而跳转回LR用于函数的返回。
二,正文
1,任务控制结构体定义(在task.h中)
这就相当与任务的身份证,可以凭借这个找到任务及该任务的相关信息,执行一些操作。
/* 任务控制块结构体 */
typedef struct tskTaskControlBlock
{
/* 栈顶指针 */
volatile StackType_t *pxTopOfStack;
/* 任务节点 */
ListItem_t xStateListItem;
/* 任务栈起始地址 */
StackType_t *pxStack;
/* 任务名称,字符串形式 */
char pcTaskName[ configMAX_TASK_NAME_LEN ];
/* 延时 */
TickType_t xTicksToDelay;
/* 优先级 */
UBaseType_t uxPriority;
} tskTCB;
typedef tskTCB TCB_t;
/* 句柄变量类型 */
typedef void * TaskHandle_t;
2.一些任务相关变量的定义(以下的代码无特别说明都在task.c中)
/* 当前正在运行的任务的任务控制块指针 */
TCB_t * volatile pxCurrentTCB = NULL;
/* 任务就绪列表 */
List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
/* 当前运任务数量 */
static volatile UBaseType_t uxCurrentNumberOfTasks = ( UBaseType_t ) 0U;
/* 运行时间存储 */
static volatile TickType_t xTickCount = ( TickType_t ) 0U;
/* 优先级位图 */
static volatile UBaseType_t uxTopReadyPriority = tskIDLE_PRIORITY;
/* 延时链表1 */
static List_t xDelayedTaskList1;
/* 延时链表2 */
static List_t xDelayedTaskList2;
/* 延时链表1指针 */
static List_t * volatile pxDelayedTaskList;
/* 延时链表2指针 */
static List_t * volatile pxOverflowDelayedTaskList;
/* 记录该任务完成阻塞再次开始执行的时间 */
static volatile TickType_t xNextTaskUnblockTime = ( TickType_t ) 0U;
/* 记录阻塞链表溢出次数 */
static volatile BaseType_t xNumOfOverflows = ( BaseType_t ) 0;
优先级位图其实你也可以看到它就是一个变量罢了,它是用来寻找当前最高优先级的就绪任务用的,具体如何实现的后面说。xNextTaskUnblockTime = 当前时间即xTickCount +延时时间,出现加法,你就得考虑溢出的问题,故此引出了两个延时链表(或叫阻塞链表),它们是如何配合解决溢出的问题,到后面相关内容再说。
3.初始化任务相关的列表函数
/*
*函数名字:prvInitialiseTaskLists
*作用:初始化任务相关的列表
*参数pxList:要初始化的链表指针
*返回值:无
*/
void prvInitialiseTaskLists( void )
{
UBaseType_t uxPriority;
for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
{
vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
}
/* 初始化阻塞链表 */
vListInitialise( &xDelayedTaskList1 );
vListInitialise( &xDelayedTaskList2 );
/* 初始化阻塞链表指针 */
pxDelayedTaskList = &xDelayedTaskList1;
pxOverflowDelayedTaskList = &xDelayedTaskList2;
}
整个函数就是调用我们链表部分写的链表初始函数将全部的就绪链表和阻塞链表进行初始化,当一个任务被阻塞函数挂起时将会从就绪链表加入到阻塞链表,该任务结束时,将会从阻塞链表加入到就绪链表,这样的结构会让任务的调度变得简单,它们是如何做到的后面再说。
3.将任务加入就绪的列表函数
/*
*函数名字:prvAddNewTaskToReadyList
*作用:将任务加入就绪的列表
*参数pxList:要初始化的链表指针
*返回值:无
*/
static void prvAddNewTaskToReadyList( TCB_t *pxNewTCB )
{
/* 进入临界段 */
portDISABLE_INTERRUPTS();
{
/* 全局任务计时器加一操作 */
uxCurrentNumberOfTasks++;
/* 如果pxCurrentTCB为空,则将pxCurrentTCB指向新创建的任务 */
if( pxCurrentTCB == NULL )
{
pxCurrentTCB = pxNewTCB;
/* 如果是第一次创建任务,则需要初始化任务相关的列表 */
if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 )
{
/* 初始化任务相关的列表 */
prvInitialiseTaskLists();
}
}
/* 如果pxCurrentTCB不为空,则根据任务的优先级将pxCurrentTCB指向最高优先级任务的TCB */
else
{
if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority )
{
pxCurrentTCB = pxNewTCB;
}
}
/* 将任务添加到就绪列表 */
prvAddTaskToReadyList( pxNewTCB );
}
/* 退出临界段 */
portENABLE_INTERRUPTS();
}
这个函数利用一些判断将链表初始化函数嵌入,这样就不用单独调用链表初始化函数进行初始化,在将任务加入就绪链表中还顺便更新了当前运行任务的控制块指针,指向优先级最高的那个任务。这里出现了临界区的概念,通俗的说就是在临界区内的代码无法被打断。它是如何实现的我们往下看。
4.不可嵌套的临界区(定义在port.h中)
/* 开启不可嵌套临界保护宏 */
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
/* 开启不可嵌套临界保护函数 */
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
/* Set BASEPRI to the max syscall priority to effect a critical
section. */
msr basepri, ulNewBASEPRI
dsb
isb
}
}
/* 关闭不可嵌套临界保护宏 */
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
代码在什么时候会被打断呢?中断。所以我们只要把中断关了是不是能完成进入临界区这个函数,恢复中断是不是可以完成退出临界区的操作。basepri是ARM内核的一个特殊寄存器,用于屏蔽中断,高4位有效,如果往这个寄存器的高4位写入11,则优先级大高于11的中断都将被屏蔽。这开启个不可嵌套其实就是向这个寄存器中写入了11,Freertos并没有关闭所有中断。关闭不可嵌套函数就是写入0。
5.可嵌套的临界区(定义在port.h中)
/* 开启可嵌套临界保护函数 */
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
mrs ulReturn, basepri
msr basepri, ulNewBASEPRI
dsb
isb
}
return ulReturn;
}
/* 关闭可嵌套临界保护宏 */
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
/* 关闭可嵌套和不可嵌套皆为一个函数 */
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
/* Barrier instructions are not used as this function is only used to
lower the BASEPRI value. */
msr basepri, ulBASEPRI
}
}
其实它和上面就多了个值的返回。多了个返回就能嵌套?如何实现的?为什么要这样做?
{
uint32_t ulReturn1,ulReturn;2;
/* 开启临界区1 */
ulReturn1 = ulPortRaiseBASEPRI( 11 );
/* 开启临界区2 */
ulReturn2 = ulPortRaiseBASEPRI( 5 );
/* 退出临界区2 */
vPortSetBASEPRI( ulReturn2 );
/* 退出临界区1 */
vPortSetBASEPRI( ulReturn1 );
}
可嵌套的临界区函数是给中断用到的,这样可以在中断处理程序中嵌套调用临界区函数,这样可以保护多个临界区,防止多个中断同时访问共享资源导致的竞态条件。
6.初始化任务
/*
*函数名字:prvInitialiseNewTask
*作用:初始化任务
*参数pxTaskCode:任务入口
*参数pcName:任务名称,字符串形式
*参数ulStackDepth:任务栈大小,单位为字
*参数pvParameters:任务形参
*参数uxPriority:任务优先级,数值越大,优先级越高
*参数pxCreatedTask:任务句柄
*参数pxTaskBuffer:任务控制块
*返回值:无
*/
static void prvInitialiseNewTask( TaskFunction_t pxTaskCode,/* 任务入口 */
const char * const pcName,/* 任务名称,字符串形式 */
const uint32_t ulStackDepth,/* 任务栈大小,单位为字 */
void * const pvParameters,/* 任务形参 */
UBaseType_t uxPriority,/* 任务优先级,数值越大,优先级越*/
TaskHandle_t * const pxCreatedTask,/* 任务句柄 */
TCB_t *pxNewTCB )/* 任务控制块 */
{
StackType_t *pxTopOfStack;
UBaseType_t x;
/* 获取栈顶地址 */
pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
/* 向下做8字节对齐 */
pxTopOfStack = (StackType_t *) (((uint32_t)pxTopOfStack) & (~((uint32_t)0x0007 )));
/* 将任务的名字存储在TCB中 */
for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
{
pxNewTCB->pcTaskName[ x ] = pcName[ x ];
if( pcName[ x ] == 0x00 )
{
break;
}
}
/* 任务名字的长度不能超过configMAX_TASK_NAME_LEN */
pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';
/* 初始化TCB中的xStateListItem节点 */
vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
/* 设置xStateListItem节点的拥有者 */
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
/* 初始化优先级 */
if( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES )
{
uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U;
}
pxNewTCB->uxPriority = uxPriority;
/* 初始化任务栈 */
pxNewTCB->pxTopOfStack=pxPortInitialiseStack( pxTopOfStack,pxTaskCode,pvParameters );
/* 让任务句柄指向任务控制块 */
if( ( void * ) pxCreatedTask != NULL )
{
*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
}
}
这个函数看起来很多其实思路并不复杂,这里主要初始化了任务的名字,所在链表即节点,优先级,栈顶指针,把链表中剩下的链表拥有者也进行初始化,此时还剩起始地址和延时时间未初始化,这个将在别的地方进行初始化。这里用到了链表的一些便利操作的宏和一个栈初始化函数,便利宏没什么好说的把下面的写入list.h即可。
/* 初始化节点的拥有者 */
#define listSET_LIST_ITEM_OWNER( pxListItem,pxOwner) ((pxListItem)->pvOwner=(void*)(pxOwner))
/* 获取节点拥有者 */
#define listGET_LIST_ITEM_OWNER( pxListItem ) ( ( pxListItem )->pvOwner )
/* 初始化节点排序辅助值 */
#define listSET_LIST_ITEM_VALUE(pxListItem, xValue) ((pxListItem)->xItemValue=(xValue))
/* 获取节点排序辅助值 */
#define listGET_LIST_ITEM_VALUE( pxListItem ) ( ( pxListItem )->xItemValue )
/* 获取链表根节点的节点计数器的值 */
#define listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxList ) ((( pxList )->xListEnd ).pxNext->xItemValue)
/* 获取链表的入口节点 */
#define listGET_HEAD_ENTRY( pxList ) ( ( ( pxList )->xListEnd ).pxNext )
/* 获取链表的第一个节点 */
#define listGET_NEXT( pxListItem ) ( ( pxListItem )->pxNext )
/* 获取链表的最后一个节点 */
#define listGET_END_MARKER(pxList) ((ListItem_t const *) (&(( pxList )->xListEnd)))
/* 判断链表是否为空 */
#define listLIST_IS_EMPTY(pxList) (( BaseType_t)((pxList)->uxNumberOfItems==((UBaseType_t)0))
/* 获取链表的节点数 */
#define listCURRENT_LIST_LENGTH( pxList ) ( ( pxList )->uxNumberOfItems )
/* 获取链表节点的OWNER,即TCB */
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \
{ \
List_t * const pxConstList = ( pxList ); \
/* 节点索引指向链表第一个节点调整节点索引指针,指向下一个节点,
如果当前链表有N个节点,当第N次调用该函数时,pxInedex则指向第N个节点 */\
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
/* 当前链表为空 */ \
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \
{ \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
} \
/* 获取节点的OWNER,即TCB */ \
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \
}
/* 得到链表下一节点拥有者 */
#define listGET_OWNER_OF_HEAD_ENTRY(pxList) ((&((pxList)->xListEnd))->pxNext->pvOwner)
7.初始化任务栈 (定义在port.c)
/*
*函数名字:pxPortInitialiseStack
*作用:任务栈初始化
*参数pxTopOfStack:要初始化的栈指针
*参数pxCode:函数入口
*参数pvParameters:函数参数
*返回值:栈顶指针
*/
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode,
void *pvParameters )
{
pxTopOfStack--;
/* xPSR的bit24必须置1 */
*pxTopOfStack = portINITIAL_XPSR;
pxTopOfStack--;
/* PC,即任务入口函数 */
*pxTopOfStack = ( ( StackType_t ) pxCode ) & 0xfffffffeUL;
pxTopOfStack--;
/* LR,函数返回地址 */
*pxTopOfStack = ( StackType_t ) prvTaskExitError;
/* R12, R3, R2 and R1 默认初始化为0 */
pxTopOfStack -= 5;
/* R0,任务形参 */
*pxTopOfStack = ( StackType_t ) pvParameters;
/* R11, R10, R9, R8, R7, R6, R5 and R4默认初始化为0 */
pxTopOfStack -= 8;
return pxTopOfStack;
}
初始化完成后栈情况,为什么要这样初始化,在任务调度函数你就能明白。
8.任务创建函数
/*
*函数名字:xTaskCreateStatic
*作用:静态创建任务
*参数pxTaskCode:任务入口
*参数pcName:任务名称,字符串形式
*参数ulStackDepth:任务栈大小,单位为字
*参数pvParameters:任务形参
*参数uxPriority:任务优先级,数值越大,优先级越高
*参数puxStackBuffer:任务栈起始地址
*参数pxTaskBuffer:任务控制块
*返回值:任务句柄
*/
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,/* 任务入口 */
const char * const pcName,/* 任务名称,字符串形式 */
const uint32_t ulStackDepth,/* 任务栈大小,单位为字 */
void * const pvParameters,/* 任务形参 */
UBaseType_t uxPriority,/* 任务优先级,数值越大,优先级越高*/
StackType_t * const puxStackBuffer,/* 任务栈起始地址 */
TCB_t * const pxTaskBuffer )/* 任务控制块 */
{
TCB_t *pxNewTCB;
TaskHandle_t xReturn;
if( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) )
{
pxNewTCB = ( TCB_t * ) pxTaskBuffer;
pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;
/* 创建新的任务 */
prvInitialiseNewTask( pxTaskCode, pcName, ulStackDepth, pvParameters,uxPriority,
&xReturn, pxNewTCB);
/* 将任务添加到就绪列表 */
prvAddNewTaskToReadyList( pxNewTCB );
}
else
{
xReturn = NULL;
}
return xReturn;
}
这个函数将控制块剩余的任务入口初始化,然后调用初始化任务的函数和加入就绪链表函数即完成任务的创建。
9.调度函数
/*
*函数名字:vTaskStartScheduler
*作用:开启调度器
*参数:无
*返回值:无
*/
void vTaskStartScheduler( void )
{
/* 创建空闲任务 */
xIdleTaskHandle = xTaskCreateStatic( (TaskFunction_t)prvIdleTask,/* 任务入口 */
(char *)"IDLE",/* 任务名称,字符串形式 */
(uint32_t)ulIdleTaskStackSize,/* 任务栈大小 */
(void *) NULL,/* 任务形参 */
(UBaseType_t) tskIDLE_PRIORITY,/* 任务优先级 */
(StackType_t *)pxIdleTaskStackBuffer,/*栈起始地址*/
(TCB_t *)&pxIdleTaskTCBBuffer );/* 任务控制块 */
/* 初始化下一个任务阻塞完成时间 */
xNextTaskUnblockTime = portMAX_DELAY;
/* 初始化时间计数值 */
xTickCount = ( TickType_t ) 0U;
/* 启动调度器 */
if( xPortStartScheduler() != pdFALSE )
{
/* 调度器启动成功,则不会返回,即不会来到这里 */
}
}
这里我们看到任务的调度实际开始于xPortStartScheduler()。这个函数定义于port.c中。
/*
*函数名字:xPortStartScheduler
*作用:任务调度函数部分1
*参数:无
*返回值:是否成功
*/
BaseType_t xPortStartScheduler( void )
{
/* 配置PendSV 和 SysTick 的中断优先级为最低 */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* 初始化SysTick */
vPortSetupTimerInterrupt();
/* 启动第一个任务,不再返回 */
prvStartFirstTask();
/* 不应该运行到这里 */
return 0;
}
因为任务调度需要用到这两中断,但我们又不希望任务的调度打断其他的中断,我们就将它们的优先级设为最低。并使用vPortSetupTimerInterrupt()函数对滴答定时器的中断频率进行初始化。真正开始第一个任务运行的为prvStartFirstTask()函数,这两个也在port.c中。
/*
*函数名字:vPortSetupTimerInterrupt
*作用:初始化滴答定时器(10ms中断一次,除100的话)
*参数:无
*返回值:无
*/
void vPortSetupTimerInterrupt( void )
{
/* 设置重装载寄存器的值 */
portNVIC_SYSTICK_LOAD_REG = ( configCPU_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
/* 设置系统定时器的时钟等于内核时钟
使能SysTick 定时器中断
使能SysTick 定时器 */
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}
没什么好说的主要是对滴答定时器相应计时器进行配置即可完成呢10ms一次的中断频率。
/*
*函数名字:prvStartFirstTask
*作用:任务调度函数部分2
*参数:无
*返回值:无
*/
__asm void prvStartFirstTask( void )
{
PRESERVE8
/* 在Cortex-M中,0xE000ED08是SCB_VTOR这个寄存器的地址,里面存放的是向量表的起始地址,即MSP的地址 */
ldr r0, =0xE000ED08
ldr r0, [r0]
ldr r0, [r0]
/* 初始化主堆栈指针msp的值 */
msr msp, r0
/* 使能全局中断 */
cpsie i
cpsie f
dsb
isb
/* 调用SVC去启动第一个任务 */
svc 0
nop
nop
}
这里主要是对主栈指针进行了初始化,然后手动触发一个svc中断(这个中断是必须马上进行响应的,不然将发生错误)开始第一个任务的运行。接下来我们看看它是如何在该中断服务函数中开始运行第一个任务的。这个服务函数也在port.c中。
/*
*函数名字:vPortSVCHandler
*作用:svc中断服务函数,将任务栈的内容放入cpu的寄存器中
*参数:无
*返回值:无
*/
__asm void vPortSVCHandler( void )
{
extern pxCurrentTCB;
PRESERVE8
/* 加载pxCurrentTCB的地址到r3 */
ldr r3, =pxCurrentTCB
/* 加载pxCurrentTCB到r1 */
ldr r1, [r3]
/* 加载pxCurrentTCB指向的值到r0,目前r0的值等于第一个任务堆栈的栈顶 */
ldr r0, [r1]
/* 以r0为基地址,将栈里面的内容加载到r4~r11寄存器,同时r0会递增 */
ldmia r0!, {r4-r11}
/* 将r0的值,即任务的栈指针更新到psp */
msr psp, r0
isb
/* 设置r0的值为0 */
mov r0, #0
/* 设置basepri寄存器的值为0,即所有的中断都没有被屏蔽 */
msr basepri, r0
/* 当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,
使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态 */
orr r14, #0xd
/* 异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器:
xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
同时PSP的值也将更新,即指向任务栈的栈顶 */
bx r14
}
其实就是将任务栈中的内容加载到cpu相应的寄存器中并更新psp指针,然后返回并切换所使用的栈指针。bx r14返回后将自动装载栈剩下的内容,cpu发现pc寄存器的值发生改变,又将会跳转到pc值指向的地址,开始执行,自此完成第一个任务的启动,这个第一个任务一定是就绪且优先级最高的任务,因为在创建的时候就确定了,后续的任务切换需要手动调用相应的阻塞函数。
10,阻塞挂起函数
/*
*函数名字:vTaskDelay
*作用:任务阻塞挂起函数
*参数xTicksToDelay:延时时间
*返回值:无
*/
void vTaskDelay( const TickType_t xTicksToDelay )
{
TCB_t *pxTCB = NULL;
/* 获取当前任务的TCB */
pxTCB = pxCurrentTCB;
/* 将任务插入到延时列表,并完成相关操作 */
prvAddCurrentTaskToDelayedList( xTicksToDelay );
/* 任务切换 */
taskYIELD();
}
先来看看插入阻塞链表函数
/*
*函数名字:prvAddCurrentTaskToDelayedList
*作用:将任务加入到阻塞链表
*参数xTicksToWait:延时时间
*返回值:无
*/
static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait )
{
TickType_t xTimeToWake;
/* 获取系统时基计数器xTickCount的值 */
const TickType_t xConstTickCount = xTickCount;
/* 将任务从就绪链表中删除,然后判断该链表下是否还有其他的节点,没有才可将相应优先级位清除 */
if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t )0 )
{
/* 将任务在优先级位图中对应的位清除 */
portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
}
/* 计算延时到期时,系统时基计数器xTickCount的值是多少 */
xTimeToWake = xConstTickCount + xTicksToWait;
/* 将延时到期的值设置为节点的排序值 */
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
/* 溢出 */
if( xTimeToWake < xConstTickCount )
{
vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
/* 没有溢出 */
else
{
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
/* 更新下一个任务解锁时刻变量xNextTaskUnblockTime的值 */
if( xTimeToWake < xNextTaskUnblockTime )
{
xNextTaskUnblockTime = xTimeToWake;
}
}
}
在了解这个函数前我们先了解一下freertos是如何寻找就绪任务最大优先级任务的(优化法),这得益于 Cortex-M 内核有一个计算前导零的指令 CLZ,所谓前导零就是计算一个变量(Cortex-M 内核单片机的变量为 32位)从高位开始第 一次出现 1 的位的前面的零的个数。比如:一个 32 位的变量 uxTopReadyPriority,其位 0、 位 24 和 位 25 均 置 1 , 其 余 位 为 0 , 具 体 见 。 那 么 使 用 前 导 零 指 令 __CLZ (uxTopReadyPriority)可以很快的计算出 uxTopReadyPriority 的前导零的个数为 6。
/*
************************************************************************
* 寻找任务优先级相关宏定义
************************************************************************
*/
/* 空闲任务优先级 */
#define tskIDLE_PRIORITY ( ( UBaseType_t ) 0U )
/* 根据优先级将优先级位图的相应位置1 */
#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities ) ( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) )
/* 根据优先级将优先级位图的相应位置0 */
#define portRESET_READY_PRIORITY( uxPriority, uxReadyPriorities ) ( uxReadyPriorities ) &= ~( 1UL << ( uxPriority ) )
/* 根据优先级设置优先级位图中相应的位 */
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
/* 将任务添加到就绪列表并初始化优先级位图 */
#define prvAddTaskToReadyList( pxTCB )\
taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority );\
vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) );
/* 查找最高优先级的就绪任务:通用方法 */
#if ( configUSE_PORT_OPTIMISED_TASK_SELECTION == 0 )
/* 修改uxTopReadyPriority即最高优先级 */
#define taskRECORD_READY_PRIORITY( uxPriority )\
{\
if( ( uxPriority ) > uxTopReadyPriority )\
{ \
uxTopReadyPriority = ( uxPriority );\
}\
}
/* 寻找最高优先级 */
#define taskSELECT_HIGHEST_PRIORITY_TASK()\
{\
UBaseType_t uxTopPriority = uxTopReadyPriority;\
/* 寻找包含就绪任务的最高优先级的队列 */\
while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )\
{\
--uxTopPriority;\
}\
/* 获取优先级最高的就绪任务的TCB,然后更新到pxCurrentTCB */\
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );\
/* 更新uxTopReadyPriority */\
uxTopReadyPriority = uxTopPriority; \
}
/* 这两个宏定义只有在选择优化方法时才用,这里定义为空 */
#define taskRESET_READY_PRIORITY( uxPriority )
#define portRESET_READY_PRIORITY( uxPriority, uxTopReadyPriority )
/* 查找最高优先级的就绪任务:根据处理器架构优化后的方法 */
#else /* configUSE_PORT_OPTIMISED_TASK_SELECTION */
/* 根据优先级将优先级位图的相应位置1 */
#define taskRECORD_READY_PRIORITY( uxPriority ) portRECORD_READY_PRIORITY( uxPriority, uxTopReadyPriority )
/* 根据优先级将优先级位图的相应位置0 */
#define taskRESET_READY_PRIORITY( uxPriority )\
{\
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 )\
{\
portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) );\
}\
}
/* 寻找最高优先级 */
#define taskSELECT_HIGHEST_PRIORITY_TASK()\
{\
UBaseType_t uxTopPriority;\
/* 寻找最高优先级 */\
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );\
/* 获取优先级最高的就绪任务的TCB,然后更新到pxCurrentTCB */\
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );\
}
#endif /* configUSE_PORT_OPTIMISED_TASK_SELECTION */
回归正题这个加入阻塞链表函数主要是将需要挂起的任务从就绪链表中移除,然后根据下次解除的时间是否溢出来决定加入那个阻塞链表,然后判断这个任务解除的时间比上一次的时间短吗?来决定是否更新xNextTaskUnblockTime这个全局变量,以此得到最先解除阻塞任务的时间,而为了支持时间片优先级图的相应位清零,要在该优先级下的就绪链表没有任务再执行。为什么要多一个溢出的阻塞链表,这是因为当xTimeToWake溢出时,本来应后执行的任务会提前执行。
举一个例子(假设是uint8_t溢出,即255溢出):
第一个挂起的任务解除阻塞的时间为xConstTickCount + xTicksToWait=20+20,第二个挂起的任务解除阻塞的时间为xConstTickCount+ xTicksToWait=128+130此时会发生溢出,根据c语的规定258以256为模的结果值是2,则任务二将插入到任务一的前面导致其比任务一先运行。而将这个溢出的任务加入另一链表可以解决这个问题。
11,任务切换函数
/*
************************************************************************
* 触发PendSV中断函数宏定义
************************************************************************
*/
#define taskYIELD() portYIELD()
#define portSY_FULL_READ_WRITE ( 15 )
/* 触发PendSV中断相应寄存器 */
#define portNVIC_INT_CTRL_REG ( * ( ( volatile uint32_t * ) 0xe000ed04 ) )
/* 触发PendSV中断相应寄存器位 */
#define portNVIC_PENDSVSET_BIT ( 1UL << 28UL )
/* 触发PendSV中断函数 */
#define portYIELD()\
{\
/* 设置 PendSV 的中断挂起位,产生上下文切换 */\
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;\
__dsb( portSY_FULL_READ_WRITE );\
__isb( portSY_FULL_READ_WRITE );\
}
这里只是触发了PendSV中断(可挂起的,无其他中断发生才触发),真正的任务切换在该中断的服务函数中进行。触发函数和中断服务函数皆定义在port.c中。
/*
*函数名字:xPortPendSVHandler
*作用:PendSV中断服务函数,完成任务切换
*参数:无
*返回值:无
*/
__asm void xPortPendSVHandler( void )
{
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
/* 当进入PendSVC Handler时,上一个任务运行的环境即:
xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
这些CPU寄存器的值会自动保存到任务的栈中,剩下的r4~r11需要手动保存 */
/* 获取任务栈指针到r0 */
mrs r0, psp
isb
/* 加载pxCurrentTCB的地址到r3 */
ldr r3, =pxCurrentTCB
/* 加载pxCurrentTCB到r2 */
ldr r2, [r3]
/* 将CPU寄存器r4~r11的值存储到r0指向的地址 */
stmdb r0!, {r4-r11}
/* 栈顶指针,放入r0 */
str r0, [r2]
/* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,
调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护;
R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护 */
stmdb sp!, {r3, r14}
/* 进入临界段 */
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
/* 调用函数vTaskSwitchContext,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */
bl vTaskSwitchContext
/* 退出临界段 */
mov r0, #0
msr basepri, r0
/* 恢复r3和r14 */
ldmia sp!, {r3, r14}
ldr r1, [r3]
/* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/
ldr r0, [r1]
/* 出栈 */
ldmia r0!, {r4-r11}
msr psp, r0
isb
/* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、
使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,
然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,
当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/
bx r14
nop
}
这个看起来多,其实就是进行了上文的保存即将上一个任务的不能自动入栈的寄存器r4-r11手动压入栈中,然后将当前任务的指针的指针保存在 r3中和lr进行入栈保护,完成当前任务的指针切换后恢复,再利用当前任务的指针的指针将不能自动出栈的r4-r11的值出栈,更新psp指针,返回,至此完成任务的切换。以下为当前任务指针切换函数
/*
*函数名字:vTaskSwitchContext
*作用:任务切换,改变当前运行任务控制块部分
*参数:无
*返回值:无
*/
void vTaskSwitchContext( void )
{
/* 获取优先级最高的就绪任务的TCB,然后更新到pxCurrentTCB */
taskSELECT_HIGHEST_PRIORITY_TASK();
}
12.滴答定时器中断服务函数
/*
*函数名字:xPortSysTickHandler
*作用:滴答定时器中断服务函数
*参数:无
*返回值:无
*/
void xPortSysTickHandler( void )
{
/* 关中断 */
portDISABLE_INTERRUPTS();
/* 更新系统时基 */
if( xTaskIncrementTick() != pdFALSE )
{
taskYIELD();
}
/* 开中断 */
portENABLE_INTERRUPTS();
}
以下的 xTaskIncrementTick()函数才真正开始任务从阻塞链表移除的操作。
/*
*函数名字:xTaskIncrementTick
*作用:滴答定时器中断服务函数
*参数:无
*返回值:无
*/
BaseType_t xTaskIncrementTick( void )
{
TCB_t *pxTCB = NULL;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;
/* 更新系统时基计数器 xTickCount 的值 */
const TickType_t xConstTickCount = xTickCount + 1;
xTickCount = xConstTickCount;
/* 如果xConstTickCount溢出,则切换延时列表,并更新下一个任务运行的时间 */
if( xConstTickCount == ( TickType_t ) 0U )
{
taskSWITCH_DELAYED_LISTS();
}
/* 有任务时间到 */
if( xConstTickCount >= xNextTaskUnblockTime )
{
for(;;)
{
/* 延时列表为空,设置xNextTaskUnblockTime为可能的最大值 */
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
xNextTaskUnblockTime = portMAX_DELAY;
break;
}
/* 延时列表不为空 */
else
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
/* 直到将延时列表中所有延时到期的任务移除才跳出for循环 */
if( xConstTickCount < xItemValue )
{
xNextTaskUnblockTime = xItemValue;
break;
}
/* 将任务从延时列表移除,消除等待状态 */
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
/* 将解除等待的任务添加到就绪列表 */
prvAddTaskToReadyList( pxTCB );
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE;
}
}
}
/* 判断就绪链表任务数大于1吗 */
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
}
return xSwitchRequired;
}
这个函数主要进行计时,然后根据时间决定需要将哪些任务从阻塞链表中移除,最后解除的任务的优先级比现在的大吗?和就绪链表任务大于1吗?决定是否切换任务。切换阻塞链表函数如下
/* 阻塞链表互换函数宏定义 */
#define taskSWITCH_DELAYED_LISTS()\
{\
List_t *pxTemp;\
pxTemp = pxDelayedTaskList;\
pxDelayedTaskList = pxOverflowDelayedTaskList;\
pxOverflowDelayedTaskList = pxTemp;\
xNumOfOverflows++;\
prvResetNextTaskUnblockTime();\
}
这段代码的更新下一个任务阻塞解除时间函数为
/*
*函数名字:prvResetNextTaskUnblockTime
*作用:用于处理溢出后的xNextTaskUnblockTime更新
*参数:无
*返回值:无
*/
static void prvResetNextTaskUnblockTime( void )
{
TCB_t *pxTCB;
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
xNextTaskUnblockTime = portMAX_DELAY;
}
else
{
( pxTCB ) = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
/* 用Value的值更新xNextTaskUnblockTime */
xNextTaskUnblockTime = listGET_LIST_ITEM_VALUE( &( ( pxTCB )->xStateListItem ) );
}
}
至此所有关键代码完成解读。以下是main.c的内容
#include "freertos.h"
/* 全局变量 */
portCHAR flag1;
portCHAR flag2;
portCHAR flag3;
/* 软件延时 */
void delay (uint32_t count)
{
for(; count!=0; count--);
}
/* 创建任务1 */
TaskHandle_t Task1_Handle;
#define TASK1_STACK_SIZE 128
StackType_t Task1Stack[TASK1_STACK_SIZE];
TCB_t Task1TCB;
/* 任务1 */
void Task1_Entry( void *p_arg )
{
for( ;; )
{
flag1 = 1;
delay( 100 );
flag1 = 0;
delay( 100 );
}
}
/* 创建任务2 */
TaskHandle_t Task2_Handle;
#define TASK2_STACK_SIZE 128
StackType_t Task2Stack[TASK2_STACK_SIZE];
TCB_t Task2TCB;
/* 任务2 */
void Task2_Entry( void *p_arg )
{
for( ;; )
{
flag2 = 1;
delay( 100 );
flag2 = 0;
delay( 100 );
}
}
/* 创建任务3 */
TaskHandle_t Task3_Handle;
#define TASK3_STACK_SIZE 128
StackType_t Task3Stack[TASK3_STACK_SIZE];
TCB_t Task3TCB;
/* 任务3 */
void Task3_Entry( void *p_arg )
{
for( ;; )
{
flag3 = 1;
vTaskDelay( 1 );
flag3 = 0;
vTaskDelay( 1 );
}
}
int main()
{
/* 创建任务 */
Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */
(char *)"Task1", /* 任务名称,字符串形式 */
(uint32_t)TASK1_STACK_SIZE , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(UBaseType_t) 2,
(StackType_t *)Task1Stack, /* 任务栈起始地址 */
(TCB_t *)&Task1TCB ); /* 任务控制块 */
Task2_Handle = xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */
(char *)"Task2", /* 任务名称,字符串形式 */
(uint32_t)TASK2_STACK_SIZE , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(UBaseType_t) 2,
(StackType_t *)Task2Stack, /* 任务栈起始地址 */
(TCB_t *)&Task2TCB ); /* 任务控制块 */
Task3_Handle = xTaskCreateStatic( (TaskFunction_t)Task3_Entry, /* 任务入口 */
(char *)"Task3", /* 任务名称,字符串形式 */
(uint32_t)TASK3_STACK_SIZE , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(UBaseType_t) 3,
(StackType_t *)Task3Stack, /* 任务栈起始地址 */
(TCB_t *)&Task3TCB ); /* 任务控制块 */
/* 启动调度器,开始多任务调度,启动成功则不返回 */
vTaskStartScheduler();
while( 1 )
{
;
}
}
三,总结
这系列文章是个人学习的总结,针对个人的困惑对关键函数进行一些更加详细的解释,许多重要的地方想要彻底理解还需自己动手编写理解。