STM32基本GPIO操作:按键输入(扫描+外部中断)

Posted wh201906

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STM32基本GPIO操作:按键输入(扫描+外部中断)相关的知识,希望对你有一定的参考价值。

(涉及专有名词较多,难免解释不到位,若有错误还请指出,谢谢!)

硬件连接图如下:
技术图片

技术图片

一、扫描

思路是在main函数中通过死循环来扫描端口电平状态检测,以此判断按键是否按下。实现较为简单。

1.初始化(注意C语言中变量声明需放在函数开头)

以下是初始化PB5端口(LED灯)的代码,每一条语句的含义在我另一篇博客里

GPIO_InitTypeDef GPIO_Init1;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

GPIO_Init1.GPIO_Pin = GPIO_Pin_5;
GPIO_Init1.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init1.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_SetBits(GPIOB, GPIO_Pin_5);                       //先熄灯

GPIO_Init(GPIOB, &GPIO_Init1);

以下是初始化PE3端口(按键)的代码

GPIO_InitTypeDef GPIO_Init2;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);

GPIO_Init2.GPIO_Pin = GPIO_Pin_3;           // 设置GPIO端口号为5
GPIO_Init2.GPIO_Mode = GPIO_Mode_IPU;       // 设置端口模式为输入上拉
// 设置为输入端口时不需要指定GPIO_Speed参数

GPIO_Init(GPIOE, &GPIO_Init2);

输入上拉与输入下拉的区别:
输入上拉(GPIO_Mode_IPU):端口与VCC通过一个电阻串连,因此没有输入或输入高电平时端口为高电平,输入低电平时端口为低电平
输入下拉(GPIO_Mode_IPD):端口与GND通过一个电阻串连,因此没有输入或输入低电平时端口为低电平,输入高电平时端口为高电平

从硬件图上得知按键与GND相连,如果端口设置为输入上拉,那么松开按键时端口为高电平,按下按键时端口为低电平,可以区分两种状态
如果端口设置为输入下拉,那么无论是按下还是松开按键时端口总为低电平,无法区分两种状态
类似地,如果按键与VCC相连,则端口需要设置为输入下拉才能区分按下/松开两种状态

2.扫描

读取PE3端口的状态:

GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)

返回值为SET则端口为高电位,返回值为RESET则端口为低电位

在main函数中放入以下死循环代码以实现扫描PE3端口并点灯的功能

while (1)
{
    if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)==SET) // 如果按键对应端口为高电平
    {
        GPIO_SetBits(GPIOB, GPIO_Pin_5);             // 熄灯(LED负极连接PB5,LED正极连接VCC,PB5高电平熄灯)
    }
    else                                             // 否则
    {
        GPIO_ResetBits(GPIOB, GPIO_Pin_5);           // 亮灯
    }
    delay_ms(10);                                    // 一些开发板例程当中提供了delay函数,需要通过delay_init()初始化后才可使用
                                                     // 若无现成delay函数,可通过一定次数的for循环来代替
}

3.例程

代码如下:

int main(void)
{
    GPIO_InitTypeDef GPIO_Init1, GPIO_Init2;
    delay_init();

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    GPIO_Init1.GPIO_Pin = GPIO_Pin_5;
    GPIO_Init1.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_Init1.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_Init1);
    GPIO_SetBits(GPIOB, GPIO_Pin_5);                       //先熄灯

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);
    GPIO_Init2.GPIO_Pin = GPIO_Pin_3;
    GPIO_Init2.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOE, &GPIO_Init2);
    
    delay_ms(200);

    while (1)
    {
        if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)==SET)
        {
            GPIO_SetBits(GPIOB, GPIO_Pin_5);
        }
        else
        {
            GPIO_ResetBits(GPIOB, GPIO_Pin_5);
        }
        delay_ms(10);
    }
}

二、中断

0.相关概念

中断:程序运行过程中,系统外部、系统内部或者现行程序本身若出现紧急事件,处理机立即中止现行程序的运行,自动转入相应的处理程序(中断服务程序),待处理完后,再返回原来的程序运行
简而言之就是触发某一事件可以使得MCU跳转执行该事件的处理程序,而按键按下或放开(GPIO口电平改变)则可作为一个外部中断,通过编写这一事件的处理程序从而达到改变灯亮灭状态的目的
(这里提到的“事件”并不是STM32当中的专有名词“事件”,而是泛指发生了某一件事)

