传感器 · 按键输入 · KY-004 · SKU 待确认

KY-004 按键模块 STM32F103 接线与代码

KY-004 就是给单片机加一个「你按一下」的输入——按键按下时信号脚 S 输出一个干净的电平跳变,STM32 用 20ms 消抖过滤掉机械毛刺,只在下降沿计一次数,OLED 大字反白显示 PRESSED / RELEASED 和按下次数。它不需要外接任何电阻,模块板上已经自带一颗上拉电阻(本块实物 2026-08-20 实测为「按下 LOW、松开 HIGH」的上拉版)。本页是 STM32F103C8T6 + KY-004 + 0.96 OLED 的完整实机验证记录,含接线表、关键参数、完整代码和 PlatformIO 工程下载。最值得看的是「网上 KY-004 上拉 / 下拉版本为什么互相矛盾,本块按下到底是 HIGH 还是 LOW」和「为什么长按只算一次、10 次短按正好 +10」这两段。

Quick Facts

  • 模块:KY-004 按键模块(三针 S / + / -,板载上拉电阻)
  • SKU:待确认(本页实物已实测,商品 SKU 未回填)
  • 接线:S→PA0(INPUT_PULLUP)、+→3.3V、-→GND
  • 电平方向:本块实测上拉版:按下 LOW、松开 HIGH(不同批次可能相反,接线判定以实测为准)
  • 消抖:20ms 稳定确认;仅 HIGH→LOW 下降沿 COUNT+1,长按不重复计
  • 显示:0.96 寸 OLED,SSD1306,地址 0x3C,I2C 400kHz(SCL→PB8 / SDA→PB9)
  • 输出:串口 115200 每 400ms 一次,状态变化立即输出;PC13 LED 跟随

Verified

实机验证结果

本项目实测 下面的结果全部来自我们自己的实机测试,实测日期 2026-08-20,更新于 2026-08-20。

3 针S / + / -,按下 S 变 LOW(本块实测)
20 ms稳定确认消抖,毛刺不计数
下降沿每次按下 +1,长按不重复计
400 ms串口周期输出,状态变化立即输出

按键行为实测

测试动作OLED / 串口结果
不按按键RAW=1,STATE=RELEASED,LED 灭,COUNT 不变
按一下松开RAW 1→0→1,STATE 变 PRESSED(大字反白)再回 RELEASED,COUNT +1
连续短按 10 次COUNT 正好 +10,无多计、无漏计
长按不松按住期间 COUNT 不再增加,松开也不加数
上电时正按着按键不误计:程序以当前电平为基准进入运行页,必须松开再按才计一次
拔掉 OLED 或接触不良0x3C 探测失败,板载 LED(PC13) 100ms 快闪报警
KY-004 实拍接线与结果:模块 + 面包板 + STM32F103C8T6 + OLED 显示 KY-004 / RELEASED / COUNT 9
实拍接线 + 结果:模块、面包板、STM32F103C8T6、OLED 同框,此刻显示 RELEASED,COUNT 已累计 9 次

完整演示视频(接线 → 烧录 → 实测)

KY-004 实拍 + 动画演示成片(720P):按下 PRESSED 反白、松开 RELEASED、连续短按计数与长按只计一次的完整实测过程

OLED / 模块特写

KY-004 运行时 OLED 特写:KY-004 标题 / STATE: RELEASED / COUNT: 9
OLED 运行页特写:标题 KY-004 + STATE: RELEASED + COUNT
KY-004 按键模块特写:轻触按键、三针 S/+/-、板载 R1 丝印 102
KY-004 模块特写:三针 S / + / -,板载 R1 丝印 102

串口输出(115200,约每 400ms 一行,状态变化立即输出)

KY-004 BUTTON TEST
Firmware: v1.0

BTN | READY | RUNNING
BTN | RAW=1 | STATE=RELEASED  | COUNT=0
BTN | RAW=0 | STATE=PRESSED   | COUNT=1
BTN | RAW=1 | STATE=RELEASED  | COUNT=1

实测环境

项目本项目实际使用
主控STM32F103C8T6(BluePill)
模块KY-004 按键模块(三针 S / + / -,本块实测板载上拉,按下 LOW)
显示0.96 寸 OLED,SSD1306,128×64,地址 0x3C
开发工具VS Code + PlatformIO
框架Arduino / STM32duino
烧录ST-Link(SWD 四线)
串口115200,每 400ms 输出一次,状态变化立即输出
I2C 时钟400 kHz(OLED)
依赖库Adafruit SSD1306、Adafruit GFX Library
未验证的环境不当成支持。本项目没有在 Arduino IDE、Keil、STM32CubeIDE 下编译或验证过,所以不提供这些环境的支持说明。

What it does

这个模块干什么

