0、思考与回答
0.1、思考一
对于 Cortex-M4 内核的 MCU 在发生异常/中断时,哪些寄存器会自动入栈,哪些需要手动入栈?
会自动入栈的寄存器如下
- R0 - R3:通用寄存器
- R12:通用寄存器
- LR (Link Register):链接寄存器,保存返回地址
- PC (Program Counter):程序计数器,保存当前执行指令的地址
- xPSR (Program Status Register):程序状态寄存器
需要手动入栈的寄存器如下
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[configMAX_TASK_NAME_LEN]; // 任务名称
- }tskTCB;
- typedef tskTCB TCB_t;
复制代码 configMAX_TASK_NAME_LEN 是一个宏,用于设置任务名称长度,具体定义如下- /* FreeRTOSConfig.h */
- // 任务名称字符串长度
- #define configMAX_TASK_NAME_LEN 24
复制代码 2、创建任务
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 1
复制代码 TaskHandle_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[x] = pcName[x];
- if(pcName[x] == 0x00)
- break;
- }
- pxNewTCB->pcTaskName[configMAX_TASK_NAME_LEN-1] = '\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[TASK_STACK_SIZE];
- // 任务控制块
- 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 15
复制代码 6.2、待改进
当前 RTOS 简单内核已实现的功能有
当前 RTOS 简单内核存在的缺点有
- 不支持任务优先级
- 任务不能并行运行
- 无中断临界段保护
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |