บทความ › ไมโครคอนโทรลเลอร์และโค้ด
ต่อปุ่มกดกับ ESP32 ให้นิ่ง pull-up และ debounce ด้วย millis()
ทำไมขาอินพุตลอยแล้วอ่านค่ามั่ว pull-up กับ pull-down ต่างกันยังไง ปุ่มเด้งกี่มิลลิวินาที โค้ด debounce ด้วย millis() และขา strapping ที่ไม่ควรต่อปุ่ม
- ขาอินพุตที่ไม่ได้ต่ออะไรจะลอย อ่านได้ทั้ง HIGH และ LOW แบบสุ่ม ต้องมี pull-up หรือ pull-down เสมอ
- ใช้ INPUT_PULLUP แล้วต่อปุ่มลง GND ง่ายที่สุด แต่ตรรกะกลับด้าน กดแล้วอ่านได้ LOW
- หน้าสัมผัสปุ่มเด้งเฉลี่ยราว 1.6 ms แต่บางตัวนานกว่านั้นมาก ตั้งเวลา debounce 20–50 ms
- ESP32 รุ่นแรกเลี่ยงขา GPIO0, 2, 5, 12, 15 และขา 34–39 ไม่มี pull-up ในตัว รุ่นอื่นต้องเช็ก datasheet ของรุ่นนั้น
ปุ่มกดดูเป็นอุปกรณ์ที่ง่ายที่สุดในวงจร แต่ต่อตรง ๆ แล้วมักได้สองอาการ: ไม่กดก็อ่านค่าได้มั่ว และกดครั้งเดียวโปรแกรมนับได้หลายครั้ง อาการแรกแก้ด้วยตัวต้านทาน pull-up หรือ pull-down อาการที่สองแก้ด้วย debounce บทนี้อธิบายทั้งสองเรื่อง พร้อมโค้ดและรายชื่อขาที่ไม่ควรต่อปุ่ม
ตัวอย่างใช้บอร์ด ESP32 DevKit (ESP32-WROOM-32) เขียนด้วย Arduino IDE 2 และ arduino-esp32 core
ขาลอย: ทำไมไม่กดก็อ่านได้มั่ว
ขา GPIO ที่ตั้งเป็นอินพุตมีความต้านทานสูงมาก ถ้าไม่ได้ต่อกับอะไรเลย แรงดันที่ขาจะถูกกำหนดโดยสัญญาณรบกวนรอบ ๆ เช่น มือที่เข้าใกล้หรือสายไฟข้าง ๆ โปรแกรมจึงอ่านได้ HIGH บ้าง LOW บ้าง สภาพนี้เรียกว่า ขาลอย (floating)
ทางแก้คือต่อตัวต้านทานให้ขามีสถานะเริ่มต้นที่แน่นอน
- Pull-up: ตัวต้านทานต่อจากขาไป 3.3 V ปุ่มต่อจากขาลง GND ไม่กด = HIGH กด = LOW
- Pull-down: ตัวต้านทานต่อจากขาลง GND ปุ่มต่อจากขาไป 3.3 V ไม่กด = LOW กด = HIGH
ชิป ESP32 มีตัวต้านทาน pull-up และ pull-down อยู่ในตัว เอกสาร arduino-esp32 ระบุว่ามีค่าราว 45 kΩ เปิดใช้ได้ด้วย pinMode()
| โหมด | ต้องต่อตัวต้านทานเอง | ไม่กดอ่านได้ | กดอ่านได้ | ใช้เมื่อไร |
|---|---|---|---|---|
INPUT | ต้อง (ไม่งั้นลอย) | ขึ้นกับวงจร | ขึ้นกับวงจร | ขา 34–39 หรือสายยาวที่อยากใช้ค่าต้านทานต่ำกว่า |
INPUT_PULLUP | ไม่ต้อง | HIGH | LOW | กรณีทั่วไป ต่อปุ่มลง GND |
INPUT_PULLDOWN | ไม่ต้อง | LOW | HIGH | อยากให้กดแล้วได้ HIGH |
คำนวณกระแสผ่านปุ่ม
ตอนกดปุ่ม กระแสไหลจาก 3.3 V ผ่านตัวต้านทาน pull-up ลง GND ตาม Ohm’s law
- ใช้ pull-up ภายในราว 45 kΩ: 3.3 ÷ 45,000 ≈ 0.073 mA
- ใช้ตัวต้านทานภายนอก 10 kΩ: 3.3 ÷ 10,000 = 0.33 mA
ทั้งสองค่าน้อยมาก ไม่มีผลกับแบต สำหรับปุ่มบนบอร์ดที่สายสั้น pull-up ภายในพอ ถ้าสายไปปุ่มยาวหลายสิบเซนติเมตรและเจอสัญญาณรบกวน ค่อยเปลี่ยนเป็นตัวต้านทานภายนอกค่าต่ำลง เช่น 10 kΩ
ปุ่มเด้ง (bounce) นานแค่ไหน
หน้าสัมผัสโลหะในปุ่มไม่ได้แตะกันครั้งเดียวแล้วนิ่ง มันกระทบแล้วเด้งหลายรอบก่อนจะติดกันสนิท ไมโครคอนโทรลเลอร์อ่านเร็วพอจะเห็นทุกรอบ จึงนับเป็นการกดหลายครั้ง
Jack Ganssle วัดเวลาเด้งของสวิตช์หลายแบบ ได้ผลประมาณนี้
- เด้งเฉลี่ยราว 1.6 ms
- ส่วนใหญ่ไม่เกินราว 6.2 ms
- มีตัวที่แย่มาก เด้งตอนปล่อยนานถึง 157 ms
เขาแนะนำให้ตั้งเวลา debounce ในช่วง 20–50 ms และทดลองพบว่าหน่วง 100 ms คนรู้สึกได้ชัด แต่ 50 ms ยังรู้สึกว่าตอบสนองทันที
debounce ไม่ได้ทำให้ปุ่มช้า ถ้าตั้งไม่เกิน 50 ms คนกดไม่รู้สึกว่ามีการหน่วง
ตัวอย่าง ถ้าตั้ง 30 ms แล้วผู้ใช้กดรัวที่สุด การกดหนึ่งครั้งประกอบด้วยช่วงกดค้างและช่วงปล่อย ซึ่งต่างต้องนิ่งอย่างน้อย 30 ms รวม 60 ms ต่อครั้ง → กดได้สูงสุดราว 1,000 ÷ 60 ≈ 16 ครั้งต่อวินาที เร็วกว่าที่นิ้วคนทำได้มาก
โค้ด debounce ด้วย millis()
หลักคือ: อ่านขา ถ้าค่าเปลี่ยนให้จดเวลาไว้ แล้วยอมรับค่าใหม่ก็ต่อเมื่อค่าคงเดิมนานเกินช่วงที่ตั้ง โค้ดนี้กดหนึ่งครั้งสลับไฟ LED เปิด/ปิดหนึ่งครั้ง
const int BUTTON_PIN = 4; // ปุ่มต่อจาก GPIO4 ลง GND
const int LED_PIN = 23; // LED + ตัวต้านทาน ต่อจาก GPIO23 ลง GND
const unsigned long DEBOUNCE_MS = 30;
int stableState = HIGH; // สถานะที่ยอมรับแล้ว (pull-up: ไม่กด = HIGH)
int lastReading = HIGH; // ค่าที่อ่านได้ครั้งก่อน
unsigned long lastChange = 0; // เวลาที่ค่าที่อ่านเปลี่ยนครั้งล่าสุด
bool ledOn = false;
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
int reading = digitalRead(BUTTON_PIN);
if (reading != lastReading) { // ยังเด้งอยู่ หรือเพิ่งเปลี่ยน: เริ่มนับเวลาใหม่
lastChange = millis();
lastReading = reading;
}
if (millis() - lastChange >= DEBOUNCE_MS && reading != stableState) {
stableState = reading; // ค่านิ่งพอแล้ว ยอมรับ
if (stableState == LOW) { // pull-up: กด = LOW
ledOn = !ledOn;
digitalWrite(LED_PIN, ledOn);
}
}
// ทำงานอื่นต่อได้ ไม่มี delay() มาขวาง
}
สองจุดที่ตั้งใจเขียนแบบนี้
millis() - lastChangeใช้unsigned longลบกัน ทำให้ยังถูกต้องเมื่อmillis()วนกลับเป็น 0 (ราว 49.7 วัน) ห้ามเขียนเป็นmillis() > lastChange + DEBOUNCE_MS- ทำงานเฉพาะตอนเปลี่ยนเป็น LOW (ขอบกด) ไม่ใช่ทุกรอบที่ปุ่มถูกกดค้าง
วิธีคำนวณค่าตัวต้านทานของ LED ที่ขา 23 ดูได้ในบทต่อ LED กับ ESP32
ขาที่ไม่ควรต่อปุ่ม (strapping pins)
ขา strapping คือขาที่ชิปอ่านสถานะตอนเปิดเครื่องหรือรีเซ็ต เพื่อตัดสินว่าจะบูตแบบไหน ถ้าต่อปุ่มหรือวงจรที่ดึงขาไปผิดระดับตอนนั้น บอร์ดอาจไม่บูต หรือเข้าโหมดดาวน์โหลดเอง ขาเหล่านี้ต่างกันตามรุ่นชิป
| ชิป | ขา strapping | เรื่องที่ต้องรู้เพิ่ม |
|---|---|---|
| ESP32 (รุ่นแรก) | GPIO0, 2, 5, 12, 15 | GPIO34–39 เป็นอินพุตอย่างเดียว ไม่มี pull-up/pull-down ในตัว ต้องต่อตัวต้านทานเอง ขาที่ต่อกับ flash ในตัวห้ามใช้ |
| ESP32-C3 | GPIO2, 8, 9 | GPIO18/19 เป็นขา USB โดยค่าเริ่มต้น |
| ESP32-S3 | GPIO0, 3, 45, 46 | GPIO19/20 เป็นขา USB โดยค่าเริ่มต้น |
ตัวเลขในตารางมาจาก datasheet และ hardware design guidelines ของ Espressif ถ้าใช้รุ่นอื่น หรือบอร์ดที่ผู้ผลิตต่อขาไว้ใช้งานอื่น (เช่น ไฟ LED บนบอร์ด) เช็กจากเอกสารของรุ่นนั้นก่อน ขาที่ปลอดภัยสำหรับปุ่มบน ESP32 รุ่นแรก เช่น GPIO4, 13, 14, 18, 19, 21–23, 25–27, 32, 33 (เว้น GPIO1 และ 3 ไว้ให้ UART ที่ใช้อัปโหลดโค้ด)
ข้อผิดพลาดที่เจอบ่อย
- ไม่มี pull-up/pull-down ไม่กดก็ทำงาน เอามือเข้าใกล้ก็ทำงาน
- ใช้ขา 34–39 กับ INPUT_PULLUP ขากลุ่มนี้ไม่มีตัวต้านทานภายใน ค่ายังลอยเหมือนเดิม
- ลืมว่าตรรกะกลับด้าน เขียน
if (digitalRead(pin) == HIGH)แล้วงงว่าทำไมไม่กดก็ทำงาน - ต่อปุ่มกับขา strapping บอร์ดไม่บูตเมื่อเปิดเครื่องตอนกดปุ่มค้าง
- ตั้ง debounce สั้นเกิน เช่น 2 ms ซึ่งน้อยกว่าเวลาเด้งของสวิตช์หลายตัว
- ใช้ interrupt โดยไม่ debounce หนึ่งการกดเรียกฟังก์ชันหลายครั้ง
แหล่งข้อมูล
- A Guide to Debouncing — Jack Ganssle
- A Guide to Debouncing, Part 2 — Jack Ganssle
- GPIO API — Arduino-ESP32 documentation (Espressif)
- ESP32 Series Datasheet — Espressif
- ESP32-C3 Hardware Design Guidelines: Schematic Checklist — Espressif
- ESP32-S3 Hardware Design Guidelines: Schematic Checklist — Espressif
คำถามที่พบบ่อย
ใช้ delay(50) หลังอ่านปุ่มแทน millis() ได้ไหม
ได้ในโปรแกรมที่ทำอย่างเดียว แต่ delay() หยุดทั้งโปรแกรม ระหว่างนั้นไฟ LED เซนเซอร์ หรือ Wi-Fi ที่ต้องอัปเดตจะค้าง ใช้ millis() แล้วโปรแกรมยังวิ่งต่อได้
ใช้ interrupt กับปุ่มแล้วนับได้หลายครั้งต่อการกดหนึ่งครั้ง
เพราะ interrupt ทำงานทุกครั้งที่ขาเปลี่ยนสถานะ รวมถึงตอนเด้ง ต้อง debounce ใน interrupt ด้วย เช่น จำเวลาครั้งล่าสุดแล้วไม่สนใจครั้งที่ห่างกันไม่ถึงช่วงที่ตั้งไว้
ต่อปุ่มกับ GPIO0 แล้วอัปโหลดโค้ดไม่ได้ หรือบอร์ดไม่บูต
GPIO0 เป็นขา strapping ถ้าถูกดึงลง LOW ตอนเปิดเครื่อง ชิปจะเข้าโหมดดาวน์โหลดแทนรันโปรแกรม ย้ายปุ่มไปขาอื่นที่ไม่ใช่ strapping
วงจรและโค้ดในบทความทดสอบกับบอร์ดตัวอย่าง บอร์ดต่างรุ่นอาจใช้ขาต่างกัน ตรวจ pinout ของบอร์ดที่ใช้ก่อนต่อไฟเสมอ