FreeRTOS任务切换过程深层解析

news2025/1/11 14:18:39

FreeRTOS 系统的任务切换最终都是在 PendSV 中断服务函数中完成的,uCOS 也是在 PendSV 中断中完成任务切换的。

【为什么用PendSV异常来做任务切换】

PendSV 可以像普通中断一样被 Pending(往 NVIC 的 PendSV 的 Pend 寄存器写 1),常用的场合是 OS 进行上下文切换;它可以手动拉起后,等到比他优先级更高的中断完成后,再执行;

假设,带 OS 系统的 CM3 中有两个就绪的任务,上下文切换可以发生在 SYSTICK 中断中:
在这里插入图片描述
这里展现的是两个任务 A 和 B 轮转调度的过程;但是,如果在产生 SYSTICK 异常时,系统正在响应一个中断,则 SYSTICK 异常会抢占其他 ISR。在这种情况下 OS 是不能执行上下文切换的,否则将使得中断请求被延迟;

而且,如果在 SYSTICK 中做任务切换,那么就会尝试切入线程模式,将导致用法 fault 异常;
在这里插入图片描述
为了解决这种问题,早期的 OS 在上下文切换的时候,检查是否有中断需要响应,没有的话,采取切换上下文,然而这种方法的问题在于,可能会将任务切换的动作拖延很久(如果此次的 SYSTICK 无法切换上下文,那么要等到下一次 SYSTICK 再来切换),严重的情况下,如果某 IRQ 来的频率和 SYSTICK 来的频率比较接近的时候,会导致上下文切换迟迟得不到进行;
引入 PendSV 以后,可以将 PendSV 的异常优先级设置为最低,在 PendSV 中去切换上下文,PendSV 会在其他 ISR 得到相应后,立马执行:
在这里插入图片描述
上图的过程可以描述为:

1、任务 A 呼叫 SVC 请求任务切换;

2、OS 收到请求,准备切换上下文,手动 Pending 一个 PendSV;

3、CPU 退出 SVC 的 ISR 后,发现没有其他 IRQ 请求,便立即进入 PendSV 执行上下文切换;

4、正确的切换到任务 B;

5、此刻发生了一个中断,开始执行此中断的 ISR;

6、ISR 执行一半,SYSTICK 来了,抢占了该 IRQ;

7、OS 执行一些逻辑,并手动 Pending PendSV 准备上下文切换;

8、退出 SYSTICK 的 ISR 后,由于之前的 IRQ 优先级高于 PendSV,所以之前的 ISR 继续执行;

9、ISR 执行完毕退出,此刻没有优先级更高的 IRQ,那么执行 PendSV 进行上下文切换;

10、PendSV 执行完毕,顺利切到任务 A,同时进入线程模式;

以上部分摘自:https://www.cnblogs.com/god-of-death/p/14856578.html

【如何设定PendSV优先级】
在这里插入图片描述
往地址为0xE000ED22的寄存器PRI_14写入PendSV优先级

NVIC_SYSPRI14   EQU     0xE000ED22
NVIC_PENDSV_PRI EQU           0xFF
    
    LDR     R1, =NVIC_PENDSV_PRI    
    LDR     R0, =NVIC_SYSPRI14    
    STRB    R1, [R0]  ;将r1 中的 [7:0]存储到 r0 对应的内存
    BX      LR   ;返回

【如何触发PendSV异常】
在这里插入图片描述
往ICSR第28位写1,即可将PendSV异常挂起。若是当前没有高优先级中断产生,那么程序将会进入PendSV handler

NVIC_INT_CTRL   EQU     0xE000ED04                              
NVIC_PENDSVSET  EQU     0x10000000                              

    LDR     R0, =NVIC_INT_CTRL                                 
    LDR     R1, =NVIC_PENDSVSET
    STR     R1, [R0]
    BX      LR

【测试PendSV异常handler实现任务切换】
如何实现任务切换?三个步骤:

步骤一:在进入中断前先设置PSP。

步骤二:将当前寄存器的内容保存到当前任务堆栈中。进入ISR时,cortex-m3会自动保存八个寄存器到PSP中,剩下的几个需要我们手动保存。

步骤三:在Handler中将下一个任务的堆栈中的内容加载到寄存器中,并将PSP指向下一个任务的堆栈。这样就完成了任务切换。

要在PendSV 的ISR中完成这两个步骤,我们先需了解下在进入PendSV ISR时,cortex-M3做了什么?

1,入栈。会有8个寄存器自动入栈。入栈内容及顺序如下:
在这里插入图片描述
在步骤一中,我们已经设置了PSP,那这8个寄存器就会自动入栈到PSP所指地址处。

