找回密码
 立即注册
首页 业界区 安全 FreeRTOS简单内核实现3 任务管理

FreeRTOS简单内核实现3 任务管理

蜴间囝 2025-6-8 13:23:52
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 作为结构体的第一个成员,其地址也即任务控制块的地址,具体如下所示
  1. /* task.h */
  2. // 任务控制块
  3. typedef struct tskTaskControlBlock
  4. {
  5.     volatile StackType_t  *pxTopOfStack;                        // 栈顶
  6.     ListItem_t            xStateListItem;                       // 任务节点
  7.     StackType_t           *pxStack;                             // 任务栈起始地址
  8.     char                  pcTaskName[configMAX_TASK_NAME_LEN];  // 任务名称
  9. }tskTCB;
  10. typedef tskTCB TCB_t;
复制代码
configMAX_TASK_NAME_LEN 是一个宏,用于设置任务名称长度,具体定义如下
  1. /* FreeRTOSConfig.h */
  2. // 任务名称字符串长度
  3. #define configMAX_TASK_NAME_LEN                 24
复制代码
2、创建任务

2.1、xTaskCreateStatic( )

静态创建任务时需要指定任务栈(地址和大小)、任务函数、任务控制块等参数,该函数就是负责将这些分散的变量联系在一起,方便后续对任务进行管理,但是其并不是真正的创建任务的函数,具体如下所示
  1. /* task.c */
  2. // 静态创建任务函数
  3. #if (configSUPPORT_STATIC_ALLOCATION == 1)
  4. TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode,     // 任务函数
  5.                             const char* const pcName,         // 任务名称
  6.                             const uint32_t ulStackDepth,      // 任务栈深度
  7.                             void* const pvParameters,         // 任务参数
  8.                             StackType_t* const puxTaskBuffer, // 任务栈起始指针
  9.                             TCB_t* const pxTaskBuffer)        // 任务栈控制指针
  10. {
  11.         TCB_t* pxNewTCB;
  12.         TaskHandle_t xReturn;
  13.         // 任务栈控制指针和任务栈起始指针不为空
  14.         if((pxTaskBuffer != NULL) && (puxTaskBuffer != NULL))
  15.         {
  16.                 pxNewTCB = (TCB_t*)pxTaskBuffer;
  17.                 // 将任务控制块的 pxStack 指针指向任务栈起始地址
  18.                 pxNewTCB->pxStack = (StackType_t*)puxTaskBuffer;
  19.                
  20.                 // 真正的创建任务函数
  21.                 prvInitialiseNewTask(pxTaskCode,
  22.                                                          pcName,
  23.                                                          ulStackDepth,
  24.                                                          pvParameters,
  25.                                                          &xReturn,
  26.                                                          pxNewTCB);
  27.         }
  28.         else
  29.         {
  30.                 xReturn = NULL;
  31.         }
  32.         // 任务创建成功后应该返回任务句柄,否则返回 NULL
  33.         return xReturn;
  34. }
  35. #endif
  36. /* task.h */
  37. // 函数声明
  38. TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode,
  39.                             const char* const pcName,
  40.                             const uint32_t ulStackDepth,
  41.                             void* const pvParameters,
  42.                             StackType_t* const puxTaskBuffer,
  43.                             TCB_t* const pxTaskBuffer);
复制代码
configSUPPORT_STATIC_ALLOCATION 是一个宏,用于配置和裁剪 RTOS 静态创建任务的功能,具体定义如下所示
  1. /* FreeRTOSConfig.h */
  2. // 是否支持静态方式创建任务
  3. #define configSUPPORT_STATIC_ALLOCATION         1
复制代码
TaskHandle_t 是一个 void * 类型的变量,用于表示任务句柄,TaskFunction_t 是一个函数指针,用于表示任务函数,具体定义如下所示
  1. /* task.h */
  2. // 任务句柄指针
  3. typedef void* TaskHandle_t;
  4. // 任务函数指针
  5. typedef void (*TaskFunction_t)(void *);
复制代码
2.2、prvInitialiseNewTask( )

