研二小白硬件学习Day4&5——外部中断实验(资源:正点原子STM32F407探索者开发板)
开头说明:
博主因误操作原因导致板子烧坏送回维修,更新实验暂时无下载验证流程。代码部分均正确,这都是我之前做过的,开这个博客也是想系统整理一下,方便以后自己做项目复习用。
一、基本介绍
1.中断定义
打断CPU执行正常的程序,转而处理紧急程序,然后返回原暂停的程序继续运行,就叫中断

2.中断的作用和意义

3.STM32 GPIO外部中断简图

二、NVIC
1.NVIC基本概念
NVIC即嵌套向量中断控制器,全称为 Nested vectored interrupt controller,属于内核器件。支持256个中断(16内核 + 240外部),支持256个优先级,允许裁剪。STM32F407系列的中断及优先级如下:

补充说明:
中断向量表可以理解为一个“中断服务函数目录”或“电话本”。它是一块固定在内存中的表格,每个中断源(例如外部中断、定时器中断等)在表中都有一个对应的条目,里面存放着该中断的服务函数(ISR)在程序中的入口地址。
-
当 CPU 响应一个中断时,它会根据中断编号自动到这个“电话本”里查找对应的号码(即函数地址),然后跳转过去执行相应的处理程序。
-
这个表格以 4 字节(32 位)为单位对齐,因为每个地址正好占用 4 个字节。
-
中断向量表通常定义在启动文件(如
startup_stm32f407xx.s)中,由编译器在编译时自动填充。程序员只需编写中断服务函数(如EXTI0_IRQHandler),并将函数名与启动文件中的向量表对应起来即可。
简单来说:中断向量表就是一张“中断→函数地址”的映射表,让 CPU 知道每个中断发生时该去执行哪一段代码。


2.相关寄存器介绍(常用的)
| 寄存器分类 | 寄存器名称 | 位数 | 个数 | 功能说明 |
|---|---|---|---|---|
| 中断使能 | ISER | 32 | 8 | 写1使能对应中断,写0无效 |
| 中断除能 | ICER | 32 | 8 | 写1禁能对应中断,写0无效 |
| 中断挂起 | ISPR | 32 | 8 | 写1挂起中断(软件触发) |
| 中断清除挂起 | ICPR | 32 | 8 | 写1清除中断挂起标志 |
| 中断活动状态 | IABR | 32 | 8 | 只读,查询当前正在执行的中断 |
| 优先级分组 | AIRCR | 32 | 1 | 位[10:8]设置优先级分组 |
| 中断优先级 | IPR | 8 | 240 | 每8位对应一个中断,STM32只用高4位 |
3.工作原理

NVIC 是单片机的中断管家,负责管理所有中断请求,决定 CPU 先处理谁。
-
中断来了:外设(如按键、定时器)产生中断信号,全部送到 NVIC。
-
使能检查:NVIC 有个“开关列表”(ISER/ICER 寄存器),每个中断对应一个开关。只有开关打开的中断才会被考虑。
-
优先级排队:打开的中断会被分配一个优先级(数值越小越优先)。NVIC 通过 IPR 寄存器给每个中断设置优先级。
-
分组规则:优先级分为“抢占级”和“响应级”(通过 AIRCR 寄存器设置分组)。抢占级高的可以打断正在执行的中断;同级则按响应级排队。
-
CPU 执行:NVIC 选出当前优先级最高的中断,通知 CPU。CPU 暂停当前任务,跳转到对应的中断服务函数执行,执行完再回来继续原来的工作。
简单说,NVIC 就是中断的“调度中心”——谁该开,谁优先,谁先执行,都由它说了算。
4.STM32中断优先级基本概念
1,抢占优先级(pre):决定了这个中断能否打断另一个正在执行的中断。抢占优先级越高(数值越小),越能打断别人。
2,响应优先级(sub):当两个中断的抢占优先级相同时,响应优先级高的(数值小)先执行。它不能打断,只能排队。
3,自然优先级:中断向量表的优先级
4,抢占和响应都相同的情况下,自然优先级越高的,先执行
5,数值越小,表示优先级越高
打个比方:
-
抢占优先级就像VIP通道:VIP等级高的人可以直接插队(打断别人)
-
响应优先级就像排队号:同等级的人按号排队,号小的先服务
5.STM32中断优先级分组

