3.2 按键输入与外部中断


文档摘要

3.2 按键输入与外部中断 上一节点亮了 LED,完成了"执行"方向;这一节打通"感知"方向——读取按键。围绕按键会自然长出三块知识:输入引脚的配置与采样(IDR)、机械抖动的真面目与状态机消抖、以及把输入事件直送 CPU 的外部中断(EXTI + NVIC)。中断是嵌入式从"轮询"迈向"事件驱动"的分水岭,第 4、6 章的定时器中断和 RTOS 都以它为前提。 输入引脚:为什么要上拉 把按键一端接 PA0、另一端接 GND,按下时 PA0 接地是低电平;可松开时 PA0 悬空——悬空引脚的电平随电磁环境漂移,读数毫无意义。解决办法是给引脚一个"默认值":上拉电阻把空闲态拉到高电平,按下才变低。STM32 允许片内集成这个上拉(约 40 kΩ),就是 3.

3.2 按键输入与外部中断

上一节点亮了 LED,完成了"执行"方向;这一节打通"感知"方向——读取按键。围绕按键会自然长出三块知识:输入引脚的配置与采样(IDR)、机械抖动的真面目与状态机消抖、以及把输入事件直送 CPU 的外部中断(EXTI + NVIC)。中断是嵌入式从"轮询"迈向"事件驱动"的分水岭,第 4、6 章的定时器中断和 RTOS 都以它为前提。

输入引脚:为什么要上拉

把按键一端接 PA0、另一端接 GND,按下时 PA0 接地是低电平;可松开时 PA0 悬空——悬空引脚的电平随电磁环境漂移,读数毫无意义。解决办法是给引脚一个"默认值":上拉电阻把空闲态拉到高电平,按下才变低。STM32 允许片内集成这个上拉(约 40 kΩ),就是 3.1 节末尾配过的"输入 + CNF=10 + ODR 写 1"组合:

#include "stm32f10x.h" void key_pa0_init(void) { RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; /* 开 GPIOA 时钟 */ RCC->APB2ENR |= RCC_APB2ENR_AFIOEN; /* AFIO 也要开:EXTI 映射归它管 */ GPIOA->CRL &= ~(0xFUL << 0); /* PA0 位段在 CRL 的 0-3 位 */ GPIOA->CRL |= (0x8UL << 0); /* MODE=00 输入,CNF=10 上拉/下拉 */ GPIOA->ODR |= (1UL << 0); /* 选上拉:空闲为高,按下为低 */ }

板上有外部上拉电阻时(很多按键模块自带),配成 CNF=01 浮空输入即可,省内部电阻。两种接法没有优劣,看硬件给没给条件。

抖动:按键不是理想开关

理想按键按下瞬间电平干净利落;真实按键的金属触点闭合时会弹跳几下,电平在高低之间抖动几毫秒到十几毫秒。用逻辑分析仪抓一次就能看到真面目:一次按下,波形上可能串着五六个毛刺。若程序读到下降沿就立刻响应,一次按压会被当成多次。

先看反面教材——最常见但最伤性能的写法:

/* 反面教材:检测到低电平就死等 20 ms。等待期间 CPU 什么都干不了, 且如果这 20 ms 里又有新事件,程序浑然不知。 */ if (!(GPIOA->IDR & (1UL << 0))) { busy_delay(20000); /* 20 ms 阻塞 */ if (!(GPIOA->IDR & (1UL << 0))) { toggle_led(); while (!(GPIOA->IDR & (1UL << 0))) { } /* 等松手,又是一段阻塞 */ } }

能工作,但三宗罪:阻塞 CPU、无法响应并发事件、等松手期间完全失聪。工程正解是状态机消抖:不等待,只定期采样,用计数把毛刺滤掉。把"按下了"变成一个状态迁移事件,主循环其他部分照常运转:

