BÀI 30 – TỰ VIẾT GPIO DRIVER CHO STM32F103
Hôm nay chúng ta ghép toàn bộ kiến thức từ Bài 22 → Bài 29 để tự xây dựng một GPIO driver đơn giản cho STM32F103C8T6, hoàn toàn bằng thanh ghi, không dùng HAL, không dùng Arduino pinMode() / digitalWrite().
Đây là một bước rất quan trọng.
Từ bài này, bạn bắt đầu chuyển từ:
“Tôi biết thanh ghi GPIO là gì”
sang:
“Tôi có thể tự viết phần mềm điều khiển GPIO.”
1. Trước tiên: chúng ta đã biết những gì?
Đến Bài 29, chúng ta đã biết:
CRL / CRH → cấu hình GPIO
IDR → đọc GPIO
ODR → trạng thái Output
BSRR → SET / RESET GPIO
BRR → RESET GPIO
Bây giờ chúng ta muốn biến chúng thành các hàm:
GPIO_Init()
GPIO_Set()
GPIO_Reset()
GPIO_Toggle()
GPIO_Read()
Sau đó chương trình chính chỉ cần gọi:
GPIO_Set(GPIOC, 13);
thay vì phải trực tiếp thao tác thanh ghi mỗi lần.
2. GPIO Driver là gì?
Hiểu đơn giản:
Ứng dụng
│
│ GPIO_Set()
│ GPIO_Reset()
│ GPIO_Read()
↓
GPIO Driver
│
│ thao tác thanh ghi
↓
GPIOA / GPIOB / GPIOC
│
↓
Phần cứng STM32
Ví dụ chương trình ứng dụng muốn:
Bật LED
không nhất thiết phải biết:
BSRR bit nào?
CRH phải cấu hình thế nào?
GPIOC nằm ở địa chỉ nào?
Driver sẽ lo toàn bộ việc đó.
3. Tạo kiểu dữ liệu cho GPIO Mode
Chúng ta bắt đầu bằng một enum.
typedef enum
{
GPIO_INPUT = 0,
GPIO_OUTPUT = 1
} GPIO_Mode_t;
Bây giờ thay vì viết:
GPIO_Init(GPIOC, 13, 1);
ta có thể viết:
GPIO_Init(GPIOC, 13, GPIO_OUTPUT);
Dễ đọc hơn rất nhiều.
4. Tạo hàm GPIO_Set()
Từ Bài 28:
void GPIO_Set(GPIO_TypeDef *GPIOx, uint8_t pin)
{
GPIOx->BSRR = (1 << pin);
}
Ví dụ:
GPIO_Set(GPIOC, 13);
STM32 thực hiện:
GPIOC
↓
BSRR
↓
bit 13 = 1
↓
PC13 HIGH
5. Tạo hàm GPIO_Reset()
Có thể sử dụng BRR:
void GPIO_Reset(GPIO_TypeDef *GPIOx, uint8_t pin)
{
GPIOx->BRR = (1 << pin);
}
Ví dụ:
GPIO_Reset(GPIOC, 13);
→ PC13 LOW.
6. Tạo hàm GPIO_Toggle()
Đây là hàm rất quan trọng.
Toggle nghĩa là:
HIGH → LOW
LOW → HIGH
Ta cần biết trạng thái hiện tại.
Đọc ODR:
GPIOx->ODR
Kiểm tra bit:
GPIOx->ODR & (1 << pin)
Nếu kết quả khác 0:
GPIO = HIGH
Nếu bằng 0:
GPIO = LOW
Vậy:
void GPIO_Toggle(GPIO_TypeDef *GPIOx, uint8_t pin)
{
if (GPIOx->ODR & (1 << pin))
{
GPIO_Reset(GPIOx, pin);
}
else
{
GPIO_Set(GPIOx, pin);
}
}
Đây chính là một GPIO Toggle driver cơ bản.
7. Nhưng có một vấn đề với Toggle
Hãy nhìn:
if (GPIOx->ODR & (1 << pin))
{
GPIO_Reset(GPIOx, pin);
}
else
{
GPIO_Set(GPIOx, pin);
}
Nó thực hiện:
Đọc ODR
↓
Kiểm tra bit
↓
Quyết định
↓
SET hoặc RESET
Trong một chương trình đơn giản thì hoàn toàn ổn.
Nhưng trong chương trình phức tạp có interrupt hoặc nhiều tác nhân điều khiển GPIO, thao tác Toggle cần được thiết kế cẩn thận.
Ở bài này chúng ta tập trung vào hiểu bản chất, chưa cần tối ưu hóa mọi trường hợp.
8. Tạo hàm GPIO_Read()
Bây giờ đọc GPIO Input.
uint8_t GPIO_Read(GPIO_TypeDef *GPIOx, uint8_t pin)
{
return (GPIOx->IDR & (1 << pin)) ? 1 : 0;
}
Ví dụ:
uint8_t state;
state = GPIO_Read(GPIOA, 0);
Nếu PA0:
HIGH → state = 1
LOW → state = 0
9. Bây giờ đến phần khó nhất: GPIO_Init()
Chúng ta cần cấu hình:
PA0
PA1
PC13
...
thành Input hoặc Output.
STM32F103 có:
GPIOx->CRL
GPIOx->CRH
10. CRL và CRH
Đây là kiến thức từ các bài GPIO trước.
CRL
Dùng cho:
Pin 0 → Pin 7
CRH
Dùng cho:
Pin 8 → Pin 15
Mỗi GPIO pin sử dụng:
4 bit
Ví dụ:
PA0 → CRL bits 0..3
PA1 → CRL bits 4..7
PA2 → CRL bits 8..11
...
Công thức:
position = pin × 4
Nhưng với pin >= 8:
position = (pin - 8) × 4
và sử dụng CRH.
11. GPIO Output cơ bản
Với STM32F103, GPIO Output có cấu hình dạng:
MODE
CNF
Ví dụ Output Push-Pull 2 MHz:
MODE = 10
CNF = 00
Ghép thành:
0010
hay:
0x2
Đây là cấu hình:
Output Push-Pull, 2 MHz
12. Tại sao phải xóa 4 bit trước?
Giả sử muốn cấu hình PA5.
Không nên đơn giản:
GPIOA->CRL |= ...
vì 4 bit cũ có thể vẫn còn.
Ta phải:
Xóa 4 bit cũ
↓
Ghi cấu hình mới
Mask:
(0xF << position)
Ví dụ PA5:
position = 5 × 4
= 20
Mask:
0xF << 20
Sau đó:
GPIOA->CRL &= ~(0xF << 20);
→ xóa cấu hình cũ của PA5.
13. Sau đó ghi cấu hình mới
Output Push-Pull 2 MHz:
GPIOA->CRL |= (0x2 << 20);
Vậy toàn bộ:
GPIOA->CRL &= ~(0xF << 20);
GPIOA->CRL |= (0x2 << 20);
PA5 trở thành:
Output
Push-Pull
2 MHz
14. Viết GPIO_Init() cho Output
Ta có thể bắt đầu bằng phiên bản đơn giản:
void GPIO_Init_Output(GPIO_TypeDef *GPIOx, uint8_t pin)
{
uint8_t position;
if (pin < 8)
{
position = pin * 4;
GPIOx->CRL &= ~(0xF << position);
GPIOx->CRL |= (0x2 << position);
}
else
{
position = (pin - 8) * 4;
GPIOx->CRH &= ~(0xF << position);
GPIOx->CRH |= (0x2 << position);
}
}
Đây là một driver thực sự đang thao tác trực tiếp vào thanh ghi.
15. Hãy phân tích hàm này
Ví dụ:
GPIO_Init_Output(GPIOC, 13);
Vì:
13 >= 8
nên:
position = (13 - 8) * 4;
Kết quả:
position = 20
Sau đó:
GPIOC->CRH &= ~(0xF << 20);
Xóa:
CRH bits 20..23
rồi:
GPIOC->CRH |= (0x2 << 20);
ghi:
0010
vào 4 bit cấu hình PC13.
Kết quả:
PC13 = Output Push-Pull 2 MHz
16. Bây giờ tạo chương trình hoàn chỉnh
Một chương trình tối giản có thể có cấu trúc:
#include "stm32f10x.h"
void GPIO_Init_Output(GPIO_TypeDef *GPIOx, uint8_t pin)
{
uint8_t position;
if (pin < 8)
{
position = pin * 4;
GPIOx->CRL &= ~(0xF << position);
GPIOx->CRL |= (0x2 << position);
}
else
{
position = (pin - 8) * 4;
GPIOx->CRH &= ~(0xF << position);
GPIOx->CRH |= (0x2 << position);
}
}
void GPIO_Set(GPIO_TypeDef *GPIOx, uint8_t pin)
{
GPIOx->BSRR = (1 << pin);
}
void GPIO_Reset(GPIO_TypeDef *GPIOx, uint8_t pin)
{
GPIOx->BRR = (1 << pin);
}
void GPIO_Toggle(GPIO_TypeDef *GPIOx, uint8_t pin)
{
if (GPIOx->ODR & (1 << pin))
{
GPIO_Reset(GPIOx, pin);
}
else
{
GPIO_Set(GPIOx, pin);
}
}
int main(void)
{
GPIO_Init_Output(GPIOC, 13);
while (1)
{
GPIO_Toggle(GPIOC, 13);
for (volatile uint32_t i = 0; i < 500000; i++);
}
}
17. Chương trình này thực sự làm gì?
Khi MCU khởi động:
main()
↓
GPIO_Init_Output(GPIOC, 13)
↓
PC13 được cấu hình Output
Sau đó:
while(1)
chạy liên tục:
GPIO_Toggle()
↓
PC13 HIGH/LOW
↓
delay
↓
GPIO_Toggle()
↓
PC13 LOW/HIGH
LED Blue Pill sẽ nhấp nháy.
Lưu ý: vì LED onboard Blue Pill thường active-low nên chiều sáng/tắt sẽ ngược với mức HIGH/LOW của PC13.
18. Nhưng chúng ta còn thiếu một thứ cực kỳ quan trọng
Đó là:
CLOCK
Bạn không thể cứ viết:
GPIOC->CRH
mà bỏ qua clock của GPIOC.
Trước khi sử dụng GPIOC, phải cấp clock cho GPIOC.
Với STM32F103:
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
Hoặc hiểu theo bit:
RCC->APB2ENR |= (1 << 4);
19. Tại sao bit 4?
Thanh ghi:
RCC->APB2ENR
có các bit enable peripheral.
Đối với GPIO:
IOPAEN → bit 2
IOPBEN → bit 3
IOPCEN → bit 4
IOPDEN → bit 5
IOPEEN → bit 6
Vì vậy:
RCC->APB2ENR |= (1 << 4);
có nghĩa:
Bật clock cho GPIOC.
20. Đây chính là kiến trúc MCU mà bạn đang học
Bạn đã học Memory Map ở Bài 17.
Bây giờ hãy kết nối kiến thức:
CPU
│
├── RCC
│ └── bật clock GPIOC
│
└── GPIOC
├── CRH
├── IDR
├── ODR
├── BSRR
└── BRR
Nếu RCC chưa cấp clock:
GPIOC
↓
không hoạt động đúng
Đây là lý do khi lập trình thanh ghi, bạn phải hiểu hệ thống, chứ không chỉ nhớ tên thanh ghi.
21. Phiên bản hoàn chỉnh hơn
Chúng ta sửa chương trình:
#include "stm32f10x.h"
void GPIO_Init_Output(GPIO_TypeDef *GPIOx, uint8_t pin)
{
uint8_t position;
if (pin < 8)
{
position = pin * 4;
GPIOx->CRL &= ~(0xF << position);
GPIOx->CRL |= (0x2 << position);
}
else
{
position = (pin - 8) * 4;
GPIOx->CRH &= ~(0xF << position);
GPIOx->CRH |= (0x2 << position);
}
}
void GPIO_Set(GPIO_TypeDef *GPIOx, uint8_t pin)
{
GPIOx->BSRR = (1 << pin);
}
void GPIO_Reset(GPIO_TypeDef *GPIOx, uint8_t pin)
{
GPIOx->BRR = (1 << pin);
}
void GPIO_Toggle(GPIO_TypeDef *GPIOx, uint8_t pin)
{
if (GPIOx->ODR & (1 << pin))
{
GPIO_Reset(GPIOx, pin);
}
else
{
GPIO_Set(GPIOx, pin);
}
}
int main(void)
{
/* Enable GPIOC clock */
RCC->APB2ENR |= (1 << 4);
/* PC13 -> Output Push-Pull 2 MHz */
GPIO_Init_Output(GPIOC, 13);
while (1)
{
GPIO_Toggle(GPIOC, 13);
for (volatile uint32_t i = 0; i < 500000; i++);
}
}
22. Bây giờ bạn đã tự xây dựng được gì?
Không dùng:
HAL_GPIO_WritePin()
HAL_GPIO_TogglePin()
HAL_GPIO_Init()
Không dùng:
pinMode()
digitalWrite()
digitalRead()
Mà tự điều khiển:
RCC
↓
GPIO CRL/CRH
↓
GPIO BSRR/BRR
↓
GPIO ODR
Đây chính là bare-metal programming ở mức cơ bản.
23. Một điều cực kỳ quan trọng: GPIO_TypeDef
Bạn có thể thắc mắc:
GPIO_TypeDef *GPIOxtừ đâu ra?
Trong CMSIS/device header của STM32F1, cấu trúc GPIO được mô tả đại khái như:
typedef struct
{
volatile uint32_t CRL;
volatile uint32_t CRH;
volatile uint32_t IDR;
volatile uint32_t ODR;
volatile uint32_t BSRR;
volatile uint32_t BRR;
volatile uint32_t LCKR;
} GPIO_TypeDef;
Vì vậy:
GPIO_TypeDef *GPIOx
là con trỏ tới một vùng thanh ghi GPIO.
Ví dụ:
GPIOC
chính là một địa chỉ GPIO cụ thể được định nghĩa trong header.
24. Đây chính là sức mạnh của struct
Bạn đã học:
Bài 7 – Struct C
và:
Bài 13 – Struct / typedef / enum
Bây giờ những kiến thức C đó bắt đầu được sử dụng vào STM32 thực sự.
Ví dụ:
GPIOC->BSRR
về bản chất là:
GPIOC
↓
con trỏ tới struct GPIO
↓
BSRR
↓
thanh ghi phần cứng
Vậy những bài C trước đây không hề học riêng lẻ.
Chúng đang chuẩn bị cho phần này.
25. Đây cũng là lý do con trỏ cực kỳ quan trọng
Hãy nhìn:
GPIO_TypeDef *GPIOx
Đây là:
con trỏ tới một struct.
Khi viết:
GPIOx->BSRR
toán tử:
->
có nghĩa:
truy cập thành viên của struct thông qua con trỏ.
Đây chính là kiến thức bạn đã học ở Bài 5 – Con trỏ và Bài 7/13 – Struct/typedef.
Bây giờ nó xuất hiện trong firmware STM32.
26. Kiến trúc tư duy mới
Bạn nên bắt đầu nhìn STM32 theo tầng:
ỨNG DỤNG
│
↓
GPIO_Toggle()
│
↓
GPIO DRIVER
│
↓
GPIOx->BSRR
│
↓
REGISTER
│
↓
HARDWARE
Đây chính là cách tư duy của một embedded programmer.
27. Bài tập Bài 30
Bài tập 1
Tự viết:
GPIO_Init_Output(GPIOA, 0);
và xác định:
CRL bit nào?
4 bit nào?
giá trị MODE/CNF là bao nhiêu?
Bài tập 2
Tự tính cấu hình:
GPIO_Init_Output(GPIOC, 13);
Hãy trả lời:
PC13 nằm ở CRL hay CRH?
position = ?
4 bit cấu hình nằm ở bit nào?
giá trị 4 bit Output Push-Pull 2 MHz là bao nhiêu?
Bài tập 3
Viết:
GPIO_Set(GPIOA, 5);
sau đó xác định:
BSRR bit nào = 1?
Bài tập 4
Viết:
GPIO_Reset(GPIOA, 5);
bằng BRR.
Bài tập 5 – rất quan trọng
Không sử dụng GPIO_Toggle().
Hãy tự viết chương trình:
PC13 sáng
↓
delay
↓
PC13 tắt
↓
delay
↓
lặp lại
Chỉ sử dụng:
GPIO_Set()
GPIO_Reset()
28. Tổng kết Bài 30
Bạn vừa nối được rất nhiều kiến thức:
C
│
├── struct
├── typedef
├── enum
├── pointer
└── bitwise operator
│
↓
STM32
│
├── RCC
├── GPIO
├── CRL / CRH
├── IDR
├── ODR
├── BSRR
└── BRR
│
↓
GPIO DRIVER
│
├── GPIO_Init()
├── GPIO_Set()
├── GPIO_Reset()
├── GPIO_Toggle()
└── GPIO_Read()
Đây là một cột mốc rất quan trọng trong quá trình học STM32 của bạn.
Và có một điều tôi muốn bạn đặc biệt ghi nhớ:
Bạn không cần học thuộc lòng hàng trăm dòng code.
Thứ cần hiểu là chuỗi tư duy:
Muốn điều khiển chân GPIO
↓
GPIO cần clock
↓
RCC cấp clock
↓
Cấu hình CRL/CRH
↓
SET/RESET bằng BSRR/BRR
↓
Đọc bằng IDR
↓
Đóng gói thành Driver
Khi đã hiểu chuỗi này, bạn có thể tự đọc Reference Manual và viết driver cho rất nhiều peripheral khác của STM32.
Bài 31 nên chuyển sang một phần cực kỳ quan trọng:
GPIO Input thực chiến – đọc nút nhấn bằng
IDR, xử lý Pull-up/Pull-down và hiểu tại sao chân Input có thể “trôi” (floating).
Đây sẽ là bước tiếp theo để bạn điều khiển nút nhấn → LED hoàn toàn bằng thanh ghi.