04-GPIO输入与NVIC中断系统.md 9.8 KB


tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(基础篇)V1.0.2 — 第7章:按键与中断系统 + 配套代码05~06" author: "尚硅谷研究院" date: 2026-07-15

created: 2026-07-15

GPIO输入与NVIC中断系统

用生活理解:中断就像你正在写代码,突然电话响了——你暂停写代码、接电话、处理完、挂电话、继续写代码。按键中断就是"按键按下 → 暂停主程序 → 处理按键 → 恢复主程序"。NVIC 就像一个智能电话秘书,知道哪个电话更重要、该先接哪个。


中断系统概述

为什么需要中断

如果没有中断,CPU 只能轮询检测按键状态——就像一直盯着电话看它响不响,浪费 CPU 时间。中断机制允许外设在需要时主动通知 CPU,CPU 不用一直等。

STM32中断体系架构

STM32F103 的中断体系由两部分组成:

部件 功能 参考手册章节
NVIC (嵌套向量中断控制器) 中断优先级管理、使能/屏蔽 §8 NVIC
EXTI (外部中断/事件控制器) 管理 GPIO 引脚的边沿检测与中断请求 §9 EXTI

中断响应过程:

GPIO引脚电平变化 → EXTI检测边沿 → NVIC判断优先级 → CPU暂停当前任务
→ 跳转到中断向量表 → 执行中断服务函数(ISR) → 返回主程序

中断优先级

NVIC 支持 16 级可编程中断优先级(0 最高,15 最低)。

优先级分组:4 位优先级可拆分为"抢占优先级 + 子优先级"

分组 分配 抢占优先级位数 子优先级位数
NVIC_PriorityGroup_0 0位抢占 + 4位子优先 0 4
NVIC_PriorityGroup_1 1位抢占 + 3位子优先 2级 8级
NVIC_PriorityGroup_2 2位抢占 + 2位子优先 4级 4级
NVIC_PriorityGroup_3 3位抢占 + 1位子优先 8级 2级
NVIC_PriorityGroup_4 4位抢占 + 0位子优先 16级 0

规则:抢占优先级高的可以打断低的;相同抢占优先级下,子优先级高的先执行但不打断。

EXTI 外部中断

STM32F103 有 20 个 EXTI 通道(EXTI0~EXTI19),每个 GPIO 引脚都可以配置为 EXTI 输入。

EXTI配置流程

1. GPIO配置(输入模式) → 2. AFIO选择中断线 → 3. EXTI配置(触发边沿+使能) → 4. NVIC配置(优先级+使能)

EXTI 关键寄存器

寄存器 功能 位说明
IMR 中断屏蔽寄存器 1=允许中断请求,0=禁止
EMR 事件屏蔽寄存器 1=允许事件请求,0=禁止
RTSR 上升沿触发选择 1=允许上升沿触发
FTSR 下降沿触发选择 1=允许下降沿触发
SWIER 软件中断事件寄存器 写1=软件触发中断
PR 挂起寄存器 1=有中断请求,写1清除

参考:参考手册 §9(EXTI 寄存器描述)+ §8(NVIC 寄存器描述)


按键输入硬件电路

KEY电路原理图

开发板上有 4 个按键:

按键 对应引脚 连接方式 按下时电平
KEY_UP (SW4) PF10 上拉输入 低变高
KEY_1 (SW2) 外部电路 取决于设计
KEY_2 外部电路 取决于设计
KEY_3 (SW5) PF10? 上拉输入 低变高

说明:PF10 对应的 KEY 为上拉输入模式——按键未按下时 PF10=低(下拉到GND),按下时 PF10=高(上拉到 VCC)。

消抖:机械按键在按下/释放瞬间会产生约 5~10ms 的抖动(电平快速跳变),需要软件消抖:

理想波形: __┃┃________
实际波形: __┃_┃_┃┃_┃_┃____  ← 抖动
软件消抖: 检测到边沿 → 等待15ms → 再次确认电平

实验:按键控制LED(中断方式)

需求描述

按下 KEY_1(PF10),翻转 LED1 的开关状态。使用外部中断 EXTI15_10 实现。

软件设计(寄存器版)

项目路径stm32/05_key_led_register

文件:stm32/05_key_led_register/Hardware/KEY/key.h

#ifndef __KEY_H
#define __KEY_H

#include "stm32f10x.h"

void KEY_Init(void);

#endif

文件:stm32/05_key_led_register/Hardware/KEY/key.c

#include "key.h"
#include "delay.h"
#include "led.h"