5.1 补充说明(帮助理解)
拆解第三列的含义(以分组2为例):
[7:6] : [5:4] 的意思是:
-
第7位到第6位(共2位)→ 分配给抢占优先级
-
第5位到第4位(共2位)→ 分配给响应优先级
-
总共用了4位(bit7~bit4),这就是为什么STM32只使用高4位
其实不需要记住位分配,只需要知道每种分组下,抢占和响应能填什么数值:
| 分组 | 抢占位数 | 响应位数 | 抢占取值范围 | 响应取值范围 | 总优先级级数 |
|---|---|---|---|---|---|
| 0 | 0位 | 4位 | 只有0 | 0~15 | 16级 |
| 1 | 1位 | 3位 | 0~1 | 0~7 | 16级 |
| 2 | 2位 | 2位 | 0~3 | 0~3 | 16级 |
| 3 | 3位 | 1位 | 0~7 | 0~1 | 16级 |
| 4 | 4位 | 0位 | 0~15 | 只有0 | 16级 |
规律:无论怎么分,总共有16级优先级(2的4次方)。
理解口诀
-
分组越大,抢占越强:分组4可以分出16种抢占等级(0~15),但没有了响应优先级
-
分组越小,响应越细:分组0只有一种抢占等级,但响应可以分16档(0~15)
-
数值越小,优先级越高:0是最高的,15是最低的
5.2 实际编程中怎么用?
大多数初学者直接使用分组2(2位抢占 + 2位响应)就够了:
// 1. 设置分组(通常在HAL_Init()中已经做了,一般不用再写)
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
// 2. 设置具体中断的优先级
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 2); // 抢占1,响应2
意思是:
-
如果另一个中断的抢占优先级是0,它可以打断这个中断
-
如果另一个中断也是抢占1,那就看响应,响应小的先执行
此外,分组0对应111,分组4对应011,好像数字越大分组越小。这是因为AIRCR寄存器的位[10:8]是取反存储的,实际使用中直接调用HAL库的宏(如NVIC_PRIORITYGROUP_2)即可,不用手动写二进制
6. STM32 NVIC使用

