天天看点

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

第十一章汇编LED灯实验​

我们前面分析的启动文件startup_stm32mp15xx.s就是用汇编编写的,它是上电后第一个执行的文件,主要的工作是设置栈指针SP、设置初始PC= Reset_Handler、设置中断向量表入口地址,并初始化向量表、最后跳转到C库中的main,然后进入main的世界,我们也得出结论,main函数并不是第一个被执行的程序。​

如果我们接触Linux、UCOS、FreeRTOS 等操作系统移植,也是会接触到一些汇编的,特别是进行嵌入式 Linux 开发的时候是绝对要掌握基本的 ARM 汇编的,因为 Cortex-A 芯片一上电 SP 指针还没初始化,C 环境还没准备好,所以还不能运行 C 代码,必须先用汇编语言设置好 C 环境,比如初始化 DDR、设置 SP指针等等,当汇编把 C 环境设置好了以后才可以运行 C 代码,所以 Cortex-A 一开始运行的是汇编代码。这点和我们接触的51单片机以及前面分析的Cortex-M也是类似的,关于启动文件startup_stm32mp15xx.s的分析,大家可以看前面第八章的介绍。​

本章节,我们使用汇编语言来编写第一个裸机程序来实现点亮一个LED,通过本章的学习,我们了解如何使用汇编语言来初始化STM32MP157的GPIO相关寄存器以及了解IO口的操作。 本章将分为如下几个小节:​

11.1、STM32MP157 GPIO简介;​

11.2、汇编语言基础;​

11.3、汇编LED灯实验;​

11.1 STM32MP157 GPIO简介​

要通过汇编语言来操作GPIO的寄存器,我们先来了解STM32MP157的GPIO以及相关的寄存器。​

11.1.1 GPIO简介​

GPIO(General-purpose input/output)即通用输入输出端口,它是一个通用可编程IO接口,可供使用者通过程序来控制,可以实现通用输入(GPI)或通用输出(GPO)或通用输入与输出(GPIO)功能。​

在嵌入式系统应用中通常需要控制一些结构简单的外围设备或者电路,这些结构简单的设备或者电路往往只需要开或者关两种状态就可以实现我们想要的功能,例如LED灯的亮和灭,继电器的开和关以及蜂鸣器的发声和关闭等等,此时使用GPIO外接这些设备,使用者可以通过GPIO来控制和读取数字电路中TTL电平的逻辑0和逻辑1,从而可以简单高效地控制设备。​

11.1.2 STM32MP15的GPIO​

STM32MP157的GPIO有GPIOA至GPIOK和GPIOZ共12组GPIO,其中GPIOA~GPIOK每组有16个IO,而GPIOZ有8个IO。所有的GPIO均带有中断功能,所有的GPIO都可以被Cortex-M4和Cortex-A7共享访问,而GPIOZ可用于TrustZone安全性相关的设置(当用于此项时仅限于Cortex-A7访问),相关的外围设备的软件访问被定义为安全性访问,常用于安全解决方案中。这里,STM32MP157共16*5+8=176+8=184个IO,不过正点原子开发板引出144 个通用 GPIO 引脚。​

GPIO​ 位数​ IO个数​ 说明​
GPIOA~GPIOK​ 16bit​ 176​ A7和M4均可以访问​
GPIOZ​ 8bit​ 8​ A7和M4均可以访问(用于安全性设置仅A7访问)​

表11.1.2. 1汇总表​

11.1.3 GPIO功能模式​

STM32MP157的GPIO可以由软件配置成如下 8 种模式中的任何一种:​

1、输入浮空​

2、输入上拉​

3、输入下拉​

4、模拟输入​

5、具有上拉或下拉功能的开漏输出​

6、具有上拉或下拉功能的推挽输出​

7、具有上拉或下拉功能的开漏式复用功能​

8、具有上拉或下拉功能的推挽式复用功能​

每个GPIO引脚都可以通过软件配置为输出(推挽或漏极开路,带或不带上拉或下拉)、输入(带或不带上拉或下拉)或外围设备复用功能。​

我们简单介绍一下上述几种模式:​

  • 输入浮空:即逻辑器件的输入引脚既不做上拉也不做下拉,也就是让引脚什么都不接,浮空着,此时输入引脚上任何的噪声都会改变输入端检测到的电平,检测引脚电平是不定的,有可能检测到高电平,也有可能坚持到低电平。​
  • 输入上拉:逻辑器件的输入引脚通过一个电阻与电源VCC相连,引脚被固定在高电平。​
  • 输入下拉:逻辑器件的输入引脚通过一个电阻与地GND相连,引脚被固定在低电平。​
  • 模拟输入:指逻辑器件的输入引脚输入模拟信号(模拟量),模拟量是未经转化的连续变化量,与数字量对应,通常应用于ADC模拟输入。​
  • 开漏输出:"漏"指的是MOS管的漏极,其输出端相当于三极管的集电极,默认情况下,开漏只能输出低电平,要得到有驱动能力的高电平状态需要加上拉电阻才行。​
  • 推挽输出:推挽输出的结构是由两个三极管或者MOS管受到互补信号的控制,两个管子始终保持一个处于截止,另一个处于导通的状态,此时电路可以真正的输出高电平或者低电平,且两种电平下都有驱动能力(即有输出电流的能力)。​
  • 推挽式复用和开漏式复用:可以理解GPIO不是作为普通的IO口使用,而是被用作第二功能的情况,例如片内外设功能(I2C的SCL和SDA)。推挽和开漏的功能与前面的讲解相同。​

关于这几种模式的电路结构分析,可以参考下面​​11.1.4小节​​的内容。​

11.1.4 GPIO基本结构分析​

我们知道了GPIO有八种工作模式,具体这些模式是怎么实现的?下面我们通过GPIO的基本结构图来分别进行详细分析,先看看总的框图,如下图所示。​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.4. 1的基本结构图​

如上图所示,可以看到右边只有I/O引脚,这个I/O引脚就是我们可以看到的芯片实物的引脚,其他部分都是GPIO的内部结构。​

① 保护二极管​

保护二极管共有两个,用于保护引脚外部过高或过低的电压输入。当引脚输入电压高于VDD时,上面的二极管导通,当引脚输入电压低于VSS时,下面的二极管导通,从而使输入芯片内部的电压处于比较稳定的值。虽然有二极管的保护,但这样的保护却很有限,大电压大电流的接入很容易烧坏芯片。所以在实际的设计中我们要考虑设计引脚的保护电路。​

② 上拉、下拉电阻​

它们阻值大概在30~50K欧之间,可以通过上、下两个对应的开关控制,这两个开关由寄存器控制。当引脚外部的器件没有干扰引脚的电压时,即没有外部的上、下拉电压,引脚的电平由引脚内部上、下拉决定,开启内部上拉电阻工作,引脚电平为高,开启内部下拉电阻工作,则引脚电平为低。同样,如果内部上、下拉电阻都不开启,这种情况就是我们所说的浮空模式。浮空模式下,引脚的电平是不可确定的。引脚的电平可以由外部的上、下拉电平决定。需要注意的是,STM32的内部上拉是一种“弱上拉”,这样的上拉电流很弱,如果有要求大电流还是得外部上拉。​

③ 施密特触发器​

对于标准施密特触发器,当输入电压高于正向阈值电压,输出为高;当输入电压低于负向阈值电压,输出为低;当输入在正负向阈值电压之间,输出不改变,也就是说输出由高电准位翻转为低电准位,或是由低电准位翻转为高电准位对应的阈值电压是不同的。只有当输入电压发生足够的变化时,输出才会变化,因此将这种元件命名为触发器。这种双阈值动作被称为迟滞现象,表明施密特触发器有记忆性。从本质上来说,施密特触发器是一种双稳态多谐振荡器。​

