🌱 Core 9. Interrupt Priority - Độ ưu tiên ngắt

🌱 Core 9. Interrupt Priority - Độ ưu tiên ngắt

    Ở bài trước chúng ta đã cùng tìm hiểu về bộ điều khiển ngắt NVIC trong vi điều khiển STM32. Vậy với khối lượng ngắt lớn như vậy (có thể lên đến 240 ngắt), thì nếu các ngắt xảy ra đồng thời, hay lồng nhau, thì NVIC sẽ xử lý ra sao? Như vậy, các ngắt sẽ cần có độ ưu tiên cao thấp khác nhau - Interrupt Priority để NVIC có thể phân biệt được ngắt nào nên được thực hiện trước.

Interrupt Priority là gì?

    Mức độ ưu tiên - Priority được coi là mức độ khẩn cấp của ngắt, tức là quy định ngắt nào cần được thực hiện trước.

    Giá trị mức độ ưu tiên – priority value là thước đo mức độ khẩn cấp của ngắt, còn gọi là mức độ ưu tiên – priority levels.  

    Với Cortex Mx, giá trị ưu tiên này càng nhỏ thì mức độ ưu tiên càng lớn. Chẳng hạn, các exception của hệ thống đa số có mức ưu tiên nhỏ hơn 0 (trong đó Reset Handler có priority value nhỏ nhất, tương đương với mức độ ưu tiên lớn nhất). 

    Nếu hai ngắt xảy ra cùng lúc thì NVIC sẽ tiếp nhận thực thi ngắt có mức độ ưu tiên cao hơn (priority value thấp hơn), và đưa ngắt còn lại vào trạng thái chờ - Pending.

Interrupt

     Ví dụ trên đây cho thấy 2 ngắt cùng xảy ra đó là ADC (Priority Value = 5) và Timer (Priority Value = 4). Vì vậy Timer Interrupt có mức độ ưu tiên cao hơn so với ADC Interrupt. Timer Interrupt sẽ được thực thi, trong khi đó ADC Interrupt sẽ được đưa vào hàng chờ Pending.

NVIC Interrupt Priority Registers

    Số lượng mức độ ưu tiên có thể được lập trình phụ thuộc vào Thanh ghi Interrupt Priority, do nhà cung cấp MCU – Các thanh ghi NVIC_IPR0 => NVIC_IPR59 (Có nói ở bài trước). 
STM32F4x có 16 mức độ ưu tiên khác nhau trong khi TI TM4C123Gx là 8.

    Mỗi thanh ghi NVIC_IPR chia làm 4 phần, mỗi phần 8 bit để kiểm soát mức độ ưu tiên cho mỗi IRQ. Chỉ một số bit được sử dụng để kiểm soát mức độ ưu tiên: Ví dụ, có 8 priority levels cần 3 bit, trong khi 16 levels 4 bits.

Interrupt

    Chẳng hạn, với 8 priority levels thì mỗi thanh ghi 8 bits sẽ cần 3 bits để triển khai.

Pre-empt Priority & Sub Priority

    Câu hỏi đặt ra là điều gì sẽ xảy ra nếu hai ngắt có cùng một mức độ ưu tiên xảy ra cùng một lúc? Xung đột có thể xảy ra, vì vậy, cần có một mức độ ưu tiên phụ (Sub Priority) để giải quyết vấn đề này.

  • Pre-Empt Priority: Khi bộ xử lý đang chạy một trình xử lý ngắt và một ngắt khác xảy ra, thì các giá trị ưu tiên trước – pre-empt priority sẽ được so sánh và ngắt với mức độ pre-empt priority cao hơn (giá trị nhỏ hơn) sẽ được cho phép chạy.
  • Sub Priority: Giá trị này chỉ được sử dụng khi hai ngắt có cùng giá trị pre-empty priority xảy ra cùng thời điểm. Trong trường hợp này, ngắt có mức ưu tiên phụ cao hơn sẽ được xử lý trước. 
Interrupt

    Có nhiều nhóm ưu tiên – priority grouping khác nhau và theo mặc định trong bộ xử lý Cortex Mx, nhóm ưu tiên = 0. Nhóm ưu tiên phụ thuộc vào việc sử dụng thanh ghi NVIC_IPRx.
