开启任务调度器API函数简介
- 任务调度
- 开启任务调度器API函数简介
- **函数** **vTaskStartScheduler()**
- **函数** **xPortStartScheduler()**
- **函数** **prvStartFirstTask()**
- **函数** **vPortSVCHandler()**
- **注意**
- 补充
- **出栈/压栈汇编指令详解**
任务调度
开启任务调度器API函数简介
函数 vTaskStartScheduler()
用于启动任务调度器,任务调度器启动后,FreeRTOS 便会开始进行任务调度,除非调用函数 xTaskEndScheduler()停止任务调度器,否则不会再返回
该函数内部如下
void vTaskStartScheduler( void )
{BaseType_t xReturn;/* 如果启用静态内存管理,则优先使用静态方式创建空闲任务 */
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
{StaticTask_t * pxIdleTaskTCBBuffer = NULL;StackType_t * pxIdleTaskStackBuffer = NULL;uint32_t ulIdleTaskStackSize;vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer,&pxIdleTaskStackBuffer,&ulIdleTaskStackSize);xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,configIDLE_TASK_NAME,ulIdleTaskStackSize,( void * ) NULL,portPRIVILEGE_BIT,pxIdleTaskStackBuffer,pxIdleTaskTCBBuffer);if( xIdleTaskHandle != NULL ){xReturn = pdPASS;}else{xReturn = pdFAIL;}
}
#else/* 未启用静态内存管理,则使用动态方式创建空闲任务 */
{xReturn = xTaskCreate( prvIdleTask,configIDLE_TASK_NAME,configMINIMAL_STACK_SIZE,( void * ) NULL,portPRIVILEGE_BIT,&xIdleTaskHandle);
}
#endif/* 如果启用软件定时器,则需要创建定时器服务任务 */
#if ( configUSE_TIMERS == 1 )
{if( xReturn == pdPASS ){xReturn = xTimerCreateTimerTask();}else{mtCOVERAGE_TEST_MARKER();}
}
#endifif( xReturn == pdPASS ){
#ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
{/* 此函数用于添加一些附加初始化,不用理会 */freertos_tasks_c_additions_init();
}
#endif/* FreeRTOS 关闭中断,* 以保证在开启任务任务调度器之前或过程中,SysTick 不会产生中断,* 在第一个任务开始运行时,会重新打开中断。*/portDISABLE_INTERRUPTS();#if ( configUSE_NEWLIB_REENTRANT == 1 )
{/* Newlib 相关 */_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif/* 初始化一些全局变量* xNextTaskUnblockTime: 下一个距离取消任务阻塞的时间,初始化为最大值* xSchedulerRunning: 任务调度器运行标志,设为已运行* xTickCount: 系统使用节拍计数器,宏 configINITIAL_TICK_COUNT 默认为 0*/xNextTaskUnblockTime = portMAX_DELAY;xSchedulerRunning = pdTRUE;xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;/* 为任务运行时间统计功能初始化功能时基定时器* 是否启用该功能,可在 FreeRTOSConfig.h 文件中进行配置*/portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();/* 调试使用,不用理会 */traceTASK_SWITCHED_IN();/* 设置用于系统时钟节拍的硬件定时器(SysTick)* 会在这个函数中进入第一个任务,并开始任务调度* 任务调度开启后,便不会再返回*/if( xPortStartScheduler() != pdFALSE ){}else{}}else{/* 动态方式创建空闲任务和定时器服务任务(如果有)时,因分配给 FreeRTOS 的堆空间* 不足,导致任务无法成功创建 */configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );}/* 防止编译器警告,不用理会 */( void ) xIdleTaskHandle;/* 调试使用,不用理会 */( void ) uxTopUsedPriority;
}
从上面的代码可以看出,函数 vTaskStartScheduler()主要做了六件事情。
-
创建空闲任务,根据是否支持静态内存管理,使用静态方式或动态方式创建空闲任务。
-
创建定时器服务任务,创建定时器服务任务需要配置启用软件定时器,创建定时器服务任务,同样是根据是否配置支持静态内存管理,使用静态或动态方式创建定时器服务任务。
-
关闭中断,使用 portDISABLE_INTERRUPT()关闭中断,这种方式只关闭受 FreeRTOS 管理的中断。关闭中断主要是为了防止 SysTick 中断在任务调度器开启之前或过程中,产生中断。FreeRTOS 会在开始运行第一个任务时,重新打开中断。
-
初始化一些全局变量,并将任务调度器的运行标志设置为已运行。
-
初始化任务运行时间统计功能的时基定时器,任务运行时间统计功能需要一个硬件定时器提供高精度的计数,这个硬件定时器就在这里进行配置,如果配置不启用任务运行时间统计功能的,就无需进行这项硬件定时器的配置。
-
最后就是调用函数 xPortStartScheduler()。
函数 xPortStartScheduler()
函数 xPortStartScheduler()完成启动任务调度器中与硬件架构相关的配置部分,以及启动第一个任务
具体的代码如下所示:
BaseType_t xPortStartScheduler( void )
{
#if ( configASSERT_DEFINED == 1 )
{/* 检测用户在 FreeRTOSConfig.h 文件中对中断相关部分的配置是否有误,代码省略 */
}
#endif/* 设置 PendSV 和 SysTick 的中断优先级为最低优先级 (在中断管理中有详细介绍)*/portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;/* 配置 SysTick* 清空 SysTick 的计数值* 根据 configTICK_RATE_HZ 配置 SysTick 的重装载值* 开启 SysTick 计数和中断*/vPortSetupTimerInterrupt();/* 初始化临界区嵌套次数计数器为 0 */uxCriticalNesting = 0;/* 使能 FPU* 仅 ARM Cortex-M4/M7 内核 MCU 才有此行代码* ARM Cortex-M3 内核 MCU 无 FPU*/prvEnableVFP();/* 在进出异常时,自动保存和恢复 FPU 相关寄存器* 仅 ARM Cortex-M4/M7 内核 MCU 才有此行代码* ARM Cortex-M3 内核 MCU 无 FPU*/*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;/* 启动第一个任务 */prvStartFirstTask();/* 不会返回这里 */return 0;
}
-
在启用断言的情况下,函数 xPortStartScheduler()会检测用户在 FreeRTOSConfig.h 文件中对中断的相关配置是否有误,感兴趣的读者请自行查看这部分的相关代码。
-
配置 PendSV 和 SysTick 的中断优先级为最低优先级
-
调用函数 vPortSetupTimerInterrupt()配置 SysTick,函数 vPortSetupTimerInterrupt()首先会将 SysTick 当 前 计 数 值 清 空 , 并 根 据 FreeRTOSConfig.h 文件中配置的configSYSTICK_CLOCK_HZ(SysTick 时钟源频率)和 configTICK_RATE_HZ(系统时钟节拍频率)计算并设置 SysTick 的重装载值,然后启动 SysTick 计数和中断。(具体可结合函数内部和M3权威指南P134)
-
初始化临界区嵌套计数器为 0。
-
调用函数 prvEnableVFP()使能 FPU,因为 ARM Cortex-M3 内核 MCU 无 FPU,此函数仅在 ARM Cortex-M4/M7 内核 MCU 平台上被调用,执行改函数后 FPU 被开启。
-
接下来将 FPCCR 寄存器的[31:30]置 1,这样在进出异常时,FPU 的相关寄存器就会自动地保存和恢复,同样地,因为 ARM Cortex-M3 内核 MCU 无 FPU,此当代码仅在 ARM Cortex-M4/M7 内核 MCU 平台上被调用。
-
调用函数 prvStartFirstTask()启动第一个任务
函数 prvStartFirstTask()
函数 prvStartFirstTask()用于初始化启动第一个任务前的环境,主要是重新设置 MSP 指针,并使能全局中断
__asm void prvStartFirstTask( void )
{/* 8 字节对齐 */PRESERVE8ldr r0, =0xE000ED08 /* 0xE000ED08 为 VTOR 地址 */ldr r0, [ r0 ] /* 获取 VTOR 的值 r0 = *r0 */ldr r0, [ r0 ] /* 获取 MSP 的初始值 r0 = *r0 *//* 初始化 MSP */msr msp, r0/* 使能全局中断 */cpsie icpsie fdsbisb/* 调用 SVC 启动第一个任务 */svc 0nopnop
}
从上面的代码可以看出,函数 prvStartFirstTask()是一段汇编代码,解析如下所示:
-
首先是使用了 PRESERVE8,进行 8 字节对齐,这是因为,栈在任何时候都是需要 4 字节对齐的,而在调用入口得 8 字节对齐,在进行 C 编程的时候,编译器会自动完成的对齐的操作,而对于汇编,就需要开发者手动进行对齐。
-
接下来的三行代码是为了获得 MSP 指针的初始值,那么这里就能够引出两个问题:
(1) 什么是 MSP 指针?
程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。当有信息保存到栈中时,MCU 会自动更新 SP 指针,使 SP 指针指向最后一个入栈的元素,那么程序就可以根据 SP 指针来从栈中存取信息。对于正点原子的 STM32F1、STM32F4、STM32F7 和 STM32H7 开发板上使用的 ARM Cortex-M 的 MCU 内核来说,ARM Cortex-M 提供了两个栈空间,这两个栈空间的堆栈指针分别是 MSP(主堆栈指针)和 PSP(进程堆栈指针)。在 FreeRTOS 中 MSP 是给系统栈空间使用的,而 PSP 是给任务栈使用的,也就是说,FreeRTOS 任务的栈空间是通过 PSP 指向的,而在进入中断服务函数时,则是使用 MSP 指针。当使用不同的堆栈指针时,SP 会等于当前使用的堆栈指针。
(2) 为什么是 0xE000ED08?
0xE000ED08 是 VTOR(向量表偏移寄存器)的地址,VTOR 中保存了向量表的偏移地址。一般来说向量表是从其实地址 0x00000000 开始的,但是在有情况下,可能需要修改或重定向向量表的首地址,因此 ARM Corten-M 提供了 VTOR 对向量表进行从定向。而向量表是用来保存中断异常的入口函数地址,即栈顶地址的,并且向量表中的第一个字保存的就是栈底的地址,
在 start_stm32xxxxxx.s 文件中有如下定义:
__Vectors DCD __initial_sp ; 栈底指针DCD Reset_Handler ; Reset HandlerDCD NMI_Handler ; NMI HandlerDCD HardFault_Handler ; Hard Fault HandlerDCD MemManage_Handler ; MPU Fault Handler
以上就是向量表(只列出前几个)的部分内容,可以看到向量表的第一个元素就是栈指针的初始值,也就是栈底指针。
在了解了这两个问题之后,接下来再来看看代码。首先是获取 VTOR 的地址,接着获取VTOR 的值,也就是获取向量表的首地址,最后获取向量表中第一个字的数据,也就是栈底指针了。
-
在获取了栈顶指针后,将 MSP 指针重新赋值为栈底指针。这个操作相当于丢弃了程序之前保存在栈中的数据,因为FreeRTOS从开启任务调度器到启动第一个任务都是不会返回的,是一条不归路,因此将栈中的数据丢弃,也不会有影响。
-
重新赋值 MSP 后,接下来就重新使能全局中断,因为之前在函数 vTaskStartScheduler()中关闭了受 FreeRTOS 的中断。
-
最后使用 SVC 指令,并传入系统调用号 0,触发 SVC 中断。
函数 vPortSVCHandler()
SVC 的中断服务函数
__asm void vPortSVCHandler( void )
{/* 8 字节对齐 */PRESERVE8/* 获取任务栈地址 */ldr r3, = pxCurrentTCB /* r3 指向优先级最高的就绪态任务的任务控制块 */ldr r1, [ r3 ] /* r1 为任务控制块地址 */ldr r0, [ r1 ] /* r0 为任务控制块的第一个元素(栈顶) *//* 模拟出栈,并设置 PSP */ldmia r0!, {r4-r11,r14} /* 任务栈弹出到 CPU 寄存器 */msr psp, r0 /* 设置 PSP 为任务栈指针 */isb/* 使能所有中断 */mov r0, # 0msr basepri,/* 使用 PSP 指针,并跳转到任务函数 */orr r14, # 0xdbx r14
}
- 首先通过 pxCurrentTCB 获取优先级最高的就绪态任务的任务栈地址,优先级最高的就绪态任务就是系统将要运行的任务
- 接下来通过任务的栈顶指针,将任务栈中的内容出栈到 CPU 寄存器中,任务栈中的内容在调用任务创建函数的时候,已经初始化了。然后再设置 PSP 指针,那么,这么一来,任务的运行环境就准备好了。
- 通过往 BASEPRI 寄存器中写 0,允许中断
- 最后通过两条汇编指令,使 CPU 跳转到任务的函数中去执行
- R14 是链接寄存器 LR,在 ISR 中(此刻我们在 SVC 的 ISR 中),它记录了异常返回值 EXC_RETURN
而EXC_RETURN 只有 6 个合法的值(M4、M7),如下表所示: (M3中不支持浮点单元故只有3个默认值)
描述 | 使用浮点单元 | 未使用浮点单元 |
---|---|---|
中断返回后进入Hamdler模式,并使用MSP | 0xFFFFFFE1 | 0xFFFFFFF1 |
中断返回后进入线程模式,并使用 MSP | 0xFFFFFFE9 | 0xFFFFFFF9 |
中断返回后进入线程模式,并使用 PSP | 0xFFFFFFED | 0xFFFFFFFD |
EXC_RETURN 各比特位的描述如下表所示
注意
SVC中断只在启动第一次任务时会调用一次,以后均不调用
中断产生时,硬件自动将xPSR,PC(R15),LR(R14),R12,R3-R0出/入栈;而R4~R11需要手动出/入栈
进入中断后硬件会强制使用MSP指针 ,此时LR(R14)的值将会被自动被更新为特殊的EXC_RETURN
补充
出栈/压栈汇编指令详解
出栈(恢复现场),方向:从下往上(低地址往高地址)
作用:把当前任务的数据加载到CPU中
假设r0地址为0x04汇编指令示例:
ldmia r0!, {r4-r6} /* 任务栈r0地址由低到高,将r0存储地址里面的内容手动加载到 CPU寄存器r4、r5、r6 */
- r0地址(0x04)内容加载到r4,此时地址r0 = r0+4 = 0x08
- r0地址(0x08)内容加载到r5,此时地址r0 = r0+4 = 0x0C
- r0地址(0x0C)内容加载到r6,此时地址r0 = r0+4 = 0x10
压栈(保存现场),方向:从上往下(高地址往低地址)
stmdb r0!, {r4-r6} } /* r0的存储地址由高到低递减,将r4、r5、r6里的内容存储到r0的任务栈里面。 */
作用:把CPU中正在处理的数据存储到对应任务的栈内
- 地址:r0 = r0-4 = 0x0C,将r6的内容(寄存器值)存放到r0所指向地址(0x0C)
- 地址:r0 = r0-4 = 0x08,将r5的内容(寄存器值)存放到r0所指向地址(0x08)
- 地址:r0 = r0-4 = 0x04,将r4的内容(寄存器值)存放到r0所指向地址(0x04)