STM32L475按键中断实验

要了解按键中断, 就要对芯片整体的中断系统有准确的把握

中断系统

在Cortex-M内核中, 中断属于异常(Exception)的子集, 所以描述中断时常用异常进入(Exception entry)异常退出(Exception exit)来描述中断进入和退出.

整个中断系统由如下两个控制器负责实现

  • NVIC(Nested Vectored Interrupt Controller ) 嵌套向量中断控制器, 负责给所有的中断排队, 分优先级, 然后按照优先级跳转到对应的中断处理函数中执行中断逻辑
  • EXTI(Extended interrupts and events controller ) 外部中断/事件控制器, 属于外设里的传感器, 负责监测引脚点评变化, 一旦引脚变化产生, 就将对应信号传递到NVIC进行中断处理.

中断源总共分为三类:

  1. 第一类, 内核异常(Internal Exceptions)
    由SysTick滴答定时器, Reset, HardFault触发, 直接通往NVIC处理
  2. 第二类, 通信与定时外设(Peripherals)
    由SPI, USART串口, I2C, 定时器, ADC等通信信号触发, 这些信号控制器有自己独立的逻辑, 产生信号后直接发给NVIC.
  3. 第三类: 外部IO中断(External Interrupts)
    由GPIO引脚触发, 引脚信号经EXTI传递给NVIC进行处理

中断的核心就是NVIC控制器, EXTI唯一的职责就是监测GPIO引脚的电平变化, 然后将触发的中断信号传递给NVIC.

中断优先级

中断优先级的判定并不是直观的理解, 优先级数字越小, 就越优先被触发. 这里面涉及到一个中断优先级分组(Priority Grouping)的概念.

STM32中优先级寄存器中只有4个bit位负责优先级, 这4位又可以分为两组, 一组名称为抢占优先级, 另一组名称为响应优先级, 然后两组优先级各有不同的排序逻辑.

抢占优先级(Preemption Priority) 逻辑: 优先级高的打断优先级低的
响应优先级/子优先级(Sub-priority) 逻辑: 优先级低的排在优先级高的后面, 不打断, 只排队

优先级分组共有下图中5种组合

Group 0: 只排队, 不打断

Group 1: 有1位负责抢占, 中断最多可以嵌套2层, 剩余3位决定排队先后

Group 2: 有2位负责抢占, 中断最多可以嵌套4层, 剩余2位决定排队先后

Group 3: 有3位负责抢占, 中断最多可以嵌套8层, 剩余1位决定排队先后

Group 4: 有4位负责抢占, 中断最多可以嵌套16层, 所有中断发生时只打断, 不排队

我们直觉上的中断优先级其实只属于Group 4的情况, 数字越小, 优先级越高的越优先执行

重点: 全局只允许有一套优先级分组规则, 程序启动时, 就先设定好优先级分组是那种, 后续所有的中断都将按照这组优先级逻辑进行执行裁定.

一般, 如果要追求系统实时性, 会选择Group 4, 全抢占. 但要注意如果中断嵌套层数太多, 会有栈溢出风险.

中断相关的关键词及含义

Level and pulse detection : 电平检测和脉冲(上升沿和下降沿)检测
Dynamic reprioritization of interrupts : 动态配置中断优先级
Grouping of priority values into group priority and subpriority fields : 优先级数值划分为抢占优先级和响应优先级两个字段
Interrupt tail-chaining 中断尾链功能
An external Non-maskable interrupt (NMI) 一路外部不可屏蔽中断
WFE(Wait For Event) 等待事件, 只要有一个唤醒信号, 就能让CPU开始工作, 不一定要执行终端函数
WFI (Wait For Interrupt) 等待中断, 必须要能够跳转中断函数才能唤醒CPU

按键中断实现

按键原理图如下:

CubeMX配置生成按键中断初始化代码

根据原理图, 设置PD8, PD9, PD10为EXTI外部中断线

