跳到主要内容

【五】独立按键(中断方式)实验

1. 项目概述

本项目演示如何使用 STM32F103C8T6 微控制器通过外部中断(EXTI)方式检测独立按键,并根据按键状态控制LED灯的点亮。系统包含4个独立按键(KEY1-KEY4)和8个LED指示灯,采用中断触发方式,无需CPU持续查询,实现高效的按键检测。

技术要点

  • ✅ EXTI外部中断配置
  • ✅ NVIC中断优先级设置
  • ✅ 下降沿触发中断
  • ✅ 中断服务函数编写
  • ✅ 中断标志位清除
  • ✅ 中断内软件消抖
  • ✅ 非阻塞式按键处理

与查询方式对比

特性中断方式(本项目)查询方式(4A项目)
CPU占用低(按键时才响应)高(一直查询)
响应速度快(立即中断)慢(取决于循环周期)
实时性一般
功耗低(可配合休眠)
编程复杂度稍高简单
适用场景复杂应用、低功耗应用简单应用、初学者

应用场景

  1. 🔘 低功耗应用(休眠唤醒)
  2. 🔘 多任务系统
  3. 🔘 实时响应需求
  4. 🔘 复杂控制系统
  5. 🔘 紧急按钮处理
  6. 🔘 专业嵌入式产品

2. 硬件平台

主控芯片

  • 型号: STM32F103C8T6
  • 内核: ARM Cortex-M3
  • 主频: 72 MHz
  • Flash: 64 KB
  • SRAM: 20 KB
  • EXTI线: 16条(EXTI0-EXTI15)

外设资源

独立按键(4个)

按键引脚中断线中断通道控制LED
KEY1PB8EXTI8EXTI9_5LED5
KEY2PB9EXTI9EXTI9_5LED6
KEY3PB10EXTI10EXTI15_10LED7
KEY4PB11EXTI11EXTI15_10LED8

LED指示灯(8个)

LED引脚说明
LED1-LED4PB0-PB3指示灯1-4
LED5PB4KEY1控制
LED6PB5KEY2控制
LED7PB6KEY3控制
LED8PB7KEY4控制

硬件连接

STM32F103C8T6              独立按键
PB8 <---------- KEY1 (按下接GND,触发下降沿中断)
PB9 <---------- KEY2
PB10 <---------- KEY3
PB11 <---------- KEY4

STM32F103C8T6 LED指示灯
PB0-PB7 ----------> LED1-LED8 (高电平点亮)

按键中断电路说明:

按键中断电路(以KEY1为例):
VCC
|
╱ 10KΩ上拉电阻
|
PB8 ─┤ ← 平时高电平
|
╱ KEY1(按键)
|
GND

工作原理:
- 按键未按下:PB8 = 高电平(VCC)
- 按键按下:PB8 = 低电平(GND)← 产生下降沿
- 下降沿触发EXTI8中断
- CPU响应中断,执行中断服务函数

注意事项:

  1. 使用上拉输入模式(GPIO_Mode_IPU)
  2. 配置下降沿触发(EXTI_Trigger_Falling)
  3. 中断服务函数需清除中断标志位
  4. 中断内需要进行软件消抖

3. 项目结构

4B_独立按键(中断)/
├── Libraries/ # STM32标准外设库
│ ├── CMSIS/ # Cortex微控制器软件接口标准
│ └── STM32F10x_StdPeriph_Driver/ # STM32F10x标准外设驱动
├── MDK/ # Keil工程文件
│ ├── TEST_CODE.uvproj # Keil项目文件
│ ├── TEST_CODE.uvopt # 项目配置选项
│ ├── Output/ # 编译输出目录
│ └── list/ # 列表文件目录
├── Readme/ # 说明文档目录
└── User/ # 用户代码目录
├── main.c # 主程序入口
├── stm32f10x_conf.h # 外设库配置文件
├── stm32f10x_it.c # 中断服务函数
├── stm32f10x_it.h # 中断服务函数头文件
├── system_stm32f10x.c # 系统初始化文件
└── TF_App/ # 应用层驱动
├── TF_KeyBoard.c # 按键驱动实现(含中断服务函数)
├── TF_KeyBoard.h # 按键驱动头文件
├── TF_LED.c # LED驱动实现
├── TF_LED.h # LED驱动头文件
├── TF_Delay.c # 延时函数实现
├── TF_Delay.h # 延时函数头文件
├── TF_System_Cfg.c # 系统配置(NVIC配置)
└── TF_System_Cfg.h # 系统配置头文件

