BÀI 23 – GPIO STM32F103: ĐỌC NÚT NHẤN, PULL-UP, PULL-DOWN VÀ DEBOUNCE
Ở Bài 22, chúng ta đã đi từ việc ghi dữ liệu vào GPIO Output đến việc điều khiển chân GPIO bằng thanh ghi.
Bài 23 sẽ làm điều ngược lại:
STM32 đọc trạng thái điện áp bên ngoài thông qua GPIO Input.
Đây là bài cực kỳ quan trọng, vì sau bài này bạn có thể bắt đầu làm:
- nút nhấn;
- công tắc;
- cảm biến;
- đọc tín hiệu từ module;
- đọc trạng thái chân GPIO;
- điều khiển LED bằng nút nhấn;
- sau này tiến tới UART, SPI, I²C, encoder…
1. GPIO Input là gì?
Ví dụ ta có:
STM32F103
+---------+
| |
| PA0|-------- Nút nhấn
| |
+---------+
Khi cấu hình PA0 là Input, STM32 không chủ động xuất HIGH hoặc LOW.
Thay vào đó, STM32 quan sát điện áp trên PA0.
Có thể hiểu:
GPIO INPUT
│
▼
điện áp tại chân
│
├── gần 0V → 0
│
└── gần VDD → 1
Ví dụ STM32 chạy 3.3 V:
PA0 = 0V → GPIO đọc 0
PA0 = 3.3V → GPIO đọc 1
2. Nhưng có một vấn đề rất quan trọng
Hãy tưởng tượng PA0 không nối với bất kỳ thứ gì:
PA0 ───────────── không nối
Bạn có thể nghĩ:
“Không nối thì chắc chắn là LOW.”
Không đúng.
Chân Input lúc này có thể bị floating.
Điện áp có thể dao động:
0
1
0
1
1
0
0
1
...
và kết quả đọc có thể không ổn định.
Đây là lý do chúng ta cần:
Pull-up hoặc Pull-down.
3. Pull-up là gì?
Pull-up nghĩa là dùng một điện trở kéo chân GPIO về VCC.
3.3V
│
[R]
│
├──── PA0
│
BUTTON
│
GND
Khi không nhấn nút:
3.3V
│
R
│
PA0
PA0 được kéo lên HIGH.
Vì vậy:
Không nhấn → PA0 = 1
Khi nhấn nút:
3.3V
│
R
│
PA0
│
BUTTON
│
GND
PA0 bị nối xuống GND:
Nhấn → PA0 = 0
Do đó:
| Nút | PA0 |
|---|---|
| Không nhấn | 1 |
| Nhấn | 0 |
Đây gọi là:
Active LOW
4. Pull-down thì ngược lại
Mạch:
3.3V
│
BUTTON
│
├──── PA0
│
[R]
│
GND
Không nhấn:
PA0 = 0
Nhấn:
PA0 = 1
Bảng:
| Nút | PA0 |
|---|---|
| Không nhấn | 0 |
| Nhấn | 1 |
Đây là:
Active HIGH
5. STM32F103 có Pull-up/Pull-down bên trong
Đây là một điểm rất hay.
Bạn không nhất thiết lúc nào cũng phải dùng điện trở bên ngoài.
STM32F103 có thể cấu hình:
GPIO Input Floating
GPIO Input Pull-up
GPIO Input Pull-down
Về ý tưởng:
VDD
│
Pull-up
│
├──── GPIO
│
Pull-down
│
GND
Bạn có thể bật một trong hai.
6. Thanh ghi nào quyết định GPIO Input?
Đây là lúc kiến thức Bài 16 → Bài 17 → Bài 18 → Bài 22 bắt đầu kết nối với nhau.
Với STM32F103, mỗi GPIO port có:
CRL
CRH
IDR
ODR
BSRR
BRR
LCKR
Trong đó hôm nay chúng ta đặc biệt quan tâm:
IDR
7. IDR – Input Data Register
IDR có tên:
Input Data Register
Nó chứa trạng thái logic mà STM32 đang đọc trên các chân GPIO.
Ví dụ:
GPIOA->IDR
có 16 bit:
bit15 bit14 ... bit2 bit1 bit0
Tương ứng:
PA15 PA14 ... PA2 PA1 PA0
Ví dụ:
GPIOA->IDR
│
└── bit 0 = trạng thái PA0
8. Đọc PA0
Ta có thể kiểm tra:
if (GPIOA->IDR & (1 << 0))
{
// PA0 = HIGH
}
Tại sao?
Ta có:
GPIOA->IDR
giả sử:
xxxxxxxxxxxxxxx1
AND với:
0000000000000001
kết quả:
0000000000000001
→ PA0 = HIGH.
9. Nếu PA0 = LOW?
Ví dụ:
GPIOA->IDR
xxxxxxxxxxxxxxx0
AND:
0000000000000001
sẽ cho:
0000000000000000
Do đó:
if (GPIOA->IDR & (1 << 0))
{
// HIGH
}
else
{
// LOW
}
10. Cách viết dễ hiểu hơn
Ta có thể tạo macro:
#define PA0_READ() ((GPIOA->IDR >> 0) & 1)
Sau đó:
if (PA0_READ())
{
// HIGH
}
hoặc:
if (PA0_READ() == 0)
{
// LOW
}
Đây chính là kỹ thuật:
đọc một bit trong một thanh ghi nhiều bit.
11. Ví dụ thực tế: nút nhấn + LED
Giả sử:
PA0 = BUTTON
PC13 = LED
Mạch:
3.3V
│
Pull-up
│
├──── PA0
│
BUTTON
│
GND
STM32
PC13 ───── LED
Logic:
Không nhấn:
PA0 = 1
LED OFF
Nhấn:
PA0 = 0
LED ON
Code ý tưởng:
while (1)
{
if ((GPIOA->IDR & (1 << 0)) == 0)
{
GPIOC->BSRR = (1 << 13);
}
else
{
GPIOC->BRR = (1 << 13);
}
}
Lưu ý: LED trên Blue Pill thường mắc theo kiểu active LOW, nên khi thực hành với LED PC13 bạn có thể phải đảo logic.
12. Một điều cực kỳ quan trọng: Input không có nghĩa là IDR luôn đúng 0/1 theo ý bạn
Có một chuỗi rất quan trọng:
Điện áp bên ngoài
↓
mạch input của GPIO
↓
ngưỡng logic
↓
IDR
↓
CPU đọc
Ví dụ:
0V → LOW
3.3V → HIGH
Nhưng vùng điện áp giữa hai mức logic không nên được xem đơn giản là:
0V → 0
1V → 1
2V → 1
3V → 1
Mà datasheet quy định các ngưỡng điện áp cụ thể.
Điều này rất quan trọng khi bạn kết nối:
- cảm biến;
- module 5 V;
- Arduino;
- ESP32;
- STM32 khác;
- transistor;
- comparator.
13. Debounce – vấn đề rất nổi tiếng của nút nhấn
Bạn nhấn nút một lần.
Bạn nghĩ STM32 sẽ thấy:
1
↓
0
Nhưng thực tế tiếp điểm cơ khí có thể rung:
1
0
1
0
1
0
0
0
trong vài ms.
STM32 chạy rất nhanh.
Ví dụ CPU chạy:
72 MHz
thì trong 1 ms CPU đã thực hiện được rất nhiều chu kỳ.
Vì vậy STM32 có thể nhìn thấy:
nhấn 1 lần
thành:
nhấn
nhả
nhấn
nhả
nhấn
14. Đây gọi là Contact Bounce
Hình dung:
Thực tế:
HIGH ─────┐
│
└─┐ ┌─┐ ┌──────
│ │ │
LOW └─┘ └─┘
Thay vì:
HIGH ──────────┐
│
└────────── LOW
Đây là lý do một nút nhấn đôi khi khiến chương trình:
“Một lần nhấn nhưng tăng biến 5 lần.”
15. Cách đơn giản nhất để debounce
Một phương pháp rất cơ bản:
if (button_pressed)
{
delay_ms(20);
if (button_pressed)
{
// xác nhận đã nhấn
}
}
Tư duy:
Phát hiện nhấn
↓
chờ 20 ms
↓
đọc lại
↓
vẫn nhấn?
↓
YES → xác nhận
Đây là debounce bằng thời gian.
Sau này chúng ta sẽ học cách làm debounce không dùng delay, tốt hơn cho hệ thống thực tế.
16. Tại sao không nên lạm dụng delay?
Ví dụ:
while (1)
{
if (button_pressed)
{
delay_ms(20);
}
read_sensor();
update_display();
control_motor();
}
Trong thời gian:
delay_ms(20);
CPU đang bị chặn.
Nếu hệ thống có:
- LCD;
- motor;
- UART;
- timer;
- cảm biến;
- nhiều nút;
thì delay sẽ trở thành vấn đề.
Sau này ta sẽ dùng:
SysTick
Timer
Interrupt
State Machine
để xử lý tốt hơn.
17. Một kiến thức rất quan trọng: GPIO Input và Output khác nhau ở đâu?
Hãy so sánh.
Output
STM32:
CPU
↓
ODR/BSRR
↓
GPIO
↓
điện áp OUT
Tức là:
STM32 điều khiển điện áp chân.
Input
điện áp bên ngoài
↓
GPIO
↓
IDR
↓
CPU
Tức là:
STM32 quan sát điện áp chân.
Đây là hai hướng dữ liệu ngược nhau.
18. Nhìn GPIO bằng tư duy thanh ghi
Đây là điều tôi muốn bạn đặc biệt ghi nhớ.
Khi gặp GPIO:
OUTPUT
hãy nghĩ:
ODR
BSRR
BRR
Khi gặp:
INPUT
hãy nghĩ:
IDR
Khi cần:
CONFIGURATION
hãy nghĩ:
CRL
CRH
Có thể tóm tắt:
GPIO
│
┌──────┼──────┐
│ │ │
Config Input Output
│ │ │
CRL/H IDR ODR/BSRR/BRR
19. Bài tập thực hành trên Blue Pill
Với Blue Pill của bạn, hãy thử bài sau.
Phần cứng
STM32 Blue Pill
│
├── PC13 → LED onboard
│
└── PA0 → nút nhấn
Bạn có thể sử dụng pull-up nội bộ để giảm linh kiện.
Mục tiêu:
Không nhấn → LED OFF
Nhấn PA0 → LED ON
Sau đó đảo lại:
Không nhấn → LED ON
Nhấn PA0 → LED OFF
20. Bài tập nâng cấp
Sau khi làm được nút nhấn, hãy tạo biến:
int counter = 0;
Mỗi lần nhấn nút:
counter++
Sau đó gửi:
Counter = 1
Counter = 2
Counter = 3
...
qua UART.
Nhưng có một yêu cầu:
Nhấn một lần chỉ được tăng đúng một lần.
Đây là lúc bạn sẽ bắt đầu hiểu thật sự về:
Button
↓
Debounce
↓
Edge detection
↓
State machine
21. Một khái niệm mới: Level và Edge
Đây là kiến thức rất quan trọng trước khi sang các bài nâng cao.
Level
Kiểm tra trạng thái hiện tại:
if (button == 0)
{
// đang nhấn
}
Nó trả lời:
“Hiện tại nút có đang nhấn không?”
Edge
Phát hiện sự thay đổi:
1 → 0
hoặc:
0 → 1
Nó trả lời:
“Có vừa xảy ra một lần nhấn không?”
Hai khái niệm này hoàn toàn khác nhau.
22. Tư duy cực kỳ quan trọng
Hãy tưởng tượng chương trình chạy:
Loop 1: button = 1
Loop 2: button = 1
Loop 3: button = 0
Loop 4: button = 0
Loop 5: button = 0
Loop 6: button = 1
Nếu bạn viết:
if (button == 0)
{
counter++;
}
thì:
Loop 3 → counter++
Loop 4 → counter++
Loop 5 → counter++
Một lần nhấn có thể tăng 3 lần.
Nhưng nếu ta phát hiện cạnh:
1 → 0
thì chỉ:
Loop 3 → counter++
Đây chính là nền tảng của button event.
23. Bạn cần ghi nhớ 6 thứ sau Bài 23
① GPIO Input
GPIO chuyển sang chế độ:
quan sát tín hiệu bên ngoài
② IDR
GPIOA->IDR
dùng để đọc trạng thái chân.
③ Pull-up
Không nhấn → HIGH
Nhấn → LOW
④ Pull-down
Không nhấn → LOW
Nhấn → HIGH
⑤ Debounce
Nút cơ khí có thể rung:
1 → 0 → 1 → 0 → 1
nên cần xử lý chống dội.
⑥ Level ≠ Edge
Level = đang ở trạng thái nào?
Edge = vừa thay đổi trạng thái?
24. Sơ đồ tư duy của toàn bộ Bài 23
GPIO INPUT
│
┌───────────┴───────────┐
│ │
Điện áp IDR
│ │
┌─────┴─────┐ │
│ │ │
HIGH LOW │
│ │ │
└─────┬─────┘ │
│ │
Button CPU đọc
│
┌────┴────┐
│ │
Pull-up Pull-down
│ │
Active LOW Active HIGH
│
Debounce
│
↓
Edge Detection
│
↓
Button Event
🎯 Bài tiếp theo
BÀI 24 chúng ta sẽ đi sâu vào một vấn đề rất quan trọng:
GPIO STM32F103: CRL/CRH thực sự cấu hình Input và Output như thế nào?
Lúc đó chúng ta sẽ không dùng HAL, mà tự tay thao tác các bit trong:
GPIOA->CRL
GPIOA->CRH
và bạn sẽ thấy chính xác vì sao một chân STM32 có thể trở thành:
Input Floating
Input Pull-up
Input Pull-down
Output Push-Pull
Output Open-Drain
Alternate Function
Đây sẽ là bài nối trực tiếp từ Bài 23 → hiểu cấu trúc GPIO của STM32F103 ở mức thanh ghi.