BÀI 24 – GPIO STM32: ĐỌC NÚT NHẤN VÀ CHỐNG DỘI PHÍM
Ở Bài 23, chúng ta đã học cách đọc trạng thái của một chân GPIO Input.
Bây giờ ta nâng lên một bước rất quan trọng:
Dùng GPIO Input để đọc nút nhấn và điều khiển LED.
Đây là bài cực kỳ quan trọng vì sau này bạn sẽ dùng nguyên lý này cho:
- nút nhấn
- công tắc
- cảm biến ON/OFF
- relay feedback
- encoder
- keypad
- tín hiệu báo lỗi
- tín hiệu từ mạch điều khiển khác
1. Mạch thực tế
Ta làm một mạch rất đơn giản:
STM32F103C8T6
┌──────────────┐
│ │
│ PC13 ├──── LED
│ │
│ PA0 ├─────┐
│ │ │
└──────────────┘ │
│
┌──┴──┐
│ NÚT │
│ NHẤN│
└──┬──┘
│
GND
Ta muốn:
Không nhấn nút → LED OFF
Nhấn nút → LED ON
Nhưng có một vấn đề:
PA0 không thể để hở khi nút không nhấn.
2. Tại sao GPIO Input không được để hở?
Giả sử:
PA0 ─────── Nút ───── GND
Khi nhấn:
PA0 = 0
Rất rõ ràng.
Nhưng khi thả nút:
PA0 ─────── X
PA0 không được nối với:
VCC
cũng không nối:
GND
Nó gọi là:
Floating Input – đầu vào bị thả nổi.
Khi đó PA0 có thể đọc:
0
1
0
0
1
1
0
...
mặc dù bạn không làm gì cả.
3. Dùng Pull-Up
Một giải pháp cực kỳ phổ biến là:
VCC
│
10kΩ
│
├──────── PA0
│
NÚT NHẤN
│
GND
Khi không nhấn:
VCC
│
10k
│
PA0
→ PA0 = 1
Khi nhấn:
VCC
│
10k
│
PA0
│
NÚT
│
GND
→ PA0 = 0
Vì vậy:
| Nút | PA0 |
|---|---|
| Không nhấn | 1 |
| Nhấn | 0 |
Đây là kiểu:
Active LOW
4. STM32 có Pull-Up bên trong
Điểm rất hay là STM32F103 có thể bật:
Internal Pull-Up
Do đó bạn thường không cần điện trở 10kΩ bên ngoài cho bài thử nghiệm đơn giản.
Ta cấu hình:
PA0 = Input Pull-Up
Sơ đồ:
STM32F103
┌────────┐
│ │
│ PA0 ───┼──── NÚT
│ │ │
│ │ GND
└────────┘
Bên trong STM32:
VCC
│
Pull-Up
│
├──── PA0
│
NÚT
│
GND
5. Logic của chương trình
Ta có:
if (PA0 == 0)
{
LED_ON();
}
else
{
LED_OFF();
}
Điều rất quan trọng cần nhớ:
Nút nhấn không nhất thiết phải tương ứng với logic 1.
Trong mạch Pull-Up:
Nhấn → 0
Thả → 1
Đây là lý do khi lập trình embedded bạn thường gặp:
ACTIVE_LOW
6. Đọc GPIO bằng Register
Bây giờ quay lại kiến thức các bài trước.
STM32 có:
GPIOA
và trạng thái Input được đọc từ:
GPIOA_IDR
IDR =
Input Data Register
Ví dụ:
uint32_t value;
value = GPIOA->IDR;
Lúc này:
value
chứa trạng thái của toàn bộ các chân GPIOA.
Ví dụ:
GPIOA_IDR
31 ................. 0
... PA2 PA1 PA0
7. Chỉ lấy PA0
PA0 tương ứng:
bit 0
Ta dùng:
if ((GPIOA->IDR & (1 << 0)) == 0)
{
// PA0 = LOW
}
Có thể viết:
if ((GPIOA->IDR & (1U << 0)) == 0)
{
// Nút đang nhấn
}
Hãy phân tích:
GPIOA->IDR
→ đọc thanh ghi Input.
(1U << 0)
→ tạo mask:
00000001
Sau đó:
GPIOA->IDR & (1U << 0)
→ chỉ giữ lại bit PA0.
8. Nếu PA0 là HIGH
Ví dụ:
GPIOA->IDR = xxxx xxxx xxxx 0001
Ta thực hiện:
0001
&
0001
----
0001
Kết quả khác 0.
→ PA0 = HIGH.
9. Nếu PA0 là LOW
Ví dụ:
GPIOA->IDR = xxxx xxxx xxxx 0000
Ta có:
0000
&
0001
----
0000
→ PA0 = LOW.
10. Code đọc nút
Ví dụ giả sử:
PA0 = button
PC13 = LED
Ý tưởng:
while (1)
{
if ((GPIOA->IDR & (1U << 0)) == 0)
{
// Nhấn nút
GPIOC->ODR &= ~(1U << 13);
}
else
{
// Thả nút
GPIOC->ODR |= (1U << 13);
}
}
Nhưng với Blue Pill, LED PC13 thường là active LOW.
Do đó:
PC13 = 0 → LED sáng
PC13 = 1 → LED tắt
Vì vậy:
GPIOC->ODR &= ~(1U << 13);
→ LED ON.
Còn:
GPIOC->ODR |= (1U << 13);
→ LED OFF.
11. Đây là điều rất đáng nhớ
Có hai loại logic khác nhau:
Nút nhấn
PA0 = 0 → nhấn
PA0 = 1 → thả
LED Blue Pill
PC13 = 0 → sáng
PC13 = 1 → tắt
Như vậy cả hai đều là:
Active LOW
Đây là một khái niệm cực kỳ quan trọng trong embedded.
12. Nhưng xuất hiện một vấn đề mới: Bounce
Bạn nhấn nút một lần.
Ta tưởng MCU nhận:
1 → 0
Nhưng thực tế cơ khí có thể tạo:
1
0
1
0
1
0
0
0
trong vài mili-giây.
Hiện tượng này gọi là:
Button Bounce / Contact Bounce
13. Vì sao xảy ra Bounce?
Nút nhấn cơ khí không đóng mạch hoàn hảo ngay lập tức.
Khi hai tiếp điểm chạm nhau:
CHẠM
↓
TÁCH
↓
CHẠM
↓
TÁCH
↓
CHẠM
↓
ỔN ĐỊNH
STM32 chạy rất nhanh.
Ví dụ:
STM32: MHz
Nút: vài ms
MCU có thể nhìn thấy hàng loạt lần chuyển trạng thái.
14. Chống dội bằng Delay
Cách đơn giản nhất:
if (button == 0)
{
delay_ms(20);
if (button == 0)
{
// thực sự nhấn
}
}
Ý tưởng:
Phát hiện LOW
↓
chờ 20 ms
↓
Đọc lại
↓
vẫn LOW?
↓
YES → nút thực sự nhấn
Đây là:
Software Debounce
15. Nhưng Delay có nhược điểm
Ví dụ:
delay_ms(20);
CPU bị giữ ở đó.
Nếu chương trình có:
UART
ADC
Motor
Timer
Sensor
Display
thì delay kiểu này không phải lúc nào cũng tốt.
Sau này chúng ta sẽ học cách chống dội tốt hơn bằng:
millis / timer / state machine
16. Một kiến thức cực kỳ quan trọng: Pull-Up và Pull-Down
Có hai kiểu phổ biến.
Pull-Up
VCC
│
R
│
GPIO ─── Button ─── GND
Logic:
Thả = 1
Nhấn = 0
Pull-Down
GPIO ─── Button ─── VCC
│
R
│
GND
Logic:
Thả = 0
Nhấn = 1
So sánh:
| Kiểu | Thả | Nhấn |
|---|---|---|
| Pull-Up | 1 | 0 |
| Pull-Down | 0 | 1 |
17. Tại sao Embedded rất thích Active LOW?
Bạn sẽ gặp rất nhiều tín hiệu kiểu:
RESET
CS
NSS
ENABLE
INT
IRQ
FAULT
và ký hiệu có thể xuất hiện dưới dạng:
RESET_N
CS_N
INT_N
Chữ:
_N
thường biểu thị:
Active LOW
Ví dụ:
RESET_N = 0
→ reset đang được kích hoạt.
Đây chính là lý do bạn nên làm quen với logic Active LOW ngay từ bây giờ.
18. Bài tập thực hành
Với Blue Pill của bạn, hãy thử mạch:
PA0 ─── Button ─── GND
Cấu hình:
PA0 = Input Pull-Up
PC13 = Output
Yêu cầu:
Không nhấn → LED tắt
Nhấn → LED sáng
Sau đó đảo lại:
Không nhấn → LED sáng
Nhấn → LED tắt
19. Bài tập nâng cao
Thêm 2 nút:
PA0 → Button 1
PA1 → Button 2
Yêu cầu:
Button 1 → LED ON
Button 2 → LED OFF
Logic:
if ((GPIOA->IDR & (1U << 0)) == 0)
{
// ON
}
if ((GPIOA->IDR & (1U << 1)) == 0)
{
// OFF
}
Đây là bước đầu tiên để bạn tiến tới:
GPIO Input
↓
Button
↓
Debounce
↓
State
↓
State Machine
↓
Menu
↓
Keypad
20. Điều cần nhớ sau Bài 24
Bạn chỉ cần nắm chắc 7 ý:
1. GPIO Input dùng để đọc tín hiệu bên ngoài.
2. Input không được để Floating.
3. Pull-Up:
Thả = 1
Nhấn = 0
4. Pull-Down:
Thả = 0
Nhấn = 1
5. GPIOx->IDR dùng để đọc GPIO Input.
6. Dùng & và mask để lấy một bit cụ thể.
7. Nút cơ khí có Bounce → cần Debounce.
Và công thức quan trọng nhất của bài:
if ((GPIOA->IDR & (1U << 0)) == 0)
{
// PA0 đang LOW
}
Bạn hãy đặc biệt ghi nhớ câu này:
IDRlà nơi STM32 cho chúng ta biết “chân GPIO hiện tại đang ở mức 0 hay 1”.
Bài 25 chúng ta có thể đi tiếp vào một phần rất quan trọng: GPIO Output Register – tại sao ODR, BSRR, BRR khác nhau và tại sao trong STM32 người ta thích dùng BSRR để điều khiển chân GPIO hơn ODR.