BÀI 20 – GPIO STM32F103: TỪ REGISTER ĐẾN CHƯƠNG TRÌNH C THỰC TẾ
Ở Bài 18, chúng ta đã bắt đầu đi vào GPIO.
Ở Bài 19, ta tiếp tục hiểu cách CPU tương tác với GPIO thông qua thanh ghi.
Bây giờ Bài 20 sẽ là bài rất quan trọng:
Tự cấu hình GPIO STM32F103 bằng cách thao tác trực tiếp vào REGISTER — không dùng HAL, không dùng CubeMX.
Đây chính là bước chuyển từ “biết C” → “lập trình được STM32 ở mức thanh ghi”.
1. Trước tiên: GPIO thực chất là gì?
Ví dụ ta muốn điều khiển LED trên chân:
PC13
Ta có:
CPU
│
│ ghi dữ liệu
▼
GPIOC REGISTER
│
▼
PC13
│
▼
LED
CPU không biết khái niệm:
digitalWrite(PC13, HIGH);
Đó là cách Arduino che giấu phần cứng.
STM32 thực sự làm việc kiểu:
GPIOC->ODR |= ...
hoặc thậm chí thấp hơn:
*(volatile uint32_t *)0x4001100C |= ...;
2. GPIOC nằm ở địa chỉ nào?
Từ Memory Map ở Bài 17, chúng ta biết vùng Peripheral bắt đầu từ:
0x40000000
GPIO của STM32F103 nằm trong vùng APB2.
Các GPIO có địa chỉ cơ sở:
GPIOA = 0x40010800
GPIOB = 0x40010C00
GPIOC = 0x40011000
GPIOD = 0x40011400
GPIOE = 0x40011800
Điều rất đáng chú ý:
GPIOA
0x40010800
GPIOB
0x40010C00
GPIOC
0x40011000
Mỗi GPIO cách nhau:
0x400
tức:
1024 bytes
3. Bên trong GPIOC có những REGISTER nào?
Ví dụ GPIOC:
GPIOC = 0x40011000
Các thanh ghi quan trọng:
| Offset | Register | Chức năng |
|---|---|---|
| +0x00 | CRL | cấu hình PC0–PC7 |
| +0x04 | CRH | cấu hình PC8–PC15 |
| +0x08 | IDR | đọc trạng thái chân |
| +0x0C | ODR | xuất dữ liệu |
| +0x10 | BSRR | Set/Reset GPIO |
| +0x14 | BRR | Reset GPIO |
| +0x18 | LCKR | khóa cấu hình |
Do đó:
GPIOC base
0x40011000
suy ra:
CRL = 0x40011000
CRH = 0x40011004
IDR = 0x40011008
ODR = 0x4001100C
BSRR = 0x40011010
BRR = 0x40011014
4. PC13 dùng CRH chứ không dùng CRL
Đây là kiến thức cực kỳ quan trọng.
GPIO được chia thành:
CRL → Pin 0 → Pin 7
CRH → Pin 8 → Pin 15
Vì:
PC13
nằm trong:
PC8 → PC15
nên PC13 phải cấu hình bằng:
GPIOC->CRH
Không phải:
GPIOC->CRL
5. Mỗi GPIO chiếm 4 bit cấu hình
Đây là chỗ rất nhiều người mới học STM32 bị rối.
Trong CRH:
PC8 → 4 bit
PC9 → 4 bit
PC10 → 4 bit
PC11 → 4 bit
PC12 → 4 bit
PC13 → 4 bit
PC14 → 4 bit
PC15 → 4 bit
Tổng cộng:
8 × 4 = 32 bit
Do đó:
CRH
31 0
┌────┬────┬────┬────┬────┬────┬────┬────┐
│15 │14 │13 │12 │11 │10 │9 │8 │
└────┴────┴────┴────┴────┴────┴────┴────┘
Mỗi pin có:
CNF[1:0]
MODE[1:0]
6. PC13 nằm ở bit nào?
PC13 là pin số 13.
Trong CRH:
pin 8 → bit 0–3
pin 9 → bit 4–7
pin 10 → bit 8–11
pin 11 → bit 12–15
pin 12 → bit 16–19
pin 13 → bit 20–23
pin 14 → bit 24–27
pin 15 → bit 28–31
Vậy:
PC13 → bit 20–23
Trong 4 bit này:
bit 20–21 = MODE13
bit 22–23 = CNF13
7. MODE13 nghĩa là gì?
Hai bit MODE:
MODE1 MODE0
quy định tốc độ/output.
Các giá trị:
00 = Input mode
01 = Output 10 MHz
10 = Output 2 MHz
11 = Output 50 MHz
Ví dụ:
MODE13 = 01
nghĩa là:
PC13 = Output 10 MHz
8. CNF13 nghĩa là gì?
CNF xác định loại input/output.
Đối với output:
CNF = 00
là:
General purpose push-pull
Tức là GPIO thông thường:
HIGH → gần VCC
LOW → gần GND
9. Muốn PC13 là Output Push-Pull 10 MHz
Ta cần:
MODE13 = 01
CNF13 = 00
Do đó 4 bit của PC13 là:
CNF MODE
00 01
hay:
0001
Bây giờ phải đặt 0001 vào:
bit 20–23
10. Dùng SHIFT để đưa 0001 vào đúng vị trí
Ta viết:
1 << 20
Nó tạo ra:
00000000 00010000 00000000 00000000
Tức:
bit 20 = 1
và:
bit 21 = 0
bit 22 = 0
bit 23 = 0
Đúng chính xác:
MODE13 = 01
CNF13 = 00
11. Nhưng trước khi cấu hình phải xóa 4 bit cũ
Giả sử CRH hiện tại:
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Ta không được đơn giản:
GPIOC->CRH |= (1 << 20);
bởi vì 3 bit còn lại có thể đang chứa giá trị cũ.
Ta phải:
Bước 1
Xóa bit 20–23.
Mask:
0xF << 20
Sau đó:
GPIOC->CRH &= ~(0xF << 20);
Ý nghĩa:
AND
NOT
MASK
12. Bước 2: ghi cấu hình mới
Sau khi xóa:
GPIOC->CRH &= ~(0xF << 20);
ta ghi:
GPIOC->CRH |= (1 << 20);
Kết quả:
PC13 = Output Push-Pull 10 MHz
13. Nhưng có một vấn đề lớn
Bạn có thể hỏi:
GPIOC hoạt động rồi, vậy tại sao đôi khi viết register lại không có tác dụng?
Vì GPIOC cũng phải được cấp clock.
STM32F103 có hệ thống clock cho peripheral.
GPIOC thuộc:
APB2
và clock của GPIOC được điều khiển bởi:
RCC
14. RCC nằm ở đâu?
RCC base:
0x40021000
Một register quan trọng:
RCC_APB2ENR
địa chỉ:
0x40021018
Register này chứa các bit enable clock cho các peripheral APB2.
15. Bit GPIOC nằm ở đâu?
Trong RCC_APB2ENR:
IOPAEN → GPIOA
IOPBEN → GPIOB
IOPCEN → GPIOC
IOPDEN → GPIOD
IOPEEN → GPIOE
GPIOC là:
IOPCEN = bit 4
Do đó:
RCC->APB2ENR |= (1 << 4);
16. Đây chính là quy trình STM32 thực sự
Muốn sử dụng PC13:
① Enable clock GPIOC
↓
② Configure PC13
↓
③ Write output
Cụ thể:
RCC->APB2ENR |= (1 << 4);
GPIOC->CRH &= ~(0xF << 20);
GPIOC->CRH |= (0x1 << 20);
Sau đó:
GPIOC->ODR |= (1 << 13);
hoặc:
GPIOC->ODR &= ~(1 << 13);
17. Đây là điều bạn cần hiểu thật sâu
Khi viết:
GPIOC->ODR |= (1 << 13);
CPU thực chất đang làm:
CPU
│
│ write
▼
0x4001100C
│
│ bit 13
▼
GPIOC
│
▼
PC13
Nó không phải phép thuật.
Nó chỉ là:
CPU ghi một bit vào một địa chỉ bộ nhớ đặc biệt.
Đây chính là lý do chúng ta học:
Memory Map
↓
Address
↓
Register
↓
Bit
↓
GPIO
18. Vậy GPIOC->ODR thực chất là gì?
Trong CMSIS, GPIOC thường được định nghĩa dựa trên địa chỉ:
#define GPIOC ((GPIO_TypeDef *)0x40011000)
Và cấu trúc:
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;
Ta có:
GPIOC
↓
0x40011000
ODR là thành viên thứ 4:
offset = 0x0C
nên:
GPIOC->ODR
tương đương:
0x40011000 + 0x0C
=
0x4001100C
19. Đây chính là lý do phải học struct
Bạn đã học:
Bài 7 → struct
Bài 8 → function
Bài 9 → struct nâng cao
Bài 10 → enum / typedef
...
Bài 16 → bit / mask / shift
Bài 17 → Memory Map
Bài 18 → GPIO
Bài 19 → Register
Bài 20 → ghép tất cả lại
Bây giờ bạn bắt đầu thấy tại sao C lại có struct.
Ví dụ:
GPIOC->CRH
GPIOC->IDR
GPIOC->ODR
GPIOC->BSRR
thực chất là cách C giúp chúng ta mô tả một khối register phần cứng.
20. Viết chương trình PC13 bằng Register
Một phiên bản đơn giản:
#include "stm32f10x.h"
int main(void)
{
// Enable GPIOC clock
RCC->APB2ENR |= (1 << 4);
// PC13 = Output Push-Pull, 10 MHz
GPIOC->CRH &= ~(0xF << 20);
GPIOC->CRH |= (0x1 << 20);
while (1)
{
GPIOC->ODR &= ~(1 << 13);
}
}
Với Blue Pill, PC13 thường nối với LED onboard theo kiểu active-low.
Vì vậy:
GPIOC->ODR &= ~(1 << 13);
thường làm LED:
ON
còn:
GPIOC->ODR |= (1 << 13);
thường:
OFF
21. Muốn LED nhấp nháy thì sao?
Ta cần delay.
Ví dụ:
GPIOC->ODR &= ~(1 << 13);
for (volatile uint32_t i = 0; i < 500000; i++);
GPIOC->ODR |= (1 << 13);
for (volatile uint32_t i = 0; i < 500000; i++);
Đặt trong:
while (1)
thì:
ON
↓
delay
↓
OFF
↓
delay
↓
ON
↓
...
22. Nhưng có cách tốt hơn ODR
STM32F1 có:
BSRR
Bit:
0–15 → SET
16–31 → RESET
Ví dụ PC13:
Set PC13
GPIOC->BSRR = (1 << 13);
Reset PC13
GPIOC->BSRR = (1 << (13 + 16));
hay:
GPIOC->BSRR = (1 << 29);
23. Tại sao BSRR rất hay?
Giả sử:
GPIOC->ODR |= (1 << 13);
đây là kiểu:
READ
↓
MODIFY
↓
WRITE
Trong một số tình huống nhiều code/interrupt cùng tác động GPIO, kiểu read-modify-write có thể gây vấn đề.
BSRR cho phép:
ghi trực tiếp SET hoặc RESET
Ví dụ:
GPIOC->BSRR = (1 << 13);
không cần đọc ODR trước.
Đây là một trong những lý do BSRR rất quan trọng trong lập trình STM32.
24. Một bài tập cực kỳ quan trọng
Bạn hãy tự tính:
Câu 1
PC13 nằm ở:
CRL hay CRH?
Câu 2
PC13 chiếm:
bit nào đến bit nào?
Câu 3
Muốn PC13:
Output Push-Pull 10 MHz
thì:
MODE = ?
CNF = ?
Câu 4
GPIOC clock nằm ở:
RCC_APB2ENR
bit số mấy?
Câu 5
Địa chỉ:
GPIOC->ODR
là bao nhiêu nếu:
GPIOC = 0x40011000
ODR offset = 0x0C
25. Bài tập thực hành với Blue Pill của bạn
Bạn có thể làm đúng bài này trên STM32 Blue Pill mà không cần CubeMX/HAL.
Mục tiêu:
PC13
↓
LED onboard
↓
ON 1 giây
↓
OFF 1 giây
↓
lặp lại
Nhưng trước khi chạy, hãy thử tự viết 3 dòng quan trọng nhất:
// 1. Enable clock GPIOC
// 2. Configure PC13
// 3. Điều khiển PC13
Đừng nhìn đáp án ngay.
26. Kiến thức quan trọng nhất của Bài 20
Bạn hãy ghi nhớ chuỗi này:
RCC
↓
Enable Clock
↓
GPIO Register
↓
CRL / CRH
↓
MODE + CNF
↓
IDR / ODR / BSRR
↓
GPIO PIN
Hay nói theo ngôn ngữ lập trình:
Address
↓
Pointer
↓
Struct
↓
Register
↓
Bit
↓
Hardware
Đây chính là nền tảng để sau này bạn có thể tự đọc Reference Manual của STM32 mà không phụ thuộc hoàn toàn vào CubeMX/HAL.
🔜 BÀI 21
Bài tiếp theo tôi đề xuất đi đúng theo lộ trình của chúng ta:
BÀI 21 – GPIO INPUT: ĐỌC NÚT NHẤN BẰNG IDR, PULL-UP/PULL-DOWN, CNF, VÀ TẠI SAO NÚT NHẤN BỊ “NHIỄU”
Ở bài đó chúng ta sẽ lấy một nút nhấn thật + LED PC13 trên chính Blue Pill của bạn, rồi tự cấu hình:
Nút nhấn
↓
GPIO INPUT
↓
IDR
↓
if()
↓
GPIO OUTPUT
↓
LED
và sau đó tôi sẽ chỉ cho bạn vì sao phải debounce nút nhấn — đây là lúc kiến thức C + GPIO + register bắt đầu biến thành một chương trình nhúng thực sự.