【五】独立按键(中断方式)实验
1. 项目概述
本项目演示如何使用 STM32F103C8T6 微控制器通过外部中断(EXTI)方式检测独立按键,并根据按键状态控制LED灯的点亮。系统包含4个独立按键(KEY1-KEY4)和8个LED指示灯,采用中断触发方式,无需CPU持续查询,实现高效的按键检测。
技术要点
- ✅ EXTI外部中断配置
- ✅ NVIC中断优先级设置
- ✅ 下降沿触发中断
- ✅ 中断服务函数编写
- ✅ 中断标志位清除
- ✅ 中断内软件消抖
- ✅ 非阻塞式按键处理
与查询方式对比
| 特性 | 中断方式(本项目) | 查询方式(4A项目) |
|---|---|---|
| CPU占用 | 低(按键时才响应) | 高(一直查询) |
| 响应速度 | 快(立即中断) | 慢(取决于循环周期) |
| 实时性 | 好 | 一般 |
| 功耗 | 低(可配合休眠) | 高 |
| 编程复杂度 | 稍高 | 简单 |
| 适用场景 | 复杂应用、低功耗应用 | 简单应用、初学者 |
应用场景
- 🔘 低功耗应用(休眠唤醒)
- 🔘 多任务系统
- 🔘 实时响应需求
- 🔘 复杂控制系统
- 🔘 紧急按钮处理
- 🔘 专业嵌入式产品
2. 硬件平台
主控芯片
- 型号: STM32F103C8T6
- 内核: ARM Cortex-M3
- 主频: 72 MHz
- Flash: 64 KB
- SRAM: 20 KB
- EXTI线: 16条(EXTI0-EXTI15)
外设资源
独立按键(4个)
| 按键 | 引脚 | 中断线 | 中断通道 | 控制LED |
|---|---|---|---|---|
| KEY1 | PB8 | EXTI8 | EXTI9_5 | LED5 |
| KEY2 | PB9 | EXTI9 | EXTI9_5 | LED6 |
| KEY3 | PB10 | EXTI10 | EXTI15_10 | LED7 |
| KEY4 | PB11 | EXTI11 | EXTI15_10 | LED8 |
LED指示灯(8个)
| LED | 引脚 | 说明 |
|---|---|---|
| LED1-LED4 | PB0-PB3 | 指示灯1-4 |
| LED5 | PB4 | KEY1控制 |
| LED6 | PB5 | KEY2控制 |
| LED7 | PB6 | KEY3控制 |
| LED8 | PB7 | KEY4控制 |
硬件连接
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响应中断,执行中断服务函数
注意事项:
- 使用上拉输入模式(GPIO_Mode_IPU)
- 配置下降沿触发(EXTI_Trigger_Falling)
- 中断服务函数需清除中断标志位
- 中断内需要进行软件消抖
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
│ └─ 等待按键释放
├─ 恢复现场
└─ 返回主循环
功能特点
-
中断驱动:
- 按键按下时才响应
- CPU大部分时间空闲
- 可执行其他任务
- 功耗低
-
快速响应:
- 中断优先级高
- 立即响应按键
- 无需等待查询周期
- 实时性好
-
下降沿触发:
- 按键按下瞬间触发
- 明确的触发时刻
- 避免电平触发的重复中断
-
软件消抖:
- 中断内延时5ms
- 再次确认按键状态
- 有效过滤抖动
-
多中断通道:
- 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); // 等待外部中断触发
}
主函数要点:
-
初始化顺序:
NVIC_Configuration(); // 先配置中断优先级
LED_Init(); // 初始化LED
GPIO_Key_Init(); // 初始化按键和EXTI -
主循环空转:
while(1); // 无限循环,什么都不做
特点:
- CPU处于空闲状态
- 等待中断触发
- 可在此执行其他任务
- 可进入低功耗模式 -
中断方式的优势:
- 主循环简洁
- 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);
}
初始化要点:
-
AFIO时钟:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
作用:
- AFIO(复用功能IO)用于EXTI线配置
- 必须使能才能使用GPIO_EXTILineConfig -
EXTI线映射:
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource8);
功能:
- 将GPIO引脚连接到EXTI线
- PB8连接到EXTI8
- 每个EXTI线只能连接一个GPIO引脚 -
中断模式:
EXTI_Mode_Interrupt // 中断模式
对比:
- 中断模式:产生中断请求到NVIC
- 事件模式:产生事件信号(用于DMA、定时器等) -
下降沿触发:
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);
}
}
中断服务函数要点:
-
检查中断标志位:
if(EXTI_GetITStatus(EXTI_Line8) != RESET)
作用:
- 判断是哪个EXTI线触发了中断
- 一个中断服务函数可能处理多个EXTI线
- 必须检查标志位,避免误处理 -
清除中断标志位:
EXTI_ClearITPendingBit(EXTI_Line8);
重要性:
- 必须清除,否则会一直触发中断
- 清除后才能响应下次中断
- 通常在中断服务函数开始时清除 -
中断内消抖:
Delay_nms(5); // 延时5ms消抖
if(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0)
{
// 确认是真实按键,不是抖动
}
注意:
- 中断内延时会阻塞其他中断
- 延时时间不宜过长
- 更好的方法是使用定时器消抖 -
阻塞等待释放:
while(GPIO_ReadInputDataBit(KEY1_GPIO, KEY1_PIN) == 0);
作用:
- 等待按键完全释放
- 防止长按重复触发
缺点:
- 阻塞中断服务函数
- 无法响应其他中断
- 建议使用非阻塞方式 -
中断服务函数命名:
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配置要点:
-
优先级分组:
NVIC_PriorityGroup_2 // 2位抢占,2位响应
说明:
- 抢占优先级:0-3(4级)
- 响应优先级:0-3(4级)
- 数字越小,优先级越高 -
抢占优先级:
NVIC_IRQChannelPreemptionPriority = 2;
作用:
- 高抢占优先级可以打断低抢占优先级
- 例如:优先级1可以打断优先级2的中断 -
响应优先级:
NVIC_IRQChannelSubPriority = 0;
作用:
- 抢占优先级相同时,响应优先级决定谁先响应
- 不能相互打断
中断嵌套示例
中断嵌套场景:
时间 →
主程序 EXTI(2,0) 主程序 EXTI(2,1)
│ │ │ │
├──────────┤ │ │
│ 执行 │ │ │
│ ├─────────┤ │
│ │ │ │中断 │ │
│ │ │ │服务 │ │
│ │ │ │函数 │ │
│ │ │ │ │ │
│ │ │←─┘ │ │
│ │ │ │ │
│←──┘ │ │ │
│ │ ├──────────┤
│ │ │ │
│ │ │←─────────┘
说明:
- EXTI(2,0)表示抢占优先级2,响应优先级0
- 两个中断抢占优先级相同(都是2)
- 不能相互打断
- 响应优先级0的先响应
9. 使用说明
9.1 硬件连接
连接步骤
-
连接按键
KEY1 → PB8(按下接GND)
KEY2 → PB9(按下接GND)
KEY3 → PB10(按下接GND)
KEY4 → PB11(按下接GND) -
连接LED
LED1-LED8 → PB0-PB7(高电平点亮) -
连接调试器
SWDIO → PA13
SWCLK → PA14
GND → GND
VCC → 3.3V
9.2 编译与下载
编译工程
- 打开
MDK/TEST_CODE.uvproj - 选择目标芯片:STM32F103C8
- 编译:Project → Build Target (F7)
- 检查编译结果:0 Error(s), 0 Warning(s)
下载程序
- 配置调试器:Options → Debug → 选择 J-Link/ST-Link
- 下载程序:Flash → Download (F8)
- 复位运行
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:按键无响应
可能原因:
- ❌ EXTI配置错误
- ❌ NVIC未使能
- ❌ 中断服务函数名称错误
- ❌ 中断标志位未清除
解决方案:
// 检查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;
}
}