施密特触发器可作为波形整形电路,能将模拟信号波形整形为数字电路能够处理的方波波形,而且由于施密特触发器具有滞回特性,所以可用于抗干扰,其应用包括在开回路配置中用于抗扰,以及在闭回路正回授/负回授配置中用于实现多谐振荡器。​

下面看看比较器跟施密特触发器的作用的比较,就清楚的知道施密特触发器对外部输入信号具有一定抗干扰能力,如下图所示。​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.4. 2比较器的(A)和施密特触发器(B)作用比较​

④ P-MOS管和N-MOS管​

这个结构控制GPIO的开漏输出和推挽输出两种模式。开漏输出:输出端相当于三极管的集电极,要得到高电平状态需要上拉电阻才行。推挽输出:这两只对称的MOS管每次只有一只导通,所以导通损耗小、效率高。输出既可以向负载灌电流,也可以从负载拉电流。推拉式输出既能提高电路的负载能力,又能提高开关速度。​

上面我们对GPIO的基本结构图中的关键器件做了介绍,下面分别介绍GPIO八种工作模式对应结构图的工作情况。​

1. 输入浮空​

输入浮空模式:上拉/下拉电阻为断开状态,施密特触发器打开,输出被禁止。​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.4. 3输入浮空模式​

2. 输入上拉​

输入上拉模式:上拉电阻导通,施密特触发器打开,输出被禁止。​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.4. 4输入上拉模式​

3. 输入下拉​

输入下拉模式:下拉电阻导通,施密特触发器打开,输出被禁止。​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.4. 5输入下拉模式​

4. 模拟功能​

模拟功能:上下拉电阻断开,施密特触发器关闭,双MOS管也关闭。其他外设可以通过模拟通道输入输出。​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.4. 6模拟功能模式​

5. 开漏输出​

开漏输出模式:开漏模式下P-MOS是不工作的(即一直关闭),如果要控制IO口输出0,N-MOS导通,使得输出接VSS。如果要控制输出1,则N-MOS关闭,此时P-MOS和N-MOS都是关闭,引脚呈现高阻态,即不输出高电平也不输出低电平,所以这时要输出高电平就必须接上拉电阻。这时可以用内部上拉电阻,但是不推荐,我们建议接一个外部的上拉电阻。因为如果接内部上拉电阻,具有线与特性,即如果有很多开漏模式的引脚连在一起的时候,只有当所有引脚都输出高阻态,电平才为1,只要有其中一个为低电平时,就等于接地,使得整条线路都为低电平0。​

另外在开漏输出模式下,施密特触发器是打开的,所以IO口引脚的电平状态会被采集到输入数据寄存器中,如果对输入数据寄存器进行读访问可以得到IO口的状态。也就是说开漏输出模式下,我们可以对IO口进行读数据。​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.4. 7开漏输出模式​

6. 推挽输出​

推挽输出模式:推挽输出跟开漏输出不同的是,推挽输出模式P-MOS管和N-MOS管都用上。如果要IO口输出高电平,即输出数据寄存器会往图中“输出控制”中输入高电平时,然后“输出控制”会输出低电平到P-MOS管,则上方的 P-MOS导通,同时“输出控制”会输出低电平到N-MOS管,则下方的 N-MOS 关闭,这时接在P-MOS的VDD与外部引脚连接,对外输出高电平。​

如果要IO口输出低电平,即输出数据寄存器会往图中“输出控制”中输入低电平时,然后“输出控制”会输出高电平到P-MOS管,则上方的 P-MOS关闭,同时“输出控制”会输出高电平到N-MOS管,则下方的 N-MOS 导通,这时接在N-MOS的VSS与外部引脚连接,对外输出低电平。​

当引脚高低电平切换时,两个管子轮流导通,一个负责灌电流,一个负责拉电流,使其负载能力和开关速度都有很大的提高。​

另外在推挽输出模式下,施密特触发器也是打开的,我们可以读取IO口的电平状态。​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.4. 8推挽输出模式​

7. 开漏式复用功能​

开漏式复用功能:一个IO口可以是通用的IO口功能,还可以是其他外设的特殊功能引脚,这就是IO口的复用功能。一个IO口可以是多个外设的功能引脚,我们需要选择作为其中一个外设的功能引脚。当选择复用功能时,引脚的状态是由对应的外设控制,而不是输出数据寄存器。除了复用功能外,其他的结构分析请参考开漏输出模式。​

另外在开漏式复用功能模式下,施密特触发器也是打开的,我们可以读取IO口的电平状态,同时外设可以读取IO口的信息。​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.4. 9开漏式复用功能​

8. 推挽式复用功能​

推挽式复用功能:复用功能介绍请查看开漏式复用功能,结构分析请参考推挽输出模式,这里不再赘述。​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.4. 10推挽式复用功能​

11.1.5 GPIO寄存器介绍​

STM32MP157的每组GPIO端口都有以下寄存器:​

4个32位配置寄存器(MODER, OTYPER, OSPEEDR和PUPDR);​

2个32位数据寄存器(IDR和ODR),1个32位设置/重置寄存器(BSRR);​

1个32位设置/重置寄存器(BSRR);​

1个32位锁定寄存器(LCKR);​

2个32位复用功能选择寄存器(AFRH和AFRL)。​

注:​

为了后面章节描述不被混淆,这里说明一下引脚(Pin)、IO口、IO端口、GPIO端口和端口位的关系。​

引脚就是芯片直接外接的管腿,例如VCC引脚、GND引脚和串口引脚等,它就是芯片外接的一个个的管腿或者管脚;​

端口就是芯片内部(CPU单元)和外部引脚的接口组,一组有好几个引脚,例如80C51单片机的 P0端口有P0.0至P0.7共8个引脚;​

IO口其实就是有输入输出功能的引脚;​

IO端口就是具有输入输出功能的端口;​

GPIO端口英文名字是General-purpose I/Os,I/O后面加了个s,看样子是有很多个IO口,就是通用的输入输出IO端口,例如STM32MP157的GPIOA端口,有PA0至PA15共16个引脚。习惯上,大多数人也称GPIO端口就是IO端口,引脚就是IO口。端口位就是端口的某个位,例如GPIOA端口有16个位(16个引脚)。​

下面我们将带大家理解本章用到的寄存器,没有介绍到的寄存器后面用到的时候会继续介绍。这里主要是带大家学会怎么理解这些寄存器的方法,其他寄存器理解方法是一样的。因为寄存器太多不可能一个个列出来讲,以后基本就是只会把重要的寄存器拿出来讲述,希望大家尽快培养自己学会看手册的能力。​

1. MODER(端口模式控制寄存器)​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.5. 1 MODER寄存器​

MODER用于控制GPIOx(x等于A 至K,和Z,下同)的工作模式,是32位可读可写寄存器,每两位寄存器为一组控制一个IO口(IO口编号为y,y等于0至15,共16个IO)的模式,由软件写入以配置I / O模式为:​

00: 输入模式​

01:通用输出模式​

10:复用功能模式​

11: 模拟模式​

例如控制GPIOA的第0个IO口为输出模式,则配置GPIOA的MODER0为01(即第0位为1,第1位为0)。​

此寄存器的复位值为0xFFFFFFFF,每一位都是1,表示每个IO默认都是模拟模式。​

2. OTYPER(端口输出类型寄存器)​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.5. 2寄存器​

OTYPER用于控制 GPIOx 的输出类型(推挽输出或开漏输出),是32位寄存器,只有低 16 位有效,每一个位控制一个 IO 口,当配置位为0的时候为推挽输出模式,配置位为1的时候为开漏输出模式,复位后,该寄存器值均为 0,即复位后默认为推挽输出模式。该寄存器仅用于输出模式,在输入模式(MODER[1:0]=00)下不起作用。​