然后配置页面中选择下降沿触发检测, 也就是按键按下时触发操作

下一步统一配置NVIC中断优先级. 点击右侧System View, 点击下方NVIC, 中间出现中断优先级设置界面. 左上角选择中断优先级分组, 我这里选择2位抢占, 2位响应

根据手册 RM0351 Reference manual 描述, EXTI 第5-9线的中断被合并到了EXTI9_5寄存器中,

EXTI第10-15线的中断被合并到了EXTI15_10寄存器中.

所以PD8, PD9的中断, 要在CubeMX中勾选 EXTI Line[9:5] interrupts

PD10的中断, 要在CubeMX中勾选 EXTI Line[15:10] interrupts

这里设置EXTI Line[9:5]和EXTI Line[15:10]的抢占优先级相同, 但响应优先级不同, 来区分他们, 这也是中断优先级的意义所在

生成代码

生成的统一配置优先级分组的代码在文件 stm32l4xx_hal_msp.c中

c 复制代码
void HAL_MspInit(void)
{

  /* USER CODE BEGIN MspInit 0 */

  /* USER CODE END MspInit 0 */

  __HAL_RCC_SYSCFG_CLK_ENABLE();
  __HAL_RCC_PWR_CLK_ENABLE();

  HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);

  /* System interrupt init*/

  /* USER CODE BEGIN MspInit 1 */

  /* USER CODE END MspInit 1 */
}

然后配置3个GPIO引脚的中断使能, 以及配置中断优先级的代码在gpio.c中

c 复制代码
void MX_GPIO_Init(void)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOH_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();
  __HAL_RCC_GPIOE_CLK_ENABLE();
  __HAL_RCC_GPIOD_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_RESET);

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOE, LED_R_Pin|LED_G_Pin|LED_B_Pin, GPIO_PIN_SET);

  /*Configure GPIO pin : BEEP_Pin */
  GPIO_InitStruct.Pin = BEEP_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(BEEP_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pins : LED_R_Pin LED_G_Pin LED_B_Pin */
  GPIO_InitStruct.Pin = LED_R_Pin|LED_G_Pin|LED_B_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);

  /*Configure GPIO pins : KEY2_Pin KEY1_Pin KEY0_Pin */
  GPIO_InitStruct.Pin = KEY2_Pin|KEY1_Pin|KEY0_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);

  /* EXTI interrupt init*/
  HAL_NVIC_SetPriority(EXTI9_5_IRQn, 3, 0);
  HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);

  HAL_NVIC_SetPriority(EXTI15_10_IRQn, 3, 0);
  HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);

}

中断处理函数在 stm32l4xx_it.c中

c 复制代码
/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file    stm32l4xx_it.c
  * @brief   Interrupt Service Routines.
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2026 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "stm32l4xx_it.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN TD */

/* USER CODE END TD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

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

/* USER CODE END 0 */

/* External variables --------------------------------------------------------*/

/* USER CODE BEGIN EV */

/* USER CODE END EV */

/******************************************************************************/
/*           Cortex-M4 Processor Interruption and Exception Handlers          */
/******************************************************************************/
/**
  * @brief This function handles Non maskable interrupt.
  */
void NMI_Handler(void)
{
  /* USER CODE BEGIN NonMaskableInt_IRQn 0 */

  /* USER CODE END NonMaskableInt_IRQn 0 */
  /* USER CODE BEGIN NonMaskableInt_IRQn 1 */
   while (1)
  {
  }
  /* USER CODE END NonMaskableInt_IRQn 1 */
}

/**
  * @brief This function handles Hard fault interrupt.
  */
void HardFault_Handler(void)
{
  /* USER CODE BEGIN HardFault_IRQn 0 */

  /* USER CODE END HardFault_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_HardFault_IRQn 0 */
    /* USER CODE END W1_HardFault_IRQn 0 */
  }
}

/**
  * @brief This function handles Memory management fault.
  */