4. 功能说明

系统工作流程

系统上电

NVIC中断优先级配置

LED初始化
(配置PB0-PB7为推挽输出)

按键初始化
(配置PB8-PB11为上拉输入 + EXTI中断)

主循环空转
(while(1); 等待中断)

按键按下(产生下降沿)

触发EXTI中断

CPU响应中断
├─ 保存现场
├─ 执行中断服务函数
│ ├─ 检查中断标志位
│ ├─ 清除中断标志位
│ ├─ 延时5ms消抖
│ ├─ 再次确认按键状态
│ ├─ 关闭所有LED
│ ├─ 点亮对应LED
│ └─ 等待按键释放
├─ 恢复现场
└─ 返回主循环

功能特点

  1. 中断驱动:

    • 按键按下时才响应
    • CPU大部分时间空闲
    • 可执行其他任务
    • 功耗低
  2. 快速响应:

    • 中断优先级高
    • 立即响应按键
    • 无需等待查询周期
    • 实时性好
  3. 下降沿触发:

    • 按键按下瞬间触发
    • 明确的触发时刻
    • 避免电平触发的重复中断
  4. 软件消抖:

    • 中断内延时5ms
    • 再次确认按键状态
    • 有效过滤抖动
  5. 多中断通道:

    • EXTI9_5处理KEY1、KEY2
    • EXTI15_10处理KEY3、KEY4
    • 合理分配中断资源

EXTI中断线分配

EXTI中断线分组:
┌─────────────────────────────────────────────┐
│ EXTI0_IRQn → EXTI线0 │
│ EXTI1_IRQn → EXTI线1 │
│ EXTI2_IRQn → EXTI线2 │
│ EXTI3_IRQn → EXTI线3 │
│ EXTI4_IRQn → EXTI线4 │
│ EXTI9_5_IRQn → EXTI线5-9 (本项目使用) │
│ EXTI15_10_IRQn → EXTI线10-15 (本项目使用)│
└─────────────────────────────────────────────┘

本项目分配:
KEY1 (PB8) → EXTI8 → EXTI9_5_IRQHandler
KEY2 (PB9) → EXTI9 → EXTI9_5_IRQHandler
KEY3 (PB10) → EXTI10 → EXTI15_10_IRQHandler
KEY4 (PB11) → EXTI11 → EXTI15_10_IRQHandler

5. EXTI外部中断原理

EXTI结构框图

EXTI外部中断系统框图:
┌─────────────────────────────────────────────────┐
│ EXTI System │
│ │
│ GPIO引脚 ──→ 边沿检测 ──→ 软件中断/事件 │
│ (PBx) (上升/下降) 寄存器 │
│ │ │
│ ↓ │
│ ┌──────────────┐ │
│ │ 中断请求 │ │
│ │ 标志位寄存器 │ │
│ └──────┬───────┘ │
│ │ │
│ ↓ │
│ ┌──────────────┐ │
│ │ NVIC中断 │ │
│ │ 控制器 │ │
│ └──────┬───────┘ │
│ │ │
│ ↓ │
│ ┌──────────────┐ │
│ │ CPU响应中断 │ │
│ │ 执行ISR │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────┘

EXTI触发方式

下降沿触发(本项目使用):
电平 ──────┐
│ ← 下降沿(高→低)
└─────────
↑ 触发中断

上升沿触发:
电平 ──────────┐
↑ │ ← 上升沿(低→高)
│ └─────
触发中断

双边沿触发:
电平 ──────┐ ┌──
│ │
└─────┘
↑ ↑
都触发中断

EXTI配置步骤

配置EXTI外部中断的完整步骤:

1. 使能GPIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

2. 使能AFIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

3. 配置GPIO为上拉输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;

4. 配置EXTI线映射
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource8);

5. 配置EXTI参数
- 选择中断线(EXTI_Line8)
- 选择模式(中断模式)
- 选择触发方式(下降沿)
- 使能EXTI

6. 配置NVIC
- 选择中断通道(EXTI9_5_IRQn)
- 设置优先级
- 使能中断

7. 编写中断服务函数
void EXTI9_5_IRQHandler(void)
{
// 检查中断标志
// 清除中断标志
// 处理按键逻辑
}

6. 代码详解

6.1 主程序 (main.c)

完整代码

