STM32的中断_外部中断实现按键松手检测/延时消抖

中断概述

数据传输方式

  • 中断本质上是处理器与外部设备进行数据传输的一种方式,常见的数据传输方式有四种:

  • 无条件方式

    处理器不必了解外部设备的状态,直接进行传输,适用于简单的数据传输任务

  • 查询方式

    又称轮询法,处理器循环查询外部设备的状态,在外部设备就绪时才进行数据传输,我们在GPIO一节的三个实验:LED、按键、LCD中使用的都是这一方法(对Systick的使用除外)

    这种方法的好处是开发简单,问题在于处理器查询过程中无法执行其他任务,利用率较低

  • 中断方式

    处理器不主动查询外部设备的状态,而是反过来让外部设备在就绪后再通知处理器,打断处理器当前执行的任务,转而处理与外部设备的数据传输,传输完成后再回到之前执行的任务

    这种方式的好处是人处理器和外部设备可以并行工作,提高了处理器利用率

  • DMA直接储存器访问方式

    传输大批量数据时采用的方式:处理器只负责处理DMA的请求,建立数据传输通道后传输过程由专门的DMA控制器来处理,不需要占用处理器

    这种方式特别时候大批量数据的传输,问题在于需要专门的硬件(DMA控制器)支持

STM32的中断系统

  • 中断源、中断优先级、中断嵌套、中断服务程序、中断向量表等关于中断的常识在STM8的中断一章中已经介绍过,不再赘述,下文重点讲解STM32中断系统的特点与HAL库对中断操作所进行的封装

  • 嵌套向量中断控制器NVIC

    能够打断当前代码运行的事件分为异常和中断两类

    • 异常:由内核自身产生,大多由软件触发,比如除法出错异常,预取指失败等
    • 中断:由内核外部产生,一般由硬件触发,比如定时器中断和外部中断等

    在Cortex-M内核设计的ARM处理器中,提供了一个专门的硬件:NVIC,nested vectored interrupt controller嵌套向量中断控制器,对所有的异常和中断进行管理,通过对NVIC编程,可以实现中断使能、中断优先级设置和中断触发方式选择

  • 中断通道

    中断源通过中断通道向内核发出中断申请,由于中断通道的数量有限,因此多个外设(比如外部中断的多个端口共用一个EXTIx_IRQ)或者是一个外设中的多个中断源(比如定时器有更新中断、捕获中断等中断源)会共用一个中断通道,在对应的中断服务程序中再去判断是哪一类中断

  • 中断向量号

    在STM32的中断向量表中:中断向量号0为空间起始地址;0-15号属于内核异常,从16开始是各个片内外设相关的中断;向量号1-3是复位、不可屏蔽与类型错误这三种固定优先级的中断,其余中断都可设置优先级

    中断向量号从小到大的排序就是默认的中断优先级,没有另外设定优先级的情况下排在前面的中断优先执行

STM32中断优先级设置

  • 设置中断的优先级实质是设置中断通道的优先级,通过NVIC中的中断优先级寄存器NVIC_IP进行设置,该寄存器有8位,但STM32只使用其中的高四位,数字越小优先级越高,分为两个优先级:抢占优先级与子优先级

  • 抢占优先级

    抢占优先级涉及中断嵌套,先判断抢占优先级,抢占优先级高的会打断抢占优先级低的,优先得到执行

  • 子优先级

    子优先级不涉及中断嵌套,比如两个中断同时响应,会优先执行子优先级高的中断

    子优先级和抢占优先级都相同时则比较他们在中断向量表中的默认优先级,编号越小优先级越高

  • 中断优先级分组

    中断优先级根据抢占优先级与子优先级的配置分为5组:

    • 第0组:NVIC_PriorityGroup_0:没有抢占优先级,4位都用于指定子优先级

    • 第1组:NVIC_PriorityGroup_1:最高1位指定抢占优先级,其余3位都用于指定子优先级

      因为抢占优先级只有1位,因此第一组的抢占优先级可分2级,其余3位设定的子优先级可分8级

    其他3组以此类推,HAL库初始化函数默认将优先级分组设置为第4组:4位全部用于抢占式优先级,共分0-16共16级,编号越小优先级越高

