Ngắt trong STM32: EXTI, Timer, UART Interrupt chi tiết

Ngắt (Interrupt) là một trong những cơ chế quan trọng nhất trong lập trình vi điều khiển STM32. Thay vì để CPU liên tục “hỏi thăm” (polling) trạng thái các ngoại vi, ngắt cho phép phần cứng chủ động báo cho CPU biết khi có sự kiện xảy ra – giúp chương trình phản hồi tức thì và tiết kiệm tài nguyên. Bài viết này trình bày ba loại ngắt phổ biến nhất trong STM32F1: ngắt ngoài (EXTI)ngắt Timer và ngắt truyền thông (UART), kèm cách cấu hình chi tiết bằng thư viện SPL.

Ngắt ngoài (External Interrupt – EXTI)

External Interrupt (EXTI) hay còn gọi là ngắt ngoài là một sự kiện ngắt xảy ra khi có tín hiệu can thiệp từ bên ngoài – từ phần cứng, từ người sử dụng hoặc từ ngoại vi khác. Khi mức tín hiệu trên chân được cấu hình thay đổi (cạnh lên hoặc cạnh xuống), EXTI sẽ tạo ra một yêu cầu ngắt gửi tới CPU.

Hình 1. Sơ đồ khối bộ điều khiển ngắt/sự kiện ngoài (EXTI) – tín hiệu từ Input Line qua khối phát hiện cạnh, các thanh ghi mask/trigger rồi tới NVIC

16 line ngắt ngoài của STM32F103

Chip STM32F103C8 có 16 line ngắt ngoài riêng biệt (EXTI0 → EXTI15). Mỗi line được chia sẻ chung cho tất cả các chân cùng số thứ tự trên mọi Port:

  • Line0 dùng chung cho tất cả chân Px0 (PA0, PB0, PC0…). Nếu đã chọn PA0 làm chân ngắt thì các chân số 0 ở Port khác không được khai báo làm ngắt ngoài nữa.
  • Line1 nếu chọn PB1 làm chân ngắt thì các chân số 1 ở Port khác không được dùng làm ngắt nữa.
  • Quy tắc tương tự áp dụng cho toàn bộ Line0 → Line15.
Hình 2. Mỗi line EXTIx nhận được từ một chân Pxx duy nhất – ví dụ EXTI0 chọn 1 trong PA0/PB0/PC0… qua thanh ghi AFIO_EXTICR

Phân bố Line ngắt vào Vector ngắt

Sau đó các Line ngắt được phân vào các vector ngắt tương ứng. Các Line0 → Line4 có vector riêng biệt, còn các line còn lại được gộp chung:

Line ngắtVector ngắt (IRQ Handler)Địa chỉ
Line0EXTI0_IRQn0x0000_0058
Line1EXTI1_IRQn0x0000_005C
Line2EXTI2_IRQn0x0000_0060
Line3EXTI3_IRQn0x0000_0064
Line4EXTI4_IRQn0x0000_0068
Line5 → Line9EXTI9_5_IRQn (dùng chung)0x0000_009C
Line10 → Line15EXTI15_10_IRQn (dùng chung)0x0000_00E0

Mức độ ưu tiên ngắt NVIC

Trên MCU STM32F103C8T6 có hai loại ưu tiên ngắtPreemption Priority (ưu tiên chiếm quyền) và Sub Priority (ưu tiên phụ). Nguyên tắc tranh chấp giữa các ngắt:

  • Mặc định, ngắt nào có Preemption Priority cao hơn sẽ được thực hiện trước (và có thể chen ngang ngắt đang chạy).
  • Khi hai ngắt cùng mức Preemption Priority, ngắt nào có Sub Priority cao hơn được thực hiện trước.
  • Khi hai ngắt cùng cả Preemption và Sub Priority, ngắt nào đến trước được thực hiện trước.

Hàm NVIC_PriorityGroupConfig() quyết định số bit chia cho Preemption và Sub Priority:

NVIC_PriorityGroupPreemption PrioritySub PriorityÝ nghĩa
NVIC_PriorityGroup_000–150 bit pre-emption, 4 bit subpriority
NVIC_PriorityGroup_10–10–71 bit pre-emption, 3 bit subpriority
NVIC_PriorityGroup_20–30–32 bit pre-emption, 2 bit subpriority
NVIC_PriorityGroup_30–70–13 bit pre-emption, 1 bit subpriority
NVIC_PriorityGroup_40–1504 bit pre-emption, 0 bit subpriority