KY-004 是一块最简单的轻触按键模块,三根针 S / + / -(不同批次也可能丝印成 S / VCC / GND,以实物为准):+ 接 3.3V、- 接 GND,S 是信号输出直接接单片机 GPIO。板上已经自带一颗电阻(丝印 102)把 S 在没按的时候钳位到一个固定电平,不需要外接上拉或下拉。按下按键,S 被拉到另一个电平,STM32 靠 20ms 稳定确认过滤掉机械抖动,只在按下那一瞬间(下降沿)计一次数,OLED 大字反白显示 PRESSED / RELEASED 与 COUNT,串口同步输出。

三个要点

要点意思
按下电平方向以实测为准网上 KY-004 资料存在上拉 / 下拉版本互相矛盾的情况;本块 2026-08-20 实测为上拉版:按下 LOW、松开 HIGH。不同批次可能相反,接到手上先按一遍看串口 RAW 值再决定代码判定方向
20ms 稳定确认消抖机械触点按下瞬间会弹跳几毫秒,直接读会一次按下被读成多次跳变;程序要求新电平连续稳定 20ms 才承认变化,短毛刺全部被过滤掉
只在下降沿计数只在电平从 HIGH→LOW 的那一瞬间 COUNT+1,按住不放期间没有新的下降沿,长按只算一次;必须松开回 HIGH 再按才会 +1(实测 10 次短按正好 +10)

Specs

关键参数

按键开关(器件本身)和 KY-004 模块(成品板)分两张表。本项目实测 是 2026-08-20 我们这块实物上量到或读出来的;资料参考 是厂家 / 公开通用资料,我们重新整理;待确认 是没测或不敢写死的,客户问到请照此说明。

器件参数(轻触按键本身)

项目参数来源
器件类型轻触开关(机械按键,按下闭合、松开断开)资料参考
触点形式单刀单掷(SPST)瞬时型资料参考
机械寿命待确认(常见轻触开关标称 10 万次,本块未实测)待确认
弹跳时间典型几 ms ~ 十几 ms,本项目软件按 20ms 稳定确认过滤资料参考

模块参数(KY-004 成品板)

项目参数来源
引脚丝印S / + / -(不同批次也可能丝印为 S / VCC / GND,接线以实物丝印为准)本项目实测
供电+ 接 3.3V,- 接 GND;本项目按 STM32 逻辑 3.3V 使用本项目实测
输出电平3.3V 逻辑电平(跟随供电)本项目实测
板载电阻 R1丝印 102(板上清晰可读);阻值未实测不写死,作用是把 S 钳到一个固定电平丝印实测 阻值待确认
上拉 / 下拉版本本块 2026-08-20 实测为上拉版:松开 HIGH、按下 LOW。网上 KY-004 上拉 / 下拉版本资料互相矛盾,不同批次可能相反,接线判定以实测为准本项目实测
是否需外接上拉不需要,板载已带本项目实测
推荐 STM32 引脚模式INPUT_PULLUP(本块按下 LOW 与之一致);若你的批次实测按下为 HIGH,改判 INPUT 或 INPUT_PULLDOWN + 反向判定本项目实测
尺寸 / 排针是否焊待确认(以商品图与实物为准)待确认

模块参数全部来自我们手上这块实物的实测或板上直接可读的丝印;未实测的项目一律标「待确认」,不猜数字。

Wiring

接线

本项目实测 KY-004 三根线 + OLED 四根线,共 7 根线。KY-004 走数字输入(S→PA0),OLED 走 I2C(SCL→PB8 / SDA→PB9),两条通路互不共用,接线前先核对模块丝印。

接线示意图(一眼看懂)

3.3V 电源(红) S → PA0 信号(黄) GND 地(黑) KY-004 按键模块(上拉版) + 3.3V S(按下 LOW) - GND STM32F103C8T6 BluePill 3.3V PA0 PB8 (SCL) PB9 (SDA) GND 0.96 OLED SSD1306 · 0x3C VCC SCL SDA GND KY-004 只走 S→PA0 一条信号线;OLED 与 STM32 共用同一条 I2C 总线(SCL→PB8、SDA→PB9,地址 0x3C) 3.3V 与 GND 是公共电源总线,KY-004、STM32、OLED 三者的 +/VCC 接一起、GND 接一起(共地)
接线示意:红色 3.3V 顶总线、黑色 GND 底总线;黄色 S→PA0 只连 KY-004 与 STM32;绿色 SCL→PB8、蓝色 SDA→PB9 只连 STM32 与 OLED;两条通路互不交叉

KY-004(三针 S / + / -,或丝印 S / VCC / GND)

信号模块丝印(本项目实物)STM32F103C8T6说明
信号SPA0按下为 LOW(本项目实测),程序引脚模式 INPUT_PULLUP
电源+3.3V模块供电;供电接反可能损坏模块
地-GND共地

OLED(0.96 寸 SSD1306,I2C 地址 0x3C)

