Arduino ESP32-S3 按键实验教程
Arduino ESP32-S3 按键实验教程
开发板:立创实战派 ESP32-S3
开发环境:VSCode + PlatformIO + Arduino Framework
实验难度:⭐⭐⭐ (中级)
预计时间:30分钟
📚 实验简介
本教程将指导你完成立创实战派ESP32-S3开发板的按键检测实验,学习如何使用中断方式检测按键输入,并实现完善的软件消抖算法。立创实战派开发板在IO0引脚上自带了一个BOOT按键,非常适合用于按键实验。
🎯 学习目标
- 掌握 VSCode + PlatformIO 开发环境的配置和使用
- 掌握 ESP32-S3 的 GPIO 配置方法
- 理解中断服务程序(ISR)的使用规范
- 学习按键消抖的原理和多层保护实现
- 掌握边沿检测和状态管理技术
- 熟练使用 Arduino 核心库函数
🛠️ 开发环境
本教程使用 VSCode + PlatformIO + Arduino Framework 开发环境。
快速开始
-
安装开发环境
- 安装 VSCode
- 安装 PlatformIO 插件(在 VSCode 扩展市场搜索 “PlatformIO IDE”)
-
创建项目
- 新建 PlatformIO 项目
- 选择开发板:
ESP32-S3-DevKitC-1 - 选择框架:
Arduino
-
配置文件 platformio.ini
[env:esp32-s3-devkitc-1] platform = espressif32 board = esp32-s3-devkitc-1 framework = arduino monitor_speed = 115200 -
编译上传
- 点击底部工具栏的 ✓ 编译
- 点击 → 上传到开发板
- 点击 🔌 打开串口监视器(波特率 115200)
🔧 硬件准备
所需材料
- 立创实战派 ESP32-S3 开发板 × 1
- USB Type-C 数据线 × 1
说明: 立创实战派开发板在IO0引脚上已经自带了一个BOOT按键,无需外接按键和杜邦线。
硬件连接
立创实战派开发板按键说明:
立创实战派 ESP32-S3 开发板
┌─────────────────────────┐
│ │
│ [BOOT按键] │
│ ↓ │
│ IO0 ──┬── 按键 ── GND │
│ │ │
│ [内部上拉] │
│ │
└─────────────────────────┘
硬件特性:
- 立创实战派开发板在 IO0 引脚上自带了一个 BOOT按键
- 按键已经硬件连接好:一端接IO0,另一端接GND
- 开发板内部已配置上拉电阻
- 无需任何外接硬件,直接使用即可
按键位置:
- BOOT按键通常位于开发板的侧面或正面
- 标注为 “BOOT” 或 “IO0”
- 与RESET按键相邻
工作原理:
- IO0是ESP32-S3的BOOT引脚,平时为高电平(3.3V)
- 按下BOOT按键时,IO0被拉低到GND(0V)
- 释放按键后,通过内部上拉电阻恢复到高电平
💻 代码实现
完整代码
#include <Arduino.h>
// Define button pin
#define BUTTON_PIN 0
// Debounce settings
const unsigned long DEBOUNCE_DELAY = 50; // 50ms debounce time
const int STABLE_COUNT = 3; // Number of consecutive stable readings required
// Variables for button state
volatile bool interruptTriggered = false;
unsigned long lastDebounceTime = 0;
int lastButtonState = HIGH;
int buttonState = HIGH;
int stableReadings = 0;
unsigned long buttonPressCount = 0;
// Interrupt handler for button press
void IRAM_ATTR buttonISR() {
// Just set a flag, do minimal work in ISR
interruptTriggered = true;
}
void setup() {
// Initialize serial communication at 115200 baud rate
Serial.begin(115200);
// Wait for serial port to connect
delay(1000);
Serial.println("\n========== ESP32-S3 Button Test ==========");
Serial.println("Board: LiChuang EDA ESP32-S3");
Serial.println("Button: BOOT button on IO0");
Serial.println("Press the BOOT button to test...");
Serial.println("==========================================\n");
// Configure button pin as input with internal pull-up
pinMode(BUTTON_PIN, INPUT_PULLUP);
// Attach interrupt to button pin (trigger on falling edge - button press)
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
Serial.println("System ready! Waiting for button press...\n");
}
void loop() {
// Read current button state
int reading = digitalRead(BUTTON_PIN);
// Check if interrupt was triggered or button state changed
if (interruptTriggered || reading != lastButtonState) {
// Reset debounce timer
lastDebounceTime = millis();
lastButtonState = reading;
stableReadings = 0;
interruptTriggered = false;
}
// Check if enough time has passed for debouncing
if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) {
// Verify the reading is stable
if (reading == lastButtonState) {
stableReadings++;
// If we have enough stable readings and state has changed
if (stableReadings >= STABLE_COUNT && reading != buttonState) {
buttonState = reading;
// Detect falling edge (button press)
if (buttonState == LOW) {
buttonPressCount++;
// Print button press information
Serial.println("========================================");
Serial.println(">>> BUTTON PRESSED! <<<");
Serial.print("Press Count: ");
Serial.println(buttonPressCount);
Serial.print("Time: ");
Serial.print(millis());
Serial.println(" ms");
Serial.print("Uptime: ");
Serial.print(millis() / 1000);
Serial.println(" seconds");
Serial.println("========================================\n");
}
// Detect rising edge (button release) - optional
else {
Serial.println("--- Button Released ---\n");
}
}
} else {
// Reading changed, reset stable count
stableReadings = 0;
}
}
// Small delay to prevent excessive CPU usage
delay(10);
}
📖 代码详解
1. 引脚定义与常量配置
#define BUTTON_PIN 0
const unsigned long DEBOUNCE_DELAY = 50;
const int STABLE_COUNT = 3;
BUTTON_PIN:按键连接的GPIO引脚号DEBOUNCE_DELAY:消抖延迟时间(50毫秒)STABLE_COUNT:需要连续稳定读数的次数
2. 状态变量说明
volatile bool interruptTriggered = false; // 中断触发标志
unsigned long lastDebounceTime = 0; // 上次消抖时间
int lastButtonState = HIGH; // 上次读取的原始状态
int buttonState = HIGH; // 当前确认的稳定状态
int stableReadings = 0; // 稳定读数计数器
unsigned long buttonPressCount = 0; // 按键按下次数
关键点:
volatile关键字:确保变量在中断和主循环间正确同步- 使用多个状态变量实现可靠的消抖机制
3. 中断服务程序(ISR)
void IRAM_ATTR buttonISR() {
interruptTriggered = true;
}
设计原则:
IRAM_ATTR:将函数放在RAM中执行,提高响应速度- ISR中只设置标志位,不做复杂处理
- 避免在ISR中使用
Serial.print()、millis()等函数
4. 初始化配置(setup)
pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
配置说明:
INPUT_PULLUP:启用内部上拉电阻(约47kΩ)FALLING:下降沿触发(HIGH → LOW)- 按键按下时产生下降沿,触发中断
5. 消抖算法(loop)
消抖流程图
开始
↓
读取按键状态
↓
状态改变? ──是──→ 重置消抖计时器
↓ 否 重置稳定计数器
↓
等待50ms
↓
连续3次稳定读数?
↓ 是
状态确认改变?
↓ 是
下降沿(按下)?──是──→ 打印按键信息
↓ 否
上升沿(释放)?──是──→ 打印释放信息
↓
延迟10ms
↓
循环
消抖机制详解
三层保护:
- 硬件消抖:内部上拉电阻提供稳定的高电平
- 时间消抖:50ms延迟过滤机械抖动
- 稳定性检测:连续3次相同读数才确认状态
工作原理:
按键按下瞬间:
时间轴: 0ms 10ms 20ms 30ms 40ms 50ms 60ms 70ms
状态: HIGH → LOW → LOW → LOW → LOW → LOW → LOW → LOW
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
抖动: 抖动期(忽略) 稳定期(确认)
↑
消抖延迟50ms后
连续3次LOW读数
确认按键按下
📚 Arduino库函数学习要点
本实验中使用了多个Arduino核心库函数,以下是详细的函数说明和使用要点。
1. 串口通信函数
Serial.begin(baudRate)
功能:初始化串口通信
Serial.begin(115200);
- 参数:
baudRate- 波特率(常用:9600, 115200) - 返回值:无
- 使用场景:程序开始时初始化串口,用于调试输出
- 注意事项:必须在
setup()中调用,且串口监视器波特率要匹配
Serial.println(data)
功能:向串口输出数据并换行
Serial.println("Hello World");
Serial.println(buttonPressCount);
- 参数:
data- 要输出的数据(字符串、数字等) - 返回值:写入的字节数
- 相关函数:
Serial.print()- 输出不换行Serial.printf()- 格式化输出
2. GPIO配置函数
pinMode(pin, mode)
功能:配置GPIO引脚的工作模式
pinMode(BUTTON_PIN, INPUT_PULLUP);
- 参数:
pin- 引脚编号(0-45)mode- 工作模式
- 模式选项:
INPUT- 输入模式(高阻态)INPUT_PULLUP- 输入模式 + 内部上拉(约47kΩ)INPUT_PULLDOWN- 输入模式 + 内部下拉OUTPUT- 输出模式
- 使用场景:在
setup()中配置引脚功能 - 注意事项:ESP32-S3所有GPIO都支持上拉/下拉
digitalRead(pin)
功能:读取数字引脚的电平状态
int reading = digitalRead(BUTTON_PIN);
- 参数:
pin- 引脚编号 - 返回值:
HIGH(1) - 高电平(3.3V)LOW(0) - 低电平(0V)
- 使用场景:读取按键、传感器等数字信号
- 注意事项:引脚必须先配置为输入模式
digitalWrite(pin, value)
功能:设置数字引脚的输出电平
digitalWrite(LED_PIN, HIGH); // 点亮LED
digitalWrite(LED_PIN, LOW); // 熄灭LED
- 参数:
pin- 引脚编号value-HIGH或LOW
- 使用场景:控制LED、继电器等输出设备
- 注意事项:引脚必须先配置为输出模式
3. 中断函数
attachInterrupt(interrupt, ISR, mode)
功能:绑定中断服务程序到指定引脚
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
- 参数:
interrupt- 中断号(通过digitalPinToInterrupt()获取)ISR- 中断服务函数名mode- 触发模式
- 触发模式:
LOW- 低电平触发HIGH- 高电平触发CHANGE- 电平变化触发FALLING- 下降沿触发(HIGH→LOW)RISING- 上升沿触发(LOW→HIGH)
- 使用场景:按键检测、脉冲计数、外部事件响应
- 注意事项:
- ISR函数必须添加
IRAM_ATTR属性 - ISR中不要使用
Serial.print()、delay()等函数 - ISR要尽可能简短快速
- ISR函数必须添加
detachInterrupt(interrupt)
功能:解除中断绑定
detachInterrupt(digitalPinToInterrupt(BUTTON_PIN));
- 参数:
interrupt- 中断号 - 使用场景:临时禁用中断、切换中断模式
digitalPinToInterrupt(pin)
功能:将GPIO引脚号转换为中断号
int interruptNum = digitalPinToInterrupt(BUTTON_PIN);
- 参数:
pin- GPIO引脚号 - 返回值:中断号
- 注意事项:ESP32-S3所有GPIO都支持中断
4. 时间函数
millis()
功能:获取程序运行时间(毫秒)
unsigned long currentTime = millis();
- 参数:无
- 返回值:
unsigned long- 从程序启动到现在的毫秒数 - 使用场景:
- 计时、延迟
- 消抖算法
- 超时检测
- 注意事项:
- 约50天后会溢出(从0重新开始)
- 不能在ISR中使用(会返回错误值)
- 精度约为1ms
micros()
功能:获取程序运行时间(微秒)
unsigned long currentTime = micros();
- 参数:无
- 返回值:
unsigned long- 从程序启动到现在的微秒数 - 使用场景:高精度计时(精度约1μs)
- 注意事项:约70分钟后会溢出
delay(ms)
功能:阻塞延迟(毫秒)
delay(1000); // 延迟1秒
- 参数:
ms- 延迟时间(毫秒) - 返回值:无
- 使用场景:简单延迟、等待
- 注意事项:
- 会阻塞程序执行
- 延迟期间无法响应其他事件
- 不推荐在需要实时响应的场景使用
delayMicroseconds(us)
功能:阻塞延迟(微秒)
delayMicroseconds(100); // 延迟100微秒
- 参数:
us- 延迟时间(微秒) - 使用场景:精确短延迟(如超声波传感器)
5. ESP32特有函数
ESP.getChipModel()
功能:获取芯片型号
Serial.println(ESP.getChipModel()); // 输出: ESP32-S3
- 返回值:字符串 - 芯片型号
ESP.getCpuFreqMHz()
功能:获取CPU频率
Serial.println(ESP.getCpuFreqMHz()); // 输出: 240
- 返回值:整数 - CPU频率(MHz)
ESP.getFreeHeap()
功能:获取可用堆内存
Serial.println(ESP.getFreeHeap());
- 返回值:整数 - 可用内存字节数
- 使用场景:内存监控、调试内存泄漏
6. 中断服务程序(ISR)规范
IRAM_ATTR 属性
功能:将函数放在RAM中执行
void IRAM_ATTR buttonISR() {
interruptTriggered = true;
}
- 作用:提高中断响应速度
- 必要性:ESP32中断函数必须添加此属性
- 原因:Flash访问速度慢,中断需要快速响应
volatile 关键字
功能:防止编译器优化变量
volatile bool interruptTriggered = false;
- 作用:确保变量在中断和主程序间正确同步
- 使用场景:所有在ISR中修改的全局变量
- 原因:防止编译器将变量优化到寄存器中
7. 函数使用最佳实践
✅ 推荐做法
// 1. 在setup()中初始化
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
}
// 2. ISR保持简短
void IRAM_ATTR buttonISR() {
interruptTriggered = true; // 只设置标志
}
// 3. 使用非阻塞延迟
if (millis() - lastTime > interval) {
// 执行任务
lastTime = millis();
}
// 4. 合理使用串口输出
#ifdef DEBUG
Serial.println("Debug info");
#endif
❌ 避免做法
// 1. 在ISR中使用复杂函数
void IRAM_ATTR buttonISR() {
Serial.println("Button!"); // ❌ 不要在ISR中使用
delay(100); // ❌ 不要在ISR中使用
}
// 2. 忘记volatile关键字
bool flag = false; // ❌ ISR中修改的变量必须用volatile
// 3. 过度使用delay()
void loop() {
delay(1000); // ❌ 阻塞程序执行
// 其他代码无法及时执行
}
// 4. 未检查溢出
unsigned long time = millis();
if (time > 1000) { // ❌ 溢出后会出错
// 应该使用: (millis() - lastTime) > 1000
}
8. 函数速查表
| 函数 | 功能 | 返回值 | 使用频率 |
|---|---|---|---|
Serial.begin() | 初始化串口 | 无 | ⭐⭐⭐⭐⭐ |
Serial.println() | 串口输出 | 字节数 | ⭐⭐⭐⭐⭐ |
pinMode() | 配置引脚模式 | 无 | ⭐⭐⭐⭐⭐ |
digitalRead() | 读取数字电平 | HIGH/LOW | ⭐⭐⭐⭐⭐ |
digitalWrite() | 输出数字电平 | 无 | ⭐⭐⭐⭐⭐ |
attachInterrupt() | 绑定中断 | 无 | ⭐⭐⭐⭐ |
millis() | 获取运行时间 | 毫秒数 | ⭐⭐⭐⭐⭐ |
delay() | 阻塞延迟 | 无 | ⭐⭐⭐ |
ESP.getChipModel() | 获取芯片型号 | 字符串 | ⭐⭐ |
9. 学习建议
- 循序渐进:先掌握基础函数(Serial、pinMode、digitalWrite)
- 实践为主:每个函数都要亲自测试
- 查阅文档:遇到问题查看官方API文档
- 代码规范:养成良好的编码习惯
- 调试技巧:善用串口输出调试信息
10. 相关资源
🔬 原理分析
按键抖动问题
机械按键在按下或释放瞬间,由于机械弹片的弹性作用,会产生一连串的抖动信号:
理想信号:
HIGH ────┐
└──────── LOW
实际信号(有抖动):
HIGH ────┐ ┌┐┌┐
└─┘└┘└─── LOW
←抖动期→
(5-10ms)
消抖方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 硬件消抖 | 可靠性高 | 需要额外元件 | 专业产品 |
| 简单延迟 | 实现简单 | 阻塞程序 | 简单应用 |
| 定时器消抖 | 不阻塞 | 占用定时器 | 多任务系统 |
| 本教程方案 | 可靠、灵活 | 代码稍复杂 | 推荐使用 |
中断 vs 轮询
中断方式(本教程采用):
- ✅ 响应及时,不会漏检
- ✅ CPU效率高,大部分时间可处理其他任务
- ✅ 适合实时性要求高的场景
轮询方式:
- ❌ 需要频繁检测,占用CPU
- ❌ 可能漏检短暂按键
- ✅ 代码简单,适合学习
🚀 编译与运行
快速操作
图形界面(推荐):
- ✓ 编译:点击底部工具栏或按
Ctrl+Alt+B - → 上传:点击底部工具栏或按
Ctrl+Alt+U - 🔌 串口监视器:点击底部工具栏或按
Ctrl+Alt+S
命令行:
pio run -t upload && pio device monitor
常见问题
| 问题 | 解决方法 |
|---|---|
| 串口被占用 | 关闭其他串口工具,重新插拔USB线 |
| 乱码输出 | 检查波特率是否为115200 |
| 无输出 | 按下RESET按键重启程序 |
📊 运行效果
启动信息
========== ESP32-S3 Button Test ==========
Board: LiChuang EDA ESP32-S3
Button: BOOT button on IO0
Press the BOOT button to test...
==========================================
System ready! Waiting for button press...
按键按下输出
========================================
>>> BUTTON PRESSED! <<<
Press Count: 1
Time: 5234 ms
Uptime: 5 seconds
========================================
--- Button Released ---
多次按键输出
========================================
>>> BUTTON PRESSED! <<<
Press Count: 2
Time: 8456 ms
Uptime: 8 seconds
========================================
--- Button Released ---
========================================
>>> BUTTON PRESSED! <<<
Press Count: 3
Time: 12789 ms
Uptime: 12 seconds
========================================
--- Button Released ---
🎯 实验任务
基础任务
- ✅ 完成硬件连接
- ✅ 上传代码并测试
- ✅ 观察串口输出
- ✅ 验证消抖效果
进阶任务
-
修改消抖参数
- 尝试修改
DEBOUNCE_DELAY为 20ms、100ms - 观察不同延迟对消抖效果的影响
- 尝试修改
-
添加长按检测
- 检测按键按下超过2秒
- 打印"长按检测"信息
-
双击检测
- 检测500ms内连续按下两次
- 打印"双击检测"信息
-
多按键扩展
- 添加第二个按键(如IO1)
- 实现多按键独立检测
挑战任务
-
状态机实现
- 使用状态机重构消抖逻辑
- 支持:空闲、按下、长按、释放等状态
-
按键组合
- 检测两个按键同时按下
- 实现类似"Ctrl+C"的组合键功能
-
PWM调光
- 按键控制LED亮度
- 短按增加亮度,长按减少亮度
🐛 常见问题
1. 按键无响应
可能原因:
- 按错了按键(确认按的是BOOT按键,不是RESET按键)
- 开发板未正常供电
- 引脚配置错误
- 按键损坏(极少见)
解决方法:
// 在loop中添加调试代码
Serial.print("Button State: ");
Serial.println(digitalRead(BUTTON_PIN));
delay(500);
2. 重复触发
可能原因:
- 消抖时间太短
- 按键质量差,抖动严重
解决方法:
// 增加消抖延迟
const unsigned long DEBOUNCE_DELAY = 100; // 改为100ms
// 增加稳定读数次数
const int STABLE_COUNT = 5; // 改为5次
3. 漏检按键
可能原因:
- 按键按下时间太短
- 中断未正确配置
解决方法:
// 检查中断配置
Serial.println("Interrupt attached!");
// 在ISR中添加计数
volatile int isrCount = 0;
void IRAM_ATTR buttonISR() {
isrCount++;
interruptTriggered = true;
}
4. 串口无输出
检查项:
- ✅ 波特率是否为115200
- ✅ USB线是否支持数据传输
- ✅ 驱动是否正确安装
- ✅ 串口端口是否选择正确
📚 扩展知识
GPIO上拉/下拉电阻
上拉电阻配置(INPUT_PULLUP):
VCC (3.3V)
|
[R] 47kΩ (内部上拉)
|
GPIO ────── 按键 ──── GND
|
读取状态
按键未按下:GPIO = HIGH (3.3V)
按键按下: GPIO = LOW (0V)
中断触发模式
| 模式 | 说明 | 适用场景 |
|---|---|---|
LOW | 低电平触发 | 持续检测 |
HIGH | 高电平触发 | 持续检测 |
CHANGE | 电平变化触发 | 双边沿检测 |
FALLING | 下降沿触发 | 按键按下 |
RISING | 上升沿触发 | 按键释放 |
ESP32-S3 GPIO特性
- 总GPIO数量:45个
- 支持中断的GPIO:所有GPIO
- 内部上拉/下拉:所有GPIO支持
- 特殊引脚:
- IO0:BOOT引脚
- IO19/IO20:USB D-/D+
- IO43/IO44:UART0 TX/RX
💡 优化建议
1. 代码优化
// 使用枚举定义按键状态
enum ButtonState {
BUTTON_IDLE,
BUTTON_PRESSED,
BUTTON_RELEASED,
BUTTON_LONG_PRESS
};
// 使用结构体管理按键
struct Button {
uint8_t pin;
uint8_t state;
unsigned long pressTime;
unsigned long releaseTime;
};
2. 性能优化
// 减少不必要的串口输出
#define DEBUG_MODE 1
#if DEBUG_MODE
Serial.println("Debug info");
#endif
// 使用位操作提高效率
#define SET_BIT(reg, bit) ((reg) |= (1 << (bit)))
#define CLEAR_BIT(reg, bit) ((reg) &= ~(1 << (bit)))
3. 可维护性优化
// 使用函数封装
bool isButtonPressed() {
return (buttonState == LOW);
}
void handleButtonPress() {
buttonPressCount++;
printButtonInfo();
}
void printButtonInfo() {
Serial.println(">>> BUTTON PRESSED! <<<");
// ...
}
📖 参考资料
官方文档
相关教程
- Arduino 中断教程
- ESP32 GPIO 编程指南
- 按键消抖算法详解
开源项目
📝 总结
通过本教程,你学习了:
✅ ESP32-S3的GPIO配置和使用
✅ 中断服务程序的编写规范
✅ 完善的按键消抖算法实现
✅ 边沿检测和状态管理技术
✅ 串口调试和问题排查方法
关键要点:
- 中断处理要快速,复杂逻辑放主循环
- 消抖是必须的,多层保护更可靠
- 状态管理要清晰,避免逻辑混乱
- 调试信息要充分,便于问题定位
🎓 下一步学习
- 定时器实验:学习硬件定时器的使用
- PWM实验:控制LED亮度和舵机
- ADC实验:读取模拟传感器数据
- WiFi实验:连接网络,实现物联网功能
- OLED显示:显示按键状态和信息
👨💻 作者信息
教程版本:v1.0
更新日期:2025-12-21
适用平台:立创实战派 ESP32-S3 + Arduino Framework
开发环境:PlatformIO / Arduino IDE
开发板:立创实战派(LiChuang EDA)ESP32-S3
📄 许可证
本教程采用 MIT 许可证,欢迎学习和分享!
祝你学习愉快!如有问题,欢迎交流讨论。 🎉
更多推荐


所有评论(0)