Cấu hình ngắt ngoài

Bước 1: Cấu hình GPIO

Chân ngắt ngoài được cấu hình ở chế độ Input. Chọn Pull-Up hay Pull-Down tùy thuộc vào cạnh ngắt mong muốn (thường Pull-Up khi bắt cạnh xuống với nút nhấn nối GND).

/* Cấu hình PA0 làm Input cho ngắt ngoài EXTI0 */
GPIO_InitTypeDef GPIO_InitStruct;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

GPIO_InitStruct.GPIO_Pin  = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;   /* Input Pull-Up */
GPIO_Init(GPIOA, &GPIO_InitStruct);

Bước 2: Cấu hình NVIC

Bộ NVIC (Nested Vectored Interrupt Controller) cấu hình các tham số ngắt và quản lý các vector ngắt. Các tham số nằm trong struct NVIC_InitTypeDef:

Tham sốMô tả
NVIC_IRQChannelChọn line ngắt, enable line tương ứng với ngắt sử dụng
NVIC_IRQChannelPreemptionPriorityCấu hình độ ưu tiên chính (preemption) của ngắt
NVIC_IRQChannelSubPriorityCấu hình độ ưu tiên phụ (sub priority)
NVIC_IRQChannelCmdCho phép (ENABLE) ngắt
/* Cấu hình NVIC cho EXTI0 */
NVIC_InitTypeDef NVIC_InitStruct;

/* Chọn nhóm ưu tiên: 2 bit preemption, 2 bit sub */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

NVIC_InitStruct.NVIC_IRQChannel                   = EXTI0_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority        = 1;
NVIC_InitStruct.NVIC_IRQChannelCmd                = ENABLE;
NVIC_Init(&NVIC_InitStruct);

Bước 3: Cấu hình EXTI

Đầu tiên, để sử dụng GPIO như ngắt ngoài, cần cấp clock cho ngoại vi AFIO:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

Tiếp theo dùng hàm GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource) để cấu hình chân sử dụng ngắt ngoài:

  • GPIO_PortSource: Chọn Port làm nguồn cho ngắt ngoài.
  • GPIO_PinSource: Chọn Pin để cấu hình.

Các tham số ngắt ngoài được cấu hình trong struct EXTI_InitTypeDef:

Tham sốMô tả
EXTI_LineChọn line ngắt (EXTI_Line0 → EXTI_Line15)
EXTI_ModeChọn chế độ: Interrupt hay Event
EXTI_TriggerCấu hình cạnh ngắt: Rising, Falling hay cả hai
EXTI_LineCmdCho phép ngắt ở Line đã cấu hình
/* Cấu hình EXTI0 trên chân PA0, ngắt theo cạnh xuống */
EXTI_InitTypeDef EXTI_InitStruct;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

/* Nối line EXTI0 với chân PA0 */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);

EXTI_InitStruct.EXTI_Line    = EXTI_Line0;
EXTI_InitStruct.EXTI_Mode    = EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct);

Hàm phục vụ ngắt ngoài (ISR)

Mỗi line ngắt có hàm phục vụ riêng, được đăng ký với tên cố địnhEXTIx_IRQHandler() (với x là line ngắt tương ứng). Hàm này được gọi tự động khi có ngắt xảy ra trên line đó.

  • EXTI_GetITStatus(EXTI_Linex): Kiểm tra cờ ngắt của line x. Chỉ khi đúng ngắt từ line x mới thực hiện các lệnh tiếp theo.
  • EXTI_ClearITPendingBit(EXTI_Linex): Xóa cờ ngắt ở line x.

Trong hàm phục vụ ngắt ngoài, ta thực hiện tuần tự: (1) kiểm tra ngắt đến từ line nào; (2) thực hiện các lệnh/hàm cần thiết; (3) xóa cờ ngắt ở line.