信号OLEDSTM32F103C8T6
电源VCC / VDD3.3V
地GNDGND
时钟SCL / SCKPB8
数据SDAPB9

烧录与串口(可选,不看串口也能跑,OLED 直接显示)

用途连接说明
烧录ST-Link → SWD 四线(3.3V / GND / SWDIO=PA13 / SWCLK=PA14)烧录用
串口PA9(TX) / PA10(RX) → CP2102115200,可选
最容易接错的三处:
1. 三针丝印各批次可能不同。KY-004 有的批次是 S / + / -,有的批次是 S / VCC / GND,接线前一定对着实物丝印确认,供电接反可能损坏模块。
2. 按下电平方向先实测。网上 KY-004 资料上拉 / 下拉版本互相矛盾;本块 2026-08-20 实测按下为 LOW。如果你的批次按下反而是 HIGH,需要改代码判定方向和引脚模式(详见「客户回复话术」和「常见问题排查」)。
3. PA13 / PA14 别占用,留给 ST-Link 调试;PA0 只给按键,别和其他外设抢。
KY-004 模块特写:轻触按键、三针 S / + / -、板载 R1 丝印 102
KY-004 模块特写:板载 R1 丝印 102(阻值未实测不写死),三针 S / + / -

Principle

工作原理

资料参考 这一节来自模块厂家资料与公开通用原理,由我们重新整理,用来帮助理解;不是我们实测的结论。实测结果只在上面「实机验证结果」和「关键参数」两节。

按键开关 + 板载电阻

KY-004 是轻触按键 + 一颗板载电阻(本项目实物 R1 丝印 102)组成的最小信号电路。没按的时候,板载电阻把 S 钳位到某一固定电平(HIGH 或 LOW,取决于电阻接向 VCC 还是 GND);按下时,按键把 S 直接接到另一个电平,形成一次干净的电平跳变。整块板上没有比较器、没有 MCU,只是给机械开关配了一个「让 S 松开时保持在固定电平」的电阻。

上拉版 / 下拉版:为什么网上资料互相矛盾

按芯片通用做法,板载电阻接向 VCC 就是上拉版(松开 HIGH、按下 LOW),接向 GND 就是下拉版(松开 LOW、按下 HIGH)。市面 KY-004 上拉版和下拉版都有,各家手册 / 教程各写各的,网上常见 Keyes 版资料多为下拉(按下 HIGH),但也有上拉版说明。本项目实测:我们手上这块实物 2026-08-20 实测为上拉版:按下 S 变 LOW、松开 S 变 HIGH。所以本块的代码用 INPUT_PULLUP + 仅在 HIGH→LOW 下降沿 +1。不同批次可能相反,接到手上第一件事是先看串口 RAW 值定方向,再决定代码判定条件。

为什么需要消抖

机械按键触点在闭合和断开的一瞬间会弹跳几毫秒(典型几 ms ~ 十几 ms),单片机直接读会得到「一次按下被读成多次电平跳变」的现象。工程上最普遍的做法是软件稳定确认:电平变化后要求连续稳定一段时间(本项目 20ms)才承认这次变化真实有效,短毛刺一律丢弃。

为什么只在下降沿计数

本项目实物是上拉版,一次按下会经过 HIGH→LOW→(按住期间保持 LOW)→松开 LOW→HIGH 两个跳变。只在「HIGH→LOW 的那一瞬间」+1,按下保持期间没有新的下降沿所以不重复计,松开也不计;必须松开回 HIGH 再按才会 +1。这是 KY-004 / 轻触按键计次最普遍的做法,也是我们 10 次短按正好 +10、长按不重复计这个实测结论的原因。

本模块的功能等效示意

未按:S 经板载电阻钳到 VCC = HIGH
按下:按键把 S 接到 GND    = LOW   // HIGH→LOW 下降沿,本项目已实测

功能等效示意,未拆解 PCB 走线逐点验证,只用来帮助理解为什么按下读到 LOW、松开读到 HIGH;不作为精确内部原理图。资料参考

Program logic

程序逻辑

本项目实测 下面是对实际代码 src/main.cpp(v1.0)逻辑的说明,不展开语法,只讲「程序是怎么想的」。

上电 -> 探测 OLED(0x3C) 失败? PC13 100ms 快闪报警并停止
     -> 探测成功: 开机页 "KY-004 BUTTON / Initializing..." 停留 1.5s
     -> 进入 RUN: 以当前实际电平为基准, COUNT 从 0 起
     -> 循环: 采样 S -> 电平连续稳定 20ms 才确认
             -> 仅在确认到的 HIGH->LOW 下降沿 COUNT+1
             -> PC13 LED 跟随: 按下亮, 松开灭
             -> 串口 115200 每 400ms 一行, 状态变化立即输出
             -> OLED 状态变了才重画, 大字反白 PRESSED / RELEASED + COUNT
