Atlas Robotics
Kiến thức nền tảngTrung bình

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

Interrupt là gì? Xử lý sự kiện mà không phải liên tục kiểm tra
Interrupt tạm dừng chương trình chính để xử lý sự kiện ngay lập 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 triggerKích hoạt khi nào
RISINGChân chuyển từ LOW sang HIGH
FALLINGChân chuyển từ HIGH sang LOW
CHANGEChâ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/ChipSố chân hỗ trợ external interrupt
Arduino UnoChỉ 2 chân (D2, D3 — tương ứng INT0/INT1)
STM32Hầu hết mọi chân GPIO (qua EXTI, xem bên dưới)
ESP32Hầ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 trong loop(), 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 volatile cho 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ưng loop() 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ể.

Interrupt là gì? Xử lý sự kiện mà không phải liên tục kiểm tra — Atlasbot