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

STM32F1+HAL库+FreeTOTS学习8——第一个任务,启动!

STM32F1+HAL库+FreeTOTS学习8——第一个任务,启动!

  • 开启任务调度器
    • 1. 函数 vTaskStartScheduler()
    • 2. 函数xPortStartScheduler()
  • 启动第一个任务
    • 1. 函数 prvStartFirstTask()
    • 2. 函数 vPortSVCHandler()

上一期我们学习了列表和列表项的相关内容和API函数实验,接下来我们来学习FreeRTOS是如何启动第一个任务开启任务调度的,以及期间发生了什么

开启任务调度器

1. 函数 vTaskStartScheduler()

void freertos_demo(void)
{taskENTER_CRITICAL();           /* 进入临界区,关闭中断,此时停止任务调度*//* 创建任务1 */xTaskCreate((TaskFunction_t )task1,(const char*    )"task1",(uint16_t       )TASK1_STK_SIZE,(void*          )NULL,(UBaseType_t    )TASK1_PRIO,(TaskHandle_t*  )&Task1Task_Handler);/* 创建任务2 */xTaskCreate((TaskFunction_t )task2,(const char*    )"task2",(uint16_t       )TASK2_STK_SIZE,(void*          )NULL,(UBaseType_t    )TASK2_PRIO,(TaskHandle_t*  )&Task2Task_Handler);taskEXIT_CRITICAL();            /* 退出临界区,重新开启中断,开启任务调度 *//*这里是开启任务调度*/vTaskStartScheduler();		//开启任务调度
}

在前面,我们已经使用过函数 vTaskStartScheduler(),作用就是开启FreeRTOS的任务调度,下面我们来具体的看一下内部实现:

/*开启任务调度器函数*/
void vTaskStartScheduler( void )
{BaseType_t xReturn;// 1. 创建空闲函数/* 如果使用的是静态内存管理,则使用静态的方式创建空闲函数 */#if ( configSUPPORT_STATIC_ALLOCATION == 1 ){StaticTask_t * pxIdleTaskTCBBuffer = NULL;StackType_t * pxIdleTaskStackBuffer = NULL;uint32_t ulIdleTaskStackSize;/* 空闲任务的创建是使用用户提供的RAM,获取到相应的地址后才会进行创建。*/vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,configIDLE_TASK_NAME,ulIdleTaskStackSize,( void * ) NULL,       /*强制类型转换对于所有编译器都是多余的. */portPRIVILEGE_BIT,     /* 实际上这里应该是是 ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), 但是 tskIDLE_PRIORITY 为0. */pxIdleTaskStackBuffer,pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. *//*创建成功*/if( xIdleTaskHandle != NULL ){xReturn = pdPASS;}/*创建不成功*/else{xReturn = pdFAIL;}}/* 否则使用动态的方式,创建空闲函数 */#else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */{/* 空闲函数动态分配RAM空间. */xReturn = xTaskCreate( prvIdleTask,configIDLE_TASK_NAME,configMINIMAL_STACK_SIZE,( void * ) NULL,portPRIVILEGE_BIT,  /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */&xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */}#endif /* 结束空闲任务创建*/// 2. 创建软件定时器任务 /*如果使能软件定时器,则需要创建定时器服务任务*/#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// 3、关闭中断,防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断portDISABLE_INTERRUPTS();/* Newlib 相关的一些东西,这里我也看不懂 */#if ( configUSE_NEWLIB_REENTRANT == 1 ){/* Switch Newlib's _impure_ptr variable to point to the _reent* structure specific to the task that will run first.* See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html* for additional information. */_impure_ptr = &( pxCurrentTCB->xNewLib_reent );}#endif /* configUSE_NEWLIB_REENTRANT */// 4. 初始化一些全局变量xNextTaskUnblockTime = portMAX_DELAY; 					// 下一个距离取消任务阻塞的时间,设置为最大,因为此时还没有运行任务// 避免阻塞超时导致任务切换xSchedulerRunning = pdTRUE;				//设置任务调度标志为开启xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;	//系统节拍计数器,初始化为0// 5. 为任务运行时间统计功能初始化功能时基定时器,是否使用该功能可在 FreeRTOSConfig.h 文件中进行配置portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();/* 调试使用 */traceTASK_SWITCHED_IN();/* xPortStartScheduler() 设置用于系统时钟节拍的硬件定时器(SysTick) 会在这个函数中进入第一个任务,并开始任务调度* 任务调度开启后,便不会再返回 */if( xPortStartScheduler() != pdFALSE ){/* 代码不会运行到这里 */}else{/* 当调用关闭任务调度器函数 xTaskEndScheduler()时会运行到这里. */}}else{/*动态方式创建空闲任务和定时器服务任务时,堆栈空间不足,会导致无法创建,进入这里 */configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );}/* 防止编译器警告,不用管*/( void ) xIdleTaskHandle;/* 调试使用,不用管*/( void ) uxTopUsedPriority;
}

