根据个人的学习方向,学习FreeRTOS。由于野火小哥把FreeRTOS讲得比较含蓄,打算在本专栏尽量细化一点。作为个人笔记,仅供参考或查阅。
配套资料:FreeRTOS内核实现与应用开发实战指南、野火FreeRTOS配套视频源码、b站野火FreeRTOS视频。搭配来看更佳哟!!!
在裸机系统中, 系统的主体就是 main 函数里面顺序执行的无限循环,这个无限循环里面 CPU 按照顺序完成各种事情。
在多任务系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务。
由于每个任务独立,互不干扰,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组, 也可以是动态分配的一段内存空间,但它们都存在于 RAM 中。
比如:
//TaskHandle_t Task1_Handle;
#define TASK1_STACK_SIZE 20
StackType_t Task1Stack[TASK1_STACK_SIZE];
//TCB_t Task1TCB;
//TaskHandle_t Task2_Handle;
#define TASK2_STACK_SIZE 20
StackType_t Task2Stack[TASK2_STACK_SIZE];
//TCB_t Task2TCB;
StackType_t 实际上就是uint32_t,重命名了。
任务是一个独立的函数,函数主体无限循环且不能返回。比如。
/* 任务1 */
void Task1_Entry( void *p_arg )
{
for( ;; )
{
flag1 = 1;delay( 100 );
flag1 = 0;delay( 100 );
/* 任务切换,这里是手动切换 */
taskYIELD();
}
}
/* 任务2 */
void Task2_Entry( void *p_arg )
{
for( ;; )
{
flag2 = 1;delay( 100 );
flag2 = 0;delay( 100 );
/* 任务切换,这里是手动切换 */
taskYIELD();
}
}
在多任务系统中, 任务的执行是由系统调度的。系统的调度是依据TCB任务控制块,每个任务都有一个TCB。
TCB的结构体
/*任务控制块(TCB)为每个任务分配,并存储任务状态信息,
*包括指向任务上下文的指针(任务的运行时环境,包括寄存器值)*/
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* 栈顶 */
ListItem_t xStateListItem; /* 任务状态节点,表示该任务的状态(就绪、阻塞、挂起)。 */
StackType_t *pxStack; /* 任务栈起始地址,一般是一个数组*/
char pcTaskName[ configMAX_TASK_NAME_LEN ]; /* 任务标识名称,字符串形式,对应任务静态创建函数的pcName参数 */
}tskTCB;
typedef tskTCB TCB_t; //对TCB结构体进行重定向
定义TCB
//TaskHandle_t Task1_Handle;
//#define TASK1_STACK_SIZE 20
//StackType_t Task1Stack[TASK1_STACK_SIZE];
TCB_t Task1TCB;
//TaskHandle_t Task2_Handle;
//#define TASK2_STACK_SIZE 20
//StackType_t Task2Stack[TASK2_STACK_SIZE];
TCB_t Task2TCB;
FreeRTOS 中,任务的创建有两种方法,一种是使用动态创建,一种是使用静态创建。
动态创建时,任务控制块和栈的内存是创建任务时动态分配的, 任务删除时,内存可以释放。
静态创建时,任务控制块和栈的内存需要事先定义好,是静态的内存 ,任务删除时 ,内存不能释放 。
下面看看静态任务创建的应用,然后搭配着看静态任务创建函数,更容易清晰逻辑。
/* 创建任务 */
Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */
(char *)"Task1", /* 任务名称,字符串形式 */
(uint32_t)TASK1_STACK_SIZE , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(StackType_t *)Task1Stack, /* 任务栈起始地址 */
(TCB_t *)&Task1TCB ); /* 任务控制块 */
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, /* 任务入口,任务函数名称 */
const char * const pcName, /* 任务标识名称,字符串形式 */
const uint32_t ulStackDepth, /* 任务栈大小,单位为字 */
void * const pvParameters, /* 任务形参 */
StackType_t * const puxStackBuffer, /* 任务栈起始地址,一般为数组 */
TCB_t * const pxTaskBuffer ) /* 任务控制块指针,任务的TCB */
{
TCB_t *pxNewTCB;
TaskHandle_t xReturn; //任务句柄,指向任务的TCB
if( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) )
{
pxNewTCB = ( TCB_t * ) pxTaskBuffer;
pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;
/* 创建新的任务 */
prvInitialiseNewTask( pxTaskCode, /* 任务入口 */
pcName, /* 任务名称,字符串形式 */
ulStackDepth, /* 任务栈大小,单位为字 */
pvParameters, /* 任务形参 */
&xReturn, /* 任务句柄 */
pxNewTCB); /* 任务栈起始地址 */
}
else
{
xReturn = NULL;
}
/* 返回任务句柄,如果任务创建成功,此时xReturn应该指向任务控制块 */
return xReturn;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
这里调用了prvInitialiseNewTask函数,层层递用。
static void prvInitialiseNewTask( TaskFunction_t pxTaskCode, /* 任务入口 ,任务函数名 */
const char * const pcName, /* 任务标识名称,字符串形式 */
const uint32_t ulStackDepth, /* 任务栈大小,单位为字 */
void * const pvParameters, /* 任务形参 */
TaskHandle_t * const pxCreatedTask, /* 任务句柄 */
TCB_t *pxNewTCB ) /* 任务控制块指针 */
{
StackType_t *pxTopOfStack;
UBaseType_t x;
/* 获取栈顶地址 */
pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
//pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
/* 向下做8字节对齐 */
/* ~( ( uint32_t ) 0x0007 = 0xFFF8 = 1111 1111 1111 1000,最低位1为8,往高位走,永远是8的倍数。所以可以做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 );
/* 初始化任务栈 */
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
/* 让任务句柄指向任务控制块 */
if( ( void * ) pxCreatedTask != NULL )
{
*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
}
}
在 Cortex-M3(Cortex-M4 或 Cortex-M7)内核的单片机中,因为总线宽度是 32 位的,通常只要栈保持 4 字节对齐就行,可这样为啥要 8 字节?难道有哪些操作是 64 位的?
确实有,那就是浮点运算,所以要 8 字节对齐(但是目前我们都还没有涉及到浮点运算,只是为了后续兼容浮点运行的考虑)。
在上面函数中,调用了pxPortInitialiseStack函数,层层递用。定义在port.c。
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
/* 异常发生时,自动加载到CPU寄存器的内容 */
pxTopOfStack--;
*pxTopOfStack = portINITIAL_XPSR; /* xPSR的bit24必须置1,,即0x0100 0000 */
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC,即任务入口地址 */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR,函数返回地址。通常任务不会返回,如果返回了就跳转到prvTaskExitError函数,使得无限循环 */
pxTopOfStack -= 5; /* R12, R3, R2 and R1 默认初始化为0 */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0,任务形参 */
/* 异常发生时,手动加载到CPU寄存器的内容 */
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4默认初始化为0 */
/* 返回栈顶指针,此时pxTopOfStack指向空闲栈 */
return pxTopOfStack;
}
此时 PC 指针就指向了任务入口地址,从而成功跳转到第一个任务。
任务创建好之后,我们需要把任务添加到就绪列表里面, 表示任务已经就绪,系统随时可以调度。定义在task.c中。main函数使用时用extern声明。
/* 任务就绪列表 */
List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
就绪列表实际上就是一个 List_t 类型的数组,数组的大小由决定最大任务优先级的宏 configMAX_PRIORITIES 决定 , configMAX_PRIORITIES 在 FreeRTOSConfig.h 中默认定义为 5,最大支持 256 个优先级。 数组的下标对应了任务的优先级,同一优先级的任务统一插入到就绪列表的同一条链表中。
就绪列表在使用前需要先初始化,就绪列表初始化的工作在函数 prvInitialiseTaskLists()里面实现。
/* 就绪列表初始化 */
void prvInitialiseTaskLists( void )
{
UBaseType_t uxPriority;
for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
{
vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
}
}
然后呢,实际应用。
/* 初始化与任务相关的列表,如就绪列表 */
prvInitialiseTaskLists();
/* 创建任务 */
Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */
(char *)"Task1", /* 任务名称,字符串形式 */
(uint32_t)TASK1_STACK_SIZE , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(StackType_t *)Task1Stack, /* 任务栈起始地址 */
(TCB_t *)&Task1TCB ); /* 任务控制块 */
/* 将任务添加到就绪列表 */
vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
Task2_Handle = xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */
(char *)"Task2", /* 任务名称,字符串形式 */
(uint32_t)TASK2_STACK_SIZE , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(StackType_t *)Task2Stack, /* 任务栈起始地址 */
(TCB_t *)&Task2TCB ); /* 任务控制块 */
/* 将任务添加到就绪列表 */
vListInsertEnd( &( pxReadyTasksLists[2] ), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );
/* 启动调度器,开始多任务调度,启动成功则不返回 */
vTaskStartScheduler();
上一个函数的启动调度器函数没讲,继续展开。
调度器是操作系统的核心,其主要功能就是实现任务的切换,即从就绪列表里面找到优先级最高的任务,然后去执行该任务。
void vTaskStartScheduler( void )
{
/* 目前我们还不支持优先级,则手动指定第一个要运行的任务 */
pxCurrentTCB = &Task1TCB;
/* 启动调度器 */
if( xPortStartScheduler() != pdFALSE )
{
/* 调度器启动成功,则不会返回 */
}
}
上面函数调用了xPortStartScheduler函数,定义在port.c。
BaseType_t xPortStartScheduler( void )
{
/* 配置PendSV 和 SysTick 的中断优先级为最低 */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* 启动第一个任务,不再返回 */
prvStartFirstTask();
/* 不应该运行到这里 */
return 0;
}
为什么配置PendSV和SysTick的中断优先级为最低呢?
这是因为PendSV和SysTick都会涉及到系统调度。系统的其他硬件中断优先级要高于系统调度的优先级,即优先相应系统中的外部硬件中断。
上面函数调用了prvStartFirstTask函数,定义在port.c。使用了汇编语言。
/*
* 参考资料《STM32F10xxx Cortex-M3 programming manual》4.4.3,百度搜索“PM0056”即可找到这个文档
* 在Cortex-M中,内核外设SCB的地址范围为:0xE000 ED00-0xE000 ED3F
* 0xE000 ED08为SCB外设中SCB_VTOR这个寄存器的地址,里面存放的是向量表的起始地址,即MSP的地址
*/
__asm void prvStartFirstTask( void )
{
PRESERVE8 //当前栈8字节对齐,本章前面内容有解释过
/* 在Cortex-M中,0xE000 ED08是SCB_VTOR这个寄存器的地址,里面存放的是向量表的起始地址,即MSP的地址 */
ldr r0, =0xE000ED08 //把0xE000 ED08加载到寄存器R0
ldr r0, [r0] //将0xE000 ED08这个地址指向的内容加载到寄存器R0,此时R0=SCB_VTOR的值=0x0000 0000 ,即memory的起始地址
ldr r0, [r0] //将0x0000 0000这个地址指向的内容加载到寄存器R0,此时R0=0x2000 08DB
/* 设置主堆栈指针msp的值 */
msr msp, r0 //再把R0的值,即0x2000 08DB传给msp主堆栈指针
/* 使能全局中断 */
cpsie i //PRIMASK置0,即开中断
cpsie f //FAULTMASK置0,即开异常
dsb
isb
/* 调用SVC去启动第一个任务 */
svc 0 //SVC中断,产生系统调用
nop
nop
}
CPS指令用法
CPSID I ;PRIMASK=1 ;关中断
CPSIE I ;PRIMASK=0 ;开中断
CPSID F ;FAULTMASK=1 ;关异常
CPSIE F ;FAULTMASK=0 ;开异常
到这里,层层调用已经结束。但系统的调度,还没结束。
上面函数使用了SVC中断,SVC中断要想被成功响应,其函数名必须和向量表注册的名称一致,在启动文件的向量表中,SVC的中断服务函数注册的名称是SVC_Handle,所以SVC中断服务函数的名称我们也要写成SVC_Handle,这应该是一个常识吧。不懂得可以记一下小笔记。
但是呢,在FreeRTOS中,官方版本写的是vPortSVCHandler(),为了能过顺利响应SVC中断,我们可以这么处理。
修改中断向量表中SVC的注册的函数名(启动文件不建议修改,怕以后移植出现问题);
使用宏重复用SVC的中断服务名称。
如:
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
#define vPortSVCHandler SVC_Handler
vPortSVCHandler()如下
__asm void vPortSVCHandler( void )
{
extern pxCurrentTCB; //用于指向当前正在运行或即将要运行的任务的任务控制块
PRESERVE8 //当前栈8字节对齐,本章前面内容有解释过
ldr r3, =pxCurrentTCB /* 加载pxCurrentTCB这个地址到r3 */
ldr r1, [r3] /* 加载pxCurrentTCB这个地址指向的值到r1 */
ldr r0, [r1] /* 加载pxCurrentTCB指向的值到r0,目前r0的值等于第一个任务堆栈的栈顶 */
ldmia r0!, {r4-r11} /* 以r0为基地址,将栈里面的内容加载到r4~r11寄存器,同时r0会递增 */
msr psp, r0 /* 将r0的值,即任务的栈指针更新到psp */
isb
mov r0, #0 /* 设置r0的值为0 */
msr basepri, r0 /* 设置basepri寄存器的值为0,即所有的中断都没有被屏蔽 */
orr r14, #0xd /* 当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,
使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态 */
bx r14 /* 异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器:
xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
同时PSP的值也将更新,即指向任务栈的栈顶 */
}
#define taskYIELD() portYIELD()
/* 中断控制状态寄存器:0xe000ed04
* Bit 28 PENDSVSET: PendSV 悬起位
*/
#define portNVIC_INT_CTRL_REG ( * ( ( volatile uint32_t * ) 0xe000ed04 ) )
#define portNVIC_PENDSVSET_BIT ( 1UL << 28UL )
#define portSY_FULL_READ_WRITE ( 15 )
#define portYIELD() \
{ \
/* 触发PendSV,产生上下文切换 */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
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
ldr r3, =pxCurrentTCB /* 加载pxCurrentTCB的地址到r3 */
ldr r2, [r3] /* 加载pxCurrentTCB到r2 */
stmdb r0!, {r4-r11} /* 将CPU寄存器r4~r11的值存储到r0指向的地址 */
str r0, [r2] /* 将任务栈的新的栈顶指针存储到当前任务TCB的第一个成员,即栈顶指针 */
stmdb sp!, {r3, r14} /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,
调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护;
R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护 */
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY /* 进入临界段 */
msr basepri, r0
dsb
isb
bl vTaskSwitchContext /* 调用函数vTaskSwitchContext,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */
mov r0, #0 /* 退出临界段 */
msr basepri, r0
ldmia sp!, {r3, r14} /* 恢复r3和r14 */
ldr r1, [r3]
ldr r0, [r1] /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/
ldmia r0!, {r4-r11} /* 出栈 */
msr psp, r0
isb
bx r14 /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、
使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,
然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,
当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/
nop
}
上面函数都是一些基本的配置,然后调用到vTaskSwitchContext函数。
void vTaskSwitchContext( void )
{
/* 两个任务轮流切换 */
if( pxCurrentTCB == &Task1TCB )
{
pxCurrentTCB = &Task2TCB; //下次切换
}
else
{
pxCurrentTCB = &Task1TCB; //下次切换
}
}