Skip to content

Latest commit

 

History

History
3409 lines (2428 loc) · 169 KB

201020a_stm32.md

File metadata and controls

3409 lines (2428 loc) · 169 KB

ARM Cortex-M ISA研究,STM32开发环境搭建(基于GNU工具链)

参考资料

《ARM Cortex-M3与Cortex-M4权威指南(第3版)》,Joseph Yiu,2015.11,清华大学出版社

ARMv7-M Architecture Reference Manual, ARM

建议《权威指南》结合ARM官方文档看,一切以官方为准

0 空降

通用寄存器

中断向量表

NVIC

SCB

内存

汇编指令集

SysTick定时器

MPU

目录

  • 0 空降
  • 1 ARMv7-M体系结构
    • 1.1 简介
    • 1.2 ISA概览
      • 1.2.1 架构,系统总线与中断控制
      • 1.2.2 指令集ISA
      • 1.2.3 OS支持特性
      • 1.2.4 工作模式
    • 1.3 常用软硬件以及程序架构
    • 1.4 寄存器和内存架构
    • 1.5 NVIC:中断与异常处理
      • 1.5.1 中断/异常架构
      • 1.5.2 中断/异常处理流程
      • 1.5.3 异常返回
      • 1.5.4 向量表
      • 1.5.5 异常程序设计
      • 1.5.6 中断/异常优先级
      • 1.5.7 运行状态与挂起(Pending)行为
      • 1.5.8 NVIC相关寄存器
      • 1.5.9 SCB相关寄存器
      • 1.5.10 中断/异常调用过程详解
      • 1.5.11 处理器对中断/异常的自动优化
    • 1.6 ISA详解:指令集
      • 1.6.1 UAL
      • 1.6.2 寄存器传送指令
      • 1.6.3 存储器指令
      • 1.6.4 算术、饱和、逻辑、移位与数据转换
      • 1.6.5 位域处理指令
      • 1.6.6 比较、测试与程序流控制
      • 1.6.7 异常指令
      • 1.6.8 休眠指令
      • 1.6.9 存储器屏障
      • 1.6.10 杂项
      • 1.6.11 Cortex-M4:浮点指令,SIMD与乘法
    • 1.7 处理器休眠与低功耗应用
    • 1.8 SysTick定时器与系统控制
      • 1.8.1 SysTick定时器
      • 1.8.2 通过寄存器直接操作SysTick
      • 1.8.3 系统特性配置与控制CCR
      • 1.8.4 协处理器访问控制CPACR
      • 1.8.5 辅助控制寄存器ACTLR
    • 1.9 内存管理以及MPU
    • 1.10 浮点计算
    • 1.11 其他核心
    • 1.12 附录
    • 1.13 补充:错误异常与处理
    • 1.14 补充:DWT
  • 2 Linux环境下STM32开发环境搭建
    • 2.1 交叉工具链:crosstool-NG
      • 2.1.1 创建Docker环境
      • 2.1.2 下载与构建crosstool-NG
      • 2.1.3 构建安装工具链
      • 2.1.4 交叉工具链的一些说明
    • 2.2 软件库
    • 2.3 下载与调试工具
  • 3 GNU汇编
  • 4 ld链接器脚本
    • 4.1 示例
    • 4.2 基本概念
    • 4.3 命令格式
    • 4.4 注释
    • 4.5 变量和赋值
      • 4.5.1 符号变量和常数
      • 4.5.2 Location Counter
      • 4.5.3 Lazy evaluation
      • 4.5.4 赋值语句出现的位置
    • 4.6 算术函数
    • 4.7 MEMORY命令
    • 4.8 SECTIONS命令
    • 4.9 OVERLAY命令
    • 4.10 PHDRS命令
    • 4.11 其他命令
  • 5 OpenOCD
    • 5.1 配置
    • 5.2 OpenOCD服务器配置
    • 5.3 调试器配置
    • 5.4 复位配置
    • 5.5 TAP声明
    • 5.6 CPU配置
    • 5.7 烧录操作
    • 5.8 GDB使用
    • 5.9 FPGA相关操作
    • 5.10 常用命令
    • 5.11 JTAG命令
    • 5.12 边界扫描命令
    • 5.13 其他功能命令
    • 5.14 ARM相关命令
  • 6 GDB终端模式使用
    • 6.1 断点
    • 6.2 程序运行控制
    • 6.3 内存读写

1 ARMv7-M体系结构

1.1 简介

目前在低功耗微控制器中最主流的32位ARM核心有Cortex-M3以及Cortex-M4。这两种核心都使用3级流水线哈佛总线结构,以及统一的内存地址空间,总共可以寻址4GB。Cortex-M系列核心适用于对成本、功耗敏感以及实时性要求较高的场合

Cortex-M核心内建了中断控制器系统,称为NVIC(Nested Vectored Interrupt Controller),中文翻译为嵌套向量中断控制器,是属于核心的一部分。它支持最多240个中断请求以及8到256个中断优先级(具体要看实际的核心配置,一般是8到16级优先级居多)

Cortex-M支持在一个MCU系统中集成多个处理器核心

另外Cortex-M系列核心还支持可选的MPU,用于提高运行时内存的安全性(在Cortex-M3、M4中如果没有特殊需求一般MPU用处不是很大,在Cortex-M7中由于Cache的存在所以需要使用到MPU)

1.2 ISA概览

由于MCU存储资源有限,为压缩程序大小,同时简化处理器设计、减少硅晶片面积占用并降低功耗,ARMv7-M的CPU只支持Thumb-2,不支持32位长度的ARM指令集。该指令集是对旧有Thumb指令集的扩展,是16位(半字)或32位(单字)可变长度的。最常用的指令很多都使用16位编码以最大限度压缩代码

Cortex-M3特性如下

  • 支持硬件除法
  • 支持字节(B)、半字(H)、单字(W)、双字(D)存储器访问与数据操作,可变的大小端模式
  • 支持累乘加MAC以及饱和指令
  • 支持多种跳转以及函数调用指令

Cortex-M4相比Cortex-M3多出了一些适用于DSP的指令,比如浮点指令和SIMD指令等

1.2.1 架构,系统总线与中断控制

架构

ARMv7-M的处理器为典型的32位机,总共可以访问4GB的内存空间。不同于8051等早期单片机,ARM使用统一的内存架构,包括SRAM,程序Flash,部分系统控制寄存器,以及各种外设寄存器都位于相同的内存空间,拥有唯一的地址。这些使用ARM Cortex-M核心的MCU既可以在Flash中运行程序,也可以将程序放到SRAM中运行

总线

由于采用哈佛结构设计,M3和M4中取指令和存取数据可以同时执行,当然前提是指令和数据不使用同一条总线

一般在一个MCU中会有多片SRAM以及Flash,尽管它们使用统一的内存编址,这些存储器都拥有物理上独立的总线。CPU核心以及DMA等通过总线交换矩阵和它们连接,由此可以同时灵活访问不同的存储器。这种情况下,如果将指令和数据放在同一片SRAM中将会导致总线冲突,降低CPU流水线执行效率,建议程序和运存不要使用同一片存储器

以下是STM32F4中典型的总线结构,其中圆圈表示该交叉节点有开关连通

基于Cortex-M的MCU内部都使用了AMBA总线,由ARM设计。MCU中常见的有两种总线类型:AHB Lite以及APB。AHB总线是系统高速总线(矩阵),运行频率高,一般用于连接CPU,SRAM,Flash,DMA,APB桥,DMA-USB等。而APB用于一些低速IO外设,如I2C,SPI,USART,CAN,ADC,DAC等,APB总线通过APB桥挂接到AHB总线。有些MCU中的APB也分为低速APB和高速APB。通常基于Cortex-M7的MCU会使用到更高级的AXI总线

Cortex-M0以及Cortex-M0+属于ARMv6-M,使用传统的冯诺依曼结构(普林斯顿结构)设计。这两种核心功耗较低,门电路数量少

中断

对于所有Cortex-M核心,NVIC的寄存器地址都是固定且相同的,并且也是使用和SRAM、Flash以及外设IO相同的地址空间。NVIC的编程模型都是相同的。NVIC支持外设中断NMI不可屏蔽中断以及处理器内部异常等多种异常和中断,总共最多支持240个外部中断,除NMI以外的中断或异常可以单独使能或禁止。NVIC还支持中断的屏蔽功能。Cortex-M3以及M4的中断优先级可以在运行时修改,Cortex-M0和M0+不支持该功能。中断向量取出由CPU硬件自动实现,无需软件判断中断源,同时中断向量表可以重定位映射到不同的地址(默认位于地址0x00000000

1.2.2 指令集ISA

所有的Cortex-M核心(包括ARMv6-M的Cortex-M0以及M0+)都只支持Thumb-2的不同子集,ARMv7-M的处理器兼容ARMv6-M所有的指令。这些处理器也舍弃了部分在旧有Thumb中的指令,如协处理器指令,指令集状态切换指令等

绝大多数使用Cortex-M4核心的MCU都为核心配备了FPU扩展(Cortex-M4F),支持额外的FPU指令(以及整数SIMD扩展)。而Cortex-M3没有FPU的支持

1.2.3 OS支持特性

Cortex-M3和M4带有SysTick节拍定时器,同时提供了两个栈指针MSPPSP,可以分别用于OS和用户进程。在一般的应用中如果不使用OS特性,一般只使用MSP

另外,这两种CPU核心还支持特权以及非特权模式。在一般的应用中默认使用的是特权模式。非特权模式一般要在RTOS中的用户程序中才会涉及到,这种模式会限制用户对于一些模块如NVIC的访问

1.2.4 工作模式

Cortex-M3和M4处理器有2种运行状态,分别为Thumb状态以及调试状态。其中Thumb状态分为2种操作模式,执行中断或异常处理程序时为处理模式,正常执行程序时为线程模式。另外Cortex-M3和M4核心除一般的特权模式以外还支持非特权模式(Cortex-M0不支持)

状态转换示意图如下,一共可以分为3种主要的工作模式

其中特权模式到非特权模式的转换是单向的,非特权模式想要到达特权模式只能通过异常来处理。执行中断或异常时称为处理模式,这种处理模式特权模式类似。非特权模式可以保护NVIC的寄存器,防止用户程序篡改

1.3 常用软硬件以及程序架构

CMSISCortex-M Software Interface Standard,由ARM提出并开发)包含了各种基本的库与头文件,以及针对各种Cortex-M核心的专有功能与对应函数(比如DSP函数库)

开发环境方面,以前最流行的是Keil MDK和IAR。然而现在各路MCU厂商都开始推广自家的开发环境,典型的有ST的STM32CubeIDE(基于Eclipse),TI的CCS(基于Eclipse),Microchip的Atmel Studio(基于Visual Studio)等。以前的Keil使用ARMCC编译器(现在MDK最新版已经转向基于LLVM的定制版工具链),现在越来越多的厂商开始使用GNU的工具链。这也是总趋势

在运行调试方面,一般的MCU都支持JTAGSWD两种调试接口。目前常用的调试硬件有JLINK,ULINK以及开源的CMSIS-DAP(Debug Access Port)。而调试上位机除了各IDE集成的以外,目前在开源平台最流行的就是OpenOCD

如果想要在Linux平台开发ARM单片机,那么最少需要GNU的ARM工具链(arm-none-eabi-gcc),OpenOCD,Make等,再使用一个小核心板烧录CMSIS-DAP固件作为调试器。可以自己使用crosstool-ng构建适用于Cortex-M核心的GNU工具链

在Windows平台可以安装MSYS2,使用MINGW提供的GNU工具链

whycan论坛有关CMSIS-DAP优化的讨论

blackmagic,一个开源的无需上位机的调试器,可以直接使用GDB连接

1.3.1 编译流程

编译单片机程序时,一般首先需要提供一个.s启动文件。这个启动文件使用汇编语言编写,类似于Bootloader,会在单片机运行于默认配置下时对程序映像进行初始化(例如数据区的初始化等)

如上图,编译一个单片机程序,需要将所有的.c源代码以及.s汇编代码全部编译为.o可重定位二进制文件,最终通过ld链接器链接成为可执行映像。其中ld链接过程需要用到的链接器脚本厂商会随开发环境发布。实际的应用中建议直接使用arm-none-eabi-gcc命令自动调用工具链

1.3.2 程序设计

轮询

轮询是最傻瓜式的程序结构,基本原理就是将整个重复的工作流程编写成为一个大循环,两次循环之间一般会加入软延时(一般是delay()函数)。这种程序经过恰当的设计可以实现功能,但是缺点较多:代码难以维护,循环间隔时间难以控制,能耗比差,CPU利用率较差,反应迟钝等。只适合在初学者在要求不严格的场合使用。最典型的程序就是基于软延时的Blink

中断

实际的应用中建议使用定时器中断驱动结合状态机的程序设计模式。这种工作模式下,定时任务不依赖CPU的软延时,而是依赖定时器产生的中断。而无法预测的突发事件同样通过中断服务程序处理。绝大部分成熟的单片机程序都依赖于中断系统,在具有优先级管理的单片机中,更高级别的中断可以打断低级别的中断程序

这种工作模式下,定时任务会有精准可计算的时间间隔,突发事件也可以得到及时的响应。同时CPU在无任务执行时可以进入休眠或最低功耗模式,节省能源。各类操作系统一般也是建立于定时器中断技术之上的(SysTick节拍定时器)

RTOS实时操作系统

随着MCU程序的不断复杂化,有时会遇到更加复杂的控制需求,比如多个任务同时执行的需求,这种情况下使用中断程序已经难以满足要求,就需要用到时间片轮转等方式,需要一个调度器

这种情况下可以使用线程调度器或RTOS,这些RTOS一般还有信号量,队列以及消息等特性。但是使用RTOS会占用更多的资源,这就要结合实际进行权衡。目前已经有上百种开源RTOS,这些RTOS也是学习操作系统原理的良好选择,相比传统庞大的系统如Linux等更易于上手,同时RTOS一般还有相对更高的实时响应性能与执行效率,更加适合工控等领域

1.4 寄存器和内存架构

1.4.1 通用寄存器组GPR

定义如下

其中,R0R12可以分为高低两组,低组8个为R0R7,高组5个为R8R12。限于Thumb指令的长度要求,绝大多数16位长度的指令都只能使用低8个寄存器。而高5个寄存器只有32位指令以及少数16位指令可用

R13寄存器又称为SP,作为栈指针使用,一般不做其他用途。栈是RAM中的一片空间,用于放置局部变量,以及在函数或子程序调用、中断处理时保存寄存器现场。在Cortex-M3和M4中两个物理上的寄存器分别为MSPPSP。在运行过程中,当前使用到的寄存器是MSP还是PSP由特殊寄存器CONTROL的设置决定,一般只有RTOS才会涉及到PSP的使用。在ARM中栈是从高地址向低地址生长的,同时栈访问必须是4字节对齐的,所以事实上SP的低2位没有作用,总是00。另外,复位后MSP的初始值是从中断向量的最开头4字节取出的

R14寄存器又称为LR,作为链接寄存器使用。在程序中如果发生了函数或子程序调用,LR寄存器会自动保存该函数返回时的地址。在函数与子程序运行结束以后,LR中的地址就会被加载到PC中,CPU就可以返回到原来的地方继续执行。然而在中断程序中情况不是这样,LR会被更新为特殊值EXC_RETURN,这会在之后的中断中讲到。此外,在处理子程序调用时同样不要忘记将LR压栈,否则当前代码返回的地址就丢失了。因为Cortex-M的Thumb-2指令集是2字节或4字节可变长的,所以指令的访问是2字节对齐的。然而LR的最低位事实上是有重要作用的,部分调用指令需要LR最低位置1表示使用Thumb状态这是Cortex-M在舍弃掉ARM指令后的遗留问题

R15寄存器又称为PC,作为程序计数器使用。由于ARM处理器的流水线结构,读取PC获得的地址是当前正在执行指令的地址加4(两条2字节指令长度)。使用一般寄存器指令写PC寄存器会触发跳转。和LR寄存器类似的,使用部分跳转指令或寄存器指令操作PC时,需要将最低位置1表示Thumb。一般情况下函数调用以及跳转会使用子程序调用指令实现,而不是使用一般寄存器指令写PC。但是在访问连续的常量字符数据时可以将PC作为基址寄存器使用

1.4.2 特殊寄存器

定义如下

特殊寄存器只能通过MRS以及MSR指令进行访问,在通用寄存器以及特殊寄存器之间传输

PSR寄存器

PSR寄存器是程序状态寄存器,物理上一共有3个PSR寄存器,分别为APSR应用PSR,IPSR中断PSR和EPSR执行PSR。这3个PSR在汇编中可以通过符号PSR访问,另外也可以使用符号APSRIPSR访问指定的PSR(符号EPSR不能访问,符号IPSR只读)。这3个寄存器的定义如下

各位的作用如下

其中N位代指Negative,如果一次整数运算导致寄存器最高位为1那么该位置1。可以用于判断结果正负

Z位代指Zero,如果一次计算导致寄存器所有位为0那么该位置1。类似CMP指令同样可以将该位置1(相当于没有输出寄存器的减法)

C位代指Carry,指示无符号运算进位。在加法中如果在最高位产生进位那么该位置1,减法看作补码加法处理。可以用于计算64位整数(C中为long long)

V位代指Overflow,指示有符号运算溢出。所谓溢出就是如加法中正正得负或负负得正的情况,看最高位。加法(减法看作补码加)中设操作数1符号S1,操作数2符号S2,输出结果符号S3,那么$ V = (\overline{S_1}\overline{S_2}S_3)+(S_1S_2\overline{S_3}) $

Q位用于饱和运算指令中表示发生了饱和。饱和运算一般用在DSP算法中,如果两个数相加会导致溢出那么就将结果设为可表示的最大或最小值。该位一旦置位需要软件清零,不会自动清零,且和条件指令BIT无关

此外,GE用于Cortex-M4的整数SIMD指令。而T是舍弃ARM指令集以后的遗留,置1表示运行在Thumb模式下

IT位共计8位,用于If-Then的执行以及可中断的LDMSTM指令。用于IT指令时称为ITSTATE

ICIIT共用,在LDMSTMPOPPUSH多寄存器操作被打断时有用,用于存储这些操作被打断之后的下一个寄存器,这样可以恢复到原先的上下文。ICIIT指令的上下文中不起作用,这些指令在IT上下文被打断后只能重新执行

Cortex-M的PSR寄存器定义和其他ARM处理器(ARM7,ARM9,ARM11,Cortex-A等)有所不同,具体对比如下

PRIMASKFAULTMASK以及BASEPRI寄存器

在ARM中,中断优先级数字越小优先级越高。这3个寄存器用于控制中断的屏蔽,作用和Cortex-A中的I(IRQ)和F(FIQ)位类似,只能在特权模式下访问,可以使用MRS MSRCPS指令更改,如下

PRIMASK只有最低一位有效,置位时将当前执行优先级设为0,会屏蔽除NMI(不可屏蔽中断)以及HardFault以外所有的异常和中断。异常返回以后不会自动清零(线程模式默认运行于最低优先级+1)

FAULTMASK同样只有最低一位有效,置位时临时将当前优先级设为-1,会屏蔽除NMI以外的所有异常和中断,一般用于HardFault错误处理流程中,防止发生新的错误造成灾难性后果。异常返回后会自动清零

BASEPRI的有效位位于该寄存器最后8位,长度需要看具体的处理器配置,依据优先级屏蔽中断以及异常。一般的Cortex-M3和M4都是支持8到16级优先级,所以有效位一般为3到4位。将BASEPRI设为0时屏蔽不起作用,设为非0时会屏蔽低于或等于(优先级值大于等于)设定优先级的中断

另外,部分ARMv6-M核心没有FAULTMASK以及BASEPRI寄存器

CONTROL寄存器

该寄存器用于控制线程模式下的特权等级,使用的栈指针,以及指示目前是否在使用FPU,定义如下。在更改CONTROL寄存器以后应该执行一下ISB同步屏障指令使得此次更改对接下来的指令生效

nPRIV只在线程模式下有效,默认置0处于特权模式,置1处于非特权模式,并且置1以后无法再访问CONTROL寄存器(只能通过发起异常返回到特权模式更改,异常处理状态下可以任意更改,该异常处理程序属于操作系统实现的一部分)

SPSEL用于选择使用MSP还是PSP默认置0使用MSP,在线程模式下可以置1使用PSP。在处理模式下永远为0且不可更改

FPCA只在带FPU的Cortex-M4核心中才有,如果是1表示当前正在使用FPU,在执行FPU指令以后FPCA会自动置位。这时在调用函数时需要记得压栈保存FPU寄存器内容。而在异常入口处FPCA会自动清0,同时将FPU寄存器数据压栈。注意,如果在浮点计算上下文中FPCA被意外清零,那么接下来一旦发生异常或中断就会导致错误(未将FPU数据压栈)