2,取向量。找到PendSV ISR的入口地址,这样就能跳到ISR了。

3,更新寄存器内容。

做完这三步后,程序就进入ISR了。

进入ISR前,我们已经完成了步骤一,cortex-M3已经帮我们完成了步骤二的一部分,剩下的需要我们手动完成。

在ISR中添加代码如下:

MRS R0, PSP

保存PSP到R0。为什么是PSP而不是MSP。因为在OS启动的时候,我们已经把SP设置为PSP了。这样使得用户程序使用任务堆栈,OS使用主堆栈,不会互相干扰。不会因为用户程序导致OS崩溃。

STMDB R0!,{R4-R11}

保存R4-R11到PSP中。C语言表达是*(–R0)={R4-R11},R0中值先自减1,然后将R4-R11的值保存到该值所指向的地址中,即PSP中。

STMDB Rd!,{寄存器列表} 连续存储多个字到Rd中的地址值所指地址处。每次存储前,Rd先自减一次。

若是ISR是从从task0进来,那么此时task0的堆栈中已经保存了该任务的寄存器参数。保存完成后,当前任务堆栈中的内容如下(假设是task0)
在这里插入图片描述
左边表格是预期值,右边是keil调试的实际值。可以看出,是一致的。在任务初始化时(步骤一),我们将PSP指向任务0的栈顶0x20000080。在进入PendSV之前,cortex-M3自动入栈八个值,此时PSP指向了0x20000060。然后我们再保存R4-R11到0x20000040~0x2000005C。

这样很容易看明白,如果需要下次再切换到task0,只需恢复R4~R11,再将PSP指向0x20000060即可。

测试例程:

#define HW32_REG(ADDRESS)  (*((volatile unsigned long  *)(ADDRESS)))
void USART1_Init(void);
void task0(void) ; 

uint32_t  curr_task=0;     // 当前执行任务
uint32_t  next_task=1;     // 下一个任务
uint32_t task0_stack[17];
uint32_t task1_stack[17];
uint32_t  PSP_array[4];

u8 task0_handle=1;
u8 task1_handle=1;

void task0(void) 
{ 
    while(1)
    {
        if(task0_handle==1)
        {
            printf("task0\n");
            task0_handle=0;
            task1_handle=1;
        }
    }
}

void task1(void)
{
    while(1)
    {
        if(task1_handle==1)
        {
            printf("task1\n");
            task1_handle=0;
            task0_handle=1;
        }
    }
}

__asm void SetPendSVPro(void)
{
NVIC_SYSPRI14   EQU     0xE000ED22
NVIC_PENDSV_PRI EQU           0xFF
    
    LDR     R1, =NVIC_PENDSV_PRI    
    LDR     R0, =NVIC_SYSPRI14    
    STRB    R1, [R0]
    BX      LR
}

__asm void TriggerPendSV(void)
{
NVIC_INT_CTRL   EQU     0xE000ED04                              
NVIC_PENDSVSET  EQU     0x10000000                              

    LDR     R0, =NVIC_INT_CTRL                                 
    LDR     R1, =NVIC_PENDSVSET
    STR     R1, [R0]
    BX      LR
}

int main(void)
{
    SetPendSVPro();
    LED_Init();
    uart_init(115200);
    
    printf("OS test\n");
    
    PSP_array[0] = ((unsigned int) task0_stack) + (sizeof task0_stack) - 16*4;
    //PSP_array中存储的为task0_stack数组的尾地址-16*4, 即task0_stack[1]的地址
    HW32_REG((PSP_array[0] + (14*4))) = (unsigned long) task0; /* PC */
    //task0的PC存储在task0_stack[1]地址+14*4, 即task0_stack[15]的地址中
    HW32_REG((PSP_array[0] + (15*4))) = 0x01000000;            /* xPSR */
  
    PSP_array[1] = ((unsigned int) task1_stack) + (sizeof task1_stack) - 16*4;
    HW32_REG((PSP_array[1] + (14*4))) = (unsigned long) task1; /* PC */
    HW32_REG((PSP_array[1] + (15*4))) = 0x01000000;            /* xPSR */    
    
    /* 任务0先执行 */
    curr_task = 0; 
     
    /* 设置PSP指向任务0堆栈的栈顶 */
    __set_PSP((PSP_array[curr_task] + 16*4)); 
    
    SysTick_Config(9000000);
    SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);//72/8=9MHZ     
    /* 使用堆栈指针,非特权级状态 */
    __set_CONTROL(0x3);
    
    /* 改变CONTROL后执行ISB (architectural recommendation) */
    __ISB();
    
    /* 启动任务0 */
    task0();  
    //LED0=0;
    while(1);
}

