当前位置: 首页 > article >正文

FreeRTOS学习8——开启任务调度器API函数简介

开启任务调度器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();
 }
}
#endif
 
 if( 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()主要做了六件事情。

  1. 创建空闲任务,根据是否支持静态内存管理,使用静态方式或动态方式创建空闲任务。

  2. 创建定时器服务任务,创建定时器服务任务需要配置启用软件定时器,创建定时器服务任务,同样是根据是否配置支持静态内存管理,使用静态或动态方式创建定时器服务任务。

  3. 关闭中断,使用 portDISABLE_INTERRUPT()关闭中断,这种方式只关闭受 FreeRTOS 管理的中断。关闭中断主要是为了防止 SysTick 中断在任务调度器开启之前或过程中,产生中断。FreeRTOS 会在开始运行第一个任务时,重新打开中断。

  4. 初始化一些全局变量,并将任务调度器的运行标志设置为已运行。

  5. 初始化任务运行时间统计功能的时基定时器,任务运行时间统计功能需要一个硬件定时器提供高精度的计数,这个硬件定时器就在这里进行配置,如果配置不启用任务运行时间统计功能的,就无需进行这项硬件定时器的配置。

  6. 最后就是调用函数 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;
}
  1. 在启用断言的情况下,函数 xPortStartScheduler()会检测用户在 FreeRTOSConfig.h 文件中对中断的相关配置是否有误,感兴趣的读者请自行查看这部分的相关代码。

  2. 配置 PendSV 和 SysTick 的中断优先级为最低优先级

  3. 调用函数 vPortSetupTimerInterrupt()配置 SysTick,函数 vPortSetupTimerInterrupt()首先会将 SysTick 当 前 计 数 值 清 空 , 并 根 据 FreeRTOSConfig.h 文件中配置的configSYSTICK_CLOCK_HZ(SysTick 时钟源频率)和 configTICK_RATE_HZ(系统时钟节拍频率)计算并设置 SysTick 的重装载值,然后启动 SysTick 计数和中断。(具体可结合函数内部和M3权威指南P134)

  4. 初始化临界区嵌套计数器为 0。

  5. 调用函数 prvEnableVFP()使能 FPU,因为 ARM Cortex-M3 内核 MCU 无 FPU,此函数仅在 ARM Cortex-M4/M7 内核 MCU 平台上被调用,执行改函数后 FPU 被开启。

  6. 接下来将 FPCCR 寄存器的[31:30]置 1,这样在进出异常时,FPU 的相关寄存器就会自动地保存和恢复,同样地,因为 ARM Cortex-M3 内核 MCU 无 FPU,此当代码仅在 ARM Cortex-M4/M7 内核 MCU 平台上被调用。

  7. 调用函数 prvStartFirstTask()启动第一个任务

函数 prvStartFirstTask()

函数 prvStartFirstTask()用于初始化启动第一个任务前的环境,主要是重新设置 MSP 指针,并使能全局中断

__asm void prvStartFirstTask( void )
{
 /* 8 字节对齐 */
 PRESERVE8
 
 ldr r0, =0xE000ED08 /* 0xE000ED08 为 VTOR 地址 */
 ldr r0, [ r0 ] 	 /* 获取 VTOR 的值 	 r0 = *r0 */
 ldr r0, [ r0 ]      /* 获取 MSP 的初始值 r0 = *r0 */
 
 /* 初始化 MSP */
 msr msp, r0
 
 /* 使能全局中断 */
 cpsie i
 cpsie f
 dsb
 isb
 
 /* 调用 SVC 启动第一个任务 */
 svc 0
 nop
 nop
}

