Hướng dẫn dùng ngắt GPIO trên ESP32: đọc nút nhấn, chống dội và đếm xung
- 1. Hỏi vòng và ngắt khác nhau thế nào?
- 2. Kiểu kích ngắt và hiện tượng dội phím
- 3. Linh kiện cần chuẩn bị
- 4. Sơ đồ nối dây
- 5. Chương trình 1: nhấn nút để bật, tắt LED
- 6. Giải thích và năm quy tắc khi viết hàm ngắt
- 7. Chương trình 2: đếm xung và tính tần số
- 8. Một số cách chống dội khác
- 9. Xử lý lỗi thường gặp
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?
- Hỏi vòng (polling):
loop()gọidigitalRead()lặp đi lặp lại. Cách này đơn giản nhưng nếuloop()đ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
| Kiểu | Hàm ngắt được gọi khi | Thường dùng cho |
|---|---|---|
FALLING | Chân chuyển từ mức cao xuống mức thấp | Nút nhấn nối xuống GND |
RISING | Chân chuyển từ mức thấp lên mức cao | Đếm xung, nút nhấn nối lên 3V3 |
CHANGE | Chân đổi mức theo cả hai chiều | Encoder, đo độ rộng xung |
ONLOW / ONHIGH | Châ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ị
- ESP32 DevKit V1 30 chân (hoặc bản dùng chip CH340) và cáp USB có truyền dữ liệu.
- Một nút nhấn thường hở.
- Breadboard MB-102 và dây cắm test board.
- Máy tính đã cài Arduino IDE 2.x và gói esp32 by Espressif Systems phiên bản 3.x (bài dùng 3.3.10). Không cần cài thêm thư viện.
4. Sơ đồ nối dây
| Linh kiện | ESP32 DevKit V1 | Ghi chú |
|---|---|---|
| Nút nhấn, chân 1 | D27 / GPIO27 | Chân ngắt, dùng điện trở kéo lên bên trong chip. |
| Nút nhấn, chân 2 | GND | Nhấn nút thì GPIO27 xuống mức thấp. |
| Một dây nối | D25 sang D26 | Chỉ dùng cho chương trình đếm xung ở mục 7. |
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:
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àmkhiNhanNut()vào chân nút nhấn, kích ở sườn xuống. Muốn gỡ ngắt, gọidetachInterrupt(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épsoLanNhanra biến thườngsoLanrồ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:
- Thêm
IRAM_ATTRtrướ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. - 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(). - Không gọi
Serial.print(),delay()và không tính toán số thực trong hàm ngắt. - Khai báo
volatilecho mọi biến dùng chung giữa hàm ngắt và chương trình chính. - 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
khoakiểuportMUX_TYPElà 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_MSlê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ọiSerial.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_MShoặ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_PULLUPvàFALLING; nút nối lên 3V3 dùngINPUT_PULLDOWNvà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óavolatile. - 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.
Xem thêm