函数 xTaskCreateStatic() 最终调用了真正的创建任务函数 prvInitialiseNewTask() ,该函数主要是对任务栈内存、任务控制块成员等进行初始化,具体如下所示
  1. /* task.c */
  2. // 使用的外部函数声明
  3. extern StackType_t* pxPortInitialiseStack(StackType_t* pxTopOfStack,
  4.                                           TaskFunction_t pxCode,
  5.                                           void* pvParameters);
  6. // 真正的创建任务函数                                                                                                                                 
  7. static void prvInitialiseNewTask(TaskFunction_t pxTaskCode,    // 任务函数
  8.                             const char* const pcName,          // 任务名称
  9.                             const uint32_t ulStackDepth,       // 任务栈深度
  10.                             void* const pvParameters,          // 任务参数
  11.                             TaskHandle_t* const pxCreatedTask, // 任务句柄
  12.                             TCB_t* pxNewTCB)                   // 任务栈控制指针
  13. {
  14.         StackType_t *pxTopOfStack;
  15.         UBaseType_t x;
  16.        
  17.         // 栈顶指针,用于指向分配的任务栈空间的最高地址
  18.         pxTopOfStack = pxNewTCB->pxStack + (ulStackDepth - (uint32_t)1);
  19.         // 8 字节对齐
  20.         pxTopOfStack = (StackType_t*)(((uint32_t)pxTopOfStack)
  21.                                      & (~((uint32_t)0x0007)));
  22.         // 保存任务名称到TCB中
  23.         for(x = (UBaseType_t)0;x < (UBaseType_t)configMAX_TASK_NAME_LEN;x++)
  24.         {
  25.                 pxNewTCB->pcTaskName[x] = pcName[x];
  26.                 if(pcName[x] == 0x00)
  27.                         break;
  28.         }
  29.         pxNewTCB->pcTaskName[configMAX_TASK_NAME_LEN-1] = '\0';
  30.        
  31.         // 初始化链表项
  32.         vListInitialiseItem(&(pxNewTCB->xStateListItem));
  33.        
  34.         // 设置该链表项的拥有者为 pxNewTCB
  35.         listSET_LIST_ITEM_OWNER(&(pxNewTCB->xStateListItem), pxNewTCB);
  36.        
  37.         // 初始化任务栈
  38.         pxNewTCB->pxTopOfStack =
  39.                   pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters);
  40.        
  41.         if((void*)pxCreatedTask != NULL)
  42.         {
  43.             *pxCreatedTask = (TaskHandle_t)pxNewTCB;
  44.         }
  45. }
复制代码
2.3、pxPortInitialiseStack( )

初始化任务栈函数,内存具体被初始化为什么样可以阅读 "2.4 任务内存详解" 小节
  1. /* port.c */
  2. // 错误出口
  3. static void prvTaskExitError(void)
  4. {
  5.     for(;;);
  6. }
  7. // 初始化栈内存
  8. StackType_t* pxPortInitialiseStack(StackType_t* pxTopOfStack,
  9.                                                                    TaskFunction_t pxCode,
  10.                                                                    void* pvParameters)
  11. {
  12.     // 异常发生时,自动加载到CPU的内容
  13.     pxTopOfStack --;
  14.     *pxTopOfStack = portINITIAL_XPSR;
  15.     pxTopOfStack --;
  16.     *pxTopOfStack = ((StackType_t)pxCode) & portSTART_ADDRESS_MASK;
  17.     pxTopOfStack --;
  18.     *pxTopOfStack = (StackType_t)prvTaskExitError;
  19.        
  20.     // r12、r3、r2 和 r1 默认初始化为 0
  21.     pxTopOfStack -= 5;
  22.     *pxTopOfStack = (StackType_t)pvParameters;
  23.        
  24.     // 异常发生时,手动加载到 CPU 的内容
  25.     pxTopOfStack -= 8;
  26.        
  27.     // 返回栈顶指针,此时 pxTopOfStack 指向空闲栈
  28.     return pxTopOfStack;
  29. }
复制代码
  1. /* portMacro.h */
  2. #define portINITIAL_XPSR            (0x01000000)
  3. #define portSTART_ADDRESS_MASK      ((StackType_t) 0xFFFFFFFEUL)