一般的应用场景参考如下

解释

之前说过有3种执行模式,分别为线程模式的非特权模式线程模式的特权模式以及处理模式

线程模式的特权模式可以有两种情况,使用MSP和使用PSP。前者00用于OS内核的运行,后者01用于特权程序的运行

线程模式的非特权模式一般只会使用PSP(虽然也可以使用MSP,但是对于大部分OS来说不会这么做)。对应11(一般不会使用10),用于非特权程序的运行。

处理模式只能使用MSP。在该模式下nPRIV位没有影响,可以任意更改(处理模式下程序本身永远处于特权模式,它有一个重要作用就是负责判断非特权模式和特权模式之间的切换),对应00或10用于绝大多数的异常以及中断程序

总之,在没有使用到OS时,无需改动CONTROL,使用默认的00(特权模式,MSP)就行。这也是绝大多数一般MCU程序的工作模式。很多简单的OS甚至只运行在特权模式下,不会使用到非特权模式,只是充当简单的调度器使用

1.4.3 FPU寄存器

FPU是Cortex-M4的可选部件

S0S31数据寄存器

S0S31寄存器单个长度为32位,可以存放一个单精度浮点数;其中两两还可通过D0D15访问,单个长度为64位,可以存放一个双精度浮点数,但是Cortex-M4的FPU不支持双精度运算

FPSCR状态以及控制寄存器

位域定义如下

这些状态位以及控制位的作用可以参考IEEE754中相关内容,包括舍入,NaN等。IDCInput DenormalIXCInexactUFCUnderflowOFCOverflowDZCDivide by zeroIOCInvalid Operation

除了内置的FPSCR特殊寄存器,还有位于内存空间的CPACR寄存器,用于使能浮点单元(默认关闭)

1.4.4 内存

Cortex-M支持Big-Endian和Little-Endian两种模式。一般MCU厂商默认使用小端模式,片内外设也使用小端模式

前面说过Cortex-M为统一内存设计,只有一个内存地址空间。这些内存空间事实上被ARM划分成为几个区块,这些区块有固定功能。SRAM和程序代码(Flash)通常使用物理上独立的总线,可以同时访问。同时ARMv7-M的处理器部分指令支持非对齐访问,并且32位指令也可以对齐单字(4字节)或半字(2字节)(但是非对齐访问会降低访存效率,不到特殊情况不要使用非对齐)

LDMSTM多寄存器存取指令,以及PUSHPOP栈操作指令,排他访问指令LDREXSTREX都不支持非对齐传输

起始地址 终止地址 主要作用
0x00000000 0x1FFFFFFF 大小512MB,一般将Flash映射到这里,用于存储程序代码以及常数数据、字符数据、查找表等。中断向量表默认也是映射到这片区域的开头
0x20000000 0x3FFFFFFF 大小512MB,一般将内置SRAM映射到这里。位段区域是可选特性,有点类似8051的位寻址区域,大小1MB,位段别名是该片区域的影子,大小32MB(1MB*32位),一般通过对位段别名的访问实现位段区域的单bit修改,读写时有效数据都位于32bit单字的最低1bit(LSB)。位段区域还可以用于程序中的状态变量
0x40000000 0x5FFFFFFF 大小512MB,一般将内置外设如SPI,I2C,USB控制模块等映射到这里,位寻址同上,不可在此执行指令
0x60000000 0x9FFFFFFF 大小为两个512MB,一般用于片外RAM
0xA0000000 0xDFFFFFFF 大小为两个512MB,一般用于片外设备
0xE0000000 0xFFFFFFFF 大小512MB,最重要的区域。SCS映射到这片区域,0xE000E0000xE000EFFF需要重点关注,其中包含了SCB,FPU,MPU,NVIC,SysTick等关键模块,大小4kB,不可在此执行指令,且这些模块除NVIC可设置允许用户程序访问,其他所有模块在非特权线程模式下都无法访问,会引发总线错误

1.4.5 栈

前面说过寄存器R13也被称为SP堆栈指针。堆栈使用PUSHPOP分别进行压栈和出栈操作

栈一般有以下几个功能:

在中断处理或函数、子程序调用时,保存寄存器现场

存储局部变量

向函数或子程序传参

ARM中的SP使用满递减方式(栈从高地址向低地址生长)工作。PUSH时,SP先减小,后将要压栈的寄存器内容存储到当前SP所指地址(32位就是先减4后压栈,相当于存到栈空间最后4个字节。SP初始值应该为栈空间最高地址+1,最低2位永远为0)。POP时相反,先输出数据到寄存器后SP才增大

1.5 NVIC:中断与异常处理

1.5.1 中断/异常架构

NVIC可以接受外设中断IRQ,NMI,节拍定时器SysTick(用于操作系统节拍或中断运行方式),以及处理器抛出的异常作为输入源。优先级以及编号如下,最多可以配置核心支持255个异常和中断源,其中外设IRQ中断最多可以240个

NVIC包含了SCB系统控制模块,其中的VTOR用于中断向量表的重定位

NVIC接受高电平作为中断触发,可以是脉冲(自动维持高电平)或电平触发(需要中断源维持高电平)。在中断、异常到来时NVIC会将其优先级和当前优先级对比,若高于(优先级值小于)当前优先级那么会转入该中断处理程序。Cortex-M核心使用硬件取出中断向量

硬件错误HardFault和总线错误、内存管理错误以及使用错误(UsageFault)相关。后三者默认是屏蔽的,就会触发HardFault

1.5.2 中断/异常处理流程

  1. 寄存器自动压栈,压入MSPPSP取决于当前使用的栈

  2. 取异常向量并取指(PC不是真正的程序计数器,只是一个操控接口,真正的程序计数器对用户不可见)。有些处理器中会和压栈并行执行

  3. 更新PSRPCLRSP以及NVIC中的寄存器。异常处理模式下永远使用MSP且为特权模式(可以在该模式下进行特权模式切换)。其中PC被更新为异常处理程序起始地址,LR被更新为EXC_RETURN,这个特殊值用于中断程序的返回。EXC_RETURN[31:5]为1,而剩下5位EXC_RETURN[4:0]用于存储一些信息,例如中断前使用了MSP还是PSP

EXC_RETURN本身可以看作是一个地址,这个特殊的机制使得异常处理可以像子程序调用与返回一样处理。EXC_RETURN的值对应的地址空间是不可执行的

1.5.3 异常返回

异常返回通过将EXC_RETURN写入PC触发,可以通过以下3种方法

  1. 如果此时EXC_RETURN还在LR,那么可以使用BX LR(见程序流控制

  2. 也可以在异常处理中将LR压栈,这样只要通过POP PC也可以实现异常返回

  3. 也可以在异常处理中将LR放到内存,这样通过LDRLDM也可以实现返回

1.5.4 向量表

中断向量表见下,其定义一般包含在MCU厂商提供的SDK中,位于.s启动汇编文件中(编译时将其置于二进制文件的开头)。如下图,地址自下向上递增,一个元素长4字节,存储中断程序的地址(最低位永远为1表示Thumb模式)。可以看到0x00存储了SP初始值(因此中断源数量最多为255而不是256)

Cortex-M支持3种复位方式,分别是上电复位(复位所有部分),系统复位(除调试部件以外)以及处理器复位(外设以及调试部件不复位)。处理器复位后会首先读取中断向量表的头两个字,分别赋值给SPPC

以下情况可以通过VTOR进行向量表重映射

  1. 在使用到Bootloader的应用中,一般存在两个中断向量表,一个是Bootloader的,一个是用户程序的。在Bootloader执行完毕进入到用户程序时,需要切换到用户程序的向量表

  2. 在程序需要被加载到RAM中执行的情况下。这种情况一般也会使用到一个类似Bootloader功能的程序,会切换向量表

  3. 需要动态修改中断向量的情况

1.5.5 异常程序设计

在一般的应用中建议使用CMSIS提供的中断访问库函数以提高可移植性。CMSIS提供了使能以及禁用总中断,外部中断,以及设置优先级等功能的库函数

复位后Cortex-M默认禁用所有中断,通常情况下使能一个中断需要以下步骤

  1. 设置优先级分组(可选),如果向量表需要重定位那么需要再设置VTOR

  2. 设置优先级

  3. 使能外设的中断触发信号

  4. 在NVIC中手动清除挂起标志位使能中断

1.5.6 中断/异常优先级

之前在特殊寄存器提到过BASEPRI优先级屏蔽寄存器使用了最低一个字节的高3位(8级优先级)或4位(16级优先级)表示优先级

中断的优先级配置寄存器的设定同理,如下图

该寄存器灰色部分无效,优先级可以设定为0x00到0xE0,使用高位是为了方便软件的移植。比较形象的对比如下,其中复位、NMI和HardFault的优先级是固定的无法更改

优先级支持分组,可以将优先级寄存器分为分组优先级子优先级两个部分。此时的优先级数量等于分组优先级数量,而子优先级只有在两个中断位于相同的优先级分组以内才会起作用,此时拥有更高子优先级的中断可以抢占低子优先级的中断

如下示例,优先级分组可以设置07。在全部8位都有效的中断配置中,设置为0时具有128级可配置优先级,设置为7时所有中断位于同一优先级组,互相之间无法中断。若两个中断在同一个时刻发生且中断优先级完全相同,那么中断编号更小的中断优先(注意这不是抢占,同优先级的异常无法互相抢占)

线程模式也可以看作是有优先级的,其优先级为全系统最低。例如一个CPU支持16级优先级(0~15),那么线程模式优先级为16

1.5.7 运行状态与挂起(Pending)行为

之前说过NVIC可以接受外设发送来的正脉冲高电平触发中断。以前一些更老版本的ARM处理器仅仅支持高电平触发中断,高电平一旦撤销中断就不会再触发。ARMv7-M的NVIC实现了中断状态存储功能,只要输入的脉冲大于1个时钟就会触发,这样就支持了脉冲触发

一个中断程序有多个属性:禁止或使能(指在NVIC中),挂起或非挂起,活跃或非活跃。其中中断的使能以及挂起状态可以读写,而活跃状态指示位是只读的

这里必须首先引入一个概念:挂起状态用于表示中断是否需要处理。NVIC在确认有中断触发之后,中断程序首先进入的是挂起状态。中断处理的时序如下

在中断没有被屏蔽且正常触发时,首先触发挂起状态(Pending置1)。如果此时没有更高优先级的代码在执行,那么处理器会自动压栈并直接进入到中断的处理,此时中断活跃状态置1。如果有更高优先级在执行那么就会保持挂起状态。挂起状态在中断被处理时自动清0(也可以提前软件清0)。之后外设信号看情况,如果信号不会自动撤销那么还需要将外设发来的信号关闭

在中断嵌套的情况下,被抢占后原先的中断活跃状态不变,保持为1。此时会有多个处于活跃状态的中断

如果中断被NVIC禁止,那么其挂起标志位仍然会正常触发并维持。使能中断以后会照常进入中断处理程序并自动将挂起状态清0

如果中断输入源是脉冲,且在处理程序开始之前触发了多次(即活跃状态置位之前),那么这些脉冲算作1次触发

1.5.8 NVIC相关寄存器

NVIC中的寄存器可以通过CMSIS中的NVIC结构指针访问,用于配置与操作外部中断,例如NVIC->ISER[0]。下表使用在C语言中的表示方法

寄存器 作用
ISER[n] 一个长度32位,向相应bit写1使能外部中断,n取决于外部中断输入数。例如只有32个外部中断输入,那么n只能取0。读取值为当前使能状态
ICER[n] 一个长度32位,向相应bit写1禁止外部中断,n同上
ISPR[n] 一个长度32位,向相应bit写1将外部中断置为挂起状态,n同上,可以用于生成软件中断。读取值为当前挂起状态
ICPR[n] 一个长度32位,向相应bit写1将外部中断置为非挂起状态,n同上,可以用于取消一个中断
IABR[n] 一个长度32位,只读存储器,指示各中断服务程序的活跃状态,在服务代码开始执行时自动置1,退出时自动清0,n同上。如果发生了中断嵌套,那么之前的活跃标志位依然会保持1
IP[n] 一个长度8位,优先级寄存器,一个n对应一个中断的优先级。所以一般IP寄存器的数量最多为ISER的32倍。这些寄存器也建议32位对齐访问
STIR 8位,软件触发寄存器,向STIR写入数字可以挂起相应的外部中断,例如写4就可以触发外部中断#4。如果想要让非特权程序访问该寄存器,需要使能SCB->CCR寄存器中的USERSETMPEND

建议通过STIR触发软件中断,使用CMSIS中的NVIC_SetPendingIRQ()函数

另外支持的中断数量也可以通过SCnSCB->ICTR得到,0表示实现的中断数量在1~32之间。依此类推

1.5.9 SCB相关寄存器

SCB主要和系统的基本配置相关,另外也控制了除外部中断以外的系统异常

NVIC类似,SCB也是通过结构体指针访问

部分寄存器定义另见低功耗SCR寄存器),系统控制CCRACRLRCPACR寄存器),附录CPUID寄存器)和错误寄存器

寄存器 作用
CPUID 只读,检验使用CPU的类型以及版本
ICSR 系统异常控制与状态
VTOR 中断向量表重定位地址
AIRCR 优先级分组以及复位控制
SCR 休眠与低功耗配置
CCR 高级特性
SHP[0]~SHP[11] 系统异常优先级配置
SHCSR 异常使能以及状态控制
CFSR 错误异常的提示信息
HFSR HardFault事件提示信息
DFSR 调试事件提示信息
MMFAR 存储管理错误的地址
BFAR 总线错误地址
AFSR 设备错误状态信息
PFR[0]~PFR[1] 只读,处理器可用特性
DFR 只读,可用调试特性
AFR 只读,可用辅助特性
MMFR[0]~MMFR[3] 只读,可用存储器特性
ISAR[0]~ISAR[4] 只读,指令集特性
CPACR Cortex-M4中的浮点使能

ICSR寄存器

长度32位,定义如下

位域 名称 作用
31 NMIPENDSET 写1挂起NMI异常,读取获得NMI挂起/活跃状态
28 PENDSVSET 写1挂起PendSV异常,读取获得PendSV挂起状态
27 PENDSVCLR 写1清除PendSV异常
26 PENDSTSET 写1挂起SysTick异常,读取获得SysTick挂起状态
25 PENDSTCLR 写1清除SysTick异常
23 ISRPREEMPT 用于调试模式,指示是否有挂起等待的异常
22 ISRPENDING 指示NVIC是否有外部中断处于挂起状态
20:12 VECTPENDING 指示当前使能并挂起的最高优先级。为0表示没有异常
11 RETTOBASE 处理模式下,为0代表当前除IPSR指示的异常以外还有其他活跃的异常。一般出现在中断嵌套中
9:0 VECTACTIVE 指示当前执行的异常号码

有关SVCPendSV

非特权程序可以执行SVC指令进行系统调用,此时产生一个SVC异常,且SVC优先级较高一般会得到立即响应(如果被屏蔽那么会变成Hardfault)。而PendSV只能通过在特权模式下写寄存器挂起,且一般将PendSV设置为较低优先级,它会像普通中断一样延迟执行。PendSV一般应用在多任务RTOS的线程调度、上下文切换中。具体应用示例见下文解释

操作系统内核依赖于SysTick定时器中断进行时间片轮转式的任务调度,所以一般将SysTick设置为较高的优先级。这就带来一个问题,如果SysTick到来时正在响应另一个异常且SysTick将其抢占,如果不加判断,那么接下来进程调度会直接试图再次进入到线程模式,这会导致触发UsageFault(用法错误异常),同时异常处理也会延迟

为了解决这个问题,可以将上下文切换操作设定为PendSV异常的处理程序(服务)。内核一旦决定进行上下文切换就可以挂起一个PendSV异常,这样可以在异常处理完毕以后再自动执行切换

VTOR寄存器

长度32位,定义了中断向量表的起始地址,如下

其中低7位无效

注意,部分处理器中VTOR的高2位不可用,向量表只能定位于内存的开头1GB空间内

AIRCR寄存器

长度32位,主要提供了自复位,大小端模式,调试,中断优先级分组等相关功能

位域 名称 作用
31:16 VECTKEY 访问键值,写入永远为0x05FA
15 ENDIANNESS 1表示使用大端,0表示使用小端。默认0小端,只能在复位后更改
10:8 PRIGROUP 优先级分组,可以设置为0到7。默认0
2 SYSRESETREQ 写1触发一次全芯片复位
1 VECTCLRACTIVE 一般用于调试,清除所有异常的活跃标记位
0 VECTRESET 一般用于调试,写1触发一次CPU核心复位

SHP寄存器

定义系统异常优先级,一共有12个,为SHP[0]~SHP[11],单个长度8位,其中只有7个有定义,见下表

编号 定义 复位值
0 MemManage优先级 0x00
1 BusFault优先级 0x00
2 UsageFault优先级 0x00
7 SVC优先级 0x00
8 DebugMonitor优先级 0x00
10 PendSV优先级 0x00
11 SysTick优先级 0x00

SHCSR寄存器

使能/禁用系统异常,指示系统异常的状态,长度32位,其中只有14位有用。一般只使用到前3位对异常进行使能或禁止

位域 名称 作用
18 USGFAULTENA UsageFault使能
17 BUSFAULTENA BusFault使能
16 MEMFAULTENA MemManage使能
15 SVCALLPENDED SVC挂起状态
14 BUSFAULTPENDED BusFault挂起状态
13 MEMFAULTPENDED MemManage挂起状态
12 USGFAULTPENDED UsageFault挂起状态
11 SYSTICKACT SysTick活跃状态
10 PENDSVACT PendSV活跃状态
8 MONITORACT DebugMonitor活跃状态
7 SVCALLACT SVC活跃状态
3 USGFAULTACT UsageFault活跃状态
1 BUSFAULTACT BusFault活跃状态
0 MEMFAULTACT MemManage活跃状态

1.5.10 中断/异常调用过程详解

由于ARM的设计,中断和异常处理得以和普通的函数调用一样编程

这里首先引入AAPCS(ARM架构过程调用标准)的定义,这个标准规定了函数调用时使用哪些寄存器传参,以及哪些寄存器在什么时候进行压栈,见下图

其中,R0R3R12R14(LR)以及PSR(M4核心还有S0S15以及FPSCR)称为调用者保存寄存器,父程序需要在进入子程序之前将这些寄存器压栈

R4R11(M4核心还有S16S31)称为被调用者保存寄存器。这些寄存器在进入子程序之前一直到子程序返回不能有更改,所以子程序如果需要用到这些寄存器就需要将它们压栈,并在返回之前出栈恢复

而在中断处理时,处理器硬件会自动完成调用者寄存器保存(在M3核心中为R0R3R12R14(LR)以及PSR)。除此之外还需要将返回地址压栈,这和传统的函数调用不同,这样一次压栈需要使用8个字(32字节)

关于中断处理和函数调用的区别:之前提到过中断时LR会被设为EXC_RETURN,然后在返回时EXC_RETURN会和普通的函数调用一样被加载到PC以触发异常返回。所以和普通的函数调用不同,异常处理的返回地址存储在栈中而不是LR中。异常返回时的地址出栈,PC赋值等众多操作同样由硬件自动处理。另外,异常处理不需要返回结果,也不能影响到原先程序的执行,所以异常在退出以后CPU对寄存器立即进行硬件出栈恢复。这和函数调用也不一样,函数调用中父程序还需要先保存返回值再出栈恢复

EXC_RETURN位域定义如下

位域 定义
31:5 全为1,0xFFFFFFE
4 表栈帧长度,1表示8字,0表示26字。进入中断时会将CONTROL见前)的FPCA取反并存入该位,之后在返回时将该位取反存入FPCA
3 表返回模式,1表示返回到线程模式0表示返回到处理模式(一般在中断嵌套情况下会发生)。如果此时有异常处于活跃状态,尝试返回到线程模式会导致UsageFault的触发
2 表返回后使用的栈,1表示使用PSP0表示使用MSP。同样取决于当前CONTROL寄存器的SPSEL
1 永远为0
0 永远为1

综上,EXC_RETURN只有6个合法值

栈帧

在异常入口处压入内存的栈数据块称为栈帧,一般Cortex-M3中栈帧大小为8个字(32字节),而带有FPU的Cortex-M4中栈帧大小为26个字(104字节)