上述代码来自FreeRTOS官方提供源码,为了跟方便看懂,去除了大部分的英文注释,该为中文注释,更适合中国宝宝体质!!!

结合上述的注释,我们可以大概明白 vTaskStartScheduler() 完成了如下内容:

  1. 创建空闲任务
  2. 创建软件定时器任务
  3. 关闭中断(确切的说是关闭FreeRTOS能够控制的中断),防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断
  4. 初始化全局变量,并将任务调度器的运行标志设置为已运行
  5. 初始化任务运行时间统计功能的时基定时器,任务运行时间统计功能需要一个硬件定时器提供高精度的计数,这个硬件定时器就在这里进行配置,如果配置不启用任务运行时间统计功能的,就无需进行这项硬件定时器的配置。
  6. 最后就是调用函数 xPortStartScheduler(),由于里面的内容比较多,所以我们下面再起一部分讲解。

需要注意以下几点:

  1. 如果使用的是静态创建任务的方式,则空闲任务和定时器任务需要用户自行定义任务堆栈和TCB(任务控制块)
  2. 软件定时器人的优先级为31(最高),空闲任务的优先级为0(最低)

2. 函数xPortStartScheduler()

函数 xPortStartScheduler()完成启动任务调度器中与硬件架构相关的配置部分,以及启动第一个任务,具体的代码如下所示:

BaseType_t xPortStartScheduler( void )
{/*1. 检测用户在 FreeRTOSConfig.h 文件中对中断相关部分的配置是否有误*/#if ( configASSERT_DEFINED == 1 ){volatile uint32_t ulOriginalPriority;volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );volatile uint8_t ucMaxPriorityValue;/* 确定可以从哪个最高优先级调用ISR安全的FreeRTOS API函数 ISR安全函数是以“FromISR”结尾的函数。FreeRTOS维护了单独的线程和ISR API函数,以确保中断进入尽可能快速和简单。保存即将被覆盖的中断优先级值。 */ulOriginalPriority = *pucFirstUserPriorityRegister;/* 确定可用的优先级位数。首先写入所有可能的位。. */*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;/* Read the value back to see how many bits stuck. */ucMaxPriorityValue = *pucFirstUserPriorityRegister;/* The kernel interrupt priority should be set to the lowest* priority. */configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );/* Use the same mask on the maximum system call priority. */ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;/* Calculate the maximum acceptable priority group value for the number* of bits read back. */ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE ){ulMaxPRIGROUPValue--;ucMaxPriorityValue <<= ( uint8_t ) 0x01;}#ifdef __NVIC_PRIO_BITS{/* Check the CMSIS configuration that defines the number of* priority bits matches the number of priority bits actually queried* from the hardware. */configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );}#endif#ifdef configPRIO_BITS{/* Check the FreeRTOS configuration that defines the number of* priority bits matches the number of priority bits actually queried* from the hardware. */configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );}#endif/* Shift the priority group value back to its position within the AIRCR* register. */ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;/* Restore the clobbered interrupt priority register to its original* value. */*pucFirstUserPriorityRegister = ulOriginalPriority;}#endif /* configASSERT_DEFINED *//* 2. 设置 PendSV 和 SysTick 的中断优先级为最低优先级  */portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;/* 3. 开启并配置SYSTick,设置系统节拍、时钟源、然后开启SysTick和中断 */vPortSetupTimerInterrupt();/* 4. 初始化临界区嵌套计数器为 0 */uxCriticalNesting = 0;/* 5. 开启FPU,但是在Crotex_M3的内核里面,没有FPU,所以这里没有相关代码*//* 6. 开启第一个任务:第一个任务,启动! */prvStartFirstTask();/* Should not get here! */return 0;
}

上述代码来自FreeRTOS官方提供源码,为了跟方便看懂,去除了大部分的英文注释,该为中文注释,更适合中国宝宝体质!!!