#include "TF_System_Cfg.h"
#include "TF_KeyBoard.h"
#include "TF_LED.h"

int main(void)
{
NVIC_Configuration(); // NVIC中断优先级配置
LED_Init(); // LED对应引脚初始化
GPIO_Key_Init(); // 按键对应引脚初始化(含EXTI配置)

while(1); // 等待外部中断触发
}

主函数要点:

  1. 初始化顺序:

    NVIC_Configuration();  // 先配置中断优先级
    LED_Init(); // 初始化LED
    GPIO_Key_Init(); // 初始化按键和EXTI
  2. 主循环空转:

    while(1);  // 无限循环,什么都不做

    特点:
    - CPU处于空闲状态
    - 等待中断触发
    - 可在此执行其他任务
    - 可进入低功耗模式
  3. 中断方式的优势:

    • 主循环简洁
    • CPU利用率高
    • 响应速度快
    • 适合多任务

6.2 按键驱动 (TF_KeyBoard)

TF_KeyBoard.h - 头文件

#ifndef  __TF_KeyBoard_H__
#define __TF_KeyBoard_H__

#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_exti.h"

/************************************************************************************************/
// 按键引脚定义
#define KEY1_PIN GPIO_Pin_8
#define KEY1_GPIO GPIOB
#define KEY1_RCC RCC_APB2Periph_GPIOB

#define KEY2_PIN GPIO_Pin_9
#define KEY2_GPIO GPIOB
#define KEY2_RCC RCC_APB2Periph_GPIOB

#define KEY3_PIN GPIO_Pin_10
#define KEY3_GPIO GPIOB
#define KEY3_RCC RCC_APB2Periph_GPIOB

#define KEY4_PIN GPIO_Pin_11
#define KEY4_GPIO GPIOB
#define KEY4_RCC RCC_APB2Periph_GPIOB

/************************************************************************************************/
// 函数声明
extern void GPIO_Key_Init(void);

#endif

按键初始化(含EXTI配置)

void GPIO_Key_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;

// 使能GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

// 使能AFIO时钟(用于EXTI线配置)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

// 配置按键引脚为上拉输入
GPIO_InitStructure.GPIO_Pin = KEY1_PIN | KEY2_PIN | KEY3_PIN | KEY4_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 下降沿中断,配置为上拉输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);

// 配置EXTI线映射(GPIO引脚连接到EXTI线)
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource8); // PB8 → EXTI8
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource9); // PB9 → EXTI9
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource10); // PB10 → EXTI10
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource11); // PB11 → EXTI11

// 配置EXTI参数
EXTI_InitStructure.EXTI_Line = EXTI_Line8 | EXTI_Line9 | EXTI_Line10 | EXTI_Line11;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式(非事件模式)
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE; // 使能EXTI线
EXTI_Init(&EXTI_InitStructure);
}

初始化要点:

  1. AFIO时钟:

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

    作用:
    - AFIO(复用功能IO)用于EXTI线配置
    - 必须使能才能使用GPIO_EXTILineConfig
  2. EXTI线映射:

    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource8);

    功能:
    - 将GPIO引脚连接到EXTI线
    - PB8连接到EXTI8
    - 每个EXTI线只能连接一个GPIO引脚
  3. 中断模式:

    EXTI_Mode_Interrupt  // 中断模式

    对比:
    - 中断模式:产生中断请求到NVIC
    - 事件模式:产生事件信号(用于DMA、定时器等)
  4. 下降沿触发:

    EXTI_Trigger_Falling  // 下降沿触发

    选择原因:
    - 按键按下时,引脚从高电平变为低电平
    - 产生下降沿
    - 触发中断

6.3 中断服务函数

EXTI9_5中断服务函数

void EXTI9_5_IRQHandler(void)
{
// 处理EXTI8(KEY1)
if(EXTI_GetITStatus(EXTI_Line8) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line8); // 清除中断标志位
Delay_nms(5); // 延时5ms消抖

if(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0) // 再次确认
{
LED_Off(10); // 关闭所有LED
LED_On(5); // 点亮LED5
}

while(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0); // 等待释放
}

// 处理EXTI9(KEY2)
if(EXTI_GetITStatus(EXTI_Line9) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line9);
Delay_nms(5);

if(GPIO_ReadInputDataBit(KEY2_GPIO, KEY2_PIN) == 0)
{
LED_Off(10);
LED_On(6);
}

