关于Cortex内核相关的若干疑问

FAQ1. Cortex-M的寄存器组有哪些,有什么作用?

        ARM架构中,处理器不能直接操作内存中的数据,必须先把内存中的数据加载到寄存器组中的寄存器内,处理完成后,若有必要,再将数据写回内存。这种方式一般称为“加载-存储架构”。

       Cortex-M的寄存器组有13个32位通用寄存器,其中前8个寄存器r0-r7为low register,后5个寄存器r8-r12为high register(R0-R7任何汇编指令都有权限访问,R8-R12 只有很少的 16位Thumb指令能访问,32位Thumb-2指令都可以访问。Thumb 指令可以理解成汇编语言的操作码(类似MOV等)),另外3个功能寄存器:r13为栈指针SP,r14为链接寄存器LR,r15为程序计数器PC。同时还有5个特殊寄存器,分别是程序状态寄存器PSR(APSR, IPSR, EPSR),异常掩码寄存器PRIMASK,FAULTMASK,BASEPRI以及控制寄存器CONTROL。

       通用寄存器:用于传送和暂存数据,也可参与算术逻辑运算,并保存运算结果。

       R13栈指针:①用于保存与恢复现场。即函数调用时,ARM处理器会自动将返回地址、部分寄存器内容压入堆栈,在函数执行完毕后,通过堆栈指针恢复堆栈内容并返回调用前的下一条指令;②用于局部变量的存储。函数内部定义的局部变量,如果不使用寄存器分配,通常会存储到堆栈中,而堆栈指针则管理这些变量的生命周期;③用于异常处理,不同的处理器模式(用户模式或者异常模式等)拥有独立的堆栈指针MSP或PSP,异常发生时,处理器会自动切换到对应的堆栈指针,确保异常处理程序不会破坏正常运行时的任务堆栈。

     R14链接寄存器:①用于调用函数或执行程序时,记录返回地址;②异常处理时,该寄存器会被写入一个异常返回值(EXC_RETURN),在异常处理结束时,触发异常返回。

       R15程序计数器:读操作返回当前指令地址+4,写操作可以跳转到预期的程序执行地址处。

       程序状态寄存器:用于反应当前系统状态、控制内核工作模式以及发生的中断等。主要记录运算单元的标志。

       异常掩码寄存器:用于异常或中断屏蔽。

       CONTROL寄存器:主要用来切换操作模式和选择堆栈指针。选择栈空间指针寄存器,用于标识上下文中是否使用了浮点运算单元。

FAQ2. 内核的操作模式有哪些,模式何时变换,操作模式的引入带来什么好处?

      基于Cortex-M内核的处理器存在两种操作模式:线程模式和Handler模式,以及两种特权等级:特权级(privileged)和用户级(unprivileged)。

       系统内核可以在特权/非特权模式下运行,有一些特定的指令和操作只能在特权模式下运行。比如在非特权模式下,用户无法访问NVIC寄存器,而在Handler Mode下,内核总是处于特权模式;而在Thread Mode下,程序可以在特权/非特权模式下切换。只有当程序处于Handler Mode时,才能将Thread Mode中的非特权模式切换为特权模式。

       处理器默认运行在Thread Mode下;异常/中断发生时,硬件自动切换到Handler Mode模式(该模式总是特权级),执行完对应异常/中断处理程序后,再切换回thread mode模式(CONTROL的nPRIV(bit0),默认为0 ,线程模式处于特权级;特权级下将该位置为1,则线程模式处于用户级,用户级下不能做此同样修改进入特权级,必须经过异常handler,在其中进行修改,才能在返回线程模式后用于特权级)。

       两种特权级别是对存储器访问提供的一种保护机制。在特权级下,程序可以访问所有范围的存储器(MPU除外),能够执行所有指令;在用户级下,不能访问系统控制空间(SCS,包含配置寄存器及调试组件的寄存器),且禁止使用MSR访问特殊功能寄存器(APSR除外)。

       在上电复位后,处理器处于线程模式与特权级,可以通过修改控制寄存器,进入用户级。注意:在用户级,特殊功能寄存器无法访问(被忽略),这样能防止用户程序的误操作使得整个系统崩溃。在RTOS中的线程一般运行在用户级(多线程,可防止一个线程异常导致系统崩溃),而裸机一般在特权级(相当与单个线程用户级就没多大意义)。