3. OSPEEDR(端口输出速度寄存器)​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.5. 3寄存器​

OSPEEDR寄存器用于控制 GPIOx 的输出速度等级, 属于32位可读可写寄存器,每 2 个位控制一个 IO 口,由软件写入以配置I / O速度为:​

00:低速​

01:中速​

10:快速​

11:高速​

该寄存器仅用于输出模式,在输入模式(MODER[1:0]=00)下不起作用。复位后该寄存器值一般为 0,即默认处于低速等级。​

4. PUPDR(端口上拉下拉寄存器)​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.5. 4寄存器​

PUPDR寄存器用于控制 GPIOx 的上拉/下拉,属于32位可读可写寄存器,每 2 个位控制一个 IO 口,由软件写入以配置I O口是上拉或者下拉,复位后,该寄存器值一般为 0,即无上拉,无下拉状态:​

00:无上拉或下拉​

01: 上拉​

10: 下拉​

11: 保留​

5. IDR(端口输入数据寄存器)​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.5. 5寄存器​

IDR用于读取 GPIOx 的输入电平状态,只有低16位有效,属于只读寄存器,每个位控制一个IO口,如果对应的位为 0(IDRy=0,y等于0至15),则说明该 IO口 输入的是低电平,如果是 1(IDRy=1),则表示该IO口输入的是高电平。复位后的低16位状态是未知的。​

6. ODR(端口输出数据寄存器)​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.5. 6寄存器​

ODR寄存器用于控制 GPIOx 输出高低电平,属于可读可写寄存器,只有低16位有效,每个位控制一个IO口,如果对应的位为 0(ODRy=0,y等于0至15),则说明该 IO口 输出的是低电平,如果是 1(ODRy=1),则表示输出的是高电平。​

控制 GPIOx 输出高低电平,也可以通过写BSRR寄存器(x 等于A至F)来分别设置和/或复位ODR位(后面我们会讲解)。ODR寄存器也仅在输出模式下有效,在输入模式(MODER[1:0]=00)下不起作用。​

复位以后,ODR寄存器值默认为0x00000000,表示低16位IO口输出低电平。​

7. BSRR(端口置位/复位寄存器)​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.5. 7寄存器​

BSRR为32位置位/复位寄存器,只写寄存器,如果读取这些寄存器,那么返回值将是0x0000。 对寄存器的高16位写1,对应的ODR位将被复位,所以对应IO口为低电平。对寄存器的低16位写1,对应的ODR位将被置1,对应IO口为高电平。如果写 0,表示无动作,也就是说操作此寄存器写1才有效,写0是无效的。​

简单地说, BSRR的高16位称作清除寄存器,低16位称作设置寄存器。​

前面我们了解了ODR寄存器可以控制GPIOx对应IO口输出高低电平,两种寄存器的比较:ODR寄存器会被中断打断,BSRR 寄存器支持原子操作(原子操作是指操作过程不会被中断打断)。​

复位以后,BSRR寄存器值默认为0x00000000,表示无动作。​

8. BRR(端口清除寄存器)​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.5. 8寄存器​

BRR为位清除寄存器,属于只写寄存器,如果读取这些位将返回值0x0000。BRR只有低16位可用,这低16位与BSRR的高16位具有相同功能,即对低16位写1,对应IO口为低电平,写0表示无效。​

复位以后,BRR寄存器值默认为0x00000000,表示无动作。​

9. LCKR(端口配置锁存寄存器)​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.1.5. 9寄存器​

LCKR寄存器使用的不多,该寄存器用于锁定端口位是否处于可以配置的状态,[31:17]位保留,[16:0]位的值用于配置LOCK键的写入顺序,当在端口的任何位上执行第一个锁定序列后,该位的配置将无法再修改,直到下一次MCU复位或外设复位以后才可以进行修改,即位被锁定了。​

第16位为锁键位,写0表示端口配置锁定键未激活。写1表示端口配置锁定键已激活,LCKR寄存器被锁定,直到下一次MCU复位或外设复位为止。此位的写入序列是写1à写0à写1à读0à读1。​

第15~0位是要锁定的端口位,这些位可读可写,但只能在第16位为0的时候可以写入。​

其中,对某一位写0表示不锁定该位的配置,对某一位写1表示锁定该该位的配置。​

那么,整体的LOCK键写入顺序是:​

WR LCKR[16] = ‘1’ + LCKR[15:0] /* 锁键位写1+[15:0]位的值 */​
WR LCKR[16] = ‘0’ + LCKR[15:0] /* 锁键位写0+[15:0]位的值 */​
WR LCKR[16] = ‘1’ + LCKR[15:0] /* 锁键位写1+[15:0]位的值 */​
RD LCKR    /* 读取LCKR寄存器 */      

复位以后,LCKR寄存器值默认为0x00000000,表示没有锁定端口位。​

关于LCKR寄存器使用的HAL库的相关函数HAL_GPIO_LockPin,后面下面的HAL库跑马灯实验章节会有介绍到。​

GPIO相关的寄存器我们就简单介绍到这里,整体分析一遍寄存器,对我们后面的实验是很有帮助的。​

10. 以上寄存器汇总​

针对以上寄存器的说明,我们汇总出如下表格,方便大家需要的时候查阅。​

寄存器​ 复位值​ 配置操作​ 说明​

MODER​

模式控制寄存器​

0xFFFFFFFF​

00:输入模式​

01:通用输出模式​

10:复用功能模式​

11:模拟模式​

32位可读可写​

OTYPER​

输出类型寄存器​

0x00000000​

0:推挽输出​

1:开漏输出​

低16位可读可写​

输入模式(MODER[1:0]=00)下无效​

OSPEEDR​

输出速度寄存器​

0x00000000​

00:低速​

01:中速​

10:快速​

11:高速​

32位可读可写​

输入模式(MODER[1:0]=00)下无效​

PUPDR​

上拉下拉寄存器​

0x00000000​

00:无上拉或下拉​

01:上拉​

10:下拉​

11:保留​

32位可读可写​

IDR​

输入数据寄存器​

0x0000XXXX​

0:输入低电平​

1:输入高电平​

低16位只读​

ODR​

输出数据寄存器​

0x00000000​

低16位有效​

0:输出低电平​

1:输出高电平​

低16位可读可写​

输入模式(MODER[1:0]=00)下无效​

BSRR​

置位/复位寄存器​

0x00000000​

高16位:​

1:输出低电平​

0:无效​

低16位:​

1:输出高电平​

0:无效​

32位只写​

BSRR 寄存器支持原子操作​

BRR​

清除寄存器​

0x00000000​

低16位:​

1:输出低电平​

0:无效​

低16位只写​

LCKR​

配置锁存寄存器​

0x00000000​

第16位:​

1:激活端口配置锁定键​

0:端口配置锁定键已激活​

低15位:​

1:锁定位​

0:无效​

低16位可读可写​

LCKR寄存器被锁定,直到下一次MCU复位或外设复位为止才可以进行写操作​

表11.1.5. 1GPIO部分寄存器汇总表​

11.1.6 使能GPIO时钟​

如果要使用某个外设,必须先开启其时钟。STM32的每个外设时钟在复位后是默认关闭的,如果要使用外设,则必须先使能外设时钟。STM32中,由RCC块负责整个电路的时钟管理和复位生成,所以我们需要操作RCC相关的寄存器。关于时钟,我们后面会有专门的章节进行讲解,这里先给大家介绍如何开启GPIO时钟。​

GPIO时钟的开启由RCC_MC_AHB4ENSETR寄存器进行控制,如下图,该寄存器的低10位控制GPIOA~GPIOK,对这8位写'1'则使能对应GPIO的时钟,读'1'意味着对应的GPIO时钟已经使能;写'0'是无效的,读取到该位为'0'则表示该位对应的GPIO时钟已经关闭。​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.6.1 RCC_MC_AHB4ENSETR寄存器​

