[Bonjour STM32] No.5-demo 2.GPIO外部中断

[Bonjour STM32] 给萌新们的demo 2.GPIO外部中断

上一章我们讲解了STM32的第一个工程,(写上一章的鸽子咕了,暂时就没有上一张了,点灯的教程反正也很简单,我就默认大家都学了),点起了流水灯,这一次我们将通过外部中断,用按键来控制LED的亮灭。

1.前言

本文运用的开发板平台为某宝爆款STM32F103C8T6核心板,将讲解以下内容:

什么是中断
STM32是如何实现中断的
如何用CubeMX对外部中断进行配置
编写外部中断的程序
*通过 SysTick 中断实现按键消抖操作

2.什么是中断

在开始对中断进行枯燥乏味的定义讲解前,让我们先设身处地想象生活中的一个场景:你在实验室里摸鱼,正在聚精会神地看着B站舞蹈区小姐姐。这时,你看到实验室老师走了进来,你连忙暂停视频,切换到工作界面假装聚精会神地填坑。这时候,我们就说你触发了一次中断,而老师进来就是这次触发中断的中断源。同样地,我们在使用单片机时也会因为各种突发情况而不得不停止当前程序的执行,转而去执行新的操作。那么如果没有中断或者终端没有及时触发会发生什么呢?在摸鱼的你被抓了现行,被老师痛骂了一顿,心情十分不爽;你的单片机阻塞在主程序中没有及时处理突发情况,导致单片机运行出错,你还是十分不爽。

看来有时候专心致志地做同一件事(尤其是在不干正事的时候)实在令人提心吊胆、身心俱疲。那么人们在为了让单片机能够及时应对突发情况,就设置了中断。中断源多种多样,既有硬件中断(如外部中断、定时器中断),也有软件中断(例如程序运行异常抛出的中断)。在中断发生时,系统会将当前程序的运行现场保护起来,把当前使用到的寄存器和中断点地址压入堆栈,然后跳入中断服务函数运行;当中断服务函数内的程序运行完成后,系统会把中断点地址弹出,并将之前保存的现场恢复,然后从中断点处继续运行,这样你就完成了一次中断操作。

我们介绍了中断的概念,接下来看看STM32是如何运行中断的:
STM32F1数据手册的第九章讲解了和中断有关的硬件信息和相关寄存器。我们挑出一部分进行讲解。首先我们看一下中断向量表:

YDNvDK.png
YDNju6.png

在这里,我们看到了一个新的概念:优先级。优先级看名字就很好理解,肯定就是优先级高的中断先运行呗。而在STM32单片机中,优先级数字越小,优先级越高,因此中断向量表中优先级最高的都是和底层硬件有关的中断,而我们所要用的外部中断(EXTI)优先级就要低一些。

STM32里面中断分为抢占优先级响应优先级两种,在CubeMX中我们可以对这两种优先级进行设置。抢占优先级高的中断可以打断抢占优先级低的终端,因此在你的当前中断服务程序正在运行时,若又触发了比当前中断抢占优先级高的中断,那么当前运行的程序就会像主程序一样保护现场,中断点地址压入堆栈,然后跳入新的中断程序运行,即发生了中断嵌套。而响应优先级指的是 当抢占优先级相同的几个中断同时触发时,中断程序按照响应优先级从高到低依次运行。在寄存器中共有4位配置优先级,若将3位用于抢占优先级(即分为8种优先级),那么剩下一位用于响应优先级(即分为2种优先级)。

在中断向量表中我们可以看到每个中断后面都有一个地址,这个地址存储的就是该中断对应的中断服务函数的地址,当中断发生时,系统就会访问这个地址从而跳到对应的服务函数去执行。

接下来我们看一下外部中断的中断线:

YDNU1I.png

外部中断共有16条中断线,每个GPIO外设的相同端口共用一条中断线。即PA0和PB0端口是一条中断线,而PA0和PA1是两条中断线。因此我们在写中断服务函数时传入的参量只有端口名而没有GPIO外设名。

这样我们大概了解了STM32的外部中断是怎么运行的了:系统检测到不同中断线传来的中断,然后判断优先级,接着跳入相应的中断服务函数执行中断。有了这些基础知识,我们就可以真正编写外部中断程序了。而我们使用其他类型的中断时,也可以在技术手册中找到相关的信息,提高自己的知(姿)识(势)水平(提高裤腰带高度)

3.运用CubeMX对外部中断进行初始化

由于淘宝某爆款STM32F103C8T6核心板并没有可供输入的按键(什么,你说上面有个按键呀?那个是复位用的),因此我们就认为大家都按照下面的电路图焊了个按键上去。YD06ld.gif

YDBoE6.png

我们打开CubeMX新建工程,将PA0设置为GPIO_EXTI模式

YDrrfU.png

在GPIO设置中,配置PA0为上升沿触发,内部下拉

YDrypF.png

然后打开NVIC选项卡,将EXTI0中断线使能并设置抢占优先级为2,响应优先级为0。其他配置不变。

YDr6l4.png

然后生成代码,这样我们就完成了外部中断的配置。

4.编写外部中断程序

我们打开工程,在“main.c”文件中编写中断服务函数HAL_GPIO_Callback():

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if(GPIO_Pin==EXTI_Pin)        //如果设置了多个中断,则需要判断是哪个端口发生了中断
    {
        HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
    }
}