6.1 HAL_NVIC_SetPriorityGrouping 函数
void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);
-
函数描述:设置中断优先级分组,决定抢占优先级和子优先级的位数分配。
-
形参:
-
PriorityGroup:优先级分组号,可选范围:-
NVIC_PRIORITYGROUP_0:0 位抢占,4 位子优先级 -
NVIC_PRIORITYGROUP_1:1 位抢占,3 位子优先级 -
NVIC_PRIORITYGROUP_2:2 位抢占,2 位子优先级 -
NVIC_PRIORITYGROUP_3:3 位抢占,1 位子优先级 -
NVIC_PRIORITYGROUP_4:4 位抢占,0 位子优先级
-
-
-
返回值:无
-
注意事项:
-
该函数通常在系统初始化时调用一次(HAL_Init 内部已调用),后续不建议修改,否则可能导致已配置的中断优先级错乱。
-
6.2 HAL_NVIC_SetPriority 函数
void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);
-
函数描述:设置指定中断的抢占优先级和子优先级。
-
形参:
-
IRQn:中断号,定义在stm32f4xx.h中(如EXTI0_IRQn、USART1_IRQn等)。 -
PreemptPriority:抢占优先级,实际有效值范围由当前优先级分组决定(例如分组 2 时取值范围 0~3,分组 4 时 0~15)。 -
SubPriority:子优先级,实际有效范围同样取决于分组。
-
-
返回值:无
-
注意事项:
-
必须在设置优先级分组后调用,且传入的优先级值应在分组允许范围内,超出范围时 HAL 库会自动屏蔽高位,但建议按需合理配置。
-
6.3 HAL_NVIC_EnableIRQ 函数
void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);
-
函数描述:使能指定中断。
-
形参:
-
IRQn:中断号,同HAL_NVIC_SetPriority。
-
-
返回值:无
-
注意事项:无
6.4 HAL_NVIC_DisableIRQ 函数
void HAL_NVIC_DisableIRQ(IRQn_Type IRQn);
-
函数描述:禁能(失能)指定中断。
-
形参:
-
IRQn:中断号,同HAL_NVIC_EnableIRQ。
-
-
返回值:无
-
注意事项:无
6.5 HAL_NVIC_SystemReset 函数
void HAL_NVIC_SystemReset(void);
-
函数描述:软件复位整个系统(包括内核和外设)。
-
形参:无
-
返回值:无
-
注意事项:
-
调用该函数后,系统将立即复位,不会返回。
-
三、EXTI
1.基本介绍
EXTI(外部中断/事件控制器)就是单片机用来检测外部信号变化并产生相应动作的模块。它支持 23 条 EXTI 线(STM32F4系列),其中 0~15 号线 对应 16 个 GPIO 引脚(如 PA0 映射到 EXTI0,PB0 也映射到 EXTI0,但同一时间只能选一个),另外几条用于内部外设(如 PVD、RTC 等)。
EXTI 对外部信号的处理分为两种模式:
-
中断模式:信号变化 → 触发 CPU 中断 → 执行中断服务函数(需要软件干预)
-
事件模式:信号变化 → 产生一个硬件脉冲 → 直接触发其他外设(如 ADC、DMA、定时器),无需 CPU 干预(速度更快,适合硬件联动)
2.相关寄存器
| 寄存器 | 全称 | 作用 |
|---|---|---|
| RTSR | 上升沿触发选择寄存器 | 写 1 使能该线的上升沿触发 |
| FTSR | 下降沿触发选择寄存器 | 写 1 使能该线的下降沿触发 |
| SWIER | 软件中断事件寄存器 | 写 1 可软件模拟触发该线(用于调试) |
| IMR | 中断屏蔽寄存器 | 写 1 允许该线产生中断 |
| EMR | 事件屏蔽寄存器 | 写 1 允许该线产生事件 |
| PR | 挂起寄存器 | 当触发条件满足时硬件置 1,写 1 清除 |
3.工作原理

1. 中断路径(信号 → CPU)
外部信号(比如按键按下)进入 EXTI 后,经历以下步骤:
输入信号 → 边沿检测(RTSR/FTSR决定触发边沿)
→ 或门(SWIER也可软件触发)
→ 与门(由IMR控制是否允许中断)
→ PR挂起寄存器置1
→ NVIC通知CPU执行中断函数
关键点:只有 IMR 对应位为 1,中断请求才能最终送到 NVIC。
2. 事件路径(信号 → 硬件外设)
输入信号 → 边沿检测(同上)
→ 或门
→ 与门(由EMR控制是否允许事件)
→ 脉冲发生器 → 输出脉冲给其他外设(如触发ADC采样)
关键点:事件模式不经过 NVIC,直接产生硬件脉冲。EMR 对应位为 1 时才允许事件输出。
重点理解:中断和事件共享边沿检测和软件触发,但屏蔽寄存器不同(IMR/EMR)。挂起寄存器 PR 在中断路径中用于记录触发状态,写 1 清除中断标志。
总结:EXTI 就是检测外部信号变化,根据你的配置(触发边沿、屏蔽设置)决定是通知 CPU 还是直接触发其他硬件。记住四个关键寄存器:RTSR/FTSR 决定怎么触发,IMR/EMR 决定触发后干什么。
四、EXTI和IO映射关系
1.SYSCFG简介