复制代码
2.4、任务内存详解

使用静态方式创建任务之前需要提前定义好任务句柄、任务栈空间、任务控制块和任务函数,具体如下程序所示
  1. // 任务句柄
  2. TaskHandle_t Task_Handle;
  3. // 任务栈大小
  4. #define TASK_STACK_SIZE                    128
  5. // 任务栈空间
  6. StackType_t TaskStack[TASK_STACK_SIZE];
  7. // 任务控制块
  8. TCB_t TaskTCB;
  9. // 任务函数
  10. void TaskFunction(void *parg)
  11. {
  12.         for(;;)
  13.         {
  14.         }
  15. }
复制代码
定义好这些变量之后都会在 RAM 中占用一定的空间,其中任务控制块 TaskTCB 的地址和结构体第一个成员 pxTopOfStack 是同一个地址,假设这些变量在空间中的占用情况如下图所示
1.png

将这些定义好的变量作为参数传递给静态创建任务函数 xTaskCreateStatic()  ,具体如下程序所示
  1. // 静态方式创建任务
  2. Task_Handle = xTaskCreateStatic(TaskFunction,
  3.                                                            "Task",
  4.                                                            128,
  5.                                                            TaskParameters,
  6.                                                            TaskStack,
  7.                                                            TaskTCB);
复制代码
该函数执行完毕之后,最终这些变量在空间中的占用情况如下图所示
2.png

为什么要按照顺序这么存放?
因为在 ARM Cortex-M 处理器中,当异常发生时硬件会自动按照上图的顺序将寄存器推入当前的栈中,同时退出异常时也会按照其相反的顺序从当前栈中取值加载到 MCU 寄存器中
为什么任务栈顶 PSR 的值为 0x01000000 ?
PSR 是程序状态寄存器,对于异常来说具体的寄存器为 EPSR ,其第 24 位为 Thumb 状态位(T位),在 ARM Cortex-M 架构中,所有代码都必须在 Thumb 模式下执行,寄存器定义可以在 Cortex-M4 Devices Generic User Guide 手册中找到,具体如下图所示
3.png

可以发现当执行 BX 指令退出异常时(后面启动任务 / 任务调度会频繁使用到异常退出指令),会将 T 位清零,而一旦清零并尝试执行指令就会导致 MCU 故障或锁定,因此必须在异常退出自动恢复 MCU 寄存器时将 T 位重新置 1
为什么任务栈存放任务函数入口地址的内容为 pxCode & portSTART_ADDRESS_MASK?
简单来说这步操作其实是为了内存对齐,portSTART_ADDRESS_MASK 是一个宏,pxCode & portSTART_ADDRESS_MASK 只是为了确保 pxCode 任务函数入口地址最低位置为 0 ,实际测试中直接将该位置写入 pxCode 也是可以的,读者可自行测试
3、就绪链表

3.1、定义

为什么要定义链表?
使用链表可以将各个独立的任务链接起来,对于多任务时会方便任务管理
  1. /* task.c */
  2. // 就绪链表
  3. List_t pxReadyTasksLists;
复制代码
3.2、prvInitialiseTaskLists( )

链表创建后不能直接使用,需要调用 vListInitialise() 函数对链表进行初始化,由于后续会创建多个链表,因此将链表初始化操作包装为一个函数,具体如下所示
  1. /* task.c */
  2. // 就绪列表初始化函数
  3. void prvInitialiseTaskLists(void)
  4. {
  5.         vListInitialise(&pxReadyTasksLists);
  6. }
  7. /* task.h */
  8. // 函数声明
  9. void prvInitialiseTaskLists(void);
复制代码
4、任务调度器

任务调度流程如下所示

  • 启动调度器

    • vTaskStartScheduler( )
    • xPortStartScheduler( )

  • 启动第一个任务

    • prvStartFirstTask( )
    • vPortSVCHandler( )

  • 产生任务调度

    • taskYIELD( )
    • xPortPendSVHandler( )
    • vTaskSwitchContext( )

4.1、vTaskStartScheduler( )

