Interrupt là gì? Xử lý sự kiện mà không phải liên tục kiểm tra
Atlasbot · 20/8/2026 · 10 phút đọc
Đếm xung encoder bằng cách gọi digitalRead() liên tục trong loop() (gọi là polling) có vấn đề rõ ràng: nếu loop() đang bận làm việc khác (VD: gửi dữ liệu qua UART) đúng lúc xung encoder xuất hiện, xung đó có thể bị bỏ lỡ hoàn toàn. Interrupt (ngắt) giải quyết đúng vấn đề này — cho phép MCU phản ứng ngay khi sự kiện xảy ra, bất kể chương trình chính đang làm gì.
Polling vs Interrupt
- Polling — chương trình chính liên tục hỏi "có sự kiện chưa?" trong vòng lặp. Đơn giản, dễ hiểu, nhưng tốn CPU (kiểm tra liên tục kể cả khi chẳng có gì xảy ra) và có thể bỏ lỡ sự kiện ngắn nếu chương trình đang bận việc khác.
- Interrupt — phần cứng tự động "báo" cho CPU ngay khi sự kiện xảy ra (VD: chân GPIO chuyển từ LOW sang HIGH), CPU tạm dừng chương trình đang chạy, nhảy vào chạy một hàm xử lý riêng (ISR — Interrupt Service Routine), xong quay lại đúng chỗ vừa dừng.
Tóm lại: Polling = chủ động hỏi liên tục, có thể bỏ lỡ. Interrupt = bị động chờ được "gọi tên", không bỏ lỡ sự kiện dù chương trình chính đang bận việc khác.
Các loại trigger của interrupt trên chân GPIO
Interrupt trên chân GPIO thường được cấu hình theo kiểu thay đổi tín hiệu nào sẽ kích hoạt:
| Kiểu trigger | Kích hoạt khi nào |
|---|---|
RISING | Chân chuyển từ LOW sang HIGH |
FALLING | Chân chuyển từ HIGH sang LOW |
CHANGE | Chân đổi trạng thái theo bất kỳ hướng nào (cả RISING lẫn FALLING) |
Đọc encoder quadrature (2 kênh A/B) thường cần CHANGE trên cả 2 kênh để không bỏ lỡ bất kỳ cạnh xung nào, đủ dữ liệu xác định cả tốc độ lẫn chiều quay.
Kiến thức kỹ thuật
Công thức
Tần số xung interrupt mà MCU phải xử lý phụ thuộc độ phân giải encoder (số xung/vòng) và tốc độ quay thực tế — con số này quyết định MCU có "theo kịp" hay không:
f (Hz) = (Số xung/vòng × Tốc độ quay (vòng/phút)) / 60
VD: encoder 500 xung/vòng, động cơ quay 3000 vòng/phút
f = (500 × 3000) / 60 = 25.000 Hz = 25kHz
→ MCU phải xử lý xong 1 lần ngắt trong dưới 1/25.000s ≈ 40µs, nếu không sẽ bắt đầu bỏ lỡ xung
Thông số
Số chân hỗ trợ external interrupt khác nhau đáng kể giữa các board — ảnh hưởng trực tiếp tới việc đọc được bao nhiêu encoder cùng lúc bằng interrupt phần cứng:
| Board/Chip | Số chân hỗ trợ external interrupt |
|---|---|
| Arduino Uno | Chỉ 2 chân (D2, D3 — tương ứng INT0/INT1) |
| STM32 | Hầu hết mọi chân GPIO (qua EXTI, xem bên dưới) |
| ESP32 | Hầu hết mọi chân GPIO |
Register / Bit / Byte
Trên vi điều khiển dòng ARM Cortex-M (STM32), ngắt được quản lý qua 2 tầng: EXTI (External Interrupt/Event Controller) quyết định chân GPIO nào và kiểu trigger nào (rising/falling) sẽ tạo ra yêu cầu ngắt, còn NVIC (Nested Vectored Interrupt Controller) là bộ điều phối trung tâm quyết định ngắt nào được ưu tiên xử lý trước khi có nhiều ngắt xảy ra gần như đồng thời — mỗi ngắt có một mức ưu tiên (priority) cấu hình được, ngắt ưu tiên cao hơn có thể chen ngang (preempt) ngắt ưu tiên thấp hơn đang chạy dở.
Ứng dụng thực tế
Robot 4 bánh (Mecanum Robot) cần đọc đồng thời 4 encoder (mỗi bánh 1 cái) ở tốc độ cao để tính odometry (ước lượng vị trí robot theo thời gian thực) cấp cho ROS2 — nếu dùng polling, vòng lặp chính chỉ đọc được lần lượt từng encoder, dễ lệch thời điểm lấy mẫu giữa các bánh và làm sai lệch phép tính vận tốc tổng hợp. Dùng interrupt trên tất cả các chân encoder đảm bảo mỗi xung được ghi nhận đúng thời điểm xảy ra, bất kể chương trình chính đang bận xử lý giao tiếp UART/ROS2 hay tính toán PID.
Ví dụ thực tế: đếm xung encoder bằng interrupt
volatile long encoderCount = 0; // volatile: bắt buộc cho biến dùng chung với ISR
void onEncoderA() {
bool b = digitalRead(PIN_ENCODER_B);
encoderCount += (digitalRead(PIN_ENCODER_A) == b) ? 1 : -1;
}
void setup() {
pinMode(PIN_ENCODER_A, INPUT_PULLUP);
pinMode(PIN_ENCODER_B, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_ENCODER_A), onEncoderA, CHANGE);
}
void loop() {
Serial.println(encoderCount); // đọc giá trị đếm được, cập nhật ngay cả khi loop() đang làm việc khác
delay(100);
}
attachInterrupt() đăng ký hàm onEncoderA chạy tự động mỗi khi chân encoder A đổi trạng thái — dù loop() đang ở giữa lệnh delay(100), xung encoder vẫn được đếm chính xác, điều không thể đảm bảo nếu dùng digitalRead() trong loop() thông thường.
Quy tắc viết ISR (Interrupt Service Routine)
- Giữ ISR càng ngắn càng tốt — trong lúc ISR đang chạy, các ngắt khác (tuỳ cấu hình) có thể bị chặn lại; ISR dài làm hệ thống mất khả năng phản ứng kịp các sự kiện khác.
- Không gọi
delay()trong ISR —delay()dựa vào timer nội bộ mà bản thân interrupt có thể ảnh hưởng tới, gây treo hoặc hành vi không xác định. - Biến chia sẻ giữa ISR và chương trình chính phải khai báo
volatile— nếu không, trình biên dịch có thể tối ưu hoá theo cách giả định biến không đổi giữa các lần đọc trongloop(), dẫn tới đọc giá trị cũ dù ISR đã cập nhật. - Tránh
Serial.print()trong ISR — các hàm giao tiếp UART thường không an toàn để gọi trong ngữ cảnh ngắt (không phải "interrupt-safe"), có thể gây treo chương trình.
Lỗi thường gặp
- Quên
volatilecho biến dùng chung với ISR — chương trình chạy sai một cách khó hiểu: ISR chạy đúng, biến thực sự đổi giá trị trong bộ nhớ, nhưngloop()vẫn đọc ra giá trị cũ do trình biên dịch tối ưu hoá sai. - Debounce nút nhấn cơ khí — tiếp điểm cơ khí của nút nhấn thường "rung" (bounce) trong vài mili-giây khi bấm/thả, khiến 1 lần bấm thực tế kích hoạt ISR nhiều lần liên tiếp — cần thêm logic debounce (VD: bỏ qua sự kiện nếu cách sự kiện trước đó dưới vài chục mili-giây) nếu dùng interrupt cho nút nhấn thay vì encoder.
- ISR quá dài hoặc tính toán nặng — đặt toàn bộ logic xử lý phức tạp trong ISR thay vì chỉ cập nhật một biến cờ/biến đếm rồi xử lý chi tiết ở
loop()— làm hệ thống phản ứng chậm với các sự kiện khác. - Số lượng chân hỗ trợ interrupt có giới hạn — không phải mọi chân GPIO đều hỗ trợ interrupt (đặc biệt trên các board Arduino cũ chỉ có 2 chân interrupt ngoài) — kiểm tra tài liệu board trước khi thiết kế mạch dựa vào interrupt trên một chân cụ thể.
Kết luận
Interrupt là công cụ bắt buộc khi cần phản ứng chính xác, không bỏ lỡ với các sự kiện tần số cao hoặc không đoán trước được thời điểm — điển hình nhất là đọc encoder trong robot. Nắm vững quy tắc viết ISR ngắn gọn và dùng volatile đúng chỗ tránh được phần lớn lỗi khó debug nhất khi mới làm quen với interrupt. Đây cũng là bài cuối trong chuyên mục Kiến thức nền tảng — các khái niệm này sẽ được dùng lại liên tục ở những chuyên mục tiếp theo về linh kiện điện tử và vi điều khiển cụ thể.