使用扫描方式获得按键输入的思路如下:

主函数()
{
    初始化()
    死循环
    {
        如果(按键按下)
        {……}
        否则
        {……}
    }
}

而使用中断获得按键输入的思路如下:

主函数()
{
    初始化()
    其它操作()
}

中断处理函数()
{
    如果(按键按下)
    {……}
    否则
    {……}
}

对比可知使用扫描方式将使得芯片无法(难以)处理其它事务

NVIC:全名为“内嵌向量中断控制器”,主要用来控制芯片中各个中断的优先级,在很多地方都会使用(串口通信、SPI通信、定时器、I?C通信等涉及到实时处理的功能都会与中断有关)

EXTI(不是EXIT):全名为“外部中断/事件控制器”,可以实现输入信号的上升沿检测和下降沿的检测

1.初始化(注意C语言中变量声明需放在函数开头)

1.1 NVIC

需要用到的初始化语句如下:

NVIC_InitTypeDef NVIC_I;                           //定义初始化结构体

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);    //设置整个系统的中断优先级分组

NVIC_I.NVIC_IRQChannel=EXTI3_IRQn;                 //设置初始化哪个中断
NVIC_I.NVIC_IRQChannelPreemptionPriority=0x02;     //设置中断抢占优先级
NVIC_I.NVIC_IRQChannelSubPriority=0x02;            //设置中断响应优先级(子优先级)
NVIC_I.NVIC_IRQChannelCmd=ENABLE;                  //中断使能(启动)

NVIC_Init(&NVIC_I);                                //初始化

中断优先级分组、抢占优先级和子优先级的关系:
STM32系列的芯片当中一般会有很多的中断,而当多个中断同时发生时就需要一个调度机制来控制它们的执行顺序,因此有了中断的优先级的概念。优先级遵循以下几点:
1.优先级的数字越小优先级越高
2.抢占优先级高的中断会先执行,它也可以打断抢占优先级低的中断
3.当抢占优先级相同时,响应优先级高的中断会先执行,但它不可以打断响应优先级低的中断
4.当两个中断的抢占优先级和响应优先级都相同时,先产生的中断先执行(按照时间顺序)

举个例子,现在有三个中断:
中断1:抢占优先级为2,响应优先级为1
中断2:抢占优先级为3,响应优先级为0
中断3:抢占优先级为2,响应优先级为0
则3个中断的优先级顺序是中断3>中断1>中断2,同时中断1、3可以打断中断2,中断3不能打断中断1

而两类优先级可以设置成哪些值呢?这取决于整个系统的中断优先级分组。通过

NVIC_PriorityGroupConfig();

可以设置整个系统的中断优先级分组,其参数可以是NVIC_PriorityGroup_0、NVIC_PriorityGroup_1、NVIC_PriorityGroup_2、NVIC_PriorityGroup_3、NVIC_PriorityGroup_4之一。具体关系如下:
NVIC_PriorityGroup_0:0位抢占优先级(无效)+4位响应优先级(0~15)
NVIC_PriorityGroup_1:1位抢占优先级(0~1)+3位响应优先级(0~7)
NVIC_PriorityGroup_2:2位抢占优先级(0~3)+2位响应优先级(0~3)
NVIC_PriorityGroup_3:3位抢占优先级(0~7)+1位响应优先级(0~1)
NVIC_PriorityGroup_4:4位抢占优先级(0~15)+0位响应优先级(无效)

例如中断分组设置为3,则所有中断的抢占优先级可以被设置为0~7,响应优先级可以被设置为0、1
需要注意的是如果程序中用到了二次封装的一些库(比如开发板例程中厂商为初学者写的串口库等),则NVIC_PriorityGroupConfig()可能已经被调用过了,此时再次修改可能会带来其它问题

1.2 EXTI

需要用到的初始化语句如下:

EXTI_InitTypeDef EXTI_I;                           //定义初始化结构体

