Mkdm的STM32单片机学习日记:GPIO简介 + 点亮LED + 按键控制LED

一. 什么是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()函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值