|
本帖最后由 正点原子 于 2022-7-29 10:32 编辑
1)实验平台:正点原子STM32MP157开发板
2) 章节摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南 V1.5.2
3)购买链接:https://detail.tmall.com/item.htm?id=631745687288
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/arm-linux/zdyzmp157.html1.0.0 文档
5)正点原子官方B站:https://space.bilibili.com/394620890
6)STM32MP157技术交流QQ群:703341432
第十章 跑马灯实验
本章节,通过一个经典的跑马灯程序,带领大家了解STM32MP1的IO口作为输出使用的方法,我们将通过代码控制开发板上的两个 LED 灯 DS0 和 DS1 交替闪烁,实现类似跑马灯的效果。
本章将分为如下几个小节:
10.1、STM32MP1 GPIO简介;
10.2、stm32mp1xx_hal_gpio.c文件;
10.3、LED灯简介;
10.4、硬件设计;
10.5、软件设计;
10.6、章节小结;
10.1 STM32MP157 GPIO简介
10.1.1 GPIO简介
GPIO(General-purpose input/output)即通用输入输出端口,它是一个通用可编程IO接口,可供使用者通过程序来控制,可以实现通用输入(GPI)或通用输出(GPO)或通用输入与输出(GPIO)功能。
在嵌入式系统应用中通常需要控制一些结构简单的外围设备或者电路,这些结构简单的设备或者电路往往只需要开或者关两种状态就可以实现我们想要的功能,例如LED灯的亮和灭,继电器的开和关以及蜂鸣器的发声和关闭等等,此时使用GPIO外接这些设备,使用者可以通过GPIO来控制和读取数字电路中TTL电平的逻辑0和逻辑1,从而可以简单高效地控制设备。
10.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 引脚。
表10.1.2. 1 GPIO汇总表
10.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)。推挽和开漏的功能与前面的讲解相同。
关于这几种模式的电路结构分析,可以参考下面10.1.4小节的内容。
10.1.4 GPIO基本结构分析
我们知道了GPIO有八种工作模式,具体这些模式是怎么实现的?下面我们通过GPIO的基本结构图来分别进行详细分析,先看看总的框图,如下图所示。
图10.1.4. 1 GPIO的基本结构图
如上图所示,可以看到右边只有I/O引脚,这个I/O引脚就是我们可以看到的芯片实物的引脚,其他部分都是GPIO的内部结构。
① 保护二极管
保护二极管共有两个,用于保护引脚外部过高或过低的电压输入。当引脚输入电压高于VDD时,上面的二极管导通,当引脚输入电压低于VSS时,下面的二极管导通,从而使输入芯片内部的电压处于比较稳定的值。虽然有二极管的保护,但这样的保护却很有限,大电压大电流的接入很容易烧坏芯片。所以在实际的设计中我们要考虑设计引脚的保护电路。
② 上拉、下拉电阻
它们阻值大概在30~50K欧之间,可以通过上、下两个对应的开关控制,这两个开关由寄存器控制。当引脚外部的器件没有干扰引脚的电压时,即没有外部的上、下拉电压,引脚的电平由引脚内部上、下拉决定,开启内部上拉电阻工作,引脚电平为高,开启内部下拉电阻工作,则引脚电平为低。同样,如果内部上、下拉电阻都不开启,这种情况就是我们所说的浮空模式。浮空模式下,引脚的电平是不可确定的。引脚的电平可以由外部的上、下拉电平决定。需要注意的是,STM32的内部上拉是一种“弱上拉”,这样的上拉电流很弱,如果有要求大电流还是得外部上拉。
③ 施密特触发器
对于标准施密特触发器,当输入电压高于正向阈值电压,输出为高;当输入电压低于负向阈值电压,输出为低;当输入在正负向阈值电压之间,输出不改变,也就是说输出由高电准位翻转为低电准位,或是由低电准位翻转为高电准位对应的阈值电压是不同的。只有当输入电压发生足够的变化时,输出才会变化,因此将这种元件命名为触发器。这种双阈值动作被称为迟滞现象,表明施密特触发器有记忆性。从本质上来说,施密特触发器是一种双稳态多谐振荡器。
施密特触发器可作为波形整形电路,能将模拟信号波形整形为数字电路能够处理的方波波形,而且由于施密特触发器具有滞回特性,所以可用于抗干扰,其应用包括在开回路配置中用于抗扰,以及在闭回路正回授/负回授配置中用于实现多谐振荡器。
下面看看比较器跟施密特触发器的作用的比较,就清楚的知道施密特触发器对外部输入信号具有一定抗干扰能力,如下图所示。
图10.1.4. 2比较器的(A)和施密特触发器(B)作用比较
④ P-MOS管和N-MOS管
这个结构控制GPIO的开漏输出和推挽输出两种模式。开漏输出:输出端相当于三极管的集电极,要得到高电平状态需要上拉电阻才行。推挽输出:这两只对称的MOS管每次只有一只导通,所以导通损耗小、效率高。输出既可以向负载灌电流,也可以从负载拉电流。推拉式输出既能提高电路的负载能力,又能提高开关速度。
上面我们对GPIO的基本结构图中的关键器件做了介绍,下面分别介绍GPIO八种工作模式对应结构图的工作情况。
1、输入浮空
输入浮空模式:上拉/下拉电阻为断开状态,施密特触发器打开,输出被禁止。
图10.1.4. 3输入浮空模式
2、输入上拉
输入上拉模式:上拉电阻导通,施密特触发器打开,输出被禁止。
图10.1.4. 4输入上拉模式
3、输入下拉
输入下拉模式:下拉电阻导通,施密特触发器打开,输出被禁止。
图10.1.4. 5输入下拉模式
4、模拟功能
模拟功能:上下拉电阻断开,施密特触发器关闭,双MOS管也关闭。其他外设可以通过模拟通道输入输出。
图10.1.4. 6模拟功能模式
5、开漏输出
开漏输出模式:开漏模式下P-MOS是不工作的(即一直关闭),如果要控制IO口输出0,N-MOS导通,使得输出接VSS。如果要控制输出1,则N-MOS关闭,此时P-MOS和N-MOS都是关闭,引脚呈现高阻态,即不输出高电平也不输出低电平,所以这时要输出高电平就必须接上拉电阻。这时可以用内部上拉电阻,但是不推荐,我们建议接一个外部的上拉电阻。因为如果接内部上拉电阻,具有线与特性,即如果有很多开漏模式的引脚连在一起的时候,只有当所有引脚都输出高阻态,电平才为1,只要有其中一个为低电平时,就等于接地,使得整条线路都为低电平0。
另外在开漏输出模式下,施密特触发器是打开的,所以IO口引脚的电平状态会被采集到输入数据寄存器中,如果对输入数据寄存器进行读访问可以得到IO口的状态。也就是说开漏输出模式下,我们可以对IO口进行读数据。
图10.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口的电平状态。
图10.1.4. 8推挽输出模式
7、开漏式复用功能
开漏式复用功能:一个IO口可以是通用的IO口功能,还可以是其他外设的特殊功能引脚,这就是IO口的复用功能。一个IO口可以是多个外设的功能引脚,我们需要选择作为其中一个外设的功能引脚。当选择复用功能时,引脚的状态是由对应的外设控制,而不是输出数据寄存器。除了复用功能外,其他的结构分析请参考开漏输出模式。
另外在开漏式复用功能模式下,施密特触发器也是打开的,我们可以读取IO口的电平状态,同时外设可以读取IO口的信息。
图10.1.4. 9开漏式复用功能
8、推挽式复用功能
推挽式复用功能:复用功能介绍请查看开漏式复用功能,结构分析请参考推挽输出模式,这里不再赘述。
图10.1.4. 10推挽式复用功能
10.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(端口模式控制寄存器)
图10.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(端口输出类型寄存器)
图10.1.5. 2 OTYPER寄存器
OTYPER用于控制 GPIOx 的输出类型(推挽输出或开漏输出),是32位寄存器,只有低 16 位有效,每一个位控制一个 IO 口,当配置位为0的时候为推挽输出模式,配置位为1的时候为开漏输出模式,复位后,该寄存器值均为 0,即复位后默认为推挽输出模式。该寄存器仅用于输出模式,在输入模式(MODER[1:0]=00)下不起作用。
3. OSPEEDR(端口输出速度寄存器)
图10.1.5. 3 OSPEEDR寄存器
OSPEEDR寄存器用于控制 GPIOx 的输出速度等级, 属于32位可读可写寄存器,每 2 个位控制一个 IO 口,由软件写入以配置I / O速度为:
00:低速
01:中速
10:快速
11:高速
该寄存器仅用于输出模式,在输入模式(MODER[1:0]=00)下不起作用。复位后该寄存器值一般为 0,即默认处于低速等级。
4. PUPDR(端口上拉下拉寄存器)
图10.1.5. 4 PUPDR寄存器
PUPDR寄存器用于控制 GPIOx 的上拉/下拉,属于32位可读可写寄存器,每 2 个位控制一个 IO 口,由软件写入以配置I O口是上拉或者下拉,复位后,该寄存器值一般为 0,即无上拉,无下拉状态:
00:无上拉或下拉
01: 上拉
10: 下拉
11: 保留
5. IDR(端口输入数据寄存器)
图10.1.5. 5 IDR寄存器
IDR用于读取 GPIOx 的输入电平状态,只有低16位有效,属于只读寄存器,每个位控制一个IO口,如果对应的位为 0(IDRy=0,y等于0至15),则说明该 IO口 输入的是低电平,如果是 1(IDRy=1),则表示该IO口输入的是高电平。复位后的低16位状态是未知的。
6. ODR(端口输出数据寄存器)
图10.1.5. 6 ODR寄存器
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(端口置位/复位寄存器)
图10.1.5. 7 BSRR寄存器
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(端口清除寄存器)
图10.1.5. 8 BRR寄存器
BRR为位清除寄存器,属于只写寄存器,如果读取这些位将返回值0x0000。BRR只有低16位可用,这低16位与BSRR的高16位具有相同功能,即对低16位写1,对应IO口为低电平,写0表示无效。
复位以后,BRR寄存器值默认为0x00000000,表示无动作。
9. LCKR(端口配置锁存寄存器)
图10.1.5. 9 LCKR寄存器
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_GPIO_LockPin,下面的第10.2.1小节有讲到。
GPIO相关的寄存器我们就简单介绍到这里,整体分析一遍寄存器,对我们后面的实验是很有帮助的,实验虽然是基于HAL库来操作,但本质上操作的还是寄存器,原理还是要弄懂的。
10. 以上寄存器汇总
针对以上寄存器的说明,我们汇总出如下表格,方便大家需要的时候查阅。
表10.1.5. 1GPIO部分寄存器汇总表
10.2 GPIO相关的API函数
下面,我们介绍stm32mp1xx_hal_gpio.c文件中几个重要的API函数,后面的实验中我们将用到这些函数。
10.2.1 HAL_GPIO_Init函数
●函数功能:根据GPIO_Init中的指定参数初始化GPIOx外设。
●函数参数:
GPIOx,其中x可以是(A..K和Z,下同)。
GPIO_Init,指向GPIO_InitTypeDef结构的指针,该结构包含指定GPIO外设的配置信息。
●函数返回值:无
●注意:HAL库的EXTI外部中断的设置功能整合到此函数里面,而不是单独独立一个文件,关于EXTI我们到外部中断实验再细讲。
HAL_GPIO_Init函数由于篇幅原因这里不列出函数的具体内容,大家可以直接在HAL库文件中查看。
1 void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)
2 {
3 /*省略代码*/
4 {
5 /*GPIO模式配置代码*/
6 }
7 {
8 /*外部中断配置代码*/
9 }
10 }
11 }
12 }
1. 形参GPIOx
形参GPIOx是端口号,可以有以下的选择,这是整个芯片可以选择的GPIO组,正点原子的STM32MP157开发板引出了144 个GPIO,并没有把所有的GPIO(184个)都引出来。
stm32mp157dxx_cm4.h文件代码
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
#define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)
#define GPIOI ((GPIO_TypeDef *) GPIOI_BASE)
#define GPIOJ ((GPIO_TypeDef *) GPIOJ_BASE)
#define GPIOK ((GPIO_TypeDef *) GPIOK_BASE)
GPIO_TypeDef结构体类型如下。可以看到GPIO_TypeDef结构体对GPIO相关的寄存器进行了封装,通过操作结构体成员即可操作寄存器。
stm32mp157dxx_cm4.h文件代码
typedef struct
{
__IO uint32_t MODER;
__IO uint32_t OTYPER;
__IO uint32_t OSPEEDR;
__IO uint32_t PUPDR;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t LCKR;
__IO uint32_t AFR[2];
__IO uint32_t BRR;
/*篇幅原因,此处省略剩下代码*/
} GPIO_TypeDef;
2. 形参GPIO_Init
GPIO_Init是一个GPIO_InitTypeDef 类型结构体,其定义如下:
stm32mp1xx_hal_gpio.h文件代码
typedef struct
{
uint32_t Pin; /*选择的引脚号*/
uint32_t Mode; /*引脚模式配置*/
uint32_t Pull; /*上拉/拉配置*/
uint32_t Speed; /*引脚速度等级配置*/
uint32_t Alternate; /*引脚复用配置*/
}GPIO_InitTypeDef;
GPIO_InitTypeDef 结构体对选择的引脚号、引脚工作模式、引脚上/下拉模式配置、引脚速度等级以及引脚复用配置进行了封装。在调用HAL_GPIO_Init函数以后,GPIOx所对应引脚工作模式将被初始化成我们配置的模式。
该结构体很重要,下面对每个成员进行介绍。
成员Pin表示引脚号,范围:GPIO_PIN_0到 GPIO_PIN_15,另外还有GPIO_PIN_All和GPIO_PIN_MASK可选。
成员Mode是GPIO的模式选择,有以下选择项:
stm32mp1xx_hal_gpio.h文件代码
#define GPIO_MODE_INPUT ((uint32_t)0x00000000U) /* 输入模式 */
#define GPIO_MODE_OUTPUT_PP ((uint32_t)0x00000001U) /* 推挽输出 */
#define GPIO_MODE_OUTPUT_OD ((uint32_t)0x00000011U) /* 开漏输出 */
#define GPIO_MODE_AF_PP ((uint32_t)0x00000002U) /* 推挽式复用 */
#define GPIO_MODE_AF_OD ((uint32_t)0x00000012U) /* 开漏式复用 */
#define GPIO_MODE_AF GPIO_MODE_AF_PP /* 输入复用 */
#define GPIO_MODE_ANALOG ((uint32_t)0x00000003U) /* 模拟模式 */
/* 外部中断,上升沿触发检测 */
#define GPIO_MODE_IT_RISING ((uint32_t)0x10110000U)
/* 外部中断,下降沿触发检测 */
#define GPIO_MODE_IT_FALLING ((uint32_t)0x10210000U)
/* 外部中断,上升和下降双沿触发检测 */
#define GPIO_MODE_IT_RISING_FALLING ((uint32_t)0x10310000U)
/* 外部事件,上升沿触发检测 */
#define GPIO_MODE_EVT_RISING ((uint32_t)0x10120000U)
/* 外部事件,下降沿触发检测 */
#define GPIO_MODE_EVT_FALLING ((uint32_t)0x10220000U)
/* 外部事件,上升和下降双沿触发检测 */
#define GPIO_MODE_EVT_RISING_FALLING ((uint32_t)0x10320000U)
成员Pull用于配置上下拉电阻,有以下选择项:
stm32mp1xx_hal_gpio.h文件代码
#define GPIO_NOPULL ((uint32_t)0x00000000U) /* 无上下拉 */
#define GPIO_PULLUP ((uint32_t)0x00000001U) /* 上拉 */
#define GPIO_PULLDOWN ((uint32_t)0x00000002U) /* 下拉 */
成员Speed用于配置GPIO的速度,有以下选择项:
stm32mp1xx_hal_gpio.h文件代码
#define GPIO_SPEED_FREQ_LOW ((uint32_t)0x00000000U) /* 低速 */
#define GPIO_SPEED_FREQ_MEDIUM ((uint32_t)0x00000001U) /* 中速 */
#define GPIO_SPEED_FREQ_HIGH ((uint32_t)0x00000002U) /* 快速 */
#define GPIO_SPEED_FREQ_VERY_HIGH ((uint32_t)0x00000003U) /* 高速 */
成员Alternate用于配置具体的复用功能,关于复用功能,我们后面有专门的实验来讲解。
10.2.2 HAL_GPIO_DeInit函数
●函数功能:将GPIOx外设寄存器初始化为其默认复位值(各个寄存器复位时默认的值)。
●函数参数:GPIOx和GPIO_Pin
●函数返回值:无
●注意:HAL库的EXTI外部中断的设置功能整合到此函数里面,而不是单独独立一个文件,关于EXTI我们到外部中断实验再细讲。
HAL_GPIO_Init函数由于篇幅原因这里不列出函数的具体内容,大家可以直接在HAL库文件中查看。
1 void HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin)
2 {
3 /*此处函数省略*/
4 }
10.2.3 HAL_GPIO_ReadPin函数
●函数功能:读取我们想要知道的引脚的电平状态。
●函数参数:GPIOx和GPIO_Pin
●函数返回值:输入端口引脚值,为0或1
1 GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
2 {
3 GPIO_PinState bitstatus;
4
5 /* 用于检查参数是否有效 */
6 assert_param(IS_GPIO_PIN(GPIO_Pin));
7
8 if((GPIOx->IDR & GPIO_Pin) != (uint32_t)GPIO_PIN_RESET)
9 {
10 bitstatus = GPIO_PIN_SET;
11 }
12 else
13 {
14 bitstatus = GPIO_PIN_RESET;
15 }
16 return bitstatus;
17 }
HAL_GPIO_ReadPin函数是读取我们想要知道的引脚的电平状态,函数的返回值为0或1。我们分析一下这段代码。
第3行,GPIO_PinState是一个枚举类型,可选值是0或者1,此行定义了一个枚举常量bitstatus。
第6行,断言,assert_param函数实际上是一个宏定义,它的作用就是检测传递给函数的参数是否是有效的参数,如果检测的参数有效,则返回true,否则返回false。
第8至第11行,IDR是GPIOx端口的输入数据寄存器,用于读取 GPIOx 的某个IO口输入电平状态,每个位控制一个IO口,如果IDR的第0位为 0,则说明该GPIOx 端口的第0个IO口输入的是低电平,如果IDR的第0位为 1,则表示该GPIOx 端口的第0个IO口输入的是是高电平。
其中,GPIO_PIN_RESET表示0,GPIO_PIN_SET 表示1。
其中的"->"是表示访问结构体的指针对应对象下的IDR成员,->运算符优先级比位与&运算符优先级要高,GPIOx->IDR可以改写为(*GPIOx).IDR。GPIOx->IDR表示IDR寄存器的低16位的数值。GPIO_Pin为16位,表示要读取的第几个IO口(如果要读取第1个IO口,GPIO_Pin的值是0x0002U)。(GPIOx->IDR & GPIO_Pin)就表示读取IO口对应的IDR寄存器的值,如果IDR和GPIO_Pin进行位与后不等于0(说明对应的IDR位是1,表示此IO口输入的是高电平),则返回bitstatus为1,反之则返回0,这样就实现了读取对应引脚的电平状态。这里的GPIO_Pin是哪一个,由我们后面调用函数的时候给定的具体的参数来决定。
10.2.4 HAL_GPIO_WritePin函数
●函数功能:让某个引脚设置为高电平或者低电平。
●函数参数:GPIOx和GPIO_Pin
●函数返回值:无
1 void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin,\ GPIO_PinState PinState)
2 {
3 /* 用于检查参数是否有效 */
4 assert_param(IS_GPIO_PIN(GPIO_Pin));
5 assert_param(IS_GPIO_PIN_ACTION(PinState));
6
7 if (PinState != GPIO_PIN_RESET)
8 {
9 GPIOx->BSRR = GPIO_Pin;
10 }
11 else
12 {
13 GPIOx->BSRR = (uint32_t)GPIO_Pin << GPIO_NUMBER;
14 }
15 }
在前面第10.1.5小节我们已经分析GPIOx_BSRR寄存器的功能了,对BSRR寄存器的高16位写1,ODR的对应位将被复位,所以对应IO口为低电平,对BSRR寄存器的低16位写1, ODR的对应位将被置1,对应IO口为高电平,如果写 0,表示无动作。
HAL_GPIO_WritePin函数中GPIO_Pin 为16位类型数据,表示要操作的IO口,PinState 为枚举类型,其中GPIO_PIN_RESET为0,GPIO_PIN_SET为1。GPIOx->BSRR表示BSRR寄存器的高16位和低16位的值。
当PinState为0的时候,GPIOx->BSRR = GPIO_Pin表示将GPIO_Pin的值赋予BSRR的低16位,GPIO_Pin中哪一位是1,则对应的BSRR的那个位就被置1,对应的IO口输出高电平。 当PinState为1的时候,(uint32_t)GPIO_Pin为32位,GPIO_NUMBER为16位,<<表示左移,GPIO_Pin左移16位后,原来的低16位移动到高16位的位置,低16位为0。GPIOx->BSRR = (uint32_t)GPIO_Pin << GPIO_NUMBER表示对BSRR寄存器高16位所对应的 GPIO_Pin 位写1,对应引脚为低电平。
这里的GPIO_Pin由我们后面调用函数的时候给定的具体的参数来决定。
10.2.5 HAL_GPIO_TogglePin函数
●函数功能:让某个GPIO口的电平翻转。
●函数参数:GPIOx和GPIO_Pin
●函数返回值:无
1 void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
2 {
3 /* Check the parameters */
4 assert_param(IS_GPIO_PIN(GPIO_Pin)); /* 用于检查参数是否有效*/
5
6 if ((GPIOx->ODR & GPIO_Pin) != 0x00u)
7 {
8 GPIOx->BRR = (uint32_t)GPIO_Pin;
9 }
10 else
11 {
12 GPIOx->BSRR = (uint32_t)GPIO_Pin;
13 }
14 }
BRR寄存器我们在第10.1.5小节有讲解过,BRR为位清除寄存器,对低16位写1,对应IO口为低电平,写0表示无效。
第6行,读取GPIO的ODR寄存器某一位是否等于0,如果不于0,表示此IO口输出低电平,则执行第8行代码。
第8行表示将BRR寄存器的对应位写1,表示将此IO口设置为低电平。
第12行,如果读取GPIO的ODR寄存器某一位是等于0,表示此IO口输出低电平,通过对BSRR寄存器的对应位写1后,此IO口输出高电平。
根据上述分析,此函数对GPIO的某个IO口实现电平翻转。
10.2.6 HAL_GPIO_LockPin函数
●函数功能:锁住 某个GPIO 引脚所涉及到的寄存器,锁定的寄存器为MODER, OTYPER, OSPEEDR, PUPDR, AFRL和AFRH。
●函数参数:GPIOx和GPIO_Pin
●函数返回值:枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(忙碌)、HAL_TIMEOUT(超时)
●注意:
①锁住的是某个引脚对应的寄存器的某个位,不是将寄存器的所有位都锁住。例如设置要锁住GPIOA的引脚GPIO_PIN_0,则GPIOA对应的寄存器的某一位:
MODER(第0~1位)、OTYPER(第0位)、OSPEEDR(第0~1位)、PUPDR(第0~1位)、AFRL(第0~3位),AFRH寄存器由于是用于配置GPIO的8至第15位,所以没有被锁住。
②锁定的GPIO引脚的那个位,不能再修改寄存器对应的位的配置(上面提到的寄存器),直到下一次复位以后才可以更改(复位以后LCKR寄存器的默认值是0x00000000,没有激活锁定键)。关于LCKR寄存器可以看前面的第10.1.5小节。
我们来看看这个函数的代码实现过程:
1 HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef* GPIOx, uint16_t \ GPIO_Pin)
2 {
3 __IO uint32_t tmp = GPIO_LCKR_LCKK; /* 0x00010000 */
4
5 /* 检查参数 */
6 assert_param(IS_GPIO_LOCK_INSTANCE(GPIOx));
7 assert_param(IS_GPIO_PIN(GPIO_Pin));
8
9 /* 应用锁定键写入顺序 */
10 tmp |= GPIO_Pin;
11 /* 将LCKx位置1:LCKK ='1'+ LCK [15-0] */
12 GPIOx->LCKR = tmp;
13 /* 复位LCKx位:LCKK ='0'+ LCK [15-0] */
14 GPIOx->LCKR = GPIO_Pin;
15 /*将LCKx位置1:LCKK ='1'+ LCK [15-0] */
16 GPIOx->LCKR = tmp;
17 /* 读取LCKK寄存器。 必须完成此读取才能完成键锁定序列 */
18 tmp = GPIOx->LCKR;
19
20 /* 再次读LCKR寄存器的值以确认锁定处于活动状态 */
21 if((GPIOx->LCKR & GPIO_LCKR_LCKK) != RESET)
22 {
23 return HAL_OK;
24 }
25 else
26 {
27 return HAL_ERROR;
28 }
29 }
第3行,GPIO_LCKR_LCKK的值为0x00010000,表示第16位为1,其它位为0,此值写入tmp中;
第10行,将用户指定要锁的某个位和tmp相或,这样一来就确定了要锁定键。
第11到第18行,是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]位的值 */
④读取LCKK寄存器(写完以后必须要读)
关于stm32mp1xx_hal_gpio.c文件的API函数我们就介绍到这里,关于EXTI部分,我们后面对应的实验会进行讲解。
10.3 LED灯简介
LED(Light Emitting Diode Light)又名发光二极管,是一种把电转化为光的半导体器件。LED 灯工作电流很小,一般在0至15mA之间,亮度随电流的增大而变亮。
不同材料的发光二极管可以直接发出红、黄、蓝、绿、青、橙、紫、白色的光,下图是可以发出黄、红、蓝三种颜色的直插型二极管实物图,这种二极管长的一端是阳极,短的一端是阴极。
图10.3. 1发光二极管
下图是开发板上用的贴片二极管实物图。贴片二极管的正面一般都有颜色标记,有标记的那端就是阴极。
图10.3. 2贴片二极管
多个发光二极管封装在一起可以组合成LED数码管,例如显示数字8的7段数码管是由7个二极管组成,8段数码管比7端数码管多了一个二极管,显示一个点。数码管有共阴和共阳两种接法,如下图,前者通常称为共阴数码管,后者为共阳数码管。共阳极的接法是发光二极管的阳极接高电平,共阴极的接法是发光二极管的阴极接地。
图10.3. 3共阳和共阴极
二极管具有单向导电性,给二极管的阳极加上正向电压,电流大小约5mA 左右,二极管就可以发光了,在规定电流范围内,电流越大,二极管发出的光亮度越强。
10.4 硬件设计
1. 例程功能
通过HAL库的API函数来驱动LED,实现LED0和LED1以500ms交替闪烁。
2. 硬件资源
表10.4. 1硬件资源
3. 原理图
打开STM32MP157开发板底板原理图, 路径为: 开发板光盘A-基础资料\2、开发板原理图\《STM32MP15x底板原理图》。
图10.4.3. 1 LED与STM32MP157连接原理图
可以看出,LED0 接到了PI0引脚上,当PI0输出低电平(0)的时候发光二极管 LED0 就会导通点亮,当PI0输出高电平(1)的时候发光二极管 LED0 不会导通,因此 LED0 也就不会点亮。LED1接在了PF3引脚上,同理,LED1的亮灭取决于PF3的输出电平,输出 0 就亮,输出 1 就灭。
10.5 软件设计
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 3 LED。
10.5.1 程序设计流程
本章节我们通过HAL库的API函数来驱动LED,实现LED0和LED1以500ms交替闪烁。其中,我们会用到HAL_GPIO_WritePin和HAL_GPIO_TogglePin函数。实验程序的设计流程如下:
图10.5.1. 1实验程序设计流程
10.5.2 GPIO功能引脚配置
我们新建一个工程LED,进入STM32CubeMX插件配置界面后,在Pin搜索管脚处输入PI0,在引脚排列视图处可以看到一个引脚在闪烁,这个就是我们要找的PI0。
图10.5.1. 2搜索PI0引脚
我们点击闪烁的引脚PI0,在弹出的复用功能菜单中选择GPIO_Output。
图10.5.1. 3配置PI0引脚复用功能
配置完PI0的复用功能,我们还要配置Pin Reserved选项,即引脚保留,也就是将这个引脚给谁用。选中PI0,右键弹出设置项,我们选择Pin ReservedCortex-M4,这里注意,如果不配置此项,在生成工程代码的时候将不会看到有关这个Pin的初始化代码。
图10.5.1. 4将PI0给CM4内核使用
按照前面同样的方法我们配置PF3。接下来,我们配置GPIO的工作模式:
打开左边的System CoreGPIO进入GPIO模式配置界面,在下方会列出该GPIO的配置项。
①选项 GPIO mode 用来设置IO 口输出模式为 Output Push Pull(推挽)还是 Output Open Drain(开漏)。本实验我们设置为推挽输出 Output Push Pull。
②选项 GPIO Pull-up/Pull-down 用来设置 IO 口是上拉/下拉/没有上下拉。本实验我们设置为上拉(Pull-up)。
③选项Mzximum ouput speed用来设置IO 口输出速度为低速(Low)/中速(Medium)/高速(Hign)/快速(Very High)。本实验我们设置为高速 High就可以了。
④选项 User Label 是用来设置初始化的 IO 口 Pin 值为我们自定义的宏,一般情况我们可以不用设置,这里我们填写为LED0。按照如上要求设置后的界面如下:
图10.5.1. 5配置LED0工作模式
按照前面同样的方法我们配置PF3,User Label为LED1:
图10.5.1. 6配置LED1工作模式
10.5.3 时钟和工程配置
时钟,我们这里暂时不做其它配置,采用默认内部高速时钟HSI(64MHz)。接下来要注意勾选Generate peripheral initialization as a pair of ".c/.h' files per peripheral选项,这样可以独立生成对应外设的初始化.h和.c 文件(这么做也是为了不让外设的初始化代码生成在main.c文件中,方便查看,也避免main.c文件变得臃肿):
图10.5.2. 7配置生成外设独立的初始化文件
10.5.4生成工程
修改完之后,按下键盘的“Ctrl+S”组合键保存保存 LED.ioc 文件,系统开始生成初始化代码。在CM4工程下新建BSP文件夹,BSP文件夹下有led.c文件和Include/led.h文件,如下所示:
图10.5.4. 1生成LED工程
添加完以后,记得在工程中将源文件和工程关联,如果将BSP文件夹创建在Core文件夹下的话,就不需要做此步操作,如下图所示:
图10.5.4. 2将源文件和工程关联
10.5.5 控制逻辑代码实现
1. 初始化代码分析
前面的操作已经生成了GPIO的初始化代码,我们打开LED_CM4工程下的Core Src gpio.c文件,gpio.c文件内容(以下代码已经把多余的注释部分删除),初始化GPIO外设的代码主要分为两个部分:
①先开启要操作的外设时钟。前面我们在时钟系统章节有分析过,复位以后,所有外设的时钟均关闭,要操作外设的话,必须先使能对应外设的时钟。
②根据用户的配置来初始化外设(也就是将这些配置写入对应的寄存器),完成外设配置。
gpio.c文件代码
1 #include "gpio.h"
2 void MX_GPIO_Init(void)
3 {
4 GPIO_InitTypeDef GPIO_InitStruct = {0};
5
6 /* 使能GPIO时钟 */
7 __HAL_RCC_GPIOI_CLK_ENABLE();
8 __HAL_RCC_GPIOF_CLK_ENABLE();
9
10 /* 配置GPIOI */
11 GPIO_InitStruct.Pin = LED0_Pin;
12 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
13 GPIO_InitStruct.Pull = GPIO_PULLUP;
14 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
15 HAL_GPIO_Init(LED0_GPIO_Port, &GPIO_InitStruct);
16
17 /* 配置GPIOF */
18 GPIO_InitStruct.Pin = LED1_Pin;
19 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
20 GPIO_InitStruct.Pull = GPIO_PULLUP;
21 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
22 HAL_GPIO_Init(LED1_GPIO_Port, &GPIO_InitStruct);
23 }
gpio.c文件的内容主要就是实现了LED0和LED1对应的IO口的初始化。
第4行,定义一个 GPIO_InitTypeDef 结构体类型变量GPIO_InitStruct,初始值设置为0。
选中GPIO_InitTypeDef然后按下F2键可以看到此结构体的声明,我们在前面第10.2.1小节有介绍过,主要是对GPIO引脚的工作模式、上/下拉、速度等级以及引脚复用关系的配置。
第7~8行是使能 LED 相关的 GPIO 外设时钟,GPIOI和GPIOF挂在AHB4总线上,所以要开启AHB4上GPIO外设时钟。我们来看这两行代码。在stm32mp1xx_hal_rcc.h文件中有如下定义:
stm32mp1xx_hal_rcc.h文件代码
#define __HAL_RCC_GPIOI_CLK_ENABLE() (RCC->MC_AHB4ENSETR = \ RCC_MC_AHB4ENSETR_GPIOIEN)
#define __HAL_RCC_GPIOF_CLK_ENABLE() (RCC->MC_AHB4ENSETR = \ RCC_MC_AHB4ENSETR_GPIOFEN)
在stm32mp157dxx_cm4.h中有如下定义。B(5)表示将第五位置1
stm32mp157dxx_cm4.h文件代码
#define RCC_MC_AHB4ENSETR_GPIOFEN B(5)
#define RCC_MC_AHB4ENSETR_GPIOIEN B(8)
综合上面分析,第7~8行表示开启GPIOI和GPIOF的时钟。
注:
MC_AHB4ENSETR是使能MCU设置寄存器,该寄存器用于启用对应的外设时钟,对相应位写1则开启对应外设时钟,写0表示无效。
第0~第10位分别对应GPIOA~GPIOk时钟使能位。
图10.5.3. 1MC_AHB4ENSETR寄存器
第11-14行表示要控制的 GPIOI端口的某个IO口(LED0_Pin)引脚的输出类型为推挽输出,引脚为上拉模式,引脚速率为高速。
第15行调用库函数HAL_GPIO_Init,根据上面对GPIO_InitStructure结构体成员赋的值来 再次初始化LED0_GPIO_Port。
选中LED0_GPIO_Port,按下Ctrl键并点击鼠标左键打开LED0_GPIO_Port声明,在main.h中看到声明,LED0_Pin就是GPIO_PIN_0,LED0_GPIO_Port就是GPIOI。
main.h文件代码
1 #ifndef __MAIN_H
2 #define __MAIN_H
3
4 #ifdef __cplusplus
5 extern "C" {
6 #endif
7
8 #include "stm32mp1xx_hal.h"
9 void Error_Handler(void);
10 #define LED0_GPIO_Port GPIOI
11 #define LED1_Pin GPIO_PIN_3
12 #define LED1_GPIO_Port GPIOF
13 #ifdef __cplusplus
14 }
15 #endif
16 #endif /* __MAIN_H */
stm32mp1xx_hal_conf.h 文件调用了stm32mp1xx_hal_gpio.h文件,在stm32mp1xx_hal_gpio.h文件中声明了GPIO_PIN_0为无符号整型0x0001,对应二进制为1,所以LED0_Pin表示取GPIOI的第0位,同理LED1_Pin表示取GPIOF的第3位。
stm32mp1xx_hal_gpio.h文件代码
1 #define GPIO_PIN_0 ((uint16_t)0x0001U) /* 选择pin0 */
2 #define GPIO_PIN_1 ((uint16_t)0x0002U) /* 选择pin0 */
3 #define GPIO_PIN_2 ((uint16_t)0x0004U) /* 选择pin1 */
4 #define GPIO_PIN_3 ((uint16_t)0x0008U)
5 #define GPIO_PIN_4 ((uint16_t)0x0010U)
6 #define GPIO_PIN_5 ((uint16_t)0x0020U)
7 #define GPIO_PIN_6 ((uint16_t)0x0040U)
8 #define GPIO_PIN_7 ((uint16_t)0x0080U)
9 #define GPIO_PIN_8 ((uint16_t)0x0100U)
10 #define GPIO_PIN_9 ((uint16_t)0x0200U)
11 #define GPIO_PIN_10 ((uint16_t)0x0400U)
12 #define GPIO_PIN_11 ((uint16_t)0x0800U)
13 #define GPIO_PIN_12 ((uint16_t)0x1000U)
14 #define GPIO_PIN_13 ((uint16_t)0x2000U)
15 #define GPIO_PIN_14 ((uint16_t)0x4000U)
16 #define GPIO_PIN_15 ((uint16_t)0x8000U) /* 选择pin15 */
17 #define GPIO_PIN_All ((uint16_t)0xFFFFU) /* 选择所有的pin */
18 #define GPIO_PIN_MASK ((uint32_t)0x0000FFFFU)/* PIN掩码用于断言测试 */
2. 逻辑代码实现
在前面,我们使用STM32CubeMX生成了基于HAL库的GPIO初始化代码,还没有控制LED0和LED1点亮和熄灭的逻辑代码,接下来我们在这个工程的基础上去手动完成这部分内容。
在LED_CM4工程下新建BSP文件夹,此文件夹下专门存放自己的逻辑代码,然后在BSP下新建Include文件夹用于存放头文件,然后分别新建led.h和led.c文件。
图10.5.3. 2新建BSP文件夹
led.h和led.c文件代码分别如下:
led.h文件代码
1 #ifndef __LED_H
2 #define __LED_H
3
4 #include"gpio.h"
5 /* LED端口定义 */
6 #define LED0(x) do{ x ? \
7 HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET) : \
8 HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET); \
9 }while(0) /* LED0 = RED */
10
11 #define LED1(x) do{ x ? \
12 HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET) : \
13 HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET); \
14 }while(0) /* LED1 = GREEN */
15
16 /* LED取反定义 */
17 #define LED0_TOGGLE() \
18 do{ \
19 HAL_GPIO_TogglePin(LED0_GPIO_Port, LED0_Pin);\
20 }while(0) /* LED0 = !LED0 */
21 #define LED1_TOGGLE() \
22 do{ \
23 HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);\
24 }while(0) /* LED1 = !LED1 */
25
26 void led_init(void); /* 初始化 */
27
28 #endif
第6~14行,调用HAL_GPIO_WritePin函数来设置或清除选定的GPIO端口位GPIO_Pin,采用三目运算符,我们来分析LED0部分:
当x为1即大于0的时候,LED0(1)为HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET),表示设置LED0的端口位为1,端口位LED0_Pin输出高电平,LED0点亮。
当x为0的时候,LED0(0)为HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET),表示设置LED0的端口位为0,端口位LED0_Pin输出低电平,LED熄灭。
其中“\”表示换行符号,因为一行写不下了写到第二行,两行之间的代码用换行符来连接。
第17~24行,调用HAL_GPIO_TogglePin函数将引脚的状态取反。
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET)表示设置GPIOI的第LED0_Pin位所对应的管脚为高电平,那么LED0关闭。
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET)表示设置GPIOI的第LED0_Pin位所对应的管脚为低电平,那么LED0被点亮。
第26行,声明led_init初始化函数。我们来看看此函数。
3. main函数调用
led.c文件是初始化代码,通过led_init函数来初始化LED0和LED1处于关闭状态:
led.c文件代码
1 #include "./Include/led.h"
2
3 void led_init(void)
4 {
5 LED0(1); /* 关闭 LED0 */
6 LED1(1); /* 关闭 LED1 */
7 }
以上实现了LED0和LED1的控制逻辑代码,我们在main函数中直接调用如下:
main.c文件代码
1 #include "main.h"
2 #include "gpio.h"
3 #include "../../BSP/Include/led.h"
4 void SystemClock_Config(void);
5 int main(void)
6 {
7 HAL_Init(); /*初始化 HAL 库 */
8 if(IS_ENGINEERING_BOOT_MODE()) /* 检查平台是否为工程启动模式 */
9 {
10 /* 配置系统时钟 */
11 SystemClock_Config();
12 }
13 /* 初始化所有已经配置的外设 */
14 MX_GPIO_Init();
15 led_init(); /* 初始化LED */
16 while (1)
17 {
18 LED0(0); /* 打开LED0 */
19 LED1_TOGGLE(); /* 关闭LED1 */
20 HAL_Delay(500);
21 LED0(1); /* 关闭LED0 */
22 LED1_TOGGLE(); /* 打开LED1 */
23 HAL_Delay(500);
24 }
25 }
26 void SystemClock_Config(void)
27 {
28 /*此处省略时钟初始化代码*/
29 }
第1~3行引用对应头文件。
第7行初始化HAL库,使用HAL库函数前,必须在main函数中先对HAL库进行初始化。
第8~12行检查平台是否为工程启动模式,如果是则初始化系统时钟。
第14行初始化已配置的外围设备,这里是初始化GPIO。
第15行初始化LED0和LED1为关闭状态。
第16~24行是LED0和LED1交替闪烁的初始化代码,调用HAL_Delay(500)函数实现每隔500ms交替闪烁一次。HAL_Delay函数的实现原理我们在第7.4.2小节有分析过。
10.5.6 编译和下载验证
保存修改后点击工具栏的小锤子进行编译,编译无报错后,按照第4.1.6小节连接好开发板和ST-Link,进入Debug模式。进入Debug以后,点击继续运行按钮来运行调试,可以看到开发板底板的LED 灯 DS0 和 DS1在交替闪烁,点击即可终止调试并退出调试界面。
图10.5.4. 1调试界面
10.6 章节小结
本章节的实验,我们调用HAL库来操作LED灯对应的寄存器,从而点亮LED,这里我们对前面提到的ODR、BRR和BSRR寄存器做一个总结。
ODR寄存器
操作:ODR寄存器可读可写,低16位有效,每个位控制一个IO。对位写1,IO口输出高电平,对位写0,IO口输出低电平。
对ODR寄存器的读写操作必须以16位形式进行,如果用ODR寄存器来改写数据以控制输出时,采用“读改写”的形式。
缺点:ODR寄存器会受中断的影响,关闭中断会导致延迟或丢失一事件的捕获,如果在中断中通过ODR操作IO口,操作越频繁影响越明显。如果在中断中操作IO口,最好还是用BRR或者BSRR寄存器,因为BSRR和BRR 寄存器可以实现对ODR 寄存器进行原子读取/
修改访问,这样可确保在读取和修改访问之间发生中断请求的时候也不会有问题。
注:
原子操作:原子(atomic)操作就是指不能再进一步分割的操作,一般原子操作用于变量或者位操作。一个程序在执行的时候可能被优先级更高的线程中断,而原子操作不会被线程调度机制打断,这种操作一旦开始,就一直运行到结束,运行期间不会有任何的上下文切换(不会切换到另一个线程)。简单的说,原子操作是指操作过程中不会被中断打断。
BRR寄存器
BRR属于只写寄存器,只有低16位可用,对BRRR寄存器的写操作必须以16位形式进行,每个位控制一个IO。对位写1,IO口输出低电平,写0无效。即BRR只能控制管脚为低电平,不能控制管脚为高电平,所以BRR也称作位清除寄存器。BRR 寄存器支持原子操作。
如果想对某个GPIO的(如GPIOA)的第1位进行复位,其它位保留原来的值,则可以使用使用BRR寄存器写1来实现:
GPIOA->BRR=0X02;
当然也可以用过ODR寄存器来实现,不过写起来就不是很方便了:
GPIOA->ODR&=0XFFFE;
BSRR寄存器
BSRR属于只写寄存器,32位有效,其中,对高16位写1,对应的ODR位将被复位,所以对应IO口为低电平;对16位写1,对应的ODR位将被置1,对应IO口为高电平;写0表示无效。所以,BSRR的高16位也称作位清除寄存器,低16位也称作设置/置位寄存器。BSRR 寄存器支持原子操作。
如果想对某个GPIO(如GPIOA)的第1位置1,第14位置0,通过BSRR可以一步到位:
GPIOA->BSRR=0X4000 0002;
以上是从对寄存器直接操作的层面来分析,当然了,我们使用HAL库以后可以不用直接操作寄存器了,不过进行开发时,有时候也会在HAL库中添加自己的代码,有些操作直接通过操作寄存器是很方便的。此外,HAL库也只是将对寄存器的操作做了封装,方便代码移植,如果要了解HAL库的实现过程,那也必须先了解寄存器,所以我们后面的实验也会先简单介绍寄存器以后,再介绍HAL库的API函数,这样结合起来学习,可以更好地理解和掌握HAL库。
|
阿莫论坛20周年了!感谢大家的支持与爱护!!
曾经有一段真挚的爱情摆在我的面前,我没有珍惜,现在想起来,还好我没有珍惜……
|