一、实验目的及要求
1、熟悉STM32外部中断及相关库函数。
2、掌握中断服务程序,实现中断服务。
二、实验内容及原理
1 . NVIC 和 EXTI 简介
1.1 NVIC 简介

什么是 NVIC?NVIC 即嵌套向量中断控制器,全称 Nested vectored interrupt controller。它是内核的器件,所以它的更多描述可以看内核有关的资料《Cortex-M3 权威指南》。M3 内核都是支持 256 个中断,其中包含了 5 个系统中断和 240 个外部中断,并且具有 256 级的可编程中断设置。然而芯片厂商一般不会把内核的这些资源全部用完,如 STM32F103ZET6 的系统中断有 10 个,外部中断有 60 个。下面我们看看系统中断部分:
表 5.1 中断向量表-系统中断部分
STM32F103的中断向量表在 STM32F103xx.h 文件中被定义。
NVIC 寄存器
NVIC 相关的寄存器定义了可以在 core_cm3.h 文件中找到。我们直接通过程序的定义来分 析 NVIC 相关的寄存器,其定义如下:

/* 中断使能寄存器 */
/* 中断清除使能寄存器 */ /* 中断使能挂起寄存器 */ /* 中断解挂寄存器 */
/* 中断有效位寄存器 */
/* 中断优先级寄存器( 8Bit 位宽) */ /* 软件触发中断寄存器 */
STM32F103 的中断在这些寄存器的控制下有序的执行的。只有了解这些中断寄存器,才能方便的使用 STM32F103 的中断。下面重点介绍这几个寄存器:
ISER[8]:ISER 全称是:Interrupt Set Enable Registers,这是一个中断使能寄存器组。上面说了 CM3 内核支持 256 个中断,这里用8 个 32 位寄存器来控制,每个位控制一个中断。但是 STM32F103 的可屏蔽中断最多只有60 个,所以对我们来说,有用的就是两个(ISER[0]和ISER[1]), 总共可以表示 64 个中断。而 STM32F103 只用了其中的 60 个。ISER[0]的 bit0~31 分别对应中 断 0~31;ISER[1]的 bit0~27 对应中断 32~59,这样总共 60 个中断就可以分别对应上了。你要使 能某个中断,必须设置相应的 ISER 位为 1,使该中断被使能(这里仅仅是使能,还要配合中断 分组、屏蔽、IO 口映射等设置才算是一个完整的中断设置)。具体每一位对应哪个中断,请参考 stm32f103xe.h 里面的第 69 行。
ICER[8]:全称是:Interrupt Clear Enable Registers,是一个中断除能寄存器组。该寄存器组与 ISER的作用恰好相反,是用来清除某个中断的使能的。其对应位的功能,也和 ICER一样。这里要专门设置一个 ICER 来清除中断位,而不是向ISER 写 0 来清除,是因为NVIC的这些寄存器都是写 1 有效的,写 0 是无效的。具体为什么是这样子,可以查看《Cortex-M3权威指南》 第 125 页,NVIC 章节。
ISPR[8]:全称是:Interrupt Set Pending Registers,是一个中断使能挂起控制寄存器组。每个位对应的中断和 ISER 是一样的。通过置 1,可以将正在进行的中断挂起,而执行同级或更高级别的中断。写 0 是无效的。
ICPR[8]:全称是:Interrupt Clear Pending Registers,是一个中断解挂控制寄存器组。其作用与 ISPR 相反,对应位也和 ISER 是一样的。通过设置 1,可以将挂起的中断解挂。写 0 无效。
IABR[8]:全称是:Interrupt Active Bit Registers,是一个中断激活标志位寄存器组。对应位所代表的中断和 ISER 一样,如果为 1,则表示该位所对应的中断正在被执行。这是一个只读寄存器,通过它可以知道当前在执行的中断是哪一个。在中断执行完了由硬件自动清零。
IP [240]:全称是:Interrupt Priority Registers,是一个中断优先级控制的寄存器组。这个寄存器组相当重要!STM32F103 的中断分组与这个寄存器组密切相关。IP 寄存器组由240 个 8bit 的寄存器组成,每个可屏蔽中断占用 8bit,这样总共可以表示 240 个可屏蔽中断。而 STM32F103只用到了其中的 60 个。IP[59]~IP[0]分别对应中断 59~0。而每个可屏蔽中断占用的 8bit 并没有全部使用,而是只用了高 4 位。这 4 位,又分为抢占优先级和子优先级。抢占优先级在前,子优先级在后。而这两个优先级各占几个位又要根据 SCB->AIRCR 中的中断分组设置来决定。关于中断优先级控制的寄存器组我们下面详细讲解。
中断优先级
STM32 中的中断优先级可以分为:抢占式优先级和响应优先级,响应优先级也称子优先级,每个中断源都需要被指定这两种优先级。抢占式优先级和响应优先级的区别:
抢占优先级:抢占优先级高的中断可以打断正在执行的抢占优先级低的中断。
响应优先级:抢占优先级相同,响应优先级高的中断不能打断响应优先级低的中断。
还有一种情况就是当两个或者多个中断的抢占式优先级和响应优先级相同时,那么就遵循 自然优先级,看中断向量表的中断排序,数值越小,优先级越高。
在 NVIC 中由寄存器 NVIC_IPR0-NVIC_IPR59 共 60 个寄存器控制中断优先级,每个寄存 器的 8位,所以就有了240个宽度为8bit的中断优先级控制寄存器,原则上每个外部中断可配 置的优先级为 0~255,数值越小,优先级越高。但是实际上 M3 芯片为了精简设计,只使用了高 四位[7:4],低四位取零,这样以至于最多只有 5 级中断嵌套,即2^4=5。
对于 NVCI 的中断优先级分组:STM32F103 将中断分为 5 个组,组 0~4。该分组的设置是由 SCB->AIRCR 寄存器的 bit10~8 来定义的。具体的分配关系如表 5.1 所示:
表 5.2 AIRCR 中断分组设置表
|-------|----------------|-----------------|-------------------|
| 优先级分组 | AIRCR[10 :8] | bit[7 :4]分配情况 | 分配结果 |
| 0 | 111 | 0 :4 | 0 位抢占优先级,4 位响应优先级 |
| 1 | 110 | 1 :3 | 1 位抢占优先级,3 位响应优先级 |
| 2 | 101 | 2 :2 | 2 位抢占优先级,2 位响应优先级 |
| 3 | 100 | 3 :1 | 3 位抢占优先级,1 位响应优先级 |
| 4 | 011 | 4 :0 | 4 位抢占优先级,0 位响应优先级 |
通过表 5.2,我们就可以清楚的看到组 0~4 对应的配置关系,例如优先级分组设置为 3,那么此时所有的 60 个中断,每个中断的中断优先寄存器的高四位中的最高 3 位是抢占优先 级,低 1 位是响应优先级。每个中断,你可以设置抢占优先级为 0~7,响应优先级为 1 或 0。抢 占优先级的级别高于响应优先级。而数值越小所代表的优先级就越高。
结合实例说明一下:假定设置中断优先级分组为 2,然后设置中断 3(RTC_WKUP 中断)的抢占优先级为 2,响应优先级为 1。中断 6(外部中断 0)的抢占优先级为 3,响应优先级为 0。 中断 7(外部中断 1)的抢占优先级为 2,响应优先级为 0。那么这 3 个中断的优先级顺序为: 中断 7>中断 3>中断 6。
三、实验条件及设备要求
硬件环境:计算机一台;正点原子战舰STM32F103开发板
四、实验步骤
1. 硬件设计
( 1 )例程功能
通过外部中断的方式让开发板上的三个独立按键控制LED 灯:KEY_UP 控制LED0 翻转, KEY1 控制 LED1 翻转,KEY0 控制LED2 翻转。
(2) 硬件资源
1)LED 灯
蜂鸣器 -- PB8;LED0 - PB5;LED1 - PE5
2)独立按键
KEY0 - PE4;KEY1 - PE3;KEY2 - PE2;KEY_UP - PA0
(3) 原理图

