本篇总结
使用外部中断,按键触发中断经消抖后串口打印按键信息。
🔑 KEIL MDK、UART调试、EXTI、NVIC
硬件平台:本例程基于 W55MH32L-EVB 开发板运行。
采购推荐:点击前往 WIZnet 官方淘宝店铺 选购 W55MH32L-EVB
本示例演示 W55MH32 的 EXTI(外部中断/事件控制器)外设。将板载按键 PG6 接入 EXTI6,配置为下降沿触发中断:按键按下时硬件自动跳转到中断服务函数,经 10ms 消抖确认后通过串口打印按键按下信息。
前置工具准备
开始本例程前,请确认以下工具已安装:
- WIZ UartTool V1.0:串口调试工具:点击下载
硬件连接
按键 KEY1 接 PG6(按下拉低,内部上拉输入)。USART1 的 TX(PA9)/RX(PA10) 通过板载 USB 转串口连接到 DEBUG USB 口,用于打印中断日志。仅需一根 Micro USB 线连接开发板 DEBUG USB 口到 PC。
| 引脚 | 功能 | 方向 | 模式 |
| PG6 | KEY1 → EXTI6 | 输入 | 上拉输入 (IPU) |
| PA9 | USART1_TX | 开发板 → PC | 复用推挽输出 (AF_PP) |
| PA10 | USART1_RX | PC → 开发板 | 浮空输入 (IN_FLOATING) |
工程结构
工程位于 SDK 内 1.SDK\ModuleDemo\EXTI\,核心文件如下:
EXTI/
├── USER/
│ ├── main.c ← EXTI/NVIC/LED 初始化 + 中断服务函数 + 主循环
│ ├── w55mh32_conf.h ← 外设头文件集中包含
│ ├── w55mh32_it.c/h ← 中断服务函数(也可放此处)
│ ├── system_w55mh32.c/h← 系统初始化(时钟配置)
│ └── startup_w55mh32.s ← 启动文件
└── SYSTEM/
└── delay/
└── delay.c/h ← SysTick 延时函数
EXTI 简介
EXTI(External interrupt/event controller)— 外部中断/事件控制器,管理 20 根中断/事件线。每根线对应一个边沿检测器,可实现上升沿或下降沿检测。EXTI0~EXTI15 用于 GPIO(如 EXTI0 可选 PA0/PB0/.../PI0,由 AFIO_EXTICR1 选择),EXTI16~19 用于 PVD、RTC 闹钟、USB 唤醒等特定外设事件。
EXTI 有两种工作模式:中断模式(信号进入 NVIC,由 CPU 执行 ISR,软件级)和事件模式(直接输出脉冲触发其他外设如 ADC/TIM,硬件级,CPU 不参与)。本例程使用中断模式。
中断线与 NVIC 向量的对应关系
EXTI0~EXTI4 各有独立中断向量(EXTI0_IRQn ~ EXTI4_IRQn),而 EXTI5~9 共用 EXTI9_5_IRQn,EXTI10~15 共用 EXTI15_10_IRQn——共用向量内需在 ISR 中判断具体哪根线触发。
步骤1:EXTI 初始化
文件:USER/main.c | 函数:GPIO_Configuration() — 完成 GPIO、AFIO 映射、NVIC、EXTI 四部分配置。
void GPIO_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
/* 1. 开启 GPIOG、AFIO 时钟(AFIO 是 EXTI 线映射必备) */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG | RCC_APB2Periph_AFIO, ENABLE);
/* 2. PG6: 上拉输入(按键按下拉低) */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOG, &GPIO_InitStructure);
/* 3. 将 PG6 映射到 EXTI6 */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOG, GPIO_PinSource6);
/* 4. NVIC: 使能 EXTI9_5 中断向量 */
NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* 5. 配置 EXTI6:中断模式 / 下降沿触发 / 使能 */
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);
}
五步完成:开时钟(GPIOG + AFIO)→ GPIO 模式(PG6 上拉输入)→ AFIO 映射 PG6 到 EXTI6 → NVIC 使能中断向量 → EXTI 配置(下降沿中断)。
如果你想自定义
- 换用其他按键引脚 → 例如 PB1:改
GPIO_Pin_1、GPIO_PortSourceGPIOB、EXTI_Line1,对应中断向量改为 EXTI1_IRQn
- 改为上升沿或双边沿触发 → 将
EXTI_Trigger_Falling 改为 EXTI_Trigger_Rising 或 EXTI_Trigger_Rising_Falling
- 改为事件模式(触发 ADC/TIM) → 将
EXTI_Mode_Interrupt 改为 EXTI_Mode_Event,此时不进 NVIC
步骤2:NVIC 配置(含于 GPIO_Configuration)
在本例程中,NVIC 配置集成在 GPIO_Configuration() 函数内(见步骤1代码第 4 步)。EXTI6 属于 EXTI9_5 共用中断向量,需使能 EXTI9_5_IRQn,配置抢占优先级 2、子优先级 3。
/* 以下代码位于 GPIO_Configuration() 内 */
NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
优先级分组只设一次
NVIC_PriorityGroupConfig() 在整个程序中只需调用一次。建议放在 main() 开头,不要在每个外设的 NVIC 配置函数里重复调用——否则后设置的分组会覆盖前面的,导致已有外设的优先级被重新解读。
步骤3:中断服务函数
函数:EXTI9_5_IRQHandler() — 名字必须与启动文件中的中断向量名一致。按键按下时先延时 10ms 消抖,再次确认按键仍为低电平后打印按键按下信息,最后清除挂起标志。
void EXTI9_5_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line6) == SET)
{
delay_ms(10); /* 1. 软件消抖 */
if (GPIO_ReadInputDataBit(GPIOG, GPIO_Pin_6) == RESET) /* 2. 确认按键仍按下 */
{
printf("The key is pressed\n"); /* 3. 打印按键信息 */
}
}
EXTI_ClearITPendingBit(EXTI_Line6); /* 4. 清除挂起标志(必不可少) */
}
三个要点:① ISR 名字必须与启动文件向量表一致(EXTI9_5_IRQHandler);② 进 ISR 先判断标志位、做消抖确认后必须清除挂起标志 EXTI_ClearITPendingBit,否则退出后立即重入;③ 延时消抖在中断中仅为演示,实际项目可用定时器扫描+状态机替代。
共用向量的判断方式
若使用 EXTI5~9(向量 EXTI9_5_IRQHandler),需在 ISR 内用 EXTI_GetITStatus(EXTI_Linex) 逐线判断到底是哪根线触发,再分别处理与清标志。
步骤4:主函数逻辑
文件:USER/main.c | 函数:main() — 初始化延时和串口 → 打印时钟 → 配置 GPIO/EXTI/NVIC → 进入空循环等待中断。
int main(void)
{
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("GPIO IO Input Test: PG6 USER key (active low).\n");
GPIO_Configuration();
while (1)
{
}
}
主循环为空 while(1),所有工作由 EXTI 中断驱动。按键按下时硬件自动跳转 ISR,执行消抖与打印后返回。
如果你想自定义
- 在主循环里做其他事 → 去掉
__WFI(),把任务放在 while(1) 中,中断来时自动抢占
- 软件触发中断 → 调用
EXTI_GenerateSWInterrupt(EXTI_Line6),无需按键也能进 ISR,用于测试或任务通知
步骤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 启动后,预期串口先打印时钟信息与提示语;随后每按一次 KEY1,串口打印一行按键信息。
SYSCLK: 72.0Mhz, HCLK: 72.0Mhz, PCLK1: 36.0Mhz, PCLK2: 72.0Mhz, ADCCLK: 36.0Mhz
GPIO IO Input Test: PG6 USER key (active low).
The key is pressed
The key is pressed
The key is pressed
每按一次 KEY1(PG6),EXTI6 下降沿触发中断,ISR 中延时 10ms 消抖并确认按键仍按下后,打印一行 "The key is pressed"。若按一次打印多条,多为按键抖动导致多次触发。
常见问题
常见问题
Q: 按键按下完全没有反应,串口也不打印
按顺序排查:① 是否忘记开 RCC_APB2Periph_AFIO 时钟(EXTI 映射必备);② GPIO_EXTILineConfig 的端口源与引脚源是否匹配实际引脚;③ ISR 函数名是否与启动文件完全一致(如 EXTI9_5_IRQHandler,拼写错则不进中断);④ 是否调用了 NVIC_Init 使能中断向量。
Q: 程序一启动就反复进中断停不下来
ISR 中忘记调用 EXTI_ClearITPendingBit(EXTI_Line6) 清除挂起标志。标志未清除时,退出 ISR 后 NVIC 认为中断仍挂起,会立即重入,导致主循环得不到执行。务必在 ISR 末尾清除对应线的挂起标志。
Q: 按一次键打印好几条(计数跳变)
机械按键存在抖动(bounce),一次按下在几毫秒内产生多次电平跳变,EXTI 下降沿会多次触发。可在 ISR 中加 10~20ms 延时消抖后再次读取引脚电平确认;或改用定时器扫描 + 状态机的方式做消抖,避免在中断里长延时。
Q: 同时用了两个 EXTI 线(如 EXTI0 和 EXTI5),只有一个能进中断
检查 NVIC 是否对两个向量都调用了 NVIC_Init 使能。另外若两条线共用一个向量(如 EXTI5 和 EXTI9 都用 EXTI9_5_IRQHandler),需要在同一个 ISR 内用 EXTI_GetITStatus 分别判断并分别清标志,不能只处理一条线。
Q: 优先级分组设了两次导致中断异常
NVIC_PriorityGroupConfig() 全程序只能调用一次。若在多个外设初始化函数里都调用了,后一次会覆盖前一次的分组设置,导致已配置的抢占/子优先级被重新解读,可能出现高优先级中断被低优先级抢占的反常现象。建议统一放在 main() 开头调用一次。
总结
本文基于 W55MH32 的 EXTI 外设实现按键中断:
- EXTI 配置四步:开 GPIO + AFIO 时钟 → GPIO 模式 → AFIO 映射引脚到 EXTI 线 → EXTI 初始化(线号/模式/触发沿/使能)
- NVIC 配置:优先级分组只设一次,再为每个中断向量配置抢占/子优先级并使能
- ISR 三要点:函数名与启动文件一致 → 判断标志位 → 处理逻辑后必须清除挂起标志
- EXTI0~4 独立向量,EXTI5~9 共用 EXTI9_5,EXTI10~15 共用 EXTI15_10——共用向量需在 ISR 内逐线判断