2.EXTI与IO对应关系
SYSCFG_EXTICR1的 EXTI0[3:0]位控制(F4/F7/H7)

Px0映射到EXTI0
Px1映射到EXTI1
...
Px14映射到EXTI14
Px15映射到EXTI15
五、如何使用中断

1.STM32 EXTI的配置步骤(GPIO外部中断)

2.STM32 EXTI的HAL库配置步骤(GPIO外部中断)

六、硬件原理图

程序设计按键为按下触发中断,KEY0、KEY1和KEY2是低电平有效,所以我们要选择下降沿触发检测,而KEY_UP是高电平有效,所以我们要选择上升沿触发。(上下拉此处不再赘述)
七、实验验证
1.exti.h
#ifndef __EXTI_H // 防止头文件重复包含:如果未定义__EXTI_H,则编译下面的内容
#define __EXTI_H // 定义__EXTI_H宏,标记本文件已被包含
#include "./SYSTEM/sys/sys.h" // 包含系统基础头文件,提供HAL库依赖、数据类型等
/******************************************************************************************/
/* 引脚、中断编号及中断服务函数定义 */
/* 这部分将硬件连接信息(端口、引脚)与软件中断配置(中断号、函数名)关联起来, */
/* 方便在代码中统一管理,提高可移植性。 */
/******************************************************************************************/
// ==================== KEY0 外部中断配置 ====================
#define KEY0_INT_GPIO_PORT GPIOE // KEY0 使用的 GPIO 端口:GPIOE
#define KEY0_INT_GPIO_PIN GPIO_PIN_4 // KEY0 使用的引脚编号:4
#define KEY0_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0) // 使能 GPIOE 时钟的宏
#define KEY0_INT_IRQn EXTI4_IRQn // KEY0 对应的中断号:EXTI4(因为 PE4 映射到 EXTI4)
#define KEY0_INT_IRQHandler EXTI4_IRQHandler // KEY0 中断服务函数的标准名称(与启动文件一致)
// ==================== KEY1 外部中断配置 ====================
#define KEY1_INT_GPIO_PORT GPIOE // KEY1 端口:GPIOE
#define KEY1_INT_GPIO_PIN GPIO_PIN_3 // KEY1 引脚:3
#define KEY1_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0) // 使能 GPIOE 时钟
#define KEY1_INT_IRQn EXTI3_IRQn // KEY1 中断号:EXTI3(PE3 映射到 EXTI3)
#define KEY1_INT_IRQHandler EXTI3_IRQHandler // KEY1 中断服务函数名
// ==================== KEY2 外部中断配置 ====================
#define KEY2_INT_GPIO_PORT GPIOE // KEY2 端口:GPIOE
#define KEY2_INT_GPIO_PIN GPIO_PIN_2 // KEY2 引脚:2
#define KEY2_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0) // 使能 GPIOE 时钟
#define KEY2_INT_IRQn EXTI2_IRQn // KEY2 中断号:EXTI2(PE2 映射到 EXTI2)
#define KEY2_INT_IRQHandler EXTI2_IRQHandler // KEY2 中断服务函数名
// ==================== WKUP 外部中断配置 ====================
#define WKUP_INT_GPIO_PORT GPIOA // WKUP 端口:GPIOA
#define WKUP_INT_GPIO_PIN GPIO_PIN_0 // WKUP 引脚:0
#define WKUP_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) // 使能 GPIOA 时钟
#define WKUP_INT_IRQn EXTI0_IRQn // WKUP 中断号:EXTI0(PA0 映射到 EXTI0)
#define WKUP_INT_IRQHandler EXTI0_IRQHandler // WKUP 中断服务函数名
/******************************************************************************************/
/* 外部接口函数声明 */
/******************************************************************************************/
void extix_init(void); // 外部中断初始化函数,需在源文件(exti.c)中实现
#endif // __EXTI_H 结束
补充说明:
-
中断号宏
KEYx_INT_IRQn
指定该引脚对应的中断通道号,用于 NVIC 配置(如HAL_NVIC_EnableIRQ)。STM32F407 的 EXTI 中断线 0~4 有独立的中断号,而 5~9 共用EXTI9_5_IRQn,10~15 共用EXTI15_10_IRQn。本实验的按键恰好使用独立的 EXTI 线,因此每个有自己独立的中断号。 -
中断服务函数名宏
KEYx_INT_IRQHandler
将自定义的宏名映射到标准中断服务函数名。这样在编写exti.c时,可以直接使用KEY0_INT_IRQHandler作为函数名,而实际编译时会被替换为EXTI4_IRQHandler,保证与启动文件一致。这种方式既保持了代码可读性,又避免了函数名错误。 -
函数声明
extix_init(void)是在exti.c中实现的初始化函数,供主程序调用。它负责将相关引脚配置为外部中断模式,并设置中断优先级和使能。
2.exti.c
// 包含必要的头文件
#include "./SYSTEM/sys/sys.h" // 系统核心头文件(时钟、寄存器等)
#include "./SYSTEM/delay/delay.h" // 延时函数头文件
#include "./BSP/LED/led.h" // LED 驱动头文件(控制 LED)
#include "./BSP/BEEP/beep.h" // 蜂鸣器驱动头文件
#include "./BSP/KEY/key.h" // 按键驱动头文件(包含引脚定义、读取宏等)
#include "./BSP/EXTI/exti.h" // 外部中断头文件(包含中断引脚、中断号、服务函数名的宏定义)
/******************************************************************************************/
/* 中断服务函数 */
/* 这些函数是硬件中断的真正入口,函数名必须与启动文件(startup_stm32f407xx.s)中定义的 */
/* 中断向量表名称一致。这里通过 exti.h 中的宏定义,将自定义名称映射为标准名称, */
/* 例如 KEY0_INT_IRQHandler 实际上被替换为 EXTI4_IRQHandler。 */
/******************************************************************************************/
/**
* @brief KEY0 外部中断服务程序(实际对应 EXTI4_IRQHandler)
* @param 无
* @retval 无
*/
void KEY0_INT_IRQHandler(void)
{
/* 调用 HAL 库的外部中断公共处理函数,传入产生中断的引脚号。
该函数会检查中断标志位、清除标志,并调用回调函数 HAL_GPIO_EXTI_Callback。
这是 HAL 库推荐的标准处理方式,将通用的中断处理(清标志)与用户逻辑分离。 */
HAL_GPIO_EXTI_IRQHandler(KEY0_INT_GPIO_PIN);
/* 再次清除中断标志位,作为双重保险。
注意:HAL_GPIO_EXTI_IRQHandler 内部已经清除了标志位,通常不需要这行代码。
此处保留是为了避免按键抖动可能导致的中断误触发(极少数情况)。
对于初学者,可以认为 HAL 库已经帮我们做好了清标志的工作。 */
__HAL_GPIO_EXTI_CLEAR_IT(KEY0_INT_GPIO_PIN);
}
/**
* @brief KEY1 外部中断服务程序(实际对应 EXTI3_IRQHandler)
*/
void KEY1_INT_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(KEY1_INT_GPIO_PIN);
__HAL_GPIO_EXTI_CLEAR_IT(KEY1_INT_GPIO_PIN);
}
/**
* @brief KEY2 外部中断服务程序(实际对应 EXTI2_IRQHandler)
*/
void KEY2_INT_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(KEY2_INT_GPIO_PIN);
__HAL_GPIO_EXTI_CLEAR_IT(KEY2_INT_GPIO_PIN);
}
/**
* @brief WK_UP 外部中断服务程序(实际对应 EXTI0_IRQHandler)
*/
void WKUP_INT_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(WKUP_INT_GPIO_PIN);
__HAL_GPIO_EXTI_CLEAR_IT(WKUP_INT_GPIO_PIN);
}
/******************************************************************************************/
/* 中断回调函数 */
/* 所有外部中断在清除标志后,都会统一调用此函数。你需要根据 GPIO_Pin 参数判断是哪个引脚 */
/* 触发了中断,并编写具体的处理逻辑。这实现了中断处理与业务逻辑的分离。 */
/******************************************************************************************/
/**
* @brief 外部中断回调函数(由 HAL_GPIO_EXTI_IRQHandler 内部调用)
* @param GPIO_Pin: 触发中断的引脚号(如 KEY0_INT_GPIO_PIN)
* @retval 无
* @note 此函数在中断上下文中执行,代码应尽量简短快速,避免长时间延时或复杂操作。
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
/* 延时 20ms 进行软件消抖。
注意:在中断服务函数中直接调用 delay_ms 会导致 CPU 阻塞 20ms,
这会严重影响系统的实时响应能力,尤其在有多个中断时可能导致其他中断丢失。
这里仅作为教学示例,展示消抖的概念。实际产品中应使用定时器或状态机来处理消抖。 */
delay_ms(20);
/* 根据触发中断的引脚号,执行对应的操作 */
switch (GPIO_Pin)
{
case KEY0_INT_GPIO_PIN: // 如果是 KEY0 中断
if (KEY0 == 0) // 再次读取引脚电平,确认按键确实按下(低电平有效)
{
LED0_TOGGLE(); // 翻转 LED0(红灯)状态
}
break;
case KEY1_INT_GPIO_PIN: // KEY1 中断
if (KEY1 == 0)
{
LED1_TOGGLE(); // 翻转 LED1(绿灯)
}
break;
case KEY2_INT_GPIO_PIN: // KEY2 中断
if (KEY2 == 0)
{
LED0_TOGGLE(); // 翻转 LED0
LED1_TOGGLE(); // 翻转 LED1
}
break;
case WKUP_INT_GPIO_PIN: // WKUP 中断
if (WK_UP == 1) // 读取引脚,高电平表示按下
{
BEEP_TOGGLE(); // 翻转蜂鸣器状态
}
break;
default: // 其他引脚(不会发生)
break;
}
}
/******************************************************************************************/
/* 外部中断初始化函数 */
/* 配置 GPIO 为中断模式,设置触发边沿,并配置 NVIC 中断优先级和使能。 */
/******************************************************************************************/
/**
* @brief 外部中断初始化程序
* @param 无
* @retval 无
*/
void extix_init(void)
{
GPIO_InitTypeDef gpio_init_struct; // 定义 GPIO 初始化结构体变量
/* 调用 key_init() 将按键引脚初始化为普通输入模式(上拉/下拉)。
这里先初始化为普通输入,然后再重新配置为中断模式,是为了复用 key_init 中的引脚定义。
实际上,我们可以直接在下面配置中断模式,无需先调用 key_init。
但这样做的好处是:如果以后又想用查询方式读取按键,key_init 已经配置好了基本输入。 */
key_init();
/* --- 配置 KEY0 为下降沿触发的外部中断 --- */
gpio_init_struct.Pin = KEY0_INT_GPIO_PIN; // 引脚号:PE4
gpio_init_struct.Mode = GPIO_MODE_IT_FALLING; // 模式:下降沿触发中断(按键按下时电平从高变低)
gpio_init_struct.Pull = GPIO_PULLUP; // 上拉电阻(空闲时高电平,按下拉低)
HAL_GPIO_Init(KEY0_INT_GPIO_PORT, &gpio_init_struct); // 初始化 GPIO,内部自动配置 SYSCFG 和 EXTI
/* --- 配置 KEY1 为下降沿触发中断 --- */
gpio_init_struct.Pin = KEY1_INT_GPIO_PIN; // PE3
HAL_GPIO_Init(KEY1_INT_GPIO_PORT, &gpio_init_struct); // 复用之前的结构体参数
/* --- 配置 KEY2 为下降沿触发中断 --- */
gpio_init_struct.Pin = KEY2_INT_GPIO_PIN; // PE2
HAL_GPIO_Init(KEY2_INT_GPIO_PORT, &gpio_init_struct); // 注意这里使用了 KEY2_INT_GPIO_PORT,与前面一致
/* --- 配置 WKUP 为上升沿触发中断 --- */
gpio_init_struct.Pin = WKUP_INT_GPIO_PIN; // PA0
gpio_init_struct.Mode = GPIO_MODE_IT_RISING; // 模式:上升沿触发(按键按下时电平从低变高)
gpio_init_struct.Pull = GPIO_PULLDOWN; // 下拉电阻(空闲时低电平,按下拉高)
HAL_GPIO_Init(WKUP_INT_GPIO_PORT, &gpio_init_struct); // 初始化
/* --- 配置 NVIC 中断优先级并使能中断 --- */
/* 注意:这里的中断号(如 KEY0_INT_IRQn)在 exti.h 中已定义为标准中断号(EXTI4_IRQn 等) */
HAL_NVIC_SetPriority(KEY0_INT_IRQn, 0, 2); // 设置抢占优先级为0,子优先级为2
HAL_NVIC_EnableIRQ(KEY0_INT_IRQn); // 使能 EXTI4 中断
HAL_NVIC_SetPriority(KEY1_INT_IRQn, 1, 2); // KEY1 抢占优先级1,子优先级2
HAL_NVIC_EnableIRQ(KEY1_INT_IRQn); // 使能 EXTI3 中断
HAL_NVIC_SetPriority(KEY2_INT_IRQn, 2, 2); // KEY2 抢占优先级2,子优先级2
HAL_NVIC_EnableIRQ(KEY2_INT_IRQn); // 使能 EXTI2 中断
HAL_NVIC_SetPriority(WKUP_INT_IRQn, 3, 2); // WKUP 抢占优先级3,子优先级2
HAL_NVIC_EnableIRQ(WKUP_INT_IRQn); // 使能 EXTI0 中断
}
补充说明:
-
双重清中断:每个中断服务函数中调用了
HAL_GPIO_EXTI_IRQHandler后,又手动调用__HAL_GPIO_EXTI_CLEAR_IT清除标志。实际上 HAL 库函数内部已经清除了标志,第二次清除是多余的。保留它是为了强调“必须清除中断标志”这一概念,初学者可以省略第二次清除。 -
回调函数中的延时:在回调函数中直接使用
delay_ms(20)进行消抖,虽然简单,但会导致 CPU 在中断中阻塞 20ms。对于实时性要求高的系统,这是不可取的。这里仅作为演示,实际开发应避免在中断中长时间延时。 -
初始化顺序:
extix_init先调用key_init()将引脚初始化为普通输入,然后重新配置为中断模式。这可能导致两次写入寄存器,但不会出错。也可以直接在extix_init中完成所有配置,无需调用key_init。 -
NVIC 优先级:
HAL_NVIC_SetPriority的第二个参数是抢占优先级,第三个是子优先级。这里四个按键分配了不同的抢占优先级(0~3),可以观察中断嵌套的效果。如果不需要嵌套,可将所有中断设为相同抢占优先级。 -
时钟使能:
HAL_GPIO_Init内部会自动使能 SYSCFG 时钟,但 GPIO 时钟需要手动使能。key_init中已经使能了 GPIOE 和 GPIOA 的时钟,所以这里不需要重复使能。
3.main.c
// 包含必要的头文件
#include "./SYSTEM/sys/sys.h" // 系统核心头文件:提供时钟配置、中断分组等基础函数
#include "./SYSTEM/usart/usart.h" // 串口头文件:提供串口初始化及发送接收函数(本实验用于调试)
#include "./SYSTEM/delay/delay.h" // 延时头文件:提供毫秒级和微秒级延时函数
#include "./BSP/LED/led.h" // LED 驱动头文件:包含 LED 初始化及控制宏
#include "./BSP/BEEP/beep.h" // 蜂鸣器驱动头文件:包含蜂鸣器初始化及控制宏
#include "./BSP/EXTI/exti.h" // 外部中断头文件:包含外部中断初始化函数
/**
* @brief 主函数:程序入口
* @param 无
* @retval 0 (实际不会返回)
*/
int main(void)
{
// 1. 初始化 HAL 库
// HAL_Init() 是 STM32 HAL 库的初始化函数,负责:
// - 设置 SysTick 定时器(用于提供 HAL 库内部的时基,如 HAL_Delay)
// - 初始化全局中断优先级分组(默认设置为 NVIC_PRIORITYGROUP_4,即 4 位抢占优先级,0 位子优先级)
// - 初始化底层硬件(如 Flash 接口)
HAL_Init();
// 2. 配置系统时钟为 168MHz
// sys_stm32_clock_init 是自定义的时钟初始化函数(来自 sys.c)
// 参数 (336, 8, 2, 7) 的含义:将外部 8MHz 晶振通过 PLL 倍频分频得到 168MHz 系统时钟
// 具体计算:8MHz / 8 = 1MHz → 1MHz × 336 = 336MHz → 336MHz / 2 = 168MHz
sys_stm32_clock_init(336, 8, 2, 7);
// 3. 初始化延时函数
// delay_init() 需要传入系统时钟频率(单位 MHz),用于精确计算延时
// 参数 168 表示系统时钟为 168MHz,这样后续 delay_ms() 和 delay_us() 才能正常工作
delay_init(168);
// 4. 初始化串口,波特率 115200
// 虽然本实验主循环中没有使用串口,但保留串口初始化可以方便调试(如 printf 输出)
usart_init(115200);
// 5. 初始化板载外设
led_init(); // 初始化 LED(配置 GPIO 为推挽输出,默认关闭)
beep_init(); // 初始化蜂鸣器(配置 GPIO 为推挽输出,默认关闭)
extix_init(); // 初始化外部中断(配置按键 GPIO 为中断模式,设置触发边沿和 NVIC)
// 6. 点亮红色 LED(LED0),作为程序运行指示
LED0(0); // 参数 0 表示输出低电平(点亮 LED,假设低电平有效)
// 7. 主循环:程序无限循环执行
while(1)
{
// 每隔 1000ms(1秒)循环一次
// 注意:外部中断的处理并不在主循环中,而是在中断服务函数中自动执行
// 当按键按下时,CPU 会暂停主循环,跳转到对应的中断服务函数(如 EXTI4_IRQHandler)
// 执行完中断服务函数后,再返回主循环继续运行
delay_ms(1000);
}
}
补充说明:
-
外部中断的处理方式
本实验采用中断方式检测按键,而不是主循环中的轮询方式。这意味着:-
按键按下时,CPU 会立即暂停主循环,跳转到对应的中断服务函数(在
exti.c中定义)。 -
中断服务函数中调用了 HAL 库的公共处理函数,最终执行用户回调函数
HAL_GPIO_EXTI_Callback,实现 LED 或蜂鸣器的翻转。 -
中断处理完成后,CPU 返回主循环继续运行。因此主循环可以只做简单的延时,而不必反复查询按键状态。
-
-
主循环中的延时
delay_ms(1000)的作用是让主循环每 1 秒循环一次,避免 CPU 空转浪费资源。即使没有这个延时,程序也会不断循环,但加入延时可以降低 CPU 占用率,也为可能的低功耗模式创造条件。在实际嵌入式系统中,如果没有其他任务,通常会进入低功耗模式或简单延时。 -
串口初始化的作用
虽然本实验未使用串口输出,但保留串口初始化是一个好习惯,便于后续调试时添加printf语句,打印按键状态或其他调试信息。 -
LED0(0) 的作用
在初始化完成后立即点亮红灯,可以作为程序正常启动的视觉提示。如果程序运行后红灯不亮,说明初始化可能有问题。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)