蜴间囝 发表于 2025-6-8 13:23:52

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]
查看完整版本: FreeRTOS简单内核实现3 任务管理