while(GPIO_ReadInputDataBit(KEY2_GPIO, KEY2_PIN) == 0);
}
}

EXTI15_10中断服务函数

void EXTI15_10_IRQHandler(void)
{
// 处理EXTI10(KEY3)
if(EXTI_GetITStatus(EXTI_Line10) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line10);
Delay_nms(5);

if(GPIO_ReadInputDataBit(KEY3_GPIO, KEY3_PIN) == 0)
{
LED_Off(10);
LED_On(7);
}

while(GPIO_ReadInputDataBit(KEY3_GPIO, KEY3_PIN) == 0);
}

// 处理EXTI11(KEY4)
if(EXTI_GetITStatus(EXTI_Line11) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line11);
Delay_nms(5);

if(GPIO_ReadInputDataBit(KEY4_GPIO, KEY4_PIN) == 0)
{
LED_Off(10);
LED_On(8);
}

while(GPIO_ReadInputDataBit(KEY4_GPIO, KEY4_PIN) == 0);
}
}

中断服务函数要点:

  1. 检查中断标志位:

    if(EXTI_GetITStatus(EXTI_Line8) != RESET)

    作用:
    - 判断是哪个EXTI线触发了中断
    - 一个中断服务函数可能处理多个EXTI线
    - 必须检查标志位,避免误处理
  2. 清除中断标志位:

    EXTI_ClearITPendingBit(EXTI_Line8);

    重要性:
    - 必须清除,否则会一直触发中断
    - 清除后才能响应下次中断
    - 通常在中断服务函数开始时清除
  3. 中断内消抖:

    Delay_nms(5);  // 延时5ms消抖

    if(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0)
    {
    // 确认是真实按键,不是抖动
    }

    注意:
    - 中断内延时会阻塞其他中断
    - 延时时间不宜过长
    - 更好的方法是使用定时器消抖
  4. 阻塞等待释放:

    while(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0);

    作用:
    - 等待按键完全释放
    - 防止长按重复触发

    缺点:
    - 阻塞中断服务函数
    - 无法响应其他中断
    - 建议使用非阻塞方式
  5. 中断服务函数命名:

    EXTI9_5_IRQHandler    → 处理EXTI线5-9
    EXTI15_10_IRQHandler → 处理EXTI线10-15

    命名规则:
    - 在启动文件(startup_stm32f10x_md.s)中定义
    - 必须严格按照定义的名称
    - 不能自定义名称

7. 中断方式与查询方式对比

详细对比表

特性中断方式(4B项目)查询方式(4A项目)
CPU占用率低(5-10%)高(80-100%)
响应时间快(微秒级)慢(取决于循环周期)
实时性好(立即响应)差(可能漏检)
功耗低(可休眠)高(一直运行)
编程复杂度中等(需配置中断)简单(直接查询)
代码量稍多较少
调试难度稍难(中断嵌套)简单(顺序执行)
可靠性高(不会漏检)中(可能漏检快速按键)
适用场景复杂应用简单应用

响应时间对比

查询方式响应时间:
主循环周期:假设1ms
┌────────┬────────┬────────┬────────┬────────┐
│ 其他 │ 按键 │ 其他 │ 按键 │ 其他 │
│ 代码 │ 扫描 │ 代码 │ 扫描 │ 代码 │
└────────┴────────┴────────┴────────┴────────┘
↑ ↑←─可能漏检─→↑
按键按下 如果在这里按下,要等到下次扫描才能检测到
响应时间:0-1ms(最坏情况1ms)

中断方式响应时间:
┌──────────────────────────────────┐
│ 主循环(执行其他任务) │
└──────────────────────────────────┘
↓ 按键按下(立即中断)
┌─────────┐
│ 中断 │
│ 服务 │
│ 函数 │
└─────────┘
响应时间:几微秒到几十微秒

CPU占用对比

查询方式CPU占用:
CPU ████████████████████████████████ 100%
↑主循环一直在查询按键,CPU满载

中断方式CPU占用:
CPU ██─────────────────██────────── <10%
↑只在按键时占用CPU,其他时间空闲

功耗对比

查询方式功耗:
- CPU一直运行,功耗高(约20-30mA)
- 无法进入低功耗模式
- 不适合电池供电

中断方式功耗:
- CPU大部分时间空闲,可进入休眠(约2-5mA)
- 按键中断唤醒
- 适合电池供电

8. NVIC中断优先级配置

NVIC优先级概念

