FreeRTOS 任务调度及相关函数详解(一)

news2025/1/11 21:00:01

文章目录

  • 一、任务调度器开启函数 vTaskStartScheduler()
  • 二、内核相关硬件初始化函数 xPortStartScheduler()
  • 三、启动第一个任务 prvStartFirstTask()
  • 四、中断服务函数 xPortPendSVHandler()
  • 五、空闲任务


一、任务调度器开启函数 vTaskStartScheduler()

这个函数的功能就是开启任务调度器的,这个函数在文件 tasks.c中有定义,缩减后的函数代码如下:

void vTaskStartScheduler( void )
{
	BaseType_t xReturn;
	xReturn = xTaskCreate( prvIdleTask, (1)
						"IDLE", configMINIMAL_STACK_SIZE,
						( void * ) NULL,
						( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
						&xIdleTaskHandle );
	#if ( configUSE_TIMERS == 1 ) //使用软件定时器使能
	{
		if( xReturn == pdPASS )
		{
			xReturn = xTimerCreateTimerTask(); (2)
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
	#endif /* configUSE_TIMERS */
	
	if( xReturn == pdPASS ) //空闲任务和定时器任务创建成功。
	{
		portDISABLE_INTERRUPTS(); (3)
		#if ( configUSE_NEWLIB_REENTRANT == 1 ) //使能 NEWLIB
		{
			_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
		}
		#endif /* configUSE_NEWLIB_REENTRANT */
		xNextTaskUnblockTime = portMAX_DELAY;
		xSchedulerRunning = pdTRUE; (4)
		xTickCount = ( TickType_t ) 0U;
		portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); (5)
		if( xPortStartScheduler() != pdFALSE ) (6)
		{
			//如果调度器启动成功的话就不会运行到这里,函数不会有返回值的
		}
		else
		{
			//不会运行到这里,除非调用函数 xTaskEndScheduler()。
		}
	}
	else
	{
		//程序运行到这里只能说明一点,那就是系统内核没有启动成功,导致的原因是在创建
		//空闲任务或者定时器任务的时候没有足够的内存。
		configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
	}
	//防止编译器报错,比如宏 INCLUDE_xTaskGetIdleTaskHandle 定义为 0 的话编译器就会提
	//示 xIdleTaskHandle 未使用。
	( void ) xIdleTaskHandle;
}

(1)、创建空闲任务,如果使用静态内存的话使用函数 xTaskCreateStatic()来创建空闲任务,优先级为 tskIDLE_PRIORITY,宏 tskIDLE_PRIORITY 为 0,也就是说空闲任务的优先级为最低。

(2)、如果使用软件定时器的话还需要通过函数xTimerCreateTimerTask()来创建定时器服务任务。定时器服务任务的具体创建过程是在函数 xTimerCreateTimerTask()中完成的。

(3)、关闭中断,在 SVC 中断服务函数 vPortSVCHandler()中会打开中断。

(4)、变量 xSchedulerRunning 设置为 pdTRUE,表示调度器开始运行。

(5)、当宏 configGENERATE_RUN_TIME_STATS 为 1 的时候说明使能时间统计功能,此时需要用户实现宏 portCONFIGURE_TIMER_FOR_RUN_TIME_STATS,此宏用来配置一个定时器/计数器。

(6)、调用函数 xPortStartScheduler()来初始化跟调度器启动有关的硬件,比如滴答定时器、FPU 单元和 PendSV 中断等等。

二、内核相关硬件初始化函数 xPortStartScheduler()

FreeRTOS 系统时钟是由滴答定时器来提供的,而且任务切换也会用到 PendSV 中断,这些硬件的初始化由函数 xPortStartScheduler()来完成,缩减后的函数代码如下:

BaseType_t xPortStartScheduler( void )
{
	/******************************************************************/
	/****************此处省略一大堆的条件编译代码**********************/
	/*****************************************************************/
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; (1)
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; (2)
	vPortSetupTimerInterrupt(); (3)
	uxCriticalNesting = 0; (4)
	prvStartFirstTask(); (5)
	//代码正常执行的话是不会到这里的!
	return 0;
}

(1)、设置 PendSV 的中断优先级,为最低优先级。

(2)、设置滴答定时器的中断优先级,为最低优先级。

(3)、调用函数 vPortSetupTimerInterrupt()来设置滴答定时器的定时周期,并且使能滴答定时器的中断,函数比较简单,大家自行查阅分析。

(4)、初始化临界区嵌套计数器。

(5)、调用函数 prvStartFirstTask()开启第一个任务。

三、启动第一个任务 prvStartFirstTask()

经过上面的操作以后我们就可以启动第一个任务了,函数 prvStartFirstTask()用于启动第一个任务,这是一个汇编函数,函数源码如下:

__asm void prvStartFirstTask( void )
{
	PRESERVE8
	ldr r0, =0xE000ED08 ;R0=0XE000ED08 (1)
	ldr r0, [r0] ;取 R0 所保存的地址处的值赋给 R0 (2)
	ldr r0, [r0] ;获取 MSP 初始值 (3)
	msr msp, r0 ;复位 MSP (4)
	cpsie I ;使能中断(清除 PRIMASK) (5)
	cpsie f ;使能中断(清除 FAULTMASK) (6)
	dsb ;数据同步屏障 (7)
	isb ;指令同步屏障 (8)
	svc 0 ;触发 SVC 中断(异常) (9)
	nop
	nop
}

(1)、将 0XE000ED08 保存在寄存器 R0 中。一般来说向量表应该是从起始地址(0X00000000)开始存储的,不过,有些应用可能需要在运行时修改或重定义向量表,Cortex-M 处理器为此提供了一个叫做向量表重定位的特性。向量表重定位特性提供了一个名为向量表偏移寄存器(VTOR)的可编程寄存器。VTOR 寄存器的地址就是 0XE000ED08,通过这个寄存器可以重新定义向量表,比如在 STM32F103 的 ST 官方库中会通过函数 SystemInit()来设置 VTOR 寄存器,

代码如下:

SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; //VTOR=0x08000000+0X00

通过上面一行代码就将向量表开始地址重新定义到了0X08000000,向量表的起始地址存储的就是 MSP 初始值。

(2)、读取 R0 中存储的地址处的数据并将其保存在 R0 寄存器,也就是读取寄存器 VTOR中的值,并将其保存在 R0 寄存器中。这一行代码执行完就以后 R0 的值应该为 0X08000000。

(3)、读取 R0 中存储的地址处的数据并将其保存在 R0 寄存器,也就是读取地址 0X08000000处存储的数据,并将其保存在 R0 寄存器中。我们知道向量表的起始地址保存的就是主栈指针MSP 的初始值,这一行代码执行完以后寄存器 R0 就存储 MSP 的初始值。现在来看(1)、(2)、(3)这三步起始就是为了获取 MSP 的初始值而已!

(4)、复位 MSP,R0 中保存了 MSP 的初始值,将其赋值给 MSP 就相当于复位 MSP。

(5)和(6)、使能中断。

(7)和(8)、数据同步和指令同步屏障。

(9)、调用 SVC 指令触发 SVC 中断,SVC 也叫做请求管理调用,SVC 和 PendSV 异常对于OS 的设计来说非常重要。SVC 异常由 SVC 指令触发。在 FreeRTOS 中仅仅使用 SVC 异常来启动第一
个任务,后面的程序中就再也用不到 SVC 了。

四、中断服务函数 xPortPendSVHandler()

在函数 prvStartFirstTask()中通过调用 SVC 指令触发了 SVC 中断,而第一个任务的启动就是在 SVC 中断服务函数中完成的,SVC 中断服务函数应该为 SVC_Handler(),但是
FreeRTOSConfig.h 中通过#define 的方式重新定义为了 xPortPendSVHandler(),如下:

#define xPortPendSVHandler PendSV_Handler

函数 vPortSVCHandler()在文件 port.c 中定义,这个函数也是用汇编写的,函数源码如下:

__asm void vPortSVCHandler( void )
{
	PRESERVE8
	ldr r3, =pxCurrentTCB ;R3=pxCurrentTCB 的地址 (1)
	ldr r1, [r3] ;取 R3 所保存的地址处的值赋给 R1 (2)
	ldr r0, [r1] ;取 R1 所保存的地址处的值赋给 R0 (3)
	ldmia r0!, {r4-r11, r14} ;出栈 ,R4~R11 和 R14 (4)
	msr psp, r0 ;进程栈指针 PSP 设置为任务的堆栈 (5)
	isb ;指令同步屏障
	mov r0, #0 ;R0=0 (6)
	msr basepri, r0 ;寄存器 basepri=0,开启中断 (7)
	orr r14, #0xd ; (8)
	bx r14 (9)
}

(1)、获取 pxCurrentTCB 指针的存储地址,pxCurrentTCB 是一个指向 TCB_t 的指针,这个指针永远指向正在运行的任务。这里先获取这个指针存储的地址,比如我现在的代码测试出来这个指针是存放在 0X20000044,如下图所示。
在这里插入图片描述
(2)、取 R3 所保存的地址处的值赋给 R1。通过这一步就获取到了当前任务的任务控制块的存储地址。比如当前我的程序中这个地址就为 0X20000EE8,如下图所示:
在这里插入图片描述
(3)、取 R3 所保存的地址处的值赋给 R0,我们知道任务控制块的第一个字段就是任务堆栈的栈顶指针 pxTopOfStack 所指向的位置,所以读取任务控制块所在的首地址(0X20000EE8)得到
的就是栈顶指针所指向的地址,当前我的程序中这个栈顶指针(pxTopOfStack)所指向的地址为0X20000E98,如下图所示
在这里插入图片描述
可以看出(1)、(2)和(3)的目的就是获取要切换到的这个任务的任务栈顶指针,因为任务所对应的寄存器值,也就是现场都保存在任务的任务堆栈中,所以需要获取栈顶指针来恢复这些寄存器值!

(4)、R4~R11,R14 这些寄存器出栈。这里使用了指令 LDMIA,LDMIA 指令是多加载/存储指令,不过这里使用的是具有回写的多加载/存储访问指令,用法如下:

LDMIA Rn! , {reg list}

表示从 Rn 指定的存储器位置读取多个字,地址在每次读取后增加(IA),Rn 在传输完成以后写回。对于 STM32 来说地址一次增加 4 字节,比如如下代码:

LDR R0, =0X800
LDMIA R0!, {R2~R4}

上面两行代码就是将 0X800 地址的数据赋值给寄存器 R2,0X804 地址的数据赋值给寄存器 R3,0X8008 地址的数据赋值给 R4 寄存器,然后,重点来了!此时 R0 为 800A!通过这一步我们就从任务堆栈中将 R4~R11 这几个寄存器的值给恢复了。这里有朋友就要问了,R0~R3,R12,PC,xPSR 这些寄存器怎么没有恢复?这是因为这些寄存器会在退出中断的时候 MCU 自动出栈(恢复)的,而 R4~R11 需要由用户手动出栈。这个我们在分析 PendSV 中断服务函数的时候会讲到。到这步以后我们来看一下堆栈的栈顶指针指到哪
里了?如下图所示:
在这里插入图片描述
从上图可以看出恢复 R4~R11 和 R14 以后堆栈的栈顶指针应该指向地址 0X20000EB8,也就是保存寄存器 R0 值的存储地址。退出中断服务函数以后进程栈指针 PSP 应该从这个地址开始恢复其他的寄存器值。

(5)、设置进程栈指针 PSP,PSP=R0=0X20000EB8,如下图所示:
在这里插入图片描述
(6)、设置寄存器 R0 为 0。

(7)、设置寄存器 BASEPRI 为 R0,也就是 0,打开中断!

(8)、R14 寄存器的值与 0X0D 进行或运算,得到的结果就是 R14 寄存器的新值。表示退出异常以后 CPU 进入线程模式并且使用进程栈!

(9)、执行此行代码以后硬件自动恢复寄存器 R0~R3、R12、LR、PC 和 xPSR 的值,堆栈使用进程栈 PSP,然后执行寄存器 PC 中保存的任务函数。至此,FreeRTOS 的任务调度器正式开始运行!

五、空闲任务

在前面讲解函数 vTaskStartScheduler()说过,此函数会创建一个名为“IDLE”的任务,这个任务叫做空闲任务。顾名思义,空闲任务就是空闲的时候运行的任务,也就是系统中其他的任务由于各种原因不能运行的时候空闲任务就在运行。空闲任务是 FreeRTOS 系统自动创建的,不需要用户手动创建。任务调度器启动以后就必须有一个任务运行!但是空闲任务不仅仅是为了满足任务调度器启动以后至少有一个任务运行而创建的,空闲任务中还会去做一些其他
的事情,如下:
1、判断系统是否有任务删除,如果有的话就在空闲任务中释放被删除任务的任务堆栈和任务控制块的内存。

2、运行用户设置的空闲任务钩子函数。

3、判断是否开启低功耗 tickless 模式,如果开启的话还需要做相应的处理空闲任务的任务优先级是最低的,为 0,任务函数为 prvIdleTask()。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/410709.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【ROS2指南-1】配置ROS2环境

资料来源Configuring your ROS 2 environment — ROS 2 Documentation: Dashing documentationhttp://docs.ros.org/en/dashing/Tutorials/Configuring-ROS2-Environment.html 目标:本教程将向您展示如何准备 ROS 2 环境。 教程级别:初学者 时间&…

js控制页面随浏览器放大缩小,页面布局不变

一.给App.vue设置minWidth、minHeight、maxWidth以及maxHeight,值为浏览器的可视窗口大小(我的浏览器不全屏的时候是1920*937,全屏的时候是1920*1080) 1.在main.js中获取浏览器的宽高,并挂载到全局变量上以便使用 // 浏览器窗口,这个地方值不会变,你任意拉扯浏览器也不会改变…

【数据库基本操作】打开数据库

一、启动与关闭 只介绍一种方法: 打开命令行工具,以管理员身份运行 1.启动数据库 net start mysql80 //80是在安装的时候设置的名字(默认),不用在意 2.关闭数据库 net stop mysql80 如题已经成功&#…

场景搭建、素材库、在线标绘等,四维轻云地理空间数据云管理平台新增了这些功能

四维轻云是一款地理空间数据云管理平台,具有地理空间数据在线管理、展示及分享等功能。在四维轻云平台中,用户可以不受时间地点的限制,随时随地管理、查看及分享各类地理空间数据。 为了更好地满足用户需求和进行地理空间数据在线管理&#…

【C++从入门到放弃】string全方面分析(常用接口、模拟实现)

🧑‍💻作者: 情话0.0 📝专栏:《C从入门到放弃》 👦个人简介:一名双非编程菜鸟,在这里分享自己的编程学习笔记,欢迎大家的指正与点赞,谢谢! strin…

d2l 文本预处理textDataset

这一节极其重要,重要到本来是d2l的内容我也要归到pyhon封面,这里面class的操作很多,让我娓娓道来! 目录 1.要实现的函数 2.读取数据集 3.词元化 4.Vocab类 4.1count_corpus(tokens) 4.2class中的各种self 4.2.1 _token_fr…

CS5260设计电路|替代RTD2169设计方案|Typec转VGA方案应用设计

CS5260,RTD2169,AG9300都可实现Type-C TO VGA转换器设计,适用于笔记本电脑、主板、台式机、适配器和对接系统等多个细分市场和显示器应用程序, CS5260设计电路如下: 2. CS5260功能特性: USB-C型规格1.2 VESA显示端口tm (DP) v1.4兼容接收机…

nvm安装及使用

nvm是一个node的版本管理工具。 nvm-windows下载 1、安装 首先要卸载电脑上已经有的node版本,注意需要卸载干净,再安装nvm 一路 next 安装就可以了。 安装成功后,以管理员身份运行(很重要!!!&…

MIPI 打怪升级之DCS篇

目录1 Overview2 Display Architectures2.1 The Type 1 Display Architecture3 Power Level3.1 Type 1 Display Architecture Power Change Sequences3.2 Type 2 Display Architecture Power Change Sequences3.3 Type 3 Display Architecture Power Change Sequences4 Gamma C…

unity的学习,准备搞一款mmo小游戏,服务器和客户端从零学

如代码所示,简单了解一下。 using System.Collections; using System.Collections.Generic; using UnityEngine;public class test : MonoBehaviour { void Awake(){Debug.Log("awake hello world!");}// 当脚本可用时,也就是打勾的时候可以…

线程同步-信号量-互斥量-条件变量

文章目录线程同步信号量互斥量条件变量线程同步 线程同步其实实现的是线程排队。防止线程同步访问共享资源造成冲突。多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同…

Java避免死锁的几个常见方法(有测试代码和分析过程)

目录 Java避免死锁的几个常见方法 死锁产生的条件 上死锁代码 然后 :jstack 14320 >> jstack.text Java避免死锁的几个常见方法 Java避免死锁的几个常见方法 避免一个线程同时获取多个锁。避免一个线程在锁内同时占用多个资源,尽量保证每个锁…

Geoserver启动时提示:The GEOSERVER_HOME variable is not defined

场景 GeoServer简介、下载、配置启动、发布shapefile全流程(图文实践): GeoServer简介、下载、配置启动、发布shapefile全流程(图文实践)_霸道流氓气质的博客-CSDN博客 在下载解压之后点击启动bat时提示: The GEOSERVER_HOME environment variable is not defin…

row_number 和 cte 使用实例:背包问题

row_number 和 cte 使用实例:背包问题背包问题01背包解决同一行数据需要引用两次的问题对 for xml 的结果进行引用时的处理完全背包多重背包小结背包问题 最近老顾从新把算法捡了起来,碰到了各种各样以前没见过的,工作中没遇到的问题&#x…

leetcode:快乐数(详解)

前言:内容包括:题目,代码实现,大致思路,代码解读 题目: 编写一个算法来判断一个数 n 是不是快乐数。 「快乐数」 定义为: 对于一个正整数,每一次将该数替换为它每个位置上的数字…

坚鹏:《银行业数字化转型指导意见》政策解读及银行数字化转型

中国银保监会《关于银行业保险业数字化转型的指导意见》政策解读及银行数字化转型课程背景: 很多银行存在以下问题: 不知道如何准确理解中国银保监会《关于银行业保险业数字化转型的指导意见》相关政策 不清楚中国银保监会《关于银行业保险业数字化转型…

使用AI进行“文本纠错”

AI在现实中的应用有很多,你有没有想过,它还可以进行文本纠错呢?传统的校对既耗时又枯燥,通过“AI纠错”,不仅能更快完成,还能提高准确度。那么AI“文本纠错”背后的原理是什么呢?和我一起看看吧…

Python综合案例-小费数据集的数据分析(详细思路+源码解析)

目录 1. 请导入相应模块并获取数据。导入待处理数据tips.xls,并显示前5行。 2、分析数据 3.增加一列“人均消费” 4查询抽烟男性中人均消费大于5的数据 5.分析小费金额和消费总额的关系,小费金额与消费总额是否存在正相关关系。画图观察。 6分析男女顾…

chatGPT写文章攻略-用chatGPT写网文

chatGPT可以写中文吗 ChatGPT可以写中文。在过去的几年中,许多深度学习机器翻译模型已经开始探索中英文翻译、去噪声、语音识别等任务,并且在这些任务中ChatGPT具有最先进的表现。 目前,例如GPT-3和GPT-2都可以用来生成中文文本。为此&…

以太坊上海升级,DeFi 3.0的序章

引言 距离以太坊Shapella升级(也曾被称为上海升级)仅剩一天的时间!自2015年以太坊上线以来,它已成为世界排名第二的加密虚拟资产,以太坊诞生于行业的意义在于它能够让开发者构建智能合约和去中心化应用(DAp…