Hướng dẫn dùng ngắt GPIO trên ESP32: đọc nút nhấn, chống dội và đếm xung

Hướng dẫn dùng ngắt GPIO trên ESP32 với Arduino IDE: attachInterrupt, các kiểu kích ngắt, chống dội nút nhấn, đếm xung đo tần số và quy tắc viết hàm ngắt an toàn.

Khi chương trình còn bận đọc cảm biến, vẽ màn hình hay gửi dữ liệu, một lần nhấn nút ngắn rất dễ bị bỏ lỡ. Ngắt (interrupt) giải quyết việc này: phần cứng tự theo dõi chân GPIO và gọi ngay một hàm riêng khi tín hiệu thay đổi. Bài viết hướng dẫn dùng ngắt GPIO trên ESP32 với Arduino IDE: đọc nút nhấn có chống dội, đếm xung để đo tần số và những quy tắc cần nhớ khi viết hàm ngắt.

1. Hỏi vòng và ngắt khác nhau thế nào?

So sánh hỏi vòng và ngắt trên ESP32: hỏi vòng bỏ lỡ xung ngắn giữa hai lần đọc, ngắt gọi hàm ngắt ngay khi có sườn xung
Cùng một xung ngắn: cách hỏi vòng có thể bỏ lỡ, cách dùng ngắt ghi nhận được (hình vẽ minh họa).
  • Hỏi vòng (polling): loop() gọi digitalRead() lặp đi lặp lại. Cách này đơn giản nhưng nếu loop() đang bận việc khác thì sự kiện ngắn sẽ bị bỏ qua.
  • Ngắt (interrupt): khi chân đổi trạng thái, ESP32 tạm dừng chương trình chính, chạy một hàm ngắn gọi là hàm phục vụ ngắt (ISR), rồi quay lại làm tiếp.

Ngắt phù hợp với nút nhấn, encoder, cảm biến phát xung như cảm biến lưu lượng và cảm biến tốc độ, hoặc chân báo dữ liệu sẵn sàng của các IC đo lường. Trên ESP32, mọi chân GPIO đọc được tín hiệu đều dùng được làm chân ngắt.

2. Kiểu kích ngắt và hiện tượng dội phím

Tín hiệu nút nhấn với sườn xuống FALLING, sườn lên RISING và hiện tượng dội phím tạo nhiều sườn xuống trong một lần nhấn
Sườn xuống, sườn lên của tín hiệu nút nhấn và hiện tượng dội phím (hình vẽ minh họa, không theo tỉ lệ).
Các kiểu kích ngắt của lệnh attachInterrupt() trên ESP32
KiểuHàm ngắt được gọi khiThường dùng cho
FALLINGChân chuyển từ mức cao xuống mức thấpNút nhấn nối xuống GND
RISINGChân chuyển từ mức thấp lên mức caoĐếm xung, nút nhấn nối lên 3V3
CHANGEChân đổi mức theo cả hai chiềuEncoder, đo độ rộng xung
ONLOW / ONHIGHChân đang ở mức thấp / mức caoÍt dùng; hàm ngắt bị gọi liên tục khi chân giữ mức đó

Dội phím: tiếp điểm cơ khí của nút nhấn không đóng dứt khoát mà nảy vài lần trong vài mili giây. Mỗi lần nảy tạo thêm một sườn xung, nên một lần nhấn có thể gây ra nhiều lần ngắt. Cách xử lý đơn giản nhất là bỏ qua mọi ngắt xảy ra trong khoảng 50 ms sau ngắt đầu tiên.

3. Linh kiện cần chuẩn bị

4. Sơ đồ nối dây

Nối nút nhấn và dây thử đếm xung với ESP32 DevKit V1
Linh kiệnESP32 DevKit V1Ghi chú
Nút nhấn, chân 1D27 / GPIO27Chân ngắt, dùng điện trở kéo lên bên trong chip.
Nút nhấn, chân 2GNDNhấn nút thì GPIO27 xuống mức thấp.
Một dây nốiD25 sang D26Chỉ dùng cho chương trình đếm xung ở mục 7.
Sơ đồ nối nút nhấn giữa D27 và GND của ESP32 DevKit V1, kèm một dây nối D25 sang D26 để thử đếm xung
Sơ đồ nối nút nhấn và dây thử đếm xung (hình vẽ minh họa).