/* 状态机消抖:每 5 ms 调用一次(节拍来自第 4 章的定时器,或主循环计次)*/ typedef enum { KEY_IDLE = 0, KEY_PRESSED } key_state_t; static key_state_t key_state = KEY_IDLE; static uint8_t stable_cnt = 0; /* 返回 1 表示"一次完整按压已确认",其余时刻返回 0 */ uint8_t key_scan_tick(void) { uint8_t pressed_now = !(GPIOA->IDR & (1UL << 0)); /* 低电平 = 按下 */ switch (key_state) { case KEY_IDLE: if (pressed_now) { if (++stable_cnt >= 4) { /* 连续 4 次(20 ms)都为按下才认账 */ key_state = KEY_PRESSED; stable_cnt = 0; return 1; /* 抖动毛刺最多持续十几 ms,4 个采样点足够滤净 */ } } else { stable_cnt = 0; /* 中途松开则清零重来 */ } break; case KEY_PRESSED: if (!pressed_now) { if (++stable_cnt >= 4) { /* 连续 4 次确认松开,防松手抖动 */ key_state = KEY_IDLE; stable_cnt = 0; } } else { stable_cnt = 0; } break; } return 0; }

这套写法的精髓是用状态而非延时来表达时间:消抖逻辑不占用任何阻塞时间,采样节拍由定时器提供。工程上几乎所有按键、编码器、开关量传感器的去抖都基于这个骨架,值得一次写对、终身复用。

外部中断:让硬件替你盯着引脚

状态机消抖依然需要主循环定期来问(轮询)。STM32 提供更彻底的方案:EXTI 外设盯着引脚,检测到指定边沿就向 NVIC 发中断请求,CPU 当场放下主程序去执行中断服务函数(ISR)。配置链路横跨四个部件:GPIO → AFIO 映射 → EXTI → NVIC,一行都少不了:

void exti0_init(void) { RCC->APB2ENR |= RCC_APB2ENR_AFIOEN; /* AFIO 时钟:引脚到 EXTI 的映射归它管 */ AFIO->EXTICR[0] &= ~AFIO_EXTICR1_EXTI0; /* EXTI0 映射到 PA0:这 4 位全 0 选 GPIOA */ EXTI->IMR |= EXTI_IMR_MR0; /* 开 EXTI0 的中断屏蔽位:允许它发请求 */ EXTI->FTSR |= EXTI_FTSR_TR0; /* 下降沿触发:按下瞬间(高到低)响应 */ NVIC_SetPriority(EXTI0_IRQn, 1); /* 抢占优先级 1(CMSIS 提供的内核函数)*/ NVIC_EnableIRQ(EXTI0_IRQn); /* 放行:NVIC 开始受理 EXTI0 */ } /* 中断服务函数:名字必须与启动文件里的向量表条目完全一致 */ void EXTI0_IRQHandler(void) { if (EXTI->PR & EXTI_PR_PR0) { /* 确认是本线路触发 */ EXTI->PR = EXTI_PR_PR0; /* 写 1 清除挂起标志——忘了这句中断会风暴 */ GPIOB->ODR ^= (1UL << 5); /* 翻转 LED:按下即翻转 */ } }

三个易错点值得单独标注。其一,AFIO 时钟是 EXTI 配置的隐形前提,漏开它,EXTICR 写了也白写,症状是"配置全对就是不进中断"。其二,PR(挂起寄存器)必须写 1 清除,且要在 ISR 里清——不清的话 ISR 一退出 NVIC 立刻再次调用它,主程序被活活饿死,这就是"中断风暴"。其三,ISR 里不要延时、不要 printf:中断要快进快出,实际工程里 ISR 常常只置一个标志或往队列丢一条消息(第 6 章的 FreeRTOS 正是把这件事制度化了)。

轮询还是中断:一张表定夺

两种输入响应方式各有地盘,选择依据是事件频率与系统的并发压力:

维度 主循环轮询 外部中断
实现复杂度 低,几行扫描 高,四个部件的配置链
CPU 占用 持续占用主循环时间 事件发生才介入
响应延迟 一个扫描周期内 微秒级,可预期
适合场景 低频、可迟钝的事件 稀疏但要求快响应的事件

经验法则:毫秒级无所谓的事件(环境光亮度变化)轮询就好;必须立刻处理的事件(急停按钮、流量脉冲计数)用中断。两者也不互斥——工业产品常是"中断置标志、主循环处理"的混合架构,兼顾响应速度与处理逻辑的完整性。

到这里,GPIO 的输入输出两个方向都通了。但还剩一块没碰:芯片之外的那圈电路。下一节把镜头拉远,看最小系统怎么搭、上电头几百毫秒芯片在干什么——那是软件世界与物理世界的接缝。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U