本篇总结
使用外部中断,按键触发中断翻转 LED。
🔑 KEIL MDK、UART调试、NVIC、EXTI
硬件平台:本例程基于 W55MH32L-EVB 开发板运行。
采购推荐:点击前往 WIZnet 官方淘宝店铺 选购 W55MH32L-EVB
本示例演示 W55MH32 的 NVIC(嵌套向量中断控制器)使用方法,以 EXTI6 外部中断为例:按下 PG6 按键触发中断,在中断服务函数中翻转 PD14 LED。
W55MH32 在内核水平上支持 15 个系统异常和 60 个外部中断,除个别异常优先级固定外,其余均可编程。配置中断只需掌握三步:使能外设中断 → 初始化 NVIC → 编写中断服务函数。
前置工具准备
开始本例程前,请确认以下工具已安装:
- WIZ UartTool V1.0:串口调试工具:点击下载
硬件连接
使用 PG6 接按键(EXTI6 外部中断),PD14 接 LED。USART1 的 TX(PA9) 和 RX(PA10) 通过板载 USB 转串口芯片连接到 DEBUG USB 口,用于观察串口日志。仅需一根 Micro USB 线连接开发板 DEBUG USB 口到 PC。
| 引脚 | 功能 | 方向 | 模式 |
| PG6 | EXTI6 按键 | 输入 | 上拉输入 (IPU) |
| PD14 | LED | 输出 | 推挽输出 (Out_PP) |
| PA9 | USART1_TX | 开发板 → PC | 复用推挽输出 (AF_PP) |
| PA10 | USART1_RX | PC → 开发板 | 浮空输入 (IN_FLOATING) |
工程结构
工程位于 SDK 内 1.SDK\ModuleDemo\NVIC\,核心文件如下:
NVIC/
├── USER/
│ ├── main.c ← 中断优先级分组 + EXTI/NVIC初始化 + LED翻转ISR + 主流程
│ ├── w55mh32_conf.h ← 外设头文件集中包含
│ ├── w55mh32_it.c/h ← 中断服务函数(本例 EXTI9_5_IRQHandler 也可放此)
│ ├── system_w55mh32.c/h← 系统初始化(时钟配置)
│ └── startup_w55mh32.s ← 启动文件(含中断向量表)
└── SYSTEM/
└── delay/
└── delay.c/h ← SysTick 延时函数
步骤1:中断优先级分组
函数:NVIC_PriorityGroupConfig() — 设置抢占优先级和子优先级的位数划分。整个程序只需调用一次,建议放在 main() 开头。
W55MH32 优先级寄存器仅用高 4 位,可分为主(抢占)优先级和子优先级。抢占优先级高的中断可以打断低的正在执行的中断(嵌套);抢占相同时比较子优先级;两者相同时比较硬件中断编号(编号小的优先)。
| 分组宏 | 主优先级 | 子优先级 | 说明 |
| NVIC_PriorityGroup_0 | 0 位 | 0-15 | 无抢占,全部子优先级 |
| NVIC_PriorityGroup_1 | 0-1 | 0-7 | 1 位抢占 + 3 位子 |
| NVIC_PriorityGroup_2 | 0-3 | 0-3 | 2 位抢占 + 2 位子(常用) |
| NVIC_PriorityGroup_3 | 0-7 | 0-1 | 3 位抢占 + 1 位子 |
| NVIC_PriorityGroup_4 | 0-15 | 0 | 4 位抢占,无子优先级 |
/* 中断优先级分组:2位抢占 + 2位子优先级(整个程序只调用一次) */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
全程序只调用一次
优先级分组一旦设定,所有外设的中断优先级都基于此分组解读。重复调用会覆盖之前的设置。建议放在 main() 开头,不要写进各外设的配置函数中。
步骤2:EXTI 与 NVIC 初始化
函数:EXTI_NVIC_Configuration() — 配置 PG6 为 EXTI6 中断源,下降沿触发,并设置 NVIC 优先级。
中断编程三要点中的前两步在此完成:①使能外设中断(EXTI_LineCmd=ENABLE);②初始化 NVIC_InitTypeDef 结构体,配置中断源、抢占优先级、子优先级并使能。
void EXTI_NVIC_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
/* 开启 GPIOG 和 AFIO 时钟(EXTI 需要 AFIO 时钟) */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG | RCC_APB2Periph_AFIO, ENABLE);
/* PG6: 上拉输入,接按键 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOG, &GPIO_InitStructure);
/* EXTI6: PG6 下降沿触发 */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOG, GPIO_PinSource6);
EXTI_InitStructure.EXTI_Line = EXTI_Line6;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
/* NVIC: EXTI9_5 中断,抢占优先级1,子优先级0 */
NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
NVIC_InitTypeDef 四个成员:NVIC_IRQChannel 指定中断源(参考 w55mh32.h 中的 IRQn_Type 枚举);PreemptionPriority 和 SubPriority 的取值范围由步骤1的分组决定;Cmd 使能或失能该中断。
如果你想自定义
- 改用其他 GPIO 触发 → 修改
GPIO_EXTILineConfig() 的端口和引脚源,以及 EXTI_Line 编号
- 改为上升沿触发 → 将
EXTI_Trigger_Falling 改为 EXTI_Trigger_Rising,或 EXTI_Trigger_Rising_Falling 双沿触发
步骤3:中断服务函数
函数:EXTI9_5_IRQHandler() — 中断编程三要点第三步。函数名必须与启动文件 startup_w55mh32.s 中预定义的一致,否则中断向量表找不到入口。
void EXTI9_5_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line6) != RESET)
{
/* 翻转 PD14 LED */
GPIO_WriteBit(GPIOD, GPIO_Pin_14,
(BitAction)(1 - GPIO_ReadOutputDataBit(GPIOD, GPIO_Pin_14)));
/* 清除中断标志位,否则会不断重入 */
EXTI_ClearITPendingBit(EXTI_Line6);
}
}
ISR 内必须先检查中断标志位(EXTI_GetITStatus),处理完后清除标志位(EXTI_ClearITPendingBit)。不清除会导致中断不断重入,程序卡死在 ISR 中。
函数名不可写错
中断服务函数名必须与启动文件中一致(如 EXTI9_5_IRQHandler、USART1_IRQHandler)。写错不会编译报错,但中断会跳转到启动文件中的空函数并无限循环。
步骤4:主函数逻辑
函数:main() — 先设置优先级分组(全程序一次)→ 初始化 UART/LED → 配置 EXTI+NVIC → 进入主循环等待中断。
int main(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_ClocksTypeDef clocks;
delay_init();
UART_Configuration(115200);
RCC_GetClocksFreq(&clocks);
printf("\n");
printf("SYSCLK: %3.1fMhz, HCLK: %3.1fMhz, PCLK1: %3.1fMhz, "
"PCLK2: %3.1fMhz, ADCCLK: %3.1fMhz\n",
(float)clocks.SYSCLK_Frequency / 1000000,
(float)clocks.HCLK_Frequency / 1000000,
(float)clocks.PCLK1_Frequency / 1000000,
(float)clocks.PCLK2_Frequency / 1000000,
(float)clocks.ADCCLK_Frequency / 1000000);
printf("NVIC Test: EXTI6 (PG6 button) -> LED (PD14) toggle.\n");
/* 1. 中断优先级分组:整个程序只调用一次 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/* 2. PD14: LED 推挽输出 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOD, &GPIO_InitStructure);
GPIO_SetBits(GPIOD, GPIO_Pin_14); /* 初始熄灭 */
/* 3. 配置 EXTI6 + NVIC */
EXTI_NVIC_Configuration();
printf("Press KEY (PG6) to trigger EXTI6 interrupt.\n");
while (1); /* 主循环空转,等待中断 */
}
主循环为空,CPU 全部工作交给中断处理。按下按键时触发 EXTI6 中断,ISR 翻转 LED。这种"事件驱动"模式让 CPU 在无事件时可进入低功耗,无需轮询。
如果你想自定义
- 增加超时去抖 → 在 ISR 中加
delay_ms(20) 软件消抖,或改用定时器中断去抖更可靠
- 多中断共存 → 为每个外设分别调用
NVIC_Init(),根据步骤1的分组合理分配抢占/子优先级
步骤5:printf 重定向
重写 fputc() 将 printf 输出重定向到 USART1,并自动在 \n 前补 \r 确保串口工具正确换行。
int fputc(int c, FILE *f)
{
if (c == '\n')
SER_PutChar('\r'); // 补回车,确保串口工具正确换行
return (SER_PutChar(c));
}
int SER_PutChar(int ch)
{
while (!USART_GetFlagStatus(USART_TEST, USART_FLAG_TC));
USART_SendData(USART_TEST, (uint8_t)ch);
return ch;
}
运行验证
打开 WIZ UartTool V1.0,选择开发板对应 COM 口,波特率 115200 / 8N1,打开串口。按 RESET 启动后,预期串口输出时钟信息和提示语。每按一次 PG6 按键,PD14 LED 翻转一次状态。
SYSCLK: 72.0Mhz, HCLK: 72.0Mhz, PCLK1: 36.0Mhz, PCLK2: 72.0Mhz, ADCCLK: 36.0Mhz
NVIC Test: EXTI6 (PG6 button) -> LED (PD14) toggle.
Press KEY (PG6) to trigger EXTI6 interrupt.
按下按键时 LED 状态翻转,松开按键无变化(下降沿触发)。若 LED 无反应,参考下方常见问题排查。
常见问题
Q: 按键后 LED 不翻转 / 进不了中断
按顺序检查:①NVIC_IRQChannelCmd 是否为 ENABLE;②EXTI_LineCmd 是否为 ENABLE;③GPIO_EXTILineConfig 端口和引脚源是否匹配;④AFIO 时钟是否开启(RCC_APB2Periph_AFIO)。
Q: 中断服务函数名写对了但编译后不执行
检查函数名是否与启动文件 startup_w55mh32.s 中的向量表完全一致(区分大小写,如 EXTI9_5_IRQHandler)。写错不会编译报错,但中断会跳转到空函数并死循环。可在 ISR 首行加 printf 验证是否进入。
Q: 中断不断重入 / 程序卡死在 ISR
ISR 中未清除中断标志位。EXTI 必须调用 EXTI_ClearITPendingBit(EXTI_Line6);其他外设类似(如 USART 调 USART_ClearITPendingBit,Timer 调 TIM_ClearITPendingBit)。
Q: 抢占优先级无效 / 中断不能嵌套
优先级分组设置错误。若设为 NVIC_PriorityGroup_0,则没有抢占优先级(0 位),所有中断不能互相抢占。改用 NVIC_PriorityGroup_2(2 位抢占 + 2 位子)即可支持嵌套。注意 NVIC_PriorityGroupConfig 全程序只能调用一次。
Q: 优先级数值越大优先级越高吗?
不是。优先级数值越小优先级越高,0 是最高优先级。抢占优先级和子优先级的取值范围由分组决定(如 Group_2 下两者均为 0-3)。设置时核心中断(如 SysTick)建议设较小数值(高优先级),普通外设中断设较大数值。
总结
本文基于 W55MH32 的 EXTI6 外部中断演示了 NVIC 中断编程完整流程:
- 优先级分组:通过
NVIC_PriorityGroupConfig() 设置抢占/子优先级位数划分,全程序只调用一次,建议放 main() 开头
- 中断三步法:①使能外设中断(EXTI_LineCmd)→ ②初始化 NVIC(NVIC_IRQChannel + 优先级 + Cmd)→ ③编写中断服务函数(函数名与启动文件一致,处理完清标志位)
- 关键细节:优先级数值越小优先级越高;ISR 中必须清除中断标志位避免重入;AFIO 时钟必须开启才能配置 EXTI
- 本例以 EXTI6+LED 为演示,其他外设(USART/SPI/Timer/DMA)的中断配置流程完全相同,仅中断源和标志位 API 不同