General-purpose input/output通用型输入输出接口,类似于51的p0-p3
GPI 通用输入
GPO 通用输出,通过寄存器来控制其输入输出的电平。
在Stm32F407中,STM32F407ZGT6
一共114个IO口
凡是数据手册中引脚描述,标有FT的标志就是兼容5V
不要直接用IO口驱动感性负载(电机,继电器等),因为断开瞬间会产生很大的反电动势,将IO烧坏。(可以接一个泄放二极管)
默认的五个口不能作为IO口输出

只有禁止了相应的端口,才能释放IO引脚

注释:I/O引脚:接收外部发出的信号或者向外部发出信号。

GPIOA 的 7 个寄存器都是 32 位的,所以每个寄存器占有 4个地址,一共占用 28 个地址,地址偏移范围为(000h~01Bh)。
因为 GPIO 都是挂载在 APB2 总线之上,所以它的基地址是由 APB2 总线的基地址+GPIOA 在 APB2 总线上的偏移地址决定的。
打开 stm32f10x.h 定位到 GPIO_TypeDef 定义处:
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t BRR;
__IO uint32_t LCKR;
} GPIO_TypeDef;
然后定位到:
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
可以看出,GPIOA 是将 GPIOA_BASE 强制转换为 GPIO_TypeDef 指针,
GPIOA 指向地址 GPIOA_BASE,GPIOA_BASE 存放的数据类型为 GPIO_TypeDef。
查看 GPIOA_BASE的宏定义:
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
依次类推,可以找到最顶层:
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define PERIPH_BASE ((uint32_t)0x40000000)
所以我们便可以算出 GPIOA 的基地址位:
GPIOA_BASE= 0x40000000+0x10000+0x0800=0x40010800
GPIOA 的各个寄存器对于 GPIOA 基地址的偏移地址,所以我们自然可以算出来每个寄存器的地址。
GPIOA 的寄存器的地址=GPIOA 基地址+寄存器相对 GPIOA 基地址的偏移值
那就是结构体存储的成员他们的地址是连续的

浮空输入GPIO_IN_FLOATING,可以做KEY识别,RX1。
GPIO_IPU——IO内部上拉电阻输入。
GPIO_IPD—— IO内部下拉电阻输入。
GPIO_AIN ——应用ADC模拟输入,或者低功耗下省电。
输出端相当于三极管的集电极,要得到高电平状态需要上拉电阻才行。适合于做电流型的驱动,其吸收电流的能力相对强(一般20mA以内)。

GPIO_OUT_OD ——IO输出0接GND,IO输出1,悬空,需要外接上拉电阻,才能实现输出高电平。当输出为1时,IO口的状态由上拉电阻拉高电平,但由于是开漏输出模式,这样IO口也就可以由外部电路改变为低电平或不变。可以读IO输入电平变化,实现C51的IO双向功能。
开漏输出和推挽输出的区别最普遍的说法就是开漏输出无法真正输出高电平,即高电平时没有驱动能力,需要借助外部上拉电阻完成对外驱动。


GPIO_OUT_PP ——IO输出0-接GND, IO输出1 -接VCC,读输入值是未知的。
推挽输出结构是由两个MOS或者三极管受到互补控制的信号控制,两个管子时钟一个在导通,一个在截止。
推挽输出的最大特点是可以真正能真正的输出高电平和低电平,在两种电平下都具有驱动能力。
推挽输出的缺点是,如果当两个推挽输出结构相连在一起,一个输出高电平,另一个输出低电平,电流会从第一个引脚的VCC通过上端MOS再经过第二个引脚的下端MOS直接流向GND,也就是会发生短路,进而可能造成端口的损害。这也是为什么推挽输出不能实现" 线与"的原因。
GPIO_AF_PP ——片内外设功能(I2C的SCL,SDA)。
GPIO_AF_OD——片内外设功能(TX1,MOSI,MISO.SCK.SS)。
10个寄存器可以控制一组GPIO的16个IO口。
一个端口模式寄存器(GPIOx_MODER)
一个端口输出类型寄存器(GPIOx_OTYPER)
一个端口输出速度寄存器(GPIOx_OSPEEDR)
一个端口上拉下拉寄存器(GPIOx_PUPDR)
一个端口输入数据寄存器(GPIOx_IDR)
一个端口输出数据寄存器(GPIOx_ODR)
一个端口置位/复位寄存器(GPIOx_BSRR)
一个端口配置锁存寄存器(GPIOx_LCKR)
两个复用功能寄存器(低位GPIOx_AFRL & GPIOx_AFRH)