全部时序基于 millis(), 无长 delay()

两阶段状态机:BOOT 1.5 秒 → RUN

阶段持续做什么
BOOT1.5 秒OLED 画开机页「KY-004 BUTTON / Initializing...」,串口打印固件版本;期间不进入计数逻辑
RUN无限进入前用 enterRun() 把当前实际电平作为初始基准(上电时正按着按键也不会被误判成一次「新按下」),然后开始采样、消抖、判下降沿、计数、显示

核心消抖与下降沿计数

步骤代码在做的事
1. 采样每轮 loop 读一次 digitalRead(BTN_PIN);如果和上次采样值不同,就把 rawChangeMs 更新为「变化刚出现的时刻」
2. 稳定确认只有当 now - rawChangeMs >= 20ms 时才把当前采样值认定为「确认电平」confirmedRaw;不足 20ms 的抖动毛刺会被下一次的采样覆盖,压根不会走到这一步
3. 下降沿判定当 confirmedRaw 发生变化,比较旧值和新值——只有 旧 HIGH → 新 LOW 才 pressCount++;松开 LOW → HIGH 不加,按住期间也没有新跳变
4. 状态标记电平被确认变化时置 stateDirty=true,触发 OLED 重画 + 串口立即输出;否则按 400ms 周期输出

OLED / LED / 串口 三处输出

输出行为
OLED(128×64,SSD1306,0x3C)顶部画标题 KY-004,下面 STATE:;再下面一整行 大字反白:按下时 fillRect(0,26,128,18,WHITE) + 黑字 PRESSED,松开时白字 RELEASED;最底部 COUNT: N。只有状态变了才重画,避免无谓刷新
PC13 板载 LED跟随按键:按下(confirmedRaw==LOW)→ LED 亮(PC13 低电平点亮),松开 → 灭
串口 115200每 400ms 输出一次 BTN | RAW=? | STATE=? | COUNT=?;状态刚变化时立即输出一次,不等满 400ms

OLED 探测失败保护

上电 setup() 里先用 Wire.beginTransmission(0x3C) + Wire.endTransmission() 检查是否有 ACK。没有 ACK 就把 displayOk=false 返回;loop() 检测到 displayOk=false 时不做按键逻辑,只让 PC13 LED 每 100ms 翻转一次形成「快闪报警」。这样避免出现「OLED 黑屏但程序还在跑」这种最难排查的假象。

非阻塞时序

整个 loop() 里没有任何 delay()。BOOT 计时、20ms 消抖计时、400ms 串口周期、100ms LED 快闪——全部基于 millis() 时间戳差值判断,保证按键响应及时、OLED 刷不阻塞按键采样。

Code

完整代码

本项目实测 下面是实际烧录进板子、拍摄素材时运行的那一份(src/main.cpp,v1.0,实测日期 2026-08-20),一字未改。环境:VS Code + PlatformIO + Arduino(STM32duino)。

关键处说明

1. INPUT_PULLUP + HIGH→LOW 下降沿计数(与实测一致)。本块实物按下为 LOW,所以 PA0 走内部上拉输入,只在 prev==HIGH && confirmedRaw==LOW 时 pressCount++。若你的批次实测按下反而是 HIGH(下拉版),把引脚模式改成 INPUT_PULLDOWN,判定条件反过来(prev==LOW && confirmedRaw==HIGH)即可。

2. 20ms 稳定确认消抖。用 lastSample / confirmedRaw / rawChangeMs 三个变量:任何一次采样和上次不同就把变化时刻刷新;只有连续稳定 20ms 后,当前采样才被吸收进 confirmedRaw。机械抖动典型几 ms ~ 十几 ms,全部被过滤掉。

3. 进入 RUN 时以当前电平为基准。enterRun() 把 confirmedRaw 直接设成当前实际读到的电平,避免「上电时正按着按键」被误判成一次新按下;COUNT 从 0 起算。

4. OLED 只在状态变化时重画。stateDirty 只在电平被确认变化时才置位,其它时间不重画;避免 OLED 无谓刷新闪烁。

5. 串口每 400ms + 状态变化立即输出。if (stateChanged || now - lastSerialMs >= SERIAL_INTERVAL)——两个条件任一满足就打印,客户拿在手上不用等周期就能看到「按下 / 松开」的瞬间反馈。

6. OLED 探测失败 PC13 快闪。上电 Wire.beginTransmission(0x3C) + Wire.endTransmission()!=0 判 ACK;没 ACK 时 displayOk=false,loop 中每 100ms 翻转 PC13 形成快闪报警,绝不出现「黑屏但程序还在跑」的迷惑现象。

7. 全部 millis() 非阻塞。BOOT 1.5 秒、消抖 20ms、串口 400ms、LED 快闪 100ms 全走 millis() 差值判断,loop 里没有一处 delay()。