// KEY初始化:配置PF10为上拉输入 + EXTI中断
void KEY_Init(void)
{
    /* ========== 1. 开启时钟 ========== */
    // GPIOF时钟(操作PF10需要)
    RCC->APB2ENR |= RCC_APB2ENR_IOPFEN;     // |=: IOPFEN位置1
    // AFIO时钟(配置中断线重映射需要)
    RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;     // |=: AFIOEN位置1

    /* ========== 2. 配置PF10为上拉输入 ========== */
    // MODE10[1:0] = 00 → 输入模式
    GPIOF->CRH &= ~GPIO_CRH_MODE10;          // &=~: MODE10位清0
    // CNF10[1:0] = 10 → 上拉/下拉输入
    GPIOF->CRH |= GPIO_CRH_CNF10_1;           // |=: CNF10_1位置1
    GPIOF->CRH &= ~GPIO_CRH_CNF10_0;         // &=~: CNF10_0位清0
    // 开启上拉电阻(ODR对应位置1 = 上拉)
    // PF10 ODR位=1 → 上拉;ODR位=0 → 下拉
    GPIOF->ODR &= ~GPIO_ODR_ODR10;            // 选择下拉(按键未按时低电平)

    /* ========== 3. AFIO选择EXTI中断线 ========== */
    // EXTI10连接到PF10(EXTICR[2]的第8~11位控制EXTI10~EXTI13)
    AFIO->EXTICR[2] |= AFIO_EXTICR3_EXTI10_PF;

    /* ========== 4. 配置EXTI ========== */
    // RTSR: 上升沿触发选择寄存器 — PF10从低变高(按键按下)时触发
    EXTI->RTSR |= EXTI_RTSR_TR10;            // |=: EXTI10上升沿触发使能
    // IMR: 中断屏蔽寄存器 — 允许EXTI10产生中断
    EXTI->IMR |= EXTI_IMR_MR10;              // |=: EXTI10中断不屏蔽

    /* ========== 5. 配置NVIC ========== */
    // 设置优先级分组为Group_3:3位抢占(8级) + 1位子优先(2级)
    NVIC_SetPriorityGrouping(3);
    // 设置EXTI15_10中断的抢占优先级=2
    NVIC_SetPriority(EXTI15_10_IRQn, 2);
    // 使能EXTI15_10中断通道
    NVIC_EnableIRQ(EXTI15_10_IRQn);
}

// EXTI15_10中断服务函数(10~15共用一个中断入口)
void EXTI15_10_IRQHandler(void)
{
    /* 1. 清除中断标志位 */
    // PR寄存器的对应位写1清除挂起标志
    // 如果不清除,退出中断后立即再次进入
    EXTI->PR |= EXTI_PR_PR10;               // |=: PR10写1清除

    /* 2. 软件消抖 */
    Delay_ms(15);   // 等待15ms避开抖动期

    /* 3. 再次确认按键确实按下 */
    // 读取IDR判断PF10是否仍为高电平(屏蔽状态)
    if ((GPIOF->IDR & GPIO_IDR_IDR10) != 0) // &测试: IDR10位是否为1
    {
        // 翻转LED1状态
        LED_Toggle(LED1);
    }
}

文件:stm32/05_key_led_register/User/main.c

#include "key.h"
#include "led.h"

int main(void)
{
    LED_Init();     // LED初始化
    KEY_Init();     // KEY初始化(含中断配置)

    while (1)
    {
        // 主循环什么都不用做,等待中断发生
        // 按键按下 → EXTI中断 → ISR翻转LED → 返回继续等
    }
}

软件设计(HAL库版)

项目路径stm32/06_key_led_hal

CubeMX 配置要点:

  1. PF10 配置为 GPIO_EXTI10 + GPIO_PULLUP(上拉输入)
  2. NVIC 中使能 EXTI15_10_IRQn
  3. stm32f1xx_it.cEXTI15_10_IRQHandler() 中添加用户代码

HAL 库中断处理流程:

外部中断 → HAL库的 HAL_GPIO_EXTI_IRQHandler() 调用
→ HAL_GPIO_EXTI_Callback() → 用户重写Callback函数

文件:stm32/06_key_led_hal/Core/Src/main.c(用户代码部分)

/* USER CODE BEGIN 0 */
// HAL库的中断回调函数(由HAL_GPIO_EXTI_IRQHandler调用)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == KEY1_Pin)   // 判断是哪个引脚触发的中断
    {
        HAL_Delay(15);                          // 消抖
        if (HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) == GPIO_PIN_SET) // 再次确认
        {
            HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin); // 翻转LED
        }
    }
}
/* USER CODE END 0 */

对比寄存器版:HAL 库将中断的底层操作(PR清除、中断线判断)封装在 HAL_GPIO_EXTI_IRQHandler 中,用户只需要重写 Callback 函数。


核心函数速查表

寄存器版

函数/操作 代码 说明
KEY初始化 KEY_Init() 时钟+GPIO+AFIO+EXTI+NVIC
清除中断标志 `EXTI->PR = EXTI_PR_PR10`
读取引脚 GPIOF->IDR & GPIO_IDR_IDR10 &测试:测量PF10电平
软件消抖 Delay_ms(15) 避开机械抖动期

HAL库版

HAL函数 说明
HAL_GPIO_ReadPin(GPIOx, pin) 读取引脚电平
HAL_GPIO_TogglePin(GPIOx, pin) 翻转引脚电平
HAL_GPIO_EXTI_IRQHandler(pin) EXTI中断入口(清标志+调Callback)
HAL_GPIO_EXTI_Callback(pin) 用户重写的回调函数

NVIC函数(core_cm3.h)

函数 说明
NVIC_EnableIRQ(IRQn) 使能中断通道
NVIC_DisableIRQ(IRQn) 禁止中断通道
NVIC_SetPriority(IRQn, priority) 设置中断优先级
NVIC_SetPriorityGrouping(group) 设置优先级分组

常见问题与避坑

  1. 中断不触发 → 检查 NVIC 是否使能、EXTI IMR 是否允许、GPIO 模式是否正确(不能是模拟输入)
  2. 中断反复触发 → EXTI 的 PR 标志位未清除(必须写1清除)
  3. 按键抖动导致误触发 → 加软件延时消抖(10~15ms)
  4. 高了优先级的任务被卡死 → 中断服务函数要尽量简短(只设标志位,不处理复杂逻辑)
  5. 多个中断共用一个入口 → 在 ISR 中判断 PR 寄存器的不同位区分来源
  6. HAL 库中中断不工作 → 检查 HAL_GPIO_EXTI_IRQHandler 是否在启动文件中正确注册