05. 通用GPIO
一、GPIO概述
GPIO 是控制或者采集外部器件的信息的外设,即负责输入输出。它按组分配存在,每组最多 16 个 IO 口,组数视芯片而定。比如:STM32F407ZGT6 芯片是 144 脚的芯片,分为 7 组,分别是:GPIOA、GPIOB、GPIOC、GPIOD、GPIOE、GPIOF 和 GPIOG,其中共有 112 个 IO 口可供我们编程使用。
STM32F407 的绝大部分 IO 口,都兼容 5V,至于到底哪些是兼容 5V 的,请看 STM32F407ZG 的数据手册(注意是数据手册,不是中文参考手册),见表 5 大容量 STM32F40xxx 引脚定义,凡是有 FT 标志的,都是兼容 5V 电平的 IO 口,可以直接接 5V 的外设(注意:如果引脚设置的是模拟输入模式,则不能接 5V!),凡是不带 FT 标志的,就建议大家不要接 5V 了,可能烧坏 MCU。
二、GPIO基本结构
如上图所示,可以看到右边只有 I/O 引脚,这个 I/O 引脚就是我们可以看到的芯片实物的引脚,其他部分都是 GPIO 的内部结构。
【1】、保护二极管
保护二极管共有两个,用于保护引脚外部过高或过低的电压输入。当引脚输入电压高于 VDD 时,上面的二极管导通,当引脚输入电压低于 VSS 时,下面的二极管导通,从而使输入芯片内部的电压处于比较稳定的值。
虽然有二极管的保护,但这样的保护却很有限,大电压大电流的接入很容易烧坏芯片。所以在实际的设计中我们要考虑设计引脚的保护电路。
【2】、上拉、下拉电阻
它们阻值大概在 30~50K 欧之间,可以通过上、下两个对应的开关控制,这两个开关由寄存器控制
当引脚外部的器件没有干扰引脚的电压时,即没有外部的上、下拉电压,引脚的电平由引脚内部上、下拉决定,开启内部上拉电阻工作,引脚电平为高,开启内部下拉电阻工作,则引脚电平为低。同样,如果内部上、下拉电阻都不开启,这种情况就是我们所说的浮空模式。
浮空模式下,引脚的电平是不可确定的。引脚的电平可以由外部的上、下拉电平决定。需要注意的是,STM32 的内部上拉是一种“弱上拉”,这样的上拉电流很弱,如果有要求大电流还是得外部上拉。
【3】、施密特触发器
对于标准施密特触发器,当输入电压高于正向阈值电压,输出为高;当输入电压低于负向阈值电压,输出为低;当输入在正负向阈值电压之间,输出不改变,也就是说输出由高电准位翻转为低电准位,或是由低电准位翻转为高电准位对应的阈值电压是不同的。只有当输入电压发生足够的变化时,输出才会变化,因此将这种元件命名为触发器。这种双阈值动作被称为迟滞现象,表明施密特触发器有记忆性。从本质上来说,施密特触发器是一种双稳态多谐振荡器。
施密特触发器可作为波形整形电路,能将模拟信号波形整形为数字电路能够处理的方波波形,而且由于施密特触发器具有滞回特性,所以可用于抗干扰,其应用包括在开回路配置中用于抗扰,以及在闭回路正回授/负回授配置中用于实现多谐振荡器。
【4】、P-MOS 管和 N-MOS 管
这个结构控制 GPIO 的开漏输出和推挽输出两种模式。开漏输出:输出端相当于三极管的集电极,要得到高电平状态需要上拉电阻才行。推挽输出:这两只对称的 MOS 管每次只有一只导通,所以导通损耗小、效率高。输出既可以向负载灌电流,也可以从负载拉电流。推拉式输出既能提高电路的负载能力,又能提高开关速度。
三、GPIO工作模式
3.1、输入浮空
上拉/下拉电阻为断开状态,施密特触发器打开,输出被禁止。输入浮空模式下,IO 口的电平完全是由外部电路决定。如果 IO 引脚没有连接其他的设备,那么检测其输入电平是不确定的。该模式可以用于按键检测等场景。
空闲时,IO 状态是不确定的,由外部环境决定;
3.2、输入上拉
上拉电阻导通,施密特触发器打开,输出被禁止。在需要外部上拉电阻的时候,可以使用内部上拉电阻,这样可以节省一个外部电阻,但是内部上拉电阻的阻值较大,所以只是“弱上拉”,不适合做电流型驱动。
空闲时,IO 呈现高电平;
3.3、输入下拉
下拉电阻导通,施密特触发器打开,输出被禁止。在需要外部下拉电阻的时候,可以使用内部下拉电阻,这样可以节省一个外部电阻,但是内部下拉电阻的阻值较大,所以不适合做电流型驱动。
空闲时,IO 呈现低电平;
3.4、模拟功能
上下拉电阻断开,施密特触发器关闭,双 MOS 管也关闭。该模式用于 ADC 采集或者 DAC 输出,或者低功耗下省电。
专门用于模拟信号输入或输出,如 ADC 和 DAC;
3.5、开漏输出
STM32 的开漏输出模式是数字电路输出的一种,从结果上看它只能输出低电平 VSS 或者高阻态。
开漏模式下,P-MOS 管是一直截止的,所以 P-MOS 管的栅极一直接 VSS。如果输出数据寄存器设置为 0 时,经过“输出控制”的逻辑非操作后,输出逻辑 1 到 N-MOS 管的栅极,这时 N-MOS 管就会导通,使得 I/O 引脚接到 VSS,即输出低电平。
如果输出数据寄存器设置为 1 时,经过“输出控制器”的逻辑非操作后,输出逻辑 0 到 NMOS 管的栅极,这时 N-MOS 管就会截止。因为 P-MOS 管是一直截止的,使得 I/O 引脚呈现高阻态,即不输出低电平,也不输出高电平。因此要 I/O 引脚输出高电平就必须接上拉电阻。这时可以接内部上拉电阻,或者接一个外部上拉电阻。由于内部上拉电阻的阻值较大,所以只是“弱上拉”。需要大电流驱动,请接外部的上拉电阻。
此外,上拉电阻具有线与特性,即如果有很多开漏模式的引脚连在一起的时候,只有当所有引脚都输出高阻态,电平才为 1,只要有其中一个为低电平时,就等于接地,使得整条线路都为低电平 0。
另外在开漏输出模式下,施密特触发器是打开的,所以 IO 口引脚的电平状态会被采集到输入数据寄存器中,如果对输入数据寄存器进行读访问可以得到 IO 口的状态。也就是说开漏输出模式下,我们可以对 IO 口进行读数据。
不能暑促高电平,必须有外部(或内部)上拉才能输出高电平;
3.6、推挽输出
STM32 的推挽输出模式,从结果上看它会输出低电平 VSS 或者高电平 VDD。推挽输出跟开漏输出不同的是,推挽输出模式 P-MOS 管和 N-MOS 管都用上。
如果输出数据寄存器设置为 0 时,经过“输出控制”的逻辑非操作后,输出逻辑 1 到 P-MOS 管的栅极,这时 P-MOS 管就会截止,同时也会输出逻辑 1 到 N-MOS 管的栅极,这时 N-MOS 管就会导通,使得 I/O 引脚接到 VSS,即输出低电平。
如果输出数据寄存器设置为 1 时,经过“输出控制”的逻辑非操作后,输出逻辑 0 到 N-MOS 管的栅极,这时 N-MOS 管就会截止,同时也会输出逻辑 0 到 P-MOS 管的栅极,这时 PMOS 管就会导通,使得 I/O 引脚接到 VDD,即输出高电平。
推挽输出模式下,P-MOS 管和 N-MOS 管同一时间只能有一个 MOS管是导通的。当引脚高低电平切换时,两个管子轮流导通,一个负责灌电流,一个负责拉电流,使其负载能力和开关速度都有很大的提高。
另外在推挽输出模式下,施密特触发器也是打开的,我们可以读取 IO 口的电平状态。由于推挽输出模式输出高电平时,是直接连接 VDD ,所以驱动能力较强,可以做电流型驱动,驱动电流最大可达 25mA。该模式也是最常用的输出模式。
可输出高低电平,驱动能力强;
3.7、开漏式复用功能
一个 IO 口可以是通用的 IO 口功能,还可以是其他外设的特殊功能引脚,这就是 IO 口的复用功能。一个 IO 口可以是多个外设的功能引脚,我们需要选择作为其中一个外设的功能引脚。当选择复用功能时,引脚的状态是由对应的外设控制,而不是输出数据寄存器。除了复用功能外,其他的结构分析请参考开漏输出模式。
另外在开漏式复用功能模式下,施密特触发器也是打开的,我们可以读取 IO 口的电平状态,同时外设可以读取 IO 口的信息。
不能输出高电平,必须有外部(或内部)上拉才能输出高电平,由其它外设控制输出;
3.8、推挽式复用功能
复用功能介绍请查看开漏式复用功能,结构分析请参考推挽输出模式。
可输出高电平,驱动能力强,由其它外设控制输出;
四、GPIO常用寄存器
STM32F4 每组(这里是 A~I)通用 GPIO 口有 7 个 32 位寄存器控制,包括 :
- 4 个 32 位配置寄存器(MODER、OTYPER、OSPEEDR 和 PUPDR)
- 2 个 32 位数据寄存器(IDR 和 ODR)
- 1 个 32 位置位/复位寄存器 (BSRR)
- 1 个 32 位锁定寄存器 (LCKR)
- 2 个 32 位复用功能选择寄存器(AFRH 和 AFRL)
4.1、GPIO端口模式寄存器
该寄存器是 GPIO 口模式控制寄存器,用于控制 GPIOx(STM32F4 最多有 9 组 IO,用大写字母表示,即 x=A/B/C/D/E/F/G/H/I,下同)的工作模式,寄存器描述如下图所示。
4.2、GPIO端口输出类型寄存器
该寄存器用于控制 GPIOx 的输出类型,寄存器描述如下图所示。
该寄存器仅用于输出模式,在输入模式(MODER[1:0]=00/11 时)下不起作用。该寄存器低16 位有效,每一个位控制一个 IO 口,复位后,该寄存器值均为 0,也就是在输出模式下 IO 口默认为推挽输出。
4.3、GPIO端口输出速度寄存器
该寄存器仅用于输出模式,在输入模式(MODER[1:0]=00/11 时)下不起作用。该寄存器低16 位有效,每两个位控制一个 IO 口。
4.4、GPIO端口上拉/下拉寄存器
该寄存器用于控制 GPIOx 的上拉/下拉,寄存器描述如下图所示。
该寄存器每两个位控制一个 IO 口,用于设置上下拉,复位后,该寄存器值一般为 0,即无上拉或下拉。
4.5、GPIO端口输入数据寄存器
该寄存器用于获取 GPIOx 的输入高低电平,寄存器描述如下图所示。
该寄存器低 16 位有效,分别对应每一组 GPIO 的 16 个引脚。当 CPU 访问该寄存器,如果对应的某位为 0(IDRy=0),则说明该 IO 口输入的是低电平,如果是 1(IDRy=1),则表示输入的是高电平,y=0~15。
4.6、GPIO端口输出数据寄存器
该寄存器低 16 位有效,分别对应每一组 GPIO 的 16 个引脚。当 CPU 写访问该寄存器,如果对应的某位写 0(ODRy=0),则表示设置该 IO 口输出的是低电平,如果写 1(ODRy=1),则表示设置该 IO 口输出的是高电平,y=0~15。
除了 ODR 寄存器,还有一个寄存器也是用于控制 GPIO 输出的,它就是 BSRR 寄存器。
4.7、GPIO端口置位/复位寄存器
该寄存器也用于控制 GPIOx 的输出高电平或者低电平,寄存器描述如下图所示。
为什么有了 ODR 寄存器,还要这个 BDRR 寄存器呢?这是因为 BSRR 寄存器是只写权限,而 ODR 寄存器是可读可写权限。
BSRR 寄存器 32 位有效,对于低 16 位(0-15),我们往相应的位写 1(BSy=1),那么对应的 IO 口会输出高电平,往相应的位写 0(BSy=0),对 IO 口没有任何影响,高 16 位(16-31)作用刚好相反,对相应的位写 1(BRy=1)会输出低电平,写 0(BRy=0)没有任何影响,y=0~15。也就是说,对于 BSRR 寄存器,你写 0 的话,对 IO 口电平是没有任何影响的。我们要设置某个 IO 口电平,只需要相关位设置为 1 即可。
ODR 寄存器,我们要设置某个 IO 口电平,我们首先需要读出来 ODR 寄存器的值,然后对整个 ODR 寄存器重新赋值来达到设置某个或者某些 IO 口的目的。
BSRR 寄存器还有一个好处,就是 BSRR 寄存器改变引脚状态的时候,不会被中断打断,而 ODR 寄存器有被中断打断的风险。
五、GPIO配置步骤
5.1、使能时钟
有关使能 GPIO 时钟的宏定义代码定义在 stm32f4xx_hal_rcc.h 和 stm32f4xx_hal_rcc_ex.h 头文件中。
#define __HAL_RCC_GPIOA_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOB_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOC_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOH_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOHEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOHEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOD_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIODEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIODEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOE_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOEEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOEEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOF_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOFEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOFEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOG_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOGEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOGEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOI_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOIEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOIEN);\
UNUSED(tmpreg); \
} while(0U)
5.2、设置工作模式
HAL 库中,提供 HAL_GPIO_Init() 函数用于配置 GPIO 功能模式,初始化 GPIO。我们还可以设置 EXTI 功能。该函数的声明如下:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
该函数的第一个形参 GPIOx 用来指定端口号,可选值如下:
#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)
第二个参数是 GPIO_InitTypeDef 类型的结构体变量,用来设置 GPIO 的工作模式,其定义如下:
typedef struct
{
uint32_t Pin; // 引脚号
uint32_t Mode; // 模式设置
uint32_t Pull; // 上下拉设置
uint32_t Speed; // 速度设置
uint32_t Alternate; // 复用功能设置
}GPIO_InitTypeDef;
成员 Pin 表示 引脚号,范围:GPIO_PIN_0 到 GPIO_PIN_15,另外还有 GPIO_PIN_All 和GPIO_PIN_MASK 可选。
#define GPIO_PIN_0 ((uint16_t)0x0001) /* Pin 0 selected */
#define GPIO_PIN_1 ((uint16_t)0x0002) /* Pin 1 selected */
#define GPIO_PIN_2 ((uint16_t)0x0004) /* Pin 2 selected */
#define GPIO_PIN_3 ((uint16_t)0x0008) /* Pin 3 selected */
#define GPIO_PIN_4 ((uint16_t)0x0010) /* Pin 4 selected */
#define GPIO_PIN_5 ((uint16_t)0x0020) /* Pin 5 selected */
#define GPIO_PIN_6 ((uint16_t)0x0040) /* Pin 6 selected */
#define GPIO_PIN_7 ((uint16_t)0x0080) /* Pin 7 selected */
#define GPIO_PIN_8 ((uint16_t)0x0100) /* Pin 8 selected */
#define GPIO_PIN_9 ((uint16_t)0x0200) /* Pin 9 selected */
#define GPIO_PIN_10 ((uint16_t)0x0400) /* Pin 10 selected */
#define GPIO_PIN_11 ((uint16_t)0x0800) /* Pin 11 selected */
#define GPIO_PIN_12 ((uint16_t)0x1000) /* Pin 12 selected */
#define GPIO_PIN_13 ((uint16_t)0x2000) /* Pin 13 selected */
#define GPIO_PIN_14 ((uint16_t)0x4000) /* Pin 14 selected */
#define GPIO_PIN_15 ((uint16_t)0x8000) /* Pin 15 selected */
#define GPIO_PIN_All ((uint16_t)0xFFFF) /* All pins selected */
#define GPIO_PIN_MASK 0x0000FFFFU /* PIN mask for assert test */
成员 Mode 是 GPIO 的 模式选择,有以下选择项:
#define GPIO_MODE_INPUT 0x00000000U // 输入模式
#define GPIO_MODE_OUTPUT_PP 0x00000001U // 推挽模式
#define GPIO_MODE_OUTPUT_OD 0x00000011U // 开漏模式
#define GPIO_MODE_AF_PP 0x00000002U // 推挽式复用
#define GPIO_MODE_AF_OD 0x00000012U // 开漏式复用
#define GPIO_MODE_ANALOG 0x00000003U // 模拟模式
#define GPIO_MODE_IT_RISING 0x10110000U // 外部中断,上升沿触发检测
#define GPIO_MODE_IT_FALLING 0x10210000U // 外部中断,下降沿触发检测
#define GPIO_MODE_IT_RISING_FALLING 0x10310000U // 外部中断,双边沿触发检测
#define GPIO_MODE_EVT_RISING 0x10120000U // 外部事件,上升沿触发检测
#define GPIO_MODE_EVT_FALLING 0x10220000U // 外部事件,下降沿触发检测
#define GPIO_MODE_EVT_RISING_FALLING 0x10320000U // 外部事件,双边沿触发检测
成员 Pull 用于 配置上下拉电阻,有以下选择项:
#define GPIO_NOPULL 0x00000000U // 无上下拉
#define GPIO_PULLUP 0x00000001U // 上拉
#define GPIO_PULLDOWN 0x00000002U // 下拉
成员 Speed 用于 配置 GPIO 的速度,有以下选择项:
#define GPIO_SPEED_FREQ_LOW 0x00000000U // 低速
#define GPIO_SPEED_FREQ_MEDIUM 0x00000001U // 中速
#define GPIO_SPEED_FREQ_HIGH 0x00000002U // 高速
#define GPIO_SPEED_FREQ_VERY_HIGH 0x00000003U // 极速
成员 Alternate 用于 配置具体的复用功能,不同的 GPIO 口可以复用的功能不同,具体可参考数据手册。
5.3、配置输出状态
我们可以通过 HAL 库提供的 HAL_GPIO_WritePin() 函数配置引脚输出高电平或者低电平,通过 BSRR 寄存器复位或者置位操作。它的声明如下:
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);
第一个参数是 端口号,可以选择范围:GPIOA~GPIOG。
第二个参数是 引脚号,可以选择范围:GPIO_PIN_0 到 GPIO_PIN_15。
第三个参数是要设置 输出的状态,是枚举型,可选值如下:
typedef enum
{
GPIO_PIN_RESET = 0, // 低电平
GPIO_PIN_SET // 高电平
}GPIO_PinState;
我们还可以通过 HAL 库的 HAL_GPIO_TogglePin() 函数设置引脚的电平翻转,也是通过 BSRR 寄存器复位或者置位操作。其声明如下:
void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
第一个参数是 端口号,可以选择范围:GPIOA~GPIOG。
第二个参数是 引脚号,可以选择范围:GPIO_PIN_0 到 GPIO_PIN_15。
5.4、读取输入状态
我们可以通过 HAL 库的 HAL_GPIO_ReadPin() 函数读取 GPIO 引脚状态,通过 IDR 寄存器读取。其声明如下:
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
第一个参数是 端口号,可以选择范围:GPIOA~GPIOG。
第二个参数是 引脚号,可以选择范围:GPIO_PIN_0 到 GPIO_PIN_15。
该函数的返回值是 引脚状态值,是枚举型有两个选择:GPIO_PIN_SET 表示 高电平,GPIO_PIN_RESET 表示 低电平。