BÀI 33 – EXTI: GPIO INTERRUPT TRÊN STM32F103
Sau Bài 32 – GPIO Input + Pull-up/Pull-down, bài tiếp theo rất quan trọng là EXTI (External Interrupt).
Đây chính là bước chuyển từ:
CPU liên tục hỏi nút nhấn → CPU được báo ngay khi nút nhấn xảy ra sự kiện.
1. Trước hết: GPIO Input hoạt động như thế nào?
Ở bài 32, chúng ta có thể đọc chân GPIO:
if (GPIOA->IDR & (1 << 0))
{
// PA0 = HIGH
}
else
{
// PA0 = LOW
}
CPU phải liên tục chạy:
while (1)
{
if (button_pressed())
{
// xử lý
}
}
Đây gọi là Polling.
Polling
CPU:
Đọc nút
↓
Chưa nhấn?
↓
Đọc lại
↓
Đọc lại
↓
Đọc lại
↓
Đọc lại
↓
...
Điều này có một nhược điểm lớn:
CPU phải liên tục kiểm tra.
2. Interrupt là gì?
Interrupt có thể hiểu rất đơn giản:
CPU đang làm việc khác, khi một sự kiện xảy ra thì phần cứng “gọi” CPU.
Ví dụ:
CPU đang chạy chương trình chính
│
│
▼
EXTI
▲
│
PA0 thay đổi
│
▼
CPU bị ngắt
│
▼
Chạy hàm xử lý
│
▼
Quay lại chương trình
Ví dụ nút nhấn nối vào PA0.
Khi nhấn nút:
PA0: HIGH → LOW
│
▼
EXTI
│
▼
NVIC
│
▼
CPU IRQ
│
▼
EXTI0_IRQHandler()
Đây là ý tưởng cốt lõi của EXTI.
3. EXTI là viết tắt của gì?
EXTI = External Interrupt/Event Controller
Trên STM32F103, EXTI quản lý các tín hiệu sự kiện bên ngoài.
Các đường EXTI chính:
EXTI0
EXTI1
EXTI2
...
EXTI15
Tức là có 16 line EXTI:
LINE 0
LINE 1
LINE 2
...
LINE 15
Nhưng có một điểm cực kỳ quan trọng:
EXTI0 không có nghĩa là chỉ PA0.
EXTI0 có thể kết nối với:
PA0
PB0
PC0
PD0
PE0
...
Tùy cấu hình AFIO.
4. Đây là kiến trúc rất quan trọng
Ví dụ bạn muốn sử dụng:
PB5
làm nút nhấn.
Bạn có:
PB5
│
▼
GPIOB
│
▼
AFIO
│
▼
EXTI5
│
▼
NVIC
│
▼
CPU
Có thể hình dung:
GPIO
│
▼
AFIO
│
▼
EXTI
│
▼
NVIC
│
▼
CPU
Có 4 khối cần nhớ:
- GPIO
- AFIO
- EXTI
- NVIC
5. EXTI line và GPIO port
Đây là phần rất dễ nhầm.
Ví dụ:
PA0 → EXTI0
PB0 → EXTI0
PC0 → EXTI0
Không thể đồng thời:
PA0 → EXTI0
PB0 → EXTI0
PC0 → EXTI0
theo kiểu tất cả cùng hoạt động độc lập.
Vì:
Mỗi EXTI line tương ứng với một số pin, còn port được chọn thông qua AFIO.
Ví dụ:
EXTI0 ← PA0 hoặc PB0 hoặc PC0...
EXTI1 ← PA1 hoặc PB1 hoặc PC1...
EXTI2 ← PA2 hoặc PB2 hoặc PC2...
Nhưng EXTI0 chỉ có một nguồn GPIO được chọn tại một thời điểm.
6. AFIO làm nhiệm vụ gì?
Trên STM32F103, AFIO chứa các thanh ghi cho phép chọn nguồn GPIO cho EXTI.
Ví dụ:
EXTI5
có thể lấy tín hiệu từ:
PA5
PB5
PC5
PD5
...
Ta dùng AFIO để nói:
“EXTI5, hãy lấy tín hiệu từ PB5.”
Đây là lý do khi học STM32F103 bạn sẽ gặp:
AFIO->EXTICR[]
7. EXTI có thể kích hoạt theo cạnh nào?
Đây là phần rất quan trọng.
EXTI có thể phát hiện:
Rising Edge
LOW → HIGH
Ví dụ:
0
│
│
└──────────┐
│
1
Đây gọi là:
cạnh lên – Rising Edge
Falling Edge
HIGH → LOW
Ví dụ:
1
│
└──────────┐
│
0
Đây gọi là:
cạnh xuống – Falling Edge
Cả hai cạnh
Có thể phát hiện:
LOW → HIGH
và:
HIGH → LOW
8. Thanh ghi điều khiển cạnh
STM32F103 có:
EXTI->RTSR
RTSR:
Rising Trigger Selection Register
Dùng để bật phát hiện cạnh lên.
Và:
EXTI->FTSR
FTSR:
Falling Trigger Selection Register
Dùng để bật phát hiện cạnh xuống.
Ví dụ EXTI0:
EXTI->RTSR |= (1 << 0);
cho phép:
LOW → HIGH
Còn:
EXTI->FTSR |= (1 << 0);
cho phép:
HIGH → LOW
9. Ví dụ nút nhấn Pull-up
Đây là cấu hình rất phổ biến.
Mạch:
3.3V
│
[Pull-up]
│
├──── PA0
│
[BUTTON]
│
GND
Khi chưa nhấn:
PA0 = HIGH
Khi nhấn:
PA0 = LOW
Vậy sự kiện nhấn nút là:
HIGH → LOW
Do đó ta sử dụng:
Falling Edge
tức:
EXTI->FTSR
10. Ví dụ thực tế
Giả sử:
PA0 = Button
PC13 = LED
Mục tiêu:
Mỗi lần nhấn nút PA0, LED PC13 đổi trạng thái.
Ban đầu:
LED OFF
Nhấn nút:
LED ON
Nhấn lần nữa:
LED OFF
Nhấn lần nữa:
LED ON
11. Các bước cấu hình EXTI
Đây là thứ bạn nên học thuộc theo trình tự.
Bước 1 – Bật clock GPIO
Ví dụ PA0:
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
Bước 2 – Cấu hình PA0 Input
Ví dụ Input Pull-up:
GPIOA->CRL &= ~(0xF << 0);
GPIOA->CRL |= (0x8 << 0);
Sau đó:
GPIOA->ODR |= (1 << 0);
Bước 3 – Bật clock AFIO
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
Đây là bước nhiều người mới học thường quên.
12. Chọn PA0 cho EXTI0
Với PA0, cấu hình AFIO tương ứng là mặc định.
Vì:
PA0 → EXTI0
nên không cần chuyển sang port khác.
Nhưng nếu sử dụng:
PB0
thì phải cấu hình:
PB0 → EXTI0
thông qua:
AFIO->EXTICR[0]
13. Bật EXTI0
Thanh ghi:
EXTI->IMR
là:
Interrupt Mask Register
Muốn EXTI0 được phép tạo interrupt:
EXTI->IMR |= (1 << 0);
Hiểu đơn giản:
IMR bit 0 = 1
↓
Cho phép EXTI0
14. Chọn Falling Edge
Vì nút Pull-up:
HIGH → LOW
nên:
EXTI->FTSR |= (1 << 0);
Không cần Rising:
EXTI->RTSR &= ~(1 << 0);
15. Bật NVIC
Đây là tầng cuối.
EXTI tạo interrupt nhưng CPU còn phải được NVIC cho phép.
Với EXTI0:
NVIC_EnableIRQ(EXTI0_IRQn);
Trong STM32F103, các interrupt EXTI được ánh xạ vào NVIC.
Một số nhóm:
EXTI0
EXTI1
EXTI2
EXTI3
EXTI4
EXTI9_5
EXTI15_10
16. Tại sao EXTI5 đến EXTI9 dùng chung IRQ?
Ví dụ:
PA5
PA6
PA7
PA8
PA9
đều có thể tạo:
EXTI5
EXTI6
EXTI7
EXTI8
EXTI9
nhưng chúng dùng chung:
EXTI9_5_IRQn
Tương tự:
EXTI10
EXTI11
EXTI12
EXTI13
EXTI14
EXTI15
dùng chung:
EXTI15_10_IRQn
Đây là kiến trúc phần cứng của STM32F103.
17. ISR là gì?
Khi interrupt xảy ra, CPU sẽ chạy một hàm đặc biệt.
Ví dụ EXTI0:
void EXTI0_IRQHandler(void)
{
// xử lý interrupt
}
ISR:
Interrupt Service Routine
Có thể hiểu:
“Hàm phục vụ ngắt.”
18. Nhưng có một vấn đề cực kỳ quan trọng
Khi interrupt xảy ra:
EXTI
↓
CPU
↓
EXTI0_IRQHandler()
Trong EXTI còn có thanh ghi:
EXTI->PR
Pending Register.
Nó cho biết:
EXTI nào đang chờ được xử lý.
19. Phải xóa cờ Pending
Ví dụ EXTI0:
EXTI->PR = (1 << 0);
Sau khi xử lý interrupt, phải xóa cờ.
Nếu không:
Interrupt
↓
PR vẫn còn
↓
CPU tiếp tục vào ISR
↓
ISR tiếp tục chạy
↓
...
Đây là lỗi rất quan trọng.
20. Chương trình hoàn chỉnh tối giản
Ví dụ:
#include "stm32f10x.h"
void GPIO_Init(void)
{
/* Enable GPIOA + GPIOC + AFIO */
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
/* PA0 = Input Pull-up */
GPIOA->CRL &= ~(0xF << 0);
GPIOA->CRL |= (0x8 << 0);
GPIOA->ODR |= (1 << 0);
/* PC13 = Output Push-Pull 2 MHz */
GPIOC->CRH &= ~(0xF << 20);
GPIOC->CRH |= (0x2 << 20);
}
void EXTI_Init(void)
{
/* PA0 -> EXTI0 */
/* Unmask EXTI0 */
EXTI->IMR |= (1 << 0);
/* Falling edge */
EXTI->FTSR |= (1 << 0);
/* Enable EXTI0 interrupt */
NVIC_EnableIRQ(EXTI0_IRQn);
}
void EXTI0_IRQHandler(void)
{
if (EXTI->PR & (1 << 0))
{
GPIOC->ODR ^= (1 << 13);
EXTI->PR = (1 << 0);
}
}
int main(void)
{
GPIO_Init();
EXTI_Init();
while (1)
{
}
}
Điểm thú vị là:
while (1)
{
}
không làm gì cả.
CPU không cần liên tục hỏi:
"Nút nhấn chưa?"
"Nút nhấn chưa?"
"Nút nhấn chưa?"
Khi nhấn nút:
PA0
↓
EXTI0
↓
NVIC
↓
CPU
↓
EXTI0_IRQHandler()
↓
LED đảo trạng thái
Sau đó CPU quay lại:
while(1)
{
}
21. So sánh Polling và Interrupt
| Polling | Interrupt |
|---|---|
| CPU liên tục đọc GPIO | GPIO báo CPU khi có sự kiện |
| Tốn thời gian CPU | Hiệu quả hơn |
| Dễ học | Khó hơn |
| Chương trình đơn giản | Cần NVIC + EXTI |
| Có thể bỏ lỡ sự kiện nhanh | Phù hợp sự kiện bất ngờ |
| Tốt cho bài đơn giản | Tốt cho hệ thống thực tế |
22. Một điều cực kỳ quan trọng: chống dội phím
Bạn có thể nghĩ:
“Vậy cứ nhấn nút → interrupt → đổi LED là xong.”
Chưa chắc.
Nút cơ khí bị:
bounce – dội phím.
Khi nhấn một lần, tín hiệu thực tế có thể:
HIGH
↓
LOW
↑
LOW
↑
LOW
↑
LOW
↓
LOW ổn định
Thay vì:
HIGH → LOW
nó có thể tạo nhiều cạnh.
Khi đó:
1 lần nhấn
↓
3 interrupt
↓
5 interrupt
↓
LED ON
↓
LED OFF
↓
LED ON
Bạn sẽ thấy LED hoạt động “điên loạn”.
Đây chính là lý do debounce sẽ là một phần rất quan trọng khi học EXTI thực tế.
23. Một nguyên tắc rất quan trọng khi viết ISR
Trong:
void EXTI0_IRQHandler(void)
{
}
không nên làm những việc quá nặng.
Ví dụ không nên:
delay_ms(1000);
hoặc:
while(...)
{
}
hoặc thực hiện một thuật toán cực kỳ dài.
ISR nên:
Nhanh
↓
Xử lý sự kiện
↓
Đặt cờ
↓
Thoát
Ví dụ tốt hơn:
volatile uint8_t button_event = 0;
void EXTI0_IRQHandler(void)
{
if (EXTI->PR & (1 << 0))
{
button_event = 1;
EXTI->PR = (1 << 0);
}
}
Sau đó main() xử lý:
while (1)
{
if (button_event)
{
button_event = 0;
// xử lý việc chính
}
}
Đây là một tư duy lập trình STM32 rất quan trọng.
24. Tại sao biến phải là volatile?
Ví dụ:
volatile uint8_t button_event;
Biến này được thay đổi trong:
EXTI0_IRQHandler()
nhưng được đọc trong:
main()
Compiler cần biết rằng:
Giá trị biến này có thể thay đổi bất ngờ bởi phần cứng/interrupt.
Vì vậy dùng:
volatile
Đây là một trong những trường hợp quan trọng nhất để hiểu volatile trong lập trình vi điều khiển.
25. Hãy nhớ sơ đồ này
Đây là sơ đồ bạn nên ghi nhớ sau Bài 33:
GPIO INPUT
│
▼
GPIO pin
│
▼
AFIO
│
▼
EXTI
┌────────┴────────┐
│ │
Rising Edge Falling Edge
│ │
└────────┬────────┘
▼
NVIC
│
▼
CPU
│
▼
EXTIx_IRQHandler()
│
▼
Xóa Pending Flag
│
▼
Quay lại main()
Nếu bạn hiểu được sơ đồ này thì bạn đã thực sự bắt đầu bước vào lập trình sự kiện trên STM32, chứ không chỉ đơn thuần điều khiển GPIO.
26. Bài thực hành cho bạn
Với Blue Pill STM32F103C8T6, hãy làm mạch:
PA0 ─── BUTTON ─── GND
│
└── Internal Pull-up
PC13 ─── LED onboard
Yêu cầu:
Bài tập 1
Nhấn nút PA0:
PC13 đảo trạng thái
Bài tập 2
Không sử dụng polling:
while(1)
{
}
Bài tập 3
Sử dụng:
EXTI0_IRQHandler()
Bài tập 4
Thử thay đổi:
EXTI->FTSR
thành:
EXTI->RTSR
và quan sát sự khác nhau.
Bài tập 5 – quan trọng
Thay vì đảo LED trực tiếp trong ISR:
GPIOC->ODR ^= (1 << 13);
hãy tạo:
volatile uint8_t button_event;
ISR chỉ đặt:
button_event = 1;
và main() xử lý LED.
Sau Bài 33, chúng ta sẽ học gì?
Tôi đề xuất đi theo chuỗi này:
Bài 32
GPIO Input + Pull-up/Pull-down
↓
Bài 33
EXTI – GPIO Interrupt
↓
Bài 34
NVIC – Nested Vectored Interrupt Controller
↓
Bài 35
Debounce nút nhấn bằng Timer
↓
Bài 36
EXTI + nhiều nút nhấn
↓
Bài 37
Interrupt + Timer
↓
Bài 38
Interrupt + UART
↓
Bài 39
Thiết kế driver GPIO/EXTI hoàn chỉnh
Đặc biệt, Bài 34 về NVIC rất đáng học kỹ, vì lúc đó bạn sẽ hiểu rõ đường đi:
GPIO
→ EXTI
→ Pending
→ NVIC
→ Vector Table
→ ISR
→ CPU
và bắt đầu hiểu bên trong STM32 thực sự xử lý một interrupt như thế nào.