{"code":1,"msg":"成功","time":"1789816322","data":{"title":"PIN 设备与外部中断：LED、按键、矩阵键盘和蜂鸣器","content":"# 第二章 PIN 设备与外部中断\r\n\r\n本章讲 GPIO：点灯、读按键、按键中断、矩阵键盘、蜂鸣器。\r\n\r\n硬件是星火一号（STM32F407ZGT6），软件用 RT-Thread Studio。\r\n\r\n---\r\n\r\n## 准备：把工程建起来\r\n\r\n在写代码之前，先把工程建好、配置打开、串口终端连上。这一套流程后面每一章都用得上，所以放在最前面讲一遍，第三章和 LED 那一章就直接引用了。\r\n\r\n### 新建工程\r\n\r\n打开 RT-Thread Studio，菜单栏点 **文件 → 新建 → RT-Thread 项目**，在弹出的对话框里切到 **「基于芯片」选项卡**，然后按下图填写：\r\n\r\n![screenshot_9a6bc92945e329fbf2790f4f607dd2f8.png](https:\/\/oss-club.rt-thread.org\/uploads\/20260918\/187c4c14f747df9ba8521bcc27252814.png)\r\n\r\n**【图 2-1】** 「新建 RT-Thread 项目」对话框，「基于芯片」选项卡\r\n\r\n各栏的含义，对照上图看一遍：\r\n\r\n| 栏位                      | 填什么               | 说明                                                      |\r\n| ------------------------- | -------------------- | --------------------------------------------------------- |\r\n| Project name              | 自己起一个           | 图中示例是 `led_test`                                     |\r\n| 使用缺省位置              | 勾上                 | 工程放在 Studio 的默认 workspace 下。不勾可以自己指定路径 |\r\n| **基于芯片 \/ 基于开发板** | 选 **基于芯片**      | 关键选项，见下面的说明                                    |\r\n| RT-Thread                 | `5.2.2`              | 按你装的版本选                                            |\r\n| 厂商                      | `STMicroelectronics` |                                                           |\r\n| 系列                      | `STM32F4`            |                                                           |\r\n| 子系列                    | `STM32F407`          |                                                           |\r\n| 芯片                      | `STM32F407ZG`        | 星火一号用的就是这颗                                      |\r\n| 控制台串口                | `UART1`              | 调试打印和 msh 命令行走这个串口                           |\r\n| 发送脚 \/ 接收脚           | `PA9` \/ `PA10`       | 选了 UART1 后自动填好，不用改                             |\r\n| 调试器                    | `ST-LINK`            | 按你手上实际的调试器选                                    |\r\n| 接口                      | `SWD`                |                                                           |\r\n\r\n**为什么要选「基于芯片」**：这个选项卡决定了工程里只有芯片、没有任何板级外设定义 —— `board\/` 目录下不会有 LED、按键这些东西。后面所有引脚和驱动都要自己配，这也正是本教程希望的教学方式。\r\n\r\n填完点 **完成**，Studio 会生成工程并自动开始第一次编译。\r\n\r\n> **注意对话框底部那句提示：「基于芯片新建工程后建议：工程使用的是芯片内部 HSI 时钟，如需修改，请完善 drv_clk.c」。**\r\n>\r\n> 这是基于芯片建工程与基于 BSP 建工程的一个实际差别：新建出来的芯片工程默认跑**内部 HSI** 时钟，不是外部晶振。如果你需要精确的波特率分频、或者要用到 USB \/ 以太网这类对时钟精度敏感的外设，就得自己把时钟树配成外部晶振（星火一号板上有 8MHz 晶振），并按提示去完善 `drv_clk.c`。**而基于 BSP 的工程，时钟已经在 `board.c` 里配好了，不用管。**\r\n>\r\n> 本章的 GPIO 和第三章的串口用默认 HSI 也能跑；但如果你发现串口波特率有偏差导致偶尔乱码，先回来检查这一处。\r\n\r\n工程建好后，左侧项目树里能看到这些目录：\r\n\r\n```\r\n你的工程名\/\r\n├── applications\/          # 你的代码写在这里\r\n├── board\/                 # 板级支持包（芯片级初始化）\r\n├── libraries\/             # HAL 库和驱动\r\n├── rt-thread\/             # 内核与组件\r\n├── Kconfig  rtconfig.h    # 配置菜单与配置结果\r\n└── SConscript             # 构建脚本\r\n```\r\n\r\n### 打开 RT-Thread Settings 做配置\r\n\r\n1. 双击工程里的 **RT-Thread Settings**，右侧会打开图形化配置界面。\r\n\r\n2. 按下面的路径勾选本章需要的项：\r\n\r\n   | 位置                                 | 勾选项                |\r\n   | ------------------------------------ | --------------------- |\r\n   | RT-Thread Components → 设备驱动      | 使用 PIN 设备驱动程序 |\r\n   | RT-Thread Components → Command shell | 使能（Shell 命令行）  |\r\n\r\n3. 点左上角的 **保存** 按钮。Studio 会自动更新 `rtconfig.h` 并触发重新编译。\r\n\r\n> 保存之后可以打开工程根目录的 `rtconfig.h` 看一眼，确认里面有 `RT_USING_PIN`、`RT_USING_FINSH`、`BSP_USING_GPIO` 这几个宏。**`BSP_USING_GPIO` 不在「设备驱动」分类下，而在 BSP 的片上外设分类里**，很容易漏，漏了的表现见后面的常见问题。\r\n\r\n### 编译和下载\r\n\r\n4. 点工具栏的 **编译** 按钮（锤子图标）编译工程。下面 Console 窗口会输出编译过程，最后出现 `Build Finished` 一类的提示、且没有 error 才算通过。\r\n\r\n5. 用 USB 线把星火一号接到电脑，点 **下载** 按钮烧录。\r\n\r\n### 打开串口终端\r\n\r\n6. 星火一号的板载 ST-Link 已经引出了虚拟串口，插上 USB 后设备管理器里会多出一个 COM 口（名字里通常带 STLink 或 STMicroelectronics）。\r\n\r\n7. 用 Studio 内置的终端，或者用外部串口助手，打开上面那个 COM 口，参数设成 **115200-8-N-1**。\r\n\r\n8. 复位一下板子。终端里应该出现 RT-Thread 的启动 banner 和 `msh >` 提示符。**看到这个提示符，说明工程能跑、串口通了**，可以开始写代码了。\r\n\r\n> 后面每章的操作步骤都会用到这套环境。如果卡在某一步，先对照第 2.8 节的常见问题排查一遍。\r\n\r\n---\r\n\r\n## 2.1 板子上有什么\r\n\r\n先把本章会用到的引脚列出来，后面的代码直接引用这些名字。\r\n\r\n| 功能     | 引脚          | 说明                                               |\r\n| -------- | ------------- | -------------------------------------------------- |\r\n| LED_R    | PF12          | 低电平点亮                                         |\r\n| LED_B    | PF11          | 低电平点亮                                         |\r\n| KEY0     | PC0           | 按下为低                                           |\r\n| KEY1     | PC1           | 按下为低                                           |\r\n| KEY2     | PC4           | 按下为低                                           |\r\n| WK_UP    | PC5           | 按下为低                                           |\r\n| BEEP     | PB0           | 高电平响                                           |\r\n| 矩阵键盘 | 走 PMOD1 接口 | 行 PA4 \/ PA7 \/ PA6 \/ PA5，列 PE2 \/ PE5 \/ PE3 \/ PE4 |\r\n\r\n这些定义跟 SDK 里的官方例程（`02_basic_led_blink`、`02_basic_key`、`02_basic_irq_beep`、`07_module_key_matrix`）是一致的，可以直接对着抄。\r\n\r\n有一点要先说明：**LED 是低电平点亮的。** 灯珠的阳极经限流电阻接 3.3V，阴极接到单片机引脚上，所以只有引脚输出低电平时才有电流流过。如果习惯性地认为\"高电平 = 亮\"，写出来的代码不会点亮任何东西。这一点看原理图就能确认，不要凭习惯推断。\r\n\r\n---\r\n\r\n## 2.2 PIN 设备框架\r\n\r\n裸机点灯的代码换一颗芯片后基本无法复用，因为要开时钟、配模式寄存器、写数据寄存器，各家的寄存器名称和地址都不一样。RT-Thread 把\"操作一根引脚\"也抽象成了设备，应用层只认一套接口：\r\n\r\n```\r\n你的代码            rt_pin_mode \/ rt_pin_write \/ rt_pin_read\r\n    │\r\nPIN 设备框架        统一接口（components\/drivers\/misc\/pin.c）\r\n    │\r\n驱动                drv_gpio.c（这里才真正去写寄存器）\r\n    │\r\n硬件                STM32F407 的 GPIOF\r\n```\r\n\r\n应用层最常用的就三个函数：\r\n\r\n| 函数                       | 作用         |\r\n| -------------------------- | ------------ |\r\n| `rt_pin_mode(pin, mode)`   | 配置引脚模式 |\r\n| `rt_pin_write(pin, value)` | 输出电平     |\r\n| `rt_pin_read(pin)`         | 读电平       |\r\n\r\n引脚用 `GET_PIN` 表示，两个参数是端口和引脚号：\r\n\r\n```c\r\n#define PIN_LED_R   GET_PIN(F, 12)   \/\/ PF12\r\n```\r\n\r\n它内部算的是\"端口序号 × 16 + 引脚号\"，所以 PF12 的值是 5×16+12 = 92。之所以要了解这个换算，是因为 msh 的 `pin` 命令会直接打印引脚编号，不知道来历的话不容易和具体引脚对应上。\r\n\r\n模式常用的就这几个：\r\n\r\n```c\r\nPIN_MODE_OUTPUT          \/\/ 推挽输出，驱动 LED 用这个\r\nPIN_MODE_INPUT           \/\/ 浮空输入\r\nPIN_MODE_INPUT_PULLUP    \/\/ 上拉输入，按键用这个\r\nPIN_MODE_INPUT_PULLDOWN  \/\/ 下拉输入\r\nPIN_MODE_OUTPUT_OD       \/\/ 开漏输出，I2C 那种总线才用\r\n```\r\n\r\n顺带一句：**不用手动开 GPIO 时钟。** `rt_hw_pin_init()` 在系统启动时已经把 GPIOA~GPIOK 的时钟全打开了，这是框架替你干的活，裸机习惯带过来反而会多写一行没用的代码。\r\n\r\n---\r\n\r\n## 2.3 点灯\r\n\r\n新建 `applications\/led_app.c`，写一个 1 秒周期的闪烁。红灯接 PF12。\r\n\r\n```c\r\n#include <bsp_system.h>\r\n\r\n#define PIN_LED_R   GET_PIN(F, 12)   \/\/ PF12 : LED_R\r\n\r\nint main(void)\r\n{\r\n    rt_pin_mode(PIN_LED_R, PIN_MODE_OUTPUT);\r\n\r\n    while (1)\r\n    {\r\n        rt_pin_write(PIN_LED_R, PIN_LOW);    \/\/ 低电平点亮\r\n        LOG_D(\"led on\");\r\n        rt_thread_mdelay(500);\r\n\r\n        rt_pin_write(PIN_LED_R, PIN_HIGH);   \/\/ 高电平熄灭\r\n        LOG_D(\"led off\");\r\n        rt_thread_mdelay(500);\r\n    }\r\n\r\n    return 0;\r\n}\r\n```\r\n\r\n编译下载后，红灯应该 1 秒一闪，同时串口终端里能看到 `led on` \/ `led off` 交替打印。\r\n\r\n想验证硬件好坏，可以先把闪烁代码停掉，直接用 msh 命令行手动操作引脚：\r\n\r\n```\r\nmsh >pin mode PF.12 output\r\nmsh >pin write PF.12 low      ← 红灯亮\r\nmsh >pin write PF.12 high     ← 红灯灭\r\nmsh >pin read PF.12\r\n```\r\n\r\n> 注意：`led_proc` 之类的线程每 500ms 就会把引脚电平写回去，`pin` 命令的效果会被覆盖。想干净地用 `pin` 命令测硬件，先把启动线程那几行注释掉。\r\n\r\n`pin read` 打印的 `on` \/ `off` 说的是**引脚电平**，不是灯亮灭。所以写完 `low` 之后红灯是亮的，终端却打印 `off`，这里很容易理解反。无法确认时用万用表测量引脚对地电压：0V 即低电平。\r\n\r\n---\r\n\r\n## 2.4 按键：先试轮询\r\n\r\nKEY0 接 PC0，按下时引脚接地。按键引脚配成上拉输入，这样不按的时候读到高、按下读到低，不用外接上拉电阻。\r\n\r\n机械按键按下去的时候触点会抖动几毫秒，电平在 0 和 1 之间跳，所以读到低之后要等一小会儿再读一次确认。\r\n\r\n```c\r\n#include <bsp_system.h>\r\n\r\n#define PIN_LED_R   GET_PIN(F, 12)\r\n#define PIN_KEY0    GET_PIN(C, 0)    \/\/ PC0 : KEY0\r\n\r\nint main(void)\r\n{\r\n    rt_pin_mode(PIN_LED_R, PIN_MODE_OUTPUT);\r\n    rt_pin_mode(PIN_KEY0,  PIN_MODE_INPUT_PULLUP);\r\n\r\n    while (1)\r\n    {\r\n        if (rt_pin_read(PIN_KEY0) == PIN_LOW)\r\n        {\r\n            rt_thread_mdelay(20);                    \/\/ 消抖\r\n            if (rt_pin_read(PIN_KEY0) == PIN_LOW)    \/\/ 还是低，说明真按下了\r\n            {\r\n                LOG_D(\"KEY0 pressed!\");\r\n                rt_pin_write(PIN_LED_R, PIN_LOW);    \/\/ 点亮\r\n            }\r\n        }\r\n        else\r\n        {\r\n            rt_pin_write(PIN_LED_R, PIN_HIGH);       \/\/ 松开就灭\r\n        }\r\n\r\n        rt_thread_mdelay(10);\r\n    }\r\n\r\n    return 0;\r\n}\r\n```\r\n\r\n这个写法可行，但代价明显：主循环必须持续轮询引脚，即使没有任何按键动作也在占用 CPU。而且按键响应的快慢完全取决于这个循环的周期。这正是下一节的中断要解决的问题。\r\n\r\n下载后按住 KEY0，红灯亮，松开熄灭；终端里会打印 `KEY0 pressed!`。\r\n\r\n---\r\n\r\n## 2.5 换中断：有事再叫我\r\n\r\nGPIO 电平变化时，硬件可以直接跳进中断服务程序，CPU 不用轮询。RT-Thread 里挂中断就两个调用：\r\n\r\n```c\r\nrt_pin_attach_irq(引脚, 触发模式, 回调函数, 参数);\r\nrt_pin_irq_enable(引脚, PIN_IRQ_ENABLE);\r\n```\r\n\r\n触发模式有这几个，注意宏名，写错直接编译不过：\r\n\r\n```c\r\nPIN_IRQ_MODE_RISING           \/\/ 上升沿\r\nPIN_IRQ_MODE_FALLING          \/\/ 下降沿\r\nPIN_IRQ_MODE_RISING_FALLING   \/\/ 双边沿\r\nPIN_IRQ_MODE_HIGH_LEVEL       \/\/ 高电平\r\nPIN_IRQ_MODE_LOW_LEVEL        \/\/ 低电平\r\n```\r\n\r\n常见笔误是写成 `PIN_IRQ_MODE_HIGH` \/ `PIN_IRQ_MODE_LOW`，这两个名字不存在。\r\n\r\nSTM32F407 这边还有两个约束，调代码时经常碰到：\r\n\r\n**第一，同一个编号的引脚不能同时用作中断源。** F407 有 EXTI0~EXTI15 共 16 条外部中断线，编号与引脚号一一对应。具体是哪一组端口的第 n 脚，由 `SYSCFG_EXTICR` 寄存器选择，一条线同一时刻只能选中一个端口。RT-Thread 的驱动层还有一层限制——回调表是按**引脚号**索引的，与端口无关：\r\n\r\n```c\r\nrt_pin_attach_irq(GET_PIN(A, 5), PIN_IRQ_MODE_FALLING, cb_a, RT_NULL);\r\nrt_pin_attach_irq(GET_PIN(B, 5), PIN_IRQ_MODE_FALLING, cb_b, RT_NULL);  \/\/ cb_a 被覆盖\r\n```\r\n\r\n所以选按键引脚的时候把编号错开，比如 KEY1 用 PC1、WK_UP 用 PC5。\r\n\r\n**第二，电平触发在 STM32 上未实现。** `drv_gpio.c` 里那个 switch 只处理了 RISING \/ FALLING \/ RISING_FALLING 三种，没有 default 分支。传 `*_LEVEL` 进去编译不会报错，但会用一个未初始化的 `GPIO_InitTypeDef` 去配置引脚，行为是未定义的（undefined behavior）。**实际使用只限边沿触发。**\r\n\r\n另外一个有用的细节：驱动在配置 FALLING 时会自动把引脚设为内部上拉，配置 RISING 时自动设为下拉。因此\"按下接地\"的按键电路配 FALLING 是匹配的。\r\n\r\n### 写中断回调\r\n\r\n需求：按 WK_UP（PC5）蜂鸣器响，按 KEY1（PC1）蜂鸣器停。\r\n\r\n```c\r\n#include <bsp_system.h>\r\n\r\n#define PIN_KEY1    GET_PIN(C, 1)   \/\/ PC1 : KEY1\r\n#define PIN_WK_UP   GET_PIN(C, 5)   \/\/ PC5 : WK_UP\r\n#define PIN_BEEP    GET_PIN(B, 0)   \/\/ PB0 : BEEP\r\n\r\nvoid irq_callback(void *args)\r\n{\r\n    rt_uint32_t sign = (rt_uint32_t)args;\r\n\r\n    switch (sign)\r\n    {\r\n    case PIN_WK_UP:\r\n        rt_pin_write(PIN_BEEP, PIN_HIGH);   \/\/ 响\r\n        break;\r\n    case PIN_KEY1:\r\n        rt_pin_write(PIN_BEEP, PIN_LOW);    \/\/ 停\r\n        break;\r\n    default:\r\n        break;\r\n    }\r\n}\r\n\r\nint main(void)\r\n{\r\n    rt_pin_mode(PIN_KEY1,  PIN_MODE_INPUT_PULLUP);\r\n    rt_pin_mode(PIN_WK_UP, PIN_MODE_INPUT_PULLUP);\r\n    rt_pin_mode(PIN_BEEP,  PIN_MODE_OUTPUT);\r\n\r\n    rt_pin_attach_irq(PIN_KEY1,  PIN_IRQ_MODE_FALLING, irq_callback, (void *)PIN_KEY1);\r\n    rt_pin_attach_irq(PIN_WK_UP, PIN_IRQ_MODE_FALLING, irq_callback, (void *)PIN_WK_UP);\r\n\r\n    rt_pin_irq_enable(PIN_KEY1,  PIN_IRQ_ENABLE);\r\n    rt_pin_irq_enable(PIN_WK_UP, PIN_IRQ_ENABLE);\r\n\r\n    while (1)\r\n    {\r\n        rt_thread_mdelay(1000);\r\n    }\r\n\r\n    return 0;\r\n}\r\n```\r\n\r\n下载后按 WK_UP 蜂鸣器响，按 KEY1 蜂鸣器停。\r\n\r\n回调里不能做耗时操作。不能用 `rt_thread_mdelay`，也不能大段 `rt_kprintf`（打印内部有锁，在中断上下文里调用可能死锁）。回调里只允许做三件事之一：置标志位、写一个 IO、发信号量唤醒线程。上面这个例子只写了一个 IO，符合这个约束；再复杂的逻辑就得用 2.7 节的做法。\r\n\r\n还有一点：按键抖动会让一次按下触发多次下降沿。这个例子里\"响\"和\"停\"是幂等操作，所以看不出问题；但如果要做\"按一次计数加一\"，就会一次加上好几下。这类场景需要在回调里做时间过滤，或者改用 2.7 节\"信号量 + 线程\"的结构在线程侧消抖。\r\n\r\n---\r\n\r\n## 2.6 4×4 矩阵键盘\r\n\r\n16 个按键如果每个占用一个 IO，需要 16 根引脚，开销过大。矩阵键盘把按键放在 4 行 4 列的交叉点上，只需要 8 个 IO。\r\n\r\n```\r\n      COL1  COL2  COL3  COL4\r\nROW1 ──┬────┬────┬────┬────\r\n       [1]  [2]  [3]  [A]\r\nROW2 ──┬────┬────┬────┬────\r\n       [4]  [5]  [6]  [B]\r\nROW3 ──┬────┬────┬────┬────\r\n       [7]  [8]  [9]  [C]\r\nROW4 ──┬────┬────┬────┬────\r\n       [*]  [0]  [#]  [D]\r\n```\r\n\r\n读法是逐行扫描：行线设成输出、默认全高，列线设成上拉输入。然后一行一行地把当前行拉低，读四根列线，哪一列变成低了，说明这一行这一列交叉点上那个键被按下了。\r\n\r\n引脚在星火一号的 PMOD1 接口上：\r\n\r\n| 行列      | 引脚                  |\r\n| --------- | --------------------- |\r\n| ROW1~ROW4 | PA4 \/ PA7 \/ PA6 \/ PA5 |\r\n| COL1~COL4 | PE2 \/ PE5 \/ PE3 \/ PE4 |\r\n\r\n```c\r\n#include <bsp_system.h>\r\n\r\n#define ROWS 4\r\n#define COLS 4\r\n\r\n#define PIN_ROW1  GET_PIN(A, 4)\r\n#define PIN_ROW2  GET_PIN(A, 7)\r\n#define PIN_ROW3  GET_PIN(A, 6)\r\n#define PIN_ROW4  GET_PIN(A, 5)\r\n#define PIN_COL1  GET_PIN(E, 2)\r\n#define PIN_COL2  GET_PIN(E, 5)\r\n#define PIN_COL3  GET_PIN(E, 3)\r\n#define PIN_COL4  GET_PIN(E, 4)\r\n\r\nstatic rt_uint32_t ROW_PINS[ROWS] = {PIN_ROW1, PIN_ROW2, PIN_ROW3, PIN_ROW4};\r\nstatic rt_uint32_t COL_PINS[COLS] = {PIN_COL1, PIN_COL2, PIN_COL3, PIN_COL4};\r\n\r\nvoid key_scan_init(void)\r\n{\r\n    int i;\r\n\r\n    for (i = 0; i < ROWS; i++)\r\n    {\r\n        rt_pin_mode(ROW_PINS[i], PIN_MODE_OUTPUT);\r\n        rt_pin_write(ROW_PINS[i], PIN_HIGH);              \/\/ 行默认全高\r\n    }\r\n    for (i = 0; i < COLS; i++)\r\n    {\r\n        rt_pin_mode(COL_PINS[i], PIN_MODE_INPUT_PULLUP);  \/\/ 列上拉输入\r\n    }\r\n}\r\n\r\n\/\/ 返回 1~16，没按键返回 -1\r\nint key_scan(void)\r\n{\r\n    int row, col;\r\n\r\n    for (row = 0; row < ROWS; row++)\r\n    {\r\n        rt_pin_write(ROW_PINS[row], PIN_LOW);             \/\/ 当前行拉低\r\n\r\n        for (col = 0; col < COLS; col++)\r\n        {\r\n            if (rt_pin_read(COL_PINS[col]) == PIN_LOW)\r\n            {\r\n                rt_thread_mdelay(15);                     \/\/ 消抖\r\n                while (rt_pin_read(COL_PINS[col]) == PIN_LOW);  \/\/ 等松开\r\n                rt_pin_write(ROW_PINS[row], PIN_HIGH);\r\n                return row * COLS + col + 1;\r\n            }\r\n        }\r\n\r\n        rt_pin_write(ROW_PINS[row], PIN_HIGH);            \/\/ 恢复，扫下一行\r\n    }\r\n\r\n    return -1;\r\n}\r\n\r\nint main(void)\r\n{\r\n    const char map[] = \"123A456B789C*0#D\";\r\n\r\n    key_scan_init();\r\n\r\n    while (1)\r\n    {\r\n        int key = key_scan();\r\n\r\n        if (key > 0)\r\n        {\r\n            LOG_D(\"Key: %c\", map[key - 1]);\r\n        }\r\n\r\n        rt_thread_mdelay(15);\r\n    }\r\n\r\n    return 0;\r\n}\r\n```\r\n\r\n逐行扫描的好处是不会出现\"两列同时按下短路\"的问题，因为任何时刻只有一行是低电平。\r\n\r\n下载后依次按下 16 个键，终端里会打印对应的字符：\r\n\r\n```\r\n[D\/main] Key: 1\r\n[D\/main] Key: 2\r\n...\r\n[D\/main] Key: D\r\n```\r\n\r\n不过 `while (...== PIN_LOW);` 这句是忙等，等松开期间 CPU 全速空转。教学代码这样写没问题，实际项目里最好改成定时轮询或者用中断。另外这段只支持单键，同时按两个会返回先扫到的那个。\r\n\r\n---\r\n\r\n## 2.7 中断里别干活：用信号量叫线程\r\n\r\n2.5 那个例子里回调直接操作蜂鸣器，只适用于\"动作简单、立即返回\"的场合。真实项目中按键往往要触发一串后续逻辑——改状态机、刷新界面、上报数据，这些都必须在线程上下文里做。\r\n\r\n标准套路是：中断回调里只发一个信号量，线程收到信号量再干活。\r\n\r\n```c\r\n#include <bsp_system.h>\r\n\r\n#define PIN_LED_R   GET_PIN(F, 12)\r\n#define PIN_WK_UP   GET_PIN(C, 5)\r\n\r\nstatic rt_sem_t key_sem;\r\n\r\n\/\/ 中断回调：只发信号量\r\nvoid wkup_irq_callback(void *args)\r\n{\r\n    rt_sem_release(key_sem);\r\n}\r\n\r\n\/\/ 线程：干真正要干的事\r\nstatic void key_thread_entry(void *param)\r\n{\r\n    while (1)\r\n    {\r\n        rt_sem_take(key_sem, RT_WAITING_FOREVER);   \/\/ 没信号就睡着\r\n\r\n        LOG_D(\"WK_UP pressed\");\r\n        rt_pin_write(PIN_LED_R, PIN_LOW);\r\n        rt_thread_mdelay(300);                      \/\/ 线程里睡多久都行\r\n        rt_pin_write(PIN_LED_R, PIN_HIGH);\r\n    }\r\n}\r\n\r\nint main(void)\r\n{\r\n    rt_thread_t tid;\r\n\r\n    rt_pin_mode(PIN_WK_UP, PIN_MODE_INPUT_PULLUP);\r\n    rt_pin_mode(PIN_LED_R, PIN_MODE_OUTPUT);\r\n\r\n    key_sem = rt_sem_create(\"key_sem\", 0, RT_IPC_FLAG_PRIO);\r\n\r\n    tid = rt_thread_create(\"key\", key_thread_entry, RT_NULL, 1024, 20, 10);\r\n    if (tid != RT_NULL)\r\n    {\r\n        rt_thread_startup(tid);\r\n    }\r\n\r\n    rt_pin_attach_irq(PIN_WK_UP, PIN_IRQ_MODE_FALLING, wkup_irq_callback, RT_NULL);\r\n    rt_pin_irq_enable(PIN_WK_UP, PIN_IRQ_ENABLE);\r\n\r\n    return 0;\r\n}\r\n```\r\n\r\n这个模式以后会反复用到——串口收到数据、定时器到点、各种外设事件，全都是\"中断发信号量 + 线程干活\"。\r\n\r\n---\r\n\r\n## 2.8 小结\r\n\r\n| 做什么   | 怎么做                                                       |\r\n| -------- | ------------------------------------------------------------ |\r\n| 引脚编号 | `GET_PIN(端口, 引脚)`，值是\"端口×16 + 引脚号\"                |\r\n| 输出     | `rt_pin_mode(pin, PIN_MODE_OUTPUT)` + `rt_pin_write`         |\r\n| 输入     | `rt_pin_mode(pin, PIN_MODE_INPUT_PULLUP)` + `rt_pin_read`    |\r\n| 消抖     | 读到低 → 延时 10~50ms → 再读一次确认                         |\r\n| 外部中断 | `rt_pin_attach_irq(.., PIN_IRQ_MODE_FALLING, cb, args)` + `rt_pin_irq_enable` |\r\n| 中断限制 | 同编号引脚不能同时中断；电平触发在 STM32 上没实现            |\r\n| 中断规矩 | 回调里不阻塞、不延时、不打印，只置标志或发信号量             |\r\n| 矩阵键盘 | 行输出列输入，逐行拉低判列，键值 = 行×4 + 列 + 1             |\r\n| 本章引脚 | LED：PF11\/PF12（低电平亮）；按键：PC0\/PC1\/PC4\/PC5；蜂鸣器：PB0 |","updatetime":"2026-09-18 08:34:28","author":"wzl1900843840"}}