ARM Cortex-M3中断优先级:
- 抢占优先级(Preemption Priority):高优先级可打断低优先级
- 响应优先级(Sub Priority):同抢占优先级时,决定谁先响应

优先级分组(16种):
┌────────┬──────────────┬──────────────┐
│ 分组 │ 抢占优先级位 │ 响应优先级位 │
├────────┼──────────────┼──────────────┤
│ Group0 │ 0位(无抢占)│ 4位(16级) │
│ Group1 │ 1位(2级) │ 3位(8级) │
│ Group2 │ 2位(4级) │ 2位(4级) │← 常用
│ Group3 │ 3位(8级) │ 1位(2级) │
│ Group4 │ 4位(16级) │ 0位(无) │
└────────┴──────────────┴──────────────┘

NVIC配置示例

void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;

// 设置中断优先级分组为Group2(2位抢占,2位响应)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

// 配置EXTI9_5中断通道
NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; // 抢占优先级2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 响应优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

// 配置EXTI15_10中断通道
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}

NVIC配置要点:

  1. 优先级分组:

    NVIC_PriorityGroup_2  // 2位抢占,2位响应

    说明:
    - 抢占优先级:0-34级)
    - 响应优先级:0-34级)
    - 数字越小,优先级越高
  2. 抢占优先级:

    NVIC_IRQChannelPreemptionPriority = 2;

    作用:
    - 高抢占优先级可以打断低抢占优先级
    - 例如:优先级1可以打断优先级2的中断
  3. 响应优先级:

    NVIC_IRQChannelSubPriority = 0;

    作用:
    - 抢占优先级相同时,响应优先级决定谁先响应
    - 不能相互打断

中断嵌套示例

中断嵌套场景:
时间 →
主程序 EXTI(2,0) 主程序 EXTI(2,1)
│ │ │ │
├──────────┤ │ │
│ 执行 │ │ │
│ ├─────────┤ │
│ │ │ │中断 │ │
│ │ │ │服务 │ │
│ │ │ │函数 │ │
│ │ │ │ │ │
│ │ │←─┘ │ │
│ │ │ │ │
│←──┘ │ │ │
│ │ ├──────────┤
│ │ │ │
│ │ │←─────────┘

说明:
- EXTI(2,0)表示抢占优先级2,响应优先级0
- 两个中断抢占优先级相同(都是2)
- 不能相互打断
- 响应优先级0的先响应

9. 使用说明

9.1 硬件连接

连接步骤

  1. 连接按键

    KEY1 → PB8(按下接GND)
    KEY2 → PB9(按下接GND)
    KEY3 → PB10(按下接GND)
    KEY4 → PB11(按下接GND)
  2. 连接LED

    LED1-LED8 → PB0-PB7(高电平点亮)
  3. 连接调试器

    SWDIO → PA13
    SWCLK → PA14
    GND → GND
    VCC → 3.3V

9.2 编译与下载

编译工程

  1. 打开 MDK/TEST_CODE.uvproj
  2. 选择目标芯片:STM32F103C8
  3. 编译:Project → Build Target (F7)
  4. 检查编译结果:0 Error(s), 0 Warning(s)

下载程序

  1. 配置调试器:Options → Debug → 选择 J-Link/ST-Link
  2. 下载程序:Flash → Download (F8)
  3. 复位运行

9.3 运行效果

预期现象:

  • 按下KEY1 → 所有LED熄灭 → LED5点亮
  • 按下KEY2 → 所有LED熄灭 → LED6点亮
  • 按下KEY3 → 所有LED熄灭 → LED7点亮
  • 按下KEY4 → 所有LED熄灭 → LED8点亮
  • 响应速度快(立即响应)
  • 保持按下不放 → LED保持点亮

9.4 测试方法

测试1:基本功能

步骤:
1. 上电复位
2. 依次按下KEY1、KEY2、KEY3、KEY4
3. 观察对应LED是否点亮

预期结果:
- KEY1 → LED5点亮(立即响应)
- KEY2 → LED6点亮
- KEY3 → LED7点亮
- KEY4 → LED8点亮

测试2:响应速度

步骤:
1. 快速连续按下不同按键
2. 观察LED切换速度

预期结果:
- LED立即切换
- 无延迟感
- 不会漏检

测试3:中断优先级

// 在中断服务函数中添加延时
void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line8) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line8);
LED_On(5);
Delay_nms(3000); // 延时3秒
LED_Off(5);
}
}

