Mở đầu: hộp đen của Bài 4, giờ mới mở ra
Sáu bài vừa qua, MCU luôn phải TỰ HỎI: Bài 3 polling đọc chân nút mỗi vòng lặp, Bài 5 lấy mẫu định kỳ để
chống dội — tất cả đều là CPU chủ động đi hỏi "có gì mới không?". Nhưng có một chỗ bạn đã âm thầm dùng một
cơ chế hoàn toàn khác mà không để ý: Bài 4, khi SysTick "tự động" tăng millis() — bạn chưa
từng viết code nào gọi hàm tăng biến đó cả. Nó tự chạy. Bằng cách nào?
Câu trả lời là ngắt (interrupt): thay vì CPU đi hỏi, phần cứng chủ động "gõ vai" CPU đúng lúc có việc, tạm dừng bất kỳ việc gì CPU đang làm, chạy một hàm xử lý riêng gọi là ISR, rồi trả lại đúng chỗ cũ như chưa hề gián đoạn. Bài này mở hộp đen SysTick, giới thiệu NVIC — bộ điều phối toàn bộ ngắt của lõi ARM — và EXTI, nguồn ngắt từ chính chân nút nhấn quen thuộc. Cẩn thận: chương này có cạm bẫy được xem là chí mạng nhất trong toàn bộ phần cứng nhúng cấp thấp.
1. Polling vs Interrupt
Polling là CPU liên tục hỏi "xong chưa? xong chưa?" trong vòng lặp — tốn CPU ngay cả khi chẳng có gì xảy ra, và CÓ THỂ bỏ lỡ sự kiện nếu CPU đang bận việc khác đúng lúc sự kiện xảy ra (Mục 7 sẽ đo trực tiếp điều này). Ngắt đảo ngược hoàn toàn: CPU không hỏi gì cả, cứ làm việc của mình — phần cứng sẽ chủ động "gõ vai" khi có việc thật sự cần xử lý.
| Polling | Interrupt |
|---|---|
| CPU chủ động hỏi liên tục — tốn CPU kể cả khi không có gì xảy ra | CPU không hỏi gì — phần cứng chủ động báo khi có việc, CPU rảnh thì làm việc khác |
| CÓ THỂ bỏ lỡ sự kiện nếu bận việc khác đúng lúc | KHÔNG bao giờ bỏ lỡ (phần cứng luôn ghi nhận, dù CPU đang làm gì) |
| Là nền tảng của chế độ tiết kiệm điện — SAI (CPU phải thức để hỏi liên tục) | Là nền tảng THẬT của tiết kiệm điện — CPU ngủ, ngắt đánh thức đúng lúc cần |
| THẮNG khi sự kiện dày đặc liên tục hoặc cần jitter cực thấp, ổn định | Có overhead chuyển ngữ cảnh mỗi lần — không lý tưởng nếu sự kiện quá dày |
Dòng cuối bảng là điểm dễ bị bỏ qua: ngắt không phải lúc nào cũng thắng tuyệt đối — khi sự kiện xảy ra liên tục dày đặc (ví dụ đọc ADC hàng trăm nghìn lần/giây), overhead vào-ra ngắt mỗi lần có thể còn tốn hơn polling đơn giản. Chọn đúng công cụ theo đúng tình huống, không phải "ngắt luôn luôn tốt hơn".
2. Mở hộp đen Bài 4: điều gì thực sự xảy ra khi ngắt "nổ"
Khi bạn viết void TIM1_UP_IRQHandler(void) { ... }, "phép thuật" nằm ở
linker: nó nhận diện đúng cái tên hàm đặc biệt này và tự nối con trỏ hàm đó vào đúng vị
trí trong vector table — một mảng con trỏ hàm nằm ngay ĐẦU vùng Flash (đã học ở
Bài 1), mỗi ô ứng với đúng 1 số IRQ.
Khi ngắt xảy ra (ví dụ nút nhấn tạo cạnh tín hiệu), phần cứng làm đúng 3 việc theo thứ tự, HOÀN TOÀN TỰ ĐỘNG, không cần 1 dòng code nào của bạn:
- Tự lưu ngữ cảnh: phần cứng tự đẩy một loạt thanh ghi (xPSR, PC, LR, R0–R3, R12) lên stack — đây chính là lý do main loop có thể "tiếp tục y hệt" sau khi ISR chạy xong: mọi thứ được lưu lại trước khi nhảy đi.
-
Nạp PC từ vector: CPU đọc đúng con trỏ hàm tại ô vector table ứng với IRQ đang xảy ra,
nhảy tới đó — đây chính là
TIM1_UP_IRQHandlerbắt đầu chạy. -
Trả về bằng
EXC_RETURN: khi ISR chạy xong (return bình thường), phần cứng nhận ra giá trị đặc biệt này trong LR, biết đây là "trả về từ ngắt" thay vì gọi hàm thường, và tự khôi phục lại toàn bộ ngữ cảnh đã lưu ở bước 1.
Không có gì "ma thuật" cả — chỉ là phần cứng làm hộ 3 bước cơ học này mỗi lần ngắt xảy ra, để lập trình viên chỉ cần lo đúng 1 việc: viết nội dung xử lý bên trong hàm có đúng tên.
3. NVIC — tổng đài ngắt của lõi ARM
NVIC là "tổng đài" quyết định: IRQ nào được PHÉP báo (enable), IRQ nào đang CHỜ được phục vụ (pending), và IRQ nào quan trọng hơn IRQ nào (priority). Giống SysTick (Bài 4), NVIC là một phần CỐ ĐỊNH của kiến trúc ARM — không phải ngoại vi riêng của hãng chip.
// ISER: ghi 1 vao bit tuong ung SO IRQ de BAT (write-1-to-set)
// KHONG the tat bang cach ghi 0 vao day - phai dung thanh ghi rieng ICER!
NVIC_ISER0 |= (1 << IRQ_EXTI1);
// IPR: 1 byte rieng cho MOI IRQ - so CANG NHO, uu tien CANG CAO
NVIC_IPR[IRQ_TIM1_UP] = 0; // uu tien CAO NHAT
NVIC_IPR[IRQ_EXTI1] = 1; // uu tien THAP hon (so lon hon)
priority = 10 để "ưu tiên cao" và ngạc nhiên khi nó bị interrupt khác
priority 0 chen ngang liên tục.
Khi 2 IRQ cùng pending một lúc (Mục 6 sẽ verify bằng số thật), NVIC LUÔN chọn IRQ có priority số nhỏ hơn chạy trước — và nếu 1 ISR đang chạy mà một IRQ ưu tiên CAO HƠN xuất hiện, NVIC cho phép nó chen ngang ngay lập tức (gọi là nested interrupt / preemption), tạm dừng ISR đang chạy, xử lý ISR ưu tiên cao hơn, rồi mới quay lại ISR ban đầu.
4. EXTI — ngắt từ chân GPIO
EXTI là ngoại vi đặc thù hãng chip (giống GPIOA, khác NVIC) biến một cạnh tín hiệu trên chân GPIO thành
một yêu cầu ngắt thật. 4 thanh ghi cốt lõi: IMR (mask — đường nào ĐƯỢC PHÉP báo lên NVIC),
RTSR/FTSR (chọn cạnh lên/xuống kích hoạt), và PR (pending — set khi
có cạnh xảy ra).
// DUNG: xoa EXTI_PR truoc khi thoat ISR (write-1-to-clear)
void EXTI1_IRQHandler(void) {
button_pressed_count++;
EXTI_PR = (1 << 1); // XOA pending - BAT BUOC, khong duoc thieu
}
// SAI: quen xoa EXTI_PR - hau qua xay ra NGAY khi ISR nay thoat
void EXTI1_IRQHandler_LOI(void) {
button_pressed_count++;
// ...quen mat dong EXTI_PR = (1 << 1);...
}
EXTI_PR còn set VÀ đường đó còn được enable qua IMR, yêu cầu ngắt VẪN CÒN
TREO. Nếu ISR không xoá PR trước khi thoát, NVIC sẽ pend lại NGAY LẬP TỨC khi ISR vừa kết
thúc — gọi lại ISR đó, ISR lại không xoá, lại pend lại... vô hạn. Kết quả quan sát
được: main loop hoàn toàn "đóng băng" — không phải vì nó bị lỗi, mà vì CPU không bao giờ thoát ra khỏi
vòng lặp ISR để quay lại main. Đây là triệu chứng kinh điển: bấm 1 nút, cả board "treo" — không phải
crash, mà là kẹt trong ngắt vô hạn. Verify bằng self-test thật ở Mục 6.
5. Quy tắc vàng: "ISR càng ngắn càng tốt"
Trong lúc 1 ISR đang chạy, mọi ngắt khác cùng hoặc thấp hơn priority PHẢI CHỜ (Mục 3). ISR càng dài, latency của những ngắt khác càng tích luỹ — với hệ thống cần phản hồi nhanh (đọc cảm biến, điều khiển động cơ), vài mili-giây trễ có thể là thảm hoạ. Pattern chuẩn: ISR chỉ làm việc TỐI THIỂU (ghi 1 cờ, đẩy 1 giá trị vào buffer), main loop xử lý phần nặng.
// XAU: ISR lam viec NANG (in chuoi qua UART co the mat hang tram micro-giay)
void EXTI1_IRQHandler(void) {
uart_print("Nut vua duoc nhan!\n"); // CHAN moi ngat khac trong luc nay
EXTI_PR = (1 << 1);
}
// TOT: ISR chi ghi co, main loop xu ly phan nang
volatile uint8_t button_flag = 0;
void EXTI1_IRQHandler(void) {
button_flag = 1; // chi 1 lenh, cuc nhanh
EXTI_PR = (1 << 1);
}
// ... trong main loop:
if (button_flag) {
uart_print("Nut vua duoc nhan!\n"); // viec NANG lam o day, khong chan ngat nao
button_flag = 0;
}
6. VMCU: NVIC + EXTI đã "sống"
VMCU thêm NVIC vào vùng System (địa chỉ thật 0xE000E100-0xE000E400) và EXTI vào
vùng Peripheral (địa chỉ thật STM32F103 0x40010400). "Vector table" được mô phỏng bằng
installIrqHandler(irq, fn) — đăng ký 1 hàm JS làm ISR cho 1 số IRQ, đúng tinh thần Mục 2
nhưng không cần mô phỏng Flash/linker thật:
// EXTI la yeu cau MUC (level) - phai tinh lai moi lan kiem tra, dung 1 lan
_syncExtiToNvic() {
const requesting = (this.extiPr & this.extiImr & (1 << EXTI_LINE1_BIT)) !== 0;
if (requesting) this.nvicIspr |= (1 << IRQ_EXTI1);
}
_serviceInterrupts() {
// ... quet IRQ dang pending+enabled, chon priority SO NHO NHAT ...
// ... chi chay neu priority do CAO HON priority dang chay hien tai ...
}
Số đo thật từ self-test (node vmcu.js):
| Kịch bản | Kết quả |
|---|---|
| 2 IRQ cùng pending (priority 0 và 1) — IRQ nào chạy trước? | Priority 0 chạy trước, dù IRQ kia được kích trước trong bitmask |
| ISR priority thấp đang chạy, ISR priority CAO xuất hiện | Chen ngang NGAY: EXTI1 start → TIM1_UP running → EXTI1 resumed |
| ISR priority cao đang chạy, ISR priority THẤP xuất hiện | KHÔNG chen ngang: EXTI1 start → EXTI1 finish → TIM1_UP running |
| ISR ĐÚNG (xoá EXTI_PR) cho 1 cạnh | Chạy đúng 1 lần |
| ISR SAI (quên xoá EXTI_PR) cho 1 cạnh | Gọi lại 1000 lần liên tiếp (chạm giới hạn an toàn của VMCU) |
| Đường EXTI bị mask (IMR=0) khi có cạnh | ISR KHÔNG chạy, nhưng EXTI_PR vẫn được set (đúng hành vi thật) |
7. Thực hành: Timeline ngắt trên VMCU thật
Demo dưới đây chạy trên VMCU thật — main loop "nhịp tim" liên tục chạy nền, và bạn có thể mô phỏng main đang bận (busy-loop) để so sánh trực tiếp polling vs ngắt. Bật cờ "ISR có bug" để tự tay kích hoạt đúng cạm bẫy Mục 4 và thấy hậu quả thật (KHÔNG giả lập bằng text, engine thật sự lặp tới giới hạn an toàn):
Timeline (main = xanh dương nhạt, đã bắt = xanh lá, bỏ lỡ = đỏ):
Ở chế độ ngắt, nút luôn được bắt NGAY LẬP TỨC dù main có đang "bận" hay không. Chuyển sang chỉ polling rồi bấm "Main bận 2 giây" — bấm nút trong lúc đó sẽ bị tính là "bỏ lỡ" vì main không rảnh để tự hỏi. Bật "ISR có bug" rồi bấm nút để tự tay kích hoạt cạm bẫy Mục 4 (engine thật sự lặp tới giới hạn an toàn 1000 lần).
// CHE DO NGAT: luon bat duoc, bat ke main dang lam gi
void EXTI1_IRQHandler(void) {
caught_count++;
EXTI_PR = (1 << 1); // nho xoa - neu co checkbox bug, dong nay bi "quen"
}
// CHE DO CHI POLLING: main phai TU HOI - neu dang ban thi bo lo
while (1) {
if (main_is_busy()) continue; // dang ban - khong kiem tra duoc
if (button_pin_is_pressed()) caught_count++;
}
import { VMCU, EXTI_IMR_ADDR, NVIC_ISER0_ADDR, IRQ_EXTI1, EXTI_PR_ADDR } from './vmcu.js';
const cpu = new VMCU();
cpu.write32(EXTI_IMR_ADDR, 1 << 1); // bat duong EXTI line 1
cpu.write32(NVIC_ISER0_ADDR, 1 << IRQ_EXTI1);
cpu.installIrqHandler(IRQ_EXTI1, (cpu) => {
caughtCount++;
if (!isrHasBug) cpu.write32(EXTI_PR_ADDR, 1 << 1); // XOA - tru khi bat bug
});
function onPressClicked() {
if (mode === 'interrupt') {
cpu.exti1EdgeOccurred(); // luon bat duoc - bat ke main dang lam gi
} else if (mainBusyUntil > Date.now()) {
missedCount++; // che do polling, main dang "ban" -> bo lo
} else {
caughtCount++;
}
}
Tóm lược
- ✅ Polling chủ động hỏi liên tục (tốn CPU, có thể bỏ lỡ); ngắt để phần cứng chủ động báo (không bỏ lỡ, nền tảng tiết kiệm điện thật) — nhưng polling vẫn thắng khi sự kiện quá dày đặc.
-
✅ Ngắt "nổ" qua 3 bước phần cứng tự động: lưu ngữ cảnh lên stack, nạp PC từ vector table, trả về bằng
EXC_RETURN— linker tự nối tên hàm..._IRQHandlervào đúng vector. - ✅ NVIC: ISER/ICER (enable), ISPR/ICPR (pending), IPR (priority) — verified: số ưu tiên NHỎ HƠN luôn chạy trước, và được phép chen ngang (nested) ISR ưu tiên thấp hơn đang chạy.
-
✅ EXTI: quên xoá
PRtrong ISR → yêu cầu ngắt vẫn treo → gọi lại vô hạn, main đóng băng (verified: 1000 lần liên tiếp chạm giới hạn an toàn). - ✅ ISR càng ngắn càng tốt — chỉ ghi cờ/đẩy buffer, để main xử lý việc nặng (nối tiếp Bài 8: race condition & ring buffer).
Trắc nghiệm ôn tập
Câu 1
Vì sao cách polling có thể bỏ lỡ một sự kiện trong khi ngắt thì không?
Câu 2
IRQ A có priority = 0, IRQ B có priority = 5. Cả 2 cùng pending một lúc. IRQ nào chạy trước?
Câu 3
Nếu ISR của EXTI quên xoá thanh ghi EXTI_PR, điều gì xảy ra?
Câu 4
Vì sao quy tắc "ISR càng ngắn càng tốt" lại quan trọng?
Tải file code thực hành minh họa bài học
File JavaScript VMCU — engine MCU ảo dùng xuyên suốt cả 16 bài, Bài 7 vừa thêm NVIC
(enable/pending/priority, preemption thật) và EXTI (ngắt từ GPIO, cạm bẫy quên xoá pending), kèm
self-test đối chiếu đúng mọi hành vi trong bài (chạy node vmcu.js, không cần cài thêm gì):
Bình luận