void EXTI0_IRQHandler(void)
{
    /* Kiểm tra đúng ngắt từ Line0 */
    if (EXTI_GetITStatus(EXTI_Line0) != RESET)
    {
        /* --- Thực hiện tác vụ khi có ngắt --- */
        GPIO_WriteBit(GPIOC, GPIO_Pin_13,
                      (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13)));

        /* Xóa cờ ngắt Line0 */
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

Ngắt Timer

Ngắt Timer cho phép tạo ra các sự kiện ngắt theo chu kỳ thời gian chính xác – rất hữu ích để tạo delay không chặn (non-blocking), lấy mẫu định kỳ hay tạo tick hệ thống.

Cấu hình Timer

Khi sử dụng ngắt Timer, ta vẫn cấu hình các tham số trong TIM_TimeBaseInitTypeDef như bình thường. Riêng TIM_Period là số chu kỳ mà Timer sẽ ngắt – tính toán và đặt giá trị để tạo khoảng thời gian ngắt mong muốn. Sau đó dùng hàm TIM_ITConfig(TIMx, TIM_IT_Update, ENABLE) để kích hoạt ngắt cho TIMERx.

/* Cấu hình TIM2 ngắt mỗi 1ms (giả sử clock 72MHz) */
TIM_TimeBaseInitTypeDef TIM_InitStruct;

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

TIM_InitStruct.TIM_Prescaler         = 7200 - 1;   /* 72MHz / 7200 = 10kHz */
TIM_InitStruct.TIM_Period            = 10 - 1;     /* 10 tick = 1ms */
TIM_InitStruct.TIM_CounterMode       = TIM_CounterMode_Up;
TIM_InitStruct.TIM_ClockDivision     = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM2, &TIM_InitStruct);

/* Kích hoạt ngắt tràn (Update) cho TIM2 */
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM2, ENABLE);

Cấu hình NVIC

Ở NVIC, ta cấu hình tương tự như ngắt ngoài EXTI, tuy nhiên NVIC_IRQChannel được đổi thành TIMx_IRQn để khớp với line ngắt của Timer.

NVIC_InitTypeDef NVIC_InitStruct;

NVIC_InitStruct.NVIC_IRQChannel                   = TIM2_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelSubPriority        = 1;
NVIC_InitStruct.NVIC_IRQChannelCmd                = ENABLE;
NVIC_Init(&NVIC_InitStruct);

Hàm phục vụ ngắt Timer

Hàm phục vụ ngắt Timer được đặt tên TIMx_IRQHandler() (với x là Timer tương ứng). Bên trong hàm, ta kiểm tra cờ TIM_IT_Update bằng hàm TIM_GetITStatus() – hàm này trả về giá trị kiểm tra xem Timer đã tràn hay chưa. Sau khi thực hiện xong, gọi TIM_ClearITPendingBit(TIMx, TIM_IT_Update) để xóa cờ ngắt.

volatile uint32_t ms_counter = 0;

void TIM2_IRQHandler(void)
{
    /* Kiểm tra cờ tràn Timer */
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
    {
        ms_counter++;   /* Đếm mỗi 1ms */

        /* Xóa cờ ngắt */
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    }
}

Ngắt truyền thông (Communication Interrupt)

STM32F1 hỗ trợ các ngắt cho các giao thức truyền nhận như SPI, I2C, UART… Ở bài này ta lấy ví dụ với UART ngắt nhận; các giao thức còn lại cũng có cách cấu hình tương tự. Ngắt UART đặc biệt hữu ích để nhận dữ liệu bất đồng bộ mà không cần CPU liên tục chờ (polling) thanh ghi.

Cấu hình ngắt UART

Đầu tiên, các cấu hình tham số cho UART thực hiện bình thường theo thứ tự: Cấu hình RCC → Cấu hình GPIO → Cấu hình tham số UART. Trước khi cho phép UART hoạt động, cần kích hoạt cho phép ngắt UART bằng hàm USART_ITConfig(), gồm 3 tham số:

  • USART_TypeDef* USARTx: Bộ UART cần cấu hình.
  • uint16_t USART_IT: Chọn nguồn ngắt UART. Có nhiều nguồn ngắt, ở bài này chú ý đến ngắt truyền (USART_IT_TXE) và ngắt nhận (USART_IT_RXNE).
  • FunctionalState NewState: Cho phép (ENABLE) ngắt.