注:多数RTOS实现过程中,两种操作模式均会仅使用特权级(硬件启动后的缺省级别),异常/中断运行于Handler Mode,任务运行于Thread Mode,这个过程由硬件自动切换。无论是异常/中断/任务,均运行于特权级下。这是因为OS代码本身就定位为非用户级别运行代码。

FAQ3. MSP和PSP分别在什么时候使用,为什么需要双堆栈指针?

MSP:复位后缺省使用的堆栈指针,用于操作系统内核以及异常处理例程(包括中断服务例程)

PSP:由用户的应用程序代码使用。

两个堆栈指针,同一时刻只能用一个。并且堆栈指针的最低两位永远是0,即堆栈总是4字节对齐。

双堆栈指针的作用:提升程序健壮性。一定程度上保证应用的数据(栈)空间不会溢出到操作系统数据(栈)空间;Cortex-M3出现“双堆栈”设计,允许将用户应用程序的堆栈与特权级/操作系统内核kernel的堆栈分开,从而阻止用户程序访问内核的堆栈,消除了内核数据被破坏的可能。避免出现应用程序恶意修改内核。

无操作系统:复位后使用MSP,异常或中断均使用MSP。

有操作系统:复位后使用MSP,任务开始运行时,osKernelStart->....->vPortStartFirstTask,将任务栈push到PSP,并切换到PSP。在发生异常或中断时,CPU进入Handler模式(CONTROL寄存器的bit[1]置为0,强制使用MSP)。OS或程序将CONTROL bit[1]设置为1,则进入线程模式,使用PSP。

FAQ4. LR寄存器有什么作用?

FAQ5. 复位序列,上电到执行main之前做了哪些事情?

1. 产生上电复位(nPoreset)信号(该复位信号不是中断,并不会进入某个中断)并进入复位状态,等待时钟信号稳定后退出复位状态。

2. 进行堆栈初始化。退出复位状态后,从地址0x00000000处(中断向量地址偏移量0)取出 MSP 的初始值,即初始化MSP;从地址 0x00000004 处(中断向量地址偏移量4)取出 PC 的初始值(复位向量),从该值对应地址处取值,进入复位中断。

3. 进入复位中断服务程序

裸机的复位中断服务程序:裸机程序中通常只是用一个sp指针(MSP)。下图是裸机的复位中断服务程序,直接对系统进行初始化后,跳转到main中去运行。

带操作系统的复位中断服务程序:一般来讲带操作系统的复位中断服务程序也是对系统进行初始化,然后跳转到main中去执行应用程序,与裸机不同的是带操作系统的复位中断复服务程序还会对操作系统内核的一些东西进行初始化。

4. 跳转到main应用程序

FAQ6. 中断的具体行为,什么是中断的悬起,中断优先级分组的意义,什么是中断咬尾?

中断悬起:进入临界区(中断屏蔽)后,如果有中断被触发,则对应的中断标志置位,但不执行中断服务程序。中断使能后,处理器检测到中断标志置位后,则执行对应的服务程序。

在ARM架构中,中断优先级分组(Priority Grouping)是用于管理和配置中断优先级的一种机制。它的意义在于灵活地分配中断优先级,支持中断嵌套,确保高优先级的中断能够及时响应,同时优化系统的中断处理效率,满足系统的实时性。

进入中断服务程序后,悬起状态便被清除,但是还未退出中断后,中断请求(不一定是同一个中断请求)再次到来,此时中断被再次悬起,且在服务程序退出后,立即又执行中断服务程序,此时内核会取消退出服务程序的出栈,以及进入服务程序的入栈,这种中断情况称为:中断咬尾、咬尾中断。

