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

FreeRTOS任务切换详解

概要 :在使用FreeRTOS的时候,我们可以很容易在网上找到移植教程,并成功地运行到我们的单片机上。很多时候可能我们只需要去使用RTOS给到的接口,很少涉及到其运行的原理,本篇就是为了揭开其中最为关键(自认为)的一部分的运行原理,基本上是针对port.c这个文件上的内容进行讲解。

需要理解的前置知识

PenSV和SVC系统服务调用

SVC(Supervisor Call,系统服务调用):可以使单片机进入特权模式创建第一个任务,FreeRTOS v9.0.0 版本之前是使用这个系统服务去创建开始第一个任务,后续版本则都是仅使用PenSV系统服务。

PendSV(Pendable Service Call,可悬起系统调用): 该系统服务可以设置很低的优先级(任务切换,延迟处理事件),确保其他中断处理完成之后才调用该中断,且会自动保存当前寄存器上下文(xPSR、R0-R15)到栈中。

PendSV 异常发生时的寄存器入栈顺序

  1. xPSR(程序状态寄存器)
  2. PC(R15)(程序计数器,保存下一条要执行的指令地址)
  3. LR(R14)(链接寄存器,保存返回地址)
  4. R12(通用寄存器)
  5. R3(通用寄存器)
  6. R2(通用寄存器)
  7. R1(通用寄存器)
  8. R0(通用寄存器)
    在这里插入图片描述
    单片机会依次将这些寄存器压入堆栈,而R4-R11则需要我们手动保存,理解这点很重要。

ARM汇编

MRS R0, CPSR    ; R0 = CPSR  将特殊寄存器的值传送到通用寄存器
MRS R0, CPSR    ; 读取CPSR到R0
ORR R0, R0, #0x80    ; 设置I位(bit7)
MSR CPSR_c, R0    ; 将修改后的值写回CPSR控制域 将通用寄存器的值传送到特殊寄存器
STMIA SP!, {R0-R3}    ; [SP] = R0, SP+4; [SP] = R1, SP+4; ... 批量存储寄存器到内存(地址自增)
LDR R0, =0x20000000    ; R0 = 0x20000000
LDR R1, [R0]    ; R1 = [0x20000000]  从内存加载数据到寄存器
ADDS R0, R1, R2    ; R0 = R1 + R2 带进位加法并更新标志位
MOVS R0, #0x5    ; R0 = 5, Z=0 (因结果非零) 数据传送并更新标志位
POP {R0-R3, PC}    ; 从栈中依次恢复R0-R3,最后恢复PC  从栈顶弹出数据到寄存器
PUSH {R4-R7, LR}    ; SP先减4*5=20,再依次存入R4-R7、LR  将寄存器值压入栈顶

源码讲解

新增任务初始化堆栈

StackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack,TaskFunction_t pxCode,void * pvParameters ){/* Simulate the stack frame as it would be created by a context switch* interrupt. */pxTopOfStack--;                                   /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */*pxTopOfStack = portINITIAL_XPSR;                 /* xPSR */pxTopOfStack--;*pxTopOfStack = ( StackType_t ) pxCode;           /* PC */pxTopOfStack--;*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR */pxTopOfStack -= 5;                                /* R12, R3, R2 and R1. */*pxTopOfStack = ( StackType_t ) pvParameters;     /* R0 */pxTopOfStack -= 8;                                /* R11..R4. */return pxTopOfStack;}

理解这个函数 是理解整个FreeRTOS任务切换的关键,上面这个函数在调用创建一个新任务的函数(xTaskCreateStatic)会被调用到,篇幅有限具体可看源码,这里对里面的代码逐步进行讲解。以上函数初始化的其实对应的是当单片机触发了PenSV异常后,单片机CPU内寄存器出入栈的动作。

**代码逐行讲解

*pxTopOfStack = portINITIAL_XPSR; /* xPSR */
这个操作是将xPSR的bit24位置一,在 ARM Cortex-M 架构中,xPSR 寄存器的 bit24 是 T 位(Thumb 状态位),T=1:表示处理器处于 Thumb 指令集模式(Cortex-M 架构只支持 Thumb 模式,不支持传统的 ARM 32 位指令集)。

*pxTopOfStack = ( StackType_t ) pxCode; /* PC */
这个是将R15 也就是 PC寄存器的值设置为任务函数的入口位置,当触发PenSV出栈的时候会去运行到这个函数(任务)。

*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR */
将R14寄存器(LR)的值填上返回函数,任务函数的返回值,我们都知道FreeRTOS在运行的时候一般不会返回,一般有返回了可能是出了点错误。