结合上述的注释,我们可以大概明白 xPortStartScheduler() 完成了如下内容:

  1. 在启用断言的情况下,函数 xPortStartScheduler()会检测用户在 FreeRTOSConfig.h 文件中对中断的相关配置是否有误
  2. 配置 PendSV 和 SysTick 的中断优先级为最低优先级
  3. 调用函数 vPortSetupTimerInterrupt()配置 SysTick,函数 vPortSetupTimerInterrupt()首先会将 SysTick 当 前 计 数 值 清 空 , 并 根 据 FreeRTOSConfig.h 文件中配置的configSYSTICK_CLOCK_HZ(SysTick 时钟源频率)和 configTICK_RATE_HZ(系统时钟节拍频率)计算并设置 SysTick 的重装载值,然后启动 SysTick 计数和中断。
  4. 初始化临界区嵌套计数器,将其置为0
  5. 调用函数 prvEnableVFP()使能 FPU,因为 ARM Cortex-M3 内核 MCU 无 FPU,此函数仅在 ARM Cortex-M4/M7 内核 MCU 平台上被调用,执行改函数后 FPU 被开启。(这里因为我们使用的是STM32F1系列,使用的Crotex_M3内核,所以没有FPU)
  6. 调用prvStartFirstTask() 函数,第一个任务启动!

启动第一个任务

1. 函数 prvStartFirstTask()

prvStartFirstTask() 函数的调用,标志着我们正式迈步FreeRTOS的领域,该函数用于初始化启动第一个任务的环境:重新设置MSP指针和使能全局中断,最后使用SVC指令,触发SVC中断。

我们来看一下是如何实现的:

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

咋一看,这一段代码有点让人看不懂,因为是汇编,但是请你放心,它一点都不简单,我们这里先来补充几个重要的知识:

  1. 0xE000ED08 是什么东西?

事实上,0xE000ED08 是向量表偏移量寄存器的地址,“ ldr r0, =0xE000ED08 ” 这一步的目的就是将向量表偏移量寄存器的地址读取到" r0 "寄存器,紧接着 “ ldr r0, [ r0 ] ” 则是获取向量表偏移量寄存器中所指向的内容,也就是我们的中断向量表,但是我们知道,中断向量表,里面存放着各个中断服务函数的地址,“ ldr r0, [ r0 ] ” 就是获取第一个中断服务函数的地址 ,查阅start_stm32xxxxxx.s文件,就可以知道,这里的目的就是获取MSP指针的初始值。

在这里插入图片描述
2. MSP指针是干嘛的?

在这里插入图片描述
Cortex‐M3 处理器拥有 R0‐R15 的寄存器组。其中 R13 作为堆栈指针 SP。SP 有两个,但在同一时刻只能有一个可以看到,这也就是所谓的“banked”寄存器。

程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。对应任务的当有信息保存到栈中时,MCU 会自动更新 SP 指针,ARM Cortex-M 内核提供了两个栈空间,

  • 主堆栈指针(MSP):复位后默认使用的堆栈指针,用于操作系统内核以及异常处理例程(包
    括中断服务例程)
  • 进程堆栈指针(PSP):由用户的应用程序代码使用。

看到这里,相信你就一目了然了,复位后默认使用的是MSP指针,所以我们先需要查找中断向量表获取MSP指针初始值,将其赋值给msp寄存器,让MSP指针回到原点,紧接着,开启全局中断,进入SVC服务函数,由操作系统进行接管。
在这里插入图片描述

在这里插入图片描述

我们来总结一下prvStartFirstTask() 里面到底干了什么:

  1. 首先是使用了 PRESERVE8,进行 8 字节对齐,这是因为,栈在任何时候都是需要 4 字节对齐的,而在调用入口得 8 字节对齐,在进行 C 编程的时候,编译器会自动完成的对齐的操作,而对于汇编,就需要开发者手动进行对齐。
  2. 了获得 MSP 指针的初始值
  3. 对MSP指针进行初始化,这个操作相当于丢弃了程序之前保存在栈中的数据,因为FreeRTOS从开启任务调度器到启动第一个任务都是不会返回的,是一条不归路,因此将栈中的数据丢弃,也不会有影响。
  4. 使能全局中断,因为之前关闭了FreeRTOS管理的中断
  5. 最后使用 SVC 指令,并传入系统调用号 0,触发 SVC 中断。

2. 函数 vPortSVCHandler()

__asm void vPortSVCHandler( void )
{/* 8 字节对齐 */PRESERVE8/* 获取任务栈地址 */ldr r3, = pxCurrentTCB /* r3 指向优先级最高的就绪态任务的任务控制块 */ldr r1, [ r3 ] /* r1 为任务控制块地址 */ldr r0, [ r1 ] /* r0 为任务控制块的第一个元素(栈顶) *//* 模拟出栈,并设置 PSP */ldmia r0 !, { r4 - r11 } /* 任务栈r0地址从低到高,将r0存储地址里面的内容手动加载到 CPU寄存器r4到r11 */msr psp, r0 /* 设置 PSP 为任务栈指针 */isb/* 使能所有中断 */mov r0, # 0msr basepri,/* 使用 PSP 指针,并跳转到任务函数 */orr r14, # 0xdbx r14
}

