ต่อปุ่มกดกับ ESP32: INPUT_PULLUP, debounce และกดค้าง
ปุ่มอ่านค่ามั่วหรือกดครั้งเดียวนับหลายครั้ง วิธีต่อปุ่มกับ ESP32 ด้วย INPUT_PULLUP ทำ debounce ด้วย millis() แยกกดสั้นกับกดค้าง และทำกันชนด้วย micro switch
ต่อปุ่มระหว่างขา GPIO กับ GND แล้วตั้ง pinMode(pin, INPUT_PULLUP) ขาจะอ่านได้ HIGH ตอนปล่อย และ LOW ตอนกด
ไม่ต้องใช้ตัวต้านทานเพิ่ม จากนั้นทำ debounce คือรอให้ค่านิ่งสัก 20–50 ms ก่อนเชื่อ เพราะหน้าสัมผัสของปุ่มเด้งไปมาตอนกด
ทำให้กดครั้งเดียวแต่โปรแกรมนับได้หลายครั้ง
ทำไมขาที่ไม่ได้ต่ออะไรอ่านค่ามั่ว
ถ้าต่อปุ่มกับ 3.3 V แล้วตั้ง INPUT เฉยๆ ตอนกดขาจะได้ 3.3 V แต่ตอนปล่อยขาไม่ได้ต่อกับอะไรเลย (ขาลอย, floating)
สายไฟทำตัวเป็นเสาอากาศรับสัญญาณรบกวน ค่าที่อ่านได้จึงสลับ HIGH / LOW เองแม้ไม่มีใครแตะ แค่เอามือเข้าใกล้ก็เปลี่ยน
วิธีแก้คือใส่ตัวต้านทานดึงขาไว้ที่ค่าหนึ่งเสมอ ESP32 มีตัวต้านทานแบบนี้อยู่ในชิปแล้ว (ราว 45 kΩ) เปิดใช้ด้วย INPUT_PULLUP:
3V3 ──[ 45 kΩ ในชิป ]──┬──▶ GPIO 27 ปล่อย: ดึงขึ้นเป็น HIGH
│
[ปุ่ม]
│
GND กด: ต่อลง GND เป็น LOW
| ขา ESP32 | ใช้กับปุ่มได้ไหม |
|---|---|
| GPIO 4, 13, 14, 16–19, 21–23, 25–27, 32, 33 | ได้ มี pull-up ในตัว |
| GPIO 34, 35, 36 (VP), 39 (VN) | ได้ แต่ไม่มี pull-up ในตัว ต้องต่อตัวต้านทาน 10 kΩ จากขาไปที่ 3V3 เอง |
| GPIO 0, 2, 5, 12, 15 | เลี่ยง เป็นขา strapping ถ้ากดค้างตอนเปิดเครื่อง บอร์ดอาจไม่บูตหรืออัปโหลดไม่ได้ (ดู ESP32 อัปโหลดโค้ดไม่ได้) |
ปุ่ม BOOT บนบอร์ด DevKit ต่ออยู่กับ GPIO 0 อยู่แล้ว หลังบูตเสร็จใช้เป็นปุ่มทดลองในโปรแกรมได้ด้วย pinMode(0, INPUT_PULLUP)
ไม่ต้องต่ออะไรเพิ่ม
Debounce: กดครั้งเดียวต้องนับครั้งเดียว
หน้าสัมผัสโลหะในปุ่มกระทบกันแล้วเด้งหลายรอบในช่วงไม่กี่มิลลิวินาทีแรก ESP32 อ่านขาได้เร็วกว่านั้นมาก จึงเห็นเป็น LOW–HIGH–LOW–HIGH หลายรอบ
โค้ดด้านล่างจำเวลาที่ค่าเปลี่ยนล่าสุด และยอมรับค่าใหม่เมื่อค่านั้นนิ่งมาแล้ว 30 ms ไม่ใช้ delay() โปรแกรมส่วนอื่นจึงทำงานต่อได้
const int BUTTON = 27;
const unsigned long DEBOUNCE_MS = 30;
bool stable = HIGH; // ค่าที่เชื่อได้แล้ว (HIGH = ปล่อย)
bool lastRead = HIGH; // ค่าที่อ่านได้รอบก่อน
unsigned long changedAt = 0; // เวลาที่ค่าเปลี่ยนล่าสุด
// คืน true ครั้งเดียวตอนเริ่มกด
bool pressed() {
bool now = digitalRead(BUTTON);
if (now != lastRead) { lastRead = now; changedAt = millis(); }
if (millis() - changedAt > DEBOUNCE_MS && now != stable) {
stable = now;
return stable == LOW;
}
return false;
}
int count = 0;
void setup() {
Serial.begin(115200);
pinMode(BUTTON, INPUT_PULLUP);
}
void loop() {
if (pressed()) Serial.println(++count); // กดหนึ่งครั้ง เลขเพิ่มหนึ่ง
}
ลองเปลี่ยน DEBOUNCE_MS เป็น 0 แล้วกดปุ่มช้าๆ จะเห็นเลขกระโดดทีละ 2–5 ปุ่มราคาถูกบางตัวเด้งนานกว่า 20 ms ถ้า 30 ms ยังนับเกิน ให้เพิ่มเป็น 50
แยกกดสั้นกับกดค้าง
ปุ่มเดียวทำได้สองหน้าที่ เช่น กดสั้นเปลี่ยนหน้าจอ กดค้าง 1 วินาทีบันทึกค่า โค้ดนี้เก็บสถานะของแต่ละปุ่มไว้ใน struct จึงใช้กับหลายปุ่มได้
const unsigned long DEBOUNCE_MS = 30, LONG_MS = 1000;
struct Button {
int pin;
bool stable = HIGH, lastRead = HIGH, longFired = false;
unsigned long changedAt = 0, downAt = 0;
};
enum Event { NONE, CLICK, LONG_PRESS };
Event update(Button &b) {
bool now = digitalRead(b.pin);
unsigned long t = millis();
if (now != b.lastRead) { b.lastRead = now; b.changedAt = t; }
if (t - b.changedAt > DEBOUNCE_MS && now != b.stable) {
b.stable = now;
if (now == LOW) { b.downAt = t; b.longFired = false; } // เริ่มกด
else if (!b.longFired) return CLICK; // ปล่อยก่อนครบ 1 วินาที
}
if (b.stable == LOW && !b.longFired && t - b.downAt > LONG_MS) {
b.longFired = true; // แจ้งครั้งเดียวต่อการกดค้าง
return LONG_PRESS;
}
return NONE;
}
Button modeButton = { 27 };
Button setButton = { 14 };
void setup() {
Serial.begin(115200);
pinMode(modeButton.pin, INPUT_PULLUP);
pinMode(setButton.pin, INPUT_PULLUP);
}
void loop() {
Event e = update(modeButton);
if (e == CLICK) Serial.println("เปลี่ยนหน้า");
if (e == LONG_PRESS) Serial.println("บันทึกค่า");
if (update(setButton) == CLICK) Serial.println("ปุ่มที่สอง");
}
กดสั้นจะรู้ผลตอนปล่อยปุ่ม เพราะตอนกดลงไปโปรแกรมยังไม่รู้ว่าคนจะกดค้างหรือเปล่า ถ้าใช้ปุ่มนั้นกดสั้นอย่างเดียว ให้ใช้ pressed() แบบแรกซึ่งตอบสนองตอนกดลงทันที
Debounce ด้วยฮาร์ดแวร์
ใส่ตัวเก็บประจุ 100 nF (0.1 µF) คร่อมขาปุ่ม (ระหว่าง GPIO กับ GND) จะกรองการเด้งได้เกือบหมด เพราะตัวเก็บประจุกับ pull-up 45 kΩ ทำให้แรงดันเปลี่ยนช้าลงเหลือราว 5 ms วิธีนี้ช่วยมากเมื่อใช้ interrupt หรือสายปุ่มยาวและมีสัญญาณรบกวน แต่โดยทั่วไป debounce ด้วยโค้ดก็พอ
Micro switch ทำกันชนหุ่นยนต์
Micro switch แบบมีก้านใช้ทำ bumper ได้ดี เมื่อหุ่นชนผนังก้านจะกดสวิตช์ สวิตช์มีสามขา: C (common), NO (ปกติเปิด) และ NC (ปกติปิด)
- ต่อ C กับ GND และ NO กับ GPIO ใช้โค้ดเดียวกับปุ่มกด ชนแล้วอ่านได้ LOW
- ต่อ C กับ GND และ NC กับ GPIO กลับกัน ปกติอ่านได้ LOW ชนแล้วเป็น HIGH ข้อดีคือถ้าสายหลุด ขาจะเป็น HIGH เหมือนชน หุ่นจึงหยุดแทนที่จะวิ่งชนต่อโดยไม่รู้ตัว
หุ่นที่วิ่งเร็วควรเช็กกันชนทุกรอบของ loop() และหยุดมอเตอร์ทันทีที่ชน ไม่ต้องรอ debounce ครบ เพราะการชนแม้แค่สั้นๆ ก็ควรหยุด
ส่วน debounce ใช้ตอนตัดสินว่า "ชนกี่ครั้ง" หรือรอให้ปล่อยก่อนถอยออก
ควรใช้ interrupt ไหม
attachInterrupt() เรียกฟังก์ชันทันทีที่ขาเปลี่ยน แต่การเด้งของปุ่มก็ทำให้ interrupt ทำงานหลายครั้งเช่นกัน และในฟังก์ชัน interrupt
ใช้ Serial หรือ delay() ไม่ได้ สำหรับปุ่มที่คนกด การอ่านใน loop() แบบด้านบนเร็วพออยู่แล้ว
interrupt จำเป็นจริงตอนให้ปุ่มปลุก ESP32 จาก deep sleep ซึ่งใช้ esp_sleep_enable_ext0_wakeup(GPIO_NUM_27, 0) ได้กับขาที่เป็น RTC GPIO เช่น 25, 26, 27, 32, 33
ลองในตัวจำลอง
ในตัวจำลองมีเซนเซอร์ button (ปุ่มที่คนกด) และ bumper (กันชน) หุ่นตั้งโต๊ะมีปุ่มบนหัวอยู่แล้ว
กดปุ่ม Head button ที่แถบ Drive หุ่นจะยิ้ม ส่วน bumper เพิ่มได้ในหน้า Robot Body แล้วลองขับไปชนผนัง
โปรเจกต์ที่ใช้ปุ่มแบบนี้: ปุ่มลัด Stream Deck ทำเอง, ไฟช่วยจอดรถในโรงรถ และ หุ่นยนต์ตั้งโต๊ะมีหน้า