Ví dụ: Nhóm ưu tiên mặc định là 0, bit [7:1] được coi là pre-empt priority, còn bit 0 là sub priority.

Priority Grouping Configuration

    Thanh ghi AIRCR (Application Interrupt and Reset Control Register) trong SCB (System Control Block) được sử dụng để cấu hình priority grouping thông qua trường PRIGROUP (bits [10:8]).

PRIGROUP Bit [7:4] IPR Bit [3:0] IPR Pre-empt Priority Bits Sub Priority Bits Pre-empt Levels Sub Levels
0b011 (3) [7:4] - 4 0 16 1
0b100 (4) [7:5] [4] 3 1 8 2
0b101 (5) [7:6] [5:4] 2 2 4 4
0b110 (6) [7] [6:4] 1 3 2 8
0b111 (7) - [7:4] 0 4 1 16

    Với STM32F4, chỉ có 4 bits được implement (bits [7:4]), do đó có 16 mức priority. Các bits [3:0] luôn đọc là 0.

Cấu hình Priority Grouping trong STM32

// 1. Cấu hình Priority Grouping = 4
// => 3 bits Preempt Priority, 1 bit Sub Priority
NVIC_SetPriorityGrouping(4);

// 2. Cấu hình Priority cho EXTI0 interrupt
// Preempt Priority = 2, Sub Priority = 0
uint32_t priorityGrouping = NVIC_GetPriorityGrouping();
uint32_t priority = NVIC_EncodePriority(priorityGrouping, 2, 0);
NVIC_SetPriority(EXTI0_IRQn, priority);
NVIC_EnableIRQ(EXTI0_IRQn);

// 3. Cấu hình Priority cho TIM2 interrupt
// Preempt Priority = 1, Sub Priority = 1
priority = NVIC_EncodePriority(priorityGrouping, 1, 1);
NVIC_SetPriority(TIM2_IRQn, priority);
NVIC_EnableIRQ(TIM2_IRQn);

Nested Interrupts - Ngắt lồng nhau

    Ngắt lồng nhau xảy ra khi một ngắt có preempt priority cao hơn xảy ra trong khi đang xử lý một ngắt có preempt priority thấp hơn. CPU sẽ tạm dừng ISR hiện tại, lưu context, và nhảy đến ISR có priority cao hơn.

Ví dụ Nested Interrupts

int main(void)
{
// 1. Cấu hình Priority Grouping = 4
// => 3 bits preempt, 1 bit subpriority
NVIC_SetPriorityGrouping(4);

uint32_t priorityGroup = NVIC_GetPriorityGrouping();

// 2. UART Interrupt: Preempt = 3 (priority thấp)
NVIC_SetPriority(USART1_IRQn, NVIC_EncodePriority(priorityGroup, 3, 0));
NVIC_EnableIRQ(USART1_IRQn);

// 3. Timer Interrupt: Preempt = 2 (priority trung bình)
NVIC_SetPriority(TIM2_IRQn, NVIC_EncodePriority(priorityGroup, 2, 0));
NVIC_EnableIRQ(TIM2_IRQn);

// 4. External Interrupt: Preempt = 1 (priority cao)
NVIC_SetPriority(EXTI0_IRQn, NVIC_EncodePriority(priorityGroup, 1, 0));
NVIC_EnableIRQ(EXTI0_IRQn);

while (1)
{
// main loop - CPU có thể bị ngắt bất kỳ lúc nào tùy mức priority
}
}

// ========================== ISR Handlers ==========================

// Đang xử lý UART interrupt (preempt = 3)
// Nếu TIM2 hoặc EXTI0 xảy ra => bị preempt
void USART1_IRQHandler(void)
{
// Xử lý ngắt UART (giả lập)
// Ví dụ: đọc dữ liệu từ USART1->DR hoặc xóa cờ
if (USART1->SR & USART_SR_RXNE)
{
volatile uint8_t data = USART1->DR;
(void)data; // tránh warning
}
}