HAL库的中断设计

  • HAL封装中断处理

    HAL库对中断函数进行统一封装,各个外设的中断处理函数都定义为HAL_PPP_IRQHandler(PPP为相应的外设名,此函数由CubeMX自动生成),而中断所需要执行的操作以中断回调函数HAL_PPP_Callback的形式提供给用户

    这样做是为了简化编程,在通用的中断处理函数中执行判断中断源、清除中断标志这样必须的操作,用户只需要编写中断回调函数来决定具体的中断处理任务即可

  • 中断回调函数

    中断回调函数发挥类似中断服务函数的作用,中断后要执行的操作被写入这个函数

    • 所谓回调函数是指一个通过函数指针调用的函数,把一个函数的指针(即其地址)作为另一个函数的参数,当这个指针被用来调用其所指向的函数时,被调用的函数被称为回调函数

      中断回调函数HAL_PPP_Callback就是中断后进入中断处理函数HAL_PPP_IRQHandler后被调用的回调函数

    HAL库预定义了默认的回调函数,该函数属性设计为weak,这一“弱属性”意味着如果该函数在其他地方没有定义,就使用默认定义的函数,如果用户在其他地方定义了该函数(需要按原本规定的函数名来编写),就使用用户定义的函数

  • 中断配置流程
    1. 设置中断触发条件:比如外部中断用何种边沿触发,定时器更新中断的时间间隔,在对应外设中间栏下方Configuration的Parameter Settings标签中设置

    2. 使能中断:在对应外设中间栏下方Configuration的NVIC Settings标签中给中断源对应Enabled选行打勾

    3. 设置中断的优先级:在左侧栏System Core中点开NVIC选项,在中间栏的NVIC Interrupt Table对应外设的Preemption Priority中设置优先级数值

    4. 判断中断源:对于有多个中断源的外设,在进入中断服务程序后需要根据中断标志来判断是发生了哪一种中断

    5. 清除中断标志:避免处理器错误判断中断的发生,重复进入中断

    6. 编写中断服务程序,确定中断后应该进行的操作


外部中断

外部中断相关硬件

  • 外部中断控制器EXTI

    对涉及GPIO引脚变化或者来自RTC、USB等外设的唤醒事件所引发的中断,在STM32微控制器内部专门设计了一个外部中断控制器EXTI进行管理

  • 外部中断线EXTI line

    EXTI提供了23个外部中断线EXTI line,其中0-15号外部中断线用于GPIO,16-22号用于RTC与USB外设的唤醒事件,当GPIO引脚与0-15号外部中断线连接后,此时的GPIO就具备外部中断功能

    中断触发方式可选上升沿、下降沿、双边沿触发中断

    显然外部中断线数量有限无法与每个引脚一一对应,因此STM32根据各个引脚尾号进行分组(而不是STM8的根据GPIO端口分组):例如所有GPIO端口组的0号引脚被分配到EXTI0端口线上(这意味着可供同时使用的外部中断引脚最多为16个)

  • 外部中断通道

    除了外部中断线有限外,由于中断通道有限,所以会有多个中断线接到同一中断通道的情况:EXTI0-4这几个外部中断线有独立的外部中断通道EXTI0-4(也即有独立的中断服务函数),EXTI5-9连接到EXTI9_5,EXTI10-15连接到EXTI15_10

外部中断的接口函数

  • 外部中断通用处理函数HAL_GPIO_EXTI_IRQHandler

    在触发中断后,处理器会根据中断向量表跳转到中断对应的中断服务程序中(具体执行哪一个中断服务程序由中断发生顺序和优先级决定),在该程序内部调用外部中断通用处理函数HAL_GPIO_EXTIx_IRQHandler,这是所有外部中断发生后都会进入该函数

    在外部中断通用处理函数内会进行两个操作:GPIO引脚的判断,与清除对应中断源的中断标志;之后,外部中断通用处理函数会再调用外部中断回调函数HAL_GPIO_EXTI_Callback

    void HAL_GPIO_EXTIx_IRQHandler(uint16_t GPIO_Pin)
    //该函数由CubeMX自动生成
    
  • 外部中断回调函数HAL_GPIO_EXTI_Callback

    该函数由外部中断通用处理函数HAL_GPIO_EXTI_IRQHandler所调用,此函数用于用户在其中编写所要完成的具体中断处理任务

    CubeMX生成的默认外部中断回调函数预定义为weak属性,当用户需要使用此函数时重新定义与之同名的函数,新定义的函数会取代默认的回调函数,用于执行所需的中断服务

    因为任何外部中断都会调用此回调函数,因此需要在此函数内部判断具体是哪一个引脚触发了中断

    void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
    //在GPIO_Pin发生中断后,将会执行其中的操作
    
  • 带有边沿检测的外部中断回调函数

    外部中断回调函数的变体,多出后缀Falling表示下降沿检测,Rising表示上升沿检测

    能够区分外部中断触发边沿极性,在上升沿或下降沿时执行

    使用这两个函数需要将GPIO的外部中断配置为双边沿触发:在CubeMX的GPIO mode选项中选择External Interrupt Mode with Rising/Falling edge trigger detection

    void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin)
    //下降沿触发
    void HAL_GPIO_EXTI_Rising_Callback(uint16_t GPIO_Pin)
    //上升沿触发
    

    需要注意的是有些型号的HAL库不包含该函数,只有HAL_GPIO_EXTI_Callback可用,这时可以跳转到EXTI_TypeDef查看是否有FPR1RPR1(注释为EXTI Falling/Rising Edge Pending mask register),如果没有说明硬件不支持检测双边沿,需要在HAL_GPIO_EXTI_Callback中判断当前是上升沿还是下降沿

    ST官方论坛查询后发现有人遇到了相反的情况:只能使用HAL_GPIO_EXTI_FALLING_CallbackHAL_GPIO_EXTI_RISING_Callback而不能使用HAL_GPIO_EXTI_Callback,这种情况可以对stm32xx_hal_gpio.c中的回调函数进行修改,将两个函数统一起来,具体见网页

