传感器 · 按键输入 · 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。
按键行为实测
| 测试动作 | 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 快闪报警 |
完整演示视频(接线 → 烧录 → 实测)
OLED / 模块特写
串口输出(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 |
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),两条通路互不共用,接线前先核对模块丝印。
接线示意图(一眼看懂)
KY-004(三针 S / + / -,或丝印 S / VCC / GND)
| 信号 | 模块丝印(本项目实物) | STM32F103C8T6 | 说明 |
|---|---|---|---|
| 信号 | S | PA0 | 按下为 LOW(本项目实测),程序引脚模式 INPUT_PULLUP |
| 电源 | + | 3.3V | 模块供电;供电接反可能损坏模块 |
| 地 | - | GND | 共地 |
OLED(0.96 寸 SSD1306,I2C 地址 0x3C)
| 信号 | OLED | STM32F103C8T6 |
|---|---|---|
| 电源 | VCC / VDD | 3.3V |
| 地 | GND | GND |
| 时钟 | SCL / SCK | PB8 |
| 数据 | SDA | PB9 |
烧录与串口(可选,不看串口也能跑,OLED 直接显示)
| 用途 | 连接 | 说明 |
|---|---|---|
| 烧录 | ST-Link → SWD 四线(3.3V / GND / SWDIO=PA13 / SWCLK=PA14) | 烧录用 |
| 串口 | PA9(TX) / PA10(RX) → CP2102 | 115200,可选 |
1. 三针丝印各批次可能不同。KY-004 有的批次是
S / + / -,有的批次是 S / VCC / GND,接线前一定对着实物丝印确认,供电接反可能损坏模块。2. 按下电平方向先实测。网上 KY-004 资料上拉 / 下拉版本互相矛盾;本块 2026-08-20 实测按下为 LOW。如果你的批次按下反而是 HIGH,需要改代码判定方向和引脚模式(详见「客户回复话术」和「常见问题排查」)。
3. PA13 / PA14 别占用,留给 ST-Link 调试;PA0 只给按键,别和其他外设抢。
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
| 阶段 | 持续 | 做什么 |
|---|---|---|
| BOOT | 1.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,一般不需要改代码。
第一次使用
- 按上面接线表接好:KY-004 S→PA0、+→3.3V、-→GND;OLED VCC→3.3V、GND→GND、SCL→PB8、SDA→PB9
- 下载 ZIP,解压,用 VS Code + PlatformIO 打开这个文件夹
- 等 Adafruit SSD1306、Adafruit GFX 两个库自动装完,点 Build,看到 SUCCESS
- 接上 ST-Link,点 Upload,看到
** Verified OK **和[SUCCESS] - OLED 开机页停留 1.5 秒后进入运行页:不按显示 RELEASED,按下变 PRESSED 反白 + COUNT +1;长按只算一次,连续短按 10 次正好 +10
** Programming Finished ** → ** Verified OK ** → [SUCCESS](ST-Link 走 SWD)pio device list,找 CP2102 / CP210x / CH340 对应的那个。本模块也可以不看串口——OLED 直接看状态和计数就行。
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 最常见的三类咨询。按客户的现象挑一段贴上就行。
References
资料来源
本页信息来源声明
| 内容 | 来源 |
|---|---|
| 接线、代码、OLED UI、串口输出、按下电平方向(LOW)、20ms 消抖、下降沿计数、10 次短按正好 +10、长按不重复计、PC13 LED 跟随、OLED 探测失败快闪 | 本项目实测 2026-08-20 实机测试 |
| 板载 R1 丝印 102 | 本项目实测(板上直接可读,阻值未实测) |
| 轻触按键结构、机械抖动、上拉 / 下拉版本差异、软件稳定确认消抖、下降沿计数的通用做法 | 资料参考 模块厂家资料与公开通用原理,由我们重新整理表述 |
| 驱动库 | 资料参考 Adafruit SSD1306 / GFX Library |
| SKU | 待确认(本页实物已实测,商品 SKU 尚未回填) |
本页所有「资料参考」内容均为模块厂家资料与公开通用原理,由我们重新整理表述,未直接复制原文,也未硬造官方链接。不同厂家 / 批次 KY-004 可能存在引脚丝印顺序、上拉 / 下拉电平方向的差异,接线与判定请以你手上这块实物的丝印和实测为准。