Cortex-M 内核的 OS 特性

news2024/12/26 21:07:09

目录

  • 一、通用堆栈知识
  • 二、双堆栈用法
  • 三、PendSV 中断介绍和用法
  • 四、SVC 软中断介绍和用法
  • 五、特权级和非特权级使用方法


一、通用堆栈知识

在前面讲解 STM32 启动文件的时候就已经提到过,有关堆栈大小的设置是在启动文件中设置的:

Heap 主要用于 Malloc、Free,动态内存申请和释放。

Stack 也非常重要,程序编译后所包含的大量 PUSHPOP 指令操作,系统根据 SP(堆栈指针) 寄存器访问当前对应栈内存,通过栈保存临时数据。大部分的栈都是向下生长的(也有向上生长的)。M 内核的栈是从高地址向低地址生长的,下面通过一个例子演示一下:

__IO uint32_t a = 0x11111111;
__IO uint32_t b = 0x22222222;
__IO uint32_t c = 0x33333333;
__IO uint32_t d = 0x44444444;
__IO uint32_t e = 0x55555555;
__IO uint32_t f = 0x66666666;
__IO uint32_t g = 0x77777777;
__IO uint32_t h = 0x88888888;

在这里插入图片描述

然后进入调试状态,可以看到现在 SP 指针是 0x20000618

可当我们实际点击单步调试的时候,SP 指针现在又变成了 0x200005F80x20000618 - 0x200005F8 等于十进制的 32。也就是说少了 32 个字节。这里是因为程序一来就把这 8 个数据,共 32 个字节的内存分配好了。

当你把 __IO 去掉后,就不会出现以上的情况。__IO 其实就是 volatile,所以在这里就是告诉编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。

下面继续执行,可以看到,数据是从高地址向低地址存储的。

当然,内存空间只是连续字节数据的抽象,本身并不区分堆和栈的概念,它做的只是存储和读写信息。因此,如何定义堆栈、初始化建立堆栈环境,在嵌入式软件运行前便显得尤为重要。这涉及到处理器提供的堆栈机制、操作系统内存管理和进程切换等方方面面。

二、双堆栈用法

下图取自权威指南:

在搭载实时操作系统内核的嵌入式软件中,栈往往分为两大类,除了满足系统基本的主栈(main stack)外,往往还需要进程/线程栈(process stack)。这两部分内存空间是独立存在的:

  • 主栈位于系统的栈区(stack)
  • 而线程堆栈往往定义在堆区(heap)或静态区(static)

无论是静态还是动态创建,线程栈都不会占用栈区的主栈空间。即线程栈并不在栈区,可能在数据区或堆区,再次说明主栈和线程栈内存空间独立。

理解这一点,是理解 MCU 堆栈的关键前提。

所谓双堆栈,本质上都是 R13,不过在不同的运行状态下使用的栈是不同的,并且在同一时刻,只有其中的一个栈可用。在系统复位后、进入线程环境前,默认使用主堆栈,中断服务程序(ISR)中也是使用主堆栈。RTOS 各线程中的应用代码,则使用线程堆栈。

  • 如果使用了双堆栈,那么在中断里只能使用 MSP;而在中断以外,可以使用 MSP,也可以使用 PSP。(不过在 RTOS 里,中断以外全部使用 PSP,后面讲原因)。

上电后,系统仅初始化了 MSP,需要通过额外的汇编代码建立完整的双堆栈系统,当实时内核准备就绪,线程调度正常运行,双堆栈机制开始工作。进中断时系统根据当前状态自动切换堆栈,进程上下文切换时会更新不同线程的 PSP,通过修改 EXC_RETURN 可以手动切换 MSP/PSP

双堆栈机制使得内核/ISR 堆栈和线程应用堆栈分开管理,通过不同的堆栈指针寄存器完成切换,大大提高了系统的效率,在绝大部分的嵌入式实时操作系统中,都使用了双堆栈机制,如 ucos、FreeRTOS、RT-Thread 等。

