开头说明:

博主因误操作原因导致板子烧坏送回维修,更新实验暂时无下载验证流程。代码部分均正确,这都是我之前做过的,开这个博客也是想系统整理一下,方便以后自己做项目复习用。

一、基本介绍

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 先处理谁。

  1. 中断来了:外设(如按键、定时器)产生中断信号,全部送到 NVIC。

  2. 使能检查:NVIC 有个“开关列表”(ISER/ICER 寄存器),每个中断对应一个开关。只有开关打开的中断才会被考虑。

  3. 优先级排队:打开的中断会被分配一个优先级(数值越小越优先)。NVIC 通过 IPR 寄存器给每个中断设置优先级。

  4. 分组规则:优先级分为“抢占级”和“响应级”(通过 AIRCR 寄存器设置分组)。抢占级高的可以打断正在执行的中断;同级则按响应级排队。

  5. 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次方)。

理解口诀

  1. 分组越大,抢占越强:分组4可以分出16种抢占等级(0~15),但没有了响应优先级

  2. 分组越小,响应越细:分组0只有一种抢占等级,但响应可以分16档(0~15)

  3. 数值越小,优先级越高: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_IRQnUSART1_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 结束

补充说明:

  1. 中断号宏 KEYx_INT_IRQn
    指定该引脚对应的中断通道号,用于 NVIC 配置(如 HAL_NVIC_EnableIRQ)。STM32F407 的 EXTI 中断线 0~4 有独立的中断号,而 5~9 共用 EXTI9_5_IRQn,10~15 共用 EXTI15_10_IRQn。本实验的按键恰好使用独立的 EXTI 线,因此每个有自己独立的中断号。

  2. 中断服务函数名宏 KEYx_INT_IRQHandler
    将自定义的宏名映射到标准中断服务函数名。这样在编写 exti.c 时,可以直接使用 KEY0_INT_IRQHandler 作为函数名,而实际编译时会被替换为 EXTI4_IRQHandler,保证与启动文件一致。这种方式既保持了代码可读性,又避免了函数名错误。

  3. 函数声明
    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 中断
}

补充说明:

  1. 双重清中断:每个中断服务函数中调用了 HAL_GPIO_EXTI_IRQHandler 后,又手动调用 __HAL_GPIO_EXTI_CLEAR_IT 清除标志。实际上 HAL 库函数内部已经清除了标志,第二次清除是多余的。保留它是为了强调“必须清除中断标志”这一概念,初学者可以省略第二次清除。

  2. 回调函数中的延时:在回调函数中直接使用 delay_ms(20) 进行消抖,虽然简单,但会导致 CPU 在中断中阻塞 20ms。对于实时性要求高的系统,这是不可取的。这里仅作为演示,实际开发应避免在中断中长时间延时。

  3. 初始化顺序extix_init 先调用 key_init() 将引脚初始化为普通输入,然后重新配置为中断模式。这可能导致两次写入寄存器,但不会出错。也可以直接在 extix_init 中完成所有配置,无需调用 key_init

  4. NVIC 优先级HAL_NVIC_SetPriority 的第二个参数是抢占优先级,第三个是子优先级。这里四个按键分配了不同的抢占优先级(0~3),可以观察中断嵌套的效果。如果不需要嵌套,可将所有中断设为相同抢占优先级。

  5. 时钟使能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);
    }
}

补充说明:

  1. 外部中断的处理方式
    本实验采用中断方式检测按键,而不是主循环中的轮询方式。这意味着:

    • 按键按下时,CPU 会立即暂停主循环,跳转到对应的中断服务函数(在 exti.c 中定义)。

    • 中断服务函数中调用了 HAL 库的公共处理函数,最终执行用户回调函数 HAL_GPIO_EXTI_Callback,实现 LED 或蜂鸣器的翻转。

    • 中断处理完成后,CPU 返回主循环继续运行。因此主循环可以只做简单的延时,而不必反复查询按键状态。

  2. 主循环中的延时
    delay_ms(1000) 的作用是让主循环每 1 秒循环一次,避免 CPU 空转浪费资源。即使没有这个延时,程序也会不断循环,但加入延时可以降低 CPU 占用率,也为可能的低功耗模式创造条件。在实际嵌入式系统中,如果没有其他任务,通常会进入低功耗模式或简单延时。

  3. 串口初始化的作用
    虽然本实验未使用串口输出,但保留串口初始化是一个好习惯,便于后续调试时添加 printf 语句,打印按键状态或其他调试信息。

  4. LED0(0) 的作用
    在初始化完成后立即点亮红灯,可以作为程序正常启动的视觉提示。如果程序运行后红灯不亮,说明初始化可能有问题。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