BÀI 34 – NVIC TRÊN STM32F103
Nested Vectored Interrupt Controller – Bộ điều khiển ngắt của Cortex-M3
Ở Bài 33, chúng ta đã học:
GPIO
↓
EXTI
↓
NVIC
↓
CPU
↓
ISR
Trong đó, NVIC là phần rất quan trọng nhưng chúng ta mới chỉ dùng:
NVIC_EnableIRQ(EXTI0_IRQn);
Hôm nay chúng ta sẽ hiểu NVIC thực sự làm gì, tại sao STM32 có nhiều mức ưu tiên interrupt, và CPU biết phải chạy hàm EXTI0_IRQHandler() bằng cách nào.
1. NVIC là gì?
NVIC viết đầy đủ là:
Nested Vectored Interrupt Controller
Có thể hiểu đơn giản:
NVIC là bộ phận đứng giữa các nguồn interrupt và CPU Cortex-M3.
Ví dụ:
GPIO PA0
│
▼
EXTI0
│
▼
NVIC
│
▼
CPU
│
▼
EXTI0_IRQHandler()
Nhưng NVIC không chỉ xử lý EXTI.
STM32F103 còn có rất nhiều nguồn interrupt:
EXTI
Timer
USART
ADC
SPI
I2C
DMA
USB
RTC
...
Tất cả đều có thể gửi yêu cầu interrupt đến NVIC.
2. Tại sao cần NVIC?
Hãy tưởng tượng CPU đang chạy:
while (1)
{
Motor_Run();
LCD_Update();
Sensor_Read();
}
Trong lúc đó:
USART nhận dữ liệu
hoặc:
Timer hết thời gian
hoặc:
Nút PA0 được nhấn
CPU cần biết:
“Nguồn nào đang yêu cầu interrupt?”
NVIC giúp CPU quản lý việc đó.
3. NVIC giống như “bộ điều phối”
Có thể hình dung:
┌── EXTI0
│
├── USART1
│
├── TIM2
│
├── ADC1
│
└── SPI1
│
▼
NVIC
│
┌────────┴────────┐
│ │
Ưu tiên Chờ
│ │
▼ │
CPU ◄──────────────┘
NVIC quyết định:
- interrupt nào được phép;
- interrupt nào đang pending;
- interrupt nào có mức ưu tiên cao hơn;
- interrupt nào được xử lý trước.
4. “Nested” nghĩa là gì?
Đây là một từ rất quan trọng.
Nested = lồng nhau.
Ví dụ CPU đang xử lý:
TIM2_IRQHandler()
nhưng trong lúc đó xuất hiện:
EXTI0 interrupt
Nếu EXTI0 có ưu tiên cao hơn TIM2:
TIM2_IRQHandler()
│
│ đang chạy
▼
EXTI0 xảy ra
│
▼
NVIC thấy EXTI0 ưu tiên cao
│
▼
tạm dừng TIM2_IRQHandler()
│
▼
EXTI0_IRQHandler()
│
▼
kết thúc
│
▼
quay lại TIM2_IRQHandler()
Đó chính là nested interrupt.
5. Ví dụ thực tế
Giả sử:
TIM2 = ưu tiên thấp
EXTI0 = ưu tiên cao
CPU:
TIM2_IRQHandler()
│
├──── đang xử lý
│
│ PA0 bị nhấn
│ ↓
│ EXTI0
│ ↓
│ NVIC
│ ↓
└──────► EXTI0_IRQHandler()
│
▼
xử lý xong
│
▼
quay lại TIM2
Đây là một trong những điểm mạnh của Cortex-M3.
6. NVIC có những nhiệm vụ chính nào?
Bạn nên nhớ 5 nhiệm vụ:
① Enable interrupt
Cho phép interrupt.
NVIC_EnableIRQ(EXTI0_IRQn);
② Disable interrupt
Tắt interrupt.
NVIC_DisableIRQ(EXTI0_IRQn);
③ Pending
Đặt một interrupt thành trạng thái đang chờ.
④ Priority
Thiết lập mức ưu tiên.
⑤ Vectoring
Chọn đúng ISR để CPU chạy.
Ví dụ:
EXTI0
↓
NVIC
↓
EXTI0_IRQHandler()
7. NVIC và EXTI khác nhau như thế nào?
Đây là chỗ người mới học rất dễ nhầm.
EXTI
Phát hiện:
PA0 HIGH → LOW
hoặc:
PA0 LOW → HIGH
NVIC
Quản lý:
"Interrupt này có được CPU xử lý không?"
Do đó:
GPIO
↓
EXTI = phát hiện sự kiện
↓
NVIC = quản lý interrupt
↓
CPU = thực thi
8. NVIC_EnableIRQ() thực sự làm gì?
Ở Bài 33 chúng ta viết:
NVIC_EnableIRQ(EXTI0_IRQn);
Nhìn thì rất đơn giản.
Nhưng bên dưới, NVIC có thanh ghi:
ISER
Tên đầy đủ:
Interrupt Set-Enable Register
Ví dụ interrupt số n.
Khi set bit tương ứng:
ISER bit = 1
thì interrupt được enable.
9. ISER
Có thể hình dung:
ISER:
bit 31 ......................... bit 0
│ │
│ └── IRQ0
│
└────────────────────────────────── IRQ31
Ví dụ:
NVIC->ISER[0] = (1 << 6);
có nghĩa là enable IRQ số 6.
Trong CMSIS, chúng ta thường không cần tự viết như vậy.
Dùng:
NVIC_EnableIRQ(...);
an toàn và dễ đọc hơn.
10. Disable interrupt
NVIC có:
ICER
Interrupt Clear-Enable Register
Ví dụ:
NVIC_DisableIRQ(EXTI0_IRQn);
CPU sẽ không tiếp tục nhận interrupt đó.
11. Pending là gì?
Đây là khái niệm cực kỳ quan trọng.
Giả sử:
CPU đang xử lý TIM2
Trong lúc đó:
EXTI0 xảy ra
CPU chưa thể xử lý ngay nếu đang bị một interrupt ưu tiên cao hơn chiếm quyền.
NVIC ghi nhận:
EXTI0 = PENDING
Nghĩa là:
“Tôi có một interrupt đang chờ.”
Khi CPU có thể xử lý:
PENDING
↓
CPU xử lý
12. Thanh ghi ISPR
NVIC có:
ISPR
Interrupt Set-Pending Register
Nó dùng để đặt interrupt ở trạng thái pending.
Ví dụ về mặt ý tưởng:
NVIC_SetPendingIRQ(EXTI0_IRQn);
Không nhất thiết phải có tín hiệu GPIO thật.
Ta có thể software trigger interrupt.
13. Thanh ghi ICPR
NVIC còn có:
ICPR
Interrupt Clear-Pending Register
Dùng để xóa trạng thái pending.
CMSIS cung cấp:
NVIC_ClearPendingIRQ(EXTI0_IRQn);
14. Đây là 4 thanh ghi cực kỳ quan trọng
Bạn nên nhớ:
ISER
↓
Enable
ICER
↓
Disable
ISPR
↓
Set Pending
ICPR
↓
Clear Pending
Tóm tắt:
| Thanh ghi | Ý nghĩa |
|---|---|
| ISER | Enable |
| ICER | Disable |
| ISPR | Set Pending |
| ICPR | Clear Pending |
15. Priority – mức ưu tiên
Đây mới là phần thú vị.
Giả sử có:
EXTI0
TIM2
USART1
Nếu cả ba xảy ra gần như cùng lúc, CPU phải quyết định:
Xử lý cái nào trước?
NVIC sử dụng priority.
Ví dụ:
EXTI0 → Priority 1
TIM2 → Priority 3
USART1 → Priority 2
Thì:
EXTI0
↓
USART1
↓
TIM2
sẽ được ưu tiên theo cấu hình.
16. Priority càng nhỏ càng ưu tiên cao
Đây là điều bạn phải nhớ.
Ví dụ:
Priority 0 → rất cao
Priority 1 → cao
Priority 2 → thấp hơn
Priority 3 → thấp hơn nữa
Không phải:
Priority 3 > Priority 1
mà ngược lại:
0 > 1 > 2 > 3
về mức ưu tiên xử lý.
17. Thiết lập Priority
CMSIS cung cấp:
NVIC_SetPriority(
EXTI0_IRQn,
1
);
Ví dụ:
NVIC_SetPriority(EXTI0_IRQn, 1);
NVIC_EnableIRQ(EXTI0_IRQn);
Nên cấu hình:
Priority
↓
Enable
18. Ví dụ nhiều interrupt
Giả sử:
NVIC_SetPriority(EXTI0_IRQn, 0);
NVIC_SetPriority(TIM2_IRQn, 2);
NVIC_SetPriority(USART1_IRQn, 1);
NVIC_EnableIRQ(EXTI0_IRQn);
NVIC_EnableIRQ(TIM2_IRQn);
NVIC_EnableIRQ(USART1_IRQn);
Ta có:
EXTI0 Priority 0
USART1 Priority 1
TIM2 Priority 2
Nếu tất cả cùng yêu cầu interrupt:
NVIC
│
┌──────┼──────┐
▼ ▼ ▼
EXTI0 USART1 TIM2
0 1 2
│
▼
CPU
EXTI0 sẽ được ưu tiên.
19. Nhưng Priority còn phức tạp hơn một chút
Cortex-M3 có khái niệm:
Preemption Priority
và:
Subpriority
Hai khái niệm này dùng để quyết định:
Preemption
Interrupt nào có thể ngắt interrupt đang chạy.
Subpriority
Nếu các interrupt có cùng khả năng preempt, cái nào được xử lý trước.
Đây là phần chúng ta sẽ đi sâu hơn sau khi bạn nắm chắc khái niệm cơ bản.
20. Vector Table là gì?
Bây giờ đến một phần cực kỳ quan trọng.
Bạn có:
void EXTI0_IRQHandler(void)
{
}
Nhưng CPU làm sao biết:
“EXTI0 phải chạy hàm này?”
Câu trả lời:
Vector Table
21. Vector Table giống như một bảng địa chỉ
Hãy tưởng tượng:
Vector Table
Reset → địa chỉ Reset_Handler
NMI → địa chỉ NMI_Handler
HardFault → địa chỉ HardFault_Handler
...
EXTI0 → địa chỉ EXTI0_IRQHandler
EXTI1 → địa chỉ EXTI1_IRQHandler
...
TIM2 → địa chỉ TIM2_IRQHandler
CPU không cần “tìm kiếm” tên hàm.
Nó chỉ cần lấy:
địa chỉ ISR
từ Vector Table.
22. Hãy hình dung toàn bộ quá trình
Khi nhấn nút PA0:
PA0
│
▼
GPIO
│
▼
EXTI0
│
▼
Pending
│
▼
NVIC
│
▼
CPU
│
▼
Vector Table
│
▼
EXTI0_IRQHandler
│
▼
xử lý
│
▼
return
Đây là một trong những sơ đồ quan trọng nhất của khóa học C + STM32 của chúng ta.
23. CPU không “nhảy” trực tiếp từ GPIO đến hàm
Đây là một hiểu nhầm thường gặp.
Không phải:
GPIO → EXTI0_IRQHandler()
mà thực tế gần giống:
GPIO
↓
EXTI
↓
NVIC
↓
Cortex-M3 interrupt mechanism
↓
Vector Table
↓
ISR
24. Vì sao ISR có tên đặc biệt?
Ví dụ:
void EXTI0_IRQHandler(void)
Tên này phải đúng với startup file và vector table.
Ví dụ:
void TIM2_IRQHandler(void)
hay:
void USART1_IRQHandler(void)
Nếu bạn viết sai:
void MyInterrupt(void)
thì vector table không tự biết rằng đây là ISR của TIM2.
Đó là lý do phải dùng đúng tên:
TIM2_IRQHandler
25. Một ví dụ hoàn chỉnh
Chúng ta quay lại bài thực hành:
PA0 = Button
PC13 = LED
Cấu hình:
void EXTI_Init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
EXTI->IMR |= (1 << 0);
EXTI->FTSR |= (1 << 0);
NVIC_SetPriority(EXTI0_IRQn, 1);
NVIC_EnableIRQ(EXTI0_IRQn);
}
ISR:
void EXTI0_IRQHandler(void)
{
if (EXTI->PR & (1 << 0))
{
GPIOC->ODR ^= (1 << 13);
EXTI->PR = (1 << 0);
}
}
Ở đây:
EXTI
phát hiện sự kiện.
NVIC
cho phép và ưu tiên interrupt.
ISR
xử lý interrupt.
26. Một thí nghiệm rất hay
Bạn có thể thử:
NVIC_SetPriority(EXTI0_IRQn, 0);
sau đó:
NVIC_SetPriority(EXTI0_IRQn, 3);
Trong chương trình chỉ có một interrupt thì bạn sẽ không thấy khác biệt rõ ràng.
Tại sao?
Vì priority chỉ thực sự có ý nghĩa khi:
Có từ 2 nguồn interrupt trở lên
cùng hoạt động.
27. Thí nghiệm tiếp theo: hai interrupt
Sau này chúng ta có thể làm:
EXTI0 → Button
TIM2 → Timer
Ví dụ:
EXTI0 Priority = 0
TIM2 Priority = 2
Khi:
TIM2_IRQHandler()
đang chạy:
PA0 nhấn
thì:
EXTI0
↓
NVIC
↓
Priority cao hơn
↓
ngắt TIM2
↓
EXTI0_IRQHandler()
↓
return
↓
TIM2_IRQHandler() tiếp tục
Đây mới là lúc bạn thực sự thấy NVIC hoạt động.
28. NVIC không phải EXTI
Hãy nhớ câu này:
EXTI phát hiện sự kiện, NVIC quản lý interrupt.
Ví dụ:
PA0 thay đổi
↓
EXTI0
"Đã có sự kiện!"
↓
NVIC
"Interrupt này được phép không?"
"Ưu tiên bao nhiêu?"
↓
CPU
"Được, tôi xử lý."
29. Một lỗi rất phổ biến
Bạn viết:
EXTI->IMR |= (1 << 0);
EXTI->FTSR |= (1 << 0);
nhưng quên:
NVIC_EnableIRQ(EXTI0_IRQn);
Kết quả:
PA0
↓
EXTI0
↓
Có sự kiện
↓
NVIC không cho phép
↓
CPU không chạy ISR
LED không phản ứng.
30. Lỗi ngược lại
Bạn viết:
NVIC_EnableIRQ(EXTI0_IRQn);
nhưng quên:
EXTI->IMR |= (1 << 0);
Kết quả:
PA0
↓
GPIO
↓
EXTI không được unmask
↓
không tạo interrupt
Cũng không chạy.
31. Lỗi thứ ba
Bạn viết ISR:
void EXTI0_IRQHandler(void)
{
GPIOC->ODR ^= (1 << 13);
}
nhưng không xóa:
EXTI->PR
Có thể dẫn đến việc interrupt tiếp tục bị xử lý lại.
Vì vậy luôn kiểm tra:
if (EXTI->PR & (1 << 0))
{
// xử lý
EXTI->PR = (1 << 0);
}
32. Ba tầng bạn phải thuộc lòng
Từ bây giờ khi debug interrupt STM32, hãy kiểm tra theo 3 tầng:
Tầng 1 – Peripheral
Ví dụ EXTI:
EXTI->IMR
EXTI->RTSR
EXTI->FTSR
EXTI->PR
Tầng 2 – NVIC
NVIC_EnableIRQ(...)
NVIC_SetPriority(...)
Tầng 3 – ISR
void EXTI0_IRQHandler(void)
{
}
Nếu interrupt không chạy, kiểm tra lần lượt 3 tầng này.
33. Bài tập Bài 34
Với Blue Pill của bạn, hãy làm bài sau:
Phần A
PA0 → Button
PC13 → LED onboard
PA0 dùng:
Input Pull-up
Nhấn nút tạo:
Falling Edge
Phần B
Thiết lập:
NVIC_SetPriority(EXTI0_IRQn, 1);
và:
NVIC_EnableIRQ(EXTI0_IRQn);
Phần C
Trong ISR:
void EXTI0_IRQHandler(void)
{
if (EXTI->PR & (1 << 0))
{
// xử lý
EXTI->PR = (1 << 0);
}
}
Phần D – thử nghiệm
Hãy thử:
NVIC_DisableIRQ(EXTI0_IRQn);
Quan sát nút còn điều khiển LED không.
Sau đó:
NVIC_EnableIRQ(EXTI0_IRQn);
34. Kiến thức cần thuộc sau Bài 34
Nếu tôi hỏi bạn:
NVIC làm nhiệm vụ gì?
Bạn phải trả lời được:
NVIC quản lý các nguồn interrupt của Cortex-M3: enable/disable, pending, priority và chuyển CPU đến ISR tương ứng thông qua cơ chế vector interrupt.
Và nhớ sơ đồ:
GPIO
↓
EXTI
↓
Pending
↓
NVIC
┌────┴────┐
│ Priority│
│ Enable │
│ Pending │
└────┬────┘
↓
CPU
↓
Vector Table
↓
ISR
↓
xử lý
↓
return
Bài tiếp theo: BÀI 35 – DEBOUNCE NÚT NHẤN + EXTI + TIMER
Bài 35 sẽ rất thực tế. Chúng ta sẽ giải quyết chính vấn đề ở Bài 33:
Nhấn 1 lần
↓
Nút cơ khí dội
↓
HIGH LOW HIGH LOW HIGH...
↓
EXTI có thể phát sinh nhiều lần
↓
LED nhảy loạn
Sau đó chúng ta sẽ tự viết debounce, trước tiên bằng phương pháp đơn giản, rồi tiến tới debounce bằng Timer interrupt. Đây là nền tảng rất hữu ích cho các project Blue Pill thực tế của bạn.