栈帧相对于MCU有限(kB级别居多)的内存来说非常庞大。这也是为什么很多ARM MCU中只会配置8到16个中断优先级(有些甚至只有4个),并且不建议使用函数递归算法或者像一般的PC应用一样滥用函数嵌套。在MCU应用中,中断或函数嵌套太多容易导致爆栈

AAPCS规定栈指针在函数/异常的入口和出口处需要对齐双字。在一般的函数调用中这需要软件实现,而在异常处理中由于是处理器硬件自动进行压栈操作,如果未对齐双字处理器会自动插入一个字

异常处理中的栈帧示意图如下,将通用寄存器放在最后,这样可以方便通过SP访问传递的参数。如果xPSR[9]=0,就表示本次压栈是对齐双字的。如果没有对齐双字那么就会在一开始xPSR之前插入一个字并将xPSR[9]置1,CPU会在异常退出时根据该位确定SP是否需要调整

事实上异常处理入口的寄存器压栈是乱序的,和栈帧的顺序不一样,如下。首先压栈的是返回地址,最后才是LR(因为压栈和取向量是并行执行的,取向量会改变PC所以要尽早将返回地址压栈

在Cortex-M4中的栈帧示意图如下

事实上栈双字对齐这个特性是可选的。在Cortex-M3 r2p0以及Cortex-M4中这个特性是默认开启的所以无需额外配置

1.5.11 处理器对中断/异常的自动优化

异常的末尾连锁

如果一个较低优先级的异常触发时遇到了高优先级的异常正在执行,那么它就会挂起并阻塞,等到高优先级的异常执行完成以后再开始执行。此时高优先级异常在返回后CPU不会进行出栈恢复操作,而是直接开始执行低优先级的异常。如下图

异常的延迟到达

如果一个低优先级异常触发了压栈操作,在压栈还未完成时发生了更高优先级的异常,那么CPU会取高优先级异常对应的向量,高优先级的异常会首先执行。如下

异常的出栈抢占

如果在某个异常执行完成出栈时又发生了异常,那么异常的出栈操作会被舍弃(Aborted),直接取向量执行下一个异常处理

惰性压栈

惰性压栈和浮点寄存器相关,只有带有FPU的Cortex-M4核心具有该特性。详情见浮点计算

由于浮点寄存器较多,压栈会占用更多的周期,这会拖慢异常响应的速度。所以Cortex-M4引入了惰性压栈,且这个特性默认开启。这样在浮点上下文中收到异常而开始压栈时,CPU可以在栈中给浮点寄存器预留空间的同时只将基本的寄存器/返回地址(32字节)压栈。这样可以将中断响应时间控制在12个时钟周期。如果异常处理中遇到了浮点指令,那么CPU会暂停并将S0S15压栈,之后继续执行异常

发生惰性压栈时,寄存器LSPACT会置位,同时寄存器FPCAR存放栈中为浮点寄存器预留空间的地址

如果不在浮点上下文中(FPCA=0),那么栈帧依然保持为8字长度

1.6 ISA详解:指令集

ARMv7-M指令集是ARMv6-M指令集的一个超集。Cortex-M4F,M3以及M0核心依次向下兼容。很多基本指令有16位(半字)和32位(单字)两种版本,编译器以及汇编器会根据需求选择尽量短的指令以缩小程序体积

GNU工具链中汇编基本格式如下示例

label:
    MOVS R0, #0x12 /* 指令格式 */

定义常数/常量

.equ    NVIC_IRQ_SETEN,     0xE000E100 /* 大常数 */
.equ    NVIC_IRQ0_ENABLE,   0x1 /* 小常数 */

LDR R0, =NVIC_IRQ_SETEN /* 将0xE000E100加载到R0,该指令是伪指令,加上=相当于取地址(指令存不下立即数,需要先将该地址存储到一个寄存器中) */
MOVS R1, #NVIC_IRQ0_ENABLE /* 将0x1装入R1。0x1可以作为立即数直接编码到MOV指令中,所以使用#修饰 */
STR R1, [R0]

.align 4    /* 强制4字节对齐 */
TEST_DATA:
.word 0x00032C10 /* 插入数据TEST_DATA为0x00032C10 */
HELLO_TEXT:
.asciz "Hello World!"

/*
数据插入使用
.byte       1字节
.hword      2字节/半字,可以表示一条16位指令
.word       4字节/单字,可以表示一条32位指令
.quad       8字节/双字
.float      单精度浮点/32位
.double     双精度浮点/64位
.ascii      字符串
.asciz      末尾添加NULL的字符串
*/

尾缀用法

ADDS.N R0, #1 /* 使用16位ADD指令(Narrow),更新APSR */
ADD.W R0, #1 /* 使用32位ADD指令(Wide),不更新APSR */
ADDSEQ.W R0, R0, R1 /* 如果APSR中Z为1那么执行该指令(EQ),更新APSR(S) */

尾缀定义

1.6.1 UAL

为了统一各代ARM汇编的写法就产生了UAL,UAL和传统汇编写法主要有以下区别

部分运算指令从2个操作数改为3个操作数,如ADD R0 R1需要改写为ADD R0 R0 R1,遵循OP Rd, Rn, Rm的格式(目标寄存器放在最前)

必须明确使用S后缀才会更新APSR,例如ADDS R0 R0 R1(ARM7中几乎大部分指令都会强制更新APSR,这和较新的v6以及v7处理器不同)

另外,几乎所有的16位Thumb指令只能访问R0R7寄存器,想要访问高寄存器只能使用32位Thumb指令

1.6.2 寄存器传送指令

指令示例 解释
MOV R0, R3 R3数据传送到R0
MOVS R0, R3 同上,更新APSR
MOV R3, #0x34 将立即数(不大于8位)传送到R3
MOVS R3, #0x34 同上,更新APSR
MOVW R6, #0x7B5A 赋值大立即数(9到16位)。立即数较大时汇编器会自动转换指令MOVMOVW
MOVT R6, #0x4D2C 赋值R6高16位
MVN R4, R3 R3取反赋值给R4
MRS R0, PRIMASK 特殊寄存器指令,将PRIMASK数据传输到R0。可以这样记忆:MRS的RS就是代表GPR在前,特殊寄存器在后
MSR PRIMASK, R0 R0数据传输到PRIMASK

32位立即数赋值一般使用伪指令(16以及32位指令无法编码),如LDR R0, =0x1728D45A,事实上使用了LDR R0, [PC, #offset]的寻址方式,将0x1728D45A编码到.pool文字池中。

还可以使用MOVWMOVT结合使用的方法,这在特定情况下可以规避LDR导致数据缓存丢失的问题

如果想要将一个寄存器设为一个32位地址,可以使用伪指令ADRADRL,如ADR R0, MyString,其中MyString是程序文件中数据的地址标记

1.6.3 存储器指令

单寄存器传输

指令示例 解释
LDRB R0, [R4, #0xF] R4+0xF所表示的地址处1字节数据传输到R0,16位指令中立即数最大可取0x1F,32位指令中可取-0xFF0xFFF,立即数域可以省略(0x0)。使用LDRSB指令对单字节数据进行符号扩展,如0x95转为0xFFFFFF95(和APSR无关,不要混淆)。32位指令可以加感叹号LDRB R0, [R4, #0xF]!表示更新R4值为R4+0xF
STRB R0, [R4, #0xF] R0低1字节存储到R4+0xF处,立即数取值同上
LDRH R0, [R3, #0x3] R3+0x32字节数据传输到R0。使用LDRSH进行符号扩展,立即数取值同上
STRH R0, [R3, #0x3] R0低2字节存储到R3+0x3处,立即数取值同上
LDR R0, [R2, #0xA] R2+0xA4字节数据传输到R0,立即数取值同上
STR R0, [R2, #0xA] R0存储到R2+0xA处,立即数取值同上

LDRB LDRSB LDRH LDRSH LDR可以使用PC作为基址寄存器

可以使用寄存器偏移方式访问,示例LDR R2, [R4, R5, LSL #3],表示从地址R4+R5<<3处读取,立即数可取0到3

还可以使用后序方式访问,示例LDR R2, [R3], #2,在每次访问之后会R3会自动加2。适用于数组访问

另外这些指令还有T结尾的版本,如LDRT R0, [R2, #0xA],用于一些操作系统中特权API的实现,可以允许特权模式下的代码访问非特权内存

双寄存器传输

指令示例 解释
LDRD R2, R3, [R1, #-0x8D] R1-0x8D8字节数据传输到R2R3,立即数可取-0xFF0xFF
STRD R2, R3, [R1, #-0x8D] R2R3依次存储到R1-0x8D处,立即数同上

LDRD可以使用PC作为基址寄存器

不支持寄存器偏移方式。支持后序方式

多寄存器传输

指令示例 解释
LDMIA R1, {R2-R4, R6} R1处16字节数据依次存储到括号中的寄存器,每次读后地址增加。可以添加感叹号LDMIA R1!, {R2-R4, R6}将地址写回R1寄存器
STMIA R1, {R2-R4, R6} 将括号中寄存器依次存储到R1处,每次写后地址增加
LDMDB R3, {R1, R5-R9} R3之前24字节数据存储到括号中的寄存器,每次读前地址减小
STMDB R3, {R1, R5-R9} 将括号中寄存器存储到R3处,每次写前地址减小

多寄存器传输和下面的栈操作在一般情况下(指不在IT上下文中)可以被中断后继续执行,原因见前PSR寄存器

栈操作

指令示例 解释
PUSH {R3, R5-R7, LR} 压栈,SP先减小后压栈。16位PUSH只能使用R0-R7以及PC LR
POP {R3, R5-R7, PC} 出栈,出栈后SP才增大

排他访问

排他访问需要硬件的支持,使用较少(在具有MPU的MCU中比较有用,需要监控器),可以针对一片需要排他访问的内存(比如shareable)设置一个寄存器作为标志位(一般在global monitor中),保证Read-Modify-Write的正常运行,由一对特殊的LOAD以及STORE指令组成。信号位一般情况下为0,LOAD(占用)时置1,正常STORE(释放)后回0

排他访问的意义在于高优先级抢占低优先级时,如果要求低优先级的RMW操作是原子的,使用传统方法只能关中断或在高优先级中加入额外判断,这会导致高优先级任务的延迟,紧急任务得不到及时响应。使用排他访问指令就可以使得低优先级任务知道自己是否正确进行了RMW操作,事后处理而不影响高优先级任务的执行,优化实时响应性能

一般导致排他RMW操作失败的原因有内存被其他处理器访问,执行了CLREX或过程中遇到了中断

指令示例 解释
LDREXB R4, [R5] R51字节数据传输到R4,立即数可以取0x000xFF
STREXB R0, R4, [R5] R4低1字节传输到R5所指地址处,同时将存储执行结果(成功为0失败为1)返回到R0中,立即数同上
LDREXH R4, [R5] R52字节数据传输到R4
STREXH R0, R4, [R5]
LDREX R4, [R5, #0xD]
STREX R0, R4, [R5, #0xD]
CLREX 用于一对LOAD以及STORE指令之间,清除标记位强制下一次排他写入失败

1.6.4 算术、饱和、逻辑、移位与数据转换

算术

加减法

指令示例 解释
ADD R1, R4, R7 寄存器加,R1=R4+R7,16位指令只能使用低寄存器,32位指令可以使用高寄存器并对R7进行移位,例如ADD.W R1, R4, R7, ASR #0x1
ADD R1, #0x2C 立即数加,R1=R1+0x2C,16位指令,立即数可以取0x000xFF
ADD R1, R4, #0x4 立即数加,R1=R4+4,16位指令立即数只能取0x00x7,32位指令(.W)可以使用高寄存器并可以使用更大的常数
ADDW R1, R4, #0xF4 立即数加,R1=R4+0xF4,只有32位,立即数最大可取0xFFF
ADC R4, R7 寄存器带进位加,R4=R4+R7,32位示例ADC.W R0, R1, R9, ASR #0x2
ADC R1, R4, #23 立即数带进位加,R1=R4+23,32位
SUB R2, R4, R5 寄存器减,R2=R4-R5,有16位和32位,同ADD
SUB R2, #0x1D 立即数减,R2=R2-0x1D,只有16位,同ADD
SUB R2, R4, #0x2 立即数减,R2=R4-2,有16位和32位,同ADD
SUBW R2, R9, #0x3D 立即数减,R2=R9-0x3D,只有32位,同ADDW
SBC R1, R6 寄存器带进位减,R1=R1-R6,有16位和32位,同ADC
SBC R2, R9, #12 立即数带进位减,R2=R9-12,只有32位,同ADC
RSB R5, R6, R8, ASR #0x1 寄存器反向减,R5=R8/2-R6,只有32位
RSB.W R4, R3, #3 立即数反向减,R4=3-R3,32位指令
RSB R1, R2, #0 立即数反向减,立即数只能取0,相当于求相反数

以上指令除ADDW SUBW外都可以在指令名称后加S后缀,如ADDS.W R1, R4, R10

乘除法

指令示例 解释
MUL R4, R5, R3 寄存器乘,R4=R5*R3,32位指令(有16位版本,RdRm使用同一个寄存器),结果只取低32位
UDIV R3, R2, R9 寄存器无符号除,R3=R2/R9,32位指令
SDIV R3, R2, R9 寄存器有符号除,R3=R2/R9,32位指令
MLA R4, R10, R5, R3 寄存器乘累加,R4=R3+R10*R5,32位指令
MLS R4, R10, R5, R3 寄存器乘累减,R4=R3-R10*R5,32位指令
SMULL R0, R1, R4, R9 有符号寄存器乘,[R1:R0]=R4*R9,32位指令,可以输出64位结果
SMLAL R0, R1, R4, R9 有符号寄存器乘累加,[R1:R0]=[R1:R0]+R4*R9,32位指令
UMULL R0, R1, R4, R9 无符号寄存器乘,[R1:R0]=R4*R9,32位指令
UMLAL R0, R1, R4, R9 无符号寄存器乘累加,[R1:R0]=[R1:R0]+R4*R9,32位指令

以上指令只有MUL可以加S后缀

饱和运算

指令示例 解释
SSAT R0, #12, R8, ASR #1 有符号饱和,取前12位,32位指令,如果饱和那么APSRQ会置位,需要写APSR清除。无可加S后缀,可以移位R8。只能LSLASR移位
USAT R0, #12, R8 无符号饱和,同理

逻辑与移位

逻辑运算

指令示例 解释
AND R4, R5 寄存器与,R4=R4&R5,16位指令,32位示例AND.W R4, R5, R8, LSL #4
AND R4, R8, #3 立即数与,R4=R8&3,32位指令
BIC R4, R5 寄存器与,R4=R4&(~R5),有16位和32位,同AND
BIC R4, R8, #3 立即数与,R4=R8&(~3),32位指令,同AND
ORR R4, R5 寄存器或,R4=R4|R5,有16位和32位,同AND
ORR R4, R8, #3 立即数或,R4=R8|3,32位指令,同AND
EOR R4, R5 寄存器异或,R4=R4^R5,有16位和32位,同AND
EOR R4, R8, #3 立即数异或,R4=R8^3,32位指令,同AND
ORN R1, R2, R8, LSL #1 寄存器或非,R1=~(R2|(R8<<1)),32位指令
ORN R1, R2, #3 立即数或非,R1=~(R2|3),32位指令

可以添加S后缀。没有逻辑非指令,可以通过其他指令等价实现

移位

指令示例 解释
ASR R1, R4 寄存器算术右移,R1=R1>>R4,16位指令,32位示例ASR.W R1, R2, R5
ASR R1, R4, #4 立即数算术右移,R1=R4>>4,16位指令,32位示例ASR.W R1, R2, #8
LSL R1, R4 寄存器逻辑左移,R1=R1<<R4,有16位和32位,同ASR
LSL R1, R4, #4 立即数逻辑左移,R1=R4<<4,有16位和32位,同ASR
LSR R1, R4 寄存器逻辑右移,R1=R1>>R4,有16位和32位,同ASR
LSR R1, R4, #4 立即数逻辑右移,R1=R4>>4,有16位和32位,同ASR
ROR R1, R4 寄存器循环右移,有16位和32位,同ASR
ROR R1, R4, #4 立即数循环右移,只有32位
RRX R4, R8 带扩展的寄存器循环右移,只有32位指令,移出的位先存入APSRC中再移入最高位(相当于33位移位寄存器)

可以添加S后缀。移位后APSR中的C等于最后移出的1位

数据转换

展开,将寄存器中的字节、半字扩展为32位单字长

指令示例 解释
SXTB R1, R4 有符号字节扩展,16位指令,32位示例SXTB.W R1, R4, ROR #5,只能使用循环右移ROR
SXTH R1, R4 有符号半字扩展,16位指令,32位示例SXTH.W R1, R4, ROR #5,同SXTB
UXTB R1, R4 无符号字节扩展,有16位和32位,同SXTB
UXTH R1, R4 无符号半字扩展,有16位和32位,同SXTB

无可用S后缀

反转,用于将寄存器中的字节交换,多用于SIMD应用

指令示例 解释
REV R1, R3 全寄存器字节反转,R1[7:0]=R3[31:24],依次类推,16位指令,32位示例REV.W R1, R10
REV16 R1, R3 半字字节反转,R1[7:0]=R3[15:8], R1[15:8]=R3[7:0],两两交叉依次类推,有16位和32位指令,和REV相同
REVSH R1, R3 有符号半字字节反转,只反转R1[7:0]=R3[15:8], R1[15:8]=R3[7:0],同时扩展符号,有16位和32位指令,和REV相同

无可用S后缀

1.6.5 位域处理指令

位域处理指令主要是设计用于DSP应用的,而并不是类似8051和AVR的位寻址功能。ARM的寻址方式永远基于字节,虽然ARM有可选的位段特性,可以在内存中设置一片位段区域支持类似8051的位寻址功能

指令示例 解释
BFC R1, #8, #16 R1[23:8]=0,将寄存器中从n位开始的m位置0,32位指令
BFI R0, R1, #8, #16 R0[23:8]=R1[15:0],将一个寄存器中指定尾数嵌入到另一个寄存器指定位置,32位指令
CLZ R0, R2 前导0计数,计算第一个1之前的0个数,32位指令。Count Leading Zeros
RBIT R1, R3 比特反转,R1[31:0]=R3[0:31],32位指令
UBFX R0, R1, #8, #16 R0=R1[8:23],提取寄存器指定位域并使用0扩展,32位指令
SBFX R0, R1, #8, #16 同上,使用符号扩展,32位指令

无可用S后缀

1.6.6 比较、测试与程序流控制

比较与测试

指令示例 解释
CMP R1, R2 寄存器比较,相当于没有输出的减法,16位指令,32位指令示例CMP.W R1, R2, ASR #1,总是更新APSR
CMP R1, #0xAF 立即数比较,16位指令,立即数最大取0xFF,32位指令示例CMP.W R10, #10
CMN R1, R2 寄存器比较,相当于没有输出的加法,有16位和32位指令,同CMP,用于判断相反数
CMN R1, #0xAF 立即数比较,有16位和32位指令,同CMP
TST R1, R2 寄存器测试,相当于没有输出的与运算,有16位和32位指令,同CMP,更新APSRN Z两位
TST R1, #0xAF 立即数比较,有16位和32位指令,同CMP
TEQ R1, R2 寄存器测试,相当于没有输出的异或运算,有16位和32位指令,同CMP,更新APSRN Z两位
TEQ R1, #0xAF 立即数比较,有16位和32位指令,同CMP

这些指令总是更新APSR,所以也没有可选的S后缀

跳转与子程序调用

几乎所有的普通指令都可以使用条件执行后缀,如ADDEQ R1, R2, R3,而事实上这些指令都是伪指令,在ARMv7-M中会自动添加IT指令

条件后缀定义传送门

指令示例 解释
B label 跳转到label处,是16位指令,32位指令可以有更大的跳转范围,示例B.W label向前或向后相对跳转(半字对齐)。从机器码层面看,这是事实上可以接受APSR中状态位判断参数的指令之一(如EQ NE GT等。另一条指令是IT)。如果使用了后缀那么跳转范围会相应的缩小,因为事实上有两条不同的B指令分别用于带后缀与不带后缀的情况
BX R1 跳转到R1所指位置,16位指令,R1最低位必须置1表示Thumb状态下,绝对地址跳转
BL label 子程序调用,跳转到label处,同时将该指令下一条指令的地址存入LR(R14),只有32位指令
BLX R1 子程序调用,跳转到R1同时将该指令下一条指令的地址存入LR(R14),只有16位指令,R1最低位必须置1
CBZ R1, label 检查R1的值,如果等于0那么跳转到label。只能向前跳转
CBNZ R1, label 不为0时跳转到label

进入子程序时使用BLBLX,而子程序返回一般使用BX LR

If-Then跳转

ARMv7-M中引入了IT指令,长度16位,在一般的汇编代码编写中用不到,汇编器会自动添加。例如我们写一条ADDEQ R1, R1, R2,汇编器会自动在前面添加IT EQ

示例1

IT EQ
ADDEQ R4, R5, R3

示例2

ITET NE
ADDNE R1, R5, R3
ADDEQ R4, R5, R3
ADDNE R7, R5, R3

示例3

ITETT GT
ADDGT R1, R5, R3
ADDLE R4, R5, R3
ADDGT R7, R5, R3
ADDGT R2, R5, R3

IT可以说是ARMv7-M中最诡异的指令。其添加后缀可以是ITE ITT ITEE ITET ITTE ITTT ITEEE ITETT等任意排列组合方式,后加EQ表示符合的条件(检查的是PSR)。IT指令之后可以跟最多4条指令,而这4条指令也必须带有后缀,如ADDNE R1, R0, R3,同时后缀一定要和ITXXX所表示的条件相符(这些后缀是无意义的,它们只是重复描述了ITETT的条件位而已,但是代码规定必须添加),T表示符合所以使用同后缀,E表示不符所以使用相反后缀。IT指令最多跟3个后缀加1个条件后缀,其本身代表一个T后缀

IT每遇到一个不符合条件的指令也会消耗一定的时间,所以有时候IT不一定比传统的基于B指令的跳转快

基于B指令实现判断语句需要使用一个有条件跳转例如BEQ加一个无条件跳转B。具体写法不再详述

注意,IT指令块中只有最后一条指令可以修改PC例如B等,只能在IT指令块中最后一条指令处出现

PSR中有ITSTATE,用于指示当前的IT执行状态,共计8位PSR[26:25][15:10],可以分为IT[7:5]IT[4:0]两部分。执行过程中遇到异常IT会被打断,之后就要使用ITSTATE进行恢复

IT[7:5]储存该IT指令的条件位高3位

IT[4:0]表示IT后的指令条数(看最低1的位置),并决定指令1到4执行与否。每运行一条IT块中的指令,右侧的0都会增加一个。参照下表,每运行一条指令都会跳到表格下一项

表格跳转

表格跳转指令TBBTBH用于实现C语言的switch语句

指令示例 解释
TBB [R0, R1] 执行到该指令时,使用R0地址作为跳转表格的基址,R1中存储跳转项在表格中的下标(Index,不是偏移地址),那么跳转项位于R0+R1,之后直接跳转到PC+([R0+R1]<<1)处的指令([R0+R1]指的是地址R0+R1处这个单字节数据)。最多可以相对偏移512字节。该指令长度32位
TBH [R0, R1, LSL #1] 表格中跳转项长度为2字节,R1为下标(LSL #1不能省略),跳转项位于R0+(R1<<1),之后跳转到PC+([R0+(R1<<1)]<<1),其余相同。最多可以相对指令表格基地址偏移128k字节。该指令长度32位

GNU汇编示例

ADR.W R0, Branch_Table
TBB [R0, R1]
Case1:
    /*Instructions*/
Case2:
    /*Instructions*/

...

Branch_Table:
.byte 0
.byte ((Case2-Case1)/2) /*因为TBB指令跳转地址左移一位所以除以2*/

和C语言中写switch()语句要加break语句的原理一样,每一个Case之后的指令一般都要在最后加上一个无条件跳转指令(BBX),否则一种Case执行完以后还会执行接下来的指令

1.6.7 异常指令

Supervisor Call系统调用

系统调用使用SVC指令,只在涉及到操作系统的场合会有应用,关于SVC中断编号见前

指令示例 解释
SVC #0xAF 应用程序产生系统调用异常,后面可以跟8位数字,用于参数传递(比如应用想要调用何种系统服务),因为NMI和HardFault优先级一定比Supervisor更高所以在这两种异常中不能执行SVC。8位参数只是一个数字对异常的行为没有影响,系统程序需要通过已经压栈的PC获取该条SVC指令的地址并读取该数字,因此PC一般需要在SVC调用之前压栈

中断屏蔽控制

指令CPS用于控制中断的屏蔽,参见1.4.2节PRIMASKFAULTMASK

指令示例 解释
CPSIE I PRIMASK清0使能中断
CPSID I PRIMASK置1禁用除NMI和HardFault外的中断
CPSIE F FAULTMASK清0
CPSID F FAULTMASK置1禁用除NMI外的中断

1.6.8 休眠指令

处理器核心休眠

指令示例 解释
WFI 等待中断,此时处理器立即进入休眠模式,可以通过中断、复位或调试唤醒
WFE 等待事件,此时处理器会条件性地进入休眠,可以通过中断、复位、调试或其他事件(如多核系统中其他处理器发送来的信号)唤醒
SEV 事件输出,在多核系统中可以向其他核心发送信号

1.6.9 存储器屏障

存储器屏障,一般用于具有超标量以及乱序执行的处理器系统中,可以解决一些乱序访存导致的错误

指令示例 解释
DMB 存储器屏障,确保下一次访存之前所有访存操作已完成
DSB 数据同步屏障,确保下一指令前所有访存操作已完成
ISB 指令同步屏障,清空流水线确保下一指令前所有指令执行完成

Cortex-M核心因为流水线比较简单,在一般情况下是不需要用到这些屏障的。使用到屏障一般有以下几种情况:

更改CONTROL寄存器之后(见前),需要使用ISB指令确保之后的指令使用正确的SP寄存器并运行在正确的模式下

如果在处理模式下使能了SCR寄存器的SLEEPONEXIT,那么就表示处理程序结束后会立即进入休眠,需要在结束时执行DSB保证数据安全写入到SRAM

异常挂起后,需要确保该异常在之后的操作之前执行,需要执行DSBISB

禁用中断后想要中断立即起效,执行DSBISB

自修复,执行DSBISB

运行时更改了程序存储Flash的映射地址,需要立即起效,执行DSBISB

运行时更改了数据SRAM的映射地址,需要立即起效,执行DSB

通过MPU更改了一片程序存储区域的配置(如权限等),且需要到该区域取指并执行,执行DSBISB

以下情况遇到问题可以尝试添加屏障

通过MPU将一片存储区域从仅允许数据访问更改为可取指,且需要到该区域取指并执行,执行DSB

执行WFIWFE休眠之前,执行DSB

执行信号量操作时,执行DMBDSB

修改了SVC指令的优先级并触发,在触发前执行DSB

通过VTOR修改了中断向量表的偏移并触发中断,在触发前执行DSB

CPU自复位之前,执行DSB

1.6.10 杂项

指令示例 解释
NOP 什么都不做,长度16位,一般可以用于指令对齐或软延时
BKPT #0x24 用于调试,CPU在执行到该指令时会暂停,触发调试异常,同时用户可以执行一些调试任务。后面的8位长数字和SVC一样,只是用于存储参数

1.6.11 Cortex-M4:浮点指令,SIMD与乘法

可以通过CMSIS-DSP库调用这些浮点以及SIMD等高级算术功能,该函数库由ARM委托DSP Concepts(dspconcepts.com)开发。FPSCR寄存器定义见1.4.3

开始本章之前建议先了解一下有关IEEE754浮点数标准,可以看这里

浮点指令

所有的浮点指令都以V开头,Cortex-M4只支持单精度浮点数的运算,所以大部分指令使用.F32作为后缀

寄存器传送

指令示例 解释
VMOV.F32 R4, S2 将32位浮点寄存器S2数据传输到R4
VMOV.F32 S0, R2 将32位浮点寄存器R2数据传输到S0
VMOV.F32 S2, S3 将32位浮点寄存器S3数据传输到S2
VMOV.F32 S5, #1.0 将单精度浮点数1.0传送到浮点寄存器S5
VMOV S0, S1, R0, R1 R0 R1两个数传输到S0 S1
VMOV R0, R1, S0, S1 S0 S1两个数传输到R0 R1
VMRS.F32 R0, FPSCR 将浮点状态寄存器FPSCR数据传送到R0
VMRS APSR_nzcv, FPSCR 将浮点状态寄存器FPSCRNZCV位传送到APSR的状态位(多用于浮点条件运算)
VMSR FPSCR, R0 R0传送到FPSCR

浮点访存

指令示例 解释
VLDR.F32 S0, [R1, #0xA] R1+0xA处4字节单精度传输到S0。8字节双精度使用.F64以及D0,立即数可取-0xFF0xFF,可以使用PC作为基址寄存器,也可以使用label,示例VLDR.F32 S0, label
VSTR.F32 S6, [R5, #0x4] S6中的单精度数存储到R5+0x4处。双精度以及立即数同上
VLDMIA.F32 R2, {S0, S3-S7} 传输到多个寄存器,R2基址递增。双精度寄存器使用.F64,可以添加感叹号VLDMIA.F32 R2!, {S0, S3-S7}表示地址写回
VSTMIA.F32 R2, {S0, S3-S7} 将多个寄存器存储到内存,R2基址递增,其余同上
VLDMDB.F32 R2, {S0, S3-S7} 传输到多个寄存器,R2基址递减
VSTMDB.F32 R2, {S0, S3-S7} 将多个寄存器存储到内存,R2基址递增
VPUSH.F32 {S3-S9, S11} 浮点寄存器压栈,双精度版本示例VPUSH.F64 {D1, D3-D10}
VPOP.F32 {S3-S9, S11} 浮点寄存器出栈,双精度版本示例VPOP.F64 {D1, D3-D10}

基本算术指令

指令示例 解释 异常
VABS.F32 S0, S1 求绝对值,S0=Abs(S1)
VNEG.F32 S0, S1 浮点取相反数,S0=-S1
VADD.F32 S0, S1, S2 加法,S0=S1+S2 IDC IOC OFC UFC IXC
VSUB.F32 S0, S1, S2 减法,S0=S1-S2 IDC IOC OFC UFC IXC
VCMP.F32 S0, S1 比较S1S0,更新FPSCR,若一个数为sNaN那么抛出IOC。立即数版本VCMP.F32 S0, #0.0只能和0进行比较。带E后缀版本VCMPE.F32 S0, S1在遇到任意NaN都会抛出IOC IDC IOC
VMUL.F32 S0, S1, S2 浮点乘法,S0=S1*S2 IDC IOC OFC UFC IXC
VNMUL.F32 S0, S1, S2 浮点乘法取反,S0=-(S1*S2) IDC IOC OFC UFC IXC
VDIV.F32 S0, S1, S2 浮点除法,S0=S1/S2 IDC IOC OFC UFC IXC DZC
VSQRT.F32 S0, S1 均方根,S0=sqrt(S1) IOC IDC IXC
VFMA.F32 S0, S1, S2 融合乘加,S0=S0+(S1*S2) IDC IOC OFC UFC IXC
VFMS.F32 S0, S1, S2 融合乘减,S0=S0-(S1*S2) IDC IOC OFC UFC IXC
VFNMA.F32 S0, S1, S2 融合负乘加,S0=-S0+(S1*S2) IDC IOC OFC UFC IXC
VFNMS.F32 S0, S1, S2 融合负乘减,S0=-S0-(S1*S2) IDC IOC OFC UFC IXC
VMLA.F32 S0, S1, S2 浮点乘加,S0=S0+(S1*S2) IDC IOC OFC UFC IXC
VMLS.F32 S0, S1, S2 浮点乘减,S0=S0-(S1*S2) IDC IOC OFC UFC IXC
VNMLA.F32 S0, S1, S2 浮点乘加取反,S0=-(S0+(S1*S2)) IDC IOC OFC UFC IXC
VNMLS.F32 S0, S1, S2 浮点乘减取反,S0=-(S0-(S1*S2)) IDC IOC OFC UFC IXC

整数转换

指令示例 解释 异常
VCVT.S32.F32 S0, S1 32位有符号转浮点,向0舍入,S0=(Float)S1。带R后缀版本示例VCVTR.S32.F32 S0, S1,表示使用FPSCR指定的舍入方式 IDC IOC IXC
VCVT.U32.F32 S0, S1 32位无符号转浮点,向0舍入,S0=(Float)S1。带R后缀同理 IDC IOC IXC
VCVT.F32.S32 S0, S1 浮点转32位有符号,S0=sInt32(S1) IDC IOC IXC
VCVT.F32.U32 S0, S1 浮点转32位无符号,S0=uInt32(S1) IDC IOC IXC

定点数转换

指令示例 解释 异常
VCVT.S16.F32 S0, S0, #12 16位有符号定点数转浮点数,小数12位,向最近数舍入,操作数和结果只能使用同一个寄存器。16位无符号定点版示例VCVT.U16.F32 S0, S0, #12,另有32位有符号以及无符号版VCVT.S32.F32VCVT.U32.F32 IDC IOC IXC
VCVT.F32.S16 S0, S0, #5 浮点数转16位有符号定点数,小数5位。有U16 S32 U32版同上 IDC IOC IXC

16位半精度转换

指令示例 解释 异常
VCVTB.F32.F16 S0, S1 单精度转半精度,S0[15:0]=(Half)S1 IDC IOC OFC UFC IXC
VCVTF.F32.F16 S0, S1 单精度转半精度,S0[31:16]=(Half)S1 IDC IOC OFC UFC IXC
VCVTB.F16.F32 S0, S1 半精度转单精度,S0=(Float)S1[15:0] IDC IOC OFC UFC IXC
VCVTF.F16.F32 S0, S1 半精度转单精度,S0=(Float)S1[31:16] IDC IOC OFC UFC IXC

SIMD指令

Cortex-M4只支持在GPR上进行整数的SIMD计算,多用于DSP应用(ADC采样本质是整数,RGB图像也是整数),和Cortex-A核心支持的NEON高级SIMD扩展不是一回事

所有的SIMD指令和浮点指令都是32位长度

在SIMD中数据在GPR中可以有以下几种格式,可以是4个单字节数据或2个半字数据

SIMD数据格式不属于标准C,如果要使用C开发就只能通过CMSIS-DSP使用SIMD功能

指令示例 解释
SADD8 R0, R1, R3 R1中4个有符号8位数和R3一一对应相加存入R0S为前缀。其他可选前缀有U无符号数,SH有符号半值(结果除以2)和UH无符号半值,Q有符号饱和(上限127下限-128)以及UQ无符号饱和(上限255下限0)。饱和运算会更新PSRQ标记位,SU会更新GE[3:0]
SSUB8 R0, R1, R3 4个字节对应相减。可用前缀同上
SADD16 R0, R1, R3 2个半字对应相加。可用前缀同上
SSUB16 R0, R1, R3 2个半字对应相减。可用前缀同上
SASX R0, R1, R3 R0[15:0]=R1[15:0]-R3[31:16], R0[31:16]=R1[31:16]+R3[15:0]。可用前缀同上
SSAX R0, R1, R3 R0[15:0]=R1[15:0]+R3[31:16], R0[31:16]=R1[31:16]-R3[15:0]。可用前缀同上
USAD8 R0, R1, R3 无符号绝对差之和,R0=Abs(R1[7:0]-R3[7:0])+Abs(R1[15:8]-R3[15:8])+Abs(R1[23:16]-R3[23:16])+Abs(R1[31:24]-R3[31:24])
USADA8 R0, R1, R2, R3 无符号绝对差累加,{USADA8 R0, R1, R2, R3}={USAD8 R0, R1, R2}+R3
USAT16 R0, #12, R1 无符号半字饱和,R1保留12位存到R0
SSAT16 R0, #12, R1 有符号半字饱和,保留12位
SEL R0, R1, R2 根据GE[3:0]选择字节,R0[7:0]=GE[0]?R1[7:0]:R2[7:0]

饱和指令

除Cortex-M3支持的整数饱和指令,M4支持更多的饱和指令

指令示例 解释
QADD R0, R1, R3 有符号32位数相加并饱和,会更新Q寄存器
QDADD R0, R1, R3 有符号32位数饱和运算R0=R1*2+R3R1*2饱和或加法饱和都会更新Q寄存器
QSUB R0, R1, R3 有符号32位数相减并饱和,同QADD
QDSUB R0, R1, R3 有符号32位数饱和运算R0=R1*2-R3,同QDADD

乘法与MAC指令

除Cortex-M3支持的乘除法指令,M4也支持额外的乘法以及MAC指令

指令示例 解释
UMAAL R0, R1, R3, R4 无符号乘加,[R1:R0]=R0+R1+R3*R4
SMULBT R0, R1, R2 有符号半字乘法,BT为后缀,R0=R1[15:0]*R2[31:16]。其他可用后缀BBTBTTB表示使用低半字,T表示使用高半字
SMLABT R0, R1, R2, R3 SMULBT累加版,BT为后缀,R0=R1[15:0]*R2[31:16]+R3。符号溢出会置位Q,后缀定义同上
SMULWB R0, R1, R2 有符号半字和字相乘取高32位,B为后缀,R0={R1*R2[15:0]}[47:16]。后缀定义如上
SMLAWB R0, R1, R2, R3 SMULWB累加版,B为后缀,R0={R1*R2[15:0]}[47:16]+R3。符号溢出会置位Q
SMMUL R0, R1, R2 相乘取高32位,R0={R1*R2}[63:32]。另有一个带舍入的SMMULR R0, R1, R2,其中R0={R1*R2+0x80000000}[63:32]
SMMLA R0, R1, R2, R3 SMMUL累加版,R0={R3<<32+R1*R2}[63:32]。带舍入版本SMMLAR R0, R1, R2, R3,其中R0={R1*R2+R3<<32+0x80000000}[63:32]
SMMLS R0, R1, R2, R3 SMMUL累减版,R0={R3<<32-R1*R2}[63:32]。带舍入版本SMMLSR R0, R1, R2, R3,其中R0={R3<<32-R1*R2+0x80000000}[63:32]
SMLALBB R0, R1, R2, R3 有符号半字乘累加,BB为后缀,[R1:R0]=[R1:R0]+R2[15:0]*R3[15:0],后缀定义同SMUL
SMUAD R0, R1, R2 有符号半字乘和,R0=R1[15:0]*R2[15:0]+R1[31:16]*R2[31:16],取低32位。有一个交换版SMUADX R0, R1, R2R0=R1[15:0]*R2[31:16]+R1[31:16]*R2[15:0]。符号溢出更新Q
SMUSD R0, R1, R2 有符号半字乘差,R0=R1[15:0]*R2[15:0]-R1[31:16]*R2[31:16],取低32位。有一个交换版SMUSDX R0, R1, R2R0=R1[15:0]*R2[31:16]-R1[31:16]*R2[15:0]
SMLAD R0, R1, R2, R3 SMUAD的32位累加版,R0=R1[15:0]*R2[15:0]+R1[31:16]*R2[31:16]+R3,交换版SMLADX R0, R1, R2, R3,符号溢出更新Q
SMLSD R0, R1, R2, R3 SMUSD的32位累加版,R0=R1[15:0]*R2[15:0]-R1[31:16]*R2[31:16]+R3,交换版SMLSDX R0, R1, R2, R3,符号溢出更新Q
SMLALD R0, R1, R2, R3 SMLAD的64位完整结果版,[R1:R0]=[R1:R0]+R1[15:0]*R2[15:0]+R1[31:16]*R2[31:16],交换版SMLALDX R0, R1, R2, R3
SMLSLD R0, R1, R2, R3 SMLSD的64位完整结果版,[R1:R0]=[R1:R0]+R1[15:0]*R2[15:0]-R1[31:16]*R2[31:16],交换版SMLSLDX R0, R1, R2, R3

数据打包

除之前的数据转换指令,M4还支持以下指令

指令示例 解释
PKHBT R0, R1, R2, LSL #3 打包两个半字,R0=[R1[15:0]:{R2[31:16]<<3}]
PKHTB R0, R1, R2, ASR #3 打包两个半字,R0=[R1[31:16]:{R2[15:0]>>3}]
SXTB16 R0, R1 有符号字节扩展,R0=[SExt16{R1[23:16]}:SExt16{R1[7:0]}],可加ROR移位,示例SXTB16 R0, R1, ROR #3,以下所有指令中寄存器移位同理
UXTB16 R0, R1 无符号字节扩展,同上
SXTAB R0, R1, R2 有符号字节扩展累加,R0=R1+SExt32{R2[7:0]}
SXTAH R0, R1, R2 有符号半字扩展累加,R0=R1+SExt32{R2[15:0]}
SXTAB16 R0, R1, R2 有符号双字节扩展累加,R0[15:0]=R1[15:0]+SExt16{R2[7:0]}, R0[31:16]=R1[31:16]+SExt16{R[23:16]}
UXTAB R0, R1, R2 SXTAB无符号扩展版
UXTAH R0, R1, R2 SXTAH无符号扩展版
UXTAB16 R0, R1, R2 SXTAB16无符号扩展版

1.7 处理器休眠与低功耗应用

一般MCU都有多种低功耗特性。这些功能常用于需要电池供电的场合,例如可以在中断驱动的程序中使用休眠替代NOP无穷循环(在操作系统中NOP循环一般也被称为CPU的Idle模式)。另外低功耗模式还可以降低干扰,这在无线通信以及高质量ADC采样中有应用

设计Cortex-M的低功耗应用,需要休眠相关指令WFI WFE SEV,配置寄存器SCB->SCR,以及事件寄存器Event Register这三者共同作用

1.7.1 配置寄存器

SCB->SCR寄存器长度32位,定义如下,有3个位

位域 名称 作用
4 SEVONPEND 置1时,异常在挂起时会作为唤醒事件,将事件寄存器置位。中断被屏蔽时唤醒依然有效(因为此时Pending依然会正常置位)
2 SLEEPDEEP 置1时为深度睡眠,唤醒耗时较长。置0为非深度睡眠
1 SLEEPONEXIT 置1时,CPU会在异常返回后自动进入休眠模式。如果此时为中断嵌套那么需要继续执行完其他活跃的异常处理。这是除WFI WFE以外第3种可以进入休眠模式的方法,其相当于自动的WFI指令

ARM建议使用到SLEEPONEXIT特性的应用中特别要注意防范spurious wakeup events,如调试事件导致的唤醒,可以加判断

1.7.2 休眠指令

休眠指令见1.6.8

处理器可以通过WFIWFE这两条指令进入到休眠模式,这些指令属于Hint Instruction,即在绝大多数情况下和NOP一样不会影响程序的运行结果

CPU是在执行这两条指令的过程中停止运行,在唤醒后睡眠指令的执行也就随即结束了。这两条指令的主要区别是WFE依赖于事件寄存器,唤醒事件(Wakeup Event)有所不同

WFESEV指令

在讲解WFE指令之前首先需要说一下事件寄存器Event Register。每一个CPU中都有一个1bit的事件寄存器,且它对于用户不可见不能直接操作(当然多核系统中每个核心都有)。这个寄存器如果置位那么就表示有事件发生

CPU复位会导致事件寄存器复位,WFE指令也会将该寄存器复位

如果有唤醒事件到来,或者触发了异常进入/返回,那么该寄存器会置位

此外,MCU设计者还可以将DMA等外设的信号输出接入到事件寄存器(一般接入到NVIC,功能类似。这样CPU就无须忙等)

对于WFE来说唤醒事件有以下几种

  1. 多CPU系统中其他正在运行的CPU执行了SEV指令(通常情况下一个CPU执行SEV会导致所有CPU的事件寄存器置位并被唤醒)

  2. SEVONPEND置位时,有异常挂起。见下表

  3. 有异常抢占(高于当前优先级)。见下表

  4. 调试事件

指令WFE的行为如下

如果执行WFE时事件寄存器已经置位,那么此时WFE指令会将事件寄存器清除并立即返回继续执行

如果此时事件寄存器未置位,那么就表示可以休眠,CPU会进入到休眠模式

很多时候需要执行2次WFE,第一次清除事件寄存器,第二次进入休眠模式

假设一个CPU被异常唤醒,此时事件寄存器置1,在异常处理中WFE将该位清0。异常退出寄存器再次置1,返回到线程模式中执行3次WFE休眠。一共需要3次WFE

假设一个CPU被异常挂起(该异常已屏蔽)唤醒,此时事件寄存器置1,在线程模式中执行2次WFE休眠

WFI指令

该指令和事件寄存器无关。对于WFI指令来说唤醒事件有以下几种

  1. 复位

  2. 有异常抢占(高于当前优先级)。PRIMASK配置决定是否执行异常处理,见下表

  3. 调试事件

  4. 具体的处理器型号定义的WFI事件

指令WFI的唤醒后行为有以下两种

如果当前有挂起的异常,且该异常可以抢占当前优先级,那么就开始处理这个异常

直接执行之后的代码(一般是调试事件唤醒)

如果使用了SLEEPONEXIT特性,那么程序流程图如下

1.7.3 低功耗应用设计

在一般的应用中,可以使用_WFI()设计中断驱动的应用,使用伪代码如下大致示意

void main() {
    app_setup();
    NVIC_setup();
    SCB->SCR |= 1 << 1; // 退出后休眠
    while(1) {
        _WFI();
    }
}

一般将_WFI()放在一个死循环中,其实就是替代了NOP达到更加节能的效果(一般所说的CPU Idle本质就是NOP死循环)。未屏蔽的中断每隔一段时间会触发,CPU就会被唤醒,结束WFI指令的执行并开始执行异常处理,结束后再次休眠,如此往复循环

然而在有些应用场合中,尤其是异常触发的时间太短或不确定,程序可能面临如下代码示例的问题

timer0_setup();
NVIC_EnableIRQ(Timer0_IRQn);
_WFI();

这段代码想要在定时器0单次定时结束后先执行Timer0的异常处理,之后继续执行_WFI()之后的代码。如果Timer0延时较长那么基本不用担心。但是如果Timer0延时过短,或者在timer0_setup()之后有中断抢占,那么_WFI()开始执行时可能已经错过唯一一次中断,这样程序就会发生异常(这里是永久停止)。如此使用_WFI()是不安全的,即便加上额外的判断也无法保证百分百的安全

volatile int timer0irq_flag;
timer0_setup();
NVIC_EnableIRQ(Timer0_IRQn);
if(timer0irq_flag == 0) {
    _WFI();
}

此时就需要使用基于事件的_WFE()

就像之前说过的,很多时候需要执行2次_WFE()才能使得CPU进入到睡眠模式,第一次将事件寄存器复位。所以可以将以上代码改写如下

volatile int timer0irq_flag;
timer0irq_flag = 0;
timer0_setup();
NVIC_EnableIRQ(Timer0_IRQn);
while(timer0irq_flag == 0) {
    _WFE();
}

这里将判断改为循环,就是为了防范事件寄存器已经置位的情况下无法进入到休眠模式。而如果此时中断已经触发,就会直接跳过循环。使用_WFE()规避了中断触发时刻的不确定性,它可以记忆事件(中断是否触发)。这样即便中断在while()判断条件或_WFE()之间触发也不会导致错过中断无法唤醒

部分Cortex-M3(版本r0p0~r2p0)由于自身缺陷,需要在中断处理中执行_SEV()一下才能正确触发事件寄存器

事件寄存器可以通过_SEV()置1,通过依次执行_SEV(); _WFE()清0

1.8 SysTick定时器与系统控制

1.8.1 SysTick定时器

SysTick长度24位,向下计数。只能在特权模式下访问,可以作为普通定时器使用,在RTOS中作为节拍定时器使用。定时器一共有4个寄存器CTRL LOAD VAL CALIB(CMSIS命名),都可以通过SysTick结构体指针访问,示例SysTick->CTRL

LOADRELOAD)为重新装载值寄存器,VALCURRENT)为当前值寄存器,CALIB为校准值寄存器,CTRL为状态以及控制寄存器

CTRL寄存器定义如下

位域 名称 作用
16 COUNTFLAG 指示位,计数到0时该位置1。向VAL寄存器写入任何值都会导致VAL寄存器清0,同时该位清0。读取CTRL也会导致该位清0
2 CLKSOURCE 置0使用STCLK外部参考时钟,置1使用CPU内核时钟
1 TICKINT 置1会在定时器计数到0时产生异常
0 ENABLE 使能SysTick

CALIB寄存器一般用不上,定义如下

位域 名称 作用
31 NOREF 外部时钟参考指示位,为0表示外部参考时钟可用
30 SKEW 为0表示TENMS中的10mS重载值是准确的
23:0 TENMS 用于10mS延时的标准重载值。为0表示不可用

1.8.2 通过寄存器直接操作SysTick

建议按以下步骤配置定时器

  1. 通过CTRL禁用定时器

  2. SysTick->LOAD重载值(定时周期减1)

  3. SysTick->VAL清0

  4. CTRL启动定时器

1.8.3 系统特性配置与控制CCR

CCR寄存器可以通过SCB->CCR访问,定义如下

位域 名称 作用
9 STKALIGN 置1强制异常压栈对齐双字
8 BFHFNMIGN 默认0,置1在NMI以及HardFault期间忽略BusFault
4 DIV_0_TRP 默认0,置1使能Divide by zero trap,如果SDIVUDIV中出现被0除就会触发异常(UsageFault或HardFault)
3 UNALIGN_TRP 默认0,置1使能非对齐访问trap,此时非对齐访问会触发异常
1 USERSETMPEND 默认0,置1时非特权程序可以写STIR寄存器触发软件中断
0 NONBASETHRDENA 默认0,在有异常活跃时试图进入到线程模式会触发异常。置1允许返回,一般保持默认。有关这个特性之前在PendSV异常中提到过

另外在Cortex-M7核心中还增加了以下几位

位域 名称 作用
18 BP 置1使能分支预测
17 IC 置1使能Icache指令缓存
16 DC 置1使能Dcache数据以及通用缓存

1.8.4 协处理器访问控制CPACR

在Cortex-M4中可用,可以通过SCB->CPACR访问

位域 名称 作用
23:22 CP11 00禁用FPU,11全使能,01仅允许特权访问
21:20 CP10 必须设置和CP11一样

1.8.5 辅助控制寄存器ACTLR

通过SCB->ACTLR访问。不同的Cortex-M核心定义不同,这里的定义适用于M3和M4核心。该寄存器一般复位值为0

位域 名称 作用
2 DISFOLD 置1禁止IT指令重叠。
1 DISDEFWBUF 置1禁止默认背景的写缓冲Buffer
0 DISMCYCINT 置1防止多周期指令被中断,如LDM

Cortex-M4F还有以下位域定义

位域 名称 作用
9 DISOOFP 由于整数指令和浮点指令不使用一条管线,该位置1可以防止整数指令和浮点指令顺序错乱
8 DISFPCA 置1可以禁止CONTROL中的FPCA自动置位(执行FPU指令时不会自动置位)

1.9 内存管理以及MPU

1.9.1 MPU简介

MPU用于配置内存属性,例如访问权限,缓冲缓存等,Cortex-M系列核心中的MPU最多可以配置8片内存区域的属性。一般只存在于一些较为高端的MCU中。Cortex-A系列核心中的MMU相比MPU增加了虚拟地址映射等众多利于操作系统实现的特性

MPU在实际应用中可以提高RTOS的健壮性,例如可以防止用户程序破坏内核使用的栈和内存(例如栈溢出攻击。可以将栈空间最低一片内存设为不可访问),非特权任务访问外设,代码注入攻击等。非法访存会触发MemManageHardFault异常(被屏蔽的情况下)。而不具有MPU的MCU就容易受到上述错误或攻击影响

在具备Cache的高性能MCU中,例如使用了Cortex-M7内核,必须依赖MPU配置内存的缓存属性

默认地址映射见内存章节,ARM官方文档中对于默认的地址映射规定如下

以上地址映射(包括区域属性)在不具有或未使能MPU的CPU中是默认的配置。而在使能MPU的CPU中这个映射可以配置为特权模式下的背景(区域号-1)

在使能MPU后,所有我们想要配置的内存区域属性都必须通过寄存器一个一个处理,最多可以配置8个内存区域。如果此时未使能背景,那么访问未配置的区域一定会抛出异常。所谓背景就是将默认的地址映射(属性)作为特权模式下的默认访问属性,而通过MPU配置的属性会覆盖默认的属性。这样就省去了很多繁琐的配置

XN表示Execute Never即不可执行(取指触发异常),WT表示Write-ThroughWBWA表示Write-Back, write allocate

默认情况下标记为Device以及SOStrongly-ordered强序访问)的地址空间都是不可执行且无缓存,这些地址空间一般用于各种寄存器。而Normal一般用于数据存储器如SRAM,外置RAM,程序Flash以及掩膜ROM等

Device区域如果配置为Shareable就表示这片区域可能同时被几个CPU或DMA访问,反之就表示同时只能有一个CPU或DMA访问。SO区域永远是可共享的

1.9.2 相关寄存器

TYPE寄存器

只读寄存器,用于确定MPU是否存在

位域 名称 作用
23:16 IREGION 8位长,永远为0
15:8 DREGION 8位长,表示支持的区域数量,MPU存在时为8,不存在时为0
0 SEPARATE 永远为0

CTRL寄存器

基本的配置,启用MPU以及背景使能

位域 名称 作用
2 PRIVDEFENA 背景使能(见上),将默认的映射以及属性作为特权模式下的默认访问属性,可以被MPU配置覆盖
1 HFNMIENA 置0在NMIHardFault异常处理模式下禁用MPU
0 ENABLE 置1使能MPU。在此之前需要先设置好内存区域属性

RNR寄存器

可以不使用该寄存器,因为下面的RBAR寄存器功能重复

位域 名称 作用
7:0 REGION 指定想要配置的区域号,因为只支持8个区域所以只有2:0有用

RBAR寄存器

配置想要的区域基址与区域号

位域 名称 作用
31:N ADDR 设置区域的基地址。最多使用高27位,所以分区最小颗粒度为32Byte
4 VALID 置1使用下面的REGION域覆盖RNR寄存器
3:0 REGION 指定想要配置的区域号,0~7

配置的内存区域可以重叠,区域号越大优先级越高

RASR寄存器

位域 名称 作用
28 XN 置1该区域禁止执行(取指)
26:24 AP 设置访问权限
21:19 TEX C B S位共同决定存储器属性
18 S 是否共享(设为1共享后意味着会有其他处理器、DMA访问,尤其在有Cache的系统中还涉及到缓存一致性的问题)
17 C 是否Cache(M3和M4核心没有内置Cache但是还是要正确设置)
16 B 是否Buffer(写入到内存时是否Buffer。和Cache是独立的概念不要混淆)
15:8 SRD 长度8,将区域等分为8个子区域后单独使能或禁止。区域大小小于128B时不可使用
5:1 SIZE 长度5,指定区域大小
0 ENABLE 置1使能该区域

有几个影子寄存器RBAR_AxRASR_Ax,它们其实也映射到RBARRASR,这样设计是方便使用STM等指令一次设定多片区域

区域大小定义如下

AP域定义如下,和XN位共同决定访问权限

TEXS C B定义如下,S位的配置只在部分情况下有效

在Cortex-M3或M4核心的MCU中,内存访问的机构如下。采用这两种核心的主流MCU也很少有配备系统级Cache,但是有Buffer写缓冲,B位的配置还是会对写入产生影响

TEX最高位配置为0时,内部缓存以及外部缓存使用相同的配置。内部缓存指的是CPU内核集成的缓存,例如Cortex-M7就拥有Icache和Dcache内部缓存,而其他核心也有写缓存。外部缓存一般属于系统,可以多个CPU共享(如L2二级缓存)

在Cortex-M3和M4中常用配置如下

由于ROM和Flash一般是只读的,速度和CPU差不多,所以无需开启Buffer,同时设置为不可共用,处理器可以随意读取,可以提高效率

内置SRAM和CPU基本一个速度所以也无需开启Buffer,同时设置为Shareable表示不是独占的(有DMA,也可能有多个CPU核心)

外置RAM由于写入延迟较高所以需要开启Buffer

而设备除了设置为Device以外也可以设置为强序Strongly-ordered

缓存读写行为分为命中和未命中两种情况(参照CSAPP第6章):

Write-ThroughWrite-Back针对的是写命中的情况,Write-Through将数据写入到Cache后再立即写入内存,Write-Back只将数据写入到Cache,只有当替换算法要驱逐这个数据时该数据才会写入到内存

Write allocateNo write allocate针对的是写未命中的情况,此时想要的数据不在Cache里面。Write allocate会将数据先从内存中复制到Cache中再更新Cache中的这个数据,而No write allocate直接绕开Cache将数据写入到内存中

在写入时,一般将Write-BackWrite allocate结合起来使用,这两种操作都只更新了Cache而没有更新内存。Write-ThroughNo write allocate结合起来使用,这两种操作都保证了内存被及时更新,但是会带来更大的总线流量,由于更大的延迟执行也会更慢

读命中的情况下,直接从Cache读取数据即可

Read allocate针对的是读未命中的情况,此时会将数据从内存中复制到Cache再读取

强序Strongly-ordered可以看作是一种特殊的Device,和Device的区别就是禁用了内存写入Buffer,使得写入操作以及之后的程序更加安全,而效率稍低。强序内存在ARMv8中废除

1.9.3 MPU配置步骤

示例

_DMB();
// 禁用MPU
MPU->CTRL = 0;
// 只配置区域0
MPU->RNR = 0;
MPU->RBAR = MPU_CFG_RBAR;
MPU->RASR = MPU_CFG_RASR;
// 禁用剩余的区域
for(i = 1; i < 8; i++) {
    MPU->RNR = i;
    MPU->RBAR = 0;
    MPU->RASR = 0;
}
// 使能MPU,使能背景,同时处理模式禁用MPU
MPU->CTRL = 0x00000005;
_DSB();
_ISB();

1.10 浮点计算

在我们开发的应用中如果使用到了浮点数,编译器会自动根据我们指定的处理器核心类型编译出相对最优化的代码。在Cortex-M3和M0+中所有的浮点计算都使用库函数处理。而在Cortex-M4F中则自动调用单精度浮点指令。在Cortex-M7中自动调用单精度和双精度浮点指令

GCC开发中使用__fp16声明半精度浮点数,float声明单精度,double声明双精度

Cortex-M4设计上将浮点单元的译码和执行单元分开

1.10.1 控制寄存器

Cortex-M4的硬件浮点支持通过CPACR使能,见前

浮点状态寄存器FPSCR定义见前,需要将FPSCR标志位传送到PSR实现条件执行。浮点比较一般使用VCMP,比较结果定义如下表

N Z C V 比较结果
0 1 1 0 相等
1 0 0 0 小于
0 0 1 0 大于
0 0 1 1 Unordered

FPCCR寄存器

浮点上下文控制寄存器,用于控制浮点上下文的惰性压栈等特性,通过FPU->FPCCR访问

位域 名称 作用
31 ASPEN 默认置1,在异常入口处自动清除CONTROL寄存器中的FPCA位,同时将S0S15以及FPSCR压栈。异常退出自动恢复寄存器以及FPCA
30 LSPEN 默认置1,使能异常的惰性压栈,在异常使用到浮点指令时才将浮点寄存器压栈
8 MONRDY 默认置0,禁止挂起调试监控
6 BFRDY 默认置0,禁止挂起总线错误
5 MMRDY 默认置0,禁止挂起内存错误
4 HFRDY 默认置0,禁止挂起硬件错误
3 THREAD 默认置0,表示分配浮点栈帧时为处理模式。置1为线程模式
1 USER 默认置0,表示分配浮点栈帧时为特权模式。置1为非特权模式
0 LSPACT 惰性压栈指示位。1表示当前已经分配浮点栈帧但是未压栈

一般只需要改动LSPEN

FPCAR寄存器

惰性压栈地址寄存器。实际应用基本不会用到

位域 作用
31:3 在惰性压栈上下文中指示S0寄存器压入的地址。栈帧双字对齐所以最低3位无效

FPDSCR寄存器

默认状态设置,该寄存器的设置会在浮点上下文切换时被复制到FPSCR寄存器。实际应用基本不会用到

位域 名称 作用
26 AHP 半精度格式默认设置
25 DN 默认NaN设置
24 FZ 默认清零设置
23:22 RMode 舍入模式默认设置

MVFR0MVFR1寄存器

只读存储器,指示FPU支持的特性

1.10.2 浮点异常

浮点常见的异常和IEEE754中定义的一致。编译器编译出来的代码在大部分情况下会忽略这些异常

此外针对Input Denormal的问题,IDC会置位同时默认将数据替换为0进行计算

1.11 其他核心

1.11.1 Cortex-M0+

Cortex-M0+是常见的M系列核心中门电路数量最少的,常用于低成本、低功耗的MCU中

指令集

Cortex-M0+属于ARMv6-M,仅仅支持少量的32位指令,且不支持除法指令。编写Cortex-M0+汇编代码时注意很多指令只能使用R0~R7,且目标寄存器Rd需要和操作数寄存器之一相同(具体要看指令)

内存访问

Cortex-M0+在区域0xE00000000xEFFFFFFF只支持对齐访问,所以如果想要开发同时适用于ARMv7-M和ARMv6-M的程序,建议强制内存对齐,将SCB->CCR寄存器STKALIGNUNALIGN_TRP位置1

特权等级

Cortex-M0+特权等级配置是可选的,有些不支持非特权模式,需要注意具体MCU产品的配置。但是依然有MSP(R13)PSP两个寄存器

NVIC

Cortex-M0+的NVIC最多仅支持32个外部中断,且只支持4级优先级。同时向量表的重定位是可选的,有些MCU可能不支持重定位

多周期指令

和ARMv7-M的处理器不同,Cortex-M0+的LDMSTM指令被中断后会直接Abandon,异常返回后从头开始执行。而不是存储到PSR中的ICI

此外,Cortex-M0+有2种不同的硬件乘法器配置,一种是单周期阵列乘法器,另一种是32周期时序乘法器。如果使用了32周期乘法器配置,MUL被中断后指令也会在异常返回后从头执行

MPU

Cortex-M0+中的MPU配置寄存器无TEX域,且没有RBARRASR的别名寄存器,不能使用一条指令设置多片区域

1.11.2 Cortex-M7

Cortex-M7是ARMv7-M中性能最高的,采用了双发射顺序流水线设计,支持分支预测,集成了数据Cache和指令Cache(支持ECC),同时具有双精度浮点的指令和硬件支持,可以运行在很高的频率。Cortex-M7核心由于其Cache的存在所以一般需要依赖MPU进行内存管理

1.12 附录

1.12.1 CPUID

CPUID定义如下

1.13 补充:错误异常与处理

在注重高可靠性的控制系统中,仅仅依靠Watchdog或掉电检测Brown-out detection是不够的,很多时候这些外挂机构无法及时复位MCU(有些应用情况下1mS的延迟都会导致致命后果)。恶劣的工况容易导致MCU程序异常,想要及时检测到就需要依赖主动的错误机制

在日常的错误调试中,常用的手段有以下几种:栈跟踪(检查已经压栈的数据,可以在异常处理开头插入一条断点指令),事件跟踪(输出程序运行失败前的异常),指令跟踪(需要ETM,输出失败前的CPU操作)等

在默认情况下,所有错误都触发HardFault异常(异常类型3,优先级-1)。其他错误如MemManageBusFaultUsageFaultHardFault的逻辑关系如下

MemManage的异常类型为4,BusFault异常类型为5,UsageFault异常类型为6

1.13.1 MemManage错误

只要CPU在访问内存时违反了内存的访问权限就会触发内存管理错误

触发MemManage一般有以下几种情况:

  1. 非特权模式下访问特权内存区域

  2. 未使能背景的情况下,访问未经过MPU定义的内存区域(私有外设除外)

  3. 写入只读区域

  4. XN区域取指

所谓内存访问包括但不限于取指,栈操作。异常入口处压栈发生MemManage为压栈错误,异常出口处为出栈错误

1.13.2 BusFault错误

总线错误一般和总线数据传输有关

触发BusFault一般有以下几种情况:

  1. 访问了一片非法的内存区域(例如没有连接任何存储器或寄存器的区域)

  2. 设备未初始化,不允许访问(例如DRAM)

  3. 设备正常工作,但是不支持或不允许该种传输

  4. 非特权模式访问私有外设总线

总线错误可能在访存指令之后立即触发(精确),也可能延迟触发(不精确,写入缓冲Buffer的存在会导致这种不精确的情况)

传统ARM处理器中,取指触发总线异常称为预取终止,读写数据时触发称为数据终止。异常入口出口处被称为压栈出栈错误

1.13.3 UsageFault错误

使用错误的触发一般有以下几种情况

  1. 执行未定义的指令或FPU禁用时执行浮点指令

  2. 执行Cortex-M系列核心中不支持的协处理器指令

  3. 尝试从Thumb切换到ARM

  4. 使用了非法的EXC_RETURN代码,尤其是在有异常处于活跃的状态下,尝试返回到线程模式会触发

  5. 执行STMLDM指令时内存非对齐

  6. 当前优先级高于SVC或相等时执行SVC指令

  7. 异常返回后处理器发现PSR中的ICI位域非0,然而执行的指令不为LDM等指令

  8. CCR寄存器中的DIV_0_TRPUNALIGN_TRP置位时,如果此时发生整数0除或内存的非对齐访问,就会触发UsageFault

1.13.4 HardFault错误

以上3种错误如果未使能,就会触发HardFault。此外,以下情况也会触发硬件错误

  1. 触发异常取向量时遇到总线错误

  2. 调试中,调试器处于连接状态且未使能调试监控异常,执行BKPT断点指令

以上所有错误的使能见SCB中的SHCSR寄存器

1.13.5 错误相关寄存器

以下所有的寄存器都可以通过SCB结构体访问

CFSR寄存器

可配置错误,包含MemManageBusFaultUsageFault的错误状态信息

位域 寄存器 作用
31:16 UFSR UsageFault错误状态
15:8 BFSR BusFault错误状态
7:0 MFSR MemManage错误状态

使用错误状态子区域

位域 名称 作用
25 DIVBYZERO 表示发生了Divide-by-zero,仅在DIV_0_TRP置位时才会触发
24 UNALIGNED 表示发生了非对齐访问,仅在UNALIGN_TRP置位时才会触发
19 NOCP 表示试图执行在Cortex-M中不被允许的协处理器指令
18 INVPC 表示使用了非法的EXC_RETURN
17 INVSTATE 表示试图切换到Cortex-M中不允许的ARM模式
16 UNDEFINSTR 表示试图执行未定义的指令

总线错误状态子区域

位域 名称 作用
15 BFARVALID 表示BFAR中的地址是否有效
13 LSPERR 表示此次为浮点惰性压栈导致的错误
12 STKERR 表示此次为压栈错误
11 UNSTKERR 表示此次为出栈错误
10 IMPRECISERR 表示此次数据访问错误是不精确的,发生错误的数据地址不会存放到BFAR
9 PRECISERR 表示此次数据访问错误是精确的,发生错误的数据地址可以通过BFAR获得,同时发生错误的指令地址可以通过异常中被压栈的PC数值确定
8 IBUSERR 表示此次是取指触发了错误

内存错误状态子区域

位域 名称 作用
7 MMARVALID 表示MMFAR中的地址是否有效
5 MLSPERR 表示此次为浮点惰性压栈导致的内存错误
4 MSTKERR 表示此次为压栈错误
3 MUNSTKERR 表示此次为出栈错误
1 DACCVIOL 表示此次是数据访问触发了内存错误
0 IACCVIOL 表示此次是取指触发了内存错误

HFSR寄存器

包含HardFault的错误状态信息

位域 名称 作用
31 DEBUGEVT 表示由调试事件触发
30 FORCED 表示由MemManageBusFaultUsageFault触发
1 VECTBL 表示由异常触发时的取向量失败触发

DFSR寄存器

包含调试错误状态信息,在调试中调试上位机会访问该寄存器

位域 名称 作用
4 EXTERNAL 表示外部信号(如EDBGRQ)触发调试事件
3 VCATCH 表示异常取向量触发调试事件,CPU可以在进入异常时自动暂停
2 DWTTRAP 表示监视点触发调试事件
1 BKPT 表示断点触发调试事件
0 HALTED 表示CPU已经被调试器暂停

MMFAR寄存器

位域 作用
31:0 MMARVALID为1时,存放导致MemManage错误触发时访问的内存地址

BFAR寄存器

位域 作用
31:0 BFARVALID为1时,存放导致BusFault错误触发时访问的内存地址

AFSR寄存器

辅助错误状态寄存器,不同MCU厂商定义会不一样,用于提供额外的错误信息

1.13.6 锁定

触发锁定的情况包括但不限于以下情况,此时由于错误的优先级低于当前优先级,CPU核心会输出LOCKUP信号,有些MCU厂商会将该信号连接至系统复位

  1. HardFaultNMI的异常处理程序中触发异常

  2. HardFaultNMI异常取向量时触发BusFault(但是异常压栈出栈不会触发锁定)

  3. HardFaultNMI的异常处理程序中试图实行SVC指令

在程序设计中需要尽量避免锁定的发生,一般有以下方法

  1. 尽量将HardFaultNMI异常的处理程序设计的短小精悍,少使用栈,只包含关键任务,其他附加任务可以通过挂起PendSV处理

  2. 检查HardFaultNMI异常的处理程序是否有SVC指令

1.14 补充:DWT

2 Linux环境下STM32开发环境搭建

2.1 交叉工具链:crosstool-NG

工具链可以使用ARM或MCU厂商提供的工具链,也可以自己构建工具链。只想使用现成工具链的到ARM官网或MCU厂商网站下载解压即可,或通过发行版的包管理器安装。这里只讲解自己手动构建工具链的方法

由于自己构建交叉工具链操作较为繁琐,下文基于crosstool-NG自动化工具进行讲解

crosstool-NG是一个专门用于构建交叉工具链的工具,支持arm armeb aarch64 xtensa mips riscv在内的许多架构,平台与ABI,支持Canadian Cross

2.1.1 创建Docker环境

也可以直接在物理机环境下编译构建,可以跳过

我们在容器中构建环境,运行crosstool-NG

基于dockerarchlinux镜像创建容器ct-01,同时创建一个卷vol-ct01挂载到/opt/crosstool-ng

docker create -it --name crosstool-01 -v vol-ct01:/opt/crosstool-ng archlinux:latest

如果想要在宿主机编辑代码,由容器编译代码,最方便的文件共享方法是再创建一个bind mount挂载,--mount type=bind,src=/home/me/work,dst=/work,将宿主机的/home/me/work映射到容器的/work

启动容器并进入shell

docker start crosstool-01
docker attach crosstool-01

可以设置root密码,创建一个普通用户供以后使用

passwd
pacman -S vi sudo
useradd -m username
passwd username
visudo
usermod -a -G wheel username

安装依赖

pacman -S base-devel git help2man python unzip wget audit rsync

/opt/crosstool-ng所有者和组更改为username

chown -R username:username /opt/crosstool-ng/

2.1.2 下载与构建crosstool-NG

切换身份,进入/opt/crosstool-ng/

su username
cd /opt/crosstool-ng/

下载crosstool-NG源码,下载最新版并解压

http://crosstool-ng.org/download/crosstool-ng/

wget http://crosstool-ng.org/download/crosstool-ng/crosstool-ng-1.25.0.tar.xz
tar -Jxvf crosstool-ng-1.25.0.tar.xz
cd crosstool-ng-1.25.0

构建ct-ng本身,只在本目录运行不安装

./configure --enable-local
make

2.1.3 构建安装工具链

ct-ng配置使用图形配置界面

./ct-ng menuconfig

这里我们先不必从头开始配置,查看一下ct-ng官方提供的配置示例

./ct-ng list-samples

arm-unknown-eabi是最适合的配置,看一下信息,并加载该配置,基于该配置进行微调

./ct-ng show-arm-unknown-eabi
./ct-ng arm-unknown-eabi
./ct-ng menuconfig

crosstool-NG配置中可以通过shell中一样的${VAR}方式引用当前的环境变量。除此之外,还有crosstool-NG提供的以下变量

${CT_TARGET} # 工具链目标名称,由4部分组成,例如arm-none-eabi,arm-none-linux-gnueabihf,aarch64-unknown-linux-gnu等
  ${CT_TARGET_ARCH} # 处理器架构,例如aarch64,arm,armeb,avr,riscv32,riscv64,xtensa,mips等
  ${CT_TARGET_VENDOR} # 厂商,例如unknown,none,rpi4等
  ${CT_TARGET_KERNEL} # 内核,不提供linux内核开发支持的工具链没有该项
  ${CT_TARGET_SYS} # ABI
${CT_TOP_DIR} # ct-ng运行的顶层目录。用不上
${CT_VERSION} # ct-ng版本。用不上

配置首页

可能感兴趣的配置项(可以输入/查找相关的配置项)

配置项 变量 解释
Paths and misc options -> Local tarballs directory CT_LOCAL_TARBALLS_DIR 软件包下载目录。如果软件包已存在,就不会再下载
Paths and misc options -> Prefix directory CT_PREFIX_DIR 安装路径,相当于我们在编译软件时在configure中指定的--prefix
Toolchain options -> Tuple's vendor string CT_TARGET_VENDOR 相当于arm-none-eabi中的none,默认unknown,可以自己更改。这里更改为none
Toolchain options -> Tuple's alias CT_TARGET_ALIAS 设置为${CT_TARGET}
Companion Libraries -> newlib-nano -> Additionally install newlib-nano libs into TARGET dir CT_NEWLIB_NANO_INSTALL_IN_TARGET 必须使能

gdb strace ltrace等调试工具需要在Debug Facilities勾选,默认没有勾选,这里勾选gdb即可

需要手动更改的配置文件:

2023.07.29 crosstool-NG version 1.25.0

zlib镜像地址错误需要更改,config/versions/zlib.in:214,更改为http://www.zlib.net/fossils

配置完成后save保存,ct-ng默认使用.config配置文件,可以多保存一份例如arm-none-eabi-01.config,将来想要再使用该配置只要将该文件内容替换到.config即可。退出界面后可以看到配置文件保存在当前目录

开始构建工具链(build.8表示make时使用8进程)

./ct-ng build.8

将安装arm-none-eabi的路径加入到PATH。如果之前没有配置,默认安装到/home/username/x-tools/arm-none-eabi/bin

vim /home/username/.bashrc
export PATH="${PATH}:/home/username/x-tools/arm-none-eabi/bin"

2.1.4 交叉工具链的一些说明

对于一个工具链来说,它一共有4个基本属性:config build host target。其中configbuild分别代指./configure该工具链源码以及make该工具链源码(生成可执行交叉工具链)的机器,绝大部分情况下为同一台机器,可以只看作build一台机器;而host指该交叉工具链运行的机器;target则代指该工具链生成的目标代码运行的平台

如果工具链的build=host=target,该工具链称为native本地工具链

如果工具链的build=host!=target,该工具链称为cross交叉工具链

如果工具链的build!=host!=target,该工具链称为canadian工具链

如果工具链的build!=host=target,该工具链称为cross-native工具链。crosstool-NG将cross-native看作一种特殊的canadian工具链处理

native工具链和cross工具链只需使用crosstool-NG进行一次构建,但是crosstool-NG通常需要不止一次构建gcc编译器(cross工具链),主要是C库和gcc编译器之间存在鸡蛋问题,需要编译两次gcc,使用第一次编译出来的gcc编译新的C库,之后再重新编译一次工具链,该工具链会使用新的C库

cross-nativecanadian需要使用crosstool-NG进行两次构建,第一次构建出在build平台运行,输出host平台代码的交叉工具链,第二次构建出在host平台运行,输出target平台代码的交叉工具链

本教程中我们构建的工具链arm-none-eabi属于cross交叉工具链。而例如mingw64下运行的arm-unknown-linux-gnueabihf工具链就属于canadian工具链,如下

ct-ng x86_64-w64-mingw64
ct-ng build
export PATH=~/x-tools/x86_64-w64-mingw32/bin:$PATH
ct-ng x86_64-w64-mingw64,arm-unknown-linux-gnueabihf
ct-ng build

在使用交叉工具链时,如果是编译使用./configure配置的软件,通常需要通过--host=host-tuple--build=build-tuple分别指定编译出软件的运行平台以及工具链运行的平台。注意这里的--host不再是指工具链运行的平台。这主要适用于arm-none-linux-gnueabihf aarch64-none-linux-gnu等工具链,例如--host=arm-none-linux-gnueabihf --build=x86_64-pc-linux-gnu,这里由于是arm-none-eabi所以暂时不涉及

而另一些仅使用Makefile的软件通常设计为通过CC CROSS_COMPILE CHOST等变量指定使用的工具链,例如CC=arm-none-linux-gnueabihf-gcc CROSS_COMPILE=arm-none-linux-gnueabihf-(编译器前缀) CHOST=arm-none-linux-gnueabihf

2.2 软件库

2.2.1 libopencm3

libopencm3 API Doxygen参考 http://libopencm3.org/docs/latest/html/index.html

如果是要在非STM32F1平台开发,建议慎重考虑libopencm3,目前libopencm3的维护和开发不是很活跃,很多平台缺少例程。建议仍然使用STM32CubeIDE或者Keil等常用IDE

libopencm3克隆到容器中

git clone https://github.com/libopencm3/libopencm3.git

编译libopencm3,生成.a库文件,在/lib

cd libopencm3
make V=1 -j8

libopencm3支持stm32/f0 stm32/f1 stm32/f4 stm32/f7 stm32/l0 stm32/l4 stm32/g0 stm32/g4 stm32/h7 lpc43xx sam/3s sam/3x等许多平台。其中NXP的lpc43xx(Cortex-M4+Cortex-M0异构)分为lpc43xx/m0lpc43xx/m4,分别支持这两个内核的程序开发

libopencm3目录结构:

我们感兴趣的文件基本都位于include/ lib/ scripts/,在include/lib/下,每一个支持的平台都有对应单独的目录。include/中主要为头文件,lib/中有各种内核、外设功能的函数实现

使用make执行构建时,make首先调用一个Python脚本scripts/irq2nvic_h基于各个平台的irq.json文件生成各平台对应的nvic.h vector_nvic.c irqhandler.h头文件。例如include/libopencm3/stm32/f1/irq.json,会对应生成include/libopencm3/stm32/f1/nvic.h(NVIC中断号定义,中断处理函数声明)lib/stm32/f1/vector_nvic.c(中断向量表宏定义,中断处理函数弱符号声明,可以由我们的工程文件重定义覆盖)include/libopencmsis/stm32/f1/irqhandlers.h(中断处理函数宏定义,将工程中函数名从CMSIS命名风格转换为libopencm3风格)共三个文件

生成完以上文件后,make依次调用lib/下各平台(例如lib/stm32/f1)的Makefile,生成平台对应的.a库文件。这些Makefile会调用lib/Makefile.include,基于所有.c源文件编译出.o模块,最后使用arm-none-eabi-ar合并为.a静态库文件

所有平台(不限于STM32或特定CPU内核)共用的模块有vector.o systick.o scb.o nvic.o assert.o sync.o dwt.o,这些模块的源文件位于lib/cm3。其余不通用模块包含外设模块,DMA模块,Flash模块,加速硬件模块等

更多细节可以参考其中的Makefile

2.2.2 FreeRTOS

FreeRTOS API 参考 https://www.freertos.org/a00106.html

下载

wget https://github.com/FreeRTOS/FreeRTOS-LTS/releases/download/202210.01-LTS/FreeRTOSv202210.01-LTS.zip
unzip FreeRTOSv202210.01-LTS.zip

FreeRTOS和一般的主机操作系统有很大差别,它更像是一个提供操作系统功能的软件库。除32位ARM以外,FreeRTOS也可以用于一些8位机平台,例如AVR。也是因此FreeRTOS的基础设计非常精简,同时又具备很强的可扩展性

在我们解压出来的文件中,FreeRTOS最少只需其中的4个.c构建出模块就可以和我们的应用程序链接成完整的FreeRTOS应用了,这四个文件分别为FreeRTOS/FreeRT0S-Kernel下的tasks.c queue.c list.c(头文件位于FreeRTOS/FreeRTOS-Kernel/include)以及FreeRTOS/FreeRTOS-Kernel/portable/GCC下对应平台的port.c(头文件位于同目录)。通常还要加上第5个文件FreeRTOS/FreeRTOS-Kernel/portable/MemMang/heap_x.c(FreeRTOS提供了5种堆)

非LTS版本的FreeRTOS目录文件结构有所不同,并且带有Demo测试样例程序

FreeRTOS中,应用创建的对象都在堆上动态分配内存。从FreeRTOS 9.0.0开始,用户也可以选择自己分配内存。现在常用的内存管理机制有heap_3 heap_4 heap_5以及用户分配的静态内存

timers.c用于提供软件定时器,event_groups.c用于提供事件组功能,stream_buffer.c用于提供消息、数据流缓冲功能。croutine.c是已经淘汰的协程机制,不建议使用

每个FreeRTOS应用都需要有一个FreeRTOSConfig.h配置头文件,这个文件定义了该应用中FreeRTOS开启的功能等配置,示例(https://www.freertos.org/a00110.html)

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

/* Here is a good place to include header files that are required across
your application. */
#include "something.h"

#define configUSE_PREEMPTION                    1
#define configUSE_PORT_OPTIMISED_TASK_SELECTION 0
#define configUSE_TICKLESS_IDLE                 0
#define configCPU_CLOCK_HZ                      60000000
#define configSYSTICK_CLOCK_HZ                  1000000
#define configTICK_RATE_HZ                      250
#define configMAX_PRIORITIES                    5
#define configMINIMAL_STACK_SIZE                128
#define configMAX_TASK_NAME_LEN                 16
#define configUSE_16_BIT_TICKS                  0
#define configTICK_TYPE_WIDTH_IN_BITS           TICK_TYPE_WIDTH_16_BITS
#define configIDLE_SHOULD_YIELD                 1
#define configUSE_TASK_NOTIFICATIONS            1
#define configTASK_NOTIFICATION_ARRAY_ENTRIES   3
#define configUSE_MUTEXES                       0
#define configUSE_RECURSIVE_MUTEXES             0
#define configUSE_COUNTING_SEMAPHORES           0
#define configUSE_ALTERNATIVE_API               0 /* Deprecated! */
#define configQUEUE_REGISTRY_SIZE               10
#define configUSE_QUEUE_SETS                    0
#define configUSE_TIME_SLICING                  0
#define configUSE_NEWLIB_REENTRANT              0
#define configENABLE_BACKWARD_COMPATIBILITY     0
#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 5
#define configUSE_MINI_LIST_ITEM                1
#define configSTACK_DEPTH_TYPE                  uint16_t
#define configMESSAGE_BUFFER_LENGTH_TYPE        size_t
#define configHEAP_CLEAR_MEMORY_ON_FREE         1

/* Memory allocation related definitions. */
#define configSUPPORT_STATIC_ALLOCATION             1
#define configSUPPORT_DYNAMIC_ALLOCATION            1
#define configTOTAL_HEAP_SIZE                       10240
#define configAPPLICATION_ALLOCATED_HEAP            1
#define configSTACK_ALLOCATION_FROM_SEPARATE_HEAP   1

/* Hook function related definitions. */
#define configUSE_IDLE_HOOK                     0
#define configUSE_TICK_HOOK                     0
#define configCHECK_FOR_STACK_OVERFLOW          0
#define configUSE_MALLOC_FAILED_HOOK            0
#define configUSE_DAEMON_TASK_STARTUP_HOOK      0
#define configUSE_SB_COMPLETED_CALLBACK         0

/* Run time and task stats gathering related definitions. */
#define configGENERATE_RUN_TIME_STATS           0
#define configUSE_TRACE_FACILITY                0
#define configUSE_STATS_FORMATTING_FUNCTIONS    0

/* Co-routine related definitions. */
#define configUSE_CO_ROUTINES                   0
#define configMAX_CO_ROUTINE_PRIORITIES         1

/* Software timer related definitions. */
#define configUSE_TIMERS                        1
#define configTIMER_TASK_PRIORITY               3
#define configTIMER_QUEUE_LENGTH                10
#define configTIMER_TASK_STACK_DEPTH            configMINIMAL_STACK_SIZE

/* Interrupt nesting behaviour configuration. */
#define configKERNEL_INTERRUPT_PRIORITY         [dependent of processor]
#define configMAX_SYSCALL_INTERRUPT_PRIORITY    [dependent on processor and application]
#define configMAX_API_CALL_INTERRUPT_PRIORITY   [dependent on processor and application]

/* Define to trap errors during development. */
#define configASSERT( ( x ) ) if( ( x ) == 0 ) vAssertCalled( __FILE__, __LINE__ )

/* FreeRTOS MPU specific definitions. */
#define configINCLUDE_APPLICATION_DEFINED_PRIVILEGED_FUNCTIONS 0
#define configTOTAL_MPU_REGIONS                                8 /* Default value. */
#define configTEX_S_C_B_FLASH                                  0x07UL /* Default value. */
#define configTEX_S_C_B_SRAM                                   0x07UL /* Default value. */
#define configENFORCE_SYSTEM_CALLS_FROM_KERNEL_ONLY            1
#define configALLOW_UNPRIVILEGED_CRITICAL_SECTIONS             1
#define configENABLE_ERRATA_837070_WORKAROUND                  1
#define configUSE_MPU_WRAPPERS_V1                              0
#define configPROTECTED_KERNEL_OBJECT_POOL_SIZE                10
#define configSYSTEM_CALL_STACK_SIZE                           128

/* ARMv8-M secure side port related definitions. */
#define secureconfigMAX_SECURE_CONTEXTS         5

/* Optional functions - most linkers will remove unused functions anyway. */
#define INCLUDE_vTaskPrioritySet                1
#define INCLUDE_uxTaskPriorityGet               1
#define INCLUDE_vTaskDelete                     1
#define INCLUDE_vTaskSuspend                    1
#define INCLUDE_xResumeFromISR                  1
#define INCLUDE_vTaskDelayUntil                 1
#define INCLUDE_vTaskDelay                      1
#define INCLUDE_xTaskGetSchedulerState          1
#define INCLUDE_xTaskGetCurrentTaskHandle       1
#define INCLUDE_uxTaskGetStackHighWaterMark     0
#define INCLUDE_uxTaskGetStackHighWaterMark2    0
#define INCLUDE_xTaskGetIdleTaskHandle          0
#define INCLUDE_eTaskGetState                   0
#define INCLUDE_xEventGroupSetBitFromISR        1
#define INCLUDE_xTimerPendFunctionCall          0
#define INCLUDE_xTaskAbortDelay                 0
#define INCLUDE_xTaskGetHandle                  0
#define INCLUDE_xTaskResumeFromISR              1

/* A header file that defines trace macro can be included here. */

#endif /* FREERTOS_CONFIG_H */

2.3 下载与调试工具

下载硬件使用STLink,或基于STM32F103制作的CMSIS-DAP

下载在物理机进行

2.3.1 ST-Link

可以通过包管理器安装

sudo pacman -S stlink

或手动编译安装

sudo pacman -S base-devel gcc cmake libusb gtk3
git clone https://github.com/stlink-org/stlink.git
cd stlink
make release
sudo make install
sudo ldconfig

stlink套件包含了st-info st-flash st-trace st-util stlink-lib stlink-gui工具,建议可以直接用GUI工具

由于stlink直接写入到MCU的Flash速度极慢,stlink烧录时实际使用了一个flashloader,先将flashloader以及程序映像的一部分放入MCU的SRAM(写入速度快),再启动SRAM中的flashloader将程序映像写入到Flash,之后再将下一块程序映像复制进去,以此重复,速度更快

命令行使用方法

写Flash

st-flash --reset write in.bin 0x8000000
st-flash --reset --format ihex write in.hex

读Flash,4096字节

st-flash read out.bin 0x8000000 4096

可用参数示例

--reset         # 烧录后触发复位(硬件NRST引脚,或AIRCR软复位)
--opt           # 烧录优化,不擦除大于程序文件大小的Flash数据块
--flash=128k    # 设定MCU的Flash大小为128k
--format binary # 烧录的文件格式,可以是binary或ihex
--freq=1.8M     # SWD/JTAG时钟频率,默认1.8M,还可以设定为5k 15k 25k 50k 100k 125k 240k 480k 950k 1.2M 4M

显示信息

st-info --probe

启动st-util并使用gdb调试,默认走4242端口,可以自己更改

st-util -p 4242
arm-none-eabi-gdb app.elf

加载app.elf并运行

(gdb) target extended localhost:4242
(gdb) load
(gdb) continue

重置

(gdb) target extended-remote localhost:4242
(gdb) kill
(gdb) run

gdb写入SRAM还是Flash可以由.elf文件决定,默认写入SRAM链接到0x20000000,写入Flash链接到0x08000000

2.3.2 OpenOCD

OpenOCD可以支持绝大多数SWD、JTAG等接口的调试器,包括CMSIS-DAP,Altera USB Blaster,J-Link,STLink等,并且不限于ARM单片机

可以直接通过包管理安装

sudo pacman -S openocd

看一下openocd哪些文件

pacman -Fl openocd

可以在udev规则中看到支持的调试硬件的信息

cat /usr/lib/udev/rules.d/60-openocd.rules

手动编译安装

sudo pacman -S make libtool pkgconf autoconf automake texinfo libusb hidapi libjaylink libftdi-compat libusb-compat libgpiod
git clone https://github.com/openocd-org/openocd.git
cd openocd
git tag
git reset v0.12.0
./bootstrap
sed -i 's|GROUP="plugdev", ||g' contrib/60-openocd.rules
./configure --prefix=/usr --enable-{amtjtagaccel,armjtagew,buspirate,ftdi,gw16012,jlink,oocd_trace,opendous,osbdm,parport,presto_libftdi,remote-bitbang,rlink,stlink,ti-icdi,ulink,usbprog,vsllink,aice,cmsis-dap,dummy,jtag_vpi,openjtag_ftdi,usb-blaster-2,usb_blaster_libftdi}
make -j8
sudo make install

openocd的操控是基于tcl的(使用的是Jim Tcl)。openocd主要依靠配置文件和用户输入的指令工作,只有以下常用命令行参数

-f <name>       # 指定使用的配置文件
-c <command>    # 运行的Tcl指令

-s <dir>        # 指定查找配置文件和脚本的目录
-d              # 设定debug level为3,加数字-d1表示设定为指定值1,无此参数默认2
-l <name>       # 将日志重定位到文件

示例,使用openocd自带的调试器和开发板配置文件(在/usr/share/openocd/scripts下)。openocd会在3333端口启动一个服务,等待gdb连接;在6666端口启动一个服务,等待tcl连接(RPC);在4444端口启动一个服务,等待telnet连接

sudo openocd -f interface/stlink.cfg -f board/stm32f103c8_blue_pill.cfg

使用telnet连接以后就可以执行tcl指令了,输入help可以查看可用的命令

telnet localhost 4444

烧录命令示例

sudo openocd -s proj/tcl -f interface/cmsis-dap.cfg -c \
"   init;
    halt;
    reset halt;
    flash write_image erase app.hex;
    reset;
    shutdown;
"

2.3.3 CMSIS-DAP制作

基于STM32F103C8T6 BluePill

仓库 https://github.com/RadioOperator/STM32F103C8T6_CMSIS-DAP_SWO.git

build/下,选择F103-DAP-SWO-CDC-BLUEPILL-SWD_PB8PB9.hexF103-DAP-SWO-CDC-BLUEPILL-SWD_REMAP.hex,使用st-flash烧录

st-flash --format ihex write F103-DAP-SWO-CDC-BLUEPILL-SWD_PB8PB9.hex

烧录完成后通过USB连接,lsusb查看。CMSIS-DAP走HID协议

Bus 003 Device 010: ID 0483:572a STMicroelectronics CMSIS-DAP

引脚定义

SWD mode:
SWDIO                PB9
SWCLK                PB8
SWO                  PB7
nRESET               PB6

JTAG mode:
JTMS                 PB9
JTCK                 PB8
JTDO                 PB7
nRESET               PB6
JTDI                 PB5
nTRST                not available

CDC Function:
UART2-TX             PA2
UART2-RX             PA3

3 GNU汇编

先行阅读:ELF文件的结构

4 ld链接器脚本

先行阅读:ELF文件的结构

ld链接器脚本是很重要的通用底层知识

4.1 示例

以下是一个典型的libopencm3工程的ld链接脚本,它分为两部分,前面一部分是在工程文件目录中的,它定义了该MCU平台中Flash和SRAM的地址、长度;而后半部分cortex-m-generic.ld是所有MCU平台通用的,该文件位于-L指定的库搜索目录下,它描述了从.o模块链接出程序映像的通用规则,链接器在执行到INCLUDE时会到该目录下查找指定.ld文件

/**
 *  Define memory regions.
 */

MEMORY
{
	rom (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
 	ccm (rwx) : ORIGIN = 0x10000000, LENGTH = 64K
	ram (rwx) : ORIGIN = 0x20000000, LENGTH = 192K
}

INCLUDE cortex-m-generic.ld
/* Enforce emmition of the vector table. */
EXTERN(vector_table)

/* Define the entry point of the output file. */
ENTRY(reset_handler)

/* Define sections. */
SECTIONS
{
	.text : {
		*(.vectors)	/* Vector table */
		*(.text*)	/* Program code */
		. = ALIGN(4);
		*(.rodata*)	/* Read-only data */
		. = ALIGN(4);
	} >rom

	/* C++ Static constructors/destructors, also used for __attribute__
	 * ((constructor)) and the likes */
	.preinit_array : {
		. = ALIGN(4);
		__preinit_array_start = .;
		KEEP (*(.preinit_array))
		__preinit_array_end = .;
	} >rom
	.init_array : {
		. = ALIGN(4);
		__init_array_start = .;
		KEEP (*(SORT(.init_array.*)))
		KEEP (*(.init_array))
		__init_array_end = .;
	} >rom
	.fini_array : {
		. = ALIGN(4);
		__fini_array_start = .;
		KEEP (*(.fini_array))
		KEEP (*(SORT(.fini_array.*)))
		__fini_array_end = .;
	} >rom

	/*
	 * Another section used by C++ stuff, appears when using newlib with
	 * 64bit (long long) printf support
	 */
	.ARM.extab : {
		*(.ARM.extab*)
	} >rom
	.ARM.exidx : {
		__exidx_start = .;
		*(.ARM.exidx*)
		__exidx_end = .;
	} >rom

	. = ALIGN(4);
	_etext = .;

	/* ram, but not cleared on reset, eg boot/app comms */
	.noinit (NOLOAD) : {
		*(.noinit*)
	} >ram
	. = ALIGN(4);

	.data : {
		_data = .;
		*(.data*)	/* Read-write initialized data */
		*(.ramtext*)    /* "text" functions to run in ram */
		. = ALIGN(4);
		_edata = .;
	} >ram AT >rom
	_data_loadaddr = LOADADDR(.data);

	.bss : {
		*(.bss*)	/* Read-write zero initialized data */
		*(COMMON)
		. = ALIGN(4);
		_ebss = .;
	} >ram

	/*
	 * The .eh_frame section appears to be used for C++ exception handling.
	 * You may need to fix this if you're using C++.
	 */
	/DISCARD/ : { *(.eh_frame) }

	. = ALIGN(4);
	end = .;
}

PROVIDE(_stack = ORIGIN(ram) + LENGTH(ram));

观察如下:

  1. MEMORY命令定义了内存区块,这里命名了rom ram ccm并给出它们的起始地址、长度、属性(rwx)。当然这些区块也可以命名为其他名称,例如flash ROM sram等,体现出这些区块的含义即可,上例中的ccm就是ARM单片机中常见的Core Coupled Memory,只可由CPU内核访问,外设无法访问。内存映射需要查看MCU手册

  2. SECTIONS中每一个节定义格式.section : { } >memory,它定义了.elf文件中这些section的放置顺序,以及.o文件中的节到.elf节的映射关系,即.o文件的哪些section放到.elf的哪个section;同时定义了每个.elf的section应当放到MCU的哪片内存,是否需要执行加载和初始化操作等(AT注解)

  3. 带有等号=的是赋值语句,所有非.变量(例如__preinit_array_start)都是符号,这些符号是可以被我们的C或汇编代码使用的实实在在的符号,例如用于加载器代码中,初始化一段内存。而变量.是一个特殊变量,它的本质就是一个递增的指针,它会随上下文增长,引用它就会获得我们当前在内存空间的位置

  4. 最后使用PROVIDE命令给出了栈底地址_stack,它也是一个符号。由于ARM处理器中栈从高地址向低地址生长,这里将栈直接放到了sram的最高处

使用指定.ld文件时需要在gcc命令行参数中通过-T参数指定

在常见的MCU厂商提供的开发套件通常使用一个.S汇编文件作为起始文件,它的作用类似于一个Bootloader或加载器,会进行系统的初始化,为执行程序做准备。而libopencm3没有采用这种做法,它的初始化与加载功能使用C编写,位于lib/cm3/vector.c,libopencm3的reset_handler就是一个加载器,所有平台通用。当然在libopencm3下用户也可以编写自己的初始化和加载器

上文通过观察示例已经有了基本认识,以下开始对ld链接脚本进行系统性学习。以下内容适用于所有GNU工具链平台,无论是操作系统下还是裸机,也无论处理器类型

4.2 基本概念

在开始之前需要首先理解一些基本概念

从抽象层面上讲,一个可执行的.elf文件主要有两种状态,分为初始化前和初始化后状态。初始化操作主要包括了.data数据的加载,以及.bss的初始化

对于单片机来说,可执行文件映像直接就存放在Flash,和RAM共用一地址空间。将单片机的复位中断handler设为加载器程序(startup.S或libopencm3的vector.c),上电复位后会首先通过加载器程序将Flash中的.data加载到RAM,再将RAM中.bss对应的区块初始化,之后开始执行程序。初始化前就是没有上电的状态,程序在Flash中;而加载后Flash内容不变,但是程序在访问.data .bss时是到RAM中访问,不是原来的位置

对于常见的PC环境来说,可执行文件需要从磁盘等IO设备加载到内存中才能执行。计算机首先将程序文件本身加载到内存(现在的操作系统不这么做,只加载必要部分,通过缺页中断补充),此时才相当于单片机未上电的状态。此后才执行.data .bss的初始化,相当于单片机的初始化程序

.elf文件中每一个section都有自己的VMA(Virtual Memory Address)和LMA(Load Memory Address)属性。VMA就是运行时该section在内存空间(可能为RAM或ROM)的位置,而LMA指代该section在执行初始化程序之前的位置

.ld脚本中基本的地址概念就是RAM地址而不是输出文件中的位置,这些文件中section的位置和顺序和VMA不一定相符

通常只有在单片机中.data需要在运行前转移地址(初始化前位于ROM中),因此.data的LMA和VMA不同,LMA位于ROM而VMA位于RAM,其他section的LMA和VMA相同,例如.bss的实体由于不存在于初始化前的程序所以只需指定VMA即可

4.3 命令格式

每个有意义的.ld文件(指的是进行INCLUDE合并后得到的完整单文件)都会有一条SECTIONS命令定义基本的layout(直接定义VMA,通过AT修饰符间接补充LMA)。而MEMORY命令是可有可无的

命令格式通常为COMMAND { },中间没有:。也有少数命令如OVERLAY会在中间加上一些参数

实际应用中(尤其是MCU应用)SRAM或Flash可能是碎片化、不连续的,也有可能同一片RAM需要分块用于不同的用途

很多单片机中SRAM就分为通用SRAM以及CPU内核专用SRAM(称为ccmtcm),所以需要使用MEMORY预先定义各个内存区块方便后续使用

4.4 注释

ld中注释格式和C源码相同

4.5 变量和赋值

所有含=的语句都是赋值语句(symbol = expression),前面示例中SECTIONSMEMORY命令中都有赋值语句

4.5.1 符号变量和常数

ld脚本中所有的数字常数只能是整数(本质是longunsigned long类型),例如__addr = 0xa11e(16进制),__addr = 4837916(10进制),__addr = 07546(8进制),__size = 1K__size = 1M

所有非常数、非.变量,例如上述__addr __size都是符号变量可以被C或汇编代码使用。C代码中使用extern声明这些变量,类型为funcp_t。这些符号变量的值可能需要到链接完成后才能计算出来。所有这些赋值语句都必须以;结尾

.ld脚本中有两种地方必须在语句末尾加上;,一个是赋值语句,一个是PHDRS命令中的ELF文件头定义后

ld可以支持C中相同的算术运算、位运算、逻辑运算与赋值符号,例如+ - * / % << >> ~ & | == != ! && || += *=等,也可以支持三元运算符?:,例如begin = DEFINED(begin) ? begin : .

4.5.2 Location Counter

特殊变量.只在SECTIONS命令中可用,它被称为Location Counter,是一个VMA地址,在SECTIONS中使用它就可以获取我们当前的VMA位置,它也相当于一个上下文或指针。我们也可以给.赋值,也就是说.可以作为左值使用,此时就会跳转到相应VMA地址,但是注意.不能后退

示例

SECTIONS {
  output : {
    file1(.text)
    . = . + 1000;
    file2(.text)
    . += 1000;
    file3(.text)
  } =0x1234;
}

上述示例中我们在file1 file2 file3中间分别插入了1000字节,这些跳过的VMA使用0x1234填充(这些填充会出现在输出的文件中,也会出现在运行时的内存中)。这里的=0x1234是一个补充的注解

4.5.3 Lazy evaluation

ld采用了lazy evaluation机制进行变量的解析。变量如果不是要被立即使用到,ld不会解析该变量。例如示例rom ram ccmORIGINLENGTH,由于后续的SECTIONS中需要使用到这些信息所以会立即解析。而SECTIONS中出现的有些符号变量在存储分配完成前它们的值是无法确定的,这些赋值表达式在必要信息可用以后才会被解析

4.5.4 赋值语句出现的位置

赋值语句可以出现在SECTIONS命令外,或SECTIONS命令内,或SECTIONS命令内的section定义中。依照它们出现的位置,可以分为absolute和relocatable两种类型

前两种类型的赋值语句是等效的,它们就是absolute类型的赋值,例如我们赋值__length = 0x1234,那么__length就固定为0x1234,在最终链接出的.elf文件中这个符号值就是0x1234

而后一种赋值语句默认是relocatable的,这意味着在这里我们执行__length = 0x1234ld会给__length加上当前section的偏移量,最终__length不是0x1234,如下示例

SECTIONS {
  abs = 0x1234 ;
  .data : {
    __length = 0x1234;
  }
  abs2 = 0x1234 + ADDR(.data);
}

最终__length的值和abs2相等,而不是abs

我们可以在一个section定义中使用ABSOLUTE函数进行absolute赋值

SECTIONS {
  abs = 0x1234 ;
  .data : {
    __length = ABSOLUTE(0x1234);
  }
  abs2 = 0x1234 + ADDR(.data);
}

这里__length值和abs相等

将Location Counter变量.赋值给符号变量时情况比较特殊(前面示例中已经展现过了),实际应用中将这样的赋值设定为上述的relocatable形式是无意义的。所以ld使用了一个特殊设计,在section定义中使用.进行上述赋值时可以无需使用ABSOLUTE(.)ld默认将其视为absolute类型的赋值

SECTIONS {
  .data : {
    *(.data)
    _edata = .;
  }
}

示例中还出现了PROVIDE函数,它的作用也是定义符号并赋值,区别是它用于定义在上面所有命令中都没有定义的符号。格式PROVIDE(symbol = expression)

4.6 算术函数

有如下算术函数可用

用法 定义
ABSOLUTE(exp) 表达式的absolute值,也会将负数转正数。用于SECTIONS命令中的section定义中
ADDR(section) 返回一个section的VMA地址,必须在对应section被定义后使用。用于SECTIONS命令中
LOADADDR(section) 返回一个section的LMA地址,例如一个section使用AT指定了LMA,那么LOADADDR(section)返回结果和ADDR(section)不同
ALIGN(exp) 返回当前.所指VMA地址按exp值的整数倍对齐后的地址,示例ALIGN(4)进行4字节对齐,通常用于section定义中,用法. = ALIGN(4)
DEFINED(symbol) 判断符号变量是否已定义,常和三元运算符一起使用定义符号变量的默认值
NEXT(exp) 作用和ALIGN(exp)大致相同,返回下一个对齐地址。但是在遇到非连续地址时(例如多片非连续SRAM的交界处),NEXT(exp)不会跳过gap。所以很少使用
BLOCK(exp) ALIGN(exp)类似,已很少使用
SIZEOF(section) 返回指定section的大小,只要该section存储已分配完毕的上下文就可以使用
SIZEOF_HEADERS 返回文件头大小(ELF文件头),相当于第一个section的起始位置
MAX(exp1, exp2) MIN(exp1, exp2) 取较大较小值

ADDR(section)示例

SECTIONS {
  .output1 : {
    start_of_output_1 = .;
  }
  .output : {
    symbol_1 = ADDR(.output1);
    symbol_2 = start_of_output_1;
  }
}

上述示例中symbol_1symbol_2值相等

SIZEOF(section)示例

SECTIONS {
  .output : {
    .start = . ;
    .end = . ;
  }
  symbol_1 = .end - .start ;
  symbol_2 = SIZEOF(.output);
}

SIZEOF(section)在当前section定义的末尾就可以使用了

4.7 MEMORY命令

MEMORY命令用于描述系统中可用内存区块的起始地址、长度和属性,该命令为后续的SECTIONS命令提供可用的内存区块(在section定义末尾通过>name指定),格式如下。如果一个内存区块空间不够(定向到该内存区块的内容太多),ld会进行提示

MEMORY {
    name (attr) : ORIGIN = origin, LENGTH = len
}

(attr)有如下可用属性,不区分大小写,可按需组合。

属性 定义
R 可读
W 可写
X 可取指
A 启动时需要分配
I 启动时需要执行初始化
L I
! 反转,例如!X不允许执行

单片机裸机应用中除r w x以外其他不常用

4.8 SECTIONS命令

4.8.1 基本格式

SECTIONS命令定义了所有的section,包括它们的VMA,LMA等,以及输入.o文件的section到输出文件section的映射关系

SECTIONS {
  .section1 : {
    *(.data)
    module1.o(.data)
  }
  .section2 0x1000 : {
    module2.o
  }
  /DISCARD/ : {
    *(.info)
  }
}

section1 section2都是输出section

SECTIONS命令中可以使用一个特殊section/DISCARD/丢弃一些不想要的输入section,这些输入的section内容不会出现在输出文件中

可以在section名称后加上一个整数指定VMA地址,上例中将section2放在了VMA地址0x1000。也可以在section后加上ALIGN(4)强制该section的VMA遵循4字节对齐

用户可以使用ENTRY(entry)定义复位后程序的执行入口(中断向量表中的复位向量),它本质指定的是可执行文件的第一条指令。在libopencm3中这个入口为函数reset_handler,它就是初始化和加载器程序,定义在vector.cENTRY(entry)可以放置在SECTIONS命令里面,也可以放在外面

每一个输出section的定义都有若干匹配项组成,每一个匹配项可以匹配输入的.o模块文件名以及其中的section名。如果一个输出section没有匹配到任何输入,那么它不会出现在最终输出的文件中

每一个匹配项的格式都为filename(section),占用一行,结合*文件名通配符使用,格式示例

匹配项 定义
file.o 将文件file.o整个放到当前section
file.o(section) file.o(section1, section2) file.o(section1 section2) file.o的指定section放到当前section,可以任意数量section
*(section) *(section1, section2) *(section1 section2) 将所有.o文件中的指定section放到当前section,常用
*(COMMON) file.o(COMMON) 将指定文件中未初始化的数据放到该section,通常.bss段中会包含(COMMON),这样的section在文件中不占有实际的空间,但实际占有VMA空间

除此之外,通配符还有?匹配单个任意字符,[a-z] [abc]在指定字符集中匹配单个字符

4.8.2 引入自定义数据

前文符号和赋值我们已经讲述过了符号变量。这些符号都是实体,在输出的文件和运行的程序映像中真实存在,并且可以在C代码中通过extern声明并使用。除此之外,我们还可以向输出文件引入以下自定义数据

额外符号CREATE_OBJECT_SYMBOLS:可以在一个section例如.text中引入,它会为每一个输入的.o文件创建一个符号,其值为来自于该.o文件的第一个字节所在VMA位置。例如我们使用了a.o b.o并将它们链接,如果将输出文件反汇编,我们可以看到符号a.o b.o,它们的值如上所述。不常用

额外数据BYTE(exp) SHORT(exp) LONG(exp) QUAD(exp) SQUAD(exp):分别表示在当前VMA位置放置无符号1字节,2字节,4字节,8字节,有符号8字节数据,它们的值由exp定义。不常用

填充FILL(exp):作用和section定义末尾的=fill基本相同,指定所有未使用VMA区域的填充格式(例如对.赋值时跳过的区域),填充数据取exp低2字节。FILL(exp)只对它后面的未使用区域有效

4.8.3 section可选属性

一个section完整的可选属性示例如下。最初的介绍中所有未出现过的属性都是可选的

SECTIONS {
  section1 0x1000 ALIGN(4) (NOLOAD) : AT (ldadr) {
    contents
  } >region :phdr =fill
}

0x1000指定VMA

ALIGN(4)表示该section的起始VMA遵循4字节对齐

(NOLOAD)表示禁止操作系统加载器加载该section到内存。

AT (ldadr)指定该section的LMA,可以使用一个MEMORY例如AT >rom。此后必须设计一个加载器将该section从指定LMA复制到VMA。正如示例所示,AT也可以放在section定义末尾

>region0x1000一样,同样指定了VMA,区别是region是在MEMORY命令中定义的一片VMA区域,表示将该section放到该片区域。ld设定本区域VMA的顺序是先查看有没有指定具体地址例如0x1000,再看是否指定了>region。如果>region也没有,那么就将该section放在当前.所指VMA

:phdr指定section在运行时映射到的segment,该segment定义于ELF头。使用:NONE表示不映射到任何segment。:phdr通常只在有操作系统的环境下有用

=fill表示填充数据,不再解释

AT示例

SECTIONS {
  .text 0x1000 : {
    *(.text)
    _etext = . ;
  }
  .mdata 0x2000 : AT (ADDR(.text) + SIZEOF(.text)) {
    _data = . ;
    *(.data);
    _edata = . ;
  }
  .bss 0x3000 : {
    _bstart = . ;
    *(.bss)
    *(COMMON)
    _bend = . ;
  }
}

尽管.mdata的VMA为0x2000,但在实际运行前它会从.text的末尾被加载到0x2000

对应的加载器代码示例

char *src = _etext;
char *dst = _data;

/* ROM has data at end of text; copy it. */
while (dst < _edata) {
  *dst++ = *src++;
}

/* Zero bss */
for (dst = _bstart; dst< _bend; dst++)
  *dst = 0;

4.9 OVERLAY命令

OVERLAY命令只能在SECTIONS命令中使用,它可以将指定的不同section映射到同一个VMA地址处,概念类似于C语言中的enum,同一时刻只有其中的一个section出现,使用较少(一般只在操作系统中有用,操作系统只要更改一个地址映射就可以实现该功能)。定义如下

OVERLAY 0x1000 : AT (0x4000) {
  section1 { contents } :phdr =fill
  section2 { contents } :phdr =fill
} >region :phdr =fill

0x1000就是这些section对应的VMA,而AT之后的地址0x4000是所有这些section的LMA起始地址。这些section在LMA按顺序连续摆放

使用OVERLAYld会自动定义LMA符号变量,上例中会生成符号变量__load_start_text0 __load_stop_text0 __load_start_text1 __load_stop_text1

4.10 PHDRS命令

ld会根据需要自动生成ELF文件头,用户也可以通过PHDRS命令指定segments。一般用不上

PHDRS {
  name type FILEHDR PHDRS AT ( address )
    FLAGS ( flags ) ;
}

PHDRS中每一项都是一个segment。FILEHDR参数表示该segment包含ELF文件头,而PHDRS参数表示该segment需要包含ELF program headers

可用type如下

类型 定义
PT_NULL 0 Unused program header
PT_LOAD 1 该header描述了一个需要从文件中加载的segment
PT_DYNAMIC 2 该segment包含了动态链接信息
PT_INTERP 3 该segment包含了解释器的名称
PT_NOTE 4 该segment包含了note
PT_SHLIB 5 保留
PT_PHDR 6 该segment中有program headers

示例

PHDRS {
  headers PT_PHDR PHDRS ;
  interp PT_INTERP ;
  text PT_LOAD FILEHDR PHDRS ;
  data PT_LOAD ;
  dynamic PT_DYNAMIC ;
}

SECTIONS {
  . = SIZEOF_HEADERS;
  .interp : {
    *(.interp)
  } :text :interp
  .text : {
    *(.text)
  } :text
  .rodata : {
    *(.rodata)
  } /* defaults to :text */
  . = . + 0x1000; /* move to a new page in memory */
  .data : {
    *(.data)
  } :data
  .dynamic : {
    *(.dynamic)
  } :data :dynamic
}

4.11 其他命令

命令 作用
FORCE_COMMON_ALLOCATION 强制给COMMON类型的符号分配空间
INPUT(a.o, libp.a) INPUT(a.o libp.a) 引入特定库文件或目标文件
GROUP(libp.a, libq.a) GROUP(libp.a libq.a) 引入特定库文件
OUTPUT(new.elf) 指定输出文件名
SEARCH_DIR(lib) 相当于-L命令行参数
STARTUP(startup.o) 指定特定文件为链接时输入的第一个文件
INCLUDE common.ld 包含其他.ld脚本

5 OpenOCD

5.1 配置

openocd所有的配置文件本质上都是tcl脚本

启动一个openocd服务器时,除了可以使用-c指定要运行的tcl命令以外,也可以自己创建一个tcl脚本(习惯上使用.cfg后缀)并将命令写入到该脚本中,启动openocd时使用-f引用该脚本即可。一个tcl脚本可以调用其他脚本

示例

source [find interface/ftdi/signalyzer.cfg]
# GDB can also flash my flash!
gdb_memory_map enable
gdb_flash_program enable
source [find target/sam7x256.cfg]

等价于

openocd -f interface/ftdi/signalyzer.cfg \
-c "gdb_memory_map enable" \
-c "gdb_flash_program enable" \
-f target/sam7x256.cfg

嵌入式软件设计注意点

可以使用类似#ifdef DEBUG_JTAG这样的宏定义控制代码

看门狗问题:调试过程中会暂停程序的执行,此时无法复位看门狗。有些情况下只能通过宏定义停止看门狗。而有些平台可以支持从openocd停止看门狗,可以在配置文件中应用。这样的宏定义同样在其他场合(例如保护硬件)有用

ARM休眠问题:ARM有WFI指令可以使CPU停止运行,此时便无法再连接JTAG,可以使用宏在调试时禁用含这些指令的代码

延时问题:有些芯片平台在启动后必须由用户添加一段延时以后才能成功连接JTAG,需要使用该宏

DCC(Debug Communications Channel):很多ARM内核支持从JTAG发送信息到主机,需要使用到openocd提供的一个库,可以支持调试信息以及Trace信息

硬件注意点

使用openocd之前一定要确保调试器连接正确,确保和interface的配置文件相符,尤其是JTAG的nTRSTnSRST的上拉下拉问题

确保芯片的启动配置正确

确保存储设备/RAM的映射和配置相符

5.1.1 内置配置

/usr/share/openocd/scripts下的interface board target是我们最感兴趣的配置文件存放目录。interface中存放的都是调试硬件的配置文件,board是各开发板的配置文件,target是芯片平台的配置文件

通常的应用中使用一个interface和一个board(或target,如果只有一个芯片。很多单芯片开发板的配置文件也只不过是source了一下对应的target)配置文件就足以调试开发板了;在我们工程文件中包含一个配置文件,source一下这些配置文件即可。有时我们需要基于内置的配置进行微调,例如设置MCU的Flash容量,也在文件中添加即可

在特定的开发项目中,用户也可以将常用的操作与指令编写成文件,简化步骤,例如从调试器输入U-Boot并执行的操作,可以编写到配置文件中

OpenOCD配置文件的概念就是逐级调用。用户配置文件会调用interfaceboard板级配置文件,而board配置文件又会调用target平台配置文件

5.1.2 配置文件

调试器配置文件

通过如下方式在用户配置中调用调试器配置

source [find interface/jlink.cfg]

find是OpenOCD提供的一个函数,它可以返回配置文件的完整路径

当前,OpenOCD支持的所有调试硬件都有可用的内置配置文件,如果只是使用这些调试器,直接调用即可(但是OpenOCD不保证这些配置文件在我们的平台可用,因为它们都是用户贡献的)

开发板配置文件

通过如下方式在用户配置中调用开发板配置

source [find board/stm32f103c8_blue_pill.cfg]

很多简单的开发板,例如上述的BluePill的配置文件中,只是简单的source [find target/stm32f1x.cfg]并在此之前添加了Flash的地址set FLASH_SIZE 0x20000说明闪存的地址

除此之外,开发板配置中有时还会给出外挂的NOR,NAND的配置,reset复位时对于芯片SDRAM和IO的配置,晶振频率以及PLL配置,调试器的复位配置等。有些开发板附加的外设也需要考虑。芯片平台target配置文件可以使用这些可配置的参数

由于OpenOCD使用的Jim-Tcl不支持标准Tcl的命名空间功能,为防止混乱,OpenOCD规定编写配置文件时所有临时变量以下划线_开头

开发板配置文件不仅和平台配置文件交互,也需要和用户配置文件交互。平台配置文件可能会从开发板配置文件获取全局变量,而开发板配置文件又会调用平台配置文件的函数等

开发板配置文件是在OpenOCD配置阶段执行的,由于此时初始化还不完全,所以可能无法在该文件中使用TAP或平台功能,无法访问寄存器和内存

在许多使用到SDRAM、闪存的开发板(例如使用ARM SoC的开发板)配置文件中,需要使用一个reset-init句柄来替代Bootloader执行初始化操作,例如初始化SDRAM,EMMC,NAND,PLL,看门狗等。该方法会被芯片平台的配置文件使用。而在大部分MCU平台没有这些配置,可能无需设定reset-init

还有一个重要的配置是JTAG的工作时钟频率。在很多ARM平台中,JTAG工作频率和CPU运行频率是挂钩的(最高1/6CPU时钟),通常系统启动后默认运行在一个较低的频率,而为了提高数据传输速度可能需要在启动完成后更改为更高的频率(adapter speed

在板级配置中,OpenOCD建议定义一个init_board函数供Run Stage阶段调用,其中包含外部SDRAM,EMMC,Flash等平台不相关的配置。该函数会在后文所述的init_targets函数之后执行

如果有需要,init_boardinit_targets函数都可以被覆盖,也即用户配置可以覆盖板级配置的init_board,而板级配置可以覆盖平台配置的init_targetsinit_targets中的配置都是该芯片平台可通用的

OpenOCD给出的简单板级配置示例

### board_file.cfg ###
# source target file that does most of the config in init_targets
source [find target/target.cfg]
proc enable_fast_clock {} {
    # enables fast on-board clock source
    # configures the chip to use it
}
# initialize only board specifics - reset, clock, adapter frequency
proc init_board {} {
    reset_config trst_and_srst trst_pulls_srst
    $_TARGETNAME configure -event reset-start {
        adapter speed 100
    }
    $_TARGETNAME configure -event reset-init {
        enable_fast_clock
        adapter speed 10000
    }
}

平台配置文件

平台配置文件通常无需用户更改,用户只需配置好板级和用户级配置即可。这里只是为了对平台配置文件有基本的了解,为板级和用户级配置提供配置思路

通过如下方式在开发板配置中调用平台配置

source [find target/stm32f1x.cfg]

平台配置文件包含了一个芯片平台的默认变量配置,扫描链TAP,CPU目标(gdb支持),CPU/芯片特性配置,以及片上Flash配置

通常最简单的单片机配置文件中,会包含一个TAP,这个TAP是一个支持gdb调试的CPU内核,包含对应的片上Flash等。其他更加复杂的芯片可能会包含多个TAP,通常一个处理器就会有一个TAP

在平台(target)配置文件中,OpenOCD定义了如下几个通用全局变量,可以被开发板配置或用户配置覆盖(在source平台配置之前set赋值),这些变量被平台配置文件使用

全局变量 定义
CHIPNAME 芯片名称,用于TAP ID中,可以设为任何名称。多芯片平台中较为重要(在板级配置中多次source平台文件),需要设置
ENDIAN 芯片的大小端,调试器收发数据需要按该设定处理,默认little。如果大小端固定,无需设定
CPUTAPID 定义合法的JTAG IDCODE寄存器值,OpenOCD检查JTAG链时会请求该寄存器来验证芯片

平台配置文件还会在调用它的配置文件中设定以下变量,这些文件被调用平台配置的配置文件(例如开发板配置文件)使用

变量 定义
_TARGETNAME 提供给调用脚本,对目标芯片进行相应的配置,例如配置reset-init脚本。如果开发板有多个芯片,使用变量_TARGETNAME0 _TARGETNAME1,依次类推

参考OpenOCD给出的平台配置示例开头如下,这是所有平台配置文件都需要包含的定义代码

# Boards may override chip names, perhaps based on role,
# but the default should match what the vendor uses
if { [info exists CHIPNAME] } {
    set _CHIPNAME $CHIPNAME
} else {
    set _CHIPNAME sam7x256
}

# ONLY use ENDIAN with targets that can change it.
if { [info exists ENDIAN] } {
    set _ENDIAN $ENDIAN
} else {
    set _ENDIAN little
}

# TAP identifiers may change as chips mature, for example with
# new revision fields (the "3" here). Pick a good default; you
# can pass several such identifiers to the "jtag newtap" command.
if { [info exists CPUTAPID ] } {
    set _CPUTAPID $CPUTAPID
} else {
    set _CPUTAPID 0x3f0f0f0f
}

平台配置文件中定义的_TARGETNAME,可以看到_TARGETNAME实质上仅仅是在前面定义的_CHIPNAME加上了.cpu后缀。target create命令创建了该调试对象(sam7x256.cpu

set _TARGETNAME $_CHIPNAME.cpu
target create $_TARGETNAME arm7tdmi -chain-position $_TARGETNAME

5.2 OpenOCD服务器配置

5.3 调试器配置

5.4 复位配置

5.5 TAP声明

5.6 CPU配置

5.7 烧录操作

5.8 GDB使用

5.9 FPGA相关操作

5.10 常用命令

5.11 JTAG命令

5.12 边界扫描命令

5.13 其他功能命令

5.14 ARM相关命令

6 GDB终端模式使用

6.1 断点

6.2 程序运行控制

6.3 内存读写