首页 > 其他分享 >FreeRTOS从代码层面进行原理分析(2 任务的启动)

FreeRTOS从代码层面进行原理分析(2 任务的启动)

时间:2024-03-26 09:31:09浏览次数:21  
标签:r0 FreeRTOS 层面 代码 portNVIC 任务 寄存器

FreeRTOS分析二—任务的启动

上一篇文章我们带着三个问题开始了对 FreeRTOS 代码的探究。

1. FreeRTOS 是如何建立任务的呢?
2. FreeRTOS 是调度和切换任务的呢?
3. FreeRTOS 是如何保证实时性呢?

并且在上一篇文章 FreeRTOS从代码层面进行原理分析(1 任务的建立) 中对任务的创建进行了分析。
这篇文章让我们一起对 FreeRTOS 是如何进行任务的调度和切换从代码的角度进行逻辑分析。

任务的调度

在 FreeRTOS 官方教程中,在创建完成任务之后只要调用 xTaskStartScheduler 函数就可以将创建的任务给执行起来了。
在这里插入图片描述
那么在 FreeRTOS 中任务究竟是如何调度起来的呢?现在让我们看一下 xTaskStartScheduler 这个函数的内部。

BaseType_t xPortStartScheduler( void )
{
   ...
   
    /* Make PendSV and SysTick the lowest priority interrupts. */
    portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

    /* Start the timer that generates the tick ISR.  Interrupts are disabled
     * here already. */
    vPortSetupTimerInterrupt();

    /* Initialise the critical nesting count ready for the first task. */
    uxCriticalNesting = 0;

    /* Start the first task. */
    prvPortStartFirstTask();

    /* Should never get here as the tasks will now be executing!  Call the task
     * exit error function to prevent compiler warnings about a static function
     * not being called in the case that the application writer overrides this
     * functionality by defining configTASK_RETURN_ADDRESS.  Call
     * vTaskSwitchContext() so link time optimisation does not remove the
     * symbol. */
    vTaskSwitchContext();
    prvTaskExitError();

    /* Should not get here! */
    return 0;
}

配置 SysTick

经过省略,上面的代码中的逻辑已经非常清晰的展现在我们的面前了。先是调整 PendSV & SysTick 的中断优先级。
然后调用 vPortSetupTimerInterrupt 设置 SysTick 中断,对应 FreeRTOS 里面就是设置任务的切换时间颗粒度。这个函数里面的代码也是很简单的。

__attribute__( ( weak ) ) void vPortSetupTimerInterrupt( void )
{
...
    /* Stop and clear the SysTick. */
    portNVIC_SYSTICK_CTRL_REG = 0UL;
    portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;

    /* Configure SysTick to interrupt at the requested rate. */
    portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
    portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT_CONFIG | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}

其中两个关于 SysTick 寄存器定义如下

#define portNVIC_SYSTICK_CTRL_REG             ( *( ( volatile uint32_t * ) 0xe000e010 ) )
#define portNVIC_SYSTICK_LOAD_REG             ( *( ( volatile uint32_t * ) 0xe000e014 ) )

在 cortex-m3 中, 0xe000e014 地址对应的是 系统滴答定时器重载值寄存器(Systick Reload Value Register)。
该寄存器类似一个倒计时的感觉,产生一次系统时钟就在这个寄存器上减少1,这个寄存器中的数值就是这个倒计时的初始值。当倒数结束时可以产生系统滴答定时器中断。
需要注意的是,如果是多次循环倒数的话,时钟脉冲就会多一次,如果需要 100 的初始值,那么实际设置 SRVR 寄存器的时候就要设置为 99 ( 所以代码中才需要减 1UL )。如果是单次倒数的话,那么就按实际情况进行设置就可以了,不用减 1 。

0xe000e010 中断类型控制寄存器(Interrupt Controller Type Register)。设置的内容在代码的注释中也说的比较清楚了,下面直接上图。

在这里插入图片描述
所以这里的代码翻译过来就是将 SysTick重新加载值寄存器 设置好后,当寄存器里面的值减到 0 时,就会引发中断。这个中断后面就会用来对 FreeRTOS 中的任务进行切换。切换的代码在后面的章节进行分析。
现在先继续分析后面的代码。

启动第一个任务

prvPortStartFirstTask 这个函数也是根据不同的单片机架构进行实现的。 在 FreeRTOS\Source\portable\GCC\ARM_CM3\port.c 中可以找到关于 Cortex-m3 的实现。

static void prvPortStartFirstTask( void )
{
    __asm volatile (
        " ldr r0, =0xE000ED08 	\n"/* Use the NVIC offset register to locate the stack. */
        " ldr r0, [r0] 			\n"
        " ldr r0, [r0] 			\n"
        " msr msp, r0			\n"/* Set the msp back to the start of the stack. */
        " cpsie i				\n"/* Globally enable interrupts. */
        " cpsie f				\n"
        " dsb					\n"
        " isb					\n"
        " svc 0					\n"/* System call to start first task. */
        " nop					\n"
        " .ltorg				\n"
        );
}