__asm void PendSV_Handler(void)
{ 
    // 保存当前任务的寄存器内容
    MRS    R0, PSP     // 得到PSP  R0 = PSP
                       // xPSR, PC, LR, R12, R0-R3已自动保存
    STMDB  R0!,{R4-R11}// 保存R4-R11共8个寄存器得到当前任务堆栈
    
    // 加载下一个任务的内容
    LDR    R1,=__cpp(&curr_task)
    LDR    R3,=__cpp(&PSP_array)
    LDR    R4,=__cpp(&next_task)
    LDR    R4,[R4]     // 得到下一个任务的ID
    STR    R4,[R1]     // 设置 curr_task = next_task
    LDR    R0,[R3, R4, LSL #2] // 从PSP_array中获取PSP的值
    LDMIA  R0!,{R4-R11}// 将任务堆栈中的数值加载到R4-R11中
  //ADDS   R0, R0, #0x20
    MSR    PSP, R0     // 设置PSP指向此任务
 // ORR     LR, LR, #0x04   
    BX     LR          // 返回
                       // xPSR, PC, LR, R12, R0-R3会自动的恢复
    ALIGN  4
}

void SysTick_Handler(void)
{
    LED0=!LED0;//位带操作
    if(curr_task==0)
        next_task=1;
    else
        next_task=0;
    TriggerPendSV();
}

串口输出:
在这里插入图片描述
可以看到在任务0和任务1之间来回切换。

【FreeRTOS任务切换源码】

上下文(任务)切换被触发的场合大致分为:
● 可以执行一个系统调用
● 系统滴答定时器(SysTick)中断。

执行系统调用就是执行 FreeRTOS 系统提供的相关 API 函数,比如任务切换函数taskYIELD(),这些 API 函数和任务切换函数
taskYIELD()都统称为系统调用。

函数 taskYIELD()其实就是个宏,在文件 task.h 中有如下定义:
#define taskYIELD() portYIELD()

函数 portYIELD()也是个宏,在文件 portmacro.h 中有如下定义

#define portYIELD()  \
{  \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \   //通过向中断控制和壮态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中断。这样就可以在 PendSV 中断服务函数中进行任务切换了。
\
__dsb( portSY_FULL_READ_WRITE );  \
__isb( portSY_FULL_READ_WRITE );  \
}

中断级的任务切换函数为 portYIELD_FROM_ISR(),定义如下:

#define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x )

#define portEND_SWITCHING_ISR( xSwitchRequired ) \
	if( xSwitchRequired != pdFALSE ) portYIELD()   //可以看出 portYIELD_FROM_ISR()最终也是通过调用函数 portYIELD()来完成任务切换的。

系统滴答定时器(SysTick)中断

void SysTick_Handler(void)
{
	if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//系统已经运行
	{
		xPortSysTickHandler();
	}
}

xPortSysTickHandler()源码如下:

void xPortSysTickHandler( void )
{
	vPortRaiseBASEPRI(); //关闭中断
	{
		if( xTaskIncrementTick() != pdFALSE )  //增加时钟计数器 xTickCount 的值
		{
			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; //通过向中断控制和壮态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中断。这样就可以在 PendSV 中断服务函数中进行任务切换了。
		}
	}
	vPortClearBASEPRIFromISR(); //打开中断
}

真正的任务切换代码在PendSV中断函数中,
FreeRTOS做了如下函数重定义
#define xPortPendSVHandler PendSV_Handler

xPortPendSVHandler函数如下 (汇编 port.c)

__asm void xPortPendSVHandler( void )
{
	extern uxCriticalNesting;
	extern pxCurrentTCB;
	extern vTaskSwitchContext;
	
	PRESERVE8
	
	mrs r0, psp  //读取进程栈指针,保存在寄存器 R0 里面。
	isb
	
	ldr r3, =pxCurrentTCB //获取当前任务的任务控制块
	ldr r2, [r3]  //接上,并将任务控制块的地址保存在寄存器 R2 里面
	
	tst r14, #0x10 //判断任务是否使用了 FPU,如果任务使用了 FPU 的话在进行任务切换的时候就
	it eq          //需要将 FPU 寄存器 s16~s31 手动保存到任务堆栈中,其中 s0~s15 和 FPSCR 是自动保存的
	
	vstmdbeq r0!, {s16-s31} //保存 s16~s31 这 16 个 FPU 寄存器
	stmdb r0!, {r4-r11, r14} //保存 r4~r11 和 R14 这几个寄存器的值
	str r0, [r2]  //将寄存器 R0 的值写入到寄存器 R2 所保存的地址中去,也就是将新的栈顶保存在任务控制块的第一个字段中。
	stmdb sp!, {r3} //将寄存器 R3 的值临时压栈,寄存器 R3 中保存了当前任务的任务控制块
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY //关闭中断,进入临界区
	msr basepri, r0  							  //关闭中断,进入临界区
	dsb
	isb
	bl vTaskSwitchContext //调用函数 vTaskSwitchContext(),此函数用来获取下一个要运行的任务,并将pxCurrentTCB 更新为这个要运行的任务
	mov r0, #0  //打开中断,退出临界区。
	msr basepri, r0  //打开中断,退出临界区。
	ldmia sp!, {r3} //刚刚保存的寄存器 R3 的值出栈,恢复寄存器 R3 的值
	ldr r1, [r3]  //获取新的要运行的任务的任务堆栈栈顶,
	ldr r0, [r1]  //接上,并将栈顶保存在寄存器 R0 中
	ldmia r0!, {r4-r11, r14} //R4~R11,R14 出栈,也就是即将运行的任务的现场
	tst r14, #0x10 //判断即将运行的任务是否有使用到 FPU,如果有的话还需要手工恢复 FPU的 s16~s31 寄存器。
	it eq          //同上
	vldmiaeq r0!, {s16-s31} //同上
	msr psp, r0 //更新进程栈指针 PSP 的值
	isb
	bx r14  //执行此行代码以后硬件自动恢复寄存器 R0~R3、R12、LR、PC 和 xPSR 的值,确定
			//异常返回以后应该进入处理器模式还是进程模式,使用主栈指针(MSP)还是进程栈指针(PSP)。
			//很明显这里会进入进程模式,并且使用进程栈指针(PSP),寄存器 PC 值会被恢复为即将运行的
			//任务的任务函数,新的任务开始运行!至此,任务切换成功。
}

参考:
https://www.cnblogs.com/WeyneChen/p/4891885.html

https://www.cnblogs.com/god-of-death/p/14856578.html

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

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

相关文章

Spark零基础入门实战(五)使用Eclipse创建Scala项目

本节讲解在Windows中使用Scala for Eclipse IDE编写Scala程序。 安装Scala for Eclipse IDE Scala for Eclipse IDE为纯Scala和混合Scala与Java应用程序的开发提供了高级编辑功能,并且有非常好用的Scala调试器、语义突出显示、更可靠的JUnit测试查找器等。 Scala for Eclip…

重磅首发!腾讯前晚最新爆出的“JVM学习笔记”,GitHub已评“钻级”,看完我爱了!

前言 “JVM”,一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。有了JVM后,Java语言在不同平台上运行时不需要重新编译,为我们提供了极大的便利性,现在在面试当中“JVM”相关的知识是必问…

5分钟部署云计算|云原生监控平台Prometheus-尚文网络xUP楠哥

~~全文共1277字,阅读需约5分钟。 进Q群11372462,领取专属报名福利,包含云计算学习路线图代表性实战训练大厂云计算面试题资料! # Prometheus介绍 Prometheus是由Go编写的时间序列监控数据库,在目前云计算|云原生时代非常流行&am…

分析linux内核qspi驱动层次

【推荐阅读】 需要多久才能看完linux内核源码? 概述Linux内核驱动之GPIO子系统API接口 https://mp.csdn.net/mp_blog/creation/editor/127819883 一篇长文叙述Linux内核虚拟地址空间的基本概括 纯干货,linux内存管理——内存管理架构(建议收藏…

【LeetCode每日一题】——237.删除链表中的节点

文章目录一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【解题思路】七【题目提示】八【时间频度】九【代码实现】十【提交结果】一【题目类别】 链表 二【题目难度】 中等 三【题目编号】 237.删除链表中的节点 四【题目描述】 有一个单链…

[附源码]JAVA毕业设计小区失物招领网站(系统+LW)

[附源码]JAVA毕业设计小区失物招领网站(系统LW) 项目运行 环境项配置: Jdk1.8 Tomcat8.5 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术…

网红家电逐渐沉寂,家电企业如何利用APS排产调整生产?

随着生活水平的提高,近年来的消费行业逐渐呈现出消费升级、个性化、多元化趋势。在这些趋势下,一大批网红小家电产品迅速出现,以创新性的功能和设计,满足消费者新需求。 近年来,小家电领域已经成为网红爆款产品的集中地…

OpenAI ChatGPT注册步骤(超详细!!!)

最近,很火的OpenAI ChatGPT,大伙都跃跃欲试。 由于注册过程比较麻烦,我整理了一下注册步骤。 一、前期准备: 1、梯子(需要科学上网,准备墙外代理) 2、国外接码平台,推荐sms-activ…

java计算机毕业设计ssm学生课堂考勤小程序947n4(附源码、数据库)

java计算机毕业设计ssm学生课堂考勤小程序947n4(附源码、数据库) 项目运行 环境配置: Jdk1.8 Tomcat8.5 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xf…

sprites精灵图+字体图标

1、sprites精灵图 使用精灵图就是为了减少网页请求服务器发送图片的次数,把一些小图标都放到一张图片(称为精灵图)精确单位,就不会请求服务器多次了 使用精灵图核心: 精灵技术主要针对于背景图片使用,就是把多个小背景图片整合到…

Qt-数据库开发-外键使用(4)

Qt-数据库开发-使用QSqlRelationalTableModel(关系表模型)来可视化数据库中[外键] 文章目录Qt-数据库开发-使用QSqlRelationalTableModel(关系表模型)来可视化数据库中[外键]1、概述2、实现效果3、主要代码4、完整源代码更多精彩内…

Swift学习笔记笔记(八) 日期选择和表现视图组件的使用

一、实验目的: 1.掌握DatePicker组件的使用 2.掌握TableView组件的使用 3.掌握代码设置属性的方法 二、实验原理: 1.属性面板设置属性的缺点 2.DatePicker中Moder属性的设置方法 3.DatePicker中Locale属性的设置方法 4.随机数函数的原型 5. 运动检测函…

Python-matplotlib画图要点【大总结】

文章目录一、x,y坐标图1、基本操作2、进阶操作(1)解决中文乱码情况(2)调整图像大小(3)加标题、坐标文字、坐标轴标签(5)去掉上边框二、柱状图1、柱状图画图原理2、三、颜色与标记形状…

运筹说 第83期丨我国网络计划奠基人——华罗庚

经过之前的学习,相信大家已经对运筹学的图论的内容有了一定的了解,接下来小编将带你学习新一章的内容,先来看看网络计划的起源发展,然后共同走近我国网络计划奠基人——华罗庚,去领略他不平凡的一生。 01 网络计划起源…

Pr:导出设置之管理显示色域体积及内容光线级别

视频 VIDEO设置因所选导出格式而异。每种格式都有独特的要求,这些要求决定了哪些设置可用。以导出 H.264 文件格式为例,下面给出 HDR 显示器及节目内容显示相关的选项及说明。管理显示色域体积Mastering Display Color Volume对内容进行分级时所使用的 H…

获取pdf中固定位置图片的二维码,然后解析

1、需要引入下面的pom坐标如下​​ ​2.完整代码如下import com.github.binarywang.utils.qrcode.BufferedImageLuminanceSource; import com.google.zxing.BinaryBitmap; import com.google.zxing.EncodeHintType; import com.google.zxing.MultiFormatReader; import com.goo…

itop3568开发板在Linux系统中使用NPU

下载rknpu2并拷贝虚拟机Ubuntu,如下图所示,RKNPU2提供了访问rk3568 芯片 NPU 的高级接口。 下载地址为“iTOP-3568 开发板\02_【iTOP-RK3568 开发板】开发资料\11_NPU 使用配套资料\01_rknpu2 工具” 对于 RK3568 来说,Linux 平台 RKNN SDK 库…

[Spring5.3.2] Servlet[springmvc]的Servlet.init()引发异常, 解析类文件失败

Spring / Spring MVC遇到问题 找了一本spring相关的教材,书上的代码拿过来就能运行,自己写就总报HTTP 500错误,反复检查没有任何一处写错,同一个错误卡了我三天,非常郁闷,今天终于解决了.特此记录. 问题表现: 图中提到的问题: 例外情况 javax.servlet.ServletException: Ser…

编码规约学习要点

工程结构应用分层 日志规约 编程规约 > 其它 【强制】日期格式化时,传入 pattern 中表示年份统一使用小写的 y。 说明:日期格式化时,yyyy 表示当天所在的年,而大写的 YYYY 代表是 week in which year(JDK7 之后引…

第二证券|热门板块再次爆发,早盘主力抢筹超10亿元!

抗原检测、房地产概念股团体冲高,板块热度居高不下。 抗原检测概念股团体上涨 12月9日早盘,新冠抗原检测概念股团体上涨,概念指数涨4.74%,明德生物涨停,九安医疗、万孚生物、热景生物涨幅居前,分别上涨8.4…