Được chứ. Nếu Bài 30 – Tự viết GPIO Driver cho STM32F103 đã giúp bạn đi từ việc thao tác trực tiếp thanh ghi sang tự xây dựng một lớp driver, thì Bài 31 rất phù hợp để bước tiếp theo là:
BÀI 31 – XÂY DỰNG GPIO DRIVER HOÀN CHỈNH: GPIO_Init(), GPIO_WritePin(), GPIO_ReadPin(), GPIO_TogglePin()
Mục tiêu của bài này là để bạn bắt đầu viết GPIO theo kiểu giống tư duy của HAL/LL, nhưng driver là do chính bạn viết.
1. Bài 31 chúng ta sẽ học gì?
Sau bài này bạn sẽ có thể viết:
GPIO_Init(GPIOA, GPIO_PIN_0, GPIO_OUTPUT_PP, GPIO_SPEED_2MHz);
GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
GPIO_TogglePin(GPIOA, GPIO_PIN_0);
uint8_t state = GPIO_ReadPin(GPIOA, GPIO_PIN_0);
Tức là thay vì chương trình của bạn đầy những dòng:
GPIOA->CRL &= ~(0xF << 0);
GPIOA->CRL |= (0x2 << 0);
GPIOA->BSRR = (1 << 0);
ta sẽ tạo ra một GPIO Driver riêng.
Kiến trúc sẽ bắt đầu giống thế này:
main.c
│
▼
GPIO Driver
│
├── GPIO_Init()
├── GPIO_WritePin()
├── GPIO_ReadPin()
└── GPIO_TogglePin()
│
▼
STM32F103 Register
│
├── CRL
├── CRH
├── IDR
├── ODR
└── BSRR
Đây là một bước rất quan trọng khi bạn muốn chuyển từ “học STM32” sang “lập trình firmware STM32”.
2. Trước tiên: GPIO Driver thực chất là gì?
Bạn có thể hiểu rất đơn giản:
GPIO Driver là lớp code đứng giữa ứng dụng và thanh ghi phần cứng.
Ví dụ ứng dụng muốn bật LED:
GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
Ứng dụng không cần biết bên trong phải làm:
GPIOC->BSRR = ...
Driver sẽ lo việc đó.
Đây chính là tư duy:
Application
↓
Driver
↓
Register
↓
Hardware
3. Tại sao không viết trực tiếp thanh ghi?
Bạn đã học ở các bài trước:
GPIOC->ODR |= (1 << 13);
Cách này hoàn toàn đúng.
Nhưng nếu chương trình lớn lên, bạn sẽ gặp vấn đề.
Ví dụ:
GPIOA->ODR |= (1 << 0);
GPIOA->ODR &= ~(1 << 1);
GPIOB->ODR |= (1 << 5);
GPIOC->ODR &= ~(1 << 13);
Sau này bạn có:
- LED
- button
- relay
- UART
- SPI
- I2C
- LCD
- motor
- sensor
thì code sẽ đầy các thao tác thanh ghi.
Driver giúp bạn biến chúng thành:
GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET);
GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET);
GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
Dễ đọc hơn rất nhiều.
4. Thiết kế GPIO Driver
Ta tạo hai file:
GPIO.h
GPIO.c
Trong dự án STM32 của bạn:
Core
│
├── Inc
│ └── GPIO.h
│
└── Src
└── GPIO.c
Nếu dùng Arduino IDE thì cũng có thể đặt:
GPIO.h
GPIO.cpp
Nhưng trong bài học này tôi muốn bạn bắt đầu làm quen với cách tổ chức kiểu firmware C:
GPIO.h
GPIO.c
5. Tạo các kiểu dữ liệu cho GPIO
Đầu tiên:
typedef enum
{
GPIO_PIN_RESET = 0,
GPIO_PIN_SET = 1
} GPIO_PinState;
Bây giờ ta có:
GPIO_PIN_RESET
và:
GPIO_PIN_SET
Thay vì:
0
1
Code sẽ dễ đọc hơn.
6. Định nghĩa các chân GPIO
Ta có thể định nghĩa:
#define GPIO_PIN_0 (1U << 0)
#define GPIO_PIN_1 (1U << 1)
#define GPIO_PIN_2 (1U << 2)
#define GPIO_PIN_3 (1U << 3)
#define GPIO_PIN_4 (1U << 4)
#define GPIO_PIN_5 (1U << 5)
#define GPIO_PIN_6 (1U << 6)
#define GPIO_PIN_7 (1U << 7)
#define GPIO_PIN_8 (1U << 8)
#define GPIO_PIN_9 (1U << 9)
#define GPIO_PIN_10 (1U << 10)
#define GPIO_PIN_11 (1U << 11)
#define GPIO_PIN_12 (1U << 12)
#define GPIO_PIN_13 (1U << 13)
#define GPIO_PIN_14 (1U << 14)
#define GPIO_PIN_15 (1U << 15)
Điều này cực kỳ quan trọng.
Ví dụ:
GPIO_PIN_13
thực chất là:
0000 0000 0000 0000
↑
bit13
hay:
0x2000
7. Định nghĩa chế độ GPIO
STM32F103 có một điểm rất đặc biệt.
GPIO được cấu hình thông qua:
CRL
CRH
Mỗi chân cần 4 bit cấu hình.
Ví dụ PA0:
CRL
bit 0 ─┐
bit 1 ├── MODE0 + CNF0
bit 2 │
bit 3 ─┘
PA1:
bit 4 → bit 7
PA2:
bit 8 → bit 11
…
PA7:
bit 28 → bit 31
Còn PA8 đến PA15 nằm trong:
CRH
8. Các mode cơ bản
Trong bài này chúng ta tạo:
#define GPIO_INPUT_ANALOG 0x0
#define GPIO_INPUT_FLOATING 0x4
#define GPIO_INPUT_PULL 0x8
#define GPIO_OUTPUT_PP 0x0
#define GPIO_OUTPUT_OD 0x4
#define GPIO_AF_PP 0x8
#define GPIO_AF_OD 0xC
Nhưng bạn phải hiểu:
Đây chưa phải toàn bộ 4 bit cấu hình.
Còn có:
MODE
để xác định tốc độ output.
9. MODE của STM32F103
STM32F103 có:
MODE = 00 → Input
MODE = 01 → Output 10 MHz
MODE = 10 → Output 2 MHz
MODE = 11 → Output 50 MHz
Ta định nghĩa:
#define GPIO_MODE_INPUT 0x0
#define GPIO_SPEED_10MHZ 0x1
#define GPIO_SPEED_2MHZ 0x2
#define GPIO_SPEED_50MHZ 0x3
Nhưng hãy chú ý:
MODE + CNF
mới tạo thành 4-bit configuration.
10. Ví dụ PA0 Output Push-Pull 2 MHz
Ta muốn:
PA0
MODE = 10
CNF = 00
Ghép lại:
CNF MODE
00 10
thành:
0010
hay:
0x2
Vì PA0 sử dụng 4 bit đầu của CRL:
GPIOA->CRL &= ~(0xF << 0);
GPIOA->CRL |= (0x2 << 0);
Đây chính là thứ Driver sẽ tự động làm.
11. GPIO_Init()
Bây giờ chúng ta thiết kế hàm:
GPIO_Init(GPIOA,
GPIO_PIN_0,
GPIO_OUTPUT_PP,
GPIO_SPEED_2MHZ);
Ý nghĩa:
GPIOA
│
└── PA0
│
├── Output
├── Push-Pull
└── 2 MHz
Driver sẽ tự:
- xác định pin
- xác định CRL hay CRH
- xác định vị trí 4 bit
- xóa cấu hình cũ
- tạo cấu hình mới
- ghi vào thanh ghi
12. Công thức cực kỳ quan trọng
Đây là phần bạn nên hiểu thật kỹ.
Với pin:
pin = 0
vị trí:
shift = 0 × 4 = 0
Pin 1:
shift = 1 × 4 = 4
Pin 2:
shift = 2 × 4 = 8
Pin 3:
shift = 3 × 4 = 12
…
Pin 7:
shift = 7 × 4 = 28
Công thức:
shift = pin_number * 4;
Đây là một trong những công thức quan trọng nhất của GPIO STM32F1.
13. CRL hay CRH?
Nếu:
pin <= 7
thì:
CRL
Nếu:
pin >= 8
thì:
CRH
Với pin 8:
CRH bit 0 → 3
Pin 9:
CRH bit 4 → 7
Vì vậy:
if (pin_number < 8)
{
// CRL
}
else
{
// CRH
}
14. Ví dụ PA13
PA13 không nằm trong CRL.
Nó nằm trong:
GPIOA->CRH
Vị trí của PA13:
13 - 8 = 5
Do mỗi pin chiếm 4 bit:
5 × 4 = 20
Vậy PA13 sử dụng:
CRH[23:20]
Đây là lý do khi bạn nhìn datasheet STM32F103 sẽ thấy PA13 cấu hình ở các bit:
20 → 23
15. GPIO_WritePin()
Sau khi cấu hình GPIO, ta cần điều khiển output.
Ta tạo:
void GPIO_WritePin(GPIO_TypeDef *GPIOx,
uint16_t GPIO_Pin,
GPIO_PinState PinState);
Ví dụ:
GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
16. Vì sao nên dùng BSRR?
Bạn vừa học BSRR ở Bài 29 nên đây chính là lúc áp dụng kiến thức đó.
Không nên ưu tiên:
GPIOC->ODR |= GPIO_PIN_13;
hoặc:
GPIOC->ODR &= ~GPIO_PIN_13;
mà Driver có thể dùng:
GPIOC->BSRR = GPIO_PIN_13;
để SET.
Và:
GPIOC->BSRR = ((uint32_t)GPIO_PIN_13 << 16);
để RESET.
17. Tại sao BSRR rất hay?
Ví dụ:
GPIOC->BSRR = GPIO_PIN_13;
nghĩa là:
BSRR bit13 = 1
↓
PC13 = SET
Còn:
GPIOC->BSRR = GPIO_PIN_13 << 16;
thì:
BSRR bit29 = 1
↓
PC13 = RESET
Bạn không cần:
read
modify
write
như với ODR.
18. GPIO_ReadPin()
Input thì chúng ta đọc:
GPIOx->IDR
Ví dụ:
if (GPIOA->IDR & GPIO_PIN_0)
{
// PA0 = HIGH
}
Driver có thể đóng gói thành:
GPIO_PinState GPIO_ReadPin(GPIO_TypeDef *GPIOx,
uint16_t GPIO_Pin);
Sau đó chương trình chỉ cần:
if (GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET)
{
...
}
19. GPIO_TogglePin()
Đây là một hàm rất hữu ích.
Ví dụ:
GPIO_TogglePin(GPIOC, GPIO_PIN_13);
LED đang:
OFF → ON
lần sau:
ON → OFF
Ta có thể thực hiện bằng ODR:
GPIOx->ODR ^= GPIO_Pin;
Đây cũng là lúc bạn thấy sự khác nhau:
BSRR
↓
SET / RESET an toàn
ODR
↓
đọc trạng thái hiện tại rồi đảo
20. Bộ API đầu tiên của GPIO Driver
Sau bài này, driver của chúng ta sẽ có bộ hàm:
GPIO_Init();
GPIO_WritePin();
GPIO_ReadPin();
GPIO_TogglePin();
Đây chính là một GPIO abstraction layer.
21. Ví dụ ứng dụng LED PC13
Giả sử Blue Pill của bạn có LED trên:
PC13
Ta có thể viết:
int main(void)
{
GPIO_Init(GPIOC,
GPIO_PIN_13,
GPIO_OUTPUT_PP,
GPIO_SPEED_2MHZ);
while (1)
{
GPIO_TogglePin(GPIOC, GPIO_PIN_13);
delay_ms(500);
}
}
Điều rất đáng chú ý là main() bây giờ không còn quan tâm CRH.
Nó không cần biết:
GPIOC->CRH
cũng không cần biết:
bit 20
bit 21
bit 22
bit 23
Toàn bộ chuyện đó nằm trong Driver.
22. Đây chính là tư duy Driver
Bạn hãy nhìn sự tiến hóa:
Cấp 1 – thao tác thanh ghi
GPIOC->CRH &= ~(0xF << 20);
GPIOC->CRH |= (0x2 << 20);
GPIOC->BSRR = (1 << 29);
↓
Cấp 2 – tự viết hàm
GPIO_Init(...);
GPIO_WritePin(...);
↓
Cấp 3 – Driver
Application
↓
GPIO API
↓
GPIO Driver
↓
STM32 Register
↓
Hardware
Đây chính là hướng mà firmware thực tế thường đi.
23. Một bài tập rất quan trọng cho bạn
Đừng chỉ copy code.
Hãy tự viết Driver để điều khiển:
PC13 LED
và:
PA0 button
Sơ đồ:
STM32F103
┌───────────┐
│ │
PA0 ───┤ INPUT │
│ │
PC13 ──┤ OUTPUT │
│ │
└───────────┘
Yêu cầu:
PA0 = 0 → PC13 bật
PA0 = 1 → PC13 tắt
Bạn phải sử dụng:
GPIO_Init()
GPIO_ReadPin()
GPIO_WritePin()
không được viết trực tiếp GPIOA->CRL, GPIOC->CRH trong main.c.
24. Một điều tôi muốn bạn đặc biệt nhớ
Ở giai đoạn hiện tại, bạn không cần học thuộc code Driver.
Bạn cần hiểu 5 thứ:
1. GPIOx->CRL
2. GPIOx->CRH
3. GPIOx->IDR
4. GPIOx->ODR
5. GPIOx->BSRR
và mối quan hệ:
CRL/CRH
↓
Cấu hình GPIO
IDR
↓
Đọc GPIO
ODR
↓
Đọc/ghi trạng thái Output
BSRR
↓
SET/RESET Output
Nếu 5 thanh ghi này bạn thực sự hiểu, việc đọc GPIO của STM32F1 sẽ nhẹ đi rất nhiều.
25. Chuỗi bài tiếp theo
Tôi đề xuất chúng ta tiếp tục theo đúng mạch đang học:
Bài 30
Tự viết GPIO Driver
↓
Bài 31
GPIO Driver hoàn chỉnh
↓
Bài 32
GPIO Input + Pull-up/Pull-down
↓
Bài 33
EXTI – External Interrupt
↓
Bài 34
NVIC – Interrupt Controller
↓
Bài 35
Timer cơ bản
↓
Bài 36
SysTick
↓
Bài 37
Delay không blocking
↓
Bài 38
UART
↓
Bài 39
Tự viết UART Driver
Bài 32 tôi đặc biệt muốn dạy bạn về Input Pull-up/Pull-down và cách STM32F103 dùng ODR để chọn Pull-up hay Pull-down. Đây là một chỗ rất thú vị vì nó khiến nhiều người mới học STM32 bị nhầm: “ODR là thanh ghi output, tại sao lại liên quan đến INPUT?”