任务创建好了怎么执行呢?
这就是启动调度器函数的工作,选择一个任务然后启动第一个任务的执行
怎么选择任务?
通过一个名为 pxCurrentTCB 的 TCB_t * 类型的指针选择要执行的任务,该指针始终指向需要运行的任务的任务控制块
选择哪一个?
在创建多个任务时,可以通过一些方法(比如根据任务优先级)来选择先执行的任务,但是这里我们的 RTOS 内核还不支持优先级,因此可以先手动指定要执行的第一个任务
  1. /* task.c */
  2. // 当前 TCB 指针
  3. TCB_t volatile *pxCurrentTCB = NULL;
  4. // 使用的外部函数声明
  5. extern BaseType_t xPortStartScheduler(void);
  6. // 在 main.c 中定义的两个任务声明
  7. extern TCB_t Task1TCB;
  8. extern TCB_t Task2TCB;
  9. // 启动任务调度器
  10. void vTaskStartScheduler(void)
  11. {
  12.         // 手动指定第一个运行的任务
  13.         pxCurrentTCB = &Task1TCB;
  14.         // 启动调度器
  15.         if(xPortStartScheduler() != pdFALSE)
  16.         {
  17.                 // 调度器启动成功则不会到这里
  18.         }
  19. }
  20. /* task.h */
  21. // 函数声明
  22. void vTaskStartScheduler(void);
复制代码
4.2、xPortStartScheduler( )

vTaskStartScheduler() 函数选择了要执行的任务,启动第一个任务执行的重任交给了 4.2 ~ 4.4 小节的三个函数,xPortStartScheduler() 函数主要设置 PendSV 和 SysTick 的中断优先级为最低,然后调用了 prvStartFirstTask() 函数
  1. /* port.c */
  2. // 启动调度器
  3. BaseType_t xPortStartScheduler(void)
  4. {
  5.         // 设置 PendSV 和 SysTick 中断优先级为最低
  6.         portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
  7.         portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
  8.        
  9.         // 初始化滴答定时器
  10.        
  11.         // 启动第一个任务,不再返回
  12.         prvStartFirstTask();
  13.        
  14.         // 正常不会运行到这里
  15.         return 0;
  16. }
复制代码
为什么要设置 PendSV 和 SysTick 中断优先级最低?
在 RTOS 中,PendSV 和 SysTick 中断服务函数均与任务调度有关,而任务调度优先级需要小于外部硬件中断的优先级,所以要将其设置为最低优先级
怎么设置 PendSV 和 SysTick 中断优先级为最低?
这里是直接操作寄存器的方法来设置 Cortex-M4 的 PendSV 和 SysTick 中断优先级的,可以在 Cortex-M4 Devices Generic User Guide 手册中找到有关 SHPR3 寄存器的地址与其每一位的含义,具体如下图所示
4.png

根据上图应该很好理解程序是怎么将 PendSV 和 SysTick 优先级设置为 15 的(NVIC 为 4 位抢占优先级,最低优先级就是 15)
  1. /* portMacro.h */
  2. #define portNVIC_SYSPRI2_REG        (*((volatile uint32_t *) 0xE000ED20))
  3. #define portNVIC_PENDSV_PRI         (((uint32_t)configKERNEL_INTERRUPT_PRIORITY) << 16UL)
  4. #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 两个引脚的电平变化,具体如下图所示
5.png

可以发现两个任务不是并行运行的,而是一个任务执行完,第二个任务才会得到执行,所以两个 LED 灯引脚电平都是每隔 600ms 翻转一次,不是我们期待的 Task1 引脚电平每隔 100 ms 翻转一次,Task2 引脚电平每隔 500ms 翻转一次,这样的效果其实可被如下简单代码取代
  1. /* FreeRTOSConfig.h */
  2. // 设置内核中断优先级(最低优先级)
  3. #define configKERNEL_INTERRUPT_PRIORITY         15
复制代码
6.2、待改进

当前 RTOS 简单内核已实现的功能有

  • 静态方式创建任务
  • 手动切换任务
当前 RTOS 简单内核存在的缺点有

  • 不支持任务优先级
  • 任务不能并行运行
  • 无中断临界段保护

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册