在一些简单的应用中,例如裸机程序,可以从头到尾都只使用主堆栈,只要确保分配足够的空间即可。

还是一样,通过例程来看,在刚才的程序下面添加:

__set_PSP(__get_MSP()); /* 设置PSP位置 */
__set_CONTROL(0x02);    /* bit1 = 1表示使用PSP,bit1 = 0表示使用MSP */ 
{
    __IO uint32_t a1 = 0x11111111;
    __IO uint32_t b1 = 0x22222222;
    __IO uint32_t c1 = 0x33333333;
    __IO uint32_t d1 = 0x44444444;
    __IO uint32_t e1 = 0x55555555;
    __IO uint32_t f1 = 0x66666666;
    __IO uint32_t g1 = 0x77777777;
    __IO uint32_t h1 = 0x88888888;
    
    printf("a1 = %x, b1 = %x\r\n", a1, b1);
}


下面继续执行:

这里将 PSP 指向 MSP 一样的地址,然后设置使用 PSP:

然后继续执行:

可以看到,现在只是改成使用 PSP 了,其它和 MSP 没有什么区别。

下面我在我的 SysTick_Handler 函数中打一个断点,然后全速执行:

栈指针又切换回 MSP 了。而退出中断后,又会自动切换回 PSP:

这也印证了刚才所说的,在任务中使用 PSP,在中断中使用 MSP。使用双堆栈,把任务和中断使用的栈区分开,这样安全性会更好一些。

三、PendSV 中断介绍和用法

PendSV 典型使用场合是在上下文切换时(在不同任务之间切换)。上下文切换的本质就是保存当前执行现场 A,切换到另一个任务 B 里面。切换回来时又要恢复执行现场 A(将一系列的寄存器入栈、出栈)。

由于 PendSV 的特点就是支持缓期执行,所以 RTOS 可以利用它这个特点,进行任务调度过程的上下文切换。而为什么要使用缓期执行的特点来进行上下文切换呢?简单的说就是任何 RTOS,都需要尽量不打断外设中断。 我们来举例说明,假如一个系统中有两个就绪的任务,上下文切换被切换的场合可以是:

  1. 执行一个系统调用
  2. 系统滴答定时器 SYSTICK 中断,触发了任务的调度。

假如我们在 Systick 中断服务程序中,启动上下文切换,流程图如下:

但若在产生 SysTick 异常时正在响应一个中断,则 SysTick 异常会抢占其ISR。在这种情况下,OS 是不能执行上下文切换的,否则将使中断请求被延迟,而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能容忍这种事。因此,在 CM3 中也是,如果 OS 在某中断活跃时尝试切入线程模式,将触犯用法 fault 异常。

为解决此问题,早期的 OS 大多会检测当前是否有中断在活跃中,只有在无任何中断需要响应时,才执行上下文切换(切换期间无法响应中断)。然而,这种方法的弊端在于,它可以把任务切换动作拖延很久(因为如果抢占了 IRQ,则本次 SysTick 在执行后不得作上下文切换,只能等待下一次 SysTick 异常),尤其是当某中断源的频率和 SysTick 异常的频率比较接近时,会发生“共振”,使上下文切换迟迟不能进行。

如果将 SysTick 的优先级设置为最低,然后在 SysTick 中进行上下文切换,然后任务调度。这样不会触发 Fault。但是也有一个问题:

  • 一般 OS 在调度任务时,会关闭中断,也就是进入临界区,而 OS 任务调度是要耗时的,这就会出现一种情况: 在任务调度期间,如果新的外部 IRQ 发生,CPU 将不能够快速响应处理。

而现在有了 PendSVPendSV 异常会自动延迟上下文切换的请求(缓期执行),直到其它的 ISR 都完成了处理后才放行。为实现这个机制,需要把PendSV 为最低优先级的异常。