void MemManage_Handler(void)
{
  /* USER CODE BEGIN MemoryManagement_IRQn 0 */

  /* USER CODE END MemoryManagement_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_MemoryManagement_IRQn 0 */
    /* USER CODE END W1_MemoryManagement_IRQn 0 */
  }
}

/**
  * @brief This function handles Prefetch fault, memory access fault.
  */
void BusFault_Handler(void)
{
  /* USER CODE BEGIN BusFault_IRQn 0 */

  /* USER CODE END BusFault_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_BusFault_IRQn 0 */
    /* USER CODE END W1_BusFault_IRQn 0 */
  }
}

/**
  * @brief This function handles Undefined instruction or illegal state.
  */
void UsageFault_Handler(void)
{
  /* USER CODE BEGIN UsageFault_IRQn 0 */

  /* USER CODE END UsageFault_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_UsageFault_IRQn 0 */
    /* USER CODE END W1_UsageFault_IRQn 0 */
  }
}

/**
  * @brief This function handles System service call via SWI instruction.
  */
void SVC_Handler(void)
{
  /* USER CODE BEGIN SVCall_IRQn 0 */

  /* USER CODE END SVCall_IRQn 0 */
  /* USER CODE BEGIN SVCall_IRQn 1 */

  /* USER CODE END SVCall_IRQn 1 */
}

/**
  * @brief This function handles Debug monitor.
  */
void DebugMon_Handler(void)
{
  /* USER CODE BEGIN DebugMonitor_IRQn 0 */

  /* USER CODE END DebugMonitor_IRQn 0 */
  /* USER CODE BEGIN DebugMonitor_IRQn 1 */

  /* USER CODE END DebugMonitor_IRQn 1 */
}

/**
  * @brief This function handles Pendable request for system service.
  */
void PendSV_Handler(void)
{
  /* USER CODE BEGIN PendSV_IRQn 0 */

  /* USER CODE END PendSV_IRQn 0 */
  /* USER CODE BEGIN PendSV_IRQn 1 */

  /* USER CODE END PendSV_IRQn 1 */
}

/**
  * @brief This function handles System tick timer.
  */
void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */

  /* USER CODE END SysTick_IRQn 0 */
  HAL_IncTick();
  /* USER CODE BEGIN SysTick_IRQn 1 */

  /* USER CODE END SysTick_IRQn 1 */
}

/******************************************************************************/
/* STM32L4xx Peripheral Interrupt Handlers                                    */
/* Add here the Interrupt Handlers for the used peripherals.                  */
/* For the available peripheral interrupt handler names,                      */
/* please refer to the startup file (startup_stm32l4xx.s).                    */
/******************************************************************************/

/**
  * @brief This function handles EXTI line[9:5] interrupts.
  */
void EXTI9_5_IRQHandler(void)
{
  /* USER CODE BEGIN EXTI9_5_IRQn 0 */

  /* USER CODE END EXTI9_5_IRQn 0 */
  /* 由于PD8和PD9的优先级是相同的, 在代码中, 
  	 代码的先后顺序再一次区分了它们的优先级,
  	 如果要调整优先级, 可以调整这里的代码顺序 */
  HAL_GPIO_EXTI_IRQHandler(KEY2_Pin);
  HAL_GPIO_EXTI_IRQHandler(KEY1_Pin);
  /* USER CODE BEGIN EXTI9_5_IRQn 1 */

  /* USER CODE END EXTI9_5_IRQn 1 */
}

/**
  * @brief This function handles EXTI line[15:10] interrupts.
  */
void EXTI15_10_IRQHandler(void)
{
  /* USER CODE BEGIN EXTI15_10_IRQn 0 */

  /* USER CODE END EXTI15_10_IRQn 0 */
  HAL_GPIO_EXTI_IRQHandler(KEY0_Pin);
  /* USER CODE BEGIN EXTI15_10_IRQn 1 */

  /* USER CODE END EXTI15_10_IRQn 1 */
}

