BÀI 26 – GPIO STM32: ĐIỀU KHIỂN NHIỀU OUTPUT & TẠO MẪU DRIVER GPIO
Ở Bài 25, chúng ta đã đi từ việc hiểu GPIO Output đến điều khiển một chân GPIO. Bây giờ ta nâng lên một bước rất quan trọng:
Không viết chương trình kiểu “điều khiển từng chân một cách tùy tiện”, mà bắt đầu tổ chức GPIO thành một hệ thống có cấu trúc.
Đây là bước rất quan trọng để sau này bạn có thể viết driver cho LED, relay, motor, LCD, sensor…
1. Ôn lại GPIO Output
Ví dụ STM32F103:
GPIOA->ODR |= (1 << 0); // PA0 = HIGH
GPIOA->ODR &= ~(1 << 0); // PA0 = LOW
GPIOA->ODR ^= (1 << 0); // PA0 đảo trạng thái
Ta có thể viết ngắn gọn:
#define LED_PIN 0
GPIOA->ODR |= (1 << LED_PIN);
GPIOA->ODR &= ~(1 << LED_PIN);
GPIOA->ODR ^= (1 << LED_PIN);
Nhưng nếu có 5 LED:
PA0 → LED1
PA1 → LED2
PA2 → LED3
PA3 → LED4
PA4 → LED5
thì chương trình sẽ bắt đầu trở nên khó quản lý.
Đây chính là lúc chúng ta cần hàm điều khiển GPIO.
2. Tạo hàm điều khiển LED
Thay vì viết:
GPIOA->ODR |= (1 << 0);
ta tạo:
void LED_On(void)
{
GPIOA->ODR |= (1 << 0);
}
và:
void LED_Off(void)
{
GPIOA->ODR &= ~(1 << 0);
}
Sau đó trong main():
LED_On();
hoặc:
LED_Off();
Điều này có vẻ đơn giản, nhưng tư duy đã thay đổi:
main()
↓
LED_On()
↓
GPIO Register
↓
Phần cứng
Thay vì:
main()
↓
đụng trực tiếp vào register
3. Tốt hơn nữa: truyền PIN vào hàm
Nếu có nhiều LED thì không nên tạo:
LED1_On();
LED2_On();
LED3_On();
LED4_On();
Ta có thể tạo:
void GPIO_Set(GPIO_TypeDef *GPIOx, uint8_t pin)
{
GPIOx->ODR |= (1 << pin);
}
và:
void GPIO_Clear(GPIO_TypeDef *GPIOx, uint8_t pin)
{
GPIOx->ODR &= ~(1 << pin);
}
Bây giờ:
GPIO_Set(GPIOA, 0);
nghĩa là:
PA0 = HIGH
Còn:
GPIO_Clear(GPIOA, 0);
nghĩa là:
PA0 = LOW
4. Vì sao phải truyền GPIOx?
STM32F103 có rất nhiều GPIO:
GPIOA
GPIOB
GPIOC
GPIOD
...
Ví dụ:
GPIO_Set(GPIOA, 0);
→ PA0 HIGH
GPIO_Set(GPIOA, 5);
→ PA5 HIGH
GPIO_Set(GPIOB, 3);
→ PB3 HIGH
GPIO_Set(GPIOC, 13);
→ PC13 HIGH
Như vậy một hàm có thể điều khiển rất nhiều chân GPIO.
Đây chính là tư duy của driver.
5. Tạo hàm Toggle
Ta đã học toán tử XOR ^ ở Bài 16.
GPIOx->ODR ^= (1 << pin);
Có thể tạo:
void GPIO_Toggle(GPIO_TypeDef *GPIOx, uint8_t pin)
{
GPIOx->ODR ^= (1 << pin);
}
Sau đó:
GPIO_Toggle(GPIOC, 13);
Mỗi lần gọi:
PC13 = 0
sẽ thành:
PC13 = 1
và ngược lại.
6. Nhưng có một vấn đề
Đoạn này:
GPIOx->ODR |= (1 << pin);
có một đặc điểm:
CPU phải đọc ODR → thay đổi bit → ghi lại ODR.
STM32F1 có cách tốt hơn:
BSRR
Thanh ghi:
GPIOx->BSRR
cho phép SET hoặc RESET GPIO.
Ví dụ:
GPIOA->BSRR = (1 << 0);
→ PA0 HIGH.
Reset:
GPIOA->BSRR = (1 << (0 + 16));
→ PA0 LOW.
Tức là:
BSRR bit 0 → SET PA0
BSRR bit 16 → RESET PA0
7. Tại sao BSRR rất quan trọng?
Giả sử:
PA0 = LED
PA1 = RELAY
PA2 = MOTOR
Nếu sử dụng ODR:
GPIOA->ODR |= (1 << 0);
GPIOA->ODR &= ~(1 << 1);
CPU thực hiện các thao tác đọc/sửa/ghi.
Trong nhiều tình huống, điều này có thể gây vấn đề khi nhiều phần mềm cùng tác động tới GPIO.
BSRR cho phép thao tác trực tiếp:
GPIOA->BSRR = (1 << 0);
và:
GPIOA->BSRR = (1 << (1 + 16));
8. Viết driver GPIO đơn giản
Ta có thể tạo:
void GPIO_WritePin(GPIO_TypeDef *GPIOx,
uint8_t pin,
uint8_t state)
{
if (state)
{
GPIOx->BSRR = (1 << pin);
}
else
{
GPIOx->BSRR = (1 << (pin + 16));
}
}
Bây giờ:
GPIO_WritePin(GPIOC, 13, 1);
→ PC13 HIGH.
GPIO_WritePin(GPIOC, 13, 0);
→ PC13 LOW.
9. Thêm Toggle
void GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint8_t pin)
{
GPIOx->ODR ^= (1 << pin);
}
Như vậy chúng ta đã có một bộ API nhỏ:
GPIO_WritePin()
GPIO_SetPin()
GPIO_ClearPin()
GPIO_TogglePin()
Đây chính là cách tư duy để sau này xây dựng:
LED Driver
Relay Driver
Button Driver
Motor Driver
LCD Driver
UART Driver
SPI Driver
I2C Driver
10. Một khái niệm rất quan trọng: abstraction
Có hai cách viết.
Cách 1 – viết trực tiếp register
GPIOC->BSRR = (1 << 13);
Cách 2 – thông qua driver
GPIO_SetPin(GPIOC, 13);
Cách 2 có lợi thế:
Ứng dụng
↓
GPIO_SetPin()
↓
GPIO Register
↓
STM32 Hardware
Ứng dụng không cần biết chi tiết register bên dưới.
Đây gọi là:
Abstraction – tầng trừu tượng hóa.
11. Đây chính là tư duy lập trình STM32 chuyên nghiệp
Một chương trình lớn không nên có hàng nghìn dòng như:
GPIOA->CRL = ...
GPIOA->ODR |= ...
GPIOB->CRH = ...
GPIOC->BSRR = ...
GPIOA->ODR &= ...
rải khắp main.c.
Thay vào đó:
main.c
│
├── LED
│
├── BUTTON
│
├── MOTOR
│
└── SENSOR
│
↓
GPIO Driver
│
↓
STM32 Register
Đây là nền tảng của Embedded Software Architecture.
12. Bài thực hành 26
Với Blue Pill của bạn, chúng ta có:
PC13 → LED onboard
Hãy tạo:
void GPIO_SetPin(GPIO_TypeDef *GPIOx, uint8_t pin)
{
GPIOx->BSRR = (1 << pin);
}
void GPIO_ClearPin(GPIO_TypeDef *GPIOx, uint8_t pin)
{
GPIOx->BSRR = (1 << (pin + 16));
}
void GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint8_t pin)
{
GPIOx->ODR ^= (1 << pin);
}
Sau đó:
while (1)
{
GPIO_TogglePin(GPIOC, 13);
for (volatile uint32_t i = 0; i < 500000; i++);
}
LED PC13 sẽ nhấp nháy.
13. Điều quan trọng nhất của Bài 26
Bạn cần nắm chắc chuỗi tư duy này:
GPIO
↓
Port + Pin
↓
Register
↓
Bit
↓
Mask
↓
Function
↓
Driver
↓
Application
Ví dụ:
GPIOC->BSRR = (1 << 13);
không còn chỉ là một câu lệnh khó hiểu.
Bạn phải nhìn được:
GPIOC
↓
Port C
13
↓
Pin 13
1 << 13
↓
Mask 0x2000
BSRR
↓
Set bit GPIO
Kết quả
↓
PC13 = HIGH
🧠 Bài tập tự làm
Không xem đáp án trước, hãy tự viết 4 hàm:
GPIO_SetPin()
GPIO_ClearPin()
GPIO_TogglePin()
GPIO_ReadPin()
Trong đó hàm cuối:
GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint8_t pin)
phải đọc trạng thái chân GPIO bằng thanh ghi:
IDR
Ví dụ mục tiêu:
if (GPIO_ReadPin(GPIOA, 0))
{
// PA0 đang HIGH
}
Bài 27 chúng ta sẽ đi tiếp vào phần rất quan trọng: GPIO Input + Pull-up/Pull-down + nút nhấn thực tế, và từ đó hiểu tại sao một chân input có thể đọc 1, 0, hoặc bị trôi (floating).