嵌入式技术应用设计实训 | GPIO 输入与外部中断:按键
本课程开源地址(Gitee):https://gitee.com/fujianxinxi/qianrushishixundianzi.git
课件、示例代码与验收脚本都在该仓库,可直接git clone或下载 ZIP 使用。B站配套视频合集:https://space.bilibili.com/572740825/lists?sid=9228122
合集《嵌入式技术应用设计实训|STM32F103 从点灯到智能家居》:每课配一节视频,可先看现象演示,再照着做。开发板资料A盘(百度网盘):https://pan.baidu.com/s/1_EXrjRUewwmGMgcy38zEZg(提取码:
zbri)
正点原子官方资料下载:http://www.openedv.com/docs/boards/stm32/zdyz_stm32f103_nano.html
模块一 基础外设开发 · 2 学时 · 环境:Keil MDK5 + VS Code + 正点原子 STM32F103 Nano V1.3
一句话概述:从“读一个引脚的电平”入手,先弄清板载 4 个按键(KEY0/KEY1/KEY2/WK_UP)按下时分别是低电平还是高电平、为什么必须配上拉或下拉;再把它们配成外部中断(EXTI)+ NVIC,写出中断入口与回调,实现“按任意一个键,8 个 LED 在全亮与全灭之间翻转一次”。重点讲清“引脚号 → EXTI 线号 → NVIC 通道号”三段绑定,以及“中断里不能延时、消抖改用时间戳锁定”。课堂练习 4 条,大作业把“一键翻转全部”扩成“4 个键各管一个灯、按一下亮再按一下灭”。
本节目标
- 能说出读一个按键要按“开时钟 → 配输入(上拉/下拉)→ 读电平”三步走,并说清 KEY0/KEY1/KEY2 为什么配上拉、WK_UP 为什么配下拉
- 会配置 EXTI + NVIC(含中断入口函数与回调函数),让按任意一个键都能把 8 个 LED 在“全亮 / 全灭”之间翻转一次
- 能说出“引脚号 → EXTI 线号 → NVIC 通道号”的三段对应关系,并解释为什么中断里不能延时消抖
一、原理说明
GPIO 做输出是往外写电平,做输入就反过来——读电平:先给端口开时钟,再把引脚配成输入、并按需要打开上拉或下拉电阻,最后用HAL_GPIO_ReadPin()读回它的高低。板载 4 个按键并不是“按键直接接在 IO 上”这么简单:KEY0/KEY1/KEY2 是按键另一端接地,所以必须打开内部上拉,不按时引脚被拉成高电平、按下才被按键拉到地(低电平);WK_UP 接的是 3.3V,必须打开内部下拉,不按时是低、按下才是高——这也是 4 个键里唯一的反相。机械按键在按下和松开的一瞬间,触点会弹跳三五毫秒,电平像下雨一样忽高忽低,所以“读一次就当按下”往往会把一次按动读成好几次。外部中断(EXTI)的作用是让引脚自己盯着电平变化:变化一发生,硬件立刻把 CPU 拉到我们写好的那段函数里执行,主循环就不用一直盯着按键了。配一个外部中断要记住三段绑定——引脚号决定 EXTI 线号,EXTI 线号决定 NVIC 里的中断通道号,三段错一个,代码照样能编译、能下载,但按键就是一点反应都没有。
二、讲解内容
2.1 知识讲解
先认清 4 个按键接在哪、按下是什么电平(以下照厂家例程HARDWARE/KEY/key.c抄,别看反):
| 板载按键 | 开发板引脚 | 不按时 | 按下时 | 引脚怎么配(HAL) |
|---|---|---|---|---|
| KEY0 | PC8 | 高(内部上拉) | 低 | GPIO_PULLUP+ 下降沿触发 |
| KEY1 | PC9 | 高(内部上拉) | 低 | 同上(和 KEY0 一次配好) |
| KEY2 | PD2 | 高(内部上拉) | 低 | 同上(只换引脚号) |
| WK_UP | PA0 | 低(内部下拉) | 高(唯一反相) | GPIO_PULLDOWN+ 上升沿触发 |
三段绑定:引脚号 → EXTI 线号 → NVIC 通道号。EXTI 的线号不看端口(PA/PC/PD 都一样),只看引脚号:PC8 是线 8、PC9 是线 9、PD2 是线 2、PA0 是线 0。而 NVIC 里的通道是按“线”分的,并且会合并——线 0、线 1 各有独立通道,线 5~9 挤在同一个通道EXTI9_5里:
| 按键 | 引脚 | EXTI 线号 | 中断入口函数(向量表按函数名找) | NVIC 通道 |
|---|---|---|---|---|
| WK_UP | PA0 | 线 0 | EXTI0_IRQHandler | EXTI0_IRQn |
| KEY2 | PD2 | 线 2 | EXTI2_IRQHandler | EXTI2_IRQn |
| KEY0 | PC8 | 线 8 | EXTI9_5_IRQHandler | EXTI9_5_IRQn |
| KEY1 | PC9 | 线 9 | 同上(和 PC8 同一个入口) | 同上 |
“线 8、线 9 共用一个入口”是新手最容易迷糊的地方:KEY0 和 KEY1 的中断都会跳进EXTI9_5_IRQHandler,所以在里面一次要把两个引脚都处理掉——本课直接写成HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_8 | GPIO_PIN_9);,谁触发就翻谁的状态,写法最简单也最不容易漏。
输入三步,和输出三步一样不能少:
| 步骤 | 干什么 | 本课的代码 | 不做会怎样 |
|---|---|---|---|
| ① 开时钟 | 给按键引脚所在端口使能时钟 | __HAL_RCC_GPIOA_CLK_ENABLE();等三行 | 引脚配置全部无效,按键没反应,且不报错 |
| ② 配模式 | 配成输入并按需要打开上拉/下拉;要中断就再加上触发边沿 | GPIO_MODE_IT_FALLING+GPIO_PULLUP | 引脚是复位后的浮空输入,电平乱飘,会误触发 |
| ③ 使能中断 | 让 NVIC 放行这条中断通道 | HAL_NVIC_EnableIRQ(EXTI9_5_IRQn); | 引脚能看到电平变化,但 CPU 永远不会被叫去处理 |
STM32F1 的一个专属细节(AFIO 时钟):F1 的 EXTI 引脚映射要经过 AFIO 外设,写寄存器版时得先__HAL_RCC_AFIO_CLK_ENABLE();。不过 HAL 库的HAL_GPIO_Init()一看到IT模式就替你把这句写好了(在stm32f1xx_hal_gpio.c里),所以本课代码里不用再写;看到别的例程多这一句,知道它在干什么就行。
抖动与消抖:两种做法,效果一样,代价不同。机械按键按下和松开的瞬间,触点在几毫秒里反复通断,电平会跳好几次——这就是“抖动”。不处理的话,一次按动会被当成两三次:
| 做法 | 怎么写 | 优点 | 代价 |
|---|---|---|---|
| 轮询 + 延时消抖 | 厂家例程的KEY_Scan():读到按下 →delay_ms(10)再确认一次 → 松手后才允许下一次 | 直观好懂,适合小实验 | CPU 一直盯着按键;delay_ms那 10ms 里什么都干不了 |
| 中断 + 时间戳锁定(本课用) | 回调里记HAL_GetTick(),200ms 内再来就丢掉 | 不占 CPU,主循环空闲 | 快速连按会被“吞”掉几次(把KEY_LOCK_MS调小可缓解) |
中断里为什么不能延时:中断一旦进入,主循环就停在原地等它执行完。如果在中断里写delay_ms(10),整个系统就被卡住 10ms,按键按得急了还会互相插队,现象会变得莫名其妙。另外HAL_GetTick()靠 SysTick 的 1ms 中断累计,如果中断优先级配得不对,它可能一直不变,用时间戳会算错。本课把 EXTI 的抢占优先级设成2(数值越大优先级越低),而 SysTick 是 0、比它高,所以在中断里HAL_GetTick()照样在走——这是“时间戳锁定”能放进中断的前提。
HAL 库的中断调用链(记住这三步就够):
引脚电平出现下降沿/上升沿(KEY0~KEY2 下降沿、WK_UP 上升沿) │ 硬件自动触发 ▼ EXTI9_5_IRQHandler() ← 中断入口函数,向量表按这个名字找它 │ 我们调用 ▼ HAL_GPIO_EXTI_IRQHandler(引脚) ← HAL 帮你清掉中断挂起标志 │ 它再调用 ▼ HAL_GPIO_EXTI_Callback(引脚) ← 我们干活的地方:翻转 8 个灯HAL_GPIO_EXTI_Callback()在 HAL 库里是弱函数(__weak):我们在main.c里重写一份,链接时就会用我们这份,不用注册、也不用函数指针。
2.2 代码示例
本课只改一个文件:课时文档/代码/课时02-GPIO 输入与外部中断-按键/按键实验/USER/main.c。工程里其余文件都是厂家例程现成的,不用动:
课时文档/代码/课时02-GPIO 输入与外部中断-按键/ └── 按键实验/ ← 正点原子官方“按键实验”例程(HAL 库版) ├── USER/KEY.uvprojx ← Keil 打开这个工程 ├── USER/main.c ← 本课唯一要改的文件 ├── HARDWARE/KEY/ ← 厂家按键驱动 key.c / key.h(轮询版,本课不调用,练习 P2.3 要用) ├── HARDWARE/LED/ ← LED 驱动:PC0~PC7 配成推挽输出,上电默认全灭 ├── HARDWARE/BEEP/ ← 蜂鸣器驱动(本课不用,练习 P2.4 会用到) ├── SYSTEM/ ← sys、delay(delay_ms 就在这里)、usart ├── HALLIB/ CORE/ ← HAL 库与启动文件(不用改) └── OBJ/ ← 编译输出(已加入 .gitignore,不进仓库)下面是本课USER/main.c的完整代码(一个文件,一个现象):
#include"sys.h"#include"delay.h"#include"usart.h"#include"led.h"/************************************************ 嵌入式技术应用设计实训 课时02 GPIO 输入与外部中断:按键 本文件基于正点原子 ALIENTEK NANO STM32F103 厂家例程(按键实验-HAL库版)改写。 例程版权归广州市星翼电子科技有限公司所有。 硬件(正点原子 STM32F103 Nano V1.3,全部板载,不用接线): DS0~DS7(红色 LED) 接 PC0~PC7,低电平点亮 KEY0 接 PC8、KEY1 接 PC9、KEY2 接 PD2 —— 按下是低电平(芯片内上拉) WK_UP 接 PA0 —— 按下是高电平(芯片内下拉,唯一反相) 本课现象(只有一个):按任意一个键,8 个 LED 在“全亮 / 全灭”之间翻转一次。 实现方式:外部中断(EXTI + NVIC)。中断只做两件事——判断抖动、翻转灯, 其余时间主循环什么都不干,等着中断来敲门。 两个必须记住的坑: 1) 中断里不能延时消抖(delay_ms 靠 SysTick,在中断里死等会把整个程序拖住), 所以本课用“时间戳锁定”:200ms 内的重复触发直接丢掉。 2) 中断入口函数 EXTIx_IRQHandler 必须存在,否则中断触发了也进不来。 厂家这个例程是轮询版,没写这三个入口,本课把它们补在下面。 ************************************************//* ---------- LED:低电平点亮,PC0~PC7 对应 DS0~DS7(与课时01 写法一致) ---------- */#defineLED_ALL_OFF()HAL_GPIO_WritePin(GPIOC,(uint16_t)0x00FF,GPIO_PIN_SET)/* 8 个灯全灭 */#defineLED_ALL_ON()HAL_GPIO_WritePin(GPIOC,(uint16_t)0x00FF,GPIO_PIN_RESET)/* 8 个灯全亮 *//* ---------- 消抖:同一个键两次响应的最小间隔(毫秒),觉得太钝就改小 ---------- */#defineKEY_LOCK_MS200staticu8 all_on=0;/* 8 个灯当前的状态:0 = 全灭,1 = 全亮 *//* ------------------------------------------------------------------ 中断入口:Cortex-M3 的向量表按函数名找入口,名字写错 = 中断永远进不来 PA0 -> EXTI0 线 0 PD2 -> EXTI2 线 2 PC8 -> EXTI9_5 线 8、线 9 共用一个入口,所以一次要处理两个引脚 HAL_GPIO_EXTI_IRQHandler() 会清中断标志,再调用下面的 HAL_GPIO_EXTI_Callback() ------------------------------------------------------------------ */voidEXTI0_IRQHandler(void){HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);/* WK_UP */}voidEXTI2_IRQHandler(void){HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_2);/* KEY2 */}voidEXTI9_5_IRQHandler(void){HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_8|GPIO_PIN_9);/* KEY0 / KEY1 */}/* ------------------------------------------------------------------ 按键中断初始化:4 个按键全部配成“外部中断”模式 输入三步一样不能少:开时钟 -> 配模式(含上拉/下拉、触发边沿)-> 使能中断 注意 WK_UP 与另外三个相反:按下是高电平,所以配下拉 + 上升沿 ------------------------------------------------------------------ */staticvoidKEY_EXTI_Init(void){GPIO_InitTypeDef GPIO_Initure;__HAL_RCC_GPIOA_CLK_ENABLE();/* WK_UP 在 PA0 */__HAL_RCC_GPIOC_CLK_ENABLE();/* KEY0/KEY1 在 PC8/PC9 */__HAL_RCC_GPIOD_CLK_ENABLE();/* KEY2 在 PD2 *//* STM32F1 的 EXTI 引脚映射要经过 AFIO:HAL 库的 HAL_GPIO_Init() 看到 IT 模式时 已经替你开了 AFIO 时钟(stm32f1xx_hal_gpio.c 里那行 __HAL_RCC_AFIO_CLK_ENABLE()), 所以这里不用再写;写寄存器版时要自己开。 *//* KEY0(PC8)、KEY1(PC9):按下是低电平 -> 上拉 + 下降沿 */GPIO_Initure.Pin=GPIO_PIN_8|GPIO_PIN_9;GPIO_Initure.Mode=GPIO_MODE_IT_FALLING;GPIO_Initure.Pull=GPIO_PULLUP;GPIO_Initure.Speed=GPIO_SPEED_HIGH;HAL_GPIO_Init(GPIOC,&GPIO_Initure);/* KEY2(PD2):同上(沿用上面的结构体,只改引脚) */GPIO_Initure.Pin=GPIO_PIN_2;HAL_GPIO_Init(GPIOD,&GPIO_Initure);/* WK_UP(PA0):按下是高电平 -> 下拉 + 上升沿,触发方式与上面相反 */GPIO_Initure.Pin=GPIO_PIN_0;GPIO_Initure.Mode=GPIO_MODE_IT_RISING;GPIO_Initure.Pull=GPIO_PULLDOWN;HAL_GPIO_Init(GPIOA,&GPIO_Initure);/* 使能 NVIC 通道:引脚号 -> EXTI 线号 -> 中断通道号,三段要一一对上 */HAL_NVIC_SetPriority(EXTI0_IRQn,2,0);/* 线 0,WK_UP */HAL_NVIC_EnableIRQ(EXTI0_IRQn);HAL_NVIC_SetPriority(EXTI2_IRQn,2,0);/* 线 2,KEY2 */HAL_NVIC_EnableIRQ(EXTI2_IRQn);HAL_NVIC_SetPriority(EXTI9_5_IRQn,2,0);/* 线 5~9,KEY0/KEY1 都在这 */HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);}/* ------------------------------------------------------------------ 中断回调:HAL 库里所有 EXTI 线最终都进这里 它在 HAL 里是“弱函数”,在 main.c 里重写一份就被调用(不用注册) 这里只干两件事:200ms 锁定消抖、翻转 8 个灯 ------------------------------------------------------------------ */voidHAL_GPIO_EXTI_Callback(uint16_tGPIO_Pin){staticuint32_tlast_tick=0;/* 上一次响应的时间戳 */uint32_tnow=HAL_GetTick();/* 开机以来的毫秒数 */if(now-last_tick<KEY_LOCK_MS)return;/* 200ms 内再触发:判为抖动,丢掉 */last_tick=now;all_on=!all_on;/* 翻转状态 */if(all_on)LED_ALL_ON();elseLED_ALL_OFF();}intmain(void){HAL_Init();/* 初始化 HAL 库:顺便把 1ms 时基(HAL_GetTick)跑起来 */Stm32_Clock_Init(RCC_PLL_MUL9);/* 系统时钟:倍频到 72MHz */delay_init(72);/* 初始化延时函数 */LED_Init();/* 初始化 LED:DS0~DS7(PC0~PC7) */KEY_EXTI_Init();/* 初始化 4 个按键的外部中断 */LED_ALL_OFF();/* 上电先全灭,按下第一个键就看到“全亮” */while(1){/* 主循环什么都不用做:按键一按,中断自己把 8 个灯翻转 (这就是中断的好处——CPU 不用一直盯着按键) */}}代码里最值得盯住的 4 处:
| 位置 | 为什么这么写 |
|---|---|
GPIO_MODE_IT_FALLING/GPIO_MODE_IT_RISING | KEY0~KEY2 按下是低电平,配下降沿;WK_UP 按下是高电平,配上升沿。配反的现象是“松手才翻”或“完全没反应” |
EXTI9_5_IRQHandler()里的GPIO_PIN_8与GPIO_PIN_9 | 线 8、线 9 共用一个入口,两个按键一次处理完,少写一个就会“KEY1 按下没反应” |
HAL_GPIO_EXTI_Callback()里的HAL_GetTick()判断 | 中断里不能延时,用时间戳做 200ms 锁定消抖 |
while (1) {}空着的主循环 | 说明“中断把主循环解放了”:按键响应不再靠轮询,主循环可以什么都不干 |
三、实验操作与现象
3.1 接线与操作步骤
本课不用任何额外接线:LED、4 个按键都在板上(这点和课时01 一样,插上就能做)。
| 模块/元件 | 开发板引脚 | 说明 |
|---|---|---|
| KEY0 | PC8 | 按下为低电平;已打开内部上拉,不用外接电阻 |
| KEY1 | PC9 | 同上(与 KEY0 共用中断入口) |
| KEY2 | PD2 | 同上(独立一条 EXTI 线) |
| WK_UP | PA0 | 按下为高电平;已打开内部下拉(唯一反相,别照 KEY0 的写法套) |
| DS0~DS7 | PC0~PC7 | 低电平点亮,本课用来显示按键状态 |
| ST-Link | SWDIO / SWCLK / GND | 下载与调试(课时00 已连好) |
操作步骤:
- Keil 打开工程:
课时文档/代码/课时02-GPIO 输入与外部中断-按键/按键实验/USER/KEY.uvprojx - 选下载器:Options for Target → Debug → ST-Link Debugger → OK(课时00 已设过,换电脑要重设)
- 看代码:确认
main.c里的KEY_LOCK_MS是200(消抖锁定时间) - 编译:Build(F7),确认
0 Error(s), 0 Warning(s) - 下载:Download(F8),确认
Programming Done. Verify OK. - 按复位键 RST(或重新上电):8 个灯全灭,程序就绪
- 依次按 KEY0、KEY1、KEY2、WK_UP:每按一次,8 个灯在“全亮 / 全灭”之间翻转一次
3.2 观察到的现象
本课现象以随课视频演示为准(视频里会把 4 个按键各按几遍、再快速连按一遍),文档不逐条配截图;你自己做完请录一段或拍一张,作为提交物。
| 操作 | 看到什么算成功 | 怎么核对 |
|---|---|---|
| 上电/复位后 | 8 个灯全灭,安静地等着 | 一个灯都不亮,也没有声音 |
| 按一下 KEY0(PC8) | 8 个灯全部亮;再按一下全部灭 | 一次按动只翻转一次,不会只亮一部分 |
| 按一下 KEY1(PC9) | 同上 | KEY1 与 KEY0 走同一个中断入口EXTI9_5,现象应完全一致 |
| 按一下 KEY2(PD2) | 同上 | 它走EXTI2,是独立的一条线 |
| 按一下 WK_UP(PA0) | 同上 | 它是高电平有效、走EXTI0;松开时不应再翻一次 |
| 快速连按同一个键(每秒 4~5 下) | 灯不会被一次按动翻两三次 | 200ms 锁定在起作用;把KEY_LOCK_MS改成500再按,感觉更明显 |
看不到现象怎么办(按最可能的原因排序):
| 现象 | 最可能的原因 | 怎么处理 |
|---|---|---|
| 4 个键按了都没反应,灯一直全灭 | 中断入口函数没写或名字拼错(例如写成EXIT9_5_IRQHandler);或忘了HAL_NVIC_EnableIRQ() | 对照 2.1 节的“三段绑定”表,逐项核对入口函数名与通道号 |
| 只有 WK_UP 有反应,另外三个没反应 | KEY0/KEY1/KEY2 的配置不对:漏了GPIO_PULLUP,或漏开 GPIOC / GPIOD 时钟 | 检查三行__HAL_RCC_GPIOx_CLK_ENABLE()是否都在 |
| 只有 KEY0 有反应,KEY1 没反应 | EXTI9_5_IRQHandler里只写了GPIO_PIN_8 | 补成GPIO_PIN_8与GPIO_PIN_9 |
| 按键一按,灯闪两三次才停 | 消抖没生效(KEY_LOCK_MS被改成 0) | 恢复KEY_LOCK_MS 200 |
| 松手才翻 / 按住不翻 | 触发边沿配反了:低有效的键配成了上升沿 | KEY0~KEY2 用GPIO_MODE_IT_FALLING,WK_UP 用GPIO_MODE_IT_RISING |
编译报重复定义(multiply defined) | 中断入口函数在stm32f1xx_it.c里也写了一份(本工程没有,换别的例程时可能遇到) | 只保留一处;本课统一写在main.c |
| 一上电 8 个灯就是全亮 | 少了LED_ALL_OFF()那句 | 在LED_Init()之后补上 |
下载报No Cortex-M Device found | 下载器没连好 / 板子没上电 | 查 SWD 三根线与供电(课时00 练过一遍) |
四、学生练习
课堂上按顺序做,一条一条改、一条一条看现象:
| 编号 | 任务 | 提示 | 预期结果 |
|---|---|---|---|
| P2.1 | 只让 KEY0 有效 | 注释掉__HAL_RCC_GPIOA_CLK_ENABLE();、__HAL_RCC_GPIOD_CLK_ENABLE();,以及 WK_UP、KEY2 的HAL_GPIO_Init()与两次HAL_NVIC_EnableIRQ()(EXTI0、EXTI2) | 只有 KEY0 能翻转 8 个灯,另外三个键按了没反应;EXTI9_5里只处理线 8 也行 |
| P2.2 | 亲眼看抖动 | 把KEY_LOCK_MS改成0,快速连按同一个键 10 次;看完再改回200 | 改 0 时灯会多翻好几下(一次按动翻两三次),改回 200 后恢复正常 |
| P2.3 | 改成轮询版 | 用工程自带的HARDWARE/KEY/key.c:while (1)里key = KEY_Scan(0);判断KEY0_PRES~WKUP_PRES;去掉 NVIC 配置与三个中断入口函数 | 按键照样能翻转灯;再把KEY_Scan里的delay_ms(10)注释掉,快速按几下看会不会变“脏” |
| P2.4 | 加一声“嘀”(选做) | 本板蜂鸣器接 PB8、低电平响(课时01 已实测):按键时HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET),100ms 后写回GPIO_PIN_SET | 每按一次“嘀”一声,同时 8 个灯翻转;注意别在中断里用delay_ms等 100ms |
| P2.5 | 挑战:把边沿改成双边沿 | 把 WK_UP 配成GPIO_MODE_IT_RISING_FALLING(下拉不变),按住、松开各看一次 | 按住翻一次、松开又翻一次——直观体会“边沿触发”与“电平”的差别 |
五、大作业(实验二:4按键控制 LED 亮灭)
本作业对应课程大纲里的实验二「4按键控制 LED 亮灭」,当堂完成、当堂验收;做不完的带回宿舍接着做,下次课开课先收。
5.1 任务书
把课上“任意一个键按下,8 个灯一起翻转”扩成“4 个键各管一个灯,按一下亮、再按一下灭”,4 个灯互不影响:
- 必须用本课学的外部中断(EXTI + NVIC)实现,处理都在
HAL_GPIO_EXTI_Callback()里 - 每盏灯的状态要用变量记住(例如
static u8 led_on[4];)——不要“读灯现在的电平再取反”,那是另一种写法,键与灯不是一对一的时候会立刻出错 - 键与灯的对应:KEY0 → DS0、KEY1 → DS1、KEY2 → DS2、WK_UP → DS3;其余四个灯(DS4~DS7)保持全灭
- 保留 200ms 锁定消抖,同一个键连按 10 次,该灯亮灭各 5 次
- 编译输出
0 Error(s), 0 Warning(s)
5.2 分步提示
- 先想清“谁管谁”:
GPIO_PIN_8(KEY0)→ 第 0 号灯,GPIO_PIN_9(KEY1)→ 第 1 号灯,GPIO_PIN_2(KEY2)→ 第 2 号灯,GPIO_PIN_0(WK_UP)→ 第 3 号灯 - 回调里先认出是哪个键:用
GPIO_Pin判断,得到一个序号n = 0~3 - 用数组记状态:
static u8 led_on[4] = {0, 0, 0, 0};,翻转led_on[n] = !led_on[n]; - 按状态写电平:
HAL_GPIO_WritePin(GPIOC, (uint16_t)(1U << n), led_on[n] ? GPIO_PIN_RESET : GPIO_PIN_SET);——本板低电平点亮,别写反 - 编译下载,4 个键各按两轮,确认互不干扰;再做 P2.2 那样的快速连按,确认不会多翻
HAL_GPIO_EXTI_Callback()改写后的样子(关键部分,其余保持课上代码不变):
staticu8 led_on[4]={0,0,0,0};/* DS0~DS3 各自的状态:0 = 灭,1 = 亮 */voidHAL_GPIO_EXTI_Callback(uint16_tGPIO_Pin){staticuint32_tlast_tick=0;uint32_tnow=HAL_GetTick();u8 n;if(now-last_tick<KEY_LOCK_MS)return;/* 消抖:200ms 内不重复响应 */last_tick=now;if(GPIO_Pin==GPIO_PIN_8)n=0;/* KEY0 -> DS0 */elseif(GPIO_Pin==GPIO_PIN_9)n=1;/* KEY1 -> DS1 */elseif(GPIO_Pin==GPIO_PIN_2)n=2;/* KEY2 -> DS2 */elsen=3;/* WK_UP -> DS3 */led_on[n]=!led_on[n];/* 翻转这个灯的状态 */HAL_GPIO_WritePin(GPIOC,(uint16_t)(1U<<n),led_on[n]?GPIO_PIN_RESET:GPIO_PIN_SET);/* 低电平点亮 */}5.3 现象与验收标准
| 验收项 | 标准 |
|---|---|
| 单键 | 按一下 KEY0:DS0 亮;再按一下:DS0 灭 |
| 互相独立 | 4 个键各按两轮,4 个灯各自亮灭,互不影响(DS1 亮着时按 KEY0,DS1 不受影响) |
| 连按 | 同一个键连按 10 次,该灯亮灭各 5 次,不多翻、不跳 |
| 消抖 | 按得快、按得慢,都不出现“一次按动翻两次” |
| 其余灯 | DS4~DS7 全程全灭 |
| 编译下载 | 0 Error(s), 0 Warning(s);Programming Done. Verify OK. |
5.4 提交物
- 代码:
课时文档/代码/课时02-GPIO 输入与外部中断-按键/按键实验/USER/main.c - 现象:一段 20 秒以内的视频(4 个键各按两轮,能让老师看清“哪个键亮了哪个灯”),或按动前后的两张照片
- 一句话说明:“灯的状态记在哪个变量里?为什么不能靠读灯的电平再取反来做?”
六、本节小结与下节预告
本节把 GPIO 输入这条线完整走了一遍:4 个按键接在哪、按下是高还是低、为什么必须配上拉或下拉(KEY0/KEY1/KEY2 按下是低、WK_UP 按下是高),再用外部中断把它接到 CPU 上——HAL_GPIO_Init()配成GPIO_MODE_IT_FALLING/..._RISING、HAL_NVIC_EnableIRQ()放行通道、EXTIx_IRQHandler()当入口、HAL_GPIO_EXTI_Callback()干活,最后只剩一句“翻转 8 个灯”。三句话记住本课:电平配反了,现象就拧着来(按住才响应、或松手才翻);中断有三段绑定(引脚号 → EXTI 线号 → NVIC 通道号),缺一段就是“编译没报错、按键没反应”;中断里要短,消抖用时间戳锁定,绝不在中断里delay_ms。下节进入串口通信:把板子里的信息打到电脑上看(USART + printf 重定向)。
本节函数与宏速查
| 名称 | 一句话作用 |
|---|---|
__HAL_RCC_GPIOA_CLK_ENABLE()等 | 给按键所在端口开时钟(PA0 / PC8 / PC9 / PD2 三个端口) |
HAL_GPIO_Init(端口, &结构体) | 配成外部中断模式:GPIO_MODE_IT_FALLING/..._RISING+ 上拉/下拉(F1 上它会自动开 AFIO 时钟) |
HAL_GPIO_ReadPin(端口, 引脚) | 读引脚电平(轮询版用,厂家key.c里就是这么读的) |
HAL_NVIC_SetPriority(通道, 抢占, 子) | 给中断排优先级(本课统一用 2,数值越大越低) |
HAL_NVIC_EnableIRQ(通道) | 放行通道:EXTI0_IRQn/EXTI2_IRQn/EXTI9_5_IRQn |
EXTIx_IRQHandler() | 中断入口函数(向量表按名字找它),里面调HAL_GPIO_EXTI_IRQHandler(引脚) |
HAL_GPIO_EXTI_IRQHandler(引脚) | HAL 提供的“清中断标志 + 转回调” |
HAL_GPIO_EXTI_Callback(引脚) | 弱函数,我们在main.c里重写:消抖与翻转灯都在这里 |
HAL_GetTick() | 开机以来的毫秒数(1ms 时基),本课用它做 200ms 锁定消抖 |
LED_ALL_ON()/LED_ALL_OFF() | 8 个灯一起亮 / 一起灭(沿用课时01 的写法) |
附录
A. 怎么编译下载(照着做就能跑)
1. 上电接线:板子 USB 供电 → ST-Link 接 SWD 三根线(SWDIO / SWCLK / GND)→ ST-Link 插电脑 2. Keil 打开:课时文档/代码/课时02-GPIO 输入与外部中断-按键/按键实验/USER/KEY.uvprojx 3. 选下载器:Options for Target → Debug → ST-Link Debugger → OK 4. 看参数 :main.c 开头的 #define KEY_LOCK_MS 200(消抖锁定时间,改小=响应快但容易多翻) 5. 编译 :Build(F7),确认 0 Error(s), 0 Warning(s) 6. 下载 :Download(F8),确认 Programming Done. Verify OK. 7. 复位 :按 RST 键,8 个灯全灭;按任意一个键,8 个灯在全亮 / 全灭之间翻转B. 本课代码
本课只改一个文件:课时文档/代码/课时02-GPIO 输入与外部中断-按键/按键实验/USER/main.c(完整代码见 2.2 节)。工程里其余文件都是厂家例程现成的,不用动:
| 文件 | 作用 |
|---|---|
USER/KEY.uvprojx | Keil 工程文件(用 Keil 打开这个) |
USER/main.c | 本课自写代码:按键 EXTI 初始化 + 三个中断入口 + 中断回调 |
USER/stm32f1xx_it.c | 中断入口的“官方住处”;本课三个 EXTI 入口写在main.c里,这个文件不用改 |
HARDWARE/KEY/key.c、key.h | 厂家按键驱动(轮询版):KEY_Init()/KEY_Scan()/KEY0_PRES等,练习 P2.3 要用 |
HARDWARE/LED/led.c、led.h | LED 驱动:PC0~PC7 配成推挽输出并上电置全灭 |
HARDWARE/BEEP/beep.c、beep.h | 蜂鸣器驱动(PB8),练习 P2.4 要用 |
SYSTEM/ | sys、delay、usart(delay_ms在这里) |
HALLIB/、CORE/ | STM32F1 HAL 库与启动文件 |
OBJ/ | 编译输出(已加入.gitignore,不进仓库) |
C. 引脚对照表(贴在板子旁边用)
| 引脚 | 板载元件 | 不按时 / 按下时 | 说明 |
|---|---|---|---|
| PC8 | KEY0 | 高 /低 | 内部上拉;按下翻转 8 个灯 |
| PC9 | KEY1 | 高 /低 | 与 KEY0 共用中断入口EXTI9_5_IRQHandler |
| PD2 | KEY2 | 高 /低 | 独立中断入口EXTI2_IRQHandler |
| PA0 | WK_UP | 低 /高 | 唯一反相;中断入口EXTI0_IRQHandler |
| PC0~PC7 | DS0~DS7 | —— | 低电平点亮(课时01 已实测) |
| PB8 | 蜂鸣器 BEEP | —— | 低电平响(课时01 已实测;练习 P2.4 用) |
相关文档
- 课程大纲(课时02 在“一、基础外设开发模块”)
- 课程文档写作规范(0.1 教学环境约定)
- 上一课:课时01 GPIO 输出:跑马灯与蜂鸣器
- 下一课:课时03 串口通信:USART 收发与 printf