📘 BÀI 21 – GPIO STM32F103: ĐỌC INPUT, PULL-UP/PULL-DOWN VÀ NÚT NHẤN
Ở Bài 17, chúng ta đã học Memory Map và hiểu rằng GPIO thực chất là các thanh ghi nằm trong vùng địa chỉ Peripheral.
Ở Bài 18–20, ta đã bắt đầu đi vào GPIO.
Hôm nay chúng ta nối các kiến thức đó thành một bài rất quan trọng:
STM32 làm thế nào để đọc trạng thái chân GPIO?
Đây chính là nền tảng để sau này đọc nút nhấn, công tắc, cảm biến, encoder, relay feedback, fingerprint sensor…
1. GPIO có hai nhiệm vụ lớn
Một chân GPIO có thể được dùng theo hai hướng:
STM32 ────> GPIO ────> THIẾT BỊ
OUTPUT
hoặc:
THIẾT BỊ ────> GPIO ────> STM32
INPUT
Ví dụ:
OUTPUT
STM32 điều khiển LED:
STM32
PA8 ───────────> LED
STM32 xuất:
0 → LED OFF
1 → LED ON
INPUT
Nút nhấn đưa tín hiệu vào STM32:
Button ─────────> PA0
STM32 đọc:
PA0 = 0
PA0 = 1
2. Điều quan trọng nhất: INPUT không phải là “0 hoặc 1 cố định”
Khi cấu hình một chân là INPUT, STM32 không tự nhiên biết nó phải là 0 hay 1.
Ví dụ:
PA0 = INPUT
Nếu PA0 không được nối với nguồn nào:
?
|
PA0
thì điện áp trên PA0 có thể dao động.
Ta gọi đó là:
Floating input – ngõ vào bị thả nổi
Khi đó chương trình có thể đọc:
0
1
0
0
1
1
0
...
một cách không ổn định.
3. Tại sao INPUT bị Floating?
Hãy tưởng tượng PA0 giống như một cái “tai”:
PA0
|
|
👂
Nếu không có tín hiệu rõ ràng:
5V ───── ?
GND ───── ?
thì nó không biết đang nghe HIGH hay LOW.
Vì vậy ta phải tạo một trạng thái mặc định.
Có hai cách phổ biến:
PULL-UP
PULL-DOWN
4. Pull-Up là gì?
Pull-Up nghĩa là kéo chân INPUT lên mức HIGH.
Ví dụ:
3.3V
|
[R]
|
+──── PA0
Điện trở kéo PA0 lên 3.3V.
Khi không có tác động:
PA0 = HIGH
5. Kết hợp Pull-Up với nút nhấn
Đây là mạch cực kỳ quan trọng.
3.3V
|
Pull-Up
|
+──────── PA0
|
BUTTON
|
GND
Không nhấn
Button hở:
3.3V
|
R
|
+──── PA0
|
X
|
GND
Do Pull-Up:
PA0 = 1
Nhấn nút
Button đóng:
3.3V
|
R
|
+──── PA0
|
BUTTON
|
GND
PA0 bị kéo xuống GND:
PA0 = 0
Vậy:
| Button | PA0 |
|---|---|
| Không nhấn | 1 |
| Nhấn | 0 |
Đây gọi là:
Active LOW
6. Pull-Down thì ngược lại
Mạch:
3.3V
|
BUTTON
|
+──── PA0
|
Pull-Down
|
GND
Không nhấn
PA0 = 0
Nhấn
PA0 = 1
| Button | PA0 |
|---|---|
| Không nhấn | 0 |
| Nhấn | 1 |
Đây là:
Active HIGH
7. STM32F103 có Pull-Up/Pull-Down bên trong
Đây là điều rất tiện.
Ta không nhất thiết phải dùng điện trở ngoài.
STM32F103 có thể cấu hình GPIO input với:
Input Floating
Input Pull-Up
Input Pull-Down
Ví dụ:
PA0 → Input Pull-Up
thì bên trong STM32 sẽ tạo đường kéo lên.
Tư duy đơn giản:
STM32
┌───────────────┐
3.3V ──>│ Pull-Up │
│ │ │
│ +──── PA0 │
└───────────────┘
8. Thanh ghi nào chứa trạng thái INPUT?
Đây là phần rất quan trọng đối với việc học STM32 theo hướng register-level.
GPIO có thanh ghi:
GPIOx_IDR
Trong đó:
IDR = Input Data Register
Ví dụ:
GPIOA_IDR
là thanh ghi chứa trạng thái input của Port A.
9. IDR có 16 bit
STM32F103 có:
PA0
PA1
PA2
...
PA15
Do đó GPIOA_IDR có 16 bit tương ứng.
Bit: 15 14 13 12 ... 3 2 1 0
| | | | | | | |
PA15 PA3 PA2 PA1 PA0
Ví dụ:
GPIOA_IDR = 0b0000000000000101
thì:
PA2 = 1
PA1 = 0
PA0 = 1
10. Đọc PA0 bằng Bit Mask
Đây là lúc kiến thức Bài 16 – BIT, MASK, & quay trở lại.
Ta muốn kiểm tra:
bit 0
Mask:
(1 << 0)
hay:
0x0001
Sau đó:
GPIOA->IDR & (1 << 0)
11. Vì sao dùng &?
Giả sử:
GPIOA->IDR = 0000 0000 0000 0101
Ta muốn kiểm tra PA0:
0000 0000 0000 0101
&
0000 0000 0000 0001
-------------------
0000 0000 0000 0001
Kết quả:
1
→ PA0 đang HIGH.
Nếu:
GPIOA->IDR = 0000 0000 0000 0100
thì:
0000 0000 0000 0100
&
0000 0000 0000 0001
-------------------
0000 0000 0000 0000
Kết quả:
0
→ PA0 đang LOW.
12. Cách viết trong C
Ví dụ:
if (GPIOA->IDR & (1 << 0))
{
// PA0 = HIGH
}
else
{
// PA0 = LOW
}
Có thể viết ngắn hơn:
if (GPIOA->IDR & (1 << 0))
{
// HIGH
}
13. Đọc PA5 thì sao?
PA5 tương ứng:
bit 5
Mask:
(1 << 5)
Đọc:
if (GPIOA->IDR & (1 << 5))
{
// PA5 HIGH
}
14. Đọc PB12?
Tương tự:
if (GPIOB->IDR & (1 << 12))
{
// PB12 HIGH
}
Công thức tổng quát:
GPIOx->IDR & (1 << PIN)
Ví dụ:
GPIOA->IDR & (1 << 0) // PA0
GPIOA->IDR & (1 << 5) // PA5
GPIOB->IDR & (1 << 12) // PB12
GPIOC->IDR & (1 << 13) // PC13
15. Bài thực hành đầu tiên
Ta sẽ dùng:
- STM32 Blue Pill
- PA0 làm INPUT
- PC13 làm OUTPUT
- nút nhấn
- LED onboard PC13
Mục tiêu:
Nhấn nút
↓
PA0 = LOW
↓
STM32 phát hiện
↓
PC13 đổi trạng thái LED
16. Code kiểu Register-Level
Ví dụ cấu hình PA0 Input Pull-Up và PC13 Output.
#include "stm32f103xb.h"
void delay(volatile uint32_t t)
{
while(t--);
}
int main(void)
{
// Enable clock GPIOA và GPIOC
RCC->APB2ENR |= (1 << 2); // IOPAEN
RCC->APB2ENR |= (1 << 4); // IOPCEN
// PA0 = Input Pull-Up
GPIOA->CRL &= ~(0xF << 0);
GPIOA->CRL |= (0x8 << 0);
// PA0 Pull-Up
GPIOA->ODR |= (1 << 0);
// PC13 = Output Push-Pull, 2 MHz
GPIOC->CRH &= ~(0xF << 20);
GPIOC->CRH |= (0x2 << 20);
while (1)
{
if ((GPIOA->IDR & (1 << 0)) == 0)
{
// Button pressed
GPIOC->ODR &= ~(1 << 13);
}
else
{
// Button released
GPIOC->ODR |= (1 << 13);
}
}
}
Lưu ý: LED onboard PC13 trên nhiều Blue Pill là active-low, nên cách bật/tắt LED có thể nhìn “ngược” với mức logic của PC13.
17. Có một chi tiết rất đáng chú ý
Ở đây ta có:
GPIOA->ODR |= (1 << 0);
nhưng PA0 đang là INPUT.
Tại sao lại ghi ODR?
Vì khi cấu hình:
Input Pull-Up / Pull-Down
thì trên STM32F1, bit tương ứng trong ODR được dùng để chọn:
ODR bit = 1 → Pull-Up
ODR bit = 0 → Pull-Down
Đây là đặc điểm rất quan trọng của STM32F1.
18. CRL và CRH
STM32F103 chia cấu hình GPIO thành hai thanh ghi:
GPIOx_CRL
GPIOx_CRH
CRL
Dùng cho:
Pin 0 → Pin 7
CRH
Dùng cho:
Pin 8 → Pin 15
Ví dụ:
PA0 → CRL
PA5 → CRL
PA7 → CRL
PA8 → CRH
PA13 → CRH
PA15 → CRH
19. Mỗi GPIO chiếm 4 bit
Đây là điểm bạn cần nhớ thật kỹ.
Trong CRL:
PA0 → bit 0..3
PA1 → bit 4..7
PA2 → bit 8..11
PA3 → bit 12..15
...
PA7 → bit 28..31
Ví dụ:
PA0
chiếm:
bits 0,1,2,3
nên ta dùng:
GPIOA->CRL &= ~(0xF << 0);
Sau đó:
GPIOA->CRL |= (0x8 << 0);
20. 0x8 nghĩa là gì?
Với GPIOF1:
MODE[1:0]
CNF[1:0]
Mỗi pin có 4 bit:
CNF1 CNF0 MODE1 MODE0
Input Pull-Up/Pull-Down:
1000
tức:
0x8
Vì vậy:
GPIOA->CRL |= (0x8 << 0);
có nghĩa:
Cấu hình PA0 thành Input Pull-Up/Pull-Down.
Sau đó ODR quyết định:
ODR = 1 → Pull-Up
ODR = 0 → Pull-Down
21. Một sơ đồ tư duy cực kỳ quan trọng
Bạn hãy nối các bài đã học lại:
STM32F103
│
▼
GPIO Peripheral
│
┌──────────┴──────────┐
│ │
INPUT OUTPUT
│ │
▼ ▼
IDR ODR
│ │
▼ ▼
đọc chân GPIO xuất chân GPIO
│ │
▼ ▼
Button/Sensor LED/Motor
Và cấu hình:
GPIOx_CRL / GPIOx_CRH
quyết định GPIO hoạt động như thế nào.
22. Đừng nhầm IDR và ODR
Đây là lỗi người mới học STM32 rất hay mắc.
IDR
GPIOA->IDR
→ đọc trạng thái chân
ODR
GPIOA->ODR
→ ghi/xuất dữ liệu ra chân
Tư duy:
THẾ GIỚI BÊN NGOÀI
│
▼
GPIO
│
▼
IDR
│
▼
CPU
Còn:
CPU
│
▼
ODR
│
▼
GPIO
│
▼
THIẾT BỊ
23. Một bài tập rất quan trọng
Không cần chạy code ngay. Hãy tự tính.
Giả sử:
GPIOA->IDR = 0b0000000000100101;
Hãy xác định:
PA0 = ?
PA1 = ?
PA2 = ?
PA5 = ?
Ta có:
bit: 5 4 3 2 1 0
1 0 0 1 0 1
Vậy:
PA0 = 1
PA1 = 0
PA2 = 1
PA5 = 1
24. Bài tập số 2
Cho:
GPIOB->IDR = 0x0800;
Hỏi:
PB11 = ?
PB10 = ?
PB12 = ?
Ta biết:
0x0800 = 1 << 11
Do đó:
PB11 = 1
PB10 = 0
PB12 = 0
25. Bài tập số 3 – cực kỳ quan trọng
Nếu:
if (GPIOA->IDR & (1 << 7))
{
...
}
thì câu lệnh này đang kiểm tra chân nào?
Đáp án:
PA7
Nếu:
GPIOC->IDR & (1 << 13)
thì:
PC13
26. Kiến thức bạn cần thuộc sau Bài 21
Bạn chưa cần thuộc hàng trăm thanh ghi.
Chỉ cần nắm chắc:
GPIOx_CRL
GPIOx_CRH
GPIOx_IDR
GPIOx_ODR
và hiểu:
CRL/CRH → cấu hình GPIO
IDR → đọc GPIO
ODR → xuất GPIO
Đặc biệt:
GPIOx->IDR & (1 << n)
có nghĩa:
Kiểm tra trạng thái chân GPIO số n.
🎯 Bài tập thực hành Bài 21
Với Blue Pill của bạn, hãy thử bài sau:
PA0 → nút nhấn
PC13 → LED onboard
Yêu cầu:
Không nhấn nút → LED OFF
Nhấn nút → LED ON
Sau khi làm được, hãy nâng cấp:
Nhấn lần 1 → LED ON
Nhấn lần 2 → LED OFF
Nhấn lần 3 → LED ON
...
Lúc đó chúng ta sẽ gặp một vấn đề rất thú vị:
Tại sao chỉ cần giữ nút nhấn mà LED có thể bị đổi trạng thái hàng nghìn lần?
Đó chính là button debounce và state detection.
➡️ Đây sẽ là cầu nối rất đẹp sang BÀI 22 – BUTTON DEBOUNCE, EDGE DETECTION: FALLING EDGE, RISING EDGE VÀ CÁCH STM32 “BIẾT” MỘT LẦN NHẤN NÚT.