展开完整代码(src/main.cpp,约 240 行)
// 测试修改
/*
 * ============================================================================
 * 13_KY-004 按键 | STM32F103C8T6 BluePill + KY-004 按键模块 + 0.96" OLED(SSD1306)
 * ----------------------------------------------------------------------------
 * 重要概念:
 *   KY-004 网上存在上拉/下拉版本差异, 本块实物已实测确认(2026-08-20):
 *   上拉版, 按下为 LOW(松开 HIGH)。R1(丝印 102)阻值未实测, 不写死。
 *   当前代码 INPUT_PULLUP + HIGH->LOW 下降沿计数, 与实测一致, 保留。
 *     RAW=1 -> RELEASED (未按下) / RAW=0 -> PRESSED (按下)   [已实测确认]
 *
 * 功能:
 *   1. PA0 读取 KY-004 信号端 S, 板载 LED(PC13, 低电平点亮) 跟随按键状态
 *   2. 仅在 HIGH->LOW 下降沿触发计数 COUNT+1 (已实测按下为 LOW; 按下一次计一次, 按住不重复计)
 *   3. 输入经 20ms 稳定确认, 按键抖动/毛刺不计数、不乱跳
 *   4. OLED: 开机页 -> 运行页(STATE: PRESSED/RELEASED 大字反白 + COUNT)
 *   5. 串口 115200, 每 400ms 输出一次状态, 按键变化时立即输出
 *   6. 全部使用 millis() 非阻塞时序, 无长 delay()
 *
 * 接线:
 *   KY-004  -> BluePill:  S->PA0   +->3.3V   -->GND
 *   OLED    -> BluePill:  VCC->3.3V GND->GND  SCL->PB8  SDA->PB9 (I2C 0x3C)
 *
 * 注意: 不同批次 KY-004 引脚丝印可能不同(常见 S/+/- 或 S/VCC/GND), 以模块丝印为准!
 * ============================================================================
 */
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define FIRMWARE_VERSION "v1.0"

/* ------------------------- 引脚常量 ------------------------- */
constexpr uint8_t BTN_PIN       = PA0;   // KY-004 信号端 S, 数字输入(内部上拉, 已实测按下为 LOW)
constexpr uint8_t LED_PIN       = PC13;  // 板载 LED, 低电平点亮
constexpr uint8_t I2C_SDA_PIN   = PB9;   // OLED SDA
constexpr uint8_t I2C_SCL_PIN   = PB8;   // OLED SCL
constexpr uint8_t OLED_ADDRESS  = 0x3C;  // SSD1306 I2C 地址

/* ------------------------- OLED ------------------------- */
constexpr int SCREEN_WIDTH  = 128;
constexpr int SCREEN_HEIGHT = 64;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

/* ------------------------- 时序参数 (ms) ------------------------- */
constexpr uint32_t BOOT_SCREEN_MS  = 1500UL;   // 开机画面停留时长
constexpr uint32_t DEBOUNCE_MS     = 20UL;     // 按键稳定确认时间(10~30ms)
constexpr uint32_t SERIAL_INTERVAL = 400UL;    // 串口输出周期

/* ------------------------- 状态变量 ------------------------- */
enum class Phase : uint8_t { BOOT, RUN };

Phase    phase        = Phase::BOOT;
uint32_t phaseStartMs = 0;

uint8_t  lastSample   = HIGH;   // 最近一次采样到的电平
uint8_t  confirmedRaw = HIGH;   // 连续稳定 DEBOUNCE_MS 后确认的电平
uint32_t rawChangeMs  = 0;      // 采样电平发生变化的时刻
uint32_t pressCount   = 0;      // 按下次数(仅 HIGH->LOW 下降沿 +1)
bool     stateDirty   = false;  // OLED 需要刷新

bool     displayOk    = false;
uint32_t lastSerialMs = 0;
uint32_t lastLedMs    = 0;

/* ------------------------- OLED 绘制辅助 ------------------------- */

// 屏幕水平居中显示一行文字(默认 5x7 字体)
void drawCentered(const char* s, int16_t y, uint8_t size = 1) {
  display.setTextSize(size);
  int16_t x1, y1;
  uint16_t w, h;
  display.getTextBounds(s, 0, 0, &x1, &y1, &w, &h);
  display.setCursor((SCREEN_WIDTH - (int16_t)w) / 2, y);
  display.print(s);
}

// 开机页
void drawBootPage() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextWrap(false);
  drawCentered("KY-004 BUTTON", 16);
  drawCentered("Initializing...", 32);
  display.display();
}