FAQ7.RTOS为什么被称为实时操作系统,实时体现在哪里?

实时操作系统(RTOS)是以实时性为前提进行设计的,高优先级任务一定会优先执行。实时操作系统的主要目标是创造一个可预见的、确定的环境。即所有任务从它被创建开始,就是可预见的,比如它必须在截止时间内返回结果。一个实时操作系统可以保证完成计算的最坏情况下的时间是预先已知的,并且完成计算的时间不会超过限制。所以可预见性和确定性是实时操作系统最突出的特点。而非实时操作系统则以保证系统性能为前提进行设计。

系统的实时性指的是在固定的时间内正确地对外部事件做响应。实时系统是一种需求倾向性的系统,或者说是一个等级系统,不同重要性的系统具有不同的优先级,重要的任务能够被优先响应执行,非重要的任务可以推后执行。

不同实时操作系统的刚性和柔性程度有所不同,系统硬度不同造就了硬实时(Hard Real-time)与软实时(Soft Real-time)的特性,其中,硬实时系统指的是严格限定在规定的时间内完成任务(不可改变的时间限制,不允许任何超出时限的错误),否则系统卡死崩溃,导致灾难性的后果,比如导弹拦截系统,汽车引擎系统等。软实时系统指的是允许偶尔出现一定的时间偏差(容忍偶然的超时错误),失败造成的后果并不严重。但随着时间推移,系统正确性也会降低。比如音乐播放系统,允许偶尔出现的声音延迟等问题。

硬实时与软实时之间最关键的差别在于,软实时只能提供统计意义上的实时。例如,有的应用要求系统在95%的情况下都会确保在规定的时间内完成某个动作,而不一定要求100%。

非实时操作系统指操作系统无法保证哪怕是最高优先级任务开始执行的最后时限。软实时操作系统指的是操作系统只能保证在xx时间内执行最高优先级的用户代码,但用户软件是否能及时完成操作,操作系统不管。

AUTOSAR 操作系统( Operating System , OS )属于系统服务层,它是一种多任务的实时操作系统( Real Time Operating System , RTOS ),并且是静态操作系统,即不可以在运行时动态创建任务。实时操作系统对系统的实时性要求较高,需要保证在特定时间内处理完相应的事件或数据。

FAQ8. RTOS的上下文切换原理是怎么样的?

       基于Cortex-M内核,堆栈是向下生长的,所以栈是从pxEndOfStack(任务的结束地址)往下存数据的。

1.保存当前任务的上下文:

      加载线程模式(Thread mode)下使用的psp,即获取到当前任务使用的堆栈指针所在的位置。进入PendSV异常处理程序时,硬件已经将栈针(r0,r1,r2,r3,r12,LR,PC以及xPSR)压入psp中了,手动保存r4-r11寄存器(再次保存r14即LR,是为了后续调用C函数返回使用,所以继续在psp的基础上进行压栈),如此便保存了当前任务的所有寄存器的值。然后将压栈后的psp值保存到当前任务的pxTopOfStack(任务栈指针的位置)中。

mrs r0, psp
ldr r3, =pxCurrentTCB
ldr r2, [r3]
stmdb r0!, {r4-r11, r14}
str r0, [r2]

2. 进行上下文切换:

(1)检索下一个要运行的任务并保存到pxCurrentTCB中。

      由于调用vTaskSwitchContext后需要使用到r0与r3寄存器,因此借用当前任务的堆栈先行保存r0与r3寄存器。

stmdb sp!, {r0, r3}
bl vTaskSwitchContext

   vTaskSwitchContext函数中,去掉统计和溢出检测代码,主要内容为:taskSELECT_HIGHEST_PRIORITY_TASK()与traceTASK_SWITCHED_IN()。其中taskSELECT_HIGHEST_PRIORITY_TASK()就是在pxReadyTasksLists中从任务就绪队列里面查找任务优先级最高的任务的TCB,然后赋值给pxCurrentTCB。