/* USER CODE BEGIN 1 */

/* USER CODE END 1 */

然后我们通过自定义void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)回调函数, 来实现按钮按下所触发的逻辑.

在这里, 我们要实现一个按键消抖逻辑, 实现稳定的按钮按下一次就触发一次逻辑的功能

自定义key.h和key.c文件

key.h

c 复制代码
#ifndef KEY_H
#define KEY_H

#include <stdint.h>
#include <stdbool.h>

typedef enum _KEY_PUSH_BUTTON_ {
    BTN_KEY_0,
    BTN_KEY_1,
    BTN_KEY_2
} KEY_PUSH_BUTTON;

typedef struct _KeyPushEvent_ {
    KEY_PUSH_BUTTON btn;
} KeyPushEvent;

// ==================== 队列配置(根据需求修改) ====================
#define QUEUE_MAX_LEN    16      // 队列最大长度,建议2的幂,可根据需求调整
#define KEY_DATA_TYPE    KeyPushButton// 队列元素类型,存储按键按下的时间戳(如ms级)
// =================================================================

// 循环队列结构体
typedef struct {
    KeyPushEvent buffer[QUEUE_MAX_LEN];
    uint8_t front;
    uint8_t rear;
    uint8_t count;
} KeyEventQueue;

extern KeyEventQueue key_queue;

// 初始化按键事件队列
void key_event_queue_init(void);
// 获取按键事件
bool key_event_queue_pop(KeyPushEvent *data);

// BUTTON KEY 0 按下事件处理
void button_key_0_pressed(void);
// BUTTON KEY 1 按下事件处理
void button_key_1_pressed(void);
// BUTTON KEY 2 按下事件处理
void button_key_2_pressed(void);

#endif

key.c

c 复制代码
#include "key.h"
#include "gpio.h"
#include "main.h"
#include "trace.h"

// 全局队列实例(用于按键中断和主循环共享)
static KeyEventQueue  key_queue;

/**
 * @brief 初始化循环队列
 */
void key_event_queue_init(void) {
    key_queue.front = 0;
    key_queue.rear = 0;
    key_queue.count = 0;
}

/**
 * @brief 判断队列是否为空
 * @return true-空,false-非空
 */
bool key_event_queue_is_empty(void) {
    return (key_queue.count == 0);
}

/**
 * @brief 判断队列是否为满
 * @return true-满,false-未满
 */
bool key_event_queue_is_full(void) {
    return (key_queue.count == QUEUE_MAX_LEN);
}

/**
 * @brief 队列入队(中断中调用)
 * @param data 要入队的按键时间戳
 * @return true-入队成功,false-队列满失败
 */
bool key_event_queue_push(KeyPushEvent data) {
    // 进入临界区:关闭总中断,防止中断与主循环同时操作队列
    __disable_irq();

    bool ret = false;
    if (!key_event_queue_is_full()) {
        key_queue.buffer[key_queue.rear] = data;
        key_queue.rear = (key_queue.rear + 1) % QUEUE_MAX_LEN;
        key_queue.count++;
        ret = true;
    }

    // 退出临界区:开启总中断
    __enable_irq();
    return ret;
}

/**
 * @brief 队列出队(主循环中调用)
 * @param data 出队数据的存储地址
 * @return true-出队成功,false-队列空失败
 */
bool key_event_queue_pop(KeyPushEvent *data) {
    if (data == NULL) return false;

    // 进入临界区:关闭总中断(防止出队时中断写入)
    __disable_irq();

    bool ret = false;
    if (!key_event_queue_is_empty()) {
        *data = key_queue.buffer[key_queue.front];
        key_queue.front = (key_queue.front + 1) % QUEUE_MAX_LEN;
        key_queue.count--;
        ret = true;
    }

    // 退出临界区:开启总中断
    __enable_irq();
    return ret;
}

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    static uint32_t prev_tick = 0;
    uint32_t cur_tick = HAL_GetTick();

    if (cur_tick - prev_tick < 50) {
        return;
    }
    prev_tick = cur_tick;

    KeyPushEvent k_event = {.btn = BTN_KEY_0};

    switch (GPIO_Pin)
    {
    case KEY0_Pin:
        k_event.btn = BTN_KEY_0;
        break;
    case KEY1_Pin:
        k_event.btn = BTN_KEY_1;
        break;
    case KEY2_Pin:
        k_event.btn = BTN_KEY_2;
        break;
    default:
        break;
    }

    key_event_queue_push(k_event);
}