// 运行页: pressed=是否按下, 大字反白显示当前状态
void drawRunPage(bool pressed) {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextWrap(false);
  drawCentered("KY-004", 0);

  display.setTextSize(1);
  display.setCursor(0, 14);
  display.print("STATE:");

  /* PRESSED / RELEASED 大字反白(按下白底黑字, 松开白字) */
  if (pressed) {
    display.fillRect(0, 26, 128, 18, SSD1306_WHITE);
    display.setTextColor(SSD1306_BLACK);
  } else {
    display.setTextColor(SSD1306_WHITE);
  }
  display.setTextSize(2);
  display.setCursor(0, 27);
  display.print(pressed ? "PRESSED" : "RELEASED");
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  char line[20];
  snprintf(line, sizeof(line), "COUNT: %lu", (unsigned long)pressCount);
  display.setCursor(0, 52);
  display.print(line);

  display.display();
}

/* ------------------------- 进入运行页 ------------------------- */

// 以当前实际电平为基准进入运行, 避免上电时正按着按键被误判为一次"新按下"
void enterRun(uint32_t now) {
  phase        = Phase::RUN;
  phaseStartMs = now;
  lastSample   = digitalRead(BTN_PIN);
  confirmedRaw = lastSample;
  rawChangeMs  = now;
  pressCount   = 0;
  stateDirty   = true;
  lastSerialMs = now;
  digitalWrite(LED_PIN, (confirmedRaw == LOW) ? LOW : HIGH);
  Serial.println("BTN | READY | RUNNING");
}

/* ------------------------- setup / loop ------------------------- */

void setup() {
  Serial.begin(115200);

  pinMode(BTN_PIN, INPUT_PULLUP);   // 已实测按下为 LOW(上拉版), 保留
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);      // 开机 LED 灭(PC13 低电平点亮)

  Wire.setSDA(I2C_SDA_PIN);
  Wire.setSCL(I2C_SCL_PIN);
  Wire.begin();
  Wire.setClock(400000);

  Serial.println("KY-004 BUTTON TEST");
  Serial.print("Firmware: ");
  Serial.println(FIRMWARE_VERSION);
  Serial.println();

  // 先探测 I2C 地址, 给出明确错误信息(display.begin 的返回值不保证真实 ACK)
  Wire.beginTransmission(OLED_ADDRESS);
  if (Wire.endTransmission() != 0) {
    Serial.println("ERROR: OLED not found at 0x3C (check wiring/I2C)");
    displayOk = false;
    return;
  }

  displayOk = display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS, true, false);
  if (!displayOk) {
    Serial.println("ERROR: OLED init failed");
    return;   // loop() 中快速闪灯提示
  }

  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextWrap(false);

  phase        = Phase::BOOT;
  phaseStartMs = millis();
  drawBootPage();
}

void loop() {
  uint32_t now = millis();

  if (!displayOk) {
    // OLED 初始化失败: PC13 快速闪灯报警(非阻塞)
    if (now - lastLedMs >= 100) {
      lastLedMs = now;
      digitalWrite(LED_PIN, !digitalRead(LED_PIN));
    }
    return;
  }

  switch (phase) {

    case Phase::BOOT:
      if (now - phaseStartMs >= BOOT_SCREEN_MS) {
        enterRun(now);
      }
      break;

    case Phase::RUN: {
      /* 1) 采样 + 稳定确认(去抖): 电平连续稳定 DEBOUNCE_MS 才承认变化 */
      uint8_t sample = digitalRead(BTN_PIN);
      if (sample != lastSample) {
        lastSample  = sample;
        rawChangeMs = now;               // 出现变化, 重新计时
      }
      if (now - rawChangeMs >= DEBOUNCE_MS) {
        if (confirmedRaw != lastSample) {  // 稳定变化被确认
          uint8_t prev = confirmedRaw;
          confirmedRaw = lastSample;
          if (prev == HIGH && confirmedRaw == LOW) {
            pressCount++;                  // 仅 HIGH->LOW 下降沿计数一次
          }
          stateDirty = true;
        }
      }

      /* 2) LED: 按下->亮, 松开->灭 (PC13 低电平点亮) */
      digitalWrite(LED_PIN, (confirmedRaw == LOW) ? LOW : HIGH);

      /* 3) 串口: 每 400ms 输出一次; 状态变化时立即输出, 与 OLED 一致 */
      bool stateChanged = stateDirty;
      if (stateChanged || now - lastSerialMs >= SERIAL_INTERVAL) {
        lastSerialMs = now;
        Serial.print("BTN | RAW=");
        Serial.print(confirmedRaw);
        Serial.print(" | STATE=");
        Serial.print(confirmedRaw == LOW ? "PRESSED  " : "RELEASED ");
        Serial.print("| COUNT=");
        Serial.println(pressCount);
      }

      /* 4) OLED: 状态变化立即刷新, 不变不刷 */
      if (stateDirty) {
        stateDirty = false;
        drawRunPage(confirmedRaw == LOW);
      }
      break;
    }
  }
}

