外部中断实验 #STM32F407

外部中断实验

此实验将外部中断配置为按键输入,通过按键输入触发外部中断,在外部中断里面实施相应的处理,具体功能:

  1. 按下KEY0,翻转LED0状态
  2. 按下KEY1,翻转LED1状态
  3. 按下KEY2,同时翻转LED0和LED1状态
  4. 按下KEY_UP,翻转BEEP状态

在中断回调函数里面使用delay进行消抖,导致中断是阻塞的,不符合中断快速执行的原则,linux中的按键处理是实验外部中断+定时器共同实现的,更具普遍性。

弄清楚:

  1. 中断在单片机中是如何实现的
  2. 外部中断处理流程(程序)
  3. 如何配置外部中断

main函数

main函数代码:

c 复制代码
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/BEEP/beep.h"
#include "./BSP/EXTI/exti.h"


int main(void)
{    
    HAL_Init();                             /* 初始化HAL库 */
    sys_stm32_clock_init(336, 8, 2, 7);     /* 设置时钟,168Mhz */
    delay_init(168);                        /* 延时初始化 */
    usart_init(115200);                     /* 串口初始化为115200 */
    led_init();                             /* 初始化LED */
    beep_init();                            /* 初始化蜂鸣器 */
    extix_init();                           /* 初始化外部中断输入 */
    LED0(0);                                /* 先点亮红灯 */

    while(1)
    {
        delay_ms(1000);
    }
}

在main中主要做的是初始化,然后在while(1)里面死循环等待中断的到来,HAL_Init是使用HAL库必须调用的初始化函数;sys_stm32_clock_init、delay_init、usart_init这三个函数是正点原子编写的SYSTEM初始化函数,配置了系统时钟,延时函数,串口配置,实现单片机开发常用基本功能;然后调用led_init、beep_init、extix_init,初始化LED引脚、BEEP引脚、EXTI外部中断,KEY引脚初始化是在extix_init中进行调用的,故未在main中体现,完成所有的硬件初始化后,先将LED0点亮。

本次实验的核心是extix_init以及中断处理函数

exti.h

c 复制代码
#ifndef __EXTI_H
#define __EXTI_H

#include "./SYSTEM/sys/sys.h"


/******************************************************************************************/
/* 引脚 和 中断编号 & 中断服务函数 定义 */ 

#define KEY0_INT_GPIO_PORT              GPIOE
#define KEY0_INT_GPIO_PIN               GPIO_PIN_4
#define KEY0_INT_GPIO_CLK_ENABLE()      do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)   /* PE口时钟使能 */
#define KEY0_INT_IRQn                   EXTI4_IRQn
#define KEY0_INT_IRQHandler             EXTI4_IRQHandler

#define KEY1_INT_GPIO_PORT              GPIOE
#define KEY1_INT_GPIO_PIN               GPIO_PIN_3
#define KEY1_INT_GPIO_CLK_ENABLE()      do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)   /* PE口时钟使能 */
#define KEY1_INT_IRQn                   EXTI3_IRQn
#define KEY1_INT_IRQHandler             EXTI3_IRQHandler

#define KEY2_INT_GPIO_PORT              GPIOE
#define KEY2_INT_GPIO_PIN               GPIO_PIN_2
#define KEY2_INT_GPIO_CLK_ENABLE()      do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)   /* PE口时钟使能 */
#define KEY2_INT_IRQn                   EXTI2_IRQn
#define KEY2_INT_IRQHandler             EXTI2_IRQHandler

#define WKUP_INT_GPIO_PORT              GPIOA
#define WKUP_INT_GPIO_PIN               GPIO_PIN_0
#define WKUP_INT_GPIO_CLK_ENABLE()      do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)   /* PA口时钟使能 */
#define WKUP_INT_IRQn                   EXTI0_IRQn
#define WKUP_INT_IRQHandler             EXTI0_IRQHandler

.h文件中将引脚相关的量宏定义,方便理解与更改,并且将中断函数重定义,如将EXTI2_IRQHandler定义成KEY2_INT_IRQHandler,方便自己编写维护

exti.c

c 复制代码
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/BEEP/beep.h"
#include "./BSP/KEY/key.h"
#include "./BSP/EXTI/exti.h"


