一、项目说明
本实验使用 STM32F103 外部中断(EXTI) 实现:按键 / 震动传感器触发外部中断 → 进入中断服务函数 → LED 亮 1 秒后自动熄灭。
之前我写的按键控制led是轮询方式,相比轮询方式,中断方式不占用 CPU 资源、响应速度更快、程序效率更高,是 STM32 开发必备核心技能。
二、硬件连接
- 按键 → PA0(外部中断输入引脚)
- LED → PA1(推挽输出控制)
- 共地 GND
三、完整代码
1. 整体工作流程
- 系统上电,初始化 LED、外部中断
- 主循环空跑,CPU 等待中断
- 按键 → PA0 从高电平变为低电平
- 产生下降沿信号 → 触发 EXTI0 外部中断
- CPU 暂停主程序,自动进入
EXTI0_IRQHandler - LED 亮 1 秒 → 熄灭
- 清除中断标志,返回主循环继续等待
2.main.c 主程序文件
// 包含STM32标准库底层驱动
#include "stm32f10x.h"
// 主头文件
#include "main.h"
// LED驱动头文件
#include "led.h"
// 震动/按键外部中断驱动头文件
#include "shake.h"
/**
* @brief 毫秒级软件延时函数
* @param time: 延时时间,单位约1ms
* @retval 无
*/
void delay(uint16_t time)
{
uint16_t i = 0;
while(time --) // 循环time次
{
i = 12000; // 内层循环,控制单次延时长度
while(i --);
}
}
/**
* @brief 主函数
* @retval int
*/
int main()
{
// 【重要】设置中断分组:2位抢占优先级 + 2位子优先级
// 整个工程只需要配置一次
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
LED_Init(); // 初始化LED引脚 PA1
Shake_Init(); // 初始化外部中断引脚 PA0
GPIO_SetBits( GPIOA, GPIO_Pin_1); // 默认LED熄灭(高电平)
while(1) // 主循环死循环
{
// 主循环空跑,所有逻辑交给中断处理
}
}
/**
* @brief 外部中断0服务函数
* @note 函数名固定,不能修改
* @retval 无
*/
void EXTI0_IRQHandler()
{
// 判断:是否是EXTI_Line0触发的中断
if(EXTI_GetITStatus(EXTI_Line0) != RESET )
{
GPIO_ResetBits(GPIOA,GPIO_Pin_1); // 点亮LED(低电平有效)
delay(1000); // 保持亮1秒
GPIO_SetBits( GPIOA, GPIO_Pin_1); // 熄灭LED
EXTI_ClearITPendingBit(EXTI_Line0); // 【必须】清除中断标志位
}
}
(1) 中断分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
- 整个工程只配置一次
- 决定中断优先级分配规则:2 位抢占、2 位子优先级
(2) 主循环空跑
while(1){}
- 不用轮询检测按键
- 效率极高,CPU 资源完全释放
(3) 中断服务函数
void EXTI0_IRQHandler()
- 函数名固定,不能自定义
- 中断触发后自动执行
- 必须清除中断标志位,否则会反复进入中断
3.shake.c 外部中断初始化文件
#include "shake.h"
// 包含STM32标准库
#include "stm32f10x.h"
/**
* @brief 按键/震动传感器外部中断初始化
* @param 无
* @retval 无
*/
void Shake_Init(void)
{
// 定义结构体变量
GPIO_InitTypeDef GPIO_Initstruct; // GPIO配置结构体
EXTI_InitTypeDef EXTIInitstruct; // 外部中断配置结构体
NVIC_InitTypeDef NVICInitstruct; // 中断优先级配置结构体
// 【重要】开启GPIOA时钟 + AFIO复用功能时钟
// 使用外部中断必须开启AFIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
// 配置PA0为上拉输入模式(按键/震动传感器输入)
GPIO_Initstruct.GPIO_Pin = GPIO_Pin_0;
GPIO_Initstruct.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入,默认高电平
GPIO_Init(GPIOA, &GPIO_Initstruct); // 初始化GPIO
// 【重映射】将PA0映射到EXTI0中断线
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0);
// 配置EXTI外部中断
EXTIInitstruct.EXTI_Line = EXTI_Line0; // 中断线0
EXTIInitstruct.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式
EXTIInitstruct.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
EXTIInitstruct.EXTI_LineCmd = ENABLE; // 使能中断线
EXTI_Init(&EXTIInitstruct); // 应用配置
// 配置NVIC中断控制器
NVICInitstruct.NVIC_IRQChannel = EXTI0_IRQn; // 选择EXTI0中断通道
NVICInitstruct.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级0(最高)
NVICInitstruct.NVIC_IRQChannelSubPriority = 0; // 子优先级0
NVICInitstruct.NVIC_IRQChannelCmd = ENABLE; // 使能中断通道
NVIC_Init(&NVICInitstruct); // 应用配置
}
(1) 必须开启 AFIO 时钟
RCC_APB2PeriphClockCmd(... | RCC_APB2Periph_AFIO, ENABLE);
使用外部中断,必须开启 AFIO,否则中断不生效!
(2) GPIO 上拉输入
GPIO_Mode_IPU
- 默认高电平
- 按键按下 / 震动触发 → 变为低电平
(3) 中断线重映射
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
将 PA0 与 EXTI0 绑定。
(4) 下降沿触发
EXTI_Trigger_Falling
高电平 → 低电平瞬间触发中断。
(5) NVIC 开启中断通道
不配置 NVIC,中断永远无法响应。
四、必记的 3 个 "坑"
- 千万别忘开 AFIO 时钟!------ 这是最容易犯的错,没开这个,不管中断配置得多对,都不会触发(相当于门铃没装电池);
- 中断分组只配置一次!------ 咱们代码里放在 main 函数最开头,整个工程只写一次,多写会导致中断紊乱(相当于给门铃定了多个规则,单片机 confusion 了);
- 中断服务函数名不能乱改!------
EXTI0_IRQHandler()是固定名字,改了单片机就找不到 "处理门铃的地方",中断触发了也没反应。