บทความ › ไมโครคอนโทรลเลอร์และโค้ด

ต่อปุ่มกดกับ ESP32 ให้นิ่ง pull-up และ debounce ด้วย millis()

ทำไมขาอินพุตลอยแล้วอ่านค่ามั่ว pull-up กับ pull-down ต่างกันยังไง ปุ่มเด้งกี่มิลลิวินาที โค้ด debounce ด้วย millis() และขา strapping ที่ไม่ควรต่อปุ่ม

เริ่มต้น อ่าน 8 นาที 22 ตุลาคม 2569 อิเล็กทรอนิกส์ ช่างมือใหม่ · บทที่ 7/10

อ่านจบแล้วจะรู้
  • ขาอินพุตที่ไม่ได้ต่ออะไรจะลอย อ่านได้ทั้ง 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ไม่ต้องHIGHLOWกรณีทั่วไป ต่อปุ่มลง GND
INPUT_PULLDOWNไม่ต้องLOWHIGHอยากให้กดแล้วได้ 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 ครั้งต่อวินาที เร็วกว่าที่นิ้วคนทำได้มาก

0 msกดปุ่ม1-6 msหน้าสัมผัสเด้ง ค่ากระพริบหลังจากนั้นค่านิ่ง เริ่มนับเวลาครบ 30 msค่ายังเท่าเดิมยอมรับนับเป็นการกด 1 ครั้ง
รอให้ค่าคงเดิมครบเวลา debounce ก่อนยอมรับว่ากด

โค้ด 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, 15GPIO34–39 เป็นอินพุตอย่างเดียว ไม่มี pull-up/pull-down ในตัว ต้องต่อตัวต้านทานเอง ขาที่ต่อกับ flash ในตัวห้ามใช้
ESP32-C3GPIO2, 8, 9GPIO18/19 เป็นขา USB โดยค่าเริ่มต้น
ESP32-S3GPIO0, 3, 45, 46GPIO19/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 หนึ่งการกดเรียกฟังก์ชันหลายครั้ง

แหล่งข้อมูล

คำถามที่พบบ่อย

ใช้ delay(50) หลังอ่านปุ่มแทน millis() ได้ไหม

ได้ในโปรแกรมที่ทำอย่างเดียว แต่ delay() หยุดทั้งโปรแกรม ระหว่างนั้นไฟ LED เซนเซอร์ หรือ Wi-Fi ที่ต้องอัปเดตจะค้าง ใช้ millis() แล้วโปรแกรมยังวิ่งต่อได้

ใช้ interrupt กับปุ่มแล้วนับได้หลายครั้งต่อการกดหนึ่งครั้ง

เพราะ interrupt ทำงานทุกครั้งที่ขาเปลี่ยนสถานะ รวมถึงตอนเด้ง ต้อง debounce ใน interrupt ด้วย เช่น จำเวลาครั้งล่าสุดแล้วไม่สนใจครั้งที่ห่างกันไม่ถึงช่วงที่ตั้งไว้

ต่อปุ่มกับ GPIO0 แล้วอัปโหลดโค้ดไม่ได้ หรือบอร์ดไม่บูต

GPIO0 เป็นขา strapping ถ้าถูกดึงลง LOW ตอนเปิดเครื่อง ชิปจะเข้าโหมดดาวน์โหลดแทนรันโปรแกรม ย้ายปุ่มไปขาอื่นที่ไม่ใช่ strapping

วงจรและโค้ดในบทความทดสอบกับบอร์ดตัวอย่าง บอร์ดต่างรุ่นอาจใช้ขาต่างกัน ตรวจ pinout ของบอร์ดที่ใช้ก่อนต่อไฟเสมอ

อ่านต่อ

#โค้ดเริ่มต้น

อ่านอุณหภูมิ ความชื้น ความกดอากาศ จาก BME280 ผ่าน I2C ด้วย ESP32

ต่อสาย I2C สองเส้น หา address ด้วย I2C scanner อ่านค่าด้วยไลบรารี Adafruit แยก BME280 กับ BMP280 ที่หน้าตาเหมือนกัน และทำไมอุณหภูมิที่อ่านได้มักสูงเกินจริง

อ่าน 6 นาที
#โค้ดกลาง

รีโมตของเล่นด้วย ESP-NOW บอร์ดคุยกันตรง ไม่ต้องมี เราเตอร์

ใช้ ESP-NOW ส่งคำสั่งจาก ESP32 ตัวหนึ่งไปอีกตัวโดยไม่ผ่าน Wi-Fi บ้าน ข้อความไม่เกิน 250 ไบต์ โค้ด MicroPython ตัวส่งตัวรับ ระบบหยุดเมื่อสัญญาณหาย และโหมด long range

อ่าน 8 นาที
#โค้ดเริ่มต้น

ESP32-C5 ชิป ESP32 ตัวแรกที่ต่อ Wi-Fi 5 GHz ได้ ของเล่นเราได้อะไร

ESP32-C5 ผลิตจริงตั้งแต่ เม.ย. 2025 เป็นชิปแรกของ Espressif ที่มี Wi-Fi 6 ทั้ง 2.4 และ 5 GHz พร้อม BLE และ Thread/Zigbee เมื่อไหร่ 5 GHz ช่วยของเล่น เมื่อไหร่ไม่ช่วย

อ่าน 6 นาที