目录
3.1 浮空输入:GPIO_Mode_IN_FLOATING
一、GPIO 简介
GPIO (General Purpose Input Output) ,意为通用输入输出口,可配置为8种输入输出模式,引脚电平:0V~3.3V,部分引脚可容忍5V(容忍的意思是可以在这个端口输入5V 的电压,也认为是高电平,具体哪些端口可以容忍 5 V 需要查找STM32的引脚定义。在引脚定义中带FT,意为Five Tolerate,就是可以输入 5 V 的端口)
输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器模拟通信协议输出时序等;
输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等。
二、GPIO的基本结构
在所有的STM32中,所有的GPIO外设都挂载在APB2外设总线上。其中GPIO外设是按照GPIOA、GPIOB、GPIOC等命名的。
每个GPIO外设有16个引脚。以GPIOA为例,它的引脚编号分别为PA0,PA1,PA2 … PA15。其他GPIO的引脚也是这样命名的。
在GPIO模块内部,主要包含了寄存器和驱动器两个模块。寄存器就是一段特殊的存储器,内核可以通过APB2总线对寄存器进行读写,以完成输出电平和读取电平的功能。
STM32是32位的单片机,其中的寄存器都是32位的。而端口只有16个,故寄存器只有低16位有对应的端口,高16位没有用到。
驱动器的作用是增加信号的驱动能力。寄存器只负责存储数据,需要用驱动器来增大驱动能力。
三、GPIO的八种工作模式
通过配置GPIO的端口配置寄存器,端口可以配置成以下8种模式:
3.1 浮空输入:GPIO_Mode_IN_FLOATING
浮空输入(Floating Input):浮空输入模式是一种高阻抗输入模式。在该模式下,引脚不连接到外部电路,处于高阻抗状态。可以通过读取引脚电平来检测外部信号。
- 特点:引脚处于高阻抗状态,未连接到外部电路,测量外部信号电平。
- 应用场景:接收外部信号的状态,如按键输入、传感器输入等
3.2 上拉输入:GPIO_Mode_IPU
上拉输入(Pull-up Input):上拉输入模式是一种具有内部上拉电阻的GPIO输入模式。在该模式下,引脚连接到外部电路,通过内部上拉电阻来维持默认电平为高电平。
- 特点:具有内部上拉电阻,引脚的默认电平为高电平。
- 应用场景:检测外部信号为低电平时,例如按键按下。
3.3 下拉输入:GPIO_Mode_IPD
下拉输入(Pull-down Input):下拉输入模式是一种具有内部下拉电阻的GPIO输入模式。在该模式下,引脚连接到外部电路,通过内部下拉电阻来维持默认电平为低电平。
- 特点:具有内部下拉电阻,引脚的默认电平为低电平。
- 应用场景:检测外部信号为高电平时,例如按键抬起。
3.4 模拟输入:GPIO_Mode_AIN
模拟输入(Analog Input):模拟输入模式是一种用于ADC(模数转换器)输入的特殊模式。在该模式下,引脚可以接收连续变化的模拟信号。
- 特点:用于接收连续变化的模拟信号,通常与ADC(模数转换器)配合使用。
- 应用场景:测量传感器信号、音频输入等模拟信号的变化
3.5 开漏输出:GPIO_Mode_Out_OD
开漏输出(Open-Drain Output):开漏输出模式是一种能够输出低电平和高阻抗的GPIO模式。在该模式下,引脚只能输出低电平,要输出高电平需要通过外部上拉电阻或其他方式。通常用于与外部器件连接,例如与开漏输出的I2C总线器件进行通信。
- 特点:只能输出低电平,需要外部上拉电阻将引脚拉高;具有一定的驱动能力。
- 应用场景:与外部器件连接时,如I2C总线,用于与其他设备进行通信。
3.6 推挽输出:GPIO_Mode_Out_PP
推挽输出(Push-Pull Output):推挽输出模式是最常见的GPIO输出模式。在该模式下,引脚可以输出高电平或低电平,同时具有一定的驱动能力。引脚在输出低电平时形成低阻抗,输出高电平时形成高阻抗,可以驱动外部电路。
- 特点:可以输出高电平和低电平,具有一定的驱动能力。
- 应用场景:用于驱动外部电路,如控制LED灯、驱动其他逻辑电路等。
3.7 复用开漏输出:GPIO_Mode_AF_OD
复用开漏输出(AF Open-Drain Output):复用开漏输出模式允许将GPIO引脚用作特定外设功能。在该模式下,引脚只能输出低电平,要输出高电平需要通过外部上拉电阻或其他方式。
- 特点:具有开漏输出的特性,可用于将GPIO引脚用作特定外设的功能。
- 应用场景:连接到外设的特殊功能引脚,如I2C总线通信引脚、故障信号输出等。
3.8 复用推挽输出:GPIO_Mode_AF_PP
复用推挽输出(AF Push-Pull Output):复用推挽输出模式允许将GPIO引脚用作特定外设功能。在该模式下,引脚可以输出高电平或低电平,并具有一定的驱动能力。
- 特点:具有推挽输出的特性,可用于将GPIO引脚用作特定外设的功能。
- 应用场景:连接到外设的特殊功能引脚,如UART串口通信引脚、PWM输出等。
四、GPIO的使用
4.1 常用函数
4.1.1 常用的RCC库函数
void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState);
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);
4.1.2 常用的GPIO库函数
GPIO外设有很多库函数,以下几个函数比较常用(GPIO_Init和8个读写函数最重要):
// 使指定的GPIO外设复位
void GPIO_DeInit(GPIO_TypeDef* GPIOx);
// 使指定的AFIO外设复位
void GPIO_AFIODeInit(void);
// 用结构体的参数初始化GPIO口
// 基本上STM32的所有外设都使用Init函数进行初始化
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
// 给结构体变量赋一个默认值
void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct);
// 下面四个是GPIO的写入函数
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);
// 下面四个是GPIO的输出函数
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal); // 这个函数可以对16个端口同时进行写入操作
4.2 操作步骤
操作STM32的GPIO输出一共需要三个步骤(下面步骤中设计RCC和GPIO两个外设):
- 使用RCC开启GPIO的时钟;
- 使用
GPIO_Init
函数初始化GPIO - 使用输出或者输入函数控制GPIO口
4.3 GPIO输出函数和GPIO输入函数的使用
4.3.1 GPIO输出函数的应用 - LED闪烁
接线图和代码如下所示:
#include "stm32f10x.h" // Device header
#include "Delay.h"
int main()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 定义结构体变量,它是一个局部变量,有些过时的编译器不支持在程序中间定义局部变量,需要在最前面声明定义
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// GPIO_SetBits(GPIOA, GPIO_Pin_0); // 给相应端口设置高电平
// GPIO_ResetBits(GPIOA, GPIO_Pin_0); // 给相应端口设置低电平
// GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET); // 在相应端口写入数据 (Bit_RESET是一个枚举,它就是写入的数据,意为写入低电平)
GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET); // 在相应端口写入数据,意为写入高电平
while(1)
{
// 下面三种方式都可以实现LED闪烁
GPIO_ResetBits(GPIOA, GPIO_Pin_0);
Delay_ms(500);
GPIO_SetBits(GPIOA, GPIO_Pin_0);
Delay_ms(500);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);
Delay_ms(500);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET);
Delay_ms(500);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)0); // 第三个参数直接写1,或者写0会报出一个警告,原因是1和0不是BitAction这个枚举类型,所以需要用到强制类型转换
Delay_ms(500);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)1);
Delay_ms(500);
}
}
4.3.2 GPIO输入函数的应用 - 按键读取LED亮灭
接线图和代码如下所示:
- LED.h
#ifndef __LED_H__
#define __LED_H__
void LED_Init(void);
void LED1_ON(void);
void LED1_OFF(void);
void LED2_ON(void);
void LED2_OFF(void);
void LED1_Turn(void);
void LED2_Turn(void);
#endif
- LED.c
#include "stm32f10x.h" // Device header
/**
* @brief LED的初始化函数,将GPIOA配置为推挽输出模式
* @param 无
* @retval 无
*/
void LED_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 如果不加入以下操作,GPIO配置完成后默认为低电平
GPIO_SetBits(GPIOA, GPIO_Pin_1 | GPIO_Pin_2); // 默认将GPIO设置为高电平
}
/**
* @brief 点亮LED1,将PA1置低电平
* @param 无
* @retval 无
*/
void LED1_ON(void)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_1);
}
/**
* @brief 熄灭LED1,将PA1置高电平
* @param 无
* @retval 无
*/
void LED1_OFF(void)
{
GPIO_SetBits(GPIOA, GPIO_Pin_1);
}
/**
* @brief 点亮LED2,将PA2置低电平
* @param 无
* @retval 无
*/
void LED2_ON(void)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_2);
}
/**
* @brief 熄灭LED2,将PA2置高电平
* @param 无
* @retval 无
*/
void LED2_OFF(void)
{
GPIO_SetBits(GPIOA, GPIO_Pin_2);
}
/**
* @brief LED1的翻转函数,实现按一下点亮,再按一下熄灭
* @param 无
* @retval 无
*/
void LED1_Turn(void)
{
if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_1) == 0) // GPIOA处于输出模式,如果此时GPIOA_Pin_1输出0(LED1亮)
{
GPIO_SetBits(GPIOA, GPIO_Pin_1); // 将灯熄灭
}
else // 如果此时GPIOA_Pin_1输出为1(LED1灭)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_1); // 将灯点亮
}
}
/**
* @brief LED2的翻转函数,实现按一下点亮,再按一下熄灭
* @param 无
* @retval 无
*/
void LED2_Turn(void) // LED2的翻转函数,实现按一下点亮,再按一下熄灭
{
if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_2) == 0) // GPIOA处于输出模式,如果此时GPIOA_Pin_2输出0(LED2亮)
{
GPIO_SetBits(GPIOA, GPIO_Pin_2); // 将灯熄灭
}
else // 如果此时GPIOA_Pin_2输出为1(LED2灭)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_2); // 将灯点亮
}
}
- Key.h
#ifndef __KEY_H
#define __KEY_H
void Key_Init(void);
uint8_t Key_GetNum(void);
#endif
- Key.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
/**
* @brief 按键初始化函数
* @param 无
* @retval 无
*/
void Key_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 这里的速度是GPIO的输出速度,在输入模式下这个参数选择没有用处
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
/**
* @brief 返回按下按键的值,若不按下按键默认返回0
* @param 无
* @retval KeyNum 按键对应的值
*/
uint8_t Key_GetNum(void)
{
uint8_t KeyNum = 0;
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) // 读取1端口的值
{
Delay_ms(20);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0); // 如果不松手,程序将在此等待
Delay_ms(20);
KeyNum = 1;
}
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0)
{
Delay_ms(20);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0); // 如果不松手,程序将在此等待
Delay_ms(20);
KeyNum = 2;
}
return KeyNum;
}
- main.c
#include "stm32f10x.h" // Device header
#include "LED.h"
#include "Key.h"
uint8_t KeyNum; // 全局变量,用于存储按键对应的值
int main()
{
LED_Init();
Key_Init();
while(1)
{
KeyNum = Key_GetNum();
if (KeyNum == 1)
{
LED1_Turn();
}
if (KeyNum == 2)
{
LED2_Turn();
}
}
}
五、个人疑惑 & 个人理解
问题:
为什么点亮LED灯用GPIO的输出函数,读取按键用GPIO的输入函数 ?
个人理解:
点亮LED灯,引脚接的是VCC(正极),给引脚输出高电平就可以点亮LED灯,所以GPIO口用的工作模式是推挽输出,所以输出控制LED亮灭的函数就用输出函数;
读取按键,按键的其中一个引脚接的是GND(负极),按键被按下时属于低电平状态,想要松开按键之后是高电平,就必须在芯片内部设置上拉输入模式,这样在按键松开时就是高电平状态,既然用的是上拉输入模式,那读取按键,用的函数就是ReadInput输入函数。
推挽输出模式 --> 向连接的引脚输出高或低电平 。
上拉输入模式 --> 将输入到引脚的电平读取出来 。