Không cần điện trở ngoài: lệnh pinMode(NUT_PIN, INPUT_PULLUP) bật điện trở kéo lên bên trong chip. Riêng GPIO34–39 không có điện trở kéo bên trong, nếu dùng các chân này phải gắn thêm điện trở khoảng 10 kΩ từ chân lên 3V3.

5. Chương trình 1: nhấn nút để bật, tắt LED

Mỗi lần nhấn nút, hàm ngắt tăng một biến đếm. loop() thấy biến đếm thay đổi thì đảo trạng thái LED có sẵn trên mạch và in ra Serial Monitor:

/*
  PYWORLD.VN - ESP32 ngắt GPIO: nhấn nút để bật, tắt LED, có chống dội
  Nút nhấn nối giữa GPIO27 và GND (dùng điện trở kéo lên bên trong chip)
  LED có sẵn trên mạch DevKit V1: GPIO2
  Không cần cài thêm thư viện
*/

#define NUT_PIN  27
#define LED_PIN  2
#define THOI_GIAN_CHONG_DOI_MS  50

// Biến dùng chung giữa hàm ngắt và loop() phải khai báo volatile
volatile unsigned long soLanNhan = 0;
volatile unsigned long lanNgatTruoc = 0;

unsigned long soLanDaXuLy = 0;

// Hàm phục vụ ngắt: thật ngắn, không dùng Serial, không dùng delay()
void IRAM_ATTR khiNhanNut() {
  unsigned long bayGio = millis();
  if (bayGio - lanNgatTruoc > THOI_GIAN_CHONG_DOI_MS) {   // Bỏ qua các xung dội
    soLanNhan++;
    lanNgatTruoc = bayGio;
  }
}

void setup() {
  Serial.begin(115200);
  delay(500);

  pinMode(LED_PIN, OUTPUT);
  pinMode(NUT_PIN, INPUT_PULLUP);   // Không nhấn: mức cao. Nhấn: mức thấp

  // Gọi hàm khiNhanNut() mỗi khi chân chuyển từ mức cao xuống mức thấp
  attachInterrupt(digitalPinToInterrupt(NUT_PIN), khiNhanNut, FALLING);

  Serial.println("San sang. Nhan nut de bat, tat LED.");
}

void loop() {
  unsigned long soLan = soLanNhan;   // Chép giá trị ra biến thường rồi mới xử lý

  if (soLan != soLanDaXuLy) {
    soLanDaXuLy = soLan;
    digitalWrite(LED_PIN, !digitalRead(LED_PIN));
    Serial.printf("Nhan lan thu %lu, LED %s\n", soLan, digitalRead(LED_PIN) ? "BAT" : "TAT");
  }

  // Trong lúc chờ nút nhấn, chương trình chính vẫn rảnh để làm việc khác
  delay(10);
}

Chọn board DOIT ESP32 DEVKIT V1, nạp chương trình rồi mở Serial Monitor ở 115200 baud:

Hình minh họa Serial Monitor khi nhấn nút ba lần: mỗi lần nhấn in một dòng và LED đổi trạng thái
Kết quả mong đợi khi nhấn nút ba lần (hình minh họa theo định dạng in của chương trình).

6. Giải thích và năm quy tắc khi viết hàm ngắt

  • attachInterrupt(digitalPinToInterrupt(NUT_PIN), khiNhanNut, FALLING): gắn hàm khiNhanNut() vào chân nút nhấn, kích ở sườn xuống. Muốn gỡ ngắt, gọi detachInterrupt(digitalPinToInterrupt(NUT_PIN)).
  • millis() trong hàm ngắt dùng để so thời gian với lần ngắt trước; các ngắt đến sớm hơn 50 ms bị coi là dội phím và bỏ qua.
  • loop() chép soLanNhan ra biến thường soLan rồi mới xử lý, để giá trị không bị đổi giữa chừng.

Hàm ngắt chen ngang chương trình chính nên phải tuân theo vài quy tắc:

  1. Thêm IRAM_ATTR trước tên hàm để hàm được đặt trong RAM. Thiếu từ khóa này, ESP32 có thể tự khởi động lại nếu ngắt xảy ra đúng lúc chương trình đang ghi bộ nhớ flash.
  2. Viết thật ngắn: chỉ ghi nhận sự kiện bằng một biến cờ hoặc biến đếm, phần xử lý để cho loop().
  3. Không gọi Serial.print(), delay() và không tính toán số thực trong hàm ngắt.
  4. Khai báo volatile cho mọi biến dùng chung giữa hàm ngắt và chương trình chính.
  5. Bảo vệ thao tác nhiều bước: khi chương trình chính vừa đọc vừa xóa một biến của hàm ngắt, đặt hai lệnh đó giữa portENTER_CRITICAL() và portEXIT_CRITICAL() như ở chương trình 2.

