FreeRTOS-启动任务调度器源码分析

2023-05-16

本章基于FreeRTOS的启动任务调度器源码分析,后续将会上传其它我对FreeRTOS的源码分析过程及理解,首先来认识一下任务调度器。

任务调度器:
任务调度器主要用于实现任务的切换,任务并不是我们所熟知的函数,而是指一段占有独立内存空间,专门用于处理一组逻辑事件的任务块。最直观的对比理解就像是可以有多个while函数,以前我们写代码都是一个main函数一个while循环,我们会在while循环里面做完所有的事,当任务大起来以后,我们会将各个功能分为多个函数来写,使用状态机的方式来实现逻辑操作,有时我们还需要借助各种中断来置位标志位,然后等待main里面的while轮询去执行。这个就是单任务系统,又称为前后台系统,中断服务函数为前台,main的操作为后台程序。这种操作方式的实时性非常差,由于是while轮询,没轮到就只能一直等着,对于简单的应用程序是够用的,但是一旦应用大起来就力不从心了。因此这里引入多任务的实现方式,相当于可以写多个while来处理我们的代码,显然这更符合我们的处理事件的逻辑,但是CPU只有一个,又如何做到一心多用呢,实际上操作系统是通过定时器中断来隔一阵子切换一个while,并不是真的是并行处理着两个while,这种处理方式我们叫做并发,各个while各占用一段时间,由任务调度器来决定下一个while是谁,具体怎么切换的就是任务调度器的工作了。

说到任务调度一定离不开堆栈,堆栈也是OS的基础,堆栈可以引起很多话题,下面只做个简单的介绍,后面代码会具体分析。

堆栈:笼统地讲,堆栈操作就是对内存的读写操作,在我们执行main函数的时候,我们在里面定义变量,修改变量实际上都是在操作堆栈,堆栈这里理解为内存即可,每定义一个变量,都会占用一定的内存空间,比如int类型变量就是4字节的内存空间,也就是占用4字节的堆栈空间,main这个进程的堆栈大小我们可以自行定义,但不能超过CPU的最大内存。如果将一块大的内存分为多个小的堆栈内存,不就可以实现类似多个main进程一样的效果了吗,每个进程使用自己的那块堆栈,大家并行不悖,当需要跳转的时候使用任务调度器进行一下堆栈切换就好了。

FreeRTOS能实现效果上多个任务的同时运行,由于任务调度器在各个任务之间的切换非常快,所以效果上看起来就像是所有的任务同时运行一样,这章主要分析FreeRTOS是如何启动任务调度器的(包括启动调度器前的准备工作和堆栈切换),至于调度器如何根据时间片和优先级来实现任务间的切换后面会继续分析源码的实现。


以一段初始化开始,一般我们在使用FreeRTOS之前都会先定义一个启动任务,然后启动任务调度器,如下:

int main()
{
	/* 初始化外设 */
	init_periph();
	/* 创建开始任务 */
	xTaskCreate((TaskFunction_t)start_task,         /* 任务函数 */
				(const char*   )"start_task",		/* 任务名称 */
				(uint16_t      )START_STK_SIZE,		/* 任务堆栈大小 */
				(void*         )NULL,				/* 传递给任务函数 */
				(UBaseType_t   )START_TASK_PRIO,	/* 任务优先级 */
				(TaskHandle_t* )&StartTask_Handler);/* 任务句柄 */

	/* 开启任务调度 */			
	vTaskStartScheduler();
}

上面先创建了一个任务,任务的创建先不管,后面会单独进行分析,这章主要分析启动调度器的过程,即“vTaskStartScheduler();”这个函数的分析。
这里先简单总结下本章讲解的整个过程:

创建开始任务 -> 创建空闲任务 -> 关闭中断 -> 根据配置文件初始化系统滴答(作为系统时钟 )-> 获取并复位MSP(主堆栈) -> 调用SVC中断
-> 找到当前控制块 -> 打开中断 -> 任务出栈 -> 执行开始任务函数(为什么不是执行空闲任务呢?留在文末说明)