x = A…I


x = A…I
中文参考手册7.4.2,187


GPIO port output speed register


可以先对寄存器的值进行&清零操作
GPIOA->CRL&=0XFFFFFF0F; //将第 4-7 位清 0
然后再与需要设置的值进行|或运算
GPIOA->CRL|=0X00000040; //设置相应位的值,不改变其他位的值
将 BSRR 寄存器的第 pinpos 位设置为 1,1左移了pinpos位
GPIOx->BSRR = (((uint32_t)0x01) << pinpos);
可以提高代码的可读性以及可重用性。
类似这样的代码很多:
GPIOA->ODR|=1<<5; //PA.5 输出高,不改变其他位
实现功能:控制某个GPIO引脚的输出电平(拉高 / 拉低)
GPIO_SetBits 拉高引脚输出电平
GPIO_ResetBits 拉低引脚输出电平
顾名思义
#define PBout(n) BIT_ADDR(GPIOB_ODR_Addr,n) //输出
#define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) //输入
通过调用函数 GPIO_ReadInputDataBit()来读取 IO 口的状态的。
实现通过4 个按钮(WK_UP、KEY0、KEY1 和 KEY2),来控制板上的 2 个 LED(DS0 和 DS1)和蜂鸣器
其中 WK_UP 控制蜂鸣器,按一次叫,再按一次停;
KEY2 控制 DS0,按一次亮,再按一次灭;
KEY1 控制 DS1,效果同KEY2;
KEY0 则同时控制 DS0 和 DS1,按一次,他们的状态就翻转一次。
按键与端口的对应关系见(按键)
按键初始化
#include "key.h"
#include "sys.h"
#include "delay.h"
//按键初始化函数
void KEY_Init(void) //IO 初始化
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|
RCC_APB2Periph_GPIOE,ENABLE); //使能 PORTA,PORTE 时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4;//GPIOE.2~4
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
GPIO_Init(GPIOE, &GPIO_InitStructure); //初始化 GPIOE2,3,4
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //初始化 WK_UP-->GPIOA.0
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0 设置成输入,下拉
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.0
}
//按键处理函数
//返回按键值
//mode:0,不支持连续按;1,支持连续按;
//0,没有任何按键按下;1,KEY0 按下;2,KEY1 按下;3,KEY2 按下 ;4,KEY3 按下 WK_UP
//注意此函数有响应优先级,KEY0>KEY1>KEY2>KEY3!!
u8 KEY_Scan(u8 mode)
{
static u8 key_up=1; //按键按松开标志
if(mode)key_up=1; //支持连按
if(key_up&&(KEY0==0||KEY1==0||KEY2==0||KEY3==1))
{
delay_ms(10); //去抖动
key_up=0;
if(KEY0==0)return KEY0_PRES;
else if(KEY1==0)return KEY1_PRES;
else if(KEY2==0)return KEY2_PRES;
else if(KEY3==1)return WKUP_PRES;
}else if(KEY0==1&&KEY1==1&&KEY2==1&&KEY3==0)key_up=1;
return 0; // 无按键按下
}
KEY_Scan()函数,则是用来扫描这 4 个 IO 口是否有按键按下。
KEY_Scan()函数,支持两种扫描方式,通过 mode 参数来设置。
当 mode 为 0 的时候,KEY_Scan()函数将不支持连续按,扫描某个按键,该按键按下之后必须要松开,才能第二次触发,否则不会再响应这个按键,这样的好处就是可以防止按一次多次触发,而坏处就是在需要长按的时候比较不合适。
当 mode 为 1 的时候,KEY_Scan()函数将支持连续按,如果某个按键一直按下,则会一直返回这个按键的键值,这样可以方便的实现长按检测。
要注意的就是,该函数的按键扫描是有优先级的,最优先的是 KEY0,第二优先的是 KEY1,接着 KEY2,最后是 WK_UP 按键。
该函数有返回值,如果有按键按下,则返回非 0 值,如果没有或者按键不正确,则返回 0。
头文件 key.h 里面的代码:
#ifndef __KEY_H
#define __KEY_H
#include "sys.h"
#define KEY0 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)//读取按键 0
#define KEY1 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)//读取按键 1
#define KEY2 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2)//读取按键 2
#define WK_UP GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)//读取按键 3(WK_UP)
#define KEY0_PRES 1 //KEY0 按下
#define KEY1_PRES 2 //KEY1 按下
#define KEY2_PRES 3 //KEY2 按下
#define WKUP_PRES 4 //WK_UP 按下(即 WK_UP/WK_UP)
void KEY_Init(void); //IO 初始化
u8 KEY_Scan(u8); //按键扫描函数
#endif
主函数的代码
#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "beep.h"
//ALIENTEK 战舰 STM32 开发板实验 3
//按键输入实验
int main(void)
{
u8 key;
delay_init(); //延时函数初始化
LED_Init(); //LED 端口初始化
KEY_Init(); //初始化与按键连接的硬件接口
BEEP_Init(); //初始化蜂鸣器端口
LED0=0; //先点亮红灯
while(1)
{
key =KEY_Scan(0); //得到键值
if(key)
{ switch(t)
{ case WKUP_PRES: //控制蜂鸣器
BEEP=!BEEP;break;
case KEY2_PRES: //控制 LED0 翻转
LED0=!LED0;break;
case KEY1_PRES: //控制 LED1 翻转
LED1=!LED1;break;
case KEY0_PRES: //同时控制 LED0,LED1 翻转
LED0=!LED0;
LED1=!LED1;break;
}
}else delay_ms(10);
}
}
在仿真调试的时候MDK 不会考虑 STM32 自带的上拉和下拉,所以我们得自己手动设置一下,来使得其初始状态和外部硬件的状态一摸一样。
在 General Purpose I/O E 窗口内的 Pins 里面勾选 2、3、4 位,
要改变状态就把 Pins 的 PE2 取消勾选,再次执行过这句,得到 key 的值为 3
这里需要注意的是:在配置 STM32 外设的时候,任何时候都要先使能该外设的时钟。
F407的挂载和F103是有差别的。 可以先参考407时钟的RCC相关函数
官方库提供了五个打开 GPIO 和外设时钟的函数分别为:
void RCC_AHB1PeriphClockCmd(uint32_t RCC_AHB1Periph, FunctionalState NewState);
void RCC_AHB2PeriphClockCmd(uint32_t RCC_AHB2Periph, FunctionalState NewState);
void RCC_AHB3PeriphClockCmd(uint32_t RCC_AHB3Periph, FunctionalState NewState);
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
F407中GPIO是挂载在AHB下的。
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能 GPIOA 时钟
GPIO的每个 IO 口可以自由编程,但 IO 口寄存器必须要按 32 位字被访问。
typedef struct
{
uint32_t GPIO_Pin;
GPIOMode_TypeDef GPIO_Mode;
GPIOSpeed_TypeDef GPIO_Speed;
GPIOOType_TypeDef GPIO_OType;
GPIOPuPd_TypeDef GPIO_PuPd;
}GPIO_InitTypeDef;
初始化代码:
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9//GPIOF9
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
GPIO_Init(GPIOF, &GPIO_InitStructure);//初始化 GPIO
一般外设使用时都用的是复用模式。
typedef enum
{
GPIO_Mode_IN = 0x00, /* 复位状态的输入!< GPIO Input Mode */
GPIO_Mode_OUT = 0x01, /*通用输出模式!< GPIO Output Mode */
GPIO_Mode_AF = 0x02, /*复用功能模式!< GPIO Alternate function Mode */
GPIO_Mode_AN = 0x03 /*模拟输入模式!< GPIO Analog Mode */
}GPIOMode_TypeDef;
F407的GPIO_Speed与F103也是有区别的,一般使用50M就可以。
typedef enum
{
GPIO_Low_Speed = 0x00, /*!< Low speed */
GPIO_Medium_Speed = 0x01, /*!< Medium speed */
GPIO_Fast_Speed = 0x02, /*!< Fast speed */
GPIO_High_Speed = 0x03 /*!< High speed */
}GPIOSpeed_TypeDef;
/* Add legacy definition */
#define GPIO_Speed_2MHz GPIO_Low_Speed
#define GPIO_Speed_25MHz GPIO_Medium_Speed
#define GPIO_Speed_50MHz GPIO_Fast_Speed
#define GPIO_Speed_100MHz GPIO_High_Speed
有四个可选值。实际上这就是配置的 GPIO对应的 OSPEEDR 寄存器的值。
GPIO_OType是F407新增的,GPIO 的输出类型设置,实际上是配置的 GPIO 的 OTYPER 寄存器的值。
typedef enum
{
GPIO_OType_PP = 0x00,
GPIO_OType_OD = 0x01
}GPIOOType_TypeDef;
输出推挽模式,那么选择值 GPIO_OType_PP。
输出开漏模式,那么设置值为 GPIO_OType_OD。
F407新增,设置 IO 口的上下拉,实际上就是设置 GPIO 的 PUPDR 寄存器的值。
typedef enum
{
GPIO_PuPd_NOPULL = 0x00,
GPIO_PuPd_UP = 0x01,
GPIO_PuPd_DOWN = 0x02
}GPIOPuPd_TypeDef;
每个GPIO口有很多功能,默认的是作为普通IO口,此时不配置复用相关的东西。
复用:内置外设基本上与I/O口共用管脚的,也就是I/O管脚的复用。
内置外设:
除单片机内核外,单片机内部可实现功能的设备都是内置外设,如串口,ADC 等。
区别:
STM32F4 系列微控制器 IO 引脚通过一个复用器连接到内置外设或模块。


