FreeRTOS简单内核实现3 任务管理
0、思考与回答0.1、思考一
对于 Cortex-M4 内核的 MCU 在发生异常/中断时,哪些寄存器会自动入栈,哪些需要手动入栈?
会自动入栈的寄存器如下
[*]R0 - R3:通用寄存器
[*]R12:通用寄存器
[*]LR (Link Register):链接寄存器,保存返回地址
[*]PC (Program Counter):程序计数器,保存当前执行指令的地址
[*]xPSR (Program Status Register):程序状态寄存器
需要手动入栈的寄存器如下
[*]R4 - R11:其他通用寄存器
0.2、思考二
这些入栈的寄存器是怎么找到要入栈的地址的呢?
依靠堆栈指针来确定入栈的地址,Cortex-M4 的堆栈指针寄存器 SP 在同一物理位置上有 MSP 和 PSP 两个堆栈指针,默认情况下会使用主堆栈指针 MSP
0.3、思考三
自动入栈的寄存器什么时候入栈,什么时候出栈恢复原来的处理器状态?
进入异常处理时会发生自动入栈,当异常处理完成并执行异常返回指令(bx r14)时,处理器会自动从堆栈中弹出这些寄存器的值
1、任务控制块
通常使用名为任务控制块(TCB)的结构体来对每个任务进行管理,该结构体中包含了任务管理所需要的一些重要成员,其中栈顶指针 pxTopOfStack 作为结构体的第一个成员,其地址也即任务控制块的地址,具体如下所示
/* task.h */
// 任务控制块
typedef struct tskTaskControlBlock
{
volatile StackType_t*pxTopOfStack; // 栈顶
ListItem_t xStateListItem; // 任务节点
StackType_t *pxStack; // 任务栈起始地址
char pcTaskName;// 任务名称
}tskTCB;
typedef tskTCB TCB_t;configMAX_TASK_NAME_LEN 是一个宏,用于设置任务名称长度,具体定义如下
/* FreeRTOSConfig.h */
// 任务名称字符串长度
#define configMAX_TASK_NAME_LEN 242、创建任务
2.1、xTaskCreateStatic( )
静态创建任务时需要指定任务栈(地址和大小)、任务函数、任务控制块等参数,该函数就是负责将这些分散的变量联系在一起,方便后续对任务进行管理,但是其并不是真正的创建任务的函数,具体如下所示
/* task.c */
// 静态创建任务函数
#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 puxTaskBuffer, // 任务栈起始指针
TCB_t* const pxTaskBuffer) // 任务栈控制指针
{
TCB_t* pxNewTCB;
TaskHandle_t xReturn;
// 任务栈控制指针和任务栈起始指针不为空
if((pxTaskBuffer != NULL) && (puxTaskBuffer != NULL))
{
pxNewTCB = (TCB_t*)pxTaskBuffer;
// 将任务控制块的 pxStack 指针指向任务栈起始地址
pxNewTCB->pxStack = (StackType_t*)puxTaskBuffer;
// 真正的创建任务函数
prvInitialiseNewTask(pxTaskCode,
pcName,
ulStackDepth,
pvParameters,
&xReturn,
pxNewTCB);
}
else
{
xReturn = NULL;
}
// 任务创建成功后应该返回任务句柄,否则返回 NULL
return xReturn;
}
#endif
/* task.h */
// 函数声明
TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode,
const char* const pcName,
const uint32_t ulStackDepth,
void* const pvParameters,
StackType_t* const puxTaskBuffer,
TCB_t* const pxTaskBuffer);configSUPPORT_STATIC_ALLOCATION 是一个宏,用于配置和裁剪 RTOS 静态创建任务的功能,具体定义如下所示
/* FreeRTOSConfig.h */
// 是否支持静态方式创建任务
#define configSUPPORT_STATIC_ALLOCATION 1TaskHandle_t 是一个 void * 类型的变量,用于表示任务句柄,TaskFunction_t 是一个函数指针,用于表示任务函数,具体定义如下所示
/* task.h */
// 任务句柄指针
typedef void* TaskHandle_t;
// 任务函数指针
typedef void (*TaskFunction_t)(void *);2.2、prvInitialiseNewTask( )
函数 xTaskCreateStatic() 最终调用了真正的创建任务函数 prvInitialiseNewTask() ,该函数主要是对任务栈内存、任务控制块成员等进行初始化,具体如下所示
/* task.c */
// 使用的外部函数声明
extern StackType_t* pxPortInitialiseStack(StackType_t* pxTopOfStack,
TaskFunction_t pxCode,
void* pvParameters);
// 真正的创建任务函数
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);
// 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 = pcName;
if(pcName == 0x00)
break;
}
pxNewTCB->pcTaskName = '\0';
// 初始化链表项
vListInitialiseItem(&(pxNewTCB->xStateListItem));
// 设置该链表项的拥有者为 pxNewTCB
listSET_LIST_ITEM_OWNER(&(pxNewTCB->xStateListItem), pxNewTCB);
// 初始化任务栈
pxNewTCB->pxTopOfStack =
pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters);
if((void*)pxCreatedTask != NULL)
{
*pxCreatedTask = (TaskHandle_t)pxNewTCB;
}
}2.3、pxPortInitialiseStack( )
初始化任务栈函数,内存具体被初始化为什么样可以阅读 "2.4 任务内存详解" 小节
/* port.c */
// 错误出口
static void prvTaskExitError(void)
{
for(;;);
}
// 初始化栈内存
StackType_t* pxPortInitialiseStack(StackType_t* pxTopOfStack,
TaskFunction_t pxCode,
void* pvParameters)
{
// 异常发生时,自动加载到CPU的内容
pxTopOfStack --;
*pxTopOfStack = portINITIAL_XPSR;
pxTopOfStack --;
*pxTopOfStack = ((StackType_t)pxCode) & portSTART_ADDRESS_MASK;
pxTopOfStack --;
*pxTopOfStack = (StackType_t)prvTaskExitError;
// r12、r3、r2 和 r1 默认初始化为 0
pxTopOfStack -= 5;
*pxTopOfStack = (StackType_t)pvParameters;
// 异常发生时,手动加载到 CPU 的内容
pxTopOfStack -= 8;
// 返回栈顶指针,此时 pxTopOfStack 指向空闲栈
return pxTopOfStack;
}/* portMacro.h */
#define portINITIAL_XPSR (0x01000000)
#define portSTART_ADDRESS_MASK ((StackType_t) 0xFFFFFFFEUL)2.4、任务内存详解
使用静态方式创建任务之前需要提前定义好任务句柄、任务栈空间、任务控制块和任务函数,具体如下程序所示
// 任务句柄
TaskHandle_t Task_Handle;
// 任务栈大小
#define TASK_STACK_SIZE 128
// 任务栈空间
StackType_t TaskStack;
// 任务控制块
TCB_t TaskTCB;
// 任务函数
void TaskFunction(void *parg)
{
for(;;)
{
}
}定义好这些变量之后都会在 RAM 中占用一定的空间,其中任务控制块 TaskTCB 的地址和结构体第一个成员 pxTopOfStack 是同一个地址,假设这些变量在空间中的占用情况如下图所示
将这些定义好的变量作为参数传递给静态创建任务函数 xTaskCreateStatic(),具体如下程序所示
// 静态方式创建任务
Task_Handle = xTaskCreateStatic(TaskFunction,
"Task",
128,
TaskParameters,
TaskStack,
TaskTCB);该函数执行完毕之后,最终这些变量在空间中的占用情况如下图所示
为什么要按照顺序这么存放?
因为在 ARM Cortex-M 处理器中,当异常发生时硬件会自动按照上图的顺序将寄存器推入当前的栈中,同时退出异常时也会按照其相反的顺序从当前栈中取值加载到 MCU 寄存器中
为什么任务栈顶 PSR 的值为 0x01000000 ?
PSR 是程序状态寄存器,对于异常来说具体的寄存器为 EPSR ,其第 24 位为 Thumb 状态位(T位),在 ARM Cortex-M 架构中,所有代码都必须在 Thumb 模式下执行,寄存器定义可以在 Cortex-M4 Devices Generic User Guide 手册中找到,具体如下图所示
可以发现当执行 BX 指令退出异常时(后面启动任务 / 任务调度会频繁使用到异常退出指令),会将 T 位清零,而一旦清零并尝试执行指令就会导致 MCU 故障或锁定,因此必须在异常退出自动恢复 MCU 寄存器时将 T 位重新置 1
为什么任务栈存放任务函数入口地址的内容为 pxCode & portSTART_ADDRESS_MASK?
简单来说这步操作其实是为了内存对齐,portSTART_ADDRESS_MASK 是一个宏,pxCode & portSTART_ADDRESS_MASK 只是为了确保 pxCode 任务函数入口地址最低位置为 0 ,实际测试中直接将该位置写入 pxCode 也是可以的,读者可自行测试
3、就绪链表
3.1、定义
为什么要定义链表?
使用链表可以将各个独立的任务链接起来,对于多任务时会方便任务管理
/* task.c */
// 就绪链表
List_t pxReadyTasksLists;3.2、prvInitialiseTaskLists( )
链表创建后不能直接使用,需要调用 vListInitialise() 函数对链表进行初始化,由于后续会创建多个链表,因此将链表初始化操作包装为一个函数,具体如下所示
/* task.c */
// 就绪列表初始化函数
void prvInitialiseTaskLists(void)
{
vListInitialise(&pxReadyTasksLists);
}
/* task.h */
// 函数声明
void prvInitialiseTaskLists(void);4、任务调度器
任务调度流程如下所示
[*]启动调度器
[*]vTaskStartScheduler( )
[*]xPortStartScheduler( )
[*]启动第一个任务
[*]prvStartFirstTask( )
[*]vPortSVCHandler( )
[*]产生任务调度
[*]taskYIELD( )
[*]xPortPendSVHandler( )
[*]vTaskSwitchContext( )
4.1、vTaskStartScheduler( )
任务创建好了怎么执行呢?
这就是启动调度器函数的工作,选择一个任务然后启动第一个任务的执行
怎么选择任务?
通过一个名为 pxCurrentTCB 的 TCB_t * 类型的指针选择要执行的任务,该指针始终指向需要运行的任务的任务控制块
选择哪一个?
在创建多个任务时,可以通过一些方法(比如根据任务优先级)来选择先执行的任务,但是这里我们的 RTOS 内核还不支持优先级,因此可以先手动指定要执行的第一个任务
/* task.c */
// 当前 TCB 指针
TCB_t volatile *pxCurrentTCB = NULL;
// 使用的外部函数声明
extern BaseType_t xPortStartScheduler(void);
// 在 main.c 中定义的两个任务声明
extern TCB_t Task1TCB;
extern TCB_t Task2TCB;
// 启动任务调度器
void vTaskStartScheduler(void)
{
// 手动指定第一个运行的任务
pxCurrentTCB = &Task1TCB;
// 启动调度器
if(xPortStartScheduler() != pdFALSE)
{
// 调度器启动成功则不会到这里
}
}
/* task.h */
// 函数声明
void vTaskStartScheduler(void);4.2、xPortStartScheduler( )
vTaskStartScheduler() 函数选择了要执行的任务,启动第一个任务执行的重任交给了 4.2 ~ 4.4 小节的三个函数,xPortStartScheduler() 函数主要设置 PendSV 和 SysTick 的中断优先级为最低,然后调用了 prvStartFirstTask() 函数
/* 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 中断优先级最低?
在 RTOS 中,PendSV 和 SysTick 中断服务函数均与任务调度有关,而任务调度优先级需要小于外部硬件中断的优先级,所以要将其设置为最低优先级
怎么设置 PendSV 和 SysTick 中断优先级为最低?
这里是直接操作寄存器的方法来设置 Cortex-M4 的 PendSV 和 SysTick 中断优先级的,可以在 Cortex-M4 Devices Generic User Guide 手册中找到有关 SHPR3 寄存器的地址与其每一位的含义,具体如下图所示
根据上图应该很好理解程序是怎么将 PendSV 和 SysTick 优先级设置为 15 的(NVIC 为 4 位抢占优先级,最低优先级就是 15)
/* portMacro.h */
#define portNVIC_SYSPRI2_REG (*((volatile uint32_t *) 0xE000ED20))
#define portNVIC_PENDSV_PRI (((uint32_t)configKERNEL_INTERRUPT_PRIORITY) << 16UL)
#define portNVIC_SYSTICK_PRI (((uint32_t)configKERNEL_INTERRUPT_PRIORITY) << 24UL)启动任务调度器后的程序执行流程如下所示,第一次执行完步骤 6 后会不断重复步骤 3 ~ 6
[*]vTaskStartScheduler() -> pxCurrentTCB = &Task1TCB; -> xPortStartScheduler()
[*]-> prvStartFirstTask() -> vPortSVCHandler() -> Task1_Entry()
[*]-> taskYIELD() -> xPortPendSVHandler() -> vTaskSwitchContext()
[*]-> Task2_Entry()
[*]-> taskYIELD() -> xPortPendSVHandler() -> vTaskSwitchContext()
[*]-> Task1_Entry()
使用逻辑分析仪捕获 GREEN_LED 和 ORANGE_LED 两个引脚的电平变化,具体如下图所示
可以发现两个任务不是并行运行的,而是一个任务执行完,第二个任务才会得到执行,所以两个 LED 灯引脚电平都是每隔 600ms 翻转一次,不是我们期待的 Task1 引脚电平每隔 100 ms 翻转一次,Task2 引脚电平每隔 500ms 翻转一次,这样的效果其实可被如下简单代码取代
/* FreeRTOSConfig.h */
// 设置内核中断优先级(最低优先级)
#define configKERNEL_INTERRUPT_PRIORITY 156.2、待改进
当前 RTOS 简单内核已实现的功能有
[*]静态方式创建任务
[*]手动切换任务
当前 RTOS 简单内核存在的缺点有
[*]不支持任务优先级
[*]任务不能并行运行
[*]无中断临界段保护
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页:
[1]