图 5.3 独立按键与 STM32F1 连接原理图
独立按键硬件部分的连接原理的如图 5.2.1 。这里需要注意的是:KEY0 、KEY1 和 KEY2 设计为采样到按键另一端的低电平为有效,而 KEY_UP 则需要采样到高电平才为按键有效,并 且外部都没有上下拉电阻,所以需要在 STM32F103 内部设置上下拉以设置空闲电平。
3 . 程序设计
(1) EXTI 的 HAL 库驱动
前面讲解 HAL_GPIO_Init 函数的时候有提到过:HAL 库的 EXTI 外部中断的设置功能整合 到 HAL_GPIO_Init 函数里面,而不是单独独立一个文件。所以我们的外部中断的初始化函数也 是用 HAL_GPIO_Init 函数。
既然是要用到外部中断,所以我们的 GPIO 的模式要从下面的三个模式中选中一个:

KEY0、KEY1 和 KEY2 是低电平有效的,程序设计为按键按下触发中断,所以我们要选择下降沿触发检测,而 KEY_UP 是高电平有效的,那么就应该选择上升沿触发检测。
EXTI 外部中断配置步骤
1 )使能对应 GPIO 口时钟
本实验用到的 GPIO 和按键输入实验是一样的,因此 GPIO 时钟使能也是一样的,请参考 上一章代码。
2 )设置 GPIO 工作模式,触发条件,开启 AFIO 时钟,设 置 IO 口与中断线的映射关系
这些步骤 HAL 库全部封装在 HAL_GPIO_Init 函数里面,我们只需要设置好对应的参数, 再调用 HAL_GPIO_Init 函数即可完成配置。
3 )配置中断优先级( NVIC ), 并使能中断。
配置好 GPIO 模式以后,我们需要设置中断优先级和使能中断, 中断优先级我们使用 HAL_NVIC_SetPriority 函数设置,中断使能我们使用 HAL_NVIC_EnableIRQ 函数设置。
4 )编写中断服务函数。
每开启一个中断,就必须编写其对应的中断服务函数,否则将导致死机(CPU 将找不到中 断服务函数)。中断服务函数接口厂家已经在 startup_stm32f103xe.s 中做好了,STM32F1 的 IO 口外部中断函数只有 7 个,分别为:

中断线0-4,每个中断线对应一个中断函数,中断线 5-9 共用中断函数EXTI9_5_IRQHandler, 中断线 10-4 共用中断函数 EXTI4_ 10_IRQHandler。一般情况下,我们可以把中断控制逻辑直 接编写在中断服务函数中,但是 HAL 库把中断处理过程进行了简单封装,请看步骤 5 讲解。
5 )编写中断处理回调函数 HAL_GPIO_EXTI_Callback
HAL 库为了用户使用方便,提供了一个中断通用入口函数HAL_GPIO_EXTI_IRQHandler, 在该函数内部直接调用回调函数 HAL_GPIO_EXTI_Callback。
我们先看一下 HAL_GPIO_EXTI_IRQHandler 函数定义:
void HAL_GPIO_EXTI_IRQHandler (uint5_t GPIO_Pin)
{
if (__HAL_GPIO_EXTI_GET_IT (GPIO_Pin) != 0x00U) {
__HAL_GPIO_EXTI_CLEAR_IT (GPIO_Pin); /* 清中断标志位 */
HAL_GPIO_EXTI_Callback(GPIO_Pin); /* 外部中断回调函数 */
}
}
该函数实现的作用非常简单,通过入口参数 GPIO_Pin 判断中断来自哪个 IO 口,然后清除 相应的中断标志位,最后调用回调函数 HAL_GPIO_EXTI_Callback()实现控制逻辑。在所有的外 部中断服务函数中直接调用外部中断共用处理函数 HAL_GPIO_EXTI_IRQHandler,然后在回调 函数 HAL_GPIO_EXTI_Callback 中通过判断中断是来自哪个 IO 口编写相应的中断服务控制逻 辑。
因此我们可以在 HAL_GPIO_EXTI_Callback 里面实现控制逻辑编写,详见本实验源码。
(2) 程序流程图
下面看看本实验的程序流程图:

图 5.4 外部中断实验程序流程图
主程序初始外设,在按键初始化时初始化按键的采样边缘。
3 . 程序解析
(1)main.c 代码
#include "main.h"
#include "usart.h"
#include "gpio.h"
uint8_t Exti_Flag1; // 外部中断触发标志
uint8_t Exti_Flag2; // 外部中断触发标志
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
while (1)
{
if(Exti_Flag1)
{
HAL_UART_Transmit(&huart1, "WK_UP is pressed.\r\n", 19, 0xffff);
Exti_Flag1 = 0;
}
if(Exti_Flag2)
{
HAL_UART_Transmit(&huart1, "KEY0 is pressed.\r\n", 18, 0xffff);
Exti_Flag2 = 0;
}
}
}
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0)
{
HAL_Delay(10);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == SET)
{
Exti_Flag1 = 1;
}
}
if(GPIO_Pin == GPIO_PIN_4)
{
HAL_Delay(10);
if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_4) == SET)
{
Exti_Flag2 = 1;
}
}
}
void Error_Handler(void)
{
__disable_irq();
while (1)
{
}
}
(2) gpio.c代码
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_4;
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
HAL_NVIC_SetPriority(EXTI4_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(EXTI4_IRQn);
}
(3)stm32f1xx_it.c代码
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}
(4)stm32f1xx_hal_msp.c代码
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
if (__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
HAL_GPIO_EXTI_Callback(GPIO_Pin);
}
}
4 . 下载验证

五、实验总结
本次外部中断实验通过按键触发GPIO外部中断实现LED状态翻转,验证了STM32外部中断从引脚配置、中断线映射到NVIC优先级配置的完整流程,实验中观察到正常触发时中断响应即时且LED翻转精准,同时发现未做防抖处理会导致中断误触发、中断服务函数执行耗时操作会造成响应延迟等问题,经添加软件防抖、精简中断服务函数逻辑后,中断触发稳定性显著提升。实验总结得出,外部中断配置需保证引脚上下拉、中断线映射、NVIC优先级的正确性,中断服务函数应精简高效并及时清除中断标志位,才能确保外部中断精准响应、稳定运行,同时硬件防抖或软件防抖措施可有效解决机械按键抖动带来的误触发问题。