下列源码省略了部分宏定义的回调函数。

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 );
		/* 创建成功返回PASS */
		if( xIdleTaskHandle != NULL )
		{
			xReturn = pdPASS;
		}
		else
		{
			xReturn = pdFAIL;
		}
	}
	#else
	{
		/* 动态方式创建空闲任务 */
		xReturn = xTaskCreate(	prvIdleTask,
								configIDLE_TASK_NAME,
								configMINIMAL_STACK_SIZE,
								( void * ) NULL,
								portPRIVILEGE_BIT, 
								&xIdleTaskHandle ); 
	}
	#endif

	/* 为1即使用配置了软件定时器 */
	#if ( configUSE_TIMERS == 1 )
	{
		if( xReturn == pdPASS )
		{
			xReturn = xTimerCreateTimerTask();/* /创建定时器任务 */
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
	#endif 

	/* 创建空闲任务成功 */
	if( xReturn == pdPASS )
	{
		
		/* 关闭中断,这句会在下面进行解析*/
		portDISABLE_INTERRUPTS();

		/* 配置使用newlib, Newlib是一个C库函数,并非FreeRTOS维护,FreeRTOS也不对使用结果负责。*/
		#if ( configUSE_NEWLIB_REENTRANT == 1 )
		{
			_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
		}
		#endif
		
		xNextTaskUnblockTime = portMAX_DELAY;/* 这是个全局变量,保存着下一个要取消阻塞的任务的最小时间 */
		xSchedulerRunning = pdTRUE;/* 调度器开始运行标志 */
		xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;/* 时钟计数器,每个定时器周期会递增,这里初始化为0 */

		portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();/* 时间统计功能函数,是个回调,需要我们自己定义一个定时器 */
		
		/* 启动调度器,一去不复返 */
		if( xPortStartScheduler() != pdFALSE )
		{
			/* 如果调度器打开成功不会运行到这里 */
		}
		else
		{
			/* 不会运行到这里除非调用xTaskEndScheduler */
		}
	}
	else
	{
		/* 如果运行到了这里可能由于创建空闲任务或定时器任务申请内存时内存不够 */
		configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
	}
	/* 如果定义宏INCLUDE_xTaskGetIdleTaskHandle为0,就会提示xIdleTaskHandle未使用 */
	( void ) xIdleTaskHandle;
}

整个过程下来除了任务创建这个函数,我们先分析一下其余的几个函数:

portDISABLE_INTERRUPTS 关闭中断函数
xPortStartScheduler 启动调度器函数

portDISABLE_INTERRUPTS 是个宏定义,实际是调用vPortRaiseBASEPRI

 #define portDISABLE_INTERRUPTS()				vPortRaiseBASEPRI()
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
/* 定义BASEPRI这个特殊功能寄存器的值,BASEPRI即除能所有优先级不高于某个具体数值的中断*/
/* #define configMAX_SYSCALL_INTERRUPT_PRIORITY 	191  */
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;

	__asm
	{
		/* (msr:写通用寄存器的值到特殊功能寄存器)中断优先级号大于191的都会被关闭 */
		msr basepri, ulNewBASEPRI	
		dsb	(数据同步隔离(与流水线、 MPU 和 cache 等有关))
		isb (指令同步隔离(与流水线和 MPU 等有关))
	}
}

<<Cortex-M3权威指南(中文)>>中的解释如下:
在这里插入图片描述

xPortStartScheduler 启动调度器函数

BaseType_t xPortStartScheduler( void )
{
	#if( configASSERT_DEFINED == 1 )
	{
		/* 有关断言机制的操作,调试代码的时候可以检查传入的参数是否合理,FreeRTOS中
		 *	关键点都会调用configASSERT(x),当x为0时说明有错误发生,使用断言的话会导致
		 *	开销加大,一般只在调试阶段使用,正式发布代码是这个宏都要去掉
		 */
	}
	#endif /* conifgASSERT_DEFINED */

	/* 设置pendsv和systick为最低优先级中断,这两个中断在后面章节解析 */
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

	/* 初始化及设置滴答定时器定时周期,并使能定时器中断,后面看滴答定时器中断函数再做分析 */
	vPortSetupTimerInterrupt();

	/* 用于记录临界区的嵌套次数,这里做初始化 */
	uxCriticalNesting = 0;

	/* 启动第一个任务,一去不复返 */
	prvStartFirstTask();

	/* 不会执行到这里 */
	return 0;
}
__asm void prvStartFirstTask( void )
{
	PRESERVE8	/* 8字节对齐 */

	/* Use the NVIC offset register to locate the stack. */
	ldr r0, =0xE000ED08	/* VTOR寄存器,存着向量表地址 */
	ldr r0, [r0]	/* 得到0x8000000这个地址 */
	ldr r0, [r0]	/* 从0x8000000获取MSP */

	msr msp, r0	/* 复位MSP */

	cpsie i	/* 开中断 */
	cpsie f	/* 开异常 */
	dsb	/* 数据同步屏障 */
	isb	/* 指令同步屏障 */

	svc 0	/* 触发SVC中断,只在启动调度器时使用一次 */
	nop
	nop
}
__asm void vPortSVCHandler( void )
{
	PRESERVE8	//8字节对齐

	/* 获取任务控制块,pxCurrentTCB	为全局变量,保存着当前需要执行任务的堆栈地址 */
	ldr	r3, =pxCurrentTCB	
	/* 获取任务控制块地址,[]为取指 */
	ldr r1, [r3]			
	/* 从任务控制块获取任务栈顶地址 */
	ldr r0, [r1]		
	/* 将获取到的栈的r4-r11出栈,其它寄存器由退出中断时自动出栈 */
	ldmia r0!, {r4-r11}		
	/* 更新进程栈(PSP)顶指针 */
	msr psp, r0				
	/* 指令同步 */
	isb
	/* 设置寄存器basepri=0,开启中断(前面通过basepri关了所有中断) */
	mov r0, #0
	msr	basepri, r0
	/* /r14和0xd进行或运算,得到的值给r14,进去线程模式并使用进程栈 */
	orr r14, #0xd
	/* 执行这句后会恢复R0~R3、R12、LR、PC和xPSR,执行PC中保存的任务函数 */
	bx r14
	/* 由于pxCurrentTCB里面定义了PC的值,所以到这里就会跳到最开始创建的那个启动任务的函数执行 */
}

