课时02 嵌入式技术应用设计实训 | GPIO 输入与外部中断:按键

嵌入式技术应用设计实训 | 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 个键各管一个灯、按一下亮再按一下灭"。

本节目标

  1. 能说出读一个按键要按"开时钟 → 配输入(上拉/下拉)→ 读电平"三步走,并说清 KEY0/KEY1/KEY2 为什么配上拉、WK_UP 为什么配下拉
  2. 会配置 EXTI + NVIC(含中断入口函数与回调函数),让按任意一个键都能把 8 个 LED 在"全亮 / 全灭"之间翻转一次
  3. 能说出"引脚号 → 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 库的中断调用链(记住这三步就够):

text 复制代码
引脚电平出现下降沿/上升沿(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。工程里其余文件都是厂家例程现成的,不用动:

text 复制代码
课时文档/代码/课时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 的完整代码(一个文件,一个现象):

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 写法一致) ---------- */
#define LED_ALL_OFF()  HAL_GPIO_WritePin(GPIOC, (uint16_t)0x00FF, GPIO_PIN_SET)    /* 8 个灯全灭 */
#define LED_ALL_ON()   HAL_GPIO_WritePin(GPIOC, (uint16_t)0x00FF, GPIO_PIN_RESET)  /* 8 个灯全亮 */

/* ---------- 消抖:同一个键两次响应的最小间隔(毫秒),觉得太钝就改小 ---------- */
#define KEY_LOCK_MS    200

static u8 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()
   ------------------------------------------------------------------ */
void EXTI0_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);                 /* WK_UP */
}

void EXTI2_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_2);                 /* KEY2  */
}

void EXTI9_5_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_8 | GPIO_PIN_9);    /* KEY0 / KEY1 */
}

/* ------------------------------------------------------------------
   按键中断初始化:4 个按键全部配成"外部中断"模式
   输入三步一样不能少:开时钟 -> 配模式(含上拉/下拉、触发边沿)-> 使能中断
   注意 WK_UP 与另外三个相反:按下是高电平,所以配下拉 + 上升沿
   ------------------------------------------------------------------ */
static void KEY_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 个灯
   ------------------------------------------------------------------ */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    static uint32_t last_tick = 0;      /* 上一次响应的时间戳 */
    uint32_t now = HAL_GetTick();       /* 开机以来的毫秒数 */

    if (now - last_tick < KEY_LOCK_MS) return;   /* 200ms 内再触发:判为抖动,丢掉 */
    last_tick = now;

    all_on = !all_on;                   /* 翻转状态 */
    if (all_on) LED_ALL_ON();
    else        LED_ALL_OFF();
}

int main(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 已连好)

操作步骤:

  1. Keil 打开工程:课时文档/代码/课时02-GPIO 输入与外部中断-按键/按键实验/USER/KEY.uvprojx
  2. 选下载器:Options for Target → Debug → ST-Link Debugger → OK(课时00 已设过,换电脑要重设)
  3. 看代码:确认 main.c 里的 KEY_LOCK_MS 是 200(消抖锁定时间)
  4. 编译:Build(F7),确认 0 Error(s), 0 Warning(s)
  5. 下载:Download(F8),确认 Programming Done. Verify OK.
  6. 按复位键 RST(或重新上电):8 个灯全灭,程序就绪
  7. 依次按 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 个灯互不影响:

  1. 必须用本课学的外部中断(EXTI + NVIC)实现,处理都在 HAL_GPIO_EXTI_Callback() 里
  2. 每盏灯的状态要用变量 记住(例如 static u8 led_on[4];)------不要"读灯现在的电平再取反",那是另一种写法,键与灯不是一对一的时候会立刻出错
  3. 键与灯的对应:KEY0 → DS0、KEY1 → DS1、KEY2 → DS2、WK_UP → DS3;其余四个灯(DS4~DS7)保持全灭
  4. 保留 200ms 锁定消抖,同一个键连按 10 次,该灯亮灭各 5 次
  5. 编译输出 0 Error(s), 0 Warning(s)

5.2 分步提示

  1. 先想清"谁管谁" :GPIO_PIN_8(KEY0)→ 第 0 号灯,GPIO_PIN_9(KEY1)→ 第 1 号灯,GPIO_PIN_2(KEY2)→ 第 2 号灯,GPIO_PIN_0(WK_UP)→ 第 3 号灯
  2. 回调里先认出是哪个键 :用 GPIO_Pin 判断,得到一个序号 n = 0~3
  3. 用数组记状态 :static u8 led_on[4] = {0, 0, 0, 0};,翻转 led_on[n] = !led_on[n];
  4. 按状态写电平 :HAL_GPIO_WritePin(GPIOC, (uint16_t)(1U << n), led_on[n] ? GPIO_PIN_RESET : GPIO_PIN_SET);------本板低电平点亮,别写反
  5. 编译下载,4 个键各按两轮,确认互不干扰;再做 P2.2 那样的快速连按,确认不会多翻

HAL_GPIO_EXTI_Callback() 改写后的样子(关键部分,其余保持课上代码不变):

c 复制代码
static u8 led_on[4] = {0, 0, 0, 0};     /* DS0~DS3 各自的状态:0 = 灭,1 = 亮 */

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    static uint32_t last_tick = 0;
    uint32_t now = 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 */
    else if (GPIO_Pin == GPIO_PIN_9) n = 1;      /* KEY1  -> DS1 */
    else if (GPIO_Pin == GPIO_PIN_2) n = 2;      /* KEY2  -> DS2 */
    else                             n = 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. 怎么编译下载(照着做就能跑)

text 复制代码
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 用)

相关文档

相关推荐
阿钱真强道3 天前
课时00 嵌入式技术应用设计实训 | 开发环境搭建与开发板认识
git·hal库·keil·vs code·开发环境搭建·stm32f103·st-link
阿钱真强道3 天前
课时01 嵌入式技术应用设计实训 | GPIO 输出:跑马灯与蜂鸣器
hal库·keil·gpio·蜂鸣器·跑马灯·推挽输出·stm32f103
仲星(._.)6 天前
GPIO
嵌入式硬件·gpio
an123212116 天前
ARM---编译工具链|程序段|GPIO外设驱动
gcc·gpio
xxy2224119 天前
ARM裸机开发(4)GPIO与LED/按键驱动详解——引脚复用与电气特性配置
arm开发·gpio
nullregedit2 个月前
OpenHarmony 小鸿 AI 开发实战 05:烧录成功仍黑屏的 GPIO14/PWR_ON 故障复盘
openharmony·gpio·故障排查·开源鸿蒙·ws63
Industio_触觉智能3 个月前
RK3576开发板GPIO驱动入门教程,基于触觉智能开发板教学-上
嵌入式硬件·硬件工程·智能硬件·开发板·gpio·瑞芯微·rk3576
菜鸟的学习日记、4 个月前
GPIO的几种模式——以STM32为例
stm32·单片机·嵌入式硬件·gpio
FPGA_小田老师5 个月前
ZYNQ7020笔记:MIO、EMIO、GPIO的区别及应用
fpga开发·gpio·zynq·外设·mio·emio