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 17 – MEMORY MAP STM32F103 FLASH, SRAM, PERIPHERAL, REGISTER VÀ ĐỊA CHỈ 0x40000000 THỰC SỰ CÓ Ý NGHĨA GÌ?

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

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ỉ 0x40000000 lạ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ó.

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