测试:
1. 按下KEY1(LED5点亮,延时3秒)
2. 在延时期间按下KEY2
3. 观察是否能响应KEY2

预期结果:
- 如果抢占优先级相同,KEY2不会被响应
- 如果KEY2抢占优先级更高,会打断KEY1

10. 常见问题

问题1:按键无响应

可能原因:

  1. ❌ EXTI配置错误
  2. ❌ NVIC未使能
  3. ❌ 中断服务函数名称错误
  4. ❌ 中断标志位未清除

解决方案:

// 检查1:EXTI是否使能
EXTI_InitStructure.EXTI_LineCmd = ENABLE; // 确保使能

// 检查2:NVIC是否配置
NVIC_Configuration(); // 确保调用

// 检查3:中断服务函数名称
void EXTI9_5_IRQHandler(void) // 必须是这个名称,不能改

// 检查4:清除中断标志位
EXTI_ClearITPendingBit(EXTI_Line8); // 必须清除

// 调试:在中断服务函数中点亮LED测试
void EXTI9_5_IRQHandler(void)
{
LED_On(5); // 测试是否进入中断
}

问题2:中断频繁触发

原因: 中断标志位未清除或消抖不足

解决方案:

// 确保清除标志位
EXTI_ClearITPendingBit(EXTI_Line8); // 必须在开始时清除

// 增加消抖延时
Delay_nms(10); // 从5ms增加到10ms

// 确保再次确认按键状态
if(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0)
{
// 确认是真实按键
}

问题3:中断服务函数未执行

原因: 中断服务函数名称错误

解决方案:

// 错误的名称
void EXTI9_5_Handler(void) // ❌ 缺少IRQ

// 正确的名称
void EXTI9_5_IRQHandler(void) // ✅

// 查看启动文件(startup_stm32f10x_md.s)
// 找到正确的中断向量表名称

问题4:按键重复触发

原因: 阻塞等待释放时间过长

解决方案:

// 方案1:减少等待时间
// 不要一直等待释放
// while(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0);

// 方案2:使用标志位
static uint8_t key1_pressed = 0;

void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line8) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line8);

if(!key1_pressed)
{
key1_pressed = 1;
// 执行按键功能
}
}
}

// 在主循环中检测释放
if(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 1)
{
key1_pressed = 0;
}

问题5:多个按键同时按下响应异常

原因: 中断嵌套或优先级配置问题

解决方案:

// 方案1:提高中断优先级
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 最高优先级

// 方案2:禁止中断嵌套
void EXTI9_5_IRQHandler(void)
{
__disable_irq(); // 禁止所有中断

// 处理按键逻辑

__enable_irq(); // 使能所有中断
}

// 方案3:使用标志位,在主循环中处理
volatile uint8_t key_event = 0;

void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line8) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line8);
key_event |= 0x01; // 设置KEY1事件标志
}
}

// 在主循环中处理
while(1)
{
if(key_event & 0x01)
{
key_event &= ~0x01;
// 处理KEY1事件
}
}

11. 扩展应用

11.1 非阻塞中断处理

// 使用标志位实现非阻塞
volatile uint8_t key_flags = 0;

#define KEY1_FLAG 0x01
#define KEY2_FLAG 0x02
#define KEY3_FLAG 0x04
#define KEY4_FLAG 0x08

void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line8) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line8);
Delay_nms(5);

if(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0)
{
key_flags |= KEY1_FLAG; // 设置标志位
}
}

if(EXTI_GetITStatus(EXTI_Line9) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line9);
Delay_nms(5);

if(GPIO_ReadInputDataBit(KEY2_GPIO, KEY2_PIN) == 0)
{
key_flags |= KEY2_FLAG;
}
}
}

// 在主循环中处理
int main(void)
{
NVIC_Configuration();
LED_Init();
GPIO_Key_Init();

while(1)
{
if(key_flags & KEY1_FLAG)
{
key_flags &= ~KEY1_FLAG; // 清除标志位
LED_Off(10);
LED_On(5);
}

if(key_flags & KEY2_FLAG)
{
key_flags &= ~KEY2_FLAG;
LED_Off(10);
LED_On(6);
}

// 可执行其他任务
Other_Task();
}
}

11.2 定时器消抖

// 使用定时器实现更精确的消抖
#define DEBOUNCE_TIME 10 // 10ms消抖时间