// Đang xử lý Timer interrupt (preempt = 2)
// Có thể preempt UART (3), bị preempt bởi EXTI0 (1)
void TIM2_IRQHandler(void)
{
// Xử lý ngắt Timer (giả lập)
if (TIM2->SR & TIM_SR_UIF)
{
TIM2->SR &= ~TIM_SR_UIF; // xóa cờ update
}
}

// Đang xử lý External interrupt (preempt = 1)
// Có thể preempt cả UART và Timer
void EXTI0_IRQHandler(void)
{
// Xử lý ngắt ngoài EXTI line 0 (giả lập)
if (EXTI->PR & EXTI_PR_PR0)
{
EXTI->PR = EXTI_PR_PR0; // xóa cờ pending
}
}

Kịch bản Nested Interrupts

Timeline:
t0: CPU đang chạy main()
t1: UART interrupt xảy ra → Nhảy vào USART1_IRQHandler()
t2: Đang trong USART1_IRQHandler(), Timer interrupt xảy ra
    → Timer có preempt priority cao hơn (2 < 3)
    → CPU lưu context của UART ISR
    → Nhảy vào TIM2_IRQHandler()
t3: Đang trong TIM2_IRQHandler(), EXTI0 interrupt xảy ra
    → EXTI0 có preempt priority cao hơn (1 < 2)
    → CPU lưu context của Timer ISR
    → Nhảy vào EXTI0_IRQHandler()
t4: EXTI0_IRQHandler() hoàn thành
    → Khôi phục context của Timer ISR
    → Tiếp tục TIM2_IRQHandler()
t5: TIM2_IRQHandler() hoàn thành
    → Khôi phục context của UART ISR
    → Tiếp tục USART1_IRQHandler()
t6: USART1_IRQHandler() hoàn thành
    → Quay lại main()

Lưu ý: Nested interrupts chỉ xảy ra khi có sự khác biệt về preempt priority. Nếu hai interrupts có cùng preempt priority, interrupt đang chạy sẽ không bị preempt, interrupt mới sẽ ở trạng thái pending.

Interrupt Latency với Priority

    Interrupt latency là thời gian từ khi interrupt request xảy ra cho đến khi CPU bắt đầu thực thi ISR. Latency phụ thuộc vào nhiều yếu tố, trong đó có priority.

Các trường hợp Interrupt Latency

Trường hợp Latency (cycles) Mô tả
CPU đang idle 12 Thời gian tối thiểu để stack pushing và vector fetch
CPU đang chạy code 12 + N N = số cycles để hoàn thành instruction hiện tại
Đang trong ISR (priority thấp hơn) 12 Preemption xảy ra ngay lập tức
Đang trong ISR (priority cao hơn hoặc bằng) 12 + T_ISR T_ISR = thời gian để ISR hiện tại hoàn thành
Tail-chaining 6 Chuyển từ ISR này sang ISR khác mà không cần pop/push stack

Ví dụ tính Interrupt Latency

// Giả sử STM32F4 chạy ở 168MHz
// 1 cycle = 1/168MHz ≈ 6ns

Scenario 1: CPU đang idle
- Latency = 12 cycles = 72ns

Scenario 2: CPU đang chạy code, instruction cần 3 cycles
- Latency = 12 + 3 = 15 cycles = 90ns

Scenario 3: Đang trong UART ISR (preempt=3), Timer IRQ (preempt=2) xảy ra
- Timer có priority cao hơn → Preemption
- Latency = 12 cycles = 72ns

Scenario 4: Đang trong Timer ISR (preempt=2), UART IRQ (preempt=3) xảy ra
- UART có priority thấp hơn → Pending
- Giả sử Timer ISR mất 500 cycles
- Latency = 12 + 500 = 512 cycles = 3.07μs

Scenario 5: Tail-chaining từ UART ISR sang Timer ISR
- Latency = 6 cycles = 36ns

Priority Inversion Problem

    Priority Inversion là hiện tượng một task/interrupt có priority cao bị chặn bởi một task/interrupt có priority thấp hơn, thường xảy ra khi sử dụng shared resources (mutex, semaphore).

Ví dụ Priority Inversion

// Shared resource được bảo vệ bởi mutex
volatile uint32_t shared_data;
osMutexId_t data_mutex;