/**
 * @brief       KEY0 外部中断服务程序
 * @param       无
 * @retval      无
 */
void KEY0_INT_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(KEY0_INT_GPIO_PIN);         /* 调用中断处理公用函数 清除KEY0所在中断线 的中断标志位 */
    __HAL_GPIO_EXTI_CLEAR_IT(KEY0_INT_GPIO_PIN);         /* HAL库默认先清中断再处理回调,退出时再清一次中断,避免按键抖动误触发 */
}

/**
 * @brief       KEY1 外部中断服务程序
 * @param       无
 * @retval      无
 */
void KEY1_INT_IRQHandler(void)
{ 
    HAL_GPIO_EXTI_IRQHandler(KEY1_INT_GPIO_PIN);         /* 调用中断处理公用函数 清除KEY1所在中断线 的中断标志位,中断下半部在HAL_GPIO_EXTI_Callback执行 */
    __HAL_GPIO_EXTI_CLEAR_IT(KEY1_INT_GPIO_PIN);         /* HAL库默认先清中断再处理回调,退出时再清一次中断,避免按键抖动误触发 */
}

/**
 * @brief       KEY2 外部中断服务程序
 * @param       无
 * @retval      无
 */
void KEY2_INT_IRQHandler(void)
{ 
    HAL_GPIO_EXTI_IRQHandler(KEY2_INT_GPIO_PIN);        /* 调用中断处理公用函数 清除KEY2所在中断线 的中断标志位,中断下半部在HAL_GPIO_EXTI_Callback执行 */
    __HAL_GPIO_EXTI_CLEAR_IT(KEY2_INT_GPIO_PIN);        /* HAL库默认先清中断再处理回调,退出时再清一次中断,避免按键抖动误触发 */
}

/**
 * @brief       WK_UP 外部中断服务程序
 * @param       无
 * @retval      无
 */
void WKUP_INT_IRQHandler(void)
{ 
    HAL_GPIO_EXTI_IRQHandler(WKUP_INT_GPIO_PIN);        /* 调用中断处理公用函数 清除KEY_UP所在中断线 的中断标志位,中断下半部在HAL_GPIO_EXTI_Callback执行 */
    __HAL_GPIO_EXTI_CLEAR_IT(WKUP_INT_GPIO_PIN);        /* HAL库默认先清中断再处理回调,退出时再清一次中断,避免按键抖动误触发 */
}

/**
 * @brief       中断服务程序中需要做的事情
 *              在HAL库中所有的外部中断服务函数都会调用此函数
 * @param       GPIO_Pin:中断引脚号
 * @retval      无
 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    delay_ms(20);      /* 消抖 */
    switch(GPIO_Pin)
    {
        case KEY0_INT_GPIO_PIN:
            if (KEY0 == 0)
            {
                LED0_TOGGLE();  /* LED0 状态取反 */ 
            }
            break;

        case KEY1_INT_GPIO_PIN:
            if (KEY1 == 0)
            {
                LED1_TOGGLE();  /* LED1 状态取反 */ 
            }
            break;

        case KEY2_INT_GPIO_PIN:
            if (KEY2 == 0)
            {
                LED1_TOGGLE();  /* LED1 状态取反 */
                LED0_TOGGLE();  /* LED0 状态取反 */ 
            }
            break;

        case WKUP_INT_GPIO_PIN:
            if (WK_UP == 1)
            {
                BEEP_TOGGLE();  /* 蜂鸣器状态取反 */ 
            }
            break;

        default : break;
    }
}

/**
 * @brief       外部中断初始化程序
 * @param       无
 * @retval      无
 */