(2)任务切换

       首先将前面保存的r0和r3寄存器(r3为pxCurrentTCB地址)出栈,然后将该地址里面的*pxCurrentTCB的内容(前四字节)加载到r1中,即r1 = &(*pxTopOfStack)。然后再把*pxTopOfStack,即待运行任务的堆栈指针加载到r0中,再将待运行任务的上下文r4~r11和r14从栈顶指针r0出栈到系统对应的寄存器中,再将出栈后的堆栈指针pxTopOfStack(R0)保存到psp中。最后调用bx r14切换回线程模式(Thread mode),退出PendSV异常。

       另外,由caller保存的r0-r3,r12,LR,PC和xPSR也将由硬件出栈,硬件并不知道我们已经修改psp到新的任务中了,它就是从psp位置处依次将这些寄存器出栈,所以新任务堆栈中的r0-r3,r12,LR,PC和xPSR将出栈到这些寄存器中,最后任务切换成功。

ldmia sp!, {r0, r3}
ldr r1, [r3]
ldr r0, [r1]
ldmia r0!, {r4-r11, r14}
msr psp, r0
bx r14

       所以前面进入异常时硬件的压栈和stmdb r0!, {r4-r11, r14}压的是原任务的上下文;而ldmia r0!, {r4-r11, r14}的出栈和退出异常时出的是新任务的上下文。

FAQ9. RTOS的线程栈在上下文切换过程中的出入栈是怎么样的?

FAQ10. 函数调用过程中出入栈的情况,为什么C语言函数的参数入栈顺序是从右向左,为什么建议函数参数不多于4个?

       对于ARM体系来说,不同语言撰写的函数之间相互调用(mix calls)遵循的是 ATPCS(ARM-Thumb Procedure Call Standard),ATPCS主要是定义了函数调用时参数的传递规则以及如何从函数返回。

ATPCSARM-Thumb Procedure Call Standard

下载网址:https://www.cs.cornell.edu/courses/cs414/2001FA/armcallconvention.pdf

该调用标准规定了子程序间调用的基本规则,其中包括子程序调用过程中寄存器使用规则,数据栈使用规则,参数传递规则等,便于单独编译的C程序与汇编程序互相调用。使用ADS编译器(Keil)编译C程序满足用户指定的ATPCS类型,但使用汇编的话,需要开发者来保证各个子程序满足ATPCS要求。

1. 寄存器使用规则:

子程序间通过R0~R3(可记作a0~a3)传递参数,被调用子程序返回前无需恢复寄存器R0~R3的内容。

子程序中使用寄存器R4~R11(可记作v1~v8)保存局部变量。如果在子程序中使用了寄存器v1~v8中的某些寄存器,则子程序进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值。在Thumb程序中,通常只能使用寄存器R4~R7来保存局部变量。

R12(IP)过程调用中间临时寄存器,子程序间的连接代码段中常常有这种使用规则。

R13(SP)用作堆栈指针,寄存器SP在进入子程序时的值和退出子程序时的值必须相等

R14(LR)称为连接寄存器,记作LR。用于保存子程序的返回地址。

R15(PC)是程序计数器。

2. 堆栈使用规则:

ATPCS规定堆栈为FD(Full Descending: sp指向最后一个压入的值,数据栈由高地址向低地址生长)类型,即满递减堆栈(参数列表 从右往左 依次从高地址到低地址入栈),并且对堆栈的操作是8字节对齐。所以经常使用的指令就有STMFD和LDMFD。

对于汇编程序,如果目标文件中包含了外部调用,则必须满足下列条件:

       外部接口的堆栈必须是8字节对齐的。

       汇编程序中使用PRESERVE8伪指令,告诉连接器本汇编程序数据是8字节对齐的。

