Cortex-M3与Cortex-M4
1. 处理器输入、输出和外设访问
一般来说,外设在使用前需要初始化,一般包括以下几步:
- 如果需要,设置时钟控制回路使能连接到外设和对应引脚的时钟。许多现代微控制器允许对时钟信号分布的精细调节,如使能/禁止到每个外设的时钟连接以节省功耗。外设时钟一般是默认关闭的,需要在编程外设前使能时钟。有些情况下,可能还需要使能外设总线系统的时钟。
- 有些情况下,可能还需要配置I/O引脚的操作模式。大多数微控制器都有复用的I/O引脚,可用于多种目的。为了使用外设,配置
I/O 引脚以匹配用途是很有必要的(如输入/输出方向、功能等)。另外,可能还需要编程其他的配置寄存器,定义输出类型等预想的电气特性(电压、上拉/下拉和开漏等)。 - 外设配置。多数外设中包含多个可编程寄存器,它们需要在使用前进行配置。有些情况下,会发现配置流程比8位微控制器要稍微复杂一些,这是因为32位微控制器的外设一般要比8位/16位系统的外设要复杂得多。另外,微控制器供应商一般会提供设备驱动库代码,可以使用这些驱动函数以降低所需的编程工作量。
- 中断配置。若外设需要使用中断操作,则需要编程
Cortex-M3/M4 处理器的中断控制器(NVIC) ,使能中断和配置中断优先级。
微控制器供应商需要将下面的部件添加到存储器系统中;
- 程序存储器,一般是Flash。
- 数据存储器,一般是SRAM。
- 外设
2. Cortex-M3与Cortex-M4 OS支持和系统级特性
Cortex M3 和Cortex-M4 处理器在设计时就考虑了对嵌人式OS的高效支持。它们具有一个内置的系统节拍定时器SysTick ,可以为OS定时提供周期性定时中断。由于SysTick 定时器在所有的Cortex-M3 和Cortex-M4 设备中都存在,嵌人式OS的源代码可以很容易地就能用在所有的这些设备上,而无须为设备相关的定时器进行修改。
Cortex-M3 和Cortex-M4 具有两个栈指针: OS内核和中断用的主栈指针(MSP) 以及应用任务用的进程栈指针(PSP) 。这样,OS内核用的栈就和应用任务的栈分离开来了,可靠性得到提高的同时,栈空间的使用也得到了优化。没有OS的简单应用可以只使用MSP 。
为了进一步提高可靠性,Cortex-M3 和Cortex-M4 支持独立的特权和非特权操作模式,处理器在启动后默认处于特权模式。当使用OS且执行用户任务时,用户任务可以在非特权操作模式中执行,这样可以增强某些限制,如阻止对一些NVIC 寄存器的访问。特权和非特权操作模式也可以同MPU一道,防止非特权任务访问某些存储器区域。这样,用户任务就无法破坏OS内核以及其他任务的数据,因此,也就提高了系统的稳定性。
3. 寄存器
3.1 R13,栈指针(SP)
R13 为栈指针,可通过PUSH 和POP 操作实现栈存储的访问。物理上存在两个栈指针:主栈指针(MSP ,有些ARM文献也称其为SP_main )为默认的栈指针,在复位后或处理器处于处理模式时,其会被处理器选择使用。另外一个栈指针名为进程栈指针(PSP ,有些ARM文献也称其为SP_process ),其只能用于线程模式。栈指针的选择由特殊寄存器CONTROL 决定。
MSP和PSP都是32位的,不过栈指针(MSP或PSP)的最低两位总是为0,对这两位的写操作不起作用。对于ARMCortex-M处理器,PUSH和POP总是32位的,栈操作的地址也必须对齐到32位的字边界上。
大多情况下,若应用不需要嵌入式OS,PSP也没必要使用。许多简单的应用可以完全依赖于MSP,一般在用到嵌人式OS时才会使用PSP ,此时OS内核同应用任务的栈是相互独立的。PSP的初始值未定义,而MSP的初始值则需要在复位流程中从存储器的第一个字中取出。
同几乎所有的处理器架构一样,Cortex-M 处理器在运行时需要栈存储和栈指针(R13)。在栈这种存储器使用机制中,存储器的一-部分可被用作后进先出的数据存储缓冲。ARM处理器将系统主存储器用于栈空间操作,且使用PUSH指令往栈中存储数据以及POP指令从栈中提取数据。每次PUSH和POP操作后,当前使用的栈指针都会自动调整。
栈可用于:
- 当正在执行的函数需要使用寄存器(寄存器组中)进行数据处理时,临时存储数据的初始值。这些数据在函数结束时可以被恢复出来,以免调用函数的程序丢失数据
- 往函数或子程序中的信息传递。
- 用于存储局部变量。
- 在中断等异常产生时保存处理器状态和寄存器数值。
3.2 R14,链接寄存器(LR)
R14也被称作链接寄存器(LR),用于函数或子程序调用时返回地址的保存。在函数或子程序结束时,程序控制可以通过将LR的数值加载程序计数器(PC)中返回调用程序处并继续执行。当执行了函数或子程序调用后,LR的数值会自动更新。若某函数需要调用另外一个函数或子程序,则它需要首先将LR的数值保存在栈中,否则,当执行了函数调用后,LR的当前值会丟失。
在异常处理期间,LR也会被自动更新为特殊的EXC_RETURN (异常返回)数值,之后该数值会在异常处理结束时触发异常返回。
3.3 R15,程序计数器(PC)
R15为程序计数器(PC),是可读可写的,读操作返回当前指令地址加4(由于设计的流水线特性及同ARM7TDMI 处理器兼容的需要)。写PC(例如,使用数据传输/处理指令)会引起跳转操作。
由于指令必须要对齐到半字或字地址,PC的最低位(LSB) 为0。不过,在使用一-些跳转/读存储器指令更新PC时,需要将新PC值的LSB置1以表示Thumb状态,否则就会由于试图使用不支持的ARM指令(如ARM7TDMI中的32位ARM指令)而触发错误异常。对于高级编程语言(包括C和C++),编译器会自动将跳转目标的LSB置位。
多数情况下,跳转和调用由专门的指令实现,利用数据处理指令更新PC的情况较为少见。不过,在访问位于程序存储器中的字符数据时,PC的数值非常有用,因此,会经常发现存储器读操作将PC作为基地址寄存器,而地址偏移则由指令中的立即数生成。
CONTROL寄存器定义了:
- 栈指针的选择(主栈指针/进程栈指针)。
- 线程模式的访问等级(特权/非特权)。
另外,对于具有浮点单元的Cortex-M4处理器,CONTROL寄存器中有一位表示当前上下文(正在执行的代码)是否使用浮点单元。
4. C实现的异常处理
用于ARM架构的C编译器遵循ARM的一个名为AAPCS(ARM架构过程调用标准) 的规范。根据这份标准,C函数可以修改R0~ R3、R12、R14(LR)以及PSR 。若C函数需要使用R4~R11 ,就应该将这些寄存器保存到栈空间中,并且在函数结束前将它们恢复,如下图所示。
R0~R3、R12、LR以及PSR 被称作“调用者保存寄存器”,若在函数调用后还需要使用这些寄存器的数值,在进行调用前,调用子程序的程序代码需要将这些寄存器的内容保存到内存中(如栈)。函数调用后不需要使用的寄存器数值则不用保存。
R4~R11 为“被调用者保存寄存器”,被调用的子程序或函数需要确保这些寄存器在函数结束时不会发生变化(与进入函数时的数值一-样)。这些寄存器的数值可能会在函数执行过程中变化,不过需要在函数退出前将它们恢复为初始值。
一般来说,函数调用将R0~R3 作为输入参数,R0 则用作返回结果。若返回值为64位,则R1 也会用于返回结果如下图。
要使C函数可以用作异常处理,异常机制需要在异常入口处自动保存R0~R3、R12、LR以及PSR ,并在异常退出时将它们恢复,这些都要由处理器硬件控制。这样,当返回到被中断的程序时,所以寄存器的数值都会和进入中断时相同。另外,在普通的C函数调用不同,返回地址(PC)的数值并没有存储在LR中(异常机制在进入异常时将EXC_RETURN 代码放入了LR中,该数值将会在异常返回时用到),因此,异常流程也需要将返回地址保存。这样对于Contex-M3 或不具有浮点单元的Contex-M4 处理器,需要在异常处理期间保存的寄存器共有8个。
当异常发生时,硬件会自动将一部分寄存器压入栈中保存,如下图所示。
5. EXXC_RETURN
异常处理函数说到底也是C函数,它与普通函数有什么区别呢?
最大的区别就在于,当调用普通函数时,LR寄存器会保留它下一个运行时的地址,便于后面返回用。而进入异常处理函数前,硬件会自动将一部分寄存器压入栈中保存,异常处理函数的LR会被设置为一个特殊值,当异常处理函数返回时,发现LR是一个特殊值的话,就会触发硬件恢复之前硬件自动保存起来的寄存器。
处理器进入异常处理或中断服务程序(ISR) 时,链接寄存器(LR) 的数值会被更新为EXC_RETURN 数值。当利用BX 、POP 或存储器加载指令(LDR 或LDM )被加载到程序寄存器中时,该数值用于触发异常返回机制。
6. 影子栈指针
Cortex-M 处理器中存在两个栈指针:
- 主栈指针
(MSP) 为默认的栈指针。当CONTROL 的bit[1](SPSEL) 为0时用于线程模式,在处理模式中则总是使用。 - 进程栈指针
(PSP) ,当CONTROL的bit[1](SPSEL) 为1时用于线程模式。
PUSH和POP指令实现的栈操作及使用SP(R13) 的多数指令都会使用当前选择的栈指针,还可以利用MRS和MSR指令直接访问MSP和PSP。对于不具有嵌入式OS或RTOS的简单应用,可以在所有操作中只使用MSP,而不用管PSP。
对于具有嵌人式OS 或RTOS 的系统,异常处理(包括部分OS内核)使用MSP,而应用任务则使用PSP。每个应用任务都有自己的栈空间。OS中的上下文切换代码在每次上下文切换时都会更新PSP。
这种设计有几个优点:
- 若应用任务遇到会导致栈破坏的问题,OS内核使用的栈和其他任务的栈不会受到影响,因此可以提高系统的可靠性。
- 每个任务的栈空间只需满足栈的最大需求加上一级栈帧(对于Cortex-M3或无浮点单元的Cortex-M4,最大9个字,包括额外插人的字,或者对于具有浮点单元的Cortex-M4则最大为27个字),用于ISR和嵌套中断处理的栈空间会被分配在主栈中。
- 有助于创建
Cortex-M 处理器用的高效OS。 - OS还可以利用存储器保护单元(MPU)定义可以访问某个栈区域的应用任务。若某应用任务具有栈溢出的问题,MPU可以触发一次
MemManage 错误异常,并且避免该任务栈空间以外的存储器区域被覆盖。上电后,MSP 被初始化为向量表中的数值,这也是处理器复位流程的一部分。工具链添加的C启动代码也可以执行对主栈进行初始化的其他操作。之后还可以利用MSR指令初始化PSP,并且写入CONTROL 设置SPSEL ,不过一般不会这么做。
7. SVC异常
对于需要高可靠性的系统,应用任务可以运行在非特权访问等级,而且有些硬件资源可被设置为只支持特权访问(利用MPU),应用任务只能通过OS的服务访问这些受保护的硬件资源。按照这种方式,由于应用任务无法获得关键硬件的访问权限,嵌人式系统会更加健壮和安全。
对于C编程环境,需要将SVC处理分为两个部分:
- 第一部分提取栈帧的起始地址,并将其作为输入参数传递给第二部分。该处理要用汇编实现,这是因为需要检查LR的数值(EXC_RETURN),而其无法用C实现。
- 第二部分从栈帧中提取压栈的PC数值,然后从程序代码中得到SVC编号。它还可以选择提取出压栈的寄存器数值等其他信息。
8. PendSV异常
PendSV (可挂起的系统调用)异常对OS操作也非常重要,其异常编号为14且具有可编程的优先级。可以写人中断控制和状态寄存器(ICSR)设置挂起位以触发PendSV 异常。与SVC异常不同,它是不精确的。因此,它的挂起状态可在更高优先级异常处理内设置,且会在高优先级处理完成后执行。
利用该特性,若将PendSV 设置为最低的异常优先级,可以让PendSV 异常处理在所有其他中断处理任务完成后执行。这对于上下文切换非常有用,也是各种OS设计中的关键。
在OS代码中,任务调度器可以决定是否应该执行,上下文切换。假定OS内核的执行由SysTick 异常触发,每次它都会决定切换到一个不同的任务。
若中断请求(IRQ)在SysTick 异常前产生,则SysTick 异常可能会抢占IRQ处理。在这种情况下,OS不应执行上下文切换。否则,IRQ处理就会被延迟,如下图所示。对于Cortex-M3和Cortex-M4处理器,当存在活跃的异常服务时,设计默认不允许返回到线程模式。若存在活跃中断服务,且OS试图返回到线程模式,则使用错误异常会被触发。
在一些OS设计中,要解决这个问题,可以在运行中断服务时不执行上下文切换,此时可以检查栈帧中的压栈xPSR或NVIC中的中断活跃状态寄存器。不过,系统的性能可能会受到影响,特别是当中断源在SysTick 中断前后持续产生请求时,这样上下文切换可能就没有执行的机会了。
为了解决这个问题,PendSV 异常将上下文切换请求延迟到所有其他IRQ处理都已经完成后,此时需要将PendSV 设置为最低优先级。若OS需要执行上下文切换,它会设置PendSV 的挂起状态,并在PendSV 异常内执行上下文切换。下图所示为利用PendSV 进行上下文切换的一个实例,它具有以下事件流程:
- A任务调用SVC进行任务切换(例如,等待一些工作完成)。
- OS收到请求,准备进行上下文切换,且挂起
PendSV 异常。 - 当CPU退出SVC时,会立即进入
PendSV 且进行上下文切换。 - 当
PendSV 完成并返回线程等级时,OS会执行B任务。 - 中断产生且进入中断处理。
- 在运行中断处理程序时,
SysTick 异常(用于OS节拍)会产生。 - OS执行重要操作,然后挂起
PendSV 异常并准备进行上下文切换。 - 当
SysTick 异常退出时,会返回到中断服务程序。 - 当中断服务程序结束后,
PendSV 开始执行实际的上下文切换操作。 - 当
PendSV 完成后,程序返回到线程等级,这次它会回到任务A并继续执行。
除了OS环境中的上下文切换,PendSV 还可用于不存在OS的环境中。例如,中断服务程序可能需要一些处理时间,要处理的部分可能会需要高优先级,不过如若整个ISR都是在高优先级中执行的,其他的中断服务可能在很长时间内都无法执行。在这种情况下,可以将中.断服务处理划分为两个部分:
- 第一部分对时间要求比较高,需要快速执行,且优先级较高。它位于普通的ISR内,在ISR结束时,设置
PendSV 的挂起状态。 - 第二部分包括中断服务所需的剩余的处理工作,它位于
PendSV 处理内且具有较低的异常优先级。
|