void extix_init(void)
{
    GPIO_InitTypeDef gpio_init_struct;
    
    key_init();
    gpio_init_struct.Pin = KEY0_INT_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_IT_FALLING;            /* 下降沿触发 */
    gpio_init_struct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(KEY0_INT_GPIO_PORT, &gpio_init_struct);    /* KEY0配置为下降沿触发中断 */

    gpio_init_struct.Pin = KEY1_INT_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_IT_FALLING;            /* 下降沿触发 */
    gpio_init_struct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(KEY1_INT_GPIO_PORT, &gpio_init_struct);    /* KEY1配置为下降沿触发中断 */
    
    gpio_init_struct.Pin = KEY2_INT_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_IT_FALLING;            /* 下降沿触发 */
    gpio_init_struct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(KEY2_INT_GPIO_PORT, &gpio_init_struct);    /* KEY2配置为下降沿触发中断 */
    
    gpio_init_struct.Pin = WKUP_INT_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_IT_RISING;             /* 上升沿触发 */
    gpio_init_struct.Pull = GPIO_PULLDOWN;
    HAL_GPIO_Init(WKUP_GPIO_PORT, &gpio_init_struct);        /* WKUP配置为上升沿触发中断 */

    HAL_NVIC_SetPriority(KEY0_INT_IRQn, 0, 2);               /* 抢占0,子优先级2 */
    HAL_NVIC_EnableIRQ(KEY0_INT_IRQn);                       /* 使能中断线4 */

    HAL_NVIC_SetPriority(KEY1_INT_IRQn, 1, 2);               /* 抢占1,子优先级2 */
    HAL_NVIC_EnableIRQ(KEY1_INT_IRQn);                       /* 使能中断线3 */
    
    HAL_NVIC_SetPriority(KEY2_INT_IRQn, 2, 2);               /* 抢占2,子优先级2 */
    HAL_NVIC_EnableIRQ(KEY2_INT_IRQn);                       /* 使能中断线2 */

    HAL_NVIC_SetPriority(WKUP_INT_IRQn, 3, 2);               /* 抢占3,子优先级2 */
    HAL_NVIC_EnableIRQ(WKUP_INT_IRQn);                       /* 使能中断线0 */

}

extix_init函数,先调用key_init将各按键IO的时钟打开,然后再将按键IO模式配置为中断模式,最后配置各中断优先级及使能。

中断触发流程:(以KEY0为例)

当KEY0变为低电平时,会执行EXTI4_IRQHandler,不过此函数在exit.h里面被重定义成KEY0_INT_IRQHandler。

c 复制代码
/**
 * @brief       KEY0 外部中断服务程序
 * @param       无
 * @retval      无
 */
void KEY0_INT_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(KEY0_INT_GPIO_PIN);         /* 调用中断处理公用函数 清除KEY0所在中断线 的中断标志位 */
    __HAL_GPIO_EXTI_CLEAR_IT(KEY0_INT_GPIO_PIN);         /* HAL库默认先清中断再处理回调,退出时再清一次中断,避免按键抖动误触发 */
}

在函数内先调用HAL_GPIO_EXTI_IRQHandler,这是HAL库内的公用处理函数,在其中会先进行一次中断标志位清除,然后调用回调函数(实际的处理函数)。

最后在KEY0_INT_IRQHandler再执行一次中断标志位清除,也就是说一次中断处理内总共执行了两次中断标志位清除,第一次是在HAL库的公用处理函数内,第二层是在自己编写的KEY0_INT_IRQHandler内,HAL库官方推荐这么写:在中断处理首末各执行一次清标志位,以防止误触发。

C 复制代码
/**
  * @brief  This function handles EXTI interrupt request.
  * @param  GPIO_Pin Specifies the pins connected EXTI line
  * @retval None
  */
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
  /* EXTI line interrupt detected */
  if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != RESET)
  {
    __HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
    HAL_GPIO_EXTI_Callback(GPIO_Pin);
  }
}

先清除中断标志位,这是第一次清中断,然后执行回调函数HAL_GPIO_EXTI_Callback,这个回调函数是HAL库内的虚函数,需要自己重写,具体如下,正式开始执行逻辑处理

c 复制代码
/**
 * @brief       中断服务程序中需要做的事情
 *              在HAL库中所有的外部中断服务函数都会调用此函数
 * @param       GPIO_Pin:中断引脚号
 * @retval      无
 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    delay_ms(20);      /* 消抖 */
    switch(GPIO_Pin)
    {
        case KEY0_INT_GPIO_PIN:
            if (KEY0 == 0)
            {
                LED0_TOGGLE();  /* LED0 状态取反 */ 
            }
            break;

        case KEY1_INT_GPIO_PIN:
            if (KEY1 == 0)
            {
                LED1_TOGGLE();  /* LED1 状态取反 */ 
            }
            break;

        case KEY2_INT_GPIO_PIN:
            if (KEY2 == 0)
            {
                LED1_TOGGLE();  /* LED1 状态取反 */
                LED0_TOGGLE();  /* LED0 状态取反 */ 
            }
            break;

        case WKUP_INT_GPIO_PIN:
            if (WK_UP == 1)
            {
                BEEP_TOGGLE();  /* 蜂鸣器状态取反 */ 
            }
            break;

        default : break;
    }
}