同样的我们来分析一些,这个函数里做了什么:

  1. 同样进行了字节对齐,因为这里是汇编的世界!
  2. 获取任务栈的地址:pxCurrentTCB 是一个全局变量,用于指向系统中优先级最高的就绪态任务的任务控制块,之前我们创建了两个任务,一个空闲任务,优先级为0,一个软件定时器服务函数,优先级为31(这部没有包含用户自己创建的任务), 这里pxCurrentTCB 是软件定时器任务的任务控制块。那么对应的就是获取软件定时器任务的栈顶地址
  3. 将软件定时器任务栈的内容出栈到CPU寄存器组内,如何设置PSP指针。
  4. 使能所有中断
  5. 对CPU寄存器里面的r14(连接寄存器)或上0x0d ,使得r14的值为 0xFFFFFFFD

R14 是链接寄存器 LR,在 ISR 中(此刻我们在 SVC 的 ISR 中),它记录了异常返回值 EXC_RETURN,而EXC_RETURN 只有 6 个合法的值(M4、M7),如下表所示:

描述使用浮点单元(M4、M7的内核)未使用浮点单元(M3的内核)
中断返回后进入Hamdler模式,并使用MSP0xFFFFFFE10xFFFFFFF1
中断返回后进入线程模式,并使用 MSP0xFFFFFFE90xFFFFFFF9
中断返回后进入线程模式,并使用 PSP0xFFFFFFED0xFFFFFFFD

经过以上步骤,最终进入线程模式,使用PSP指针,开始运行第一个任务,软件定时器任务,至此第一个任务正式启动!!!

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • linux-基础知识2
  • 白盒测试及其测试方法
  • Linux高级编程-进程间通信(IPC)
  • 构建大师:深入理解Linux下的Make和Makefile
  • UE5学习笔记20-给游戏添加声音
  • 安装VC++Redist报错0x80070003的解决办法
  • 大连网站建设手机网页页面设计
  • STM32入门教程:SPI通信
  • RabbitMQ 集群与高可用性
  • 每日算法!!
  • 利用Spring Boot实现微服务的API版本管理
  • C语言 面向对象编程
  • Java项目中的分库分表实践指南
  • QNN:基于QNN+example重构之后的yolov8det部署
  • DRF序列化_data传参
  • ES6指北【2】—— 箭头函数
  • hexo+github搭建个人博客
  • Git初体验
  • JAVA之继承和多态
  • Just for fun——迅速写完快速排序
  • node-glob通配符
  • python大佬养成计划----difflib模块
  • ReactNative开发常用的三方模块
  • RedisSerializer之JdkSerializationRedisSerializer分析
  • Spring Cloud Feign的两种使用姿势
  • yii2中session跨域名的问题
  • 得到一个数组中任意X个元素的所有组合 即C(n,m)
  • 分享一个自己写的基于canvas的原生js图片爆炸插件
  • 和 || 运算
  • 双管齐下,VMware的容器新战略
  • 探索 JS 中的模块化
  • 通过git安装npm私有模块
  • 为什么要用IPython/Jupyter?
  • 我是如何设计 Upload 上传组件的
  • 一文看透浏览器架构
  • 优秀架构师必须掌握的架构思维
  • 原生 js 实现移动端 Touch 滑动反弹
  • 源码之下无秘密 ── 做最好的 Netty 源码分析教程
  • ​Distil-Whisper:比Whisper快6倍,体积小50%的语音识别模型
  • ​如何使用QGIS制作三维建筑
  • # Maven错误Error executing Maven
  • #pragma data_seg 共享数据区(转)
  • (1)无线电失控保护(二)
  • (5)STL算法之复制
  • (Forward) Music Player: From UI Proposal to Code
  • (Redis使用系列) Springboot 使用redis实现接口幂等性拦截 十一
  • (八)Flink Join 连接
  • (二十一)devops持续集成开发——使用jenkins的Docker Pipeline插件完成docker项目的pipeline流水线发布
  • (附源码)spring boot校园拼车微信小程序 毕业设计 091617
  • (附源码)计算机毕业设计SSM在线影视购票系统
  • (更新)A股上市公司华证ESG评级得分稳健性校验ESG得分年均值中位数(2009-2023年.12)
  • (每日持续更新)jdk api之FileReader基础、应用、实战
  • (牛客腾讯思维编程题)编码编码分组打印下标题目分析
  • (三维重建学习)已有位姿放入colmap和3D Gaussian Splatting训练
  • (十七)Flask之大型项目目录结构示例【二扣蓝图】