/* Kích hoạt ngắt nhận cho USART1 */
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
USART_Cmd(USART1, ENABLE);

Cấu hình NVIC

Ở cấu hình NVIC cho UART cũng như các ngoại vi khác, cần chọn line ngắt tương ứng với bộ ngoại vi sử dụng. Ở đây chọn line UART là USARTx_IRQn (với x là bộ UART sử dụng).

NVIC_InitTypeDef NVIC_InitStruct;

NVIC_InitStruct.NVIC_IRQChannel                   = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelSubPriority        = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd                = ENABLE;
NVIC_Init(&NVIC_InitStruct);

Hàm phục vụ ngắt UART

Hàm USARTx_IRQHandler() được gọi khi xảy ra ngắt trên line UART đã cấu hình. Trong hàm phục vụ ngắt, ta kiểm tra ngắt đến là ngắt nhận RXNE hay ngắt truyền TXE bằng hàm USART_GetITStatus(). Tùy theo tín hiệu ngắt mà lập trình tác vụ khác nhau. Sau khi thực thi xong có thể xóa cờ ngắt để đảm bảo không còn ngắt trên line (thông thường cờ RXNE sẽ tự động xóa khi đọc thanh ghi dữ liệu).

void USART1_IRQHandler(void)
{
    /* Ngắt nhận: có byte mới trong thanh ghi dữ liệu */
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        uint8_t data = USART_ReceiveData(USART1);   /* Đọc data → tự xóa cờ RXNE */

        /* --- Xử lý byte nhận được tại đây --- */
        USART_SendData(USART1, data);               /* Ví dụ: echo lại */
    }
}

So sánh ba loại ngắt

Loại ngắtNguồn kích hoạtVector / IRQỨng dụng tiêu biểu
Ngắt ngoài (EXTI)Cạnh tín hiệu trên chân GPIOEXTIx_IRQnNút nhấn, cảm biến tiệm cận, encoder
Ngắt TimerTimer đếm tràn (Update)TIMx_IRQnDelay non-blocking, lấy mẫu định kỳ, PWM
Ngắt truyền thôngSự kiện UART/SPI/I2C (RXNE, TXE…)USARTx_IRQnNhận dữ liệu bất đồng bộ, giao tiếp module

Lưu ý khi sử dụng ngắt trong STM32

  • Luôn xóa cờ ngắt (ClearITPendingBit) trong ISR, nếu không ngắt sẽ được gọi lặp vô hạn.
  • Giữ hàm phục vụ ngắt (ISR) càng ngắn gọn càng tốt – tránh delay, tránh hàm nặng; chỉ set cờ rồi xử lý ở vòng lặp chính.
  • Biến dùng chung giữa ISR và main() nên khai báo volatile để trình biên dịch không tối ưu sai.
  • Cấu hình độ ưu tiên NVIC hợp lý: ngắt quan trọng, thời gian thực (giao tiếp) nên có Preemption Priority cao hơn.
  • Với ngắt ngoài, nhớ cấp clock AFIO trước khi gọi GPIO_EXTILineConfig().
  • Mỗi số chân chỉ dùng được cho một Port tại một thời điểm (Line0 dùng PA0 thì không dùng PB0…).

Kết luận

Ngắt (Interrupt) là nền tảng giúp hệ thống nhúng phản hồi tức thì với các sự kiện mà không lãng phí thời gian CPU cho việc polling. Qua bài viết, bạn đã nắm được ba loại ngắt cốt lõi trong STM32F1: ngắt ngoài EXTI (16 line, phân bố vector, ưu tiên NVIC), ngắt Timer (tạo sự kiện theo chu kỳ) và ngắt truyền thông UART (nhận dữ liệu bất đồng bộ).

Điểm chung của cả ba loại là quy trình cấu hình theo thứ tự: bật clock → cấu hình ngoại vi → cấu hình NVIC → viết hàm phục vụ ngắt và xóa cờ. Nắm vững cơ chế NVIC và độ ưu tiên ngắt sẽ giúp bạn xây dựng các ứng dụng automotive embedded thời gian thực ổn định như đọc nút bấm, giao tiếp cảm biến qua UART, hay tạo lịch tác vụ chính xác bằng Timer.

Leave a Reply