BÀI 32 – GPIO INPUT + PULL-UP / PULL-DOWN TRÊN STM32F103
Đây là bài rất quan trọng, bởi vì sau khi hiểu bài này bạn sẽ có thể đọc nút nhấn đúng cách, hiểu tại sao chân input bị floating, và đặc biệt hiểu một điểm rất thú vị của STM32F103:
Khi GPIO ở chế độ Input Pull-up/Pull-down, thanh ghi
ODRlại được dùng để lựa chọn kéo lên hay kéo xuống.
1. GPIO Input dùng để làm gì?
Ở các bài trước, chúng ta chủ yếu dùng GPIO như:
STM32 → GPIO → LED
Tức là STM32 điều khiển tín hiệu.
Ví dụ:
PC13 → LED
Bây giờ chúng ta đổi chiều:
Nút nhấn → GPIO → STM32
Ví dụ:
Button → PA0
STM32 phải biết:
PA0 = HIGH
hay:
PA0 = LOW
Để làm việc đó, chúng ta đọc:
GPIOA->IDR
2. IDR – Input Data Register
IDR là thanh ghi dùng để đọc trạng thái thực tế của chân GPIO.
Ví dụ:
GPIOA->IDR
Có 16 bit tương ứng:
GPIOA->IDR
bit15 → PA15
bit14 → PA14
...
bit1 → PA1
bit0 → PA0
Nếu:
PA0 = HIGH
thì:
GPIOA->IDR & GPIO_PIN_0
sẽ khác 0.
Nếu:
PA0 = LOW
thì:
GPIOA->IDR & GPIO_PIN_0
sẽ bằng 0.
3. Ví dụ đọc PA0
Bạn có thể viết:
if (GPIOA->IDR & GPIO_PIN_0)
{
// PA0 = HIGH
}
else
{
// PA0 = LOW
}
Hoặc với Driver chúng ta đã xây dựng ở Bài 31:
if (GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET)
{
// HIGH
}
Đây chính là lý do chúng ta viết Driver.
main.c không cần quan tâm trực tiếp đến IDR.
4. Vấn đề cực kỳ quan trọng: Floating Input
Giả sử bạn cấu hình:
PA0 = Input
và để chân PA0 hoàn toàn không nối với gì.
Bạn có thể tưởng tượng:
PA0
│
│
?
Điện áp trên chân này không được xác định rõ.
Nó có thể đọc:
HIGH
rồi:
LOW
rồi:
HIGH
mặc dù bạn không làm gì cả.
Đây gọi là:
Floating Input
5. Tại sao Floating nguy hiểm?
GPIO input có trở kháng rất cao.
Nó gần giống như:
GPIO input
│
│
└──── rất ít dòng điện
Do đó các nhiễu điện từ môi trường có thể làm điện áp chân GPIO thay đổi.
Ví dụ:
PA0
│
├── dây dài
├── nhiễu nguồn
├── nhiễu motor
├── nhiễu relay
└── tín hiệu xung quanh
STM32 có thể liên tục nhìn thấy:
0 → 1 → 0 → 1 → 0
mặc dù không có ai nhấn nút.
6. Giải pháp: Pull-up và Pull-down
Ta cần một điện trở để ép chân GPIO về một trạng thái xác định.
Có hai cách:
Pull-up
và:
Pull-down
7. Pull-up là gì?
Pull-up nghĩa là:
Kéo chân GPIO về HIGH khi không có tín hiệu bên ngoài.
Sơ đồ:
VCC
│
R
│
├──── PA0
│
Button
│
GND
Khi button không nhấn:
VCC
│
R
│
PA0
nên:
PA0 = HIGH
Khi button nhấn:
PA0
│
Button
│
GND
nên:
PA0 = LOW
8. Logic của nút nhấn Pull-up
| Button | PA0 |
|---|---|
| Không nhấn | HIGH |
| Nhấn | LOW |
Đây là kiểu rất phổ biến.
Vì vậy bạn sẽ thường thấy:
if (GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
{
// Button đang nhấn
}
Chứ không phải:
if (GPIO_ReadPin(...) == GPIO_PIN_SET)
9. Pull-down là gì?
Pull-down làm ngược lại.
Sơ đồ:
VCC
│
Button
│
├──── PA0
│
R
│
GND
Khi không nhấn:
PA0 = LOW
Khi nhấn:
PA0 = HIGH
Bảng logic:
| Button | PA0 |
|---|---|
| Không nhấn | LOW |
| Nhấn | HIGH |
10. So sánh Pull-up và Pull-down
PULL-UP
Không nhấn → HIGH
Nhấn → LOW
Trong khi:
PULL-DOWN
Không nhấn → LOW
Nhấn → HIGH
Bạn chỉ cần nhớ:
Pull-up kéo mặc định lên 1.
Pull-down kéo mặc định xuống 0.
11. STM32F103 có điện trở Pull nội không?
Có.
Đây là điểm rất tiện.
Bạn không nhất thiết lúc nào cũng phải mắc điện trở bên ngoài.
STM32F103 có thể cấu hình:
Input Pull-up
hoặc:
Input Pull-down
bằng chính GPIO configuration.
12. Nhưng STM32F103 có một điểm rất đặc biệt
Khi cấu hình:
Input Pull-up/Pull-down
thì MODE = 00.
Còn CNF được đặt:
CNF = 10
Tức là cấu hình 4 bit:
CNF MODE
10 00
thành:
1000
hay:
0x8
Vậy:
Input Pull-up/Pull-down
=
1000
13. Và đây là chỗ rất thú vị
Bạn có thể nghĩ:
“Nếu đang Input thì tại sao lại dùng ODR?”
Đúng.
Thông thường chúng ta nghĩ:
ODR
↓
Output
Nhưng trên STM32F1:
Input Pull-up/Pull-down
thì:
ODR bit
↓
chọn Pull-up hoặc Pull-down
14. Cơ chế cụ thể
Giả sử PA0:
CRL bit3:0 = 1000
tức:
Input Pull-up/Pull-down
Sau đó:
GPIOA->ODR |= GPIO_PIN_0;
thì:
ODR0 = 1
→ Pull-up
Ngược lại:
GPIOA->ODR &= ~GPIO_PIN_0;
thì:
ODR0 = 0
→ Pull-down
15. Đây là điều bạn nên ghi nhớ
Với STM32F103:
Input Pull-up/Pull-down
thì:
ODR = 1 → Pull-up
ODR = 0 → Pull-down
Đây là một đặc điểm rất quan trọng của GPIO STM32F1.
16. Cấu hình PA0 Pull-up bằng thanh ghi
Ta có:
GPIOA->CRL
PA0 sử dụng:
CRL[3:0]
Muốn:
Input Pull-up/Pull-down
ta ghi:
1000
Do đó:
GPIOA->CRL &= ~(0xF << 0);
GPIOA->CRL |= (0x8 << 0);
Sau đó:
GPIOA->ODR |= GPIO_PIN_0;
PA0 trở thành:
Input Pull-up
17. Cấu hình PA0 Pull-down
Phần CRL vẫn giống hệt:
GPIOA->CRL &= ~(0xF << 0);
GPIOA->CRL |= (0x8 << 0);
Nhưng ODR thay đổi:
GPIOA->ODR &= ~GPIO_PIN_0;
Kết quả:
PA0 = Input Pull-down
18. Đây chính là lý do Driver rất hữu ích
Nếu không có Driver, bạn phải nhớ:
GPIOA->CRL &= ~(0xF << 0);
GPIOA->CRL |= (0x8 << 0);
GPIOA->ODR |= GPIO_PIN_0;
Nhưng với Driver, chúng ta có thể thiết kế:
GPIO_Init(GPIOA,
GPIO_PIN_0,
GPIO_INPUT_PULLUP,
0);
Hoặc:
GPIO_Init(GPIOA,
GPIO_PIN_0,
GPIO_INPUT_PULLDOWN,
0);
GPIO.c sẽ lo toàn bộ việc cấu hình thanh ghi.
19. Thiết kế Driver tốt hơn
Ở Bài 31 chúng ta có thể mở rộng enum:
typedef enum
{
GPIO_MODE_INPUT_FLOATING,
GPIO_MODE_INPUT_PULLUP,
GPIO_MODE_INPUT_PULLDOWN,
GPIO_MODE_OUTPUT_PP,
GPIO_MODE_OUTPUT_OD
} GPIO_Mode;
Sau đó:
GPIO_Init(GPIOA,
GPIO_PIN_0,
GPIO_MODE_INPUT_PULLUP,
0);
Rất dễ đọc.
20. Đọc trạng thái nút nhấn
Sau khi cấu hình:
GPIO_Init(GPIOA,
GPIO_PIN_0,
GPIO_MODE_INPUT_PULLUP,
0);
Ta có:
while (1)
{
if (GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
{
// Button được nhấn
}
}
Nhớ:
Pull-up:
Không nhấn → 1
Nhấn → 0
21. Điều khiển LED PC13 bằng Button PA0
Đây là bài thực hành rất tốt.
Phần cứng:
Blue Pill
PA0 ─── Button
│
GND
PC13 ─── LED onboard
Cấu hình:
PA0 = Input Pull-up
PC13 = Output
Logic:
PA0 = HIGH
↓
Không nhấn
↓
LED OFF
PA0 = LOW
↓
Nhấn
↓
LED ON
22. Code ứng dụng
Với Driver của chúng ta:
int main(void)
{
GPIO_Init(GPIOA,
GPIO_PIN_0,
GPIO_MODE_INPUT_PULLUP,
0);
GPIO_Init(GPIOC,
GPIO_PIN_13,
GPIO_MODE_OUTPUT_PP,
GPIO_SPEED_2MHZ);
while (1)
{
if (GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
{
GPIO_WritePin(GPIOC,
GPIO_PIN_13,
GPIO_PIN_RESET);
}
else
{
GPIO_WritePin(GPIOC,
GPIO_PIN_13,
GPIO_PIN_SET);
}
}
}
Lưu ý: LED trên Blue Pill thường mắc active-low, vì vậy PC13 = LOW thường là LED sáng.
23. Tại sao không dùng Pull-up ngoài?
Có thể dùng.
Ví dụ:
3.3V
│
10k
│
├──── PA0
│
Button
│
GND
Đây là:
External Pull-up
Trong khi STM32 có:
Internal Pull-up
Hai cách đều có thể sử dụng.
24. Internal Pull-up và External Pull-up
Internal Pull-up
3.3V
│
[resistor bên trong STM32]
│
PA0
Ưu điểm:
- Không cần thêm linh kiện.
- Mạch đơn giản.
- Tiết kiệm PCB.
External Pull-up
3.3V
│
[10k]
│
PA0
Ưu điểm:
- Bạn chủ động chọn giá trị điện trở.
- Có thể phù hợp hơn với một số mạch tốc độ cao/nhiễu cao.
- Dễ kiểm soát đặc tính điện.
25. Giá trị điện trở Pull-up
Ví dụ phổ biến:
4.7k
10k
22k
47k
Với nút nhấn thông thường, 10kΩ là một giá trị rất dễ gặp.
Nhưng không nên hiểu rằng:
“Mọi GPIO đều bắt buộc phải dùng 10k.”
Giá trị phù hợp còn phụ thuộc:
- tốc độ tín hiệu
- nhiễu
- dòng tiêu thụ
- điện dung đường mạch
- thiết kế phần cứng
26. Một vấn đề khác: Button Bounce
Giả sử bạn nhấn nút một lần.
Bạn nghĩ STM32 sẽ thấy:
0
một lần.
Nhưng thực tế tiếp điểm cơ khí có thể tạo:
0
1
0
1
0
0
trong vài mili-giây.
Đây gọi là:
Mechanical Bounce
Vì vậy:
Một lần nhấn
không nhất thiết tạo:
Một lần chuyển trạng thái
27. Đây sẽ là bài học tiếp theo rất hay
Chúng ta có thể xử lý debounce bằng:
Cách 1
Delay:
delay_ms(20);
Cách 2
Timer.
Cách 3
State machine.
Cách 4
Interrupt + debounce.
Trong firmware thực tế, state machine + timer thường là cách rất đáng học.
Nhưng hiện tại chưa cần vội.
28. Một câu hỏi kiểm tra kiến thức
Giả sử:
GPIOA->CRL
của PA0 là:
1000
Bạn biết PA0 đang ở chế độ:
Input Pull-up/Pull-down
Nếu:
GPIOA->ODR |= GPIO_PIN_0;
thì PA0 là:
Pull-up.
Nếu:
GPIOA->ODR &= ~GPIO_PIN_0;
thì PA0 là:
Pull-down.
Đây là một câu hỏi bạn nên tự trả lời được mà không cần nhìn tài liệu.
29. Sơ đồ tư duy của Bài 32
Bạn hãy ghi nhớ toàn bộ bài bằng sơ đồ này:
GPIO INPUT
│
┌───────┴───────┐
│ │
Floating Pull
│ │
Không ổn định ┌────┴────┐
│ │
Pull-up Pull-down
│ │
HIGH LOW
mặc định mặc định
│ │
ODR=1 ODR=0
Còn dữ liệu thực tế của chân vẫn được đọc bằng:
GPIOx->IDR
│
┌─────┴─────┐
│ │
HIGH LOW
30. Bốn thanh ghi GPIO bạn đã học đến đây
Sau Bài 32, bạn nên bắt đầu hình thành bảng này trong đầu:
| Thanh ghi | Chức năng |
|---|---|
CRL |
Cấu hình PA0–PA7 |
CRH |
Cấu hình PA8–PA15 |
IDR |
Đọc trạng thái GPIO |
ODR |
Output / chọn Pull-up-Pull-down |
BSRR |
SET/RESET GPIO |
Đặc biệt:
CRL/CRH → GPIO configuration
IDR → GPIO input
ODR → GPIO output + Pull selection
BSRR → Atomic SET/RESET
31. Bài thực hành của bạn
Tôi muốn bạn làm một bài duy nhất nhưng rất quan trọng:
STM32 Blue Pill
Kết nối:
PA0 ─── Button ─── GND
Không cần điện trở ngoài.
Cấu hình:
PA0 → Input Pull-up
PC13 → Output Push-Pull
Yêu cầu:
Không nhấn nút → LED tắt
Nhấn nút → LED sáng
Và trong main.c không được phép viết trực tiếp:
GPIOA->CRL
GPIOA->CRH
GPIOA->IDR
GPIOA->ODR
GPIOA->BSRR
Chỉ được sử dụng:
GPIO_Init()
GPIO_ReadPin()
GPIO_WritePin()
Như vậy bạn sẽ thực sự bắt đầu lập trình bằng Driver, thay vì chỉ thao tác thanh ghi.
32. Và đây là bước tiếp theo rất quan trọng
Sau khi bạn làm xong bài này, chúng ta nên sang:
BÀI 33 – EXTI: GPIO INTERRUPT TRÊN STM32F103
Lúc đó chúng ta sẽ chuyển từ:
CPU liên tục hỏi:
"Button đã nhấn chưa?"
"Button đã nhấn chưa?"
"Button đã nhấn chưa?"
sang:
Button nhấn
↓
GPIO
↓
EXTI
↓
NVIC
↓
CPU bị ngắt
↓
Interrupt Handler
Đây chính là bước đầu tiên để bạn hiểu Interrupt thực sự hoạt động như thế nào trên STM32, thay vì chỉ gọi một hàm HAL có sẵn.