/**
 * @brief BUTTON KEY 0 按下后, 耗时操作
 */
void button_key_0_pressed() {
    HAL_GPIO_WritePin(GPIOE, LED_R_Pin, GPIO_PIN_SET);
    HAL_GPIO_WritePin(GPIOE, LED_G_Pin, GPIO_PIN_SET);
    HAL_GPIO_WritePin(GPIOE, LED_B_Pin, GPIO_PIN_SET);
    static uint8_t i = 0;
    i++;
    switch (i % 3)
    {
    case 0:
        HAL_GPIO_WritePin(GPIOE, LED_R_Pin, GPIO_PIN_RESET);
        TRACE_DEBUG("Key 0 been pressed, turn red on");
        break;
    case 1:
        HAL_GPIO_WritePin(GPIOE, LED_G_Pin, GPIO_PIN_RESET);
        TRACE_DEBUG("Key 0 been pressed, turn green on");
        break;
    case 2:
        HAL_GPIO_WritePin(GPIOE, LED_B_Pin, GPIO_PIN_RESET);
        TRACE_DEBUG("Key 0 been pressed, turn blue on");
        break;

    default:
        break;
    }
}

/**
 * @brief BUTTON KEY 1 按下后, 耗时操作
 */
void button_key_1_pressed() {
    TRACE_DEBUG("Key 1 been pressed, do switch light");
}

/**
 * @brief BUTTON KEY 2 按下后, 耗时操作
 */
void button_key_2_pressed() {
    TRACE_DEBUG("Key 2 been pressed, do switch light");
}

main.c中轮训引脚变化

c 复制代码
/* USER CODE BEGIN WHILE */

  KeyPushEvent k_event = {.btn = BTN_KEY_0};

  while (1)
  {
    if(key_event_queue_pop(&k_event)) {
      switch (k_event.btn)
      {
      case BTN_KEY_0:
        button_key_0_pressed();
        break;
      case BTN_KEY_1:
        button_key_1_pressed();
        break;
      case BTN_KEY_2:
        button_key_2_pressed();
        break;
      default:
        break;
      }
    }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

按键中断运行效果

相关推荐
破晓单片机2 小时前
STM32单片机分享:智能恒温箱系统
stm32·单片机·嵌入式硬件·智能家居
小痞同学2 小时前
【铁头山羊STM32】HAL库 6.中断部分
stm32·单片机·嵌入式硬件
你爱写程序吗(新H)2 小时前
基于单片机的洗衣机控制系统设计 单片机洗衣机控制(设计+文档)
c语言·汇编·单片机·嵌入式硬件·matlab
淘晶驰AK2 小时前
新手学单片机,主要是玩,学什么好?
单片机·嵌入式硬件
恶魔泡泡糖2 小时前
51单片机I-O扩展1
c语言·嵌入式硬件·51单片机
TEC_INO2 小时前
stm32_16:列表
单片机·嵌入式硬件
一路往蓝-Anbo2 小时前
【第48期】:嵌入式工程师的自我修养与进阶之路
开发语言·网络·stm32·单片机·嵌入式硬件
破晓单片机3 小时前
STM32单片机分享:智能浇花系统
stm32·单片机·嵌入式硬件
llilian_163 小时前
gps对时扩展装置 抢险救灾中时间同步精确的重要性分析 电力系统同步时钟
网络·功能测试·单片机·嵌入式硬件