🌱 Core 8. Ngắt và bộ quản lý ngắt NVIC lõi ARM Cortex-M
Ngắt là một kỹ thuật thiết kế chương trình rất quan trọng trong lập trình Nhúng. Ngoài việc để chương trình chạy liên tục trong một vòng while(1) (kiểu polling), thì với những chương trình lớn, việc các sự kiện quan trọng không thể chờ đợi những sự kiện khác, thì việc sử dụng các ngắt là rất quan trọng.
Trong vi điều khiển STM32 hỗ trợ lên đến trên 200 ngắt khác nhau (240), chúng hoàn toàn có thể xảy ra cùng lúc, lồng nhau, ... Vì vậy, giống như một công ty hơn 200 nhân viên, chúng ta cần một nhà quản lý - đối với ngắt là NVIC - Nested Vector Interrupt Controller (Bộ điều khiển các vector ngắt lồng nhau).
# ARM Cortex M Exception Model
NVIC là gì?
NVIC - Nested Vector Interrupt Controller, là bộ điều khiển vector ngắt lồng nhau, nó là một ngoại vi của Core Cortex Mx. NVIC được sử dụng để cấu hình 240 interrupts: External, SPI, CAN, DMA, … Số lượng ngắt này là do nhà sản xuất Vi điều khiển quyết định. (Ví dụ TI chỉ dùng 154 interrupts cho TIVA MCU, trong khi ST chỉ dùng 83 interrupts cho dòng STM32F401xx).
- Sử dụng bộ thanh ghi NVIC để Enable/Disable/Pend các ngắt khác nhau và đọc trạng thái các hoạt động & pending interrupts.
- Tên gọi "Nested" – Tức là các ngắt lồng nhau xảy ra, bộ xử lý sẽ tạm thời dừng việc thực thi interrupt handler có mức ưu tiên thấp hơn khi một interrupt handler có mức độ ưu tiên cao hơn xảy ra.
- Bạn có thể cấu hình mức độ ưu tiên và phân nhóm ưu tiên.
Bộ thanh ghi NVIC
💬 ISER - Interrupt Set-Enable Registers
Có 8 thanh ghi để kích hoạt ngắt – dành cho 240 ngắt khác nhau. NVIC_ISER0 => NVIC_ISER7. Mỗi thanh ghi có kích thước là 32 bits => Mỗi thanh ghi có thể kích hoạt 32 ngắt khác nhau.
💬 ICER - Interrupt Clear-Enable Registers
Có 8 thanh ghi để kích hoạt ngắt – dành cho 240 ngắt khác nhau. NVIC_ICER0 => NVIC_ICER7. Mỗi thanh ghi có kích thước là 32 bits => Mỗi thanh ghi có thể tắt 32 ngắt khác nhau.
💬 ISPR - Interrupt Set-Pending Registers
Có 8 thanh ghi để kích hoạt ngắt – dành cho 240 ngắt khác nhau. NVIC_ISPR0 => NVIC_ISPR7, dùng để buộc ngắt vào trạng thái chờ xử lý. Bộ xử lý sẽ kiểm tra mức độ ưu tiên của các ngắt đang chờ xử lý để lần lượt thực thi chúng.
💬 ICPR - Interrupt Clear-Pending Registers
Có 8 thanh ghi để kích hoạt ngắt – dành cho 240 ngắt khác nhau. NVIC_ICPR0 => NVIC_ICPR7, dùng để loại bỏ trạng thái chờ xử lý. Đọc nó để xem ngắt nào đang chờ xử lý.
💬 IABR - Interrupt Active Bit Registers
Có 8 thanh ghi, NVIC_IABR0 => NVIC_IABR7, biểu thị cho việc ngắt hoạt động hay không.
💬 IPR - Interrupt Priority Registers
NVIC_IPR0 => NVIC_IPR59 cung cấp một trường ưu tiên 8-bit cho mỗi ngắt và mỗi thanh ghi giữ 4 trường ưu tiên này. Các thanh ghi này có thể truy cập từng byte.
Cách hoạt động của NVIC với ngắt
Mỗi ngắt trong vi điều khiển được đại diện bằng một IRQ number.
- Khi có sự kiện ngắt xảy ra, ngoại vi sẽ kích hoạt một tín hiệu ngắt trên đường IRQ này.
- NVIC sẽ đặt nó và trạng thái chờ - pending.
- Bộ NVIC sẽ lần lượt thực hiện các pending interrupts, tùy thuộc vào mức ưu tiên.
- Khi đến lượt của một interrupt (IRQ Enable = 1), NVIC sẽ gửi tín hiệu ngắt đến CPU.
- CPU tiếp nhận tín hiệu ngắt từ NVIC nếu ngắt tới có mức độ ưu tiên cao hơn ISR đang hoạt động.
- Khi ngắt tới được CPU chấp nhận, CPU sẽ lấy địa chỉ chương trình con phục vụ ngắt - ISR tương ứng với IRQ number từ bảng vector ngắt (Vector Table). Sau đó thực thi ISR đó.
Việc nắm được cách hoạt động của ngắt và ngoại vi NVIC là rất quan trọng khi thiết kế chương trình với ngắt.
Cấu hình NVIC trong thực tế
💬 Cách tính địa chỉ thanh ghi NVIC
Để enable một ngắt cụ thể, ta cần biết IRQ number của nó và tính toán thanh ghi ISER tương ứng:
// Ví dụ: Enable EXTI0 interrupt (IRQ6 trên STM32F4)
#define EXTI0_IRQn 6
// Tính toán thanh ghi ISER và bit position
uint8_t iser_reg = EXTI0_IRQn / 32; // = 0 (NVIC_ISER0)
uint8_t bit_pos = EXTI0_IRQn % 32; // = 6
// Enable interrupt
NVIC->ISER[iser_reg] = (1 << bit_pos);
// Hoặc sử dụng CMSIS function
NVIC_EnableIRQ(EXTI0_IRQn);
💬 Cấu hình Priority cho ngắt
Mỗi ngắt có một trường priority 8-bit, nhưng không phải tất cả 8 bit đều được sử dụng. STM32F4 chỉ sử dụng 4 bit cao nhất (bit [7:4]):
// Cấu hình priority cho EXTI0
// Priority value: 0 = highest, 15 = lowest (với 4 bits)
#define EXTI0_PRIORITY 5
// Cách 1: Truy cập trực tiếp thanh ghi IPR
NVIC->IP[EXTI0_IRQn] = (EXTI0_PRIORITY << 4);
// Cách 2: Sử dụng CMSIS function
NVIC_SetPriority(EXTI0_IRQn, EXTI0_PRIORITY);
// Đọc priority hiện tại
uint32_t current_priority = NVIC_GetPriority(EXTI0_IRQn);
💬 Ví dụ hoàn chỉnh: Cấu hình EXTI interrupt
void EXTI0_Config(void)
{
// 1. Enable clock cho GPIOA và SYSCFG
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
// 2. Cấu hình PA0 là input
GPIOA->MODER &= ~(3 << 0); // Input mode
// 3. Kết nối EXTI0 với PA0
SYSCFG->EXTICR[0] &= ~(0xF << 0); // Clear
SYSCFG->EXTICR[0] |= (0 << 0); // PA0
// 4. Cấu hình EXTI0: Rising edge trigger
EXTI->RTSR |= (1 << 0); // Rising trigger
EXTI->FTSR &= ~(1 << 0); // Disable falling trigger
// 5. Unmask EXTI0
EXTI->IMR |= (1 << 0);
// 6. Cấu hình NVIC
NVIC_SetPriority(EXTI0_IRQn, 5); // Priority 5
NVIC_EnableIRQ(EXTI0_IRQn); // Enable interrupt
}
// Interrupt handler
void EXTI0_IRQHandler(void)
{
// Kiểm tra pending bit
if (EXTI->PR & (1 << 0))
{
// Xử lý ngắt
// ...
// Clear pending bit
EXTI->PR = (1 << 0);
}
}
💬 Kiểm tra trạng thái ngắt
// Kiểm tra xem ngắt có được enable không
uint32_t is_enabled = NVIC->ISER[EXTI0_IRQn / 32] & (1 << (EXTI0_IRQn % 32));
// Kiểm tra pending status
uint32_t is_pending = NVIC->ISPR[EXTI0_IRQn / 32] & (1 << (EXTI0_IRQn % 32));
// Kiểm tra active status
uint32_t is_active = NVIC->IABR[EXTI0_IRQn / 32] & (1 << (EXTI0_IRQn % 32));
// Set pending bit bằng software (trigger ngắt)
NVIC->ISPR[EXTI0_IRQn / 32] = (1 << (EXTI0_IRQn % 32));
// Clear pending bit
NVIC->ICPR[EXTI0_IRQn / 32] = (1 << (EXTI0_IRQn % 32));
Interrupt Latency và Tail-Chaining
💬 Interrupt Latency
Interrupt Latency là khoảng thời gian từ khi ngắt xảy ra đến khi CPU bắt đầu thực thi ISR. Trên ARM Cortex-M, latency bao gồm:
- Interrupt acceptance: 1-2 cycles (NVIC kiểm tra priority)
- State saving: 12 cycles (push 8 registers: R0-R3, R12, LR, PC, xPSR)
- Vector fetch: 2-3 cycles (đọc địa chỉ ISR từ vector table)
Tổng latency: ~15-17 cycles trong trường hợp tốt nhất. Với CPU chạy 168MHz (STM32F4), latency ≈ 90-100ns.
// Đo interrupt latency bằng GPIO toggle
void EXTI0_IRQHandler(void)
{
GPIOA->BSRR = (1 << 1); // Set PA1 HIGH (bắt đầu ISR)
// Xử lý ngắt
// ...
GPIOA->BSRR = (1 << (1 + 16)); // Set PA1 LOW (kết thúc ISR)
EXTI->PR = (1 << 0); // Clear pending
}
// Dùng oscilloscope đo khoảng thời gian từ rising edge PA0 đến rising edge PA1
// = Interrupt latency
💬 Tail-Chaining
Tail-Chaining là kỹ thuật tối ưu của phần cứng ARM Cortex-M: khi một ISR kết thúc và có ngắt khác đang pending, CPU sẽ không restore context mà nhảy trực tiếp sang ISR tiếp theo.
- Không cần pop 8 registers (tiết kiệm 12 cycles)
- Không cần push lại 8 registers (tiết kiệm 12 cycles)
- Chỉ cần fetch vector mới: ~6 cycles thay vì ~30 cycles
// Ví dụ: 2 ngắt xảy ra liên tiếp
void TIM2_IRQHandler(void) // Priority 5
{
// Xử lý TIM2
TIM2->SR &= ~TIM_SR_UIF;
}
void TIM3_IRQHandler(void) // Priority 6 (pending trong khi TIM2 đang chạy)
{
// Nhờ tail-chaining, ISR này được gọi ngay sau TIM2
// mà không cần restore/save context
TIM3->SR &= ~TIM_SR_UIF;
}
💬 Late Arrival
Late Arrival: Nếu một ngắt có priority cao hơn xảy ra trong khi CPU đang save context cho ngắt priority thấp, CPU sẽ hủy việc save context và xử lý ngắt priority cao trước.
Nested Interrupts - Ngắt lồng nhau
💬 Cơ chế hoạt động
Ngắt lồng nhau xảy ra khi một ISR đang chạy bị gián đoạn bởi một ngắt có priority cao hơn:
// Cấu hình 2 ngắt với priority khác nhau
void Interrupt_Config(void)
{
// EXTI0: Priority 5 (thấp hơn)
NVIC_SetPriority(EXTI0_IRQn, 5);
NVIC_EnableIRQ(EXTI0_IRQn);
// EXTI1: Priority 3 (cao hơn)
NVIC_SetPriority(EXTI1_IRQn, 3);
NVIC_EnableIRQ(EXTI1_IRQn);
}
void EXTI0_IRQHandler(void) // Priority 5
{
GPIOA->BSRR = (1 << 2); // Set PA2 = 1
// Giả sử EXTI1 xảy ra tại đây
// CPU sẽ tạm dừng EXTI0_IRQHandler
// và nhảy sang EXTI1_IRQHandler
for (volatile int i = 0; i < 100000; i++); // Delay
GPIOA->BSRR = (1 << (2 + 16)); // Set PA2 = 0
EXTI->PR = (1 << 0);
}
void EXTI1_IRQHandler(void) // Priority 3 (cao hơn)
{
GPIOA->BSRR = (1 << 3); // Set PA3 = 1
// Xử lý ngắt priority cao
for (volatile int i = 0; i < 50000; i++);
GPIOA->BSRR = (1 << (3 + 16)); // Set PA3 = 0
EXTI->PR = (1 << 1);
// Sau khi return, CPU sẽ tiếp tục EXTI0_IRQHandler
}
💬 Vô hiệu hóa Nested Interrupts
Trong một số trường hợp, bạn muốn ISR không bị gián đoạn:
void EXTI0_IRQHandler(void)
{
// Cách 1: Disable toàn bộ interrupts
__disable_irq(); // CPSID i
// Critical section - không bị gián đoạn
// ...
__enable_irq(); // CPSIE i
// Cách 2: Tăng BASEPRI để chặn interrupts có priority thấp hơn
__set_BASEPRI(4 << 4); // Chặn priority >= 4
// Critical section
// ...
__set_BASEPRI(0); // Cho phép tất cả interrupts
EXTI->PR = (1 << 0);
}
💬 Ví dụ thực tế: UART + DMA
// UART interrupt: Priority 5 (xử lý từng byte)
void USART1_IRQHandler(void)
{
if (USART1->SR & USART_SR_RXNE)
{
uint8_t data = USART1->DR;
// Xử lý data
// DMA interrupt có thể xảy ra tại đây nếu có priority cao hơn
}
}
// DMA interrupt: Priority 3 (xử lý buffer đầy)
void DMA2_Stream2_IRQHandler(void)
{
if (DMA2->LISR & DMA_LISR_TCIF2)
{
// Buffer đầy, xử lý ngay
Process_Buffer();
DMA2->LIFCR = DMA_LIFCR_CTCIF2;
}
}
Lưu ý và Best Practices
💬 Các lỗi thường gặp
1. Quên clear pending bit:
// SAI
void EXTI0_IRQHandler(void)
{
// Xử lý ngắt
// Quên clear pending bit
}
// Kết quả: ISR được gọi liên tục
// ĐÚNG
void EXTI0_IRQHandler(void)
{
// Xử lý ngắt
EXTI->PR = (1 << 0); // Clear pending bit
}
2. ISR quá dài:
// SAI - ISR quá dài
void TIM2_IRQHandler(void)
{
for (int i = 0; i < 1000000; i++) // Delay quá lâu
{
// Xử lý phức tạp
}
TIM2->SR &= ~TIM_SR_UIF;
}
// ĐÚNG - Dùng flag và xử lý trong main
volatile uint8_t tim2_flag = 0;
void TIM2_IRQHandler(void)
{
tim2_flag = 1; // Set flag
TIM2->SR &= ~TIM_SR_UIF;
}
int main(void)
{
while (1)
{
if (tim2_flag)
{
tim2_flag = 0;
// Xử lý phức tạp ở đây
}
}
}
3. Truy cập biến toàn cục không an toàn:
// SAI - Không dùng volatile
uint32_t counter = 0;
void TIM2_IRQHandler(void)
{
counter++; // Compiler có thể tối ưu hóa sai
TIM2->SR &= ~TIM_SR_UIF;
}
// ĐÚNG - Dùng volatile
volatile uint32_t counter = 0;
void TIM2_IRQHandler(void)
{
counter++; // Compiler không tối ưu hóa
TIM2->SR &= ~TIM_SR_UIF;
}
// ĐÚNG HƠN - Dùng atomic operations (nếu cần)
#include
atomic_uint counter = 0;
void TIM2_IRQHandler(void)
{
atomic_fetch_add(&counter, 1);
TIM2->SR &= ~TIM_SR_UIF;
}
💬 Best Practices
- Giữ ISR ngắn gọn: Chỉ làm những việc cần thiết, xử lý phức tạp trong main loop
- Sử dụng từ khóa volatile: Cho tất cả biến được truy cập từ cả ISR và main
- Clear pending bit: Luôn clear pending bit ở cuối ISR
- Cấu hình priority hợp lý: Ngắt quan trọng/thời gian thực có priority cao hơn
- Tránh blocking operations: Không dùng delay, printf, malloc trong ISR
- Kiểm tra pending bit: Trước khi xử lý, kiểm tra xem ngắt có thực sự xảy ra không
💬 Ví dụ template ISR chuẩn
// Template ISR chuẩn
void EXTI0_IRQHandler(void)
{
// 1. Kiểm tra pending bit
if (EXTI->PR & (1 << 0))
{
// 2. Xử lý ngắt (ngắn gọn)
// Set flag hoặc cập nhật biến
// 3. Clear pending bit
EXTI->PR = (1 << 0);
}
}
// Template cho multiple sources
void TIM2_IRQHandler(void)
{
uint32_t sr = TIM2->SR;
// Update interrupt
if (sr & TIM_SR_UIF)
{
// Xử lý update event
TIM2->SR &= ~TIM_SR_UIF;
}
// Capture/Compare interrupt
if (sr & TIM_SR_CC1IF)
{
// Xử lý capture/compare
TIM2->SR &= ~TIM_SR_CC1IF;
}
}
💬 Debug interrupts
// Hàm debug: In ra trạng thái NVIC
void NVIC_Debug_Print(IRQn_Type IRQn)
{
uint8_t iser_reg = IRQn / 32;
uint8_t bit_pos = IRQn % 32;
printf("IRQ %d:\n", IRQn);
printf(" Enabled: %d\n",
(NVIC->ISER[iser_reg] >> bit_pos) & 1);
printf(" Pending: %d\n",
(NVIC->ISPR[iser_reg] >> bit_pos) & 1);
printf(" Active: %d\n",
(NVIC->IABR[iser_reg] >> bit_pos) & 1);
printf(" Priority: %d\n",
NVIC_GetPriority(IRQn));
}
// Đếm số lần ISR được gọi
volatile uint32_t exti0_count = 0;
void EXTI0_IRQHandler(void)
{
exti0_count++;
EXTI->PR = (1 << 0);
}
>>>>>> Follow ngay <<<<<<<
Để nhận được những bài học miễn phí mới nhất nhé 😊
Chúc các bạn học tập tốt 😊






