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.

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.

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ắt | Vector ngắt (IRQ Handler) | Địa chỉ |
|---|---|---|
| Line0 | EXTI0_IRQn | 0x0000_0058 |
| Line1 | EXTI1_IRQn | 0x0000_005C |
| Line2 | EXTI2_IRQn | 0x0000_0060 |
| Line3 | EXTI3_IRQn | 0x0000_0064 |
| Line4 | EXTI4_IRQn | 0x0000_0068 |
| Line5 → Line9 | EXTI9_5_IRQn (dùng chung) | 0x0000_009C |
| Line10 → Line15 | EXTI15_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ắt: Preemption 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_PriorityGroup | Preemption Priority | Sub Priority | Ý nghĩa |
|---|---|---|---|
NVIC_PriorityGroup_0 | 0 | 0–15 | 0 bit pre-emption, 4 bit subpriority |
NVIC_PriorityGroup_1 | 0–1 | 0–7 | 1 bit pre-emption, 3 bit subpriority |
NVIC_PriorityGroup_2 | 0–3 | 0–3 | 2 bit pre-emption, 2 bit subpriority |
NVIC_PriorityGroup_3 | 0–7 | 0–1 | 3 bit pre-emption, 1 bit subpriority |
NVIC_PriorityGroup_4 | 0–15 | 0 | 4 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_IRQChannel | Chọn line ngắt, enable line tương ứng với ngắt sử dụng |
NVIC_IRQChannelPreemptionPriority | Cấu hình độ ưu tiên chính (preemption) của ngắt |
NVIC_IRQChannelSubPriority | Cấu hình độ ưu tiên phụ (sub priority) |
NVIC_IRQChannelCmd | Cho 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_Line | Chọn line ngắt (EXTI_Line0 → EXTI_Line15) |
EXTI_Mode | Chọn chế độ: Interrupt hay Event |
EXTI_Trigger | Cấu hình cạnh ngắt: Rising, Falling hay cả hai |
EXTI_LineCmd | Cho 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ố định: EXTIx_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ắt | Nguồn kích hoạt | Vector / IRQ | Ứng dụng tiêu biểu |
|---|---|---|---|
| Ngắt ngoài (EXTI) | Cạnh tín hiệu trên chân GPIO | EXTIx_IRQn | Nút nhấn, cảm biến tiệm cận, encoder |
| Ngắt Timer | Timer đếm tràn (Update) | TIMx_IRQn | Delay non-blocking, lấy mẫu định kỳ, PWM |
| Ngắt truyền thông | Sự kiện UART/SPI/I2C (RXNE, TXE…) | USARTx_IRQn | Nhậ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áovolatileđể 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.