在回调函数中先延时20毫秒以消抖,根据不同的触发引脚在switch中执行相应的操作。

执行完这个回调函数后,就回到了KEY0_INT_IRQHandler内,然后执行第二次清中断标志位,一次外部中断处理就完成了。

实际上外部中断处理并不复杂,硬件初始化后,程序就等待中断的到来,IO电平变低,中断寄存器中的标志位被置高,程序会立刻跳转到EXTI4_IRQHandler这个函数中,这个函数其实就是一个地址,其在启动文件内定义,是一个固定的地址,用户需要在自己的代码中重写这个函数,在里面加上业务处理函数,若最后加上一个清中断标志位的函数,那该中断可以被重复触发,反之则只能触发一次,这个函数被执行完后,程序又立即跳转到触发中断前执行的位置,这就是STM32的外部中断处理。

HAL库对EXTI4_IRQHandler这个函数进行多次封装,所以变得复杂

在STM32中,几乎每个引脚都可以被配置为外部中断,那么问题来了,我怎么哪个引脚会执行哪个中断处理函数?这需要去查询单片机的参考手册

原来STM32是依靠引脚位来分割中断处理函数的,在启动汇编文件里面也一一对应

当中断引脚为0~4时,就使用:

EXTI0_IRQHandler

EXTI1_IRQHandler

EXTI2_IRQHandler

EXTI3_IRQHandler

EXTI4_IRQHandler

当中断引脚为5~9时,就使用:

EXTI9_5_IRQHandler

当中断引脚为10~15时,就使用:

EXTI15_10_IRQHandler

在HAL库中如何编写外部中断

  1. 初始化IO引脚,将其配置为中断上升沿触发或下降沿触发
  2. 使用HAL_NVIC_SetPriority配置中断优先级并使用HAL_NVIC_EnableIRQ使能其中断号
  3. 重写EXTIx_IRQHandler,在内部加上HAL_GPIO_EXTI_IRQHandler和__HAL_GPIO_EXTI_CLEAR_IT
  4. 重写回调函数HAL_GPIO_EXTI_Callback,在内部编写具体业务处理代码

Linux中的外部中断+定时器实现按键大概处理思路

当按键按下,触发该按键对应的中断,在其中断处理函数内启动一个10ms的定时器中断,当10ms结束,定时器中断内再判断该按键电平是否按下,如按下,则将事件标志位置高,在正常程序内查询事件标志位以做出响应,如判断为 没按下,则说明可能是抖动电平,忽略此次中断触发。

中断会打断单片机当前执行的程序,因此不应在中断内过多耗费时间,使用中断来置各种标志位是常用的做法。

相关推荐
易水寒陈13 分钟前
FreeRTOS的事件组
stm32·单片机
鹏展-penggeon8 小时前
STM32学习笔记【大学生电子设计竞赛】【嵌入式】【标准库学习】【HAL库学习】
stm32
年*D-清仁9 小时前
STM32+Proteus+DS18B20数码管仿真实验
stm32·单片机·proteus
想要成为糕手。9 小时前
stm32-wifi模块
stm32·嵌入式硬件·php
kongba00711 小时前
c语言样式主题 清爽风格 代码色彩 keil风格 适合单片机开发GD32 STM32等 cursor或者vscode 的settings.json文件
c语言·vscode·stm32·单片机
end_SJ11 小时前
STM32启动过程概述
stm32
mftang12 小时前
STM32G4系列微控制器深度解析
stm32·单片机·嵌入式硬件
cherry_rainyyy12 小时前
51单片机07 串口通信
单片机·嵌入式硬件·51单片机
promising-w12 小时前
PWM波形输出
单片机