3. 参数传递规则:

(1)根据参数个数是否固定,分为参数个数固定的子程序和参数个数可变化的子程序。               

对于参数个数可变的子程序,当参数个数不超过4个时,可以使用寄存器R0~R3来传递参数;当参数超过4个时,使用堆栈来传递参数。(参数个数直接影响调用函数的速度,参数越多,调用函数越慢;参数个数少,程序显得精练、简洁,有助于检查和发现程序中的错误。)

在传递参数时,将所有参数看作是存放在连续的内存字单元的字数据。然后,依次将各字数据传递到寄存器R0,R1,R2和R3中。如果参数多于4个,则将剩余的字数据传递到堆栈中。入栈的顺序与参数传递顺序相反,即最后一个字数据先入栈。

(2)参数个数固定子程序参数传递规则

如果系统不包含浮点运算的硬件部件,浮点参数会通过相应的规则转换成整数参数(若没有浮点参数,此步省略),然后依次将各字数据传送到寄存器R0~R3中。如果参数多于4个,将剩余的字数据传送堆栈中,入栈的顺序与参数顺序相反,即最后一个字数据先入栈。

4. 子程序返回结果:

规则:

结果为一个32位整数时,通过寄存器R0返回;

结果为一个64位整数时,通过寄存器R0和R1返回;

结果为一个浮点数时,通过浮点运算部件的寄存器f0、d0或s0来返回;

结果为复合型浮点数(如复数)时,可以通过寄存器f0~fn或d0~dn来返回;

对于位数更多的结果,需要通过内存来传递。

注:AAPCSARM Archtecture Procedure Call Standard,或Procedure Call Standard for the ARM Architecture是ATPCS的改进版本。

下载网址:https://developer.arm.com/documentation/102374/0101/Procedure-Call-Standard

内容详解:https://www.cnblogs.com/Five100Miles/p/8458561.html

AAPCS 是 ARM ABI(Application Binary Interface) 接口文档的一份,定义了:

对寄存器使用的限制;使用栈的惯例;在函数调用之间传递/返回参数。可以被回溯的基于栈的结构的格式,用来提供从失败点到程序入口函数的列表,包括函数参数。

C/C++中规定了函数参数的压栈顺序是从右至左,函数调用协议会影响函数参数的入栈方式、栈内数据的清除方式、编译器函数名的修饰规则等。好像也有特殊情况的,压栈顺序从左至右。

引用实测例子:

 地址高低和操作系统的实现有关系,不同的操作系统中栈的生长方向可能不一样,所以不能只看地址来确定栈底还是栈顶。(不一定都是满递减堆栈,也不一定是操作系统的原因,应该与芯片硬件平台或者编译器有关-ck2024.11.14)

代码段:保存程序文本,指令指针EIP就是指向代码段,可读可执行不可写,如果发生写操作则会提示segmentation fault

数据段:保存初始化的全局变量和静态变量,可读可写不可执行

BSS:未初始化的全局变量和静态变量

堆(Heap):动态分配内存,向地址增大的方向增长,可读可写可执行

栈(Stack):存放局部变量,函数参数,当前状态,函数调用信息等,向地址减小的方向增长,可读可写可执行

函数调用栈:指将函数信息(活动记录或栈帧),按照调用顺序依次压入栈中,等最上层的函数执行结束后,就弹出相应栈帧,栈帧主要包括以下几个内容:函数的返回地址和参数、本地变量、调用前后上下文。

具体流程:

  • 函数参数入栈;
  • 将当前指令的下一条指令压栈(便于返回后继续执行),然后跳转至函数(需要调用函数)内执行;
  • 数据处理/逻辑运算,上下文保存等;
  • 被调用函数执行完,恢复相关寄存器数据;
  • 从栈帧中取到返回地址,并回到调用函数处下一条指令执行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ChenK21_idea

看后有收获,请赐些奶粉钱,谢谢

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值