32 位寄存器 GPIOx_AFRL 每四个位控制一个 IO 口,所以每个寄存器控制32/4=8 个 IO 口。寄存器对应四位的值配置决定这个 IO 映射到哪个复用功能 AF。
配置 GPIOx_AFRL 或者 GPIOx_AFRH 寄存器
同样的,串口1的发送接收引脚是PA9,PA10。当我们把PA9,PA10不用做GPIO,而用做复用功能串口1的发送接收引脚的时候,叫端口复用。
使用配置:
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//使能USART1时钟
//串口1对应引脚复用映射
GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1); //*PA9 连接 AF7,复用为 USART1_TX */
GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1); ///* PA10 连接 AF7,复用为 USART1_RX*/
//USART1端口配置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; //GPIOA9与GPIOA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_Init(GPIOA,&GPIO_InitStructure); //初始化PA9,PA10
复用外设选择:
#define IS_GPIO_AF(AF) (((AF) == GPIO_AF_RTC_50Hz) ||((AF) == GPIO_AF_TIM14) || \
((AF) == GPIO_AF_MCO) || ((AF) == GPIO_AF_TAMPER) || \
((AF) == GPIO_AF_SWJ) || ((AF) == GPIO_AF_TRACE) || \
((AF) == GPIO_AF_TIM1) || ((AF) == GPIO_AF_TIM2) || \
((AF) == GPIO_AF_TIM3) || ((AF) == GPIO_AF_TIM4) || \
((AF) == GPIO_AF_TIM5) || ((AF) == GPIO_AF_TIM8) || \
((AF) == GPIO_AF_I2C1) || ((AF) == GPIO_AF_I2C2) || \
((AF) == GPIO_AF_I2C3) || ((AF) == GPIO_AF_SPI1) || \
((AF) == GPIO_AF_SPI2) || ((AF) == GPIO_AF_TIM13) || \
((AF) == GPIO_AF_SPI3) || ((AF) == GPIO_AF_TIM14) || \
((AF) == GPIO_AF_USART1) || ((AF) == GPIO_AF_USART2) || \
((AF) == GPIO_AF_USART3) || ((AF) == GPIO_AF_UART4) || \
((AF) == GPIO_AF_UART5) || ((AF) == GPIO_AF_USART6) || \
((AF) == GPIO_AF_CAN1) || ((AF) == GPIO_AF_CAN2) || \
((AF) == GPIO_AF_OTG_FS) || ((AF) == GPIO_AF_OTG_HS) || \
((AF) == GPIO_AF_ETH) || ((AF) == GPIO_AF_OTG_HS_FS) || \
((AF) == GPIO_AF_SDIO) || ((AF) == GPIO_AF_DCMI) || \
((AF) == GPIO_AF_EVENTOUT) || ((AF) == GPIO_AF_FSMC))