可以看出这里就是使用汇编代码进行实现的了,代码中的注释比较多,非常有助于我们对逻辑的理解。
0xE000ED08 是向量表偏移寄存器(Vector Table Offset Register),这个寄存器是用于记录 中断向量表的存储位置和其地址的偏移。

在这里插入图片描述在这里插入图片描述
在汇编代码中将中断向量表的的起始地址读出来,写入到 MSP 中,用于在 handler 模式下。
这里稍微补充一下 Cortex-m3 中的 SP 寄存器分为 MSP 和 PSP,MSP 用于在 handler 模式下, PSP 用于在 thread 模式下。这样对实现 OS 的各个进程/线程 的多个堆栈是十分方便的办法。详细可以看这篇优秀的博客中的介绍

后面的 cpsie i & f 注释中已经说的比较清晰了就是 中断的开启。 再往下的 dsb & isb 是设置内存屏障,在这里面作为刷新流水线的作用。

重点介绍 SVC 0 这一行汇编代码。

在这里插入图片描述

SVC 指令会引发 SVC 异常,在 STM32F10x_Startup.s 文件中有中断向量表,其顺序对应 STM32 参考手册中的中断和异常向量。代码和文档都太长了,所以就各截取了部分。
在这里插入图片描述

从上面途中可以看出 SVC 异常对应的就是 SCVall 对应到代码中就是调用 vPortSVCHandler 函数。让我们继续看这个函数。

void vPortSVCHandler( void )
{
    __asm volatile (
        "	ldr	r3, pxCurrentTCBConst2		\n"/* Restore the context. */
        "	ldr r1, [r3]					\n"/* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
        "	ldr r0, [r1]					\n"/* The first item in pxCurrentTCB is the task top of stack. */
        "	ldmia r0!, {r4-r11}				\n"/* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
        "	msr psp, r0						\n"/* Restore the task stack pointer. */
        "	isb								\n"
        "	mov r0, #0 						\n"
        "	msr	basepri, r0					\n"
        "	orr r14, #0xd					\n"
        "	bx r14							\n"
        "									\n"
        "	.align 4						\n"
        "pxCurrentTCBConst2: .word pxCurrentTCB				\n"
        );
}

这里前三行汇编代码表明, r0 中存入了当前 TCB(Task Control Blank, 前面提到过可以利用这个结构索引到任务的栈空间) 的栈定位置的内容。

使用 msr 指令将 r0 的内容存到 PSP 中。关于 PSP 前面有介绍,要是忘记的话这里可直接理解为线程堆栈的指针。

isb 内存屏障,因为本文的主旨是探究 FreeRTOS 是如何对任务进行调度和切换的,所以 这里就暂时不深究为什么 prvPortStartFirstTask 函数使用了 dsb & isb 两个指令,而 vPortSVCHandler 函数只使用了 isb 这一个指令。好奇的读者也可以自行研究一下,欢迎把查到的资料放到评论区哦。

msr	basepri, r0	

对于这行汇编也十分的好懂,我就直接把 basepri 的介绍放在下面的图片中了。
在这里插入图片描述

切换上下文到任务中

"	orr r14, #0xd					\n"
"	bx r14	      						\n"

看到了这两行代码之后大家可能立马就出现了了个疑问, 我知道 R14 代表的是 LR 寄存器,里面一般存的都是函数或者异常的返回地址。
那么新的任务的运行地址是什么时候如何操作才能把它存到 R14 中呢?

实际情况并不是这样的,如果从 handler 模式中返回有三种情况可以使得将 EXC_RETURN 载入 PC 寄存器中。

  1. 使用 POP 指令从 PC 中进行弹栈操作
  2. 使用 BX 指令跳转到任意寄存器
  3. 使用 LDM 或者 LDR 指令给 PC 指定地址

实际上汇编代码正是使用 BX 指令。而 EXC_RETURN[3:0] bits 是可以指定接下来的 SP 是使用 MSP 还是 PSP。

在这里插入图片描述
到这里我们已经知道了,BX r14 会返回 thread mode 并将 SP 设置为 PSP。
但是这样还是不够的,要如何能跳转到正确的任务呢?

其实 《Cortex-m3 技术参考手册》上还介绍了,当处理器陷于异常的时候会自动对8个寄存器进行压栈,而从中断返回时又会对这 8 个寄存器自动进行出栈操作。