从上面的代码可以看出,函数 prvStartFirstTask()是一段汇编代码,解析如下所示:

  1. 首先是使用了 PRESERVE8,进行 8 字节对齐,这是因为,栈在任何时候都是需要 4 字节对齐的,而在调用入口得 8 字节对齐,在进行 C 编程的时候,编译器会自动完成的对齐的操作,而对于汇编,就需要开发者手动进行对齐。

  2. 接下来的三行代码是为了获得 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 Handler
     DCD NMI_Handler 					; NMI Handler
     DCD HardFault_Handler 				; Hard Fault Handler
     DCD MemManage_Handler 				; MPU Fault Handler
    

    ​ 以上就是向量表(只列出前几个)的部分内容,可以看到向量表的第一个元素就是栈指针的初始值,也就是栈底指针。

    ​ 在了解了这两个问题之后,接下来再来看看代码。首先是获取 VTOR 的地址,接着获取VTOR 的值,也就是获取向量表的首地址,最后获取向量表中第一个字的数据,也就是栈底指针了。

  3. 在获取了栈顶指针后,将 MSP 指针重新赋值为栈底指针。这个操作相当于丢弃了程序之前保存在栈中的数据,因为FreeRTOS从开启任务调度器到启动第一个任务都是不会返回的,是一条不归路,因此将栈中的数据丢弃,也不会有影响。

  4. 重新赋值 MSP 后,接下来就重新使能全局中断,因为之前在函数 vTaskStartScheduler()中关闭了受 FreeRTOS 的中断。

  5. 最后使用 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, # 0
 msr basepri,
 
 /* 使用 PSP 指针,并跳转到任务函数 */
 orr r14, # 0xd
 bx r14
}
  • 首先通过 pxCurrentTCB 获取优先级最高的就绪态任务的任务栈地址,优先级最高的就绪态任务就是系统将要运行的任务
  • 接下来通过任务的栈顶指针,将任务栈中的内容出栈到 CPU 寄存器中,任务栈中的内容在调用任务创建函数的时候,已经初始化了。然后再设置 PSP 指针,那么,这么一来,任务的运行环境就准备好了。
  • 通过往 BASEPRI 寄存器中写 0,允许中断
  • 最后通过两条汇编指令,使 CPU 跳转到任务的函数中去执行
  • R14 是链接寄存器 LR,在 ISR 中(此刻我们在 SVC 的 ISR 中),它记录了异常返回值 EXC_RETURN

EXC_RETURN 只有 6 个合法的值(M4、M7),如下表所示: (M3中不支持浮点单元故只有3个默认值

描述使用浮点单元未使用浮点单元
中断返回后进入Hamdler模式,并使用MSP0xFFFFFFE10xFFFFFFF1
中断返回后进入线程模式,并使用 MSP0xFFFFFFE90xFFFFFFF9
中断返回后进入线程模式,并使用 PSP0xFFFFFFED0xFFFFFFFD

EXC_RETURN 各比特位的描述如下表所示

image-20241101160954907

注意

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)

http://www.kler.cn/a/381183.html

相关文章:

  • Linux的mmap
  • RabbitMQ 的7种工作模式
  • git clone必须使用sudo否则失败 git推送错误想再次编辑和推送
  • springboot vue 会员营销系统
  • MyBatis通过注解配置执行SQL语句原理源码分析
  • 解析在OceanBase创建分区的常见问题|OceanBase 用户问题精粹
  • 【运维项目经历|046】Jenkins自动化部署与持续集成优化项目
  • MySQL秘密武器:索引与事务
  • 【51单片机】DS1302实时时钟
  • RabbitMQ最全教程-Part1(基础使用)
  • 活着就好20241106
  • 人脸检测之MTCNN算法网络结构
  • VUE+Vite之环境文件配置及使用环境变量
  • 全面解析:区块链技术及其应用
  • 轴承故障全家桶更新 | 基于时频图像的分类算法
  • 科技改变阅读习惯:最新研究揭示电子阅读器的普及趋势
  • WonderWorld: Interactive 3D Scene Generation from a Single Image 论文解读
  • 【go从零单排】在 Go 语言中,:= 是什么意思?
  • TOEIC 词汇专题:旅游计划篇
  • ClickHouse数据库SSL配置和SSL连接测试
  • C语言 | Leetcode C语言题解之第540题有序数组中的单一元素
  • MySQL详细安装教程
  • 【机器学习】25. 聚类-DBSCAN(density base)
  • 云计算Openstack 虚拟机调度策略
  • Docker-- cgroups资源控制实战
  • 【C++刷题】力扣-#705-设计哈希集合