一、STM32F1内核基础知识
1.Cortex-M3 内核结构
STM32F1使用的是属于ARMv7-M架构的ARM Cortex-M3 内核 ,是一个32位的处理器内核,其内部的数据路径是32位的、寄存器是32位的以及存储器接口也是32位的。并且使用了哈佛结构,拥有独立的指令总线和数据总线为数字信号的处理提供了较高的性能。下方是Cortex-M3 内核的简略图。
![](https://i-blog.csdnimg.cn/blog_migrate/3b81cf1cf6ab1bbf10876b560f69d8fe.png)
2.Cortex-M3 内核的指令
Cortex-M3 内核只使用Thumb‐2 指令集,其中既包括16位的指令,又包括32位的指令,这使得在编程时不必再像从前一样去手动切换指令状态(32 位的ARM 状态和 16 位的 Thumb 状态,切换时会消耗额外的切换时间),即可达到性能和存储之间的平横。因为在32位指令下,性能极高;而在16位指令下,代码密度提高了一倍,可以在相同的内存和缓存中存放更多的指令。
为了使芯片结构保持简单情况下进一步提升系统的运行效率,Cortex-M3 内核采用简单的3级流水线。每个指令的执行分为三个步骤:取指令、译码和执行。"取指令"时需要通过指令总线读取存储器中的对应指令,此操作会占用指令总线;"译码"时将指令转换为具体的控制信号,不占用任何总线;"执行"时进行读取寄存器堆栈、操作数在通信移位器中移位、ALU产生相应的运算结果并写到目的寄存器中并根据需求更改状态寄存器条件位,此时占用数据总线。
因为Cortex-M3 内核使用的是哈弗结构,具有单独的数据总线和指令总线,所以在组成3级流水线时,这三个操作并不会相互影响,从而将单个指令的执行时间为3周期,但在单周期内可以有1个指令的吞吐率。单周期指令的3级流水线如下如图所示。
![](https://i-blog.csdnimg.cn/blog_migrate/e95a8ef7b7f40d2f1f299d2005a010f0.png)
3.Cortex-M3 内核的寄存器组
Cortex-M3内核拥有通用寄存器 R0‐R15 以及一些特殊功能寄存器,其中R0-R12属于通用寄存器,但是大多数的16位指令只能使用R0‐R7(低组寄存器),而 32 位的指令则可以访问所有通用寄存器。特殊功能寄存器有预定义的功能,而且必须通过专用的指令来访问。寄存器组如下图所示。
![](https://i-blog.csdnimg.cn/blog_migrate/df70ef32d8f2cae6d06bcce0bbdc601a.png)
3.1 堆栈指针寄存器(MSP和PSP)
R13是堆栈指针寄存器,且Cortex-M3 内核中共有两个堆栈指针,分别是主堆栈指针(MSP)和进程堆栈指针(PSP)。主堆栈指针(MSP)是缺省的堆栈指针,它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用。进程堆栈指针(PSP),用于常规的应用程序代码(不处于异常服用例程中时)。因为有两个堆栈指针,所以Cortex-M3 内核也就支持两个堆栈。当引用 R13(或写作SP)时,你引用到的是当前正在使用的那一个,另一个必须用特殊的指令来访问(MRS,MSR 指令)。而且堆栈指针的最低两位永远是 0,这意味着堆栈总是 4 字节对齐的。
注:在 ARM 编程领域中,凡是打断程序顺序执行的事件,都被称为异常(exception)。在不严格的上下文中,异常与中断也可以混用。
3.2 连接寄存器(LR)
R14是连接寄存器(LR),用于在调用子程序时存储返回地址。
3.3 程序计数器(PC)
R15是程序计数器(PC),用于指示下次欲执行的指令的地址,因为 CM3 内部使用了指令流水线,读 PC 时返回的值是当前指令的地址+4。
3.4 特殊功能寄存器
特殊功能寄存器包含三种功能的寄存器:程序状态寄存器(xPSR或PSR)、中断屏蔽寄存器和控制寄存器(CONTROLl)。他们只能用MSR和MRS指令访问,而且他们也没有寄存器地址。
3.4.1 程序状态寄存器(xPSR或PSR)
程序状态寄存器内部各位定义如下:
![](https://i-blog.csdnimg.cn/blog_migrate/675e57c36ef57120a1b763087573e86d.png)
✦N:正负标志,N=1表示运算结果为负数,N=0表示运算结果为正数或者0。
✦Z:零标志,Z=1表示运算结果为0,Z=0表示运算结果为非0。
✦C:加法运算时表示进位标志,C=1表示产生进位,C=0表示未产生进位;减法运算时表示借位标志,C=0表示产生借位,C=1表示未产生借位;
✦V:溢出标志,V=1表示有溢出,V=0表示无溢出。
✦Q:表示增强的DSP指令是否发生溢出,在ARMv7-M架构的ARM Cortex-M3 内核无定义。
✦ICI/IT[26:25]:暂未找到解释,后期再补上。
✦T:暂未找到解释,后期再补上。
✦ICI/IT[15:10]:暂未找到解释,后期再补上。
✦Exception Number[8:0]:中断号。
3.4.2 中断屏蔽寄存器
中断屏蔽寄存器包含三个寄存器,用于控制异常的使能和除能。只有在特权级下,才允许访问这 3 个寄存器。
✦PRIMASK:此寄存器只有一个位,设置为1时,会关闭所有的可屏蔽异常(中断),只有NMI和硬件异常(fault)可以相应。默认值0。
✦FAULTMASK:此寄存器只有一个位,设置为1时,只有NMI会响应,其他的所有异常(中断)和硬件异常(fault)全部关闭响应。默认值0。
✦BASEPRI:这个寄存器最多有 9 位(由表达优先级的位数决定)。它定义了被屏蔽优先级的阈值。当它被设成某个值后,所有优先级号大于等于此值的中断都被关(优先级号越大,优先级越低)。但若被设成 0,则不关闭任何中断,0 也是默认值。
3.4.3 控制寄存器(CONTROL)
控制寄存器用于定义特权级别,还用于选择当前使用哪个堆栈指针。
位 | 功能 |
CONTROL[1] | 堆栈指针的选择; 0 = 选择主堆栈指针 MSP(复位后缺省值) 1 = 选择进程堆栈指针 PSP 在线程或基础级(没有在响应异常——译注),可以使用 PSP。 在 handler 模式下,只允许使用 MSP,所以此时不得往该位写 1。 |
CONTROL[0] | 0 = 特权级的线程模式; 1 = 用户级的线程模式; Handler模式永远都是特权级的。 |
4.Cortex‐M3 内核的操作模式
Cortex‐M3 支持 2 个模式:处理者模式(handler模式)和线程模式(Thread模式),以及两个特权等级:特权级和用户级。当处理器处在线程状态下时,既可以使用特权级,也可以使用用户级;另一方面,处理者模式总是特权级的。在复位后,处理器进入线程模式+特权级。对应的模式和级别关系如下图所示。
特权级 | 用户级 | |
异常时处理的代码 | 处理者模式 | 错误用法 |
主应用程序的代码 | 线程模式 | 线程模式 |
在特权级下的代码可以通过置位 CONTROL[0]来进入用户级。而不管是任何原因产生了任何异常,处理器都将以特权级来运行其服务例程,异常返回后将回到产生异常之前的特权级。用户级下的代码不能再试图修改 CONTROL[0]来回到特权级。它必须通过一个异常 handler,由那个异常 handler 来修改 CONTROL[0],才能在返回到线程模式后拿到特权级。改变流程如下图所示。
![](https://i-blog.csdnimg.cn/blog_migrate/92ee04787d79c6308be94f4ae56e6ffb.png)
5.Cortex‐M3 内核的异常
Cortex‐M3 内核支持大量异常,包括 11 个系统异常和最多240个外部中断(IRQ),但是外部中断具体用多少个要看具体的MCU的设计。异常说明以及向量表偏移地址,如下表所示。其中异常标号用于分辨当先发生的异常类型,以及根据偏移地址计算中断回调函数的地址。异常编号可以在程序状态寄存器(xPSR或PSR)中获取到。
异常编号 | 类型 | 偏移地址 | 优先级 | 简介 |
0 | N/A | 0x00 | N/A | 异常编号为0表示此时无异常发生。 因为此时无异常,故向量表偏移地址0x00处无中断回调函数,所以此处用来存储MSP(主堆栈指针)的初始值。 |
1 | 复位 | 0x04 | -3(最高) | 复位中断,偏移地址0x04处存放复位中断回调函数地址。 特殊的,此处存储的地址在内核上电复位后自动赋值给PC(程序计数器指针),故程序会首先运行复位中断回调函数。 |
2 | NMI | 0x08 | -2 | 不可屏蔽中断(来自外部NMI输入脚) |
3 | 硬件异常 (hard fault) | 0x0C | -1 | 当被失能的异常触发时将产生硬件异常(hard fault)。 |
4 | 存储器管理异常 (Mem Manage fault) | 0x10 | 可编程 | 存储器管理异常。 MPU 访问犯规以及访问非法位置均可引发。企图在“非执行区”取指也会引发此 fault。 |
5 | 总线异常 | 0x14 | 可编程 | 从总线系统收到了错误响应,原因可以是预取流产(Abort)或数据流产,或者企图访问协处理器 |
6 | 用法异常 | 0x18 | 可编程 | 由于程序错误导致的异常。通常是使用了一条无效指令,或者是非法的状态转换,例如尝试切换到 ARM 状态。 |
7-10 | 保留 | 0x1C-0x28 | N/A | N/A |
11 | SVCall | 0x2C | 可编程 | 执行系统服务调用指令(SVC)引发的异常 |
12 | 调试监视器异常 | 0x30 | 可编程 | 调试监视器发起的请求产生的异常,包括设置的断点,数据观察点,或者是外部调试请求。(注意,异常并不一定是错误,所有打断正常循序执行的事件都称为异常) |
13 | 保留 | 0x34 | N/A | N/A |
14 | PendSV | 0x38 | 可编程 | 为系统设备而设的“可悬挂请求”(pendable request) |
15 | SysTick | 0x3C | 可编程 | 系统滴答定时器(周期性溢出的一个内核级别的定时器) |
16-255 | 外部中断16-255 | 0x40-0x3FF | 可编程 | 外部中断16 到 外部中断255 “外部”指 Cortex‐M3 内核的外部,与stm32的外部中断注意区分。 |
二、STM32F1基础外设知识
1.内存分布
STM32F103x的内存分布如下图所示。左侧部分是 Cortex‐M3 内核给出的可寻址的内存全部地址,框内的说明是STM32F1使用的情况,右侧引出的部分是本系列教程会涉及到的关键内容及地址。在后期文章实际用到时会详细说明。
![](https://i-blog.csdnimg.cn/blog_migrate/656dbb7c93f0a3f4b329a6d7fbbf25f5.png)
2.启动配置
在上面的内存分布小节的图中可以看到地址0x0000 0000 - 0x0007 FFFF处的内存作用是不定的,可以是Flash也可以是系统内存,并且被BOOT引脚设置。接下来将介绍这么做的原因。
因为Cortex-M3内核在启动时始终从代码区0x0000 0000开始,导致在启动时只能从Cortex-M3内核设置的flash启动。而STM32F1系列单片机将开始的0x0000 0000 - 0x0007 FFFF地址空出来,并且通过后期映射的方法实现了从FLASH启动、系统存储器启动以及SRAM启动的功能。
在STM32F10xxx里,可以通过BOOT[1:0]引脚选择三种不同启动模式,如下表所示。
启动模式选择引脚 | 启动模式 | 说明 | |
BOOT1 | BOOT0 | ||
X | 0 | FLASH | FLASH被选为启动区域 |
0 | 1 | 系统存储器 | 系统存储器被选为启动区域 |
1 | 1 | SRAM | SRAM被选为启动区域 |
根据选定的启动模式,FLASH、系统存储器或SRAM可以按照以下方式访问:
● 从FLASH启动:FLASH被映射到启动空间(0x0000 0000),但仍然能够在它原有的地址(0x0800 0000)访问它,即FLASH的内容可以在两个地址区域访问,0x0000 0000或0x0800 0000。
● 从系统存储器启动:系统存储器被映射到启动空间(0x0000 0000),但仍然能够在它原有的地址0x1FFF F000访问它。
● 从内置SRAM启动:只能在0x2000 0000开始的地址区访问SRAM。
3.时钟树与系统结构
对于时钟树的理解其实是很简单的,只要下载个官方配置软件:STM32CubeMX,进入时钟配置界面,可以一目了然的看到各个时钟的走向,各种倍频分频可设置部分非常详尽,每个外设所挂载的总线很容易获取。下图是我翻译后的版本,图很高清,请点击打开后观看。
![](https://i-blog.csdnimg.cn/blog_migrate/efb326d823f5f43de6b77c3f3810355a.png)
在下方的STM32F1系统结构图中可以很清楚的看到各种外设、总线之间的关系,可以获取APB1总线和APB2总线下到底连接着哪些外设。
![](https://i-blog.csdnimg.cn/blog_migrate/0437721a82ac352282533e49cfb0c234.png)
4.GPIO
GPIO全名叫做General-purpose input/output,即多功能(或通用)输入输出引脚,从名称即可知道GPIO口可以设置为多种模式。可设置的模式中共有三大种类:输出模式、输入模式和复用模式。而根据电路结构又可以分为上拉、下拉、浮空、推挽等模式。下表只描述常用功能,复用等其他特殊功能后期用到在详细说。
模式 | 结构 | 全称 |
输入 | 上拉 | 上拉输入 |
下拉 | 下拉输入 | |
浮空 | 浮空输入 | |
模拟 | 模拟输入 | |
输出 | 推挽 | 推挽输出 |
开漏 | 开漏输出 |
每个GPIO端口有7个32位的常用配置寄存器,例如GPIOA口的七个常用寄存器功能和地址如下表所示,其他端口除了地址不同外其他与此完全一致。
寄存器名称 | 地址 | 功能 |
GPIOA_CRL | 0x4001 0600 | 端口配置低寄存器 设置GPIOA 0-7引脚的输入输出模式及IO翻转速度 |
GPIOA_CRH | 0x4001 0620 | 端口配置高寄存器 设置GPIOA 8-15引脚的输入输出模式及IO翻转速度 |
GPIOA_IDR | 0x4001 0640 | 端口输入数据寄存器 读取GPIOA 16个引脚的状态值 |
GPIOA_ODR | 0x4001 0660 | 端口输出数据寄存器 控制GPIOA 16个引脚的输出高低电平,会一次性控制16个引脚状态 |
GPIOA_BSRR | 0x4001 0680 | 端口位设置/清除寄存器 将GPIOA 16个引脚的某些引脚的输出设置为1或清除为0,可单独操作某些引脚 |
GPIOA_BRR | 0x4001 06A0 | 端口位清除寄存器 将GPIOA 16个引脚的某些引脚的输出清除为0,可单独操作某些引脚 |
GPIOA_LCKR | 0x4001 06C0 | 端口配置锁定寄存器 该寄存器用来锁定端口位的配置,设置后在下次系统复位之前将不能再更改端口位的配置 |
三、ARM汇编指令
1.ARM汇编语言基础语法
ARM汇编指令一般较为完整的书写格式如下所示:
{标号}
操作码{执行条件}{S} 目的寄存器,操作数 1,操作数 2, … {;注释}
注:{ }不包含在内,意思为{ }中的内容是可选的。
其中:
①标号是可选的,如果有,它必须顶格写。标号的作用是让汇编器来计算程序转移的地址,类似与C语言里的函数名。
②操作码是指令的助记符,也就是常写的汇编指令,它的前面必须有至少一个空白符,通常使用一个 “Tab” 键来产生。
③操作码紧跟的是执行条件后缀,要紧贴操作码写。可以空着不写,表示无条件执行本指令。执行条件往往配合比较指令来使用。执行条件如下表所示(稍微往后放一下,以防打乱阅读节奏)。
④执行条件后紧跟影响标志后缀:S,如果没有执行条件则紧贴操作码写。S可以空着不写,表示此指令执行的结果不影响程序状态寄存器(xPSR或PSR)的值。但是在使用比较指令时,无需加S即可改变程序状态寄存器(xPSR或PSR)的值。
⑤目的寄存器,指令执行后结果的存放寄存器。
⑥操作数往往会有若干个,不同指令需要不同数目的操作数,并且对操作数的语法要求也可以不同。
注:“x” 表示与之比较的值。
条件码助记符 | 含义 | 使用 程序状态寄存器(xPSR或PSR) 的相关位 |
EQ | = x | Z = 1 |
NE | ≠ x | Z = 0 |
CS | 无符号数 ≥ x | C = 1 |
CC | 无符号数 < x | C = 0 |
MI | x 为负数 | N = 1 |
PL | x 为正数或0 | N = 0 |
VS | 溢出 | V = 1 |
VC | 未溢出 | V = 0 |
HI | 无符号数 > x | C = 1 且 Z = 0 |
LS | 无符号数 ≤ x | C = 0 且 Z = 1 |
GE | 有符号数 ≥ x | N = V |
LT | 有符号数 < x | N ≠ V |
GT | 有符号数 > x | Z = 0 且 N = V |
LE | 有符号数 ≤ x | Z = 1 或 N ≠ V |
AL | 无条件执行 | 忽略 |
为了可以更好的理解,现举几个例子进行说明。其中MOV是数据传送指令,CMP是比较指令(比较指令无需加S后缀便可自动更新程序状态寄存器的标志位)。普通数值前加符号"#“表示为常数数值,在汇编语言中称为"立即数”。
注意:ARM汇编指令可以使用大写字母也可以小写字母,只要统一格式即可,只要有一处用大写字母就要全部用大写字母,反之全部用小写字母。
![](https://i-blog.csdnimg.cn/blog_migrate/7240d65486156439d64330efebfeadeb.png)
- Thumb-2 常用指令
下表仅列出Thumb-2 常用的一些指令,后期用到时再做详细使用说明。
指令助记符 | 描述 | 指令助记符 | 描述 | |
ADC | 带进位加法 | LSR | 逻辑右移 | |
ADD | 加法 | MOV | 数据传送 | |
AND | 逻辑与 | MUL | 乘法 | |
ASR | 算术右移 | MVN | 取反后的数据传送 | |
B | 分支指令 | NEG | 取反 | |
BIC | 逻辑位清除 | ORR | 逻辑或运算 | |
POP | 出栈 | PUSH | 压栈 | |
BL | 带链接的相对分支指令 | BLX | 带交换的分支指令 | |
ROR | 循环右移 | CMN | 相反数比较 | |
SBC | 带借位减法 | CMP | 比较指令 | |
STM | 多寄存器存储 | EOR | 逻辑异或 | |
STR | 单寄存器数据存储 | LDM | 多寄存器加载 | |
SUB | 减法 | LDR | 单寄存器加载 | |
LSL | 逻辑 左移 | SWI | 软中断 | |
BKPT | 断点指令 | TST | 位测试 |
3.常用伪指令
3.1 Thumb的伪指令
Thumb的伪指令不是Thumb指令集中的指令,只是为了编程方便而定义的伪指令,使用时可以像其他的Thumb指令一样使用,但在编译的时候编译器会将其替换为Thumb存在的等效指令。Thumb的伪指令共有三个:ADR、LDR和NOP。其中LDR伪指令与Thumb中的LDR指令的区别是,LDR伪指令的参数中有等号:“=”。其他两个都是新符号。
伪指令助记符 | 格式 | 功能说明 |
ADR | ADR 目标寄存器,地址表达式 | 获取地址值,可以是获取某标号处的地址值 注意:要获取的地址值相对程序计数器(PC)的地址之间的偏移量要为正数并小于1KB。 |
NOP | NOP | 空操作 会被替换为像如 "MOV R0,R0"这样的指令 |
LDR | LDR 目标寄存器,=地址表达式 | 用于加载一个32位立即数或一个地址值到目标寄存器。 注意:如果要加载的常数超过MOV或MVN的范围,则会创建文字池,那么文字池地址值相对程序计数器(PC)的地址之间的偏移量要小于1KB。 |
3.2 ARMCC编译器的伪指令
RAMCC编译器是Keil5软件带有的ARM编译器,RAMCC编译器的伪指令是用于告诉RAMCC编译器如何进行汇编的指令。暂时整理以下常用伪指令,后期有需要在添加和详细说明。
伪指令助记符 | 格式 | 功能说明 |
AREA | AREA 段名 属性1,属性2… | 定义一个代码段或者数据段 |
ENTRY | ENTRY | 汇编程序的入口点 |
END | END | 汇编程序的程序的结尾 |
EQU | 常量名 EQU 常量数值 | 定义一个常量,相当于c语言中的 define |
EXPORT | EXPORT 标号 | 声明一个全局的标号,可以在其他文件使用声明的标号 |
IMPORT | IMPORT 标号 | 通知编译器要使用一个在其他文件中定义的标号,相当于c语言中的 extern |
DCD | DCD 表达式 | 用于分配一片连续的字存储单元并用伪指令中指定的表达式初始化,表达式可以是程序标号或数字表达式 |
PROC | PROC | 表示一个汇编子程序的开始。相当于C语言中函数开始。 |
ENDP | ENDP | 表示一个汇编子程序的结束。相当于C语言中函数结束。 |
四、参考文献
[1]Cotex-M3权威指南(中文版),宋岩(译)
[2]汇编语言程序设计–基于ARM体系结构(第2版),北京航空航天大学出版社。
[3]STM32F103xCDE_DS_CH_V5.pdf