当输出1的时候无电压差,灭。
当输出0 的时候有压差,亮。
应该设置推挽输出,上拉,默认就是灭。
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
第一个作用:读取某个GPIO的输入电平。实际操作的是GPIOx_IDR寄存器。
GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5);//读取GPIOA.5的输入电平
第二个作用:读取某组GPIO的输入电平。实际操作的是GPIOx_IDR寄存器。
GPIO_ReadInputData(GPIOA);//读取GPIOA组中所有io口输入电平
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);
作用:读取某个GPIO的输出电平。实际操作的是GPIO_ODR寄存器。
GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_5);//读取GPIOA.5的输出电平
作用:读取某组GPIO的输出电平。实际操作的是GPIO_ODR寄存器。
GPIO_ReadOutputData(GPIOA);//读取GPIOA组中所有io口输出电平
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
作用:设置某个IO口输出为高电平(1)。实际操作BSRRL寄存器
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
作用:设置某个IO口输出为低电平(0)。实际操作的BSRRH寄存器。
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);
这两个函数不常用,也是用来设置IO口输出电平。
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);//使能GPIOF时钟
//GPIOF9,F10初始化设置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;//LED0和LED1对应IO口
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
GPIO_Init(GPIOF, &GPIO_InitStructure);//初始化GPIO
GPIO_SetBits(GPIOF,GPIO_Pin_9 | GPIO_Pin_10);//GPIOF9,F10设置高,灯灭
}
int main(void)
{
delay_init(168); //初始化延时函数
LED_Init(); //初始化LED端口
/**下面是通过直接操作库函数的方式实现IO控制**/
while(1)
{
GPIO_ResetBits(GPIOF,GPIO_Pin_9); //LED0对应引脚GPIOF.9拉低,亮 等同LED0=0;
GPIO_SetBits(GPIOF,GPIO_Pin_10); //LED1对应引脚GPIOF.10拉高,灭 等同LED1=1;
delay_ms(500); //延时300ms
GPIO_SetBits(GPIOF,GPIO_Pin_9); //LED0对应引脚GPIOF.0拉高,灭 等同LED0=1;
GPIO_ResetBits(GPIOF,GPIO_Pin_10); //LED1对应引脚GPIOF.10拉低,亮 等同LED1=0;
delay_ms(500); //延时300ms
}
}