// Task Low Priority (Priority = 1)
void Task_LowPriority(void *argument) {
    while(1) {
        osMutexAcquire(data_mutex, osWaitForever);
        // Đang giữ mutex, xử lý shared_data
        shared_data = process_data();
        osDelay(100); // Giả lập xử lý lâu
        osMutexRelease(data_mutex);
    }
}

// Task Medium Priority (Priority = 2)
void Task_MediumPriority(void *argument) {
    while(1) {
        // Không dùng shared resource
        do_something_else();
        osDelay(50);
    }
}

// Task High Priority (Priority = 3)
void Task_HighPriority(void *argument) {
    while(1) {
        osMutexAcquire(data_mutex, osWaitForever);
        // Cần truy cập shared_data
        uint32_t value = shared_data;
        process_value(value);
        osMutexRelease(data_mutex);
        osDelay(200);
    }
}

/* Priority Inversion xảy ra:
 * t0: Task_LowPriority acquire mutex
 * t1: Task_HighPriority cần mutex → Blocked
 * t2: Task_MediumPriority preempt Task_LowPriority
 * t3: Task_HighPriority vẫn blocked vì Task_LowPriority chưa release mutex
 *     → High priority task bị chặn bởi medium priority task!
 */

Giải pháp: Priority Inheritance

// Sử dụng Priority Inheritance Protocol
// Khi Low Priority task giữ mutex mà High Priority task cần,
// Low Priority task tạm thời được nâng priority lên bằng High Priority task

// Cấu hình mutex với priority inheritance
osMutexAttr_t mutex_attr = {
    .name = "DataMutex",
    .attr_bits = osMutexPrioInherit  // Enable priority inheritance
};
data_mutex = osMutexNew(&mutex_attr);

/* Với Priority Inheritance:
 * t0: Task_LowPriority acquire mutex (priority = 1)
 * t1: Task_HighPriority cần mutex → Blocked
 *     Task_LowPriority priority tạm thời = 3 (inherit từ High)
 * t2: Task_MediumPriority không thể preempt vì Low task có priority = 3
 * t3: Task_LowPriority release mutex, priority trở về 1
 * t4: Task_HighPriority acquire mutex và chạy
 */

Best Practices cho Interrupt Priority

1. Phân loại Interrupts theo tính chất

Loại Interrupt Priority Level Ví dụ
Critical Real-time 0-2 (Cao nhất) Motor control, Safety shutdown, High-speed ADC
Time-sensitive 3-5 (Cao) Timer PWM, Encoder, CAN bus
Normal I/O 6-10 (Trung bình) UART, SPI, I2C, GPIO
Background tasks 11-15 (Thấp) DMA completion, Low-priority timers

2. Giữ ISR ngắn gọn

3. Tránh blocking operations trong ISR

// ❌ BAD: Blocking trong ISR
void EXTI0_IRQHandler(void) {
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);

// Blocking delay trong ISR
HAL_Delay(100); // NEVER DO THIS!

// Blocking I2C transaction
HAL_I2C_Master_Transmit(&hi2c1, addr, data, size, 1000); // BAD!
}

// ✅ GOOD: Non-blocking operations
void EXTI0_IRQHandler(void) {
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);

// Sử dụng interrupt-driven hoặc DMA
HAL_I2C_Master_Transmit_IT(&hi2c1, addr, data, size);

// Hoặc set flag để xử lý sau
i2c_pending = 1;
}

4. Cẩn thận với Shared Variables

// ❌ BAD: Race condition
volatile uint32_t counter = 0;

void TIM2_IRQHandler(void) {
counter++; // Có thể bị race condition nếu counter > 8 bits
}

int main(void) {
uint32_t local_counter = counter; // Có thể đọc giá trị không nhất quán
}

// ✅ GOOD: Atomic access hoặc disable interrupt
volatile uint32_t counter = 0;

void TIM2_IRQHandler(void) {
counter++;
}

int main(void) {
uint32_t local_counter;

// Cách 1: Disable interrupt khi đọc
__disable_irq();
local_counter = counter;
__enable_irq();

// Cách 2: Sử dụng atomic operations (nếu có)
local_counter = __atomic_load_n(&counter, __ATOMIC_SEQ_CST);
}

5. Sử dụng Priority Grouping hợp lý