如果 OS 检测到某 IRQ 正在活动并且被 SysTick 抢占,它将悬起一个 PendSV 异常,以便缓期执行上下文切换。

下面用一个数组模拟栈空间,来展示一下函数的切换:

uint32_t  PspStack[100];  /* PspStack栈空间 */

int main(void)
{
	...
	
	p_stk = &PspStack[100];
	    
	/* 硬件自动入栈 */
	*(--p_stk) = (uint32_t)0x01000000;  /* xPSR    */
	*(--p_stk) = (uint32_t)PrintfLogo;  /* PC  */
	*(--p_stk) = (uint32_t)0x00000000;  /* R14 (LR) */
	*(--p_stk) = (uint32_t)0xAAAAAAAA;  /* R12 */
	*(--p_stk) = (uint32_t)0xBBBBBBBB;  /* R3  */
	*(--p_stk) = (uint32_t)0xCCCCCCCC;  /* R2  */
	*(--p_stk) = (uint32_t)0xDDDDDDDD;  /* R1  */
	*(--p_stk) = (uint32_t)0xEEEEEEEE;  /* R0 : argument */
	  
	/* 根据需要手动入栈的 */
	*(--p_stk) = (uint32_t)0x11111111;  /* R11*/
	*(--p_stk) = (uint32_t)0x22222222;  /* R10*/
	*(--p_stk) = (uint32_t)0x33333333;  /* R9 */
	*(--p_stk) = (uint32_t)0x44444444;  /* R8 */
	*(--p_stk) = (uint32_t)0x55555555;  /* R7 */
	*(--p_stk) = (uint32_t)0x66666666;  /* R6 */
	*(--p_stk) = (uint32_t)0x77777777;  /* R5 */
	*(--p_stk) = (uint32_t)0x88888888;  /* R4 */

	/* 使能PendSV中断 */
	SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
	
	/*##- 测试SVC软中断 ####################################################*/
    SVCTest(0x11223344, 0x55667788, 0xaabbddee);
	...
}

void PendSV_Handler(void)
{
	__asm("BKPT 0");
	__set_PSP((uint32_t)&PspStack[92]);
}

在 M 内核,R0、R1、R2、R3、R12、R14(LR)、R15(PC)、xPSR,这 8 个寄存器是自动入栈的。而其余的寄存器是手动入栈的。

开启了 PendSV 中断后,当来到 SVCTest(后面会介绍)时就会触发 PendSV 异常。

PendSV_Handler 函数中,我们使 PSP 指向 &PspStack[92],也就是 PrintfLogo。所以当触发 PendSV 中断的时候,我们会将 PSP 指针指向 PrintfLogo 函数。也就是说,原本正常情况下,PC 指针保存的是当前 main 函数中下一条指令的地址,这里我们手动将其修改成了另一个函数的地址。这就是任务切换的本质。

来看现象:

再执行一步的时候就会跳转到 PendSV_Handler

当跳出 PendSV_Handler 函数后,正常情况下我们应该继续在 main 函数中向后执行,而这里会跳转到 PrintfLogo 函数中:

旁边的寄存器的值和我们硬件自动入栈的值也是对应的:

四、SVC 软中断介绍和用法

SVC 用于生成系统函数调用的请求,例如,用户程序不允许直接访问硬件,操作系统可以通过 SVC 提供对硬件的访问。因此,当用户程序想要使用某些硬件时,可以使用 SVC 指令,然后执行操作系统中的软件异常处理程序,并提供用户应用程序请求的服务。通过这种方式,对硬件的访问由操作系统控制,操作系统可以阻止用户应用程序直接访问硬件,从而提供更可靠的系统。

SVC 还可以使软件更具可移植性,因为用户程序不需要知道硬件的编程细节。用户程序只需要知道应用程序编程接口(API)函数 ID 和参数,而实际的硬件级编程是由设备驱动程序处理的。