EXTI_I.EXTI_Line=EXTI_Line3;                       //设置初始化哪条中断/事件线
EXTI_I.EXTI_Mode = EXTI_Mode_Interrupt;            //设置为产生中断(EXTI_Mode_Event为产生事件)
EXTI_I.EXTI_Trigger = EXTI_Trigger_Falling;        //设置为下降沿触发
EXTI_I.EXTI_LineCmd = ENABLE;                      //使能

EXTI_Init(&EXTI_I);                                //初始化

中断/事件的区别:
中断产生后经由NVIC交给MCU进行处理(软件层面)
事件最终作为一个脉冲信号直接触发其它硬件(硬件层面)
附一张EXTI的框图便于理解,蓝色是中断,红色是事件
技术图片

EXTI、NVIC与GPIO的对应关系:
如图所示
技术图片

上升/下降沿:
低电平跳到高电平为上升沿,高电平跳到低电平为下降沿

1.3 GPIO

与扫描方式的初始化代码相同

GPIO_InitTypeDef GPIO_Init1, GPIO_Init2;

GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource3);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_Init1.GPIO_Pin = GPIO_Pin_5;
GPIO_Init1.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init1.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_Init1);
GPIO_SetBits(GPIOB, GPIO_Pin_5);                       //先熄灯

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);
GPIO_Init2.GPIO_Pin = GPIO_Pin_3;
GPIO_Init2.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOE, &GPIO_Init2);
1.4 其它

目前不明确这两条语句的作用

GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource3);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);          //开启端口复用,涉及到GPIO口做外部中断时都需要这一条语句

2.中断处理函数

函数名与中断/事件线有着对应关系,可参照上一张表

void EXTI3_IRQHandler(void)
{
    if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)==SET)
    {
        GPIO_SetBits(GPIOB, GPIO_Pin_5);
    }
    else
    {
        GPIO_ResetBits(GPIOB, GPIO_Pin_5);
    }
    EXTI_ClearITPendingBit(EXTI_Line3);
}

最后的EXTI_ClearITPendingBit()用于清除中断标志位,避免对之后的中断造成影响

3.例程

代码如下:

int main(void)
{
    GPIO_InitTypeDef GPIO_Init1, GPIO_Init2;
    NVIC_InitTypeDef NVIC_I;
    EXTI_InitTypeDef EXTI_I;

    GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource3);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
    
    NVIC_I.NVIC_IRQChannel=EXTI3_IRQn;
    NVIC_I.NVIC_IRQChannelPreemptionPriority=0x02;
    NVIC_I.NVIC_IRQChannelSubPriority=0x02;
    NVIC_I.NVIC_IRQChannelCmd=ENABLE;
    NVIC_Init(&NVIC_I);

    EXTI_I.EXTI_Line=EXTI_Line3;
    EXTI_I.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_I.EXTI_Trigger = EXTI_Trigger_Falling;
    EXTI_I.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_I);

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    GPIO_Init1.GPIO_Pin = GPIO_Pin_5;
    GPIO_Init1.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_Init1.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_Init1);
    GPIO_SetBits(GPIOB, GPIO_Pin_5);                       //先熄灯

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);
    GPIO_Init2.GPIO_Pin = GPIO_Pin_3;
    GPIO_Init2.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOE, &GPIO_Init2);

}

void EXTI3_IRQHandler(void)
{
    if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)==SET)
    {
        GPIO_SetBits(GPIOB, GPIO_Pin_5);
    }
    else
    {
        GPIO_ResetBits(GPIOB, GPIO_Pin_5);
    }
    EXTI_ClearITPendingBit(EXTI_Line3);
}

2019.12.22

以上是关于STM32基本GPIO操作:按键输入(扫描+外部中断)的主要内容,如果未能解决你的问题,请参考以下文章

stm32开发笔记:stm32系列的GPIO基本功能之输出驱动LED灯输入按键KEY以及Demo

stm32开发笔记:stm32系列的GPIO基本功能之输出驱动LED灯输入按键KEY以及Demo

STM32 HAL库学习系列第9篇---NVIC按键外部中断函数

第13章 GPIO输入—按键检测

STM32 按键输入

STM32--GPIO口的八种工作模式