*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */ R0寄存器的值,也就是我们的任务的调用参数。

pxTopOfStack -= 8; /* R11..R4. */
这个是R4-R11寄存器的值,需要我们手动保存,这个会在任务切换里面实现。

开启第一个任务

__asm void prvPortStartFirstTask( void ){extern pxCurrentTCB;PRESERVE8/* The MSP stack is not reset as, unlike on M3/4 parts, there is no vector* table offset register that can be used to locate the initial stack value.* Not all M0 parts have the application vector table at address 0. *//* *INDENT-OFF* */ldr r3, = pxCurrentTCB /* Obtain location of pxCurrentTCB. */ldr r1, [ r3 ]ldr r0, [ r1 ]         /* The first item in pxCurrentTCB is the task top of stack. */adds r0, # 32          /* Discard everything up to r0. */msr psp, r0            /* This is now the new top of stack to use in the task. */movs r0, # 2           /* Switch to the psp stack. */msr CONTROL, r0isbpop { r0 - r5 } /* Pop the registers that are saved automatically. */mov lr, r5 /* lr is now in r5. */pop { r3 } /* The return address is now in r3. */pop { r2 } /* Pop and discard the XPSR. */cpsie i /* The first task has its context and interrupts can be enabled. */bx r3 /* Finally, jump to the user defined task code. */ALIGN/* *INDENT-ON* */}

上面是FreeRTOS官方源码部分,现在使用的是FreeRTOS V9.0.0版本,所以没有使用SVC进行启动第一个任务。

**代码逐步讲解

ldr r3, =pxCurrentTCB 获取<font color = red>pxCurrentTCB的地址</font> 相当于 &pxCurrentTCB`` 保存到r3

``ldr r1, [r3]
获取pxCurrentTCB的值,保存在r1 里面

ldr r0, [r1] 因为pxCurrentTCB也是指针 所以这一步是获取pxCurrentTCB第一个指向的位置,也就是volatile StackType_t *pxTopOfStack; ``任务块栈顶的位置

adds r0, #32
msr psp, r0

因为之前在初始化的时候我们手动设置了R4-R11的信息
现在R0的地址加上32个字节,所以R0现在指向的是上面那个位置,然后更新到PSP中去。

movs r0, #2
msr CONTROL, r0
isb

这三步是切换栈指针 将栈指针从MSP切换到PSP,并等待指令完成。

pop {r0-r5}
mov lr, r5

这两步是将刚刚我们获取TCB块指针中的内容弹出,依次对应的是

获取值对应之前压入堆栈的值
R0R0
R1R1
R2R2
R3R3
R4R12
R5R14(LR)

然后又继续将r5的值复制到lr 和之前我们设置的是一致的

pop {r3}
pop {r2}

继续弹出r3 此时对应的是之前 存入TCB块的 R15的值也就是 任务函数入口

R2 对应的是 xPSR的值

cpsie i
bx r3

这两步的作用是 开中断 然后进行跳转,跳转的就是pxCurrentTCB 指向的那个任务函数入口

PenSV任务切换

__asm void xPortPendSVHandler( void )
{extern vTaskSwitchContextextern pxCurrentTCB/* *INDENT-OFF* */PRESERVE8mrs r0, pspldr r3, = pxCurrentTCB /* Get the location of the current TCB. */ldr r2, [ r3 ]subs r0, # 32  /* Make space for the remaining low registers. */str r0, [ r2 ] /* Save the new top of stack. */stmia r0 !, { r4 - r7 } /* Store the low registers that are not saved automatically. */mov r4, r8 /* Store the high registers. */mov r5, r9mov r6, r10mov r7, r11stmia r0 !, { r4 - r7 }push { r3, r14 }cpsid ibl vTaskSwitchContextcpsie ipop { r2, r3 } /* lr goes in r3. r2 now holds tcb pointer. */ldr r1, [ r2 ]ldr r0, [ r1 ] /* The first item in pxCurrentTCB is the task top of stack. */adds r0, # 16  /* Move to the high registers. */ldmia r0 !, { r4 - r7 } /* Pop the high registers. */mov r8, r4mov r9, r5mov r10, r6mov r11, r7msr psp, r0   /* Remember the new top of stack for the task. */subs r0, # 32 /* Go back for the low registers that are not automatically restored. */ldmia r0 !, { r4 - r7 } /* Pop low registers.  */bx r3ALIGN/* *INDENT-ON* */}
mrs r0, psp
ldr r3, =pxCurrentTCB
ldr r2, [r3]

将psp 进程栈指针的值存入r0,r2为获取pxCurrentTCB当前任务的栈顶指针

stmia r0!, {r4-r7}
mov r4, r8
mov r5, r9
mov r6, r10
mov r7, r11
stmia r0!, {r4-r7}

这几步是保存上下当前任务的上下文,正如前面提到的r4-r11的值在中断发生的时候不会自动保存,至于为什么不直接使用stmia r0!, {r4-r11},这是因为现在使用的是M0架构的单片机,不允许直接同时操作高低寄存器,r4-r7属于低寄存器 r8-r11是属于高寄存器

push {r3, r14}
cpsid i
bl vTaskSwitchContext
cpsie i

将r3 r14的值压入堆栈 r3是pxCurrentTCB的指针(经过vTaskSwitchContext 函数后会切换到下一个指针 r14就是跳转错误链接)

ldr r1, [r2]
ldr r0, [r1]
adds r0, r0, #16
ldmia r0!, {r4-r7}
mov r8, r4
mov r9, r5
mov r10, r6
mov r11, r7

r0获取任务切换后的pxCurrentTCB栈顶指针,adds r0, r0, #16,是为了先恢复高寄存器,

msr psp, r0
subs r0, r0, #32
ldmia r0!, {r4-r7}

将r0的值赋值给psp 此时r0指向的是栈顶指针
然后subs r0, r0, #32 是为了恢复低寄存器也就是r4-r7

结尾

一开始学习的时候可能是会有点绕,多看几遍基本上就可以很清晰地了解整个过程了,加油共勉。


文章转载自:

http://Cg9O0HrQ.rLxnc.cn
http://Q8fnJkr9.rLxnc.cn
http://8g7h36xx.rLxnc.cn
http://XDnAVMfb.rLxnc.cn
http://YID5d4IU.rLxnc.cn
http://ELq7yoKw.rLxnc.cn
http://HK9HvzAc.rLxnc.cn
http://R66Q6ROt.rLxnc.cn
http://WipMUldO.rLxnc.cn
http://bMlyzEwz.rLxnc.cn
http://ubRWB1Lq.rLxnc.cn
http://akg9nuhR.rLxnc.cn
http://7UuXOeRj.rLxnc.cn
http://VrCUgQD9.rLxnc.cn
http://Y6u58hN5.rLxnc.cn
http://QFsu79mr.rLxnc.cn
http://r1sDUoNy.rLxnc.cn
http://r25fYdfZ.rLxnc.cn
http://7EAhrOXm.rLxnc.cn
http://KAgCLdeN.rLxnc.cn
http://FymNWCvc.rLxnc.cn
http://ouDDc8Bd.rLxnc.cn
http://JxvJZKiI.rLxnc.cn
http://2bmP6rBG.rLxnc.cn
http://qvVZ3R2e.rLxnc.cn
http://wdfEeW6b.rLxnc.cn
http://GJLEokyl.rLxnc.cn
http://eFwX223T.rLxnc.cn
http://n8TR7j4L.rLxnc.cn
http://9swbM38N.rLxnc.cn
http://www.dtcms.com/a/375362.html

相关文章:

  • 面试不会问题
  • 享元模式,用Qt/C++绘制森林
  • GO RPC 教学文档
  • Atlantis Word Processor:全方位的文字处理专家
  • [iOS] 单例模式的深究
  • 视频通话实现语音转文字
  • String-HashCode源码分析
  • 深入浅出C++继承机制:从入门到实战
  • 级联框的实现
  • android 性能优化—内存泄漏,内存溢出OOM
  • 从PyTorch到ONNX:模型部署性能提升
  • JAVA:实现快速排序算法的技术指南
  • SQL 触发器从入门到进阶:原理、时机、实战与避坑指南
  • 无标记点动捕技术:重塑展厅展馆的沉浸式数字交互新时代
  • 【Agent】DeerFlow Planner:执行流程与架构设计(基于真实 Trace 深度解析)
  • R语言读取excel文件数据-解决na问题
  • 在钉钉上长出的AI组织:森马的路径与启示
  • IntelliJ IDEA 中 JVM 配置参考
  • JVM(二)--- 类加载子系统
  • 9.ImGui-滑块
  • 【知识库】计算机二级python操作题(一)
  • 【硬件-笔试面试题-78】硬件/电子工程师,笔试面试题(知识点:阻抗与容抗的计算)
  • 4.5Vue的列表渲染
  • 使用YOLO11进行路面裂缝检测
  • 常见并行概念解析
  • 9月9日
  • centos系统上部署安装minio
  • 下载CentOS 7——从阿里云上下载不同版本的 CentOS 7
  • 《预约一团乱麻?预约任务看板让你告别排班噩梦!宠物店效率翻倍指南》
  • Shell 脚本条件测试与 if 语句