7. Chương trình 2: đếm xung và tính tần số

Nhiều cảm biến báo kết quả bằng số xung: cảm biến lưu lượng nước, cảm biến tốc độ quay, đồng hồ đo gió. Chương trình sau đếm số sườn lên trong mỗi giây để ra tần số. Để thử mà không cần cảm biến, ESP32 tự phát xung 1000 Hz ở GPIO25 và đếm lại ở GPIO26 qua một dây nối:

/*
  PYWORLD.VN - ESP32 ngắt GPIO: đếm xung và tính tần số
  Nối một dây từ GPIO25 (phát xung thử 1000 Hz) sang GPIO26 (đếm xung)
  Không cần cài thêm thư viện (gói esp32 phiên bản 3.x)
*/

#define PHAT_XUNG_PIN  25
#define DEM_XUNG_PIN   26

volatile unsigned long soXung = 0;
portMUX_TYPE khoa = portMUX_INITIALIZER_UNLOCKED;

// Mỗi sườn lên của tín hiệu gọi hàm này một lần
void IRAM_ATTR khiCoXung() {
  portENTER_CRITICAL_ISR(&khoa);
  soXung++;
  portEXIT_CRITICAL_ISR(&khoa);
}

void setup() {
  Serial.begin(115200);
  delay(500);

  // Tạo xung thử 1000 Hz, duty 50 % trên GPIO25 bằng bộ PWM
  ledcAttach(PHAT_XUNG_PIN, 1000, 8);
  ledcWrite(PHAT_XUNG_PIN, 128);

  pinMode(DEM_XUNG_PIN, INPUT_PULLDOWN);
  attachInterrupt(digitalPinToInterrupt(DEM_XUNG_PIN), khiCoXung, RISING);
}

void loop() {
  delay(1000);

  // Lấy số xung của 1 giây vừa qua rồi xóa bộ đếm; tạm khóa để ngắt không chen vào giữa hai lệnh
  portENTER_CRITICAL(&khoa);
  unsigned long dem = soXung;
  soXung = 0;
  portEXIT_CRITICAL(&khoa);

  Serial.printf("Tan so: %lu Hz\n", dem);
}

Ví dụ kết quả trên Serial Monitor (số minh họa):

Tan so: 1000 Hz
Tan so: 1000 Hz
Tan so: 1001 Hz
  • Biến khoa kiểu portMUX_TYPE là khóa bảo vệ biến đếm. Trong hàm ngắt dùng cặp lệnh có đuôi _ISR, trong chương trình chính dùng cặp lệnh thường.
  • Số đếm có thể lệch một đơn vị vì khoảng 1 giây tạo bằng delay() không chính xác tuyệt đối.
  • Khi nối cảm biến thật, bỏ hai dòng ledcAttach(), ledcWrite() và đưa tín hiệu của cảm biến vào GPIO26. Tín hiệu phải nằm trong khoảng 0–3,3 V.
  • Cách đếm bằng ngắt phù hợp với tín hiệu đến vài chục kHz. Tần số cao hơn nên dùng bộ đếm xung phần cứng (PCNT) của ESP32.

8. Một số cách chống dội khác

  • Tăng thời gian chống dội: nút nhấn cũ hoặc công tắc hành trình có thể dội lâu hơn; thử tăng THOI_GIAN_CHONG_DOI_MS lên 100–200.
  • Chống dội bằng phần cứng: mắc một tụ gốm 100 nF song song với nút nhấn để làm mượt các xung dội.
  • Nhiều nút nhấn: mỗi nút một chân ngắt và một hàm ngắt riêng. Với màn hình OLED 0.96 inch loại kèm 4 nút nhấn, cách này giúp làm menu mà không bỏ lỡ lần nhấn nào trong lúc màn hình đang vẽ.