外部中断按键实现

  • 初始化设置
    1. 先分配引脚功能为GPIO_EXTI*来启动外部中断(而非GPIO_Input)

    2. 在GPIO的Configuration中配置:

      外部中断引脚的GPIO mode(选择哪一个边沿触发)

      GPIO Pull-up/Pull-down(选择是否启用上拉/下拉电阻)

      User Label(启用了外部中断的引脚名称,如B1_EXTI)

    3. 使能外部中断:在GPIO的NVIC标签页中勾选使能引脚对应的外部中断线EXTI line的中断功能

    4. 设置中断优先级:在System Core的NVIC页中的中断向量表里修改所启用中断的优先级,根据前文所述分为Preemption Priority抢占优先级与Sub Priority子优先级两类,因为HAL库将优先级设置为第4组,所以只设置抢占优先级为0-15之间即可(无特殊要求也可以按默认优先级)

  • 代码实现

    main.c文件中直接写HAL_GPIO_EXTI_Callback外部中断回调函数,在中断发生时会自动跳转到此函数执行

    //在中断回调函数中除了中断要执行的操作,别忘记判断发生中断的引脚
    void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
    {
    	if(GPIO_Pin == B1_EXTI_PIN)//判断发生中断的引脚是否与前面所设置的引脚标号相同
    	//如果中断源较多,使用switch-case进行判断
    	{
    		//按键按下后要执行的操作,如果操作简单直接写在此处即可
    		//如果操作复杂,在中断函数中执行过久会导致运行阻塞
    		//可以在此处只置位一个中断标志变量,把中断处理任务放在主函数中循环等待标志位
    	}
    }
    
  • 带有防抖与松手检测的完整中断按键处理函数

    在按键一节中,我们通过轮询获得按键值后再对按键值进行运算以判断按键按下与松开的状态,而在使用外部中断后,能够更高效地实现这些功能

    以为按下时点亮LED,释放时熄灭为例,注意要将GPIO的外部中断配置为双边沿触发,对不能直接使用硬件判断上升沿或下降沿的单片机进行软件进行判断

      uint8_t buttonPressed = 0;        // 记录按键的按下状态
      uint32_t buttonPressTime = 0;     // 记录按键按下的时刻
      uint32_t lastFallTime = 0;        // 记录上次下降沿时刻
      uint32_t lastRiseTime = 0;        // 记录上次上升沿时刻
    
      void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
      {
      	if(GPIO_Pin == GPIO_PIN_0)
      	{
      		uint32_t currentTime = HAL_GetTick();
      		if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET) // 下降沿,按键按下
      		{
      			// 消抖处理:20ms内只处理一次下降沿
      			if(currentTime - lastFallTime > 20)
      			{
      				buttonPressed = 1;
      				buttonPressTime = currentTime;
      				lastFallTime = currentTime;
      				// 此处添加按键按下操作
      			}
      		}
      		else // 上升沿,按键释放
      		{
      			// 消抖处理:20ms内只处理一次上升沿,且必须之前是按下状态
      			if((currentTime - lastRiseTime > 20) && buttonPressed)
      			{
      				uint32_t pressDuration = currentTime - buttonPressTime;
      				buttonPressed = 0;
      				lastRiseTime = currentTime;
      				// 此处添加按键释放操作
      			}
      		}
      	}
      }
    
  • 如果硬件支持HAL_GPIO_EXTI_Falling_CallbackHAL_GPIO_EXTI_Rising_Callback则可以直接通过判断按键的上升沿与下降沿来判断按键状态,此外,使用HAL_GetTick函数获取当前时间,从而将延时防抖与长按检测一并用中断方式实现,写法如下:

    // 按键状态变量
    uint8_t buttonPressed = 0;//记录按键的按下状态以判断是否松手
    uint32_t buttonPressTime = 0;//记录按键按下的时刻以计算按下时长
    uint32_t lastFallTime = 0;//记录按键按下的时刻
    uint32_t lastRiseTime = 0;//记录按键松开的时刻
    void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin)//下降沿:按键按下
    {
        if(GPIO_Pin == GPIO_PIN_0)
        {
            uint32_t currentTime = HAL_GetTick();
            if(currentTime - lastFallTime > 20)//20ms消抖
            {
                buttonPressed = 1;//按键被按下
                buttonPressTime = currentTime;
    			//按键按下操作
            }
            lastFallTime = currentTime;
        }
    }
    void HAL_GPIO_EXTI_Rising_Callback(uint16_t GPIO_Pin)//上升沿:按键释放
    {
        if(GPIO_Pin == GPIO_PIN_0)
        {
            uint32_t currentTime = HAL_GetTick();
            // 消抖处理且确保之前是按下状态
            if((currentTime - lastRiseTime > 20) && buttonPressed)
            {
                buttonPressed = 0;
                uint32_t PassTime = currentTime - buttonPressTime;
                // 按键释放后操作
            }
            lastRiseTime = currentTime;
        }
    }
    

posted on 2025-05-08 20:43  无术师  阅读(930)  评论(0)    收藏  举报