依赖库:Adafruit SSD1306、Adafruit GFX Library,用 PlatformIO 打开工程会按 platformio.ini 自动下载,不用手动安装。

Download

下载

解压后用 VS Code + PlatformIO 打开这个文件夹就能 Build,一般不需要改代码。

PlatformIO 工程 ZIP 含 platformio.ini、src/main.cpp、README.md。已去掉本机构建路径,解压后 VS Code 打开即可 Build。
下载 ZIP

第一次使用

  1. 按上面接线表接好:KY-004 S→PA0、+→3.3V、-→GND;OLED VCC→3.3V、GND→GND、SCL→PB8、SDA→PB9
  2. 下载 ZIP,解压,用 VS Code + PlatformIO 打开这个文件夹
  3. 等 Adafruit SSD1306、Adafruit GFX 两个库自动装完,点 Build,看到 SUCCESS
  4. 接上 ST-Link,点 Upload,看到 ** Verified OK ** 和 [SUCCESS]
  5. OLED 开机页停留 1.5 秒后进入运行页:不按显示 RELEASED,按下变 PRESSED 反白 + COUNT +1;长按只算一次,连续短按 10 次正好 +10
KY-004 烧录成功截图:VS Code + PlatformIO 终端 Programming Finished / Verified OK / SUCCESS
烧录成功截图:PlatformIO 终端 ** Programming Finished ** → ** Verified OK ** → [SUCCESS](ST-Link 走 SWD)
不要照抄 COM 号。截图里出现的串口号是我们这台电脑的,你的电脑多半不一样。查当前串口:pio device list,找 CP2102 / CP210x / CH340 对应的那个。本模块也可以不看串口——OLED 直接看状态和计数就行。
这块板子不能用网页烧录器。网页烧录(WebSerial)主要面向 ESP32 / ESP8266 这类自带 USB 串口下载的芯片。STM32F103C8T6 本项目用的是 ST-Link + SWD,浏览器接触不到 SWD 接口。

Troubleshooting

常见问题排查

下面每一条都是这个模块真实会遇到的问题。标注「实测」的是我们自己踩过的。

按下没反应 / 一直显示 RELEASED

先对着实物核对模块丝印:S 是不是真的接到了 PA0?+ 是不是接的 3.3V、- 是不是接的 GND?不同批次 KY-004 排列可能是 S / + / - 或 S / VCC / GND,一定要以实物丝印为准。三根针里供电接错可能损坏模块。串口(115200)打开看 RAW 值——按下的瞬间 RAW 应该变化;如果 RAW 一直是 1 完全不动,说明 S 根本没通到 PA0,或者模块没供电。

一直显示 PRESSED(松开也是 PRESSED)

本项目实测本块按下为 LOW(上拉版);如果你手上这块按下反而是 HIGH(下拉版),当前代码 INPUT_PULLUP + 只判 HIGH→LOW 会把「松开」误认成「按下」。验证:接串口 115200,看松开和按下时 RAW 值——若松开时是 0、按下时是 1,就是下拉版,把 pinMode(BTN_PIN, INPUT_PULLUP) 改成 INPUT_PULLDOWN,把 if (prev == HIGH && confirmedRaw == LOW) 改成 if (prev == LOW && confirmedRaw == HIGH) 就行。

按一次数字跳好几下(多计)

20ms 消抖已经过滤掉了正常范围内的毛刺,还会多计一般是供电不稳或杜邦线接触不良——换一组接触好的杜邦线、确认 3.3V 电源稳定(不要用一路被别的模块拉低的 3.3V)。我们实测正常接线连续短按 10 次正好 +10,不会多计。

长按时 COUNT 一直在涨

本项目实测长按只计一次——这是代码的正常特性,只在 HIGH→LOW 下降沿 +1,按住不放没有新的下降沿。如果你看到长按在涨,八成是接线接触不良或者供电抖,让 S 上的电平在按住期间反复跳,被程序误认为是新的下降沿。换杜邦线、稳定供电再看。

OLED 黑屏 + 板载 LED 快闪

程序启动时探测 OLED 地址 0x3C 失败,进入了 OLED 报警模式:不做按键逻辑,只让 PC13 LED 每 100ms 翻转一次快闪。查 OLED 的 SCL→PB8、SDA→PB9、VCC→3.3V、GND→GND 四根线有没有插紧,特别是共地。

PA0 我接了别的东西,可以换吗?

可以,改代码里 BTN_PIN 那一行就行。不要用 PA13 / PA14(SWDIO / SWCLK 留给 ST-Link 调试)。PB8 / PB9 是 OLED 的 I2C 总线,别再占用。

串口看不到数据

先查当前串口:

pio device list

找到 CP2102 / CP210x / CH340 对应的那个 COM,然后在自己电脑的 platformio.ini 里加:

monitor_port = COMx
monitor_speed = 115200