9. Xử lý lỗi thường gặp

  • ESP32 tự khởi động lại, Serial Monitor hiện "Guru Meditation Error": hàm ngắt thiếu IRAM_ATTR, chạy quá lâu hoặc có gọi Serial.print(), delay(). Rút gọn hàm ngắt theo mục 6.
  • Nhấn một lần mà đếm nhiều lần: thời gian chống dội còn ngắn so với nút đang dùng; tăng THOI_GIAN_CHONG_DOI_MS hoặc thêm tụ 100 nF.
  • Nhấn nút không có phản ứng: kiểu kích không khớp với cách nối. Nút nối xuống GND dùng INPUT_PULLUP và FALLING; nút nối lên 3V3 dùng INPUT_PULLDOWN và RISING.
  • Ngắt tự xảy ra khi không ai nhấn: chân đang thả nổi do thiếu điện trở kéo, hoặc dây nối quá dài bị nhiễu. Bật điện trở kéo bên trong hoặc thêm điện trở ngoài 10 kΩ.
  • Biến đếm trong loop() không thay đổi: thiếu từ khóa volatile.
  • Chương trình 2 luôn in 0 Hz: chưa nối dây D25 sang D26, hoặc đang dùng gói esp32 phiên bản 2.x chưa có lệnh ledcAttach().
  • Không nạp được chương trình: dùng cáp USB có truyền dữ liệu, chọn đúng cổng COM và giữ nút BOOT khi IDE dừng ở dòng Connecting...

Xem thêm: dùng thanh cảm biến dò line 5 kênh TCRT5000 với ESP32, ESP32 điều khiển module relay 4 kênh và ESP32 làm web server điều khiển relay qua Wi-Fi.


Tin tức liên quan

Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE
Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE

7 Lượt xem

Hướng dẫn nối load cell với module HX711 và ESP32 DevKit V1, gá load cell đúng cách, hiệu chuẩn bằng vật đã biết khối lượng và viết chương trình cân trên Arduino IDE.

Hướng dẫn làm đồng hồ Internet với ESP32, NTP và màn hình OLED 0.96 inch
Hướng dẫn làm đồng hồ Internet với ESP32, NTP và màn hình OLED 0.96 inch

5 Lượt xem

Hướng dẫn ESP32 lấy giờ Việt Nam từ Internet bằng NTP trên Arduino IDE, in ra Serial Monitor và làm đồng hồ hiển thị thứ, ngày, giờ trên màn hình OLED 0.96 inch SSD1306.

Hướng dẫn dùng MQTT với ESP32: gửi dữ liệu và điều khiển relay bằng PubSubClient
Hướng dẫn dùng MQTT với ESP32: gửi dữ liệu và điều khiển relay bằng PubSubClient

4 Lượt xem

Hướng dẫn dùng MQTT với ESP32 trên Arduino IDE: broker, topic, publish, subscribe, cài Mosquitto và PubSubClient, gửi dữ liệu và điều khiển module relay qua mạng nội bộ.

Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32
Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32

4 Lượt xem

Hướng dẫn dùng Bluetooth Classic của ESP32 trên Arduino IDE: ghép đôi với điện thoại Android, gửi nhận chữ bằng BluetoothSerial và điều khiển module relay bằng lệnh ON, OFF.

Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE
Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE

4 Lượt xem

Hướng dẫn bật OTA trên ESP32 bằng thư viện ArduinoOTA có sẵn, nạp chương trình qua Wi-Fi từ Arduino IDE 2.x, đặt mật khẩu và các quy tắc để không mất khả năng cập nhật từ xa.

Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano
Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano

4 Lượt xem

Hướng dẫn nối màn hình OLED 0.96 inch SSD1306 (I2C) với Arduino Nano, cài thư viện Adafruit, hiện chữ nhiều cỡ, vẽ thanh mức theo điện áp và lưu ý về bộ nhớ RAM.

Hướng dẫn đọc ADC trên ESP32 và đo điện áp pin 18650 bằng cầu phân áp
Hướng dẫn đọc ADC trên ESP32 và đo điện áp pin 18650 bằng cầu phân áp

5 Lượt xem

Hướng dẫn đọc điện áp analog bằng ADC của ESP32 trên Arduino IDE, dùng analogReadMilliVolts, làm cầu phân áp đo pin Li-ion 18650 và ước lượng phần trăm pin còn lại.

Hướng dẫn dùng Deep Sleep trên ESP32: tiết kiệm pin, đánh thức bằng timer và nút nhấn
Hướng dẫn dùng Deep Sleep trên ESP32: tiết kiệm pin, đánh thức bằng timer và nút nhấn

4 Lượt xem

Hướng dẫn đưa ESP32 vào Deep Sleep trên Arduino IDE để tiết kiệm pin: các chế độ nguồn, đánh thức bằng timer và nút nhấn, giữ biến trong bộ nhớ RTC và ước tính thời lượng pin.


Bình luận
Đã thêm vào giỏ hàng