上面这句“ orr r14, #0xd ”在权威手册中有提到,这个值的定义如下:
在这里插入图片描述
什么是主堆栈和进程栈呢,这是Cortex-M3 采用的双堆栈机制,在复位后缺省使用的主堆栈(MSP),一些简单的应用程序只使用MSP就足够了,以往我们的程序main和中断都共用MSP,但是当应用一旦大起来,使用OS的话使用双堆栈就更加合适,在用户应用进程使用PSP,在异常处理时使用MSP,主要为了避免系统堆栈(MSP)因应用程序的错误使用而毁坏,给应用程序专门配一个堆栈,不让它共享操作系统内核的堆栈。具体切换过程核心手册中有提到,如下:
在这里插入图片描述
上面我们使用的SVC中断,为什么要在SVC中断中去执行堆栈的切换呢,直接在用户进程中切换行不行呢,这里不得不提到Cortex的操作模式了,Cortex‐M3 支持 2 个模式(Handler模式线程模式)和两个特权等级(特权级和用户级)。在复位缺省状态下,处理器进入线程模式+特权级。此时允许写特殊功能寄存器和 NVIC中寄存器,这种操作方式存在风险,所以在OS中会让用户进程处在用户级,而异常处理则只能处在特权级,上面的SVC中断就处于特权级,在这里面去操作CONTROL寄存器来实现主堆栈和进程堆栈的切换是比较合适的。Cortex‐M3权威手册中的也有说明,如下:
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
关于SVC中断的更具体的内容可以参考<<Cortex‐M3权威手册>>


整个过程创建了两个任务,一个启动任务一个空闲任务,但在进行任务堆栈切换的时候,调用了pxCurrentTCB,当前任务控制块是用的启动任务的还是空闲任务的呢?这里pxCurrentTCB使用的是启动任务的堆栈,因为空闲任务的优先级已经被设置为了最低,而启动任务的优先级高于它,所以当前会先获取启动任务的堆栈。这个启动任务是自己创建的,可有可无,只是为了方便用于管理任务创建,如果没创建任何任务,调度器启动后就会执行空闲任务。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

FreeRTOS-启动任务调度器源码分析 的相关文章

