一. 什么是GPIO
general purpose input output 通用输入输出端口
负责采集信息(输入),控制外部器件工作(输出)
二. STM32 GPIO简介
GPIO特点:快速翻转,只需要两个时钟周期就可以完成一次翻转(如果是F1,最大工作频率是72MHz,而GPIO的频率是36MHz)
每一个端口都可以中断
GPIO电气特性:
1.工作电压范围:2V到3.6V(一般接3.3V)
2.GPIO识别电压范围:
CMOS端口(不能兼容5V):-0.3V到1.164V是低电平识别范围,1.833V到3.6V是高电平识别范围,在两者之间的电压是不确定值
TTL端口(可以兼容5V,在手册中标着FT)
3.GPIO输出电流:单个IO口,最大输出25mA(芯片的总输入输出电流最大是150mA)
GPIO引脚分布:
电源引脚:V开头的 eg:VBAT
晶振引脚:我们要外接HSE和LSE两个晶振,所以需要四个晶振引脚
(低速晶振会加上32,而高速晶振的引脚没有)
复位引脚:NRST
下载引脚:很多
BOOT引脚:BOOT0,BOOT1
GPIO引脚:以P开头的
三. IO端口基本结构介绍
上面是F1的IO结构图,对于输入:可以选择上拉和下拉,然后可以选择几种输入:模拟输入(ADC/DAC),复用功能(就是第二功能),或者是输入到寄存器(IDR Input Data Register)中
对于输出:选择要输出哪一个,然后如果想要输出高电平,就将P-MOS导通,否则将N-MOS导通
注意:
1.在输入时,如果输入高电平5V,上面的二极管导通,下面的不导通,上面的二极管承受的压降是(5V-3.3V=1.7V),导致二极管烧坏
所以在输入时要接一个保护电阻,使得流过保护二极管的电流不要太大,二极管正常分压0.3V,输入一个3.6V的电压
同理输入-5V,实际输入了-0.3V
2.上下拉电阻:电阻大约在几十千欧,导致能够驱动的电流很小
3.施密特触发器:整形电路,将波形整流为方波(有一个正向阈值和负向阈值,比负阈值低就是低电平,比正阈值高就是高电平)
四. GPIO的八种模式
工作模式:
前三种称为通用输入,开漏输出,推挽输出称为通用输出
4.1 输入浮空
下图√表示打开,×表示关闭
由于浮空,所以上下拉都关闭
特点:在空闲时(也就是外部是高阻态的状态下),IO状态不确定,只由外部环境来决定
eg:外部输入3.3V就输入高电平,输入0V就输入低电平
(高阻态不是高电平,也不是低电平,是两者之间的,也就是不确定)
4.2 输入上拉
上拉电阻开启
如果外部不是高阻态,那么输入高电平就IO口寄存器就输入高电平,输入低电平寄存器就输入低电平
外部是高阻态时,IO呈现高电平(但是由于上拉电阻阻值很大,电流很小,称为弱上拉)
4.3 输入下拉
与输入上拉同理
空闲时,IO呈现低电平
4.4 模拟功能(也是输入)
施密特触发器关闭,使得信号进入模拟输入(eg:ADC和DAC这两个外设要用)
4.5 开漏输出(Open Drain)
上下拉电阻关闭(别影响输出),施密特触发器打开(可以读取到这个引脚的高低电平)
P-MOS管不导通(G极一直接3.3V高电平,一直不变)
如果ODR(Output Data Register)要输出0,在输出控制器中有一个逻辑非操作,导致N-MOS导通,IO输出0低电平
如果ODR输出1,N-MOS不导通,变成高阻态
(如果想要输出1,IO也输出1,那么需要在外面再多加一个上拉电阻在高阻态时输出1)
(在F4/F7/H7系列中,原来输入驱动器中的上下拉电阻可以兼任这个功能,少接一个上拉电阻)
注意:开漏输出只能输出低电平或者高阻态
4.6 开漏式复用功能
就是不是寄存器来输出,而是复用功能输出
注意:其他外设来控制复用功能的输出
4.7 推挽输出(Push Pull / PP)
上下拉电阻关闭,施密特打开
ODR输出1,P-MOS导通,ODR输出0,N-MOS导通
推挽输出既可以输出高电平也可以输出低电平,驱动能力强(因为没经过电阻,电流较大)
4.8 推挽输出复用功能
你知道的
一个问题:STM32能否输出5V电压?
答:当使用开漏输出,并且外接的上拉电阻的电压是5V才可以
五. GPIO寄存器介绍
对于STM32F407IGT6芯片,有GPIOA到GPIOI,每一个GPIO都有其对应的多个寄存器,而并不是一个寄存器就可以负责多个GPIO
MODER,OTYPER,OSPEEDR,PUPDR都是配置工作模式的,以及输出速度
5.1 端口配置高寄存器(CRH) 端口配置低寄存器(CRL)
一个寄存器有32位,两个寄存器一共64位,对于一个GPIO(eg:GPIOA)有16个口(PA0到PA15),平均分到每一个口上就是4位 eg:如图0到3就是PA0,4到7就是PA1,CRL控制了PA0到PA7,CRH控制了PA8到PA15
要配置相关的模式就要按照上面的改变寄存器的位
注意:如果要选择上拉还是下拉输入,都是选择CNF为10,此时就要选择ODR寄存器来控制,如果ODR是0就是下拉,ODR是1就是下拉(对于F1需要ODR来控制上下拉)
5.2 端口输出数据寄存器(ODR)
ODR寄存器的低16位可以用,但是高16位保留,始终记为0,所以PA0到PA15每一个口分到了一个位
ODR可读可写,如果要PC10这个口输出高电平3.3V,就将ODR10写为1
5.3 端口输入数据寄存器(IDR)
一个口分配到一个寄存器位
发现IDR是只读的寄存器,如果读到IDR0=1,那么说明PA0被输入了一个高电平
5.4 端口位设置/清除寄存器(BSRR)
set and reset register
BSRR可以控制ODR
BSRR的这些位只能进行写,不能读
BSSR的高16位可以对ODR的低16位进行复位(Set)
BSSR的低16位可以对ODR的低16位进行置位(Reset)
注意:Set的优先级比Reset更高
用ODR直接控制输出和BSSR控制ODR进而控制输出的区别:ODR寄存器在读和修改访问之间产生中断时可能会发生风险;BSRR无风险
eg:如果要修改ODR:GPIOB->ODR = 1<<3;(这句话先读取了ODR的状态,然后再加上了第三位的1进行ODR的修改)先读,再改,再写
而如果修改BSRR:GPIOB->ODR=0x00000008;(将BS3改为1),只有一个写的过程
综上,使用BSRR寄存器更安全
5.5 MODER寄存器(端口模式寄存器)+ 输出类型寄存器OTYPER + OSPEEDR端口输出速度寄存器 + 端口上下拉寄存器PUPDR
上面是MODER,下面是OTYPER
OSPEEDR(只有输出的时候才会配置速度寄存器,输入的时候不需要配置)
PUPDR
eg:要将PD13变成推挽输出模式:
MODER13设置为01(通用输出)OTYPER13设置为0(推挽输出)OSPEEDR13设置选一个速度
PUPDR13设置为00(推挽不需要上下拉)
可以参考下面表格:
六. 通用外设驱动模型
要驱动某一外设:
step1:初始化
进行时钟设置(开启时钟,选择时钟源),参数设置(选择工作模式等需要选择参数),IO设置(除了GPIO以外的外设,要设置需要哪几个IO,设置IO为复用功能),中断设置(开启中断等操作)
step2:读函数(可选)
从外设读取数据
step3:写函数(可选)
像外设写入数据
step4:中断服务函数(可选)
根据中断标志,处理外设中断
七. GPIO配置步骤
step1:使能时钟 __HAL_RCC_GPIOx_CLK_ENABLE();
step2:设置工作模式:HAL_GPIO_Init();
step3:设置输出状态(就是写函数)
HAL_GPIO_WritePin();
HAL_GPIO_TogglePin();
step4:读取输入状态(就是读函数)
HAL_GPIO_ReadPin();
7.1 __HAL_RCC_GPIOx_CLK_ENABLE();
使能时钟:
使用Ctrl+F找到这个宏定义(eg:选择GPIOA的进行搜索)
SET_BIT搜索一下
#define SET_BIT(REG, BIT) ((REG) |= (BIT))
就是一个或关系,将这个GPIOAEN或到AHB1ENR这个寄存器上,发现后面这个宏就是指代一个第零位,或关系使得第零位变成1,从而完成时钟使能
7.2 HAL_GPIO_Init();
初始化函数,要寻找函数,可以在Keil提供的Function栏目中寻找,
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)
{
uint32_t position;
uint32_t ioposition = 0x00U;
uint32_t iocurrent = 0x00U;
uint32_t temp = 0x00U;
/* Check the parameters */
assert_param(IS_GPIO_ALL_INSTANCE(GPIOx));
assert_param(IS_GPIO_PIN(GPIO_Init->Pin));
assert_param(IS_GPIO_MODE(GPIO_Init->Mode));
assert_param(IS_GPIO_PULL(GPIO_Init->Pull));
/* Configure the port pins */
for(position = 0U; position < GPIO_NUMBER; position++)
{
/* Get the IO position */
ioposition = 0x01U << position;
/* Get the current IO position */
iocurrent = (uint32_t)(GPIO_Init->Pin) & ioposition;
if(iocurrent == ioposition)
{
/*--------------------- GPIO Mode Configuration ------------------------*/
/* In case of Output or Alternate function mode selection */
if(((GPIO_Init->Mode & GPIO_MODE) == MODE_OUTPUT) || \
(GPIO_Init->Mode & GPIO_MODE) == MODE_AF)
{
/* Check the Speed parameter */
assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));
/* Configure the IO Speed */
temp = GPIOx->OSPEEDR;
temp &= ~(GPIO_OSPEEDER_OSPEEDR0 << (position * 2U));
temp |= (GPIO_Init->Speed << (position * 2U));
GPIOx->OSPEEDR = temp;
/* Configure the IO Output Type */
temp = GPIOx->OTYPER;
temp &= ~(GPIO_OTYPER_OT_0 << position) ;
temp |= (((GPIO_Init->Mode & GPIO_OUTPUT_TYPE) >> 4U) << position);
GPIOx->OTYPER = temp;
}
if((GPIO_Init->Mode & GPIO_MODE) != MODE_ANALOG)
{
/* Activate the Pull-up or Pull down resistor for the current IO */
temp = GPIOx->PUPDR;
temp &= ~(GPIO_PUPDR_PUPDR0 << (position * 2U));
temp |= ((GPIO_Init->Pull) << (position * 2U));
GPIOx->PUPDR = temp;
}
/* In case of Alternate function mode selection */
if((GPIO_Init->Mode & GPIO_MODE) == MODE_AF)
{
/* Check the Alternate function parameter */
assert_param(IS_GPIO_AF(GPIO_Init->Alternate));
/* Configure Alternate function mapped with the current IO */
temp = GPIOx->AFR[position >> 3U];
temp &= ~(0xFU << ((uint32_t)(position & 0x07U) * 4U)) ;
temp |= ((uint32_t)(GPIO_Init->Alternate) << (((uint32_t)position & 0x07U) * 4U));
GPIOx->AFR[position >> 3U] = temp;
}
/* Configure IO Direction mode (Input, Output, Alternate or Analog) */
temp = GPIOx->MODER;
temp &= ~(GPIO_MODER_MODER0 << (position * 2U));
temp |= ((GPIO_Init->Mode & GPIO_MODE) << (position * 2U));
GPIOx->MODER = temp;
/*--------------------- EXTI Mode Configuration ------------------------*/
/* Configure the External Interrupt or event for the current IO */
if((GPIO_Init->Mode & EXTI_MODE) == EXTI_MODE)
{
/* Enable SYSCFG Clock */
__HAL_RCC_SYSCFG_CLK_ENABLE();
temp = SYSCFG->EXTICR[position >> 2U];
temp &= ~(0x0FU << (4U * (position & 0x03U)));
temp |= ((uint32_t)(GPIO_GET_INDEX(GPIOx)) << (4U * (position & 0x03U)));
SYSCFG->EXTICR[position >> 2U] = temp;
/* Clear EXTI line configuration */
temp = EXTI->IMR;
temp &= ~((uint32_t)iocurrent);
if((GPIO_Init->Mode & GPIO_MODE_IT) == GPIO_MODE_IT)
{
temp |= iocurrent;
}
EXTI->IMR = temp;
temp = EXTI->EMR;
temp &= ~((uint32_t)iocurrent);
if((GPIO_Init->Mode & GPIO_MODE_EVT) == GPIO_MODE_EVT)
{
temp |= iocurrent;
}
EXTI->EMR = temp;
/* Clear Rising Falling edge configuration */
temp = EXTI->RTSR;
temp &= ~((uint32_t)iocurrent);
if((GPIO_Init->Mode & RISING_EDGE) == RISING_EDGE)
{
temp |= iocurrent;
}
EXTI->RTSR = temp;
temp = EXTI->FTSR;
temp &= ~((uint32_t)iocurrent);
if((GPIO_Init->Mode & FALLING_EDGE) == FALLING_EDGE)
{
temp |= iocurrent;
}
EXTI->FTSR = temp;
}
}
}
}
这个函数的两个输入都是结构体指针类型,打开这两个结构体的定义
GPIO_TypeDef结构体包含GPIO的各个寄存器
如果要调用GPIOB的Init函数,第一个形参可以直接写GPIOB
看一下,GPIOB_BASE是GPIOB的基地址,将其强转为一个结构体指针类型,现在就是一个首地址指向基地址的一个指针,所以可以直接写这个指针
GPIO_InitTypeDef结构体定义了Pin,Mode等
Pin等内容不是随便输入的,需要有一个参考值,看到后面的注释,发现给出了reference,Pin可以参考后面的GPIO_pins_define,然后搜索这个关键词,得到下面的详细定义
如果要选哪一个Pin,就可以参考相应的宏定义
对于Pin需要有参考,其他的结构体成员如Mode也需要参考,同样是上面的方法
这些宏定义的意思都在旁边的注释里面写的很清楚
GPIO_MODE_INPUT:输入模式
GPIO_MODE_OUTPUT_PP:推挽输出(PUSH PULL)
GPIO_MODE_OUTPUT_OD:开漏输出(open Drain)
Alternate表示复用
(Pin表示0到15要选哪一个端口,Mode表示输入输出模式,Pull表示上下拉,Speed表示输出速度,Alternate表示复用)
下面还有其他mode,GPIO还可以有中断和事件的mode,表示上升沿触发或者是下降沿触发
7.3 HAL_GPIO_WritePin();
以一个例子来说明GPIO的用法:
八. 点亮LED
先来看一下板子上LED的原理图:
这是一个共阳的二极管原理图
左边连接着IO口,这里不需要复用功能,只需要通用输出:推挽输出或者开漏输出
推挽输出:输出高电平二极管就熄灭,输入低电平二极管点亮
开漏输出:输出低电平二极管点亮,输出高阻态相当于断路,二极管熄灭
如果是共阴极那么开漏就不能满足上面的要求(因为高阻态和输出低电平LED都是熄灭的)
下面开始新建工程开始写代码:
先新建两个文件:led.c,led.h,并且保存在BSP文件夹中
在MDK里面添加这两个文件及其路径
led.h文件如下:
#ifndef __LED_H
#define __LED_H
#include "./SYSTEM/sys/sys.h"
#endif
就是防止重定义,并且include sys.h文件
led.c文件如下:
#include "./BSP/LED/led.h"//同样先include头文件
void led_init(void)//定义led初始化函数
{
GPIO_InitTypeDef gpio_init_struct;//先定义一个结构体,方便后面调用HAL库的GPIO初始化函数
__HAL_RCC_GPIOF_CLK_ENABLE();
//使能GPIOF的时钟(因为在F4探索者开发板中LED连接在PF9和PF10口上了)
//下面依次设置结构体,然后传入初始化函数进行GPIO工作模式的配置
gpio_init_struct.Pin = GPIO_PIN_9;//PF9
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;//PushPull 推挽输出
gpio_init_struct.Speed = GPIO_SPEED_FREQ_LOW;//选择低速
HAL_GPIO_Init(GPIOF,&gpio_init_struct); //要将结构体取址再输入
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_SET);//初始化结束后输出一个高电平
}
led.c只定义了一个led的初始化函数:使能时钟,先用HAL库对GPIO进行初始化,初始化以后再GPIO输出一个高电平,使得led熄灭
main.c文件如下:
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"//记得这个新的头文件
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
delay_init(168); /* 延时初始化 */
led_init(); /* 初始化LED */
while(1)
{
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_RESET); /* LED0 亮 */
delay_ms(500);
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_SET); /* LED0 灭 */
delay_ms(500);
}
}
这样就可以看到一个LED灯的闪烁
九. 按键控制LED亮灭
已知按键有抖动,抖动时间大约在5到10ms,要进行消抖(软件消抖和硬件消抖,详见51单片机介绍)
要分析这几个GPIO口的工作模式,因为都是输入且不是模拟输入,所以可能是输入上拉,下拉,浮空
对于WK_UP这个IO口,选择输入下拉,按键按下输入高电平,按键不按下是高阻态,就输入低电平
同理KEY0到KEY2配置为输入上拉
代码如下:
key.c文件:
#include "./BSP/KEY/key.h"
#include "./SYSTEM/delay/delay.h"
/* 按键初始化函数 */
void key_init(void)//定义led初始化函数
{
GPIO_InitTypeDef gpio_init_struct;//先定义一个结构体,方便后面调用HAL库的GPIO初始化函数
__HAL_RCC_GPIOE_CLK_ENABLE();
gpio_init_struct.Pin = GPIO_PIN_2;//PE2
gpio_init_struct.Mode = GPIO_MODE_INPUT;//输入模式
gpio_init_struct.Pull = GPIO_PULLUP;//选择上拉
HAL_GPIO_Init(GPIOF,&gpio_init_struct); //要将结构体取址再输入
}
/* 按键扫描函数 */
uint8_t key_scan(void)
{
if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_2) == 0)
{
delay_ms(10);/* 延时10ms躲过抖动 */
if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_2) == 0)
{
while(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_2) == 0);/* 只要按下就一直保持在这条语句,松开按键才能返回结果 */
return 1; /* 按键按下了 */
}
}
return 0; /* 按键没有按下 */
}
新增了key_scan()函数