在这里插入图片描述
到这里是不是对前一篇文章 FreeRTOS从代码层面进行原理分析(1 任务的建立)xTaskCreate 函数在创建任务的时候 在 port.c 文件中 Cortex-m3 是怎么对栈进行初始初始化的嘛~ 帮你回忆下~

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 ) & portSTART_ADDRESS_MASK; /* PC */
    pxTopOfStack--;
    *pxTopOfStack = ( StackType_t ) portTASK_RETURN_ADDRESS;             /* LR */
    pxTopOfStack -= 5;                                                   /* R12, R3, R2 and R1. */
    *pxTopOfStack = ( StackType_t ) pvParameters;                        /* R0 */
    pxTopOfStack -= 8;                                                   /* R11, R10, R9, R8, R7, R6, R5 and R4. */

    return pxTopOfStack;
}

hhhh~ 是不是一下子就明白了,这些就是处理器自动帮助出入栈的寄存器。 其他寄存器 R4 ~ R11 单独也留了位置。这些设计好的位置会保存所有的寄存器免遭破坏,以达到恢复之前中断任务的作用(后面任务切换的时候会再提)。

再回头看 vPortSVCHandler 函数是不是就逻辑很清晰了~ 把 PSP 指向当前任务的堆栈。再设置通过 R14 设置 EXC_RETURN,将进入 thread 模式,SP 使用 PSP。 再通过 Cortex-m3 的自动将这 8 个寄存器弹栈的功能,完成切换当前执行任务的功能。

这篇写的也有点长了,任务的切换写到下一篇 FreeRTOS从代码层面进行原理分析(3 任务的切换)

标签:r0,FreeRTOS,层面,代码,portNVIC,任务,寄存器
From: https://blog.csdn.net/Megahertz66/article/details/136989896

相关文章

  • 自然语言处理NLP:情感分析疫情下的新闻数据|附代码数据
    原文链接:http://tecdat.cn/?p=11583新冠肺炎的爆发让今年的春节与往常不同。与此同时,新闻记录下了这场疫情发展的时间轴 ( 点击文末“阅读原文”获取完整代码数据******** )。为此我们分析了疫情相关的新闻内容、发布时期以及发布内容的主题和情感倾向这些方面的数据,希望通过这......
  • IDEA常用代码规范设置
    对开发java时,IDEA的部分常用代码规范进行设置设置前的注意事项写完代码后,Ctrl+Alt+L格式化代码设置完之后,不要忘记Apply确认不要在单个项目中设置,全局设置,请关闭项目,在如图所示处设置代码换行在设置前已经超过代码行宽度限制的代码不会自动换行超过120列换行......
  • 代码随想录第20天| 654.最大二叉树 617.合并二叉树
     654.最大二叉树654.最大二叉树-力扣(LeetCode)代码随想录(programmercarl.com)又是构造二叉树,又有很多坑!|LeetCode:654.最大二叉树_哔哩哔哩_bilibili给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建:创建一个根节点,其值为 ......
  • 代码随想录第18天 | 513.找左下角的值 112.路径总和
    513.找左下角的值513.找树左下角的值-力扣(LeetCode)代码随想录(programmercarl.com)怎么找二叉树的左下角?递归中又带回溯了,怎么办?|LeetCode:513.找二叉树左下角的值_哔哩哔哩_bilibili给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。假......
  • 生信小白菜之GEO芯片数据分析流程--附画图代码
    title:“GEOdataanalysis”author:“yuluyang”date:“2024-03-22”生信技能树数据挖掘课程笔记~小洁老师授课主要内容:数据分组的内容关键词的分组和多分组比较idmap报错的原因及解决方法基因组的注释流程数据的行列互换及方差数值画图示例代码示例数据libr......
  • 齿轮系统故障跟踪:一种基于现场测量的传递路径分析方法(Matlab代码实现)
     ......
  • js代码的循环
      上次我们学了条件语句及switch的应用,现在我们学js代码循环的知识。什么是?循环是重复的去执行代码,分别是:for循环、while循环、do-while循环、continue循环和break循环,接下来我们逐条分析。 一、for循环语法:for(初始值;执行的条件;初始值++){执行的代码......
  • js代码知识的启程
           今日,我们踏上征服js代码知识的旅途当中。日后,我们必当手持键盘和鼠标,运用我等在此学到的知识来利于高山之巅。     首先,我们要了解js是什么?js可以干什么?为啥要使用js技术?在计算机当中js可以在啥位置,它的输入方式共分为几种?js代码的语法要注意啥,......
  • 数学建模常用代码
    主成分分析PCA步骤:(1)对原始数据进行标准化处理(2)计算样本相关系数矩阵(3)计算相关系数矩阵R的特征值和相应的特征向量(4)选择重要的主成分,写出主成分表达式例子:下例中企业综合实力排序问题,其中各列分别为:企业序号;净利润率;固定资产利润率;总产值利润率;销售收入利润率;产品成本利......
  • 代码随想录第六天: 哈希表(数组+HashSet+HashMap)
    语言:Java参考资料:代码随想录、ChatGPT3.5当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。如果在做面试题目的时候遇到需要判断一个......