// Ứng dụng cần nhiều mức preemption, ít sub-priority
NVIC_SetPriorityGrouping(3); // 4 bits preempt, 0 bit sub

// Ứng dụng cần cân bằng
NVIC_SetPriorityGrouping(4); // 3 bits preempt, 1 bit sub

// Ứng dụng cần nhiều sub-priority để xử lý interrupts cùng level
NVIC_SetPriorityGrouping(5); // 2 bits preempt, 2 bits sub

Một số lỗi thường gặp:

  • Không cấu hình priority grouping trước khi set priority → Kết quả không như mong đợi
  • ISR quá dài → Tăng interrupt latency cho các interrupts khác
  • Quá nhiều nested interrupts → Stack overflow
  • Không bảo vệ shared resources → Race conditions
  • Sử dụng blocking functions trong ISR → Deadlock hoặc system hang

Ví dụ thực tế: Motor Control System

#include "stm32f4xx.h"

// ========================== Global Variables ==========================
volatile uint8_t emergency_stop = 0;
volatile uint16_t motor_pwm_duty = 500;
volatile uint32_t encoder_count = 0;
volatile float motor_speed = 0.0f;

#define CURRENT_LIMIT 2000

// ========================== Function Prototypes ==========================
void Motor_Control_Init(void);
float calculate_speed(uint32_t encoder_count);
void process_can_message(uint32_t id, uint8_t *data);
void uart_rx_handler(uint8_t c);

// ========================== Initialization ==========================
void Motor_Control_Init(void) {
// Cấu hình Priority Grouping: 3 bits preempt, 1 bit sub
NVIC_SetPriorityGrouping(4);

// 1. Emergency Stop - Priority cao nhất (Preempt = 0)
NVIC_SetPriority(EXTI15_10_IRQn, NVIC_EncodePriority(4, 0, 0));
NVIC_EnableIRQ(EXTI15_10_IRQn);

// 2. High-speed ADC for current sensing (Preempt = 1)
NVIC_SetPriority(ADC_IRQn, NVIC_EncodePriority(4, 1, 0));
NVIC_EnableIRQ(ADC_IRQn);

// 3. PWM Timer for motor control (Preempt = 2)
NVIC_SetPriority(TIM1_UP_TIM10_IRQn, NVIC_EncodePriority(4, 2, 0));
NVIC_EnableIRQ(TIM1_UP_TIM10_IRQn);

// 4. Encoder Timer (Preempt = 3)
NVIC_SetPriority(TIM2_IRQn, NVIC_EncodePriority(4, 3, 0));
NVIC_EnableIRQ(TIM2_IRQn);

// 5. CAN bus communication (Preempt = 4)
NVIC_SetPriority(CAN1_RX0_IRQn, NVIC_EncodePriority(4, 4, 0));
NVIC_EnableIRQ(CAN1_RX0_IRQn);

// 6. UART debug (Preempt = 5)
NVIC_SetPriority(USART1_IRQn, NVIC_EncodePriority(4, 5, 0));
NVIC_EnableIRQ(USART1_IRQn);
}

// ========================== Interrupt Service Routines ==========================

// 1. Emergency Stop - Highest Priority
void EXTI15_10_IRQHandler(void) {
if (EXTI->PR & (1U << 15)) { // Kiểm tra cờ pending
EXTI->PR = (1U << 15); // Ghi 1 để xóa cờ

// Dừng PWM ngay lập tức
TIM1->CCR1 = 0;
TIM1->CCR2 = 0;

emergency_stop = 1;
}
}

// 2. ADC Interrupt - Current sensing
void ADC_IRQHandler(void) {
if (ADC1->SR & ADC_SR_EOC) { // End Of Conversion flag
uint16_t current = ADC1->DR; // Đọc giá trị ADC

// Kiểm tra quá dòng
if (current > CURRENT_LIMIT) {
if (motor_pwm_duty > 10)
motor_pwm_duty -= 10;
}
}
}

// 3. PWM Timer Interrupt - Motor Control Loop
void TIM1_UP_TIM10_IRQHandler(void) {
if (TIM1->SR & TIM_SR_UIF) {
TIM1->SR &= ~TIM_SR_UIF; // Clear update flag

// Cập nhật duty cycle
TIM1->CCR1 = motor_pwm_duty;
}
}

