BÀI 17 – MEMORY MAP STM32F103
FLASH, SRAM, PERIPHERAL, REGISTER VÀ ĐỊA CHỈ 0x40000000 THỰC SỰ CÓ Ý NGHĨA GÌ?
Bài này cực kỳ quan trọng. Nếu Bài 16 giúp bạn hiểu:
bit,byte,mask,&,|,^,~,<<,>>
thì Bài 17 sẽ nối chúng với phần cứng thật của STM32.
Sau bài này, bạn sẽ bắt đầu hiểu tại sao viết:
GPIOA->ODR |= (1 << 5);
lại có thể thực sự làm một chân GPIO trên con chip đổi trạng thái.
1. Trước hết: MEMORY MAP là gì?
Hãy tưởng tượng STM32 có một “bản đồ địa chỉ”.
Mỗi vùng trong con chip được gán một khoảng địa chỉ.
Ví dụ đơn giản:
Địa chỉ thấp
│
▼
0x00000000
│
│ FLASH
│ Chương trình
│
0x08000000
│
│ SRAM
│ Biến, stack...
│
0x20000000
│
│ PERIPHERAL
│ GPIO, TIMER, USART, ADC...
│
0x40000000
│
▼
Địa chỉ cao hơn
Đây gọi là memory map.
Điều cực kỳ quan trọng:
Không phải tất cả những địa chỉ này đều là “RAM”.
Một địa chỉ có thể đại diện cho:
- ô nhớ Flash
- ô nhớ SRAM
- thanh ghi Peripheral
- vùng System Memory
- vùng ngoại vi khác
CPU nhìn tất cả chúng dưới dạng địa chỉ.
2. CPU thực sự nhìn thấy cái gì?
STM32F103C8T6 sử dụng lõi ARM Cortex-M3.
CPU không suy nghĩ kiểu:
“Đây là GPIOA.”
CPU chỉ thấy:
“Tôi cần đọc/ghi dữ liệu tại địa chỉ X.”
Ví dụ:
CPU
│
│ đọc/ghi địa chỉ
▼
0x20000000
0x08000000
0x4001080C
...
Phần cứng bên trong STM32 sẽ quyết định:
Địa chỉ nào
↓
được chuyển đến
↓
FLASH / SRAM / Peripheral
Đây chính là tư duy memory-mapped architecture.
3. Ba vùng quan trọng nhất
Với STM32F103, bạn cần nhớ trước tiên:
FLASH → 0x08000000
SRAM → 0x20000000
PERIPHERAL → 0x40000000
Đây là ba mốc cực kỳ quan trọng.
4. FLASH nằm ở 0x08000000
Chương trình C của bạn cuối cùng được biên dịch thành machine code.
Ví dụ:
int main(void)
{
while(1)
{
}
}
Sau khi compile:
C code
↓
Compiler
↓
Machine code
↓
.hex / .bin
↓
FLASH
Với STM32F103C8T6, vùng Flash bắt đầu tại:
0x08000000
Ví dụ:
0x08000000
0x08000004
0x08000008
...
CPU có thể lấy instruction từ Flash để thực thi.
5. SRAM bắt đầu tại 0x20000000
SRAM dùng cho dữ liệu khi chương trình đang chạy.
Ví dụ:
int counter = 123;
Biến counter cần một vùng nhớ.
Hoặc:
uint8_t buffer[100];
cũng cần SRAM.
Stack cũng nằm trong SRAM.
Ví dụ:
SRAM
0x20000000
│
├── biến
├── buffer
├── stack
└── dữ liệu runtime
Điều cần nhớ:
FLASH chủ yếu chứa chương trình và dữ liệu không thay đổi; SRAM chứa dữ liệu runtime.
6. Nhưng 0x40000000 mới là phần thú vị
Đây là câu hỏi quan trọng của bài:
Tại sao địa chỉ
0x40000000lại có ý nghĩa?
Vì đây là mốc đầu của vùng Peripheral trong memory map ARM Cortex-M.
Nói đơn giản:
0x40000000
│
├── GPIO
├── TIMER
├── USART
├── SPI
├── I2C
├── ADC
└── các peripheral khác
Nhưng cần hiểu chính xác hơn:
0x40000000 không phải bản thân GPIO.
Nó là địa chỉ bắt đầu của một vùng địa chỉ dành cho peripheral.
Các peripheral cụ thể nằm ở những offset khác nhau.
7. Hãy tưởng tượng một thành phố
Đây là cách rất dễ hiểu.
Hãy coi memory map là một thành phố:
Thành phố STM32
│
├── Khu FLASH
│
├── Khu SRAM
│
└── Khu PERIPHERAL
│
├── Nhà GPIOA
├── Nhà GPIOB
├── Nhà GPIOC
├── Nhà USART1
├── Nhà TIMER
└── Nhà ADC
0x40000000 giống như:
“Địa chỉ đầu đường của khu Peripheral.”
Còn GPIOA có địa chỉ riêng.
8. GPIOA nằm ở đâu?
Với STM32F103, GPIOA có base address:
GPIOA = 0x40010800
GPIOB:
GPIOB = 0x40010C00
GPIOC:
GPIOC = 0x40011000
Bạn thấy ngay:
Peripheral
0x40000000
│
│
│
GPIOA
0x40010800
Tức là:
GPIOA_BASE = 0x40010800
9. Nhưng GPIOA chỉ là một “khối”
GPIOA không chỉ có một thanh ghi.
Nó có nhiều register.
Ví dụ:
GPIOA
│
├── CRL
├── CRH
├── IDR
├── ODR
├── BSRR
├── BRR
└── LCKR
Mỗi register có một offset.
Ví dụ:
GPIOA_BASE = 0x40010800
Thanh ghi:
CRL = +0x00
CRH = +0x04
IDR = +0x08
ODR = +0x0C
BSRR = +0x10
BRR = +0x14
LCKR = +0x18
Do đó:
GPIOA->ODR
thực chất liên quan đến:
0x40010800 + 0x0C
=
0x4001080C
🔥 Đây chính là khoảnh khắc rất quan trọng.
10. GPIOA->ODR thực sự là gì?
Trong thư viện CMSIS, bạn thường thấy:
GPIOA->ODR
Có vẻ rất đơn giản.
Nhưng phía dưới nó chính là việc truy cập một địa chỉ phần cứng.
Về mặt tư duy:
GPIOA
↓
0x40010800
ODR offset
↓
0x0C
Địa chỉ ODR
↓
0x4001080C
Vậy:
GPIOA->ODR
về bản chất là:
đọc/ghi một vùng nhớ đặc biệt tại địa chỉ
0x4001080C.
Nhưng vùng nhớ đó không phải SRAM.
Nó là register của phần cứng GPIO.
11. Đây chính là “Memory-Mapped Register”
Tên rất quan trọng:
Memory-Mapped Register
Nghĩa là:
Thanh ghi phần cứng được ánh xạ vào không gian địa chỉ của CPU.
Ví dụ:
CPU
│
│ ghi 1
▼
0x4001080C
│
▼
GPIOA ODR
│
▼
mạch phần cứng GPIO
│
▼
PA5 thay đổi trạng thái
Đây là một trong những ý tưởng nền tảng nhất của lập trình Embedded.
12. Vậy tại sao viết GPIOA->ODR |= (1 << 5)?
Hãy phân tích từng bước.
GPIOA->ODR |= (1 << 5);
Trước tiên:
1 << 5
cho:
0010 0000
tức:
0x20
Sau đó:
GPIOA->ODR |= 0x20;
Thực chất gần giống:
GPIOA->ODR = GPIOA->ODR | 0x20;
CPU sẽ:
Bước 1
Đọc:
0x4001080C
Bước 2
Lấy giá trị ODR hiện tại.
Bước 3
OR với:
0000 0000 0010 0000
Bước 4
Ghi kết quả trở lại:
0x4001080C
Bước 5
Phần cứng GPIO nhận giá trị mới.
PA5 có thể chuyển HIGH.
13. Hãy nhìn toàn bộ chuỗi
Đây là chuỗi bạn cần thuộc lòng về mặt tư duy:
C code
↓
GPIOA->ODR
↓
Register
↓
Memory address
↓
0x4001080C
↓
Bus
↓
GPIO hardware
↓
PA5
🔥 Đây chính là cầu nối:
C → CPU → địa chỉ → register → phần cứng
14. Thế còn GPIOA->IDR?
IDR là Input Data Register.
Địa chỉ:
GPIOA_BASE = 0x40010800
IDR offset = 0x08
Do đó:
GPIOA_IDR
=
0x40010808
Nếu bạn viết:
if(GPIOA->IDR & (1 << 0))
{
// PA0 đang HIGH
}
CPU đọc:
0x40010808
Phần cứng GPIO trả về trạng thái chân PA0.
Ví dụ:
PA0 = HIGH
IDR:
0000 0000 0000 0001
↑
PA0
15. Một điều cực kỳ quan trọng: Register không phải RAM
Đây là lỗi người mới học rất dễ mắc.
Bạn thấy:
0x4001080C
và nghĩ:
“Đây là một ô nhớ.”
Đúng nhưng chưa đủ.
Nó là một địa chỉ trong không gian địa chỉ, nhưng phía sau địa chỉ đó là phần cứng GPIO, không phải một ô SRAM bình thường.
Ví dụ:
0x20000000
có thể là SRAM.
Trong khi:
0x4001080C
là GPIO register.
CPU đều dùng thao tác đọc/ghi địa chỉ, nhưng phần cứng phía sau hoàn toàn khác nhau.
16. Ví dụ cực kỳ trực quan
Giả sử:
uint32_t x;
x = 123;
x nằm trong SRAM.
Ví dụ tưởng tượng:
x
↓
0x20000010
↓
SRAM
CPU ghi:
123
vào SRAM.
Không có chân GPIO nào thay đổi.
Nhưng:
GPIOA->ODR = 123;
thì khác.
CPU ghi:
123
vào:
0x4001080C
và phần cứng GPIOA nhận giá trị đó.
Kết quả:
PA0
PA1
PA2
...
PA7
có thể thay đổi.
17. Đây là lý do Embedded C khác C trên PC
Trên PC:
int x = 10;
thường chỉ là dữ liệu trong bộ nhớ.
Nhưng trên MCU:
*(volatile uint32_t *)0x4001080C = 10;
có thể tác động trực tiếp đến phần cứng.
Đây là một trong những khác biệt quan trọng nhất giữa:
C thông thường
và:
Embedded C
18. Từ khóa volatile xuất hiện ở đây
Bạn sẽ thường thấy định nghĩa kiểu:
typedef struct
{
volatile uint32_t CRL;
volatile uint32_t CRH;
volatile uint32_t IDR;
volatile uint32_t ODR;
...
} GPIO_TypeDef;
Tại sao phải có:
volatile
?
Vì giá trị của register phần cứng có thể thay đổi không phải do code bình thường của CPU.
Ví dụ:
GPIOA->IDR
có thể thay đổi vì điện áp bên ngoài thay đổi trên chân PA0.
Compiler phải hiểu:
“Đừng tự ý tối ưu và cho rằng giá trị này luôn giữ nguyên.”
19. GPIO_TypeDef thực ra rất thông minh
Bạn có thể thấy:
GPIOA->ODR
và tưởng rằng đây là một chức năng đặc biệt của STM32.
Không.
Nó là C struct + địa chỉ.
Ví dụ khái niệm:
typedef struct
{
volatile uint32_t CRL;
volatile uint32_t CRH;
volatile uint32_t IDR;
volatile uint32_t ODR;
} GPIO_TypeDef;
Mỗi thành viên có vị trí:
CRL → +0x00
CRH → +0x04
IDR → +0x08
ODR → +0x0C
Sau đó:
#define GPIOA ((GPIO_TypeDef *)0x40010800)
Khi bạn viết:
GPIOA->ODR
C hiểu:
GPIOA
↓
con trỏ tới 0x40010800
↓
truy cập member ODR
↓
offset 0x0C
↓
0x4001080C
🔥 Đó chính là lý do Bài 13 về struct, typedef cực kỳ quan trọng.
20. Bây giờ nối Bài 13 → Bài 16 → Bài 17
Bạn đã học:
Bài 13
struct
typedef
enum
Bạn học cách mô tả dữ liệu có cấu trúc.
Bài 16
bit
byte
&
|
^
~
<<
>>
mask
Bạn học cách điều khiển từng bit.
Bài 17
Bạn ghép chúng lại:
struct
+
pointer
+
địa chỉ
+
bit manipulation
↓
Peripheral Register
↓
Hardware
Đây chính là nền móng của STM32 HAL/LL/CMSIS và lập trình register-level.
21. Một ví dụ cực kỳ quan trọng: bật clock GPIOA
Bạn sẽ thường gặp:
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
Thoạt nhìn khá đáng sợ.
Nhưng giờ bạn có thể phân tích:
RCC
↓
Peripheral
↓
register
↓
APB2ENR
↓
một địa chỉ cụ thể
↓
một bit cụ thể
Sau đó:
|=
được dùng để set bit.
Tức:
địa chỉ
+
register
+
bit
+
mask
đang điều khiển phần cứng.
22. Toàn bộ Peripheral Space
Đừng nghĩ chỉ có GPIO.
Trong vùng Peripheral còn có:
RCC
GPIO
AFIO
USART
SPI
I2C
ADC
TIM
DMA
EXTI
...
Ví dụ:
0x40000000
│
├── Peripheral
│
├── APB1
│ ├── TIM2
│ ├── USART2
│ ├── I2C1
│ └── ...
│
└── APB2
├── GPIOA
├── GPIOB
├── GPIOC
├── ADC
├── USART1
└── ...
23. APB và AHB là gì?
Đây là bước tiếp theo bạn sẽ học.
STM32 không nối tất cả peripheral trực tiếp vào CPU một cách đơn giản.
Có các bus nội bộ.
Ví dụ:
CPU
│
AHB
│
┌────────┴────────┐
│ │
SRAM Peripheral
│
┌─────┴─────┐
APB1 APB2
│ │
TIM2 GPIOA
USART2 GPIOB
I2C1 GPIOC
USART1
Bạn chưa cần học thuộc toàn bộ ngay.
Chỉ cần hiểu:
CPU giao tiếp với peripheral thông qua hệ thống bus nội bộ.
24. Vì sao GPIOA là 0x40010800 chứ không phải 0x40000000?
Đây là câu hỏi rất hay.
0x40000000 là mốc đầu vùng peripheral.
Nhưng bên trong vùng đó có rất nhiều peripheral.
Có thể hình dung:
0x40000000
│
│
├── vùng peripheral
│
│
├── ...
│
├── GPIOA
│ 0x40010800
│
├── GPIOB
│ 0x40010C00
│
└── GPIOC
0x40011000
Vì vậy:
0x40000000
không có nghĩa:
“GPIO nằm ở đây.”
Mà có nghĩa:
“Đây là điểm bắt đầu của vùng địa chỉ Peripheral.”
25. Còn 0x08000000 và 0x20000000?
Bạn nên ghi nhớ bảng này:
| Vùng | Địa chỉ bắt đầu | Ý nghĩa |
|---|---|---|
| FLASH | 0x08000000 |
Chương trình |
| SRAM | 0x20000000 |
Dữ liệu runtime |
| PERIPHERAL | 0x40000000 |
Thanh ghi phần cứng |
Đây là ba mốc địa chỉ quan trọng để bắt đầu học STM32.
26. Một câu hỏi rất hay: tại sao địa chỉ cách nhau lớn như vậy?
Bạn thấy:
FLASH:
0x08000000
SRAM:
0x20000000
PERIPHERAL:
0x40000000
Khoảng cách rất lớn.
Điều này không có nghĩa STM32F103 thực sự có hàng trăm MB RAM hay Flash.
Đây là không gian địa chỉ được quy hoạch.
Chỉ một phần trong đó thực sự được hiện thực bằng phần cứng.
Ví dụ:
Address Space
0x00000000 ───────────────
...
0x08000000 ── FLASH
...
0x20000000 ── SRAM
...
0x40000000 ── PERIPHERAL
...
27. Điều này giải thích một điều rất thú vị
Bạn có thể viết:
uint32_t *p = (uint32_t *)0x20000000;
và:
uint32_t *q = (uint32_t *)0x4001080C;
Hai cái đều là con trỏ.
Nhưng:
p
↓
SRAM
còn:
q
↓
GPIOA ODR
Một cái là bộ nhớ dữ liệu.
Một cái là phần cứng.
Đây chính là sức mạnh và cũng là sự nguy hiểm của C trong Embedded.
28. Nếu ghi nhầm địa chỉ thì sao?
Ví dụ:
*(volatile uint32_t *)0x4001080C = 0xFFFF;
Bạn vừa trực tiếp ghi vào GPIOA ODR.
Nếu địa chỉ sai:
*(volatile uint32_t *)0x12345678 = 123;
thì có thể:
- không có phần cứng phản hồi
- gây HardFault
- tạo hành vi không xác định
- hoặc phá hoạt động của hệ thống
Vì vậy lập trình register-level cần cực kỳ cẩn thận.
29. Một cách nhìn cực kỳ quan trọng
Từ giờ khi nhìn một dòng như:
GPIOA->ODR |= (1 << 5);
đừng chỉ đọc nó như một câu lệnh C.
Hãy đọc thành:
GPIOA
↓
base address
↓
register ODR
↓
địa chỉ 0x4001080C
↓
đọc register
↓
OR bit 5
↓
ghi register
↓
GPIO hardware
↓
PA5
Nếu bạn hiểu được chuỗi này, bạn đang thực sự bước vào thế giới Embedded C.
30. Bài tập tư duy
Không cần chạy code. Hãy tự trả lời.
Câu 1
Nếu:
GPIOA_BASE = 0x40010800
và:
ODR offset = 0x0C
thì địa chỉ ODR là bao nhiêu?
Câu 2
Nếu:
GPIOA->ODR |= (1 << 5);
thì bit nào được set?
Câu 3
Nếu:
GPIOA->ODR &= ~(1 << 5);
thì bit nào bị clear?
Câu 4
0x40000000 có phải địa chỉ của GPIOA không?
Câu 5
0x20000000 thuộc vùng nào?
Câu 6
0x08000000 thuộc vùng nào?
31. Có một “bí mật” rất quan trọng phía sau bài này
Đến đây bạn đã thấy:
GPIOA->ODR
thực chất dựa trên:
struct
+
typedef
+
pointer
+
address
+
offset
+
volatile
+
bit manipulation
Tức là những thứ bạn đã học từ Bài 1 → Bài 17 bắt đầu ghép thành một hệ thống hoàn chỉnh.
Và đây chính là lý do tôi không muốn bạn chỉ học thuộc:
HAL_GPIO_WritePin(...)
mà không hiểu bên dưới nó có gì.
32. Sơ đồ tổng kết Bài 17
Hãy lưu sơ đồ này:
STM32F103
│
▼
CPU Cortex-M3
│
Address Bus
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
FLASH SRAM PERIPHERAL
0x08000000 0x20000000 0x40000000
│ │ │
│ │ ┌──────┼───────┐
│ │ │ │ │
│ │ GPIO USART TIMER
│ │ │
│ │ GPIOA
│ │ 0x40010800
│ │ │
│ │ ├── CRL +0x00
│ │ ├── CRH +0x04
│ │ ├── IDR +0x08
│ │ ├── ODR +0x0C
│ │ └── ...
│ │ │
│ │ ▼
│ │ 0x4001080C
│ │ │
│ │ GPIOA ODR
│ │ │
│ │ ▼
│ │ PA5
│ │
│ └── variables / stack
│
└── machine code
🔑 5 điều bạn cần nhớ sau Bài 17
1.
0x08000000 → FLASH
2.
0x20000000 → SRAM
3.
0x40000000 → vùng Peripheral
4.
GPIOA = 0x40010800
và:
GPIOA ODR = 0x4001080C
5. Quan trọng nhất:
Peripheral register là các thanh ghi phần cứng được ánh xạ vào không gian địa chỉ của CPU.
Vì vậy:
GPIOA->ODR
không phải “một biến C bình thường”.
Nó là cánh cửa từ code C đi thẳng tới phần cứng GPIO.
🔜 BÀI 18 – PHẦN QUAN TRỌNG NHẤT SAU MEMORY MAP
Bài tiếp theo chúng ta nên đi thẳng vào:
BÀI 18 – POINTER TRONG STM32: *, &, CAST ĐỊA CHỈ VÀ TẠI SAO GPIOA = (GPIO_TypeDef *)0x40010800 LẠI HOẠT ĐỘNG?
Ở bài đó tôi sẽ giải thích từ con số 0:
uint32_t x = 10;
uint32_t *p = &x;
*p = 20;
rồi từ từ biến nó thành:
*(volatile uint32_t *)0x4001080C
và cuối cùng quay lại:
GPIOA->ODR
Khi hiểu được bài đó, bạn sẽ thực sự hiểu GPIOA->ODR, chứ không còn chỉ biết sử dụng nó.