随机推荐

  • Lock与RLock的区别

    目录 往期推荐介绍区别一区别二 往期推荐 Python多线程的使用 Python线程池的使用 Python多线程的安全问题 B站同名 有温度的算法 已经上线 想观看视频讲解的同学 点击此处直达B站 介绍 在上节中为大家说明了线程访问临界资源
  • ROS 小技巧 - OpenCV4 与 CV_Bridge 配合使用

    1 现象 ROS默认的Python版本是3 3 xff0c 但我系统安装的是OpenCV4 5 如果直接在pkg中使用cv bridge和opencv4 5就会有问题 会有一些undefined reference问题 参考资料 xff1a
  • 【做题系统】后端设计

    目录 一 设计思路 1 项目背景 2 技术栈选择 二 系统设计 1 系统结构图 2 项目结构 3 数据建模 4 数据流图 5 主要流程图 三 问题及解决办法 1 实现安全登录 访问 2 数据库中的信息安全问题 3 Mybatis plus如
  • C/C++字符串查找函数

    C C 43 43 string库 xff08 string h xff09 提供了几个字符串查找函数 xff0c 如下 xff1a memchr在指定内存里定位给定字符strchr在指定字符串里定位给定字符strcspn返回在字符串str
  • ssh命令-manpage

    SSH Section User Commands 1 Index Return to Main Contents BSD mandoc NAME ssh OpenSSH SSH 客户端 远程登录程序 总览 SYNOPSIS ssh l l
  • 一小时做出Java实战项目——飞翔的小鸟

    学姐又来啦 xff0c 今日分享一个Java实战项目 飞翔的小鸟 相信大家都玩过这个游戏 xff0c 这个游戏陪伴了我们整整一个童年 xff0c 是我们青春的回忆 飞翔的小鸟 xff0c 游戏中玩家只需通过点击方向键操纵让小鸟避开绿色管道等
  • 搭建本地仓库源

    一 如何搭建仓库源 之前讲了定制ISO的方法 xff1a 使用chroot定制系统 xff0c 但有时候我们想自定义的安装包不在上游的仓库源中 xff0c 在我们本地应该怎么办呢 xff1f 如果我们将deb包拷贝到iso目录再安装有点过于
  • 节点操作案例

    1 下拉菜单 xff08 仿微博 xff09 lt DOCTYPE html gt lt html lang 61 34 en 34 gt lt head gt lt meta charset 61 34 UTF 8 34 gt lt me
  • document获取对象的三种三方法

    Document对象中有几个常用的方法 xff0c 我们在Dom简介中提到过 说到获取JavaScript对象的方法 xff0c 最常用的可能就是getElementById了 xff0c 它是Document中最常用的获取对象的方式之一
  • 程序员,最关键的跨越是什么?做到了月薪可能翻上几番~

    黑马程序员视频库 播妞微信号 xff1a boniu236 传智播客旗下互联网资讯 学习资源免费分享平台 作为一名程序员 xff0c 最关键的跨越是什么 xff1f 从普通程序员进阶为熟练开发者 xff0c 从熟练开发者跃升到技术专家或架构
  • 黑马程序员:3分钟带你读懂C/C++学习路线

    随着互联网及互联网 43 深入蓬勃的发展 xff0c 经过40余年的时间洗礼 xff0c C C 43 43 俨然已成为一门贵族语言 xff0c 出色的性能使之成为高级语言中的性能王者 而在今天 xff0c 它又扮演着什么样重要的角色呢 x
  • 数据归一化

    原文链接 xff1a 从公式出发 xff1a 什么是模型收敛的有效方法 xff1f 大家好 xff0c 我是泰哥 数据归一化在模型收敛中起着至关重要的作用 xff0c 从经典机器学习到深度学习的数据归一化方法是如何一步步演变的呢 xff1f
  • 【Python面试】 说说Python变量、函数、类的命名规则?

    最近公众号新增加了一个栏目 xff0c 就是每天给大家解答一道Python常见的面试题 xff0c 反正每天不贪多 xff0c 一天一题 xff0c 正好合适 xff0c 只希望这个面试栏目 xff0c 给那些正在准备面试的同学 xff0c
  • ​LeetCode刷题实战46:全排列

    算法的重要性 xff0c 我就不多说了吧 xff0c 想去大厂 xff0c 就必须要经过基础知识和业务逻辑面试 43 算法面试 所以 xff0c 为了提高大家的算法能力 xff0c 这个公众号后续每天带大家做一道算法题 xff0c 题目就从
  • Android硬件访问服务-Service

    Android有四大组件 xff1a 一 Activity 二 Service 三 Broadcast Receiver 四 Content Provider Service是Android中一个类 xff0c 它是Android四大组件之
  • android6.0第三方APP获得设备节点的访问权限

    之前使用android4 4的系统进行开发时 system app xff08 系统自带APP xff09 目录下的 app 可以直接访问 dev 目录下的设备节点 xff0c Android 5 0 以后 xff0c 因为采取了 SEAn
  • U-boot取消或修改启动延时bootdelay

    在我们的实际项目中都希望uboot尽量能够快速启动 xff0c 这就涉及到uboot的裁剪工作 xff0c 由于裁剪的工作量和内容比较多 xff0c 这里暂不描述 但是uboot有个启动延时bootdelay xff0c 在我们进入linu
  • uboot启动分析第一阶段(start.S)

    前面分析了启动脚本 Makefile mkconfig xff0c 接下来就是uboot的start S这个启动代码了 xff0c 下面是本章的平台介绍 xff1a 单板 xff1a 迅为4412开发板 Exynos 4412 SDRAM
  • Android使用串口(基于android-serialport-api)

    运行平台 xff1a CPU xff1a 全志V40 Android版本 xff1a 6 0 1 关于安卓设备上使用串口 xff0c 谷歌官方在github上有提供代码实例 xff0c 里面有JNI的代码和串口API的java文件 xff0
  • FreeRTOS-启动任务调度器源码分析

    本章基于FreeRTOS的启动任务调度器源码分析 xff0c 后续将会上传其它我对FreeRTOS的源码分析过程及理解 xff0c 首先来认识一下任务调度器 任务调度器 xff1a 任务调度器主要用于实现任务的切换 xff0c 任务并不是我