COMx 要换成你自己电脑实际识别到的端口号,不要直接抄。只有需要看串口时才加这两行,正常用 ST-Link 烧录不需要。本模块也可以不看串口,OLED 直接看状态和计数就行。

Reply templates

客户回复话术

下面每段都可以整段复制发给客户,覆盖 KY-004 最常见的三类咨询。按客户的现象挑一段贴上就行。

「是不是坏了」—— 按下没反应 / 一直显示 PRESSED

您好,先不用急,这个大概率不是模块坏了,而是按下电平方向和代码判定不一致。KY-004 市面上存在「上拉版」和「下拉版」两种电路(各家手册资料互相矛盾),我们手上这块实测是上拉版:按下时 S 变 LOW、松开时 S 变 HIGH,我们代码就按这个方向做的(引脚模式 INPUT_PULLUP,只在按下 HIGH→LOW 那一瞬间计数)。如果您这块按下反而是 HIGH(松开 LOW),说明是下拉版,只需要改两处:①把 pinMode(BTN_PIN, INPUT_PULLUP) 改成 pinMode(BTN_PIN, INPUT_PULLDOWN);②把代码里判断按下的条件从 prev==HIGH && confirmedRaw==LOW 改成 prev==LOW && confirmedRaw==HIGH,其它不用动。验证方法很简单:接上串口(115200),按一下按键看 RAW 值——正常应该看到松开 RAW=1、按下 RAW=0(本块实测),或者相反(下拉版)。如果 RAW 完全不变化,那多半是接线问题,请对照下一条排查;如果您把按前和按后的 RAW 值发我,我直接告诉您改哪几行。

「接了没反应」—— OLED 不显示或状态一点不变

您好,接了没反应基本都是接线问题,按下面这个顺序查一遍一般都能好:①核对模块丝印——KY-004 是三个针,不同批次排列不一定相同,常见有 S / + / - 或 S / VCC / GND 两种叫法,请以您这块实物上的丝印为准;正确接法是 S 接 STM32 的 PA0、+(或 VCC)接 3.3V、-(或 GND)接 GND,供电千万别接反,接反可能损坏模块;②必须共地——模块的 GND、OLED 的 GND 和 STM32 的 GND 三个要连在一起,只接电源不共地是最常见的「没反应」原因;③确认 PA0 没被别的东西占用(比如面包板上其他跳线插到 PA0 了),也别用 PA13 / PA14(这两个脚要留给 ST-Link 下载调试用);④如果 OLED 是黑屏、STM32 板载小灯(PC13)还在快速一闪一闪——这不是按键问题,是程序开机探测不到 OLED(I2C 地址 0x3C)在报警,请重点查 OLED 的四根线:VCC→3.3V、GND→GND、SCL→PB8、SDA→PB9 有没有插紧。按完这几步一般就能正常显示;还有问题请拍一张接线整体照发我,我直接给您指哪里没接对。

「计数不对」—— 长按不涨 / 按一下跳好几下

您好,计数这块分两种现象:①长按不重复计是正常特性,不是坏了——程序只在按键按下的那一瞬间(电平从高变到低的「下降沿」)加一次数,按住期间电平一直是低电平没有新的下降沿,所以长按只会 +1,必须松开再按才会继续加。我们实测连续短按 10 次正好 +10,没有多计也没有漏计。②如果是「按一下数字乱跳好几下」,程序里已经做了 20ms 稳定确认消抖,正常按键的机械抖动(一般几毫秒)都会被过滤掉,会乱跳通常有两类原因:一是电源不稳,比如 3.3V 被别的大电流模块拉低;二是杜邦线接触不良,让信号线时通时不通。建议换一组接触好的杜邦线、把 3.3V 单独稳定供电再试一次。如果换线换电源还乱跳,把现象拍视频发我进一步排查。

References

资料来源

本页信息来源声明

内容来源
接线、代码、OLED UI、串口输出、按下电平方向(LOW)、20ms 消抖、下降沿计数、10 次短按正好 +10、长按不重复计、PC13 LED 跟随、OLED 探测失败快闪本项目实测 2026-08-20 实机测试
板载 R1 丝印 102本项目实测(板上直接可读,阻值未实测)
轻触按键结构、机械抖动、上拉 / 下拉版本差异、软件稳定确认消抖、下降沿计数的通用做法资料参考 模块厂家资料与公开通用原理,由我们重新整理表述
驱动库资料参考 Adafruit SSD1306 / GFX Library
SKU待确认(本页实物已实测,商品 SKU 尚未回填)

本页所有「资料参考」内容均为模块厂家资料与公开通用原理,由我们重新整理表述,未直接复制原文,也未硬造官方链接。不同厂家 / 批次 KY-004 可能存在引脚丝印顺序、上拉 / 下拉电平方向的差异,接线与判定请以你手上这块实物的丝印和实测为准。