这样我们就完成了中断服务函数的编写,而不同类型的中断服务函数也不相同,函数名定义可以从相应的HAL库文件中查找。

接下来我们打开“stm32f1xx_it.c”文件,找到函数EXTI0_IRQHandler(void),可以看到里面有个中断处理函数HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0),找到它的定义:

void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
  /* EXTI line interrupt detected */
  if (__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u)
  {
    __HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
    HAL_GPIO_EXTI_Callback(GPIO_Pin);
  }
}

这样可以看到,当外部中断触发后,程序先判断中断发生的位置,然后把中断标志位清除,最后跳入我们刚才编写的服务函数执行中断操作。

5.如何实现按键消抖

我们把程序下载到单片机中运行,有时候却发现按下按键后LED并没有像程序中写的那样实现亮灭的转换,或者按住按键不松手只是左右晃动,LED就自动亮灭了。这个问题出现在按键的机械结构中,当我们按下按键时,案件内的触点并不会牢牢接触,有时会因为震动反复接触断开,从而触发了多次中断。为了消除这种现象,我们需要对按键做软件消抖。

那么我们该如何实现按键消抖呢?有的萌新会说,在中断程序里面加个延时函数,判断延时前后是否均满足外部中断的条件即可。如果真的这么简单我就不会写这一部分了(谁还不想老老实实摸个鱼呢)在中断程序中使用延时函数是绝对禁止的,虽然现在程序简单用一下延时不会出现什么错误,但中断里使用延时函数会影响其他中断的响应,更严重的结果就是,如果延时过长且反复触发同一中断,那么会导致那个中断不断嵌套,然后程序就死在里面了。

处理按键消抖的方法有许多,在这里我介绍一种使用 SysTick 中断来消抖的方法:

打开原来配置的CubeMX文件,将PA0设置为GPIO_Input

YD2yp6.png

在GPIO设置中,配置PA0为内部下拉,然后重新生成工程文件。

YD2rfx.png

打开工程,把之前写的中断服务函数删除,然后打开“stm32f1xx_it.c”,在文件前面添加三个标志位,其中把按键状态标志位指定为外部变量:

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
uint8_t SW0Count = 0;       //按键按下的时间计数器
uint8_t pushFlag = 0;       //长按标志
extern uint8_t SW0State;    //按键状态
/* USER CODE END PV */

随后添加函数Key_Scan(),注意在“stm32f1xx_it.h”中声明函数:

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/**
  * @brief  按键消抖函数
  *
  * @note   该函数不区分是否为按键长按。即只有按下并
    *         抬起才能算作一次完整的按键操作。
  *
  * @param  None
  *
  * @retval None
  */
void Key_Scan(void)
{
        /*检测按键是否按下*/
    if (HAL_GPIO_ReadPin(SW0_GPIO_Port, SW0_Pin) == GPIO_PIN_SET)
    {
        SW0Count++;           //每按下1ms,计时器加1
        if (SW0Count >= 10)   //当时间超过10ms时,再次判断按键状态
        {
            if (pushFlag == 0)//如果不是长时间按下,则长按标志位置1,按键状态置1,表明已按下
            {
                SW0State = 1;
                SW0Count = 0;
                pushFlag = 1;
            }
            else
                SW0Count = 0;
        }
        else
            SW0State = 0;
    }
    else                      //若无按键按下,则三个变量全部置0
    {
        SW0State = 0;
        SW0Count = 0;
        pushFlag = 0;
    }
}
/* USER CODE END 0 */

这个就是我们的消抖函数了,也许有人会疑问,为什么定时器变量数值为10就表明延时了10ms呢?这里我们打开“stm32f1xx_hal.c”文件,找到函数HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority):

__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
  /* Configure the SysTick to have interrupt in 1ms time basis*/
  if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U)
  {
    return HAL_ERROR;
  }
  /* Configure the SysTick IRQ priority */
  if (TickPriority < (1UL << __NVIC_PRIO_BITS))
  {
    HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
    uwTickPrio = TickPriority;
  }
  else
  {
    return HAL_ERROR;
  }
  /* Return function status */
  return HAL_OK;
}

可以看到,HAL库将 SysTick配置为每1ms触发一次,而且当我们打开HAL_Delay()时,也可以发现延时函数就是靠 SysTick的中断来延时相应的时间的。

之后我们编写 SysTick中断处理函数:

void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */
    Key_Scan();
  /* USER CODE END SysTick_IRQn 0 */
  HAL_IncTick();
  /* USER CODE BEGIN SysTick_IRQn 1 */
    HAL_SYSTICK_IRQHandler();
  /* USER CODE END SysTick_IRQn 1 */
}

回到"main.c",首先声明外部变量 SW0State,然后在while(1)编写LED开关的程序:

while (1)
{
    if(SW0State==1)
        HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
}

这样我们就完成了按键消抖程序的编写。

6.总结

本文我们讲解了如何编写GPIO的外部中断程序,现在将步骤简要概括如下:

1.配置相应的端口为GPIO_EXTI模式,配置GPIO。
2.使能中断,设置中断的优先级,完成初始化。
3.编写中断服务函数HAL_GPIO_Callback()

发表回复