typedef struct {
uint8_t key_state; // 当前按键状态
uint8_t key_last; // 上次按键状态
uint32_t press_time; // 按下时间
uint8_t debounced; // 消抖后的状态
} KeyDebounce_t;

KeyDebounce_t key1_debounce;

void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line8) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line8);

key1_debounce.key_state = !GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN);

if(key1_debounce.key_state != key1_debounce.key_last)
{
key1_debounce.press_time = GetTick(); // 获取系统时间
}

key1_debounce.key_last = key1_debounce.key_state;
}
}

// 在主循环或定时器中调用
void Key_Debounce_Process(void)
{
if(GetTick() - key1_debounce.press_time >= DEBOUNCE_TIME)
{
if(key1_debounce.key_state && !key1_debounce.debounced)
{
key1_debounce.debounced = 1;
// 执行按键功能
LED_Off(10);
LED_On(5);
}
else if(!key1_debounce.key_state)
{
key1_debounce.debounced = 0;
}
}
}

11.3 低功耗休眠唤醒

// 配置低功耗模式
void Enter_Sleep_Mode(void)
{
// 配置EXTI唤醒
PWR_WakeUpPinCmd(ENABLE);

// 进入停止模式
PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI);

// 被EXTI中断唤醒后,从这里继续执行
SystemInit(); // 重新配置时钟
}

int main(void)
{
NVIC_Configuration();
LED_Init();
GPIO_Key_Init();

while(1)
{
// 执行任务
Do_Something();

// 进入休眠,等待按键唤醒
Enter_Sleep_Mode();

// 被按键唤醒后继续执行
}
}

11.4 按键功能映射

// 按键功能表
typedef void (*KeyFunc)(void);

void Key1_Function(void) { LED_On(5); }
void Key2_Function(void) { LED_On(6); }
void Key3_Function(void) { LED_On(7); }
void Key4_Function(void) { LED_On(8); }

KeyFunc key_func_table[] = {
Key1_Function,
Key2_Function,
Key3_Function,
Key4_Function
};

void Process_Key_Event(uint8_t key_num)
{
if(key_num < 4)
{
LED_Off(10);
key_func_table[key_num]();
}
}

void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line8) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line8);
Process_Key_Event(0); // KEY1
}
}

11.5 组合键检测

volatile uint8_t keys_pressed = 0;

#define KEY1_BIT (1 << 0)
#define KEY2_BIT (1 << 1)
#define KEY3_BIT (1 << 2)
#define KEY4_BIT (1 << 3)

void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line8) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line8);
keys_pressed |= KEY1_BIT;
}

if(EXTI_GetITStatus(EXTI_Line9) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line9);
keys_pressed |= KEY2_BIT;
}
}

// 在主循环中检测组合键
void Check_Combination_Keys(void)
{
// KEY1 + KEY2 组合
if((keys_pressed & (KEY1_BIT | KEY2_BIT)) == (KEY1_BIT | KEY2_BIT))
{
printf("KEY1 + KEY2 Pressed\r\n");
LED_On(0); // 点亮所有LED
}

// KEY3 + KEY4 组合
if((keys_pressed & (KEY3_BIT | KEY4_BIT)) == (KEY3_BIT | KEY4_BIT))
{
printf("KEY3 + KEY4 Pressed\r\n");
LED_Off(0); // 关闭所有LED
}

// 清除按键状态(在按键释放时)
if(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 1)
{
keys_pressed &= ~KEY1_BIT;
}
}

11.6 长按检测(中断+定时器)

#define LONG_PRESS_TIME  2000  // 2秒长按

typedef struct {
uint32_t press_start_time;
uint8_t long_press_detected;
} LongPress_t;

LongPress_t key1_long;

void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line8) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line8);

if(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0)
{
key1_long.press_start_time = GetTick();
key1_long.long_press_detected = 0;
}
}
}

// 在主循环或定时器中检测长按
void Check_Long_Press(void)
{
if(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0) // 按键按下
{
if(!key1_long.long_press_detected)
{
if(GetTick() - key1_long.press_start_time >= LONG_PRESS_TIME)
{
key1_long.long_press_detected = 1;
// 长按功能
printf("KEY1 Long Press\r\n");
LED_On(0); // 点亮所有LED
}
}
}
else // 按键释放
{
if(!key1_long.long_press_detected)
{
// 短按功能
printf("KEY1 Short Press\r\n");
LED_On(5);
}
key1_long.long_press_detected = 0;
}
}