Skip to content

Công nghệ 9 giây

“Website hướng dẫn, thử nghiệm và giải thích công nghệ theo cách dễ hiểu cho mọi người.”

Menu
  • Home
  • Công nghệ
  • STM32
  • ESP32
  • Altium Desinger
  • Kiến thức điện máy
Menu

BÀI 30 – TỰ VIẾT GPIO DRIVER CHO STM32F103

Posted on August 20, 2026August 23, 2026 by congnghe9s

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 *GPIOx từ đâ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.

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Recent Posts

  • Bạn có thể hướng dẫn tôi quy trình sửa chữa một mainboard laptop cũng như mainboard của máy tính để bàn không?
  • Có những mức nguồn DC nào trên một bo mạch mainboard laptop?Kỹ thuật đốt dòng để kiểm tra nguồn laptop được thực hiện như thế nào?
  • bài tiếp theo hợp lý là: BÀI 63 – SENSOR FUSION THỰC CHIẾN: ENCODER + IMU MPU6050 + EKF + ODOMETRY X,Y,θ TRÊN STM32F103
  • BÀI 62 – EKF (EXTENDED KALMAN FILTER) CHO ROBOT 2 BÁNH: FUSION ENCODER + IMU + ƯỚC LƯỢNG X, Y, θ, V, ω
  • BÀI 61 – KALMAN FILTER CHO ENCODER + IMU: ƯỚC LƯỢNG TỐC ĐỘ, GÓC θ VÀ SENSOR FUSION

Recent Comments

No comments to show.

Archives

  • September 2026
  • August 2026
  • July 2026
  • June 2026
  • May 2026
  • February 2026

Categories

  • Altium Desinger
  • Công nghệ
  • ESP32
  • Khác
  • Kiến thức điện máy
  • STM32
Live 3D Globes Visitor
©2026 Công nghệ 9 giây | Design: Newspaperly WordPress Theme