// 4. Encoder Timer Interrupt - Motor Speed Feedback
void TIM2_IRQHandler(void) {
if (TIM2->SR & TIM_SR_UIF) {
TIM2->SR &= ~TIM_SR_UIF;

encoder_count = TIM2->CNT;
motor_speed = calculate_speed(encoder_count);
}
}

// 5. CAN Bus Interrupt - Communication
void CAN1_RX0_IRQHandler(void) {
if (CAN1->RF0R & CAN_RF0R_FMP0) { // Có message trong FIFO0
uint32_t id = (CAN1->sFIFOMailBox[0].RIR >> 21);
uint8_t data[8];
uint32_t RDLR = CAN1->sFIFOMailBox[0].RDLR;
uint32_t RDHR = CAN1->sFIFOMailBox[0].RDHR;

for (int i = 0; i < 4; i++) {
data[i] = (RDLR >> (8 * i)) & 0xFF;
data[i + 4] = (RDHR >> (8 * i)) & 0xFF;
}

process_can_message(id, data);

CAN1->RF0R |= CAN_RF0R_RFOM0; // Release FIFO0
}
}

// 6. UART Debug Interrupt - Console output
void USART1_IRQHandler(void) {
if (USART1->SR & USART_SR_RXNE) { // Dữ liệu mới nhận
uint8_t c = USART1->DR;
uart_rx_handler(c);
}
}

// ========================== Support Functions ==========================

// Tính toán tốc độ dựa vào encoder count
float calculate_speed(uint32_t encoder_count) {
static uint32_t prev_count = 0;
int32_t delta = (int32_t)(encoder_count - prev_count);
prev_count = encoder_count;

// Giả sử 1000 xung/vòng, timer update mỗi 10ms
float speed_rps = (float)delta / 1000.0f / 0.01f;
return speed_rps * 60.0f; // RPM
}

// Xử lý message CAN nhận được
void process_can_message(uint32_t id, uint8_t *data) {
// Ví dụ: ID = 0x200 điều khiển duty PWM
if (id == 0x200) {
motor_pwm_duty = data[0];
}
}

// Xử lý dữ liệu UART nhận được (debug)
void uart_rx_handler(uint8_t c) {
// Ví dụ: gõ 's' để dừng khẩn
if (c == 's' || c == 'S') {
emergency_stop = 1;
}
}

// ========================== Main ==========================
int main(void) {
// Khởi tạo hệ thống
Motor_Control_Init();

while (1) {
if (emergency_stop) {
// Dừng toàn bộ hệ thống
TIM1->CCR1 = 0;
TIM1->CCR2 = 0;
// Có thể thêm báo lỗi tại đây
}

// Main control loop
// (Các tác vụ nhẹ, không blocking)
}
}

Giải thích thiết kế priority:

  • Emergency Stop (0): Cao nhất vì liên quan đến an toàn, phải xử lý ngay lập tức
  • ADC Current (1): Cao để phát hiện overcurrent kịp thời, bảo vệ động cơ
  • PWM Timer (2): Cần cập nhật PWM đều đặn để điều khiển mượt mà
  • Encoder (3): Đọc tốc độ quan trọng nhưng có thể chậm hơn PWM một chút
  • CAN bus (4): Communication có thể chậm hơn control loop
  • UART Debug (5): Thấp nhất vì chỉ dùng để debug, không ảnh hưởng control

>>>>>> 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 😊

Nguyễn Văn Nghĩa

Mình là một người thích học hỏi và chia sẻ các kiến thức về Nhúng IOT.

2 Nhận xét

  1. Hi anh, có thể cho em xin tài liệu về ngắt NVIC mà anh đã học không ạ.

    Trả lờiXóa
    Trả lời
    1. Anh chỉ xem trên tài liệu ARM Cortex M Device Generic User Guide và Technical của ARM là chính thôi e ạ.

      Xóa
Mới hơn Cũ hơn
//
Avatar

Đăng nhập

Người dùng mới? Đăng ký ngay

hoặc

Bằng việc tạo tài khoản, bạn đồng ý với Chính sách bảo mật & Chính sách Cookie.