SVC 异常由 SVC 指令产生,该指令需要一个立即数作为参数,根据这个参数执行不同的 SVC 处理函数。例:

SVC #0x3 ;  调用SVC 函数3
SVC 0x3 ;   传统的语法(没有#)也可行

在 C 语言中,可以使用编译器关键字函数 __svc 或者使用内联汇编代码来执行 SVC 指令。

对于操作系统来说,当 SVC 处理程序被执行时,我们可以通过读取堆栈中的 PC 值来确定 SVC 指令中的立即数据值,然后从该地址读取指令并屏蔽不需要的位。如果使用的PSP堆栈,则还需要通过LR寄存器判断当前使用的是哪个堆栈。

在 ARM7 中有一个软件中断指令 SWI(Software interrupt instruction)。实际上,SVC 指令的二进制编码与 ARM7 中的 SWI 是相同的。由于异常模型发生了变化,这条指令被重命名,以确保程序员能够正确地将软件代码从 ARM7 移植到 Cortex-M3。

SVC 指令被执行后,处理器会根据中断向量表中的信息跳转到相应的 SVC 中断处理函数,执行 SVC 指令时,处理器会自动保存当前的上下文,并将处理器的模式切换为特权模式,从而允许执行受限的操作。

简单来说,RTOS 内核中的各个 API,不是在任务里执行的,而是在 SVC 软中断里执行的。这样一来,就把 RTOS 内核和任务隔离开了。

__svc 是 MDK 内置的一个定义前缀,用于设置 SVC 软中断的序号(0~255),可以用来区分不同的使用场景

void __svc(0)  SVCTest(uint32_t a, uint32_t b, uint32_t c); /* SVC软中断 */

...

/*##- 测试SVC软中断 ####################################################*/
SVCTest(0x11223344, 0x55667788, 0xaabbddee);


单步运行后直接跳转,并且把函数数据也一并保存在 R0、R1、R2 寄存器中:


当我们调用 OS 内核的 API 的时候,一调用就会进入到 SVC 中断,让这个函数直接在 SVC 中断中执行,这样把 OS 的函数和我们自己写的应用程序隔离开了。

五、特权级和非特权级使用方法

特权级和非特权级只是针对中断外的任务来说的,中断全部都是特权级

如果代码是在中断里执行,则只能是特权级;中断以外可以是特权级,也可以是非特权级。

非特权级主要是 RTX 在使用。如果是在非特权级的模式下,则不能访问和配置 Cortex-M 内核相关的寄存器(例如 SCB),否则会触发异常。

/*##- 测试非特权级 ####################################################*/
__set_CONTROL(0x01);  /* bit0 = 1表示应用程序工作在非特权级模式,bit1 = 0表示应用程序工作在特权级模式 */ 
SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;  /* 操作SCB就会进入异常 */

例程中先开启非特权模式,然后再操作 SCB(系统控制块)寄存器时会触发异常:

直接就来到 HardFault 了。

那非特权级模式下核心外设寄存器如何初始化?

如果用户将 RTX 操作系统的任务设置在非特权级模式下运行,那么核心外设寄存器应该放在哪里进行初始化呢,主要有以下两种方法:

  • 使用 SVC(Supervisor Call)软中断。
  • 在初始化和开启RTX多任务前做核心外设的初始化。

那在 Cortex-M3/M4/M7 内核如何切换两种模式?Cortex-M3/M4/M7 中的特殊功能寄存器包括:

  • 程序状态寄存器组(PSRs、xPSR)
  • 中断屏蔽寄存器组(PRIMASK、FAULTMASK、BASEPRI)
  • 控制寄存器(CONTROL)

其中控制寄存器 CONTROL 是用来设置特权级和非特权级切换的,CONTROL 寄存器定义如下:

前面的例子中也演示了如何设置 CONTROL 寄存器:

__set_CONTROL(uint32_t control)

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

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

相关文章

学习Redisson实现分布式锁

官网&#xff1a;https://redisson.org/ 官方文档&#xff1a;https://redisson.org/docs/getting-started/ 官方中文文档&#xff1a;https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95 1、引入依赖 <!--redisson--> <dependency><groupId>or…

基础教程 | 用VuePress搭建一个简单的个人博客(附源码)

先附上自己个人博客页面&#xff1a;https://illusionno.github.io/ 源码也在这里&#xff1a;https://github.com/illusionno/my-blog &#xff08;如果觉得有帮助&#xff0c;可以点颗star✨&#xff09; 使用的主题是vuepress-theme-reco2.x&#xff0c;并在上面进行了一些调…

基于Java Web众筹系统的设计与实现

文未可获取一份本项目的java源码和数据库参考。 体育俱乐部是我国体育产业的重要组成部分&#xff0c;而乒乓球作为“国球”&#xff0c;在我国拥有最广泛的群众基础。在世界乒坛&#xff0c;面对如此激烈的外部竞争环境&#xff0c;我国乒乓球运动应扎扎实实地研究基层职业乒乓…

考研C语言程序设计_语法相关(持续更新)

目录 一、语法题strlen转义字符内置数据类型字符串结束标志局部变量和全局变量名字冲突 局部优先switch语句中的关键字数组初始化是否正确注意define不是关键字C语言中不能用连等判断switch( )的括号里可以是什么类型?关于if关于switch 二、程序阅读题有关static有关continue说…

初级前端面试(2)

1.讲一下闭包相关知识&#xff0c;和普通函数有什么区别 闭包是什么&#xff1a;JS中内层函数可以访问外层函数的变量&#xff0c;外层函数无法操作内存函数的变量的特性。我们把这个特性称作闭包。 闭包的好处&#xff1a; 隔离作用域&#xff0c;保护私有变量&#xff1b;…

快速了解接口测试

1、定义 什么是接口测试&#xff1f; 接口测试是测试系统组件间接口的一种测试。接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。测试的重点是要检查数据的交换&#xff0c;传递和控制管理过程&#xff0c;以及系统间的相互逻辑依赖关系等。 接口测…

Vue3高级API的使用

介绍 在前面的内容中&#xff0c;我们已经学习到了很多 Vue 3 的基础语法与一些常用的 API。 本节我们将介绍一些高级 API&#xff0c;带领大家进一步深入了解 Vue 3。 主要内容 ●customRef() ●markRaw() ●toRaw() ●获取 DOM 元素 customRef 方法 customRef&#…

DNS隧道技术原理及其典型应用场景剖析

DNS隧道&#xff08;DNS Tunneling&#xff09;本质为一种网络通信技术&#xff0c;它利用DNS协议在客户端和服务器之间传输数据&#xff0c;主要用来绕过网络限制进行数据隐蔽传输&#xff0c;但在实际场景中&#xff0c;DNS隧道已经成为黑客忠爱的攻击媒介。 1. 概览 DNS是互…

Linux之HugePage的原理与使用

Linux之HugePage的原理与使用 虚拟地址与物理地址虚拟地址物理地址虚拟地址与物理地址的转换 HugePage的概念Linux使用HugePage创建HugePage在程序中使用HugePage 总结 虚拟地址与物理地址 在研究HugePage之前&#xff0c;首先需要明白虚拟地址和物理地址的概念。在计算机系统…

baby_web-CTFWeb进阶wp-攻防世界14

baby_web-CTFWeb进阶wp-攻防世界14 页面显示1.php&#xff0c;可以先访问下源码和index.php&#xff0c;发现内容一致&#xff0c;再查看下cookie和header&#xff0c;看一下有没有藏flag&#xff0c;发现没。 dirsearch一下&#xff0c;发现访问index.php会重定向到1.php。抓…

想一键获取视频文案?2024年这3款好用的视频转文字工具来帮忙

嘿&#xff0c;咱们现在活在21世纪&#xff0c;这信息多得跟啥似的。视频这东西&#xff0c;不管是学东西、娱乐还是工作&#xff0c;都挺受欢迎的&#xff0c;因为它直观、生动嘛。但是&#xff0c;有时候咱们想从视频里提取点有用的信息&#xff0c;还挺费劲的。幸好啊&#…

深圳大学-Java程序设计-选实验2 类的初级应用

实验目的与要求&#xff1a; 实验目的&#xff1a;初步掌握面向对象编程中类的编写。 实验要求&#xff1a; (1).运行第4章课件中第4页、第24页、第32页和第34页中的四个程序&#xff0c;并对每一行语句加上注释。对每一行语句加上注释。在报告中附上程序截图、运行结果截图和…

zerotier安装配置

目录 1.简介1.1.基本概念1.1.1.节点1.它可以是:2.每个节点都有以下特征:3.节点的主要功能包括: 1.1.2.根服务器1.定义&#xff1a;2.功能&#xff1a;3.特点&#xff1a;4.角色&#xff1a;5.安全性&#xff1a;6.与普通节点的区别&#xff1a;7.自托管选项&#xff1a; 1.1.3.…

51单片机的智能电饭煲【proteus仿真+程序+报告+原理图+演示视频】

1、主要功能 该系统由AT89C51/STC89C52单片机LCD1602显示模块温度传感器继电器按键、LED等模块构成。适用于智能电饭锅等相似项目。 可实现功能: 1、LCD1602实时显示温度、运行模式、煮饭等模式的计时时间 2、首先选择电饭锅的模式&#xff0c;如煮饭、煮粥、蒸、煲汤&#…

React入门 9:React Router

1. 什么是路由 路由&#xff08;routing&#xff09;就是通过互联的网络把信息从源地址传输到目的地址的活动。 以上是中文维基百科对路由的解释。通俗的来讲&#xff0c;把一个地方的信息传输到他想去的目的地的过程&#xff0c;就叫路由。 2. 用代码解释路由 需求&#xff1a…

【Spring】获取Cookie和Session(@CookieValue()和@SessionAttribute())

获取 Cookie 传统获取 Cookie 这是没有 Spring 的时候&#xff0c;用 Servlet 来获取&#xff08;获取所有的 Cookie&#xff09; Spring MVC 是基于 Servlet API 构建的原始 Web 框架&#xff0c;也是在 Servlet 的基础上实现的 RequestMapping("/getcookie") …

CubeMX电机驱动以及编码器计数配置

一、选型 电机MG310&#xff1a; 电机驱动板TB6612&#xff1a; STM32F407VETx&#xff1a; 二、CubeMX配置 配置外部时钟&#xff1a; GPIO口配置&#xff1a; 需要四个I/O输出口&#xff1a; PWM口配置&#xff1a; 这里使用的是定时器TIM1&#xff0c;设置预分频器71&…

使用mnist数据集和LeakyReLU高级激活函数训练神经网络示例代码

一、概述 神经网络中的激活函数是用于增加网络的非线性特性的函数&#xff0c;没有激活函数&#xff0c;神经网络将仅仅是一个线性模型&#xff0c;无法解决复杂的非线性问题。激活函数的选择对神经网络的性能有很大的影响。 基础激活函数是神经网络中使用较早、较为简单的激…

AOA定位技术在智慧停车场中的应用

‌AOA定位技术是一种基于信号到达角度的定位方法&#xff0c;通过测量无线信号到达接收器的入射角度来实现精确定位。‌ AOA定位技术的核心原理是利用接收器内的多个天线阵列来测量信号的相位差&#xff0c;从而计算出信号的到达角度&#xff0c;最终确定位置‌。 在智慧停…

SpringBoot在高校学科竞赛平台中的应用与优化

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…