这里只看到GPIOA~GPIOK,那GPIOZ呢?我们知道,GPIOZ挂在AHB5上,所以要看RCC_MC_AHB5ENSETR寄存器,该寄存器各位如下:​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

11.6.2 RCC_MC_AHB5ENSETR寄存器​

我们只需要将第0位设置为1即可开启GPIOZ。​

如果要关闭GPIO的时钟,需要操作RCC_MP_AHB4ENCLRR和RCC_MC_AHB5ENCLRR寄存器即可,将对应的位置1即关闭对应的GPIO。​

11.2 汇编语言基础​

本章节我们学习Cortex-M的汇编语法,这里我们可以参考开发板光盘A-基础资料\4、参考资料下的《ARM Cortex-M3和Cortex-M4处理器权威指南(第三版)》,如果想看中文的可以参考《Cortex-M3权威指南(中文)》,这两个文档中有介绍。​

不同开发环境下的汇编语法是不一样的,因为不同的开发环境使用的编译器不一样,MDK使用的编译器是armcc和armasm,IAR使用的编译器是iar,STM32CubeIDE或者Linux操作系统下使用的是gcc编译器,不同的编译器的库不一样,语法规则会不一样,你会发现,直接将MDK下的汇编代码拷贝到STM32CubeIDE下编译的话就会报错(MDK目前也可以安装gcc编译器了),将MDK下的汇编代码拷贝到IAR下编译也会报错。等我们学习Linux操作系统的时候,还会接触到更多的编译器,例如arm-linux-gnueabihf-gcc和arm-none-linux-gnueabi-gcc等。本章节我们是在MDK下开发的,所以要使用使用ARM汇编器的语法,如果是在使用gcc编译器的IDE中开发的话,就要使用GNU 汇编器的语法(GNU开发的gcc编译器)。​

在之后的学习之路上我们还会遇见汇编,例如,我们接触UCOS、FreeRTOS 等操作系统移植,也是会接触到一些汇编的,后期想学习Uboot和Linux内核的同学,少不了接触汇编。大家听到汇编千万不要气馁,在Linux操作系统开发中其实也主要是以C语言为基础的,接触的汇编只是少部分,而汇编部分也主要是在芯片上电后的初始化工作,很少涉及到复杂的代码,使用的指令也都比较简单,掌握了常见的指令也就基本看懂这段代码了,一般芯片厂商已经为我们做好了这步操作,不需要我们自己去编写。既然芯片厂商已经为我们做好了这步操作,为什么我们还要去学习呢?因为掌握这方面的内容对我们后期的学习和理解以及调试很有帮助。下面我们来学习基本的汇编语法吧。​

11.2.1 ARM 汇编语法​

对于ARM汇编(适用于Keil、DS-5等控制器开发套件)指令格式如下:​

标号​

操作码 操作数1,操作数2, … ; 注释      

如下的一段代码,Start就是标号,LDR、ORR、STR是操作码,R0、R1及其后面的是操作数。​

Start​
;1、设置RCC_MC_AHB4ENSETR寄存器,使能GPIOI时钟​
 LDR R0, =RCC_MC_AHB4ENSETR​
 LDR R1, [R0]  ;读取RCC_MC_AHB4ENSETR寄存器的值到R1​
 ORR R1, #(1 << 8) 置1,使能GPIOI时钟​
 STR R1, [R0]  ;写入到RCC_MC_AHB4ENSETR寄存器      

对以上的指令格式做如下讲解:​

1)标号表示地址位置,是可选的,如果有标号,写的时候必须顶格来写。标号的作用是让汇编器来计算程序转移的地址,通过中的更标号可以得到指令的地址。注意的是,标号也可以表示数据的地址。​

2)操作码是指令的助记符,也就是指令的名称,操作码的前面必须要有一个空白符,可以按下键盘的Tab键来产生空白符。​

3)操作码后面跟着的是多个操作数,操作数是参与某种功能操作的数据。操作数有三种方式提供:立即数、寄存器存放的数据、内存中的数据。对于存储器读指令,第1个操作数是数据被加载进去的寄存器;对于存储器写指令,第1个操作数是待写入存储器的数据的寄存器;对于数据处理指令,第1个操作数是操作的目的。​

指令中操作数的个数取决于指令的种类,不同的指令需要不同的操作数。​

4)这里我们说一下立即数。立即数也就是​​立即寻址方式​​指令中给出的数,即写在指令里的常数,且立即数必须以“#”开头。汇编语言规定:立即数不能作为指令中的第一操作数(目的操作数),我们举个例子:​

MOV R0, #0x12 ; 设置R0=0x12(十六进制)​
MOV R1, #’A’  ; 设置R1= ASCII 码字符A      

以上代码中“;”号表示注释,注释可以提高代码的可读性,且不会影响程序的运行。MOV就是操作码(也叫助记符),R0是操作数,0x12是立即数,其前面必须要有“#”开头。​

11.2.2 定义常量或者比较指令​

1. EQU指令​

汇编中使用EQU来定义常量,一般指令的格式是:​

符号名 EQU 表达式  ;左边的符号名表示右边的表达式      

表达式可以是一个数字常量或者一串字符。这里是将左边的符号名表示右边的表达式,如果要使用右边的表达式,直接写左边的符号名即可,这个和我们C语言中常用的#define类似。这里注意的是,符号名不能和其它的符号名重复,也不能重新被定义,EQU不会给符号名分配存储空间。​

我们以代码举例子介绍:​

PERIPH_BASE  EQU (0x40000000)​
MCU_AHB4_PERIPH_BASE  EQU (PERIPH_BASE + 0x10000000)      

以上代码中,用符号PERIPH_BASE表示0x40000000。​

2. CMP指令​

用于比较两个数的指令CMP,用于把一个寄存器的内容和另一个寄存器的内容或一个立即数进行比较,同时更新CPSR中条件标志位的值。指令将第一操作数减去第二操作数,但不存储结果,只更改条件标志位。​

CMP R1, R0 ;做R1-R0的操作​
CMP R1,#10 ;做R1-10的操作      

11.2.3 存储器访问指令​

ARM 不能直接访问存储器,比如 RAM 中的数据,这个时候就要借助存储器访问指令,一般先将要配置的值写入到 Rx(x=0~12)寄存器中,然后借助存储器访问指令将 Rx 中的数据写入到STM32MP1的寄存器中,读取STM32MP1寄存器也是一样的,只是过程相反。常用的存储器访问指令有两种:LDR 和STR。​

1. LDR指令​

LDR指令的格式为:​

LDR{条件}目的寄存器,<存储器地址>​
LDR{条件} 目的寄存器,=立即数​
 ;指令常见的格式就是:​
LDR Rx, [Rn, #offset] ;从地址 Rn+offset 处读取一个字到 Rx      

LDR操作的数据类型是32位的,即1个字。LDR指令用于从存储器中将一个32位的字数据加载到目的寄存器(Rx)中,或者将一个立即数加载到目的寄存器(Rx)中。这里注意的是,当加载的是立即数时,使用“=”,而不是“#”,如下示例:​

LDR R0,=0x123456 ;将立即数0x123456传送给寄存器R0(将R0设置为0x123456)      

在嵌入式开发中,LDR 最常用的就是读取 CPU 的寄存器值,比如 STM32MP157有个寄存器 GPIOI_ODR,其绝对地址为0x5000A014,我们现在要读取这个寄存器中的数据,示例代码如下:​

LDR R0, =0x5000A014 ;将寄存器地址0x5000A014加载到R0中,即R0=0x5000A014​
LDR R1, [R0]    ;读取地址 0x5000A014 中的数据到R1寄存器中      

上述代码就是读取寄存器GPIOI_ODR中的值,读取到的寄存器值保存在 R1 寄存器中,​

上面代码中 offset 是 0,也就是没有用到 offset。​

LDR指令可以全地址范围内跳转,当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。如下,表示将PC指针指向复位中断,PC指针是程序运行的位置,PC指针在哪里,程序就在那里执行。​

LDR PC, =Reset_Handler      

下面我们列举LDR使用的例子:​

LDR R0,[R1] ;从地址为R1处读取一个字到R0​
LDR R0,[R1,R2]   ;从地址为R1+R2处读取一个字到R0​
LDR R0,[R1,#8]  ;从地址为R1+8处读取一个字到R0​
;从地址为R1+R2处读取一个字到R0,并将新地址R1+R2写入R1​
LDR R0,[R1,R2]!​
;从地址为R1+8处读取一个字到R0,并将新地址R1+8写入R1 ​
LDR R0,[R1,#8]!​
;从地址为R1处读取一个字到R0,并将新地址R1+R2写入R1​
LDR R0,[R1],R2 ​
;从地址为R1+R2×4处读取一个字到R0,并将新地址R1+R2×4写入R1 ​
LDR R0,[R1,R2,LSL#2]!​
; 从地址为R1处读取一个字到R0,并将新地址R1+R2×4写入R1​
LDR R0,[R1],R2,LSL#2      

2. STR指令​

STR指令用于从源寄存器中将一个32位的字数据存储至存储器中。该指令在程序设计中比较常用。STR指令的格式为:​

STR{条件}源寄存器,<存储器地址>​

;指令常见的格式:​
STR Rx, [Rn, #offset] ;把Rx中的低字存储到地址 Rn+offset 处      

我们以STM32MP157的寄存器 GPIOI_ODR为例,其绝对地址为0x5000A014,现在我们要将一个数据0x1写入到GPIOI_ODR中,代码如下:​

LDR R0, =0x5000A014 ;将寄存器地址 0x5000A014 加载到R0中,即R0=0x5000A014​
LDR R1, =0x1    ;R1保存要写入到寄存器的值,即R1=0x1​
STR R1, [R0] ;将R1中的值写入到R0中所保存的地址中,也就是GPIOI_ODR的值为0x1      

Cortex-M3和Cortex-M4支持许多存储器访问指令,以上的LDR和STR针对的数据是32位的,还有8位、16位以及有符号和无符号位数据类型的存储器访问指令,只需要在后面加上B或者H,如下表:​

数据类型​ 加载(读存储器)​ 存储(写存储器)​
8位无符号​ LDRB​ STRB​
8位有符号​ LDRSB​ STRB​
16位无符号​ LDRH​ STRH​
16位有符号​ LDRSH​ STRH​
32位​ LDR​ STR​
多个32位​ LDM​ STM​
双字(64位)​ LDRD​ STRD​

表11.2.1存储器访问指令​

11.2.4 处理器内部数据传输指令​

处理器做的最多事情就是在处理器内部来回的传递数据,数据传送类型包括:​

①两个寄存器间传送数据(这里的寄存器是指通用寄存器)​

②将数据从一个寄存器传递到特殊寄存器,如CONTROL、PRIMASK、CPSR 和 SPSR​

③寄存器与存储器间传送数据​

④将立即数传递到寄存器​

数据传输常用的指令有三个:MOV、MRS 和 MSR。在寄存器间传送数据的指令是MOV,如果有特殊寄存器,就会涉及MRS和MSR这两个指令,这三个指令的用法如下表所示:​

指令​ 目的​ 源​ 描述​
MOV​ R0​ R1​ 将 R1 里面的数据复制到 R0 中​
MRS​ R0​ CPSR​ 将特殊寄存器 CPSR 里面的数据复制到 R0 中​
MSR​ CPSR​ R1​ 将 R1 里面的数据复制到特殊寄存器 CPSR 里中​

表11.2.4.1常用数据传输指令​

1. MOV指令​

MOV 指令用于将数据从一个寄存器拷贝到另外一个寄存器,或者将一个立即数传递到寄​

存器里面,使用示例如下:​

MOV R0,R1 ;将寄存器 R1 中的数据传递给 R0,即 R0=R1​
MOV R0, #0X12  ;将立即数 0X12 传递给 R0 寄存器,即 R0=0X12      

2. MRS指令​

MRS 指令用于将特殊寄存器(如 CPSR 和 SPSR)中的数据传递给通用寄存器,要读取特殊寄存器的数据只能使用 MRS 指令!使用示例如下:​

MRS R0, CPSR ;将特殊寄存器 CPSR 里面的数据传递给 R0,即 R0=CPSR      

3. MSR指令​

MSR 指令和 MRS 刚好相反,MSR 指令用来将普通寄存器的数据传递给特殊寄存器,也就是写特殊寄存器,写特殊寄存器只能使用 MSR,使用示例如下:​

MSR CPSR, R0 ;将 R0 中的数据复制到 CPSR 中,即 CPSR=R0      

11.2.5 压栈和出栈指令​

我们通常会在 A 函数中调用 B 函数,当 B 函数执行完以后再回到 A 函数继续执行。要想在跳回 A 函数以后代码能够接着正常运行,那就必须在跳到 B 函数之前将当前处理器状态保存起来(就是保存 R0~R15 这些寄存器值),当 B 函数执行完成以后再用前面保存的寄存器值恢复R0~R15 即可。保存 R0~R15 寄存器的操作就叫做现场保护,恢复 R0~R15 寄存器的操作就叫做恢复现场。在进行现场保护的时候需要进行压栈(入栈)操作,恢复现场就要进行出栈操作。​

1. PUSH和POP指令​

压栈的指令为 PUSH,出栈的指令为 POP,PUSH 和 POP 是一种多存储和多加载指令,即可以一次操作多个寄存器数据,他们利用当前的栈指针 SP 来生成地址,PUSH 和 POP 的用法如表11.2.5.1所示:​

指令​ 描述​
PUSH <reg list>​ 将寄存器列表存入栈中​
POP <reg list>​ 从栈中恢复寄存器列表​

表11.2.5.1压栈和出栈指令​

汇编里把一段内存空间定义为一个栈,栈是先进后出的,且由高到低使用的。新压入的数据的位置更低。SP指向新压入栈的数据的位置。可以将栈比作一堆叠放的盘子,出栈就是取出顶端的盘子,顶端的盘子是最后入栈的,而进栈(压栈)就是把盘子叠放到顶端,所以栈是先进后出的。假如我们现在要将 R0~R3 和 R12 这 5 个寄存器压栈,假设当前的 SP 指针指向 0X80000000地址,使用的汇编代码如下:​

PUSH {R0~R3, R12} ;将 R0~R3 和 R12 压栈      

压栈完成以后的堆栈如图11.2.5.1所示:​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.2.5.1 R0~R3和R12压栈完成​

此时的SP指向了0X7FFFFFEC。假如我们现在要再将 LR 进行压栈,汇编代码如下:​

PUSH {LR}  ;将 LR 进行压栈      

对 LR 进行压栈完成以后的堆栈模型如图 11.2.5.2所示:​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.2.5.2 LR压栈完成​

以上的过程是分两步对 R0~R3,R2 和 LR 进行压栈以后的堆栈模型,如果我们要出栈的话,使用如下代码:​

POP {LR}    ;先恢复 LR​
POP {R0~R3,R12}  ;再恢复 R0~R3,R12      

栈是先进后出的,出栈的就是从栈顶,也就是 SP 当前执行的位置开始,地址依次减小来提取堆栈中的数据到要恢复的寄存器列表中。​

2. STMFD和LDMFD指令​

PUSH 和 POP 的另外一种写法是“STMFD SP!”和“LDMFD SP!”, 因此上面的汇编代码可以改为:​

STMFD SP!,{R0~R3, R12}   ;R0~R3,R12 入栈​
STMFD SP!,{LR}     ;LR 入栈​

LDMFD SP!, {LR}     ;先恢复​
LDMFD SP!, {R0~R3, R12}   ;再恢复      

STMFD和LDMFD这两个指令一般用于进行程序搬移等大规模操作前的cpu现场保护和操作结束后的现场恢复,属于非单一连续的压栈和出栈。STMFD 可以分为两部分:STM 和 FD,同理,LDMFD 也可以分为 LDM 和 FD。​

FD是Full Descending 的缩写,即满递减的意思。STM 和 LDM是多存储和多加载,可以连续的读写存储器中的多个连续数据。根据 ATPCS 规则,ARM 使用的 FD 类的堆栈,SP 指向最后一个入栈的数值,堆栈是由高地址向下增长的,也就是前面说的向下增长的堆栈,因此最常用的指令就是 STMFD 和 LDMFD。STM 和 LDM 的指令寄存器列表中编号小的对应低地址,编号高的对应高地址。​

11.2.6 跳转指令​

转指令用于实现程序流程的跳转,在 ARM 程序中有两种方法可以实现程序流程的跳转:​

①使用专门的跳转指令,如B、BL、BX 等。​

②直接向程序计数器 PC 写入跳转地址值。​

述两种方法都可以完成跳转操作,但是一般常用的还是 B、BL 或 BX,下面我们来了解这两种方法。​

跳转指令B、BL 或 BX描述如下表11.2.6.1所示:​

指令​ 描述​
B <label>​ 跳转到 label,如果跳转范围超过了+/-2KB,可以指定 B.W<label>使用 32 位版本的跳转指令, 这样可以得到较大范围的跳转​
BX <Rm> ​ 间接跳转,跳转到存放于 Rm 中的地址处,并且切换指令集​
BL <label> ​ 跳转到标号地址,并将返回地址保存在 LR 中。​
BLX <Rm>​ 结合 BX 和 BL 的特点,跳转到 Rm 指定的地址,并将返回地址保存在 LR 中,切换指令集。​

表11.2.6.1跳转指令​

1. B指令​

B 指令是最简单的跳转指令,B 指令会将 PC 寄存器的值设置为跳转目标地址,一旦遇到一个 B 指令,ARM 处理器将立即跳转到给定的目标地址,从那里继续执行。B 指令的格式为:​

B{条件}目标地址​

存储在跳转指令中的实际值是相对当前PC 值的一个偏移量,而不是一个绝对地址,它的值由汇编器来计算。如果要调用的函数不会再返回到原来的执行处,那就可以用 B 指令,如下示例:​

LDR SP, =SystemInit ; 设置栈指针​
B R0     ; 程序无条件跳转到SystemInit函数      

上述代码只是一部分,设置SP指向SystemInit函数,然后使用B指令跳转到SystemInit函数处。​

2. BL指令​

BL在跳转之前,会在寄存器R14 中保存PC 的当前内容,因此,可以通过将R14 的内容重新加载到PC 中来继续从跳转之前的代码处运行,该指令是实现子程序调用的一个基本但常用的手段。如果跳转后的代码执行完毕,还需要再返回来直接执行之前跳转前的位置处继续执行,这个时候就不能直接使用B 指令了,因为 B 指令一旦跳转就再也不会回来了,这个时候要使用 BL 指令。​

BL Label ;当程序无条件跳转到标号 Label 处执行时,同时将当前的 PC 值保存到 R14 中      

3. BX指令​

BX 指令跳转到指令中所指定的目标地址,目标地址处的指令既可以是ARM 指令,也可以是Thumb指令。BX 指令的格式为:​

BX{条件}目标地址​

4. BLX 指令​

BLX 指令的格式为:​

BLX 目标地址​

BLX 指令从ARM 指令集跳转到指令中所指定的目标地址,并将处理器的工作状态由ARM 状态切换到Thumb 状态,该指令同时将PC 的当前内容保存到寄存器R14 中,子程序的返回可以通过将寄存器R14 值复制到PC 中来完成。。因此,当子程序使用Thumb 指令集,而调用者使用ARM 指令集时,可以通过BLX 指令实现子程序的调用和处理器工作状态的切换。​

11.2.7 算术运算指令​

汇编中也可以进行算术运算,比如加减乘除,例如加法常用的指令是ADD和ADC:​

(1)加法指令 ADD将源操作数和目的操作数相加,结果送到目标操作数:​

ADD CL,20H ;CL←CL+20H      

(2)带进位加法指令ADC具有进位标志CF,其格式和功能和ADD相似,只不过ADC在求和运算时加入了CF位:​

;当CF=1​
MOV AL,7EH​
ADC AL,0ABH ;执行完之后AL=2AH=7EH+0ABH+1且CF=1      

(3)这里再介绍一个位清除指令:BIC,指令格式:​

;语法格式:​
Rd, Rn, Oprand2​
;举例:​
BIC R0, R0,  #0xF0000000  ;将 R0高4位清零​
BIC R1, R1, #0x0F  ;将R1低4位清0      

BIC(位清除)指令对 Rn 中的值 和 Operand2 值的反码按位进行逻辑“与”运算​

算术运算常用的运算指令用法如表11.2.7.1所示:​

指令​ 计算公式​ 描述​
ADD Rd, Rn, Rm​ Rd = Rn + Rm​ 加法运算,指令为 ADD​
ADD Rd, Rn, #immed​ Rd = Rn + #immed​
ADC Rd, Rn, Rm​ Rd = Rn + Rm + 进位​ 带进位的加法运算,指令为 ADC​
ADC Rd, Rn, #immed​ Rd = Rn + #immed +进位​
SUB Rd, Rn, Rm​ Rd = Rn – Rm​ 减法​
SUB Rd, #immed​ Rd = Rd - #immed​
SUB Rd, Rn, #immed​ Rd = Rn - #immed​
SBC Rd, Rn, #immed​ Rd = Rn - #immed – 借位​ 带借位的减法​
SBC Rd, Rn ,Rm​ Rd = Rn – Rm – 借位​
MUL Rd, Rn, Rm​ Rd = Rn * Rm​ 乘法(32 位)​
UDIV Rd, Rn, Rm​ Rd = Rn / Rm​ 无符号除法​
SDIV Rd, Rn, Rm​ Rd = Rn / Rm​ 有符号除法​

表11.2.7.1常用运算指令​

11.2.8 逻辑运算指令​

C语言中会用到的逻辑运算符,如与或非“&”、“|”、“!”,汇编也有逻辑运算符,如表11.2.8.1所示:​

指令​ 计算公式​ 描述​
AND Rd, Rn​ Rd = Rd &Rn​ 按位与​
AND Rd, Rn, #immed​ Rd = Rn &#immed​
AND Rd, Rn, Rm​ Rd = Rn & Rm​
ORR Rd, Rn​ Rd = Rd | Rn​ 按位或​
ORR Rd, Rn, #immed​ Rd = Rn | #immed​
ORR Rd, Rn, Rm​ Rd = Rn | Rm​
BIC Rd, Rn​ Rd = Rd & (~Rn)​ 位清除​
BIC Rd, Rn, #immed​ Rd = Rn & (~#immed)​
BIC Rd, Rn , Rm​ Rd = Rn & (~Rm)​
ORN Rd, Rn, #immed​ Rd = Rn | (#immed)​ 按位或非​
ORN Rd, Rn, Rm​ Rd = Rn | (Rm)​
EOR Rd, Rn​ Rd = Rd ^ Rn​ 按位异或​
EOR Rd, Rn, #immed​ Rd = Rn ^ #immed​
EOR Rd, Rn, Rm​ Rd = Rn ^ Rm​

表11.2.7.8逻辑运算指令​

常用的ARM汇编指令还有很多,例如用于比较两个数的指令CMP、​

11.2.9 其它指令​

汇编语言结束符是以END结尾的,此外我们常常会看到如下代码:​

AREA |.text|, CODE, READONLY, ALIGN=2​
 THUMB​
 EXPORT Start      

AREA用于定义一个代码段或数据段,段名需用“|”括起来,如|.text|,使用格式如下:​

AREA 段名 属性1 ,属性2 ,……      

属性表示该代码段(或数据段)的相关属性,多个属性用逗号分隔,属性介绍如下:​

— CODE 属性:用于定义代码段,默认为READONLY 。 ​

DATA 属性:用于定义数据段,默认为READWRITE 。 ​

READONLY 属性:指定本段为只读,代码段默认为READONLY 。 ​

READWRITE 属性:指定本段为可读可写,数据段的默认属性为READWRITE 。​

— ALIGN 属性:ALIGN=n表示2的n次方字节对齐。​

— COMMON 属性:该属性定义一个通用的段,不包含任何的用户代码和数据。各源文件中同名的COMMON段共享同一段存储单元。​

THUMB表示Thumb 代码;​

THUMB 、AREA 、EXPORT、ALIGN等指令我们前面在启动文件的时候已经分析过,大家可以返回前面第八章节启动文件介绍部分的内容。​

关于汇编的语法就介绍到这里了,掌握了本章节提到的语法就基本可以操作本章实验了。汇编指令有很多,我们也不能一一记住,实际使用中可以用到哪个就查询哪个。​

11.3 汇编LED灯实验​

本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\3、M4裸机驱动例程\ MP157-M4 HAL库V1.2\实验0 汇编LED灯实验。​

11.3.1 LED灯简介​

LED(Light Emitting Diode Light)又名发光二极管,是一种把电转化为光的半导体器件。LED 灯工作电流很小,一般在0至15mA之间,亮度随电流的增大而变亮。​

不同材料的发光二极管可以直接发出红、黄、蓝、绿、青、橙、紫、白色的光,下图是可以发出黄、红、蓝三种颜色的直插型二极管实物图,这种二极管长的一端是阳极,短的一端是阴极。​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.3.1.1发光二极管​

下图是开发板上用的贴片二极管实物图。贴片二极管的正面一般都有颜色标记,有标记的那端就是阴极。​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.3.1.2贴片二极管​

多个发光二极管封装在一起可以组合成LED数码管,例如显示数字8的7段数码管是由7个二极管组成,8段数码管比7端数码管多了一个二极管,显示一个点。数码管有共阴和共阳两种接法,如下图,前者通常称为共阴数码管,后者为共阳数码管。共阳极的接法是发光二极管的阳极接高电平,共阴极的接法是发光二极管的阴极接地。​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.3.1.3共阳和共阴极​

二极管具有单向导电性,给二极管的阳极加上正向电压,电流大小约5mA 左右,二极管就可以发光了,在规定电流范围内,电流越大,二极管发出的光亮度越强。​

11.3.2 硬件设计​

1. 例程功能​

通过汇编语言控制STM32MP157开发板的LED0闪烁。​

2. 硬件资源​

LED0接在芯片的PI0引脚上,我们通过程序控制PI0的电平间隔变化即可看到LED灯的亮和灭。​

3. 原理图​

打开STM32MP157开发板底板原理图, 路径为: 开发板光盘A-基础资料\2、开发板原理图\《STM32MP15x底板原理图》。​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.3.2. 1 LED与STM32MP157连接原理图​

可以看出,LED0 接到了PI0引脚上,当PI0输出低电平(0)的时候发光二极管 LED0 就会导通点亮,当PI0输出高电平(1)的时候发光二极管 LED0 不会导通,因此 LED0 也就不会点亮。LED1接在了PF3引脚上,同理,LED1的亮灭取决于PF3的输出电平,输出 0 就亮,输出 1 就灭。​

11.3.3 软件设计​

1. 创建工程​

按照前面第六章新建MDK工程 的步骤新建一个工程,如下图,我们可以修改工程中目录的名字(改为LED_A和Source),然后我们新建两个文件:startup_stm32mp15xx.s启动文件和汇编main.s文件(C语言中是main.c文件)。​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验

图11.3.3.1新建工程​

2. 编写启动文件代码​

startup_stm32mp15xx.s的代码如下:​

1 ;*******************************************************************​
2 ;* @file  : startup_stm32mp15xx.s​
3 ;* @author  : 正点原子Linux团队​
4  ;* @version  : V1.0​
5 ;* @date  : 2020-05-03​
6  ;* Description  : STM32MP1设备的中断向量表​
7 ;* 本文件功能:​
8 初始化SP​
9 ;* - STM32P1启动以后先执行Reset_Handler函数​
10 ;* - Reset_Handler函数里面跳转去运行Start函数​
11 ;* @license : Copyright (c) 2020-2032, 广州市星翼电子科技有限公司​
12 ;******************************************************************* ​
13​
14 Stack_Size EQU 0x00000400 ;栈大小为0X400,C语言运行要使用​
15​
16 AREA STACK, NOINIT, READWRITE, ALIGN=3​
17 __stack_limit​
18 Stack_Mem SPACE Stack_Size​
19 __initial_sp      ;初始化SP​
20​
21 AREA RESET, DATA, READONLY​
22 EXPORT __Vectors​
23 EXPORT __Vectors_End​
24 EXPORT __Vectors_Size​
25 ​
26 __Vectors DCD __initial_sp    ;栈顶​
27  DCD Reset_Handler   ;复位中断向量表​
28​
29​
30 __Vectors_End​
31 __Vectors_Size EQU __Vectors_End - __Vectors​
32 ​
33 AREA |.text|, CODE, READONLY​
34 ​
35 Reset_Handler PROC    ;复位中断函数 ​
36  IMPORT Start​
37 ​
38  LDR R0, =Start    ;跳转执行Start函数​
39 BX R0​
40  ENDP      

这里的启动文件的代码实际上是模仿STM32Cube固件包里startup_stm32mp15xx.s的写法,只是将中断向量表中的大多中断向量给去掉了,只留下复位中断。关于startup_stm32mp15xx.s,我们在前面第八章节有分析过。注意第35~40行的代码,我们手动做了修改,我们利用PROC、ENDP这一对伪指令把程序段分为1个过程:​

第35行是复位中断函数Reset_Handler,下面我们看看该函数做了哪些工作。​

第36行,表示标号Start来自外部文件,该标号我们会在main.s文件中实现;​

第38行,使用LDR指令从存储器加载Start到寄存器R0中;​

第39行,使用BX指令跳转到放于R0中的地址处;​

第40行,Reset_Handler函数结束。​

以上代码也就是初始化SP、复位后先执行Reset_Handler函数,而Reset_Handler函数做的工作就是跳转到Start函数处,Start函数就是本实验的LED0点灯程序,在main.s文件中。​

3.编写main.s代码​

main.s文件代码如下,代码中已经附上了详细的注释,可以很容易看懂:​

1 ;*******************************************************************​
2 ;* @file : main.s​
3 ;* @author : 正点原子Linux团队​
4 ;* @version : V1.0​
5  ;* @date  : 2020-05-03​
6 ;* @description : MP157开发板M4裸机例程main汇编文件​
7 ;* 本文件功能:​
8 ;* - 定义所要使用的寄存器​
9 ;* - Start函数编写,复位中断函数Reset_Handler会执行Start函数​
10 使能GPIOI时钟,初始化GPIOI_0这个IO为推挽输出​
11 循环里面周期性的点亮/熄灭LED0​
12 ;* @license : Copyright (c) 2020-2032, 广州市星翼电子科技有限公司​
13 ;******************************************************************* ​
14 ​
15 PERIPH_BASE  EQU (0x40000000)​
16 MCU_AHB4_PERIPH_BASE  EQU (PERIPH_BASE + 0x10000000)​
17 RCC_BASE  EQU (MCU_AHB4_PERIPH_BASE + 0x0000) ​
18 RCC_MC_AHB4ENSETR  EQU (RCC_BASE + 0XAA8)​
19 GPIOI_BASE  EQU (MCU_AHB4_PERIPH_BASE + 0xA000) ​
20 GPIOI_MODER  EQU (GPIOI_BASE + 0x0000) ​
21 GPIOI_OTYPER  EQU (GPIOI_BASE + 0x0004) ​
22 GPIOI_OSPEEDR  EQU (GPIOI_BASE + 0x0008) ​
23 GPIOI_PUPDR  EQU (GPIOI_BASE + 0x000C) ​
24 GPIOI_BSRR  EQU (GPIOI_BASE + 0x0018) ​
25 ​
26 AREA |.text|, CODE, READONLY, ALIGN=2​
27 THUMB​
28 EXPORT Start​
29 ​
30 Start​
31 ;1、设置RCC_MC_AHB4ENSETR寄存器,使能GPIOI时钟​
32 LDR R0, =RCC_MC_AHB4ENSETR​
33 LDR R1, [R0] ;读取RCC_MC_AHB4ENSETR寄存器的值到R1​
34 ORR R1, #(1 << 8) ;bit8置1,使能GPIOI时钟​
35 STR R1, [R0] ;写入到RCC_MC_AHB4ENSETR寄存器​
36 ​
37 ;2、GPIOI_MODER寄存器,设置GPIOI_0输出模式​
38 LDR R0, =GPIOI_MODER​
39 LDR R1, [R0]  ;读取GPIOI_MODER寄存器的值到R1​
40 BIC R1, #(3 << (2 * 0))  ;bit1:0 清零​
41 ORR R1, #(1 << (2 * 0))  ;bit1:0 设置为01​
42 STR R1, [R0]  ;写入到GPIOI_MODER寄存器​
43 ​
44 、GPIOI_OTYPER寄存器,设置GPIOI_0为推挽模式​
45 LDR R0, =GPIOI_OTYPER ​
46 LDR R1, [R0]  ;读取GPIOI_OTYPER寄存器值到R1​
47 BIC R1, #(1 << 0) ;bit0清零,设置为推挽输出​
48 STR R1, [R0]  ;写入到GPIOI_OTYPER寄存器 ​
49 ​
50 ;4、GPIOI_OSPEEDR寄存器,设置GPIOI_0为高速​
51 LDR R0, =GPIOI_OSPEEDR​
52 LDR R1, [R0]  ;读取GPIOI_OSPEEDR寄存器的值到R1​
53 BIC R1, #(3 << (2 * 0)) ;bit1: 0清零​
54 ORR R1, #(2 << (2 * 0))  ;bit1: 0设置为10​
55 STR R1, [R0]  ;写入到GPIOI_OSPEEDR寄存器 ​
56 ​
57 ;5、GPIOI_PUPDR寄存器,设置GPIOI_0上拉​
58 LDR R0, =GPIOI_PUPDR​
59 LDR R1, [R0]  ;读取GPIOI_PUPDR寄存器的值到R1​
60 BIC R1, #(3 << (2 * 0)) ;bit1:0 清零​
61 ORR R1, #(1 << (2 * 0))  ;bit1:0 设置为01​
62 STR R1, [R0]  ;写入到GPIOI_PUPDR寄存器 ​
63 ​
64 ;6、GPIOI_BSRR寄存器,设置GPIOI_0为低,点亮LED0​
65 LDR R0, =GPIOI_BSRR​
66 LDR R1, [R0]  ;读取GPIOI_BSRR寄存器的值到R1​
67 ORR R1, #(1 << 16) ;bit16 设置为1,PI0输出低电平​
68 STR R1, [R0]  ;写入到GPIOI_BSRR寄存器​
69 ​
70 ;循环​
71 Loop​
72 BL Led0_on  ;开灯​
73 BL Delay   ;延时​
74 BL Led0_off  ;关灯​
75 BL Delay  ;延时​
76 B Loop​
77 ​
78 ​
79 ;打开LED0​
80 Led0_on​
81 LDR R0, =GPIOI_BSRR​
82 LDR R1, [R0] 读取GPIOI_BSRR寄存器的值到R1​
83 ORR R1, #(1 << 16) ;bit16设置为1,PI0输出低电平​
84 STR R1, [R0] ;写入到GPIOI_BSRR寄存器​
85 BX LR​
86 ​
87 ;关闭LED0​
88 Led0_off​
89 LDR R0, =GPIOI_BSRR​
90 LDR R1, [R0] ;读取GPIOI_BSRR寄存器的值到R1​
91 ORR R1, #(1 << 0) ;bit0 设置为1,PI0输出高电平​
92 STR R1, [R0] ;写入到GPIOI_BSRR寄存器​
93 BX LR​
94 ​
95 延时函数​
96 Delay​
97 LDR R2, =0X4FFFFF​
98 LDR R3, =0X0​
99 Delay_loop​
100 SUB R2, R2, #1 ;R2寄存器减1​
101 CMP R2, R3  ;R2和R3寄存器的值进行比较​
102 BNE Delay_loop ;R2与R3的值不相等,说明没有R2还没有减完,继续​
103 BX LR  ;返回LR​
104 END      

第15~24行,使用EQU指令定义一些宏(类似于c语言的#define);​

第26行,定义一个代码段.text,且为只读模式,4字节对齐;​

第27行,汇编的固定格式,指THUMBM指令;​

第28行,EXPORT声明Start为全局属性,所以可以被外部文件调用;​

第30~68行,全局标号Start的内容,其内容是:​

1)GPIOI挂在AHB4总线上,如果要想使用GPIOI,则必须先使能GPIOI的时钟,对寄存器RCC_MC_AHB4ENSETR的第8位写1即可开启GPIOI的时钟;​

2)先清除GPIOI_MODER寄存器的MODER0[1:0]为00,然后再设置MODER0[1:0]为01,表示配置引脚为输出模式;​

3)设置GPIOI_OTYPER寄存器的OT0位为0,表示设置PI0为推挽输出模式;​

4)设置GPIOI_OSPEEDR寄存器的OSPEEDR0[1:0]为10,即配置PI0为高速模式;​

5)设置GPIOI_PUPDR寄存器的PUPDR0[1:0]为01,即上拉模式;​

6)配置GPIOI_BSRR寄存器的第16位BR0写1,则PI0低电平,LED0亮;​

第71~76行是循环函数Loop,该函数是执行Led0_on函数à Delay函数à函数à Delay函数,然后再返回来重新执行,如此反复。下面我们看看这些函数都做了什么:​

Led0_on函数:设置GPIOI_BSRR寄存器的第16位为1,则PI0输出低电平,LED0点亮;​

Led0_off函数:设置GPIOI_BSRR寄存器的第0位为0,则PI0输出高电平,PED0灭;​

Delay函数:让R2寄存器从初始值0X4FFFFF开始逐渐减1,直到减为0,递减的过程实现延时功能。​

这样就实现了点灯à延时一段时间à灭灯à延时一段时间à点灯...的循环过程,我们就看到LED0闪烁了。​

11.3.4 编译和测试​

编译工程无报错后,进入仿真界面,然后运行程序,可以看到LED0在闪烁。进入仿真界面以及MDK配置和ST LINK连接方法参考前面第四章节的操作步骤。​

《STM32MP1 M4裸机HAL库开发指南》第十一章 汇编LED灯实验