Được bạn. Chúng ta tiếp tục đúng lộ trình nhé. 😄
🟦 STM32 – BÀI 2
ĐỊA CHỈ BỘ NHỚ, CON TRỎ *, TOÁN TỬ & VÀ REGISTER
Nếu Bài 1 giúp bạn hiểu:
“Biến được lưu trong RAM như thế nào?”
thì Bài 2 sẽ trả lời câu hỏi quan trọng hơn:
“Làm thế nào CPU biết phải đọc/ghi đúng ô nhớ nào?”
Và đây chính là cánh cửa để hiểu GPIO, ADC, Timer, UART, SPI, I2C của STM32.
1. Hãy tưởng tượng RAM là một dãy nhà
Hãy tưởng tượng RAM của STM32 giống như một dãy nhà.
Mỗi căn nhà có một địa chỉ:
Địa chỉ Dữ liệu
0x20000000 ?
0x20000001 ?
0x20000002 ?
0x20000003 ?
0x20000004 ?
...
CPU muốn lấy dữ liệu ở đâu thì phải biết:
Địa chỉ của dữ liệu đó.
Ví dụ:
0x20000000
đây là một địa chỉ bộ nhớ.
2. Biến có địa chỉ
Ví dụ:
int number = 100;
Bạn đã biết number chứa:
100
Nhưng nó cũng có một địa chỉ.
Ví dụ giả định:
number
│
▼
0x20000010
Ta có:
Tên biến Giá trị Địa chỉ
number 100 0x20000010
Đây là một khái niệm cực kỳ quan trọng.
3. Toán tử &
Trong C:
&number
có nghĩa:
Lấy địa chỉ của biến
number.
Ví dụ:
int number = 100;
Serial.println((uint32_t)&number, HEX);
Bạn có thể nhận được một địa chỉ kiểu:
20000010
Địa chỉ thực tế có thể khác.
Điều quan trọng không phải con số cụ thể.
Điều quan trọng là:
number
↓
giá trị
&number
↓
địa chỉ
4. Đây là lúc con trỏ xuất hiện
Ta viết:
int *ptr;
Đây là:
Một biến con trỏ dùng để lưu địa chỉ của một biến
int.
Ví dụ:
int number = 100;
int *ptr;
ptr = &number;
Bây giờ:
number
┌─────────────┐
│ 100 │
└─────────────┘
▲
│
│
ptr
ptr không chứa 100.
Nó chứa:
địa chỉ nơi
100đang được lưu.
5. Phân biệt cực kỳ quan trọng
Có hai thứ:
number
và
&number
Ví dụ:
number
↓
100
nhưng:
&number
↓
0x20000010
Do đó:
ptr = &number;
nghĩa là:
ptr = địa chỉ của number
6. Toán tử * khi dùng với con trỏ
Đây là phần dễ gây rối nhất lúc mới học C.
Ví dụ:
int number = 100;
int *ptr = &number;
Bây giờ:
*ptr
có nghĩa:
Lấy dữ liệu tại địa chỉ mà
ptrđang trỏ tới.
Vì ptr đang trỏ tới number, nên:
*ptr
chính là:
100
7. Hình dung như thế này
RAM
địa chỉ 0x20000010
│
▼
┌─────────┐
number ─────►│ 100 │
└─────────┘
▲
│
│
ptr
ptr chứa:
0x20000010
Còn:
*ptr
đọc dữ liệu tại:
0x20000010
và nhận:
100
8. *ptr không chỉ đọc được
Bạn còn có thể ghi:
*ptr = 500;
Khi đó:
number
sẽ trở thành:
500
Bởi vì:
ptr
↓
địa chỉ của number
↓
ghi 500 vào đó
Ví dụ hoàn chỉnh:
int number = 100;
int *ptr = &number;
*ptr = 500;
Kết quả:
number = 500
9. Đây chính là sức mạnh của con trỏ
Bạn có thể thao tác dữ liệu thông qua địa chỉ.
Ví dụ:
int a = 10;
int b = 20;
int *p;
p = &a;
*p = 100;
p = &b;
*p = 200;
Sau chương trình:
a = 100
b = 200
10. Nhưng STM32 dùng con trỏ ở đâu?
Đây mới là phần quan trọng.
STM32 có rất nhiều thanh ghi phần cứng – register.
Ví dụ:
GPIO
ADC
TIMER
USART
SPI
I2C
Mỗi peripheral có các thanh ghi nằm tại những địa chỉ bộ nhớ cụ thể.
Ví dụ tưởng tượng:
0x40010800 → GPIOA
0x40010C00 → GPIOB
...
Đây không phải RAM thông thường.
Đó là vùng địa chỉ mà CPU sử dụng để giao tiếp với phần cứng.
11. Đây được gọi là Memory-Mapped I/O
Tên này rất quan trọng:
Memory-Mapped I/O
Có thể hiểu đơn giản:
CPU
│
│ đọc/ghi địa chỉ
▼
Memory Map
│
├── RAM
├── Flash
└── Peripheral Registers
├── GPIO
├── ADC
├── TIMER
├── UART
└── SPI
Vì peripheral được ánh xạ vào không gian địa chỉ, CPU có thể dùng các lệnh đọc/ghi bộ nhớ để điều khiển phần cứng.
12. Ví dụ cực kỳ quan trọng
Giả sử có một register ở:
0x4001080C
Ta có thể tưởng tượng:
*(volatile uint32_t *)0x4001080C
Đây là một biểu thức rất quan trọng.
Hãy tách nó ra.
13. 0x4001080C là gì?
Đây là một số hexadecimal.
0x4001080C
được xem như một địa chỉ.
14. (uint32_t *)
Phần này nói:
Hãy xem địa chỉ đó như một con trỏ tới dữ liệu
uint32_t.
Tức:
(uint32_t *)0x4001080C
có thể hiểu là:
con trỏ
↓
địa chỉ 0x4001080C
↓
một vùng dữ liệu 32-bit
15. * phía trước
Tiếp theo:
*(volatile uint32_t *)0x4001080C
* có nghĩa:
Truy cập dữ liệu nằm tại địa chỉ đó.
Và nếu ghi:
*(volatile uint32_t *)0x4001080C = 1;
thì CPU sẽ thực hiện một thao tác ghi tới địa chỉ đó.
Nếu địa chỉ đó là register điều khiển GPIO, thao tác này có thể làm thay đổi trạng thái phần cứng.
16. volatile là gì?
Đây là từ khóa bạn sẽ gặp rất nhiều trong STM32.
Ví dụ:
volatile uint32_t register_value;
volatile nói với compiler đại ý rằng:
Giá trị này có thể thay đổi bởi thứ bên ngoài luồng code bình thường, vì vậy đừng tự ý tối ưu hóa việc đọc/ghi nó.
Điều này cực kỳ quan trọng đối với:
GPIO register
ADC register
Timer register
UART status register
Interrupt flag
17. Tại sao register phải volatile?
Ví dụ UART có một thanh ghi trạng thái.
Phần cứng có thể tự thay đổi:
CPU UART Hardware
│ │
│ │
│ ├── nhận dữ liệu
│ │
│ ▼
│ Status = READY
│ │
▼ │
đọc register ◄───────────┘
CPU không phải là thứ duy nhất thay đổi giá trị.
Phần cứng cũng có thể thay đổi.
Vì vậy compiler phải biết:
“Đừng giả định giá trị này luôn giống lần đọc trước.”
18. Đây chính là lý do bạn thấy những thứ như
Khi đọc code STM32 HAL/LL hoặc CMSIS, bạn sẽ gặp:
volatile uint32_t
hoặc các định nghĩa register rất dài.
Đừng sợ.
Bây giờ bạn đã biết nền tảng:
volatile
+
pointer
+
address
+
register
19. Ví dụ với GPIO
Giả sử chúng ta có:
GPIOA
GPIOA có các register.
Ví dụ khái niệm:
GPIOA
│
├── CRL
├── CRH
├── IDR
├── ODR
├── BSRR
├── BRR
└── LCKR
Bạn chưa cần học hết chúng ngay.
Hôm nay chỉ cần hiểu:
GPIO không phải là một hàm thần bí. GPIO cuối cùng vẫn được CPU điều khiển bằng cách đọc/ghi các register.
20. Ví dụ bật LED
Trên Blue Pill, LED onboard thường nối với:
PC13
Nếu dùng Arduino framework, bạn có thể viết:
digitalWrite(PC13, LOW);
Rất đơn giản.
Nhưng phía dưới abstraction đó, cuối cùng phần cứng vẫn phải thực hiện:
CPU
↓
GPIOC register
↓
bit tương ứng PC13
↓
GPIO output
↓
LED
Đây chính là điểm khác biệt giữa:
Cấp cao
digitalWrite(PC13, LOW);
và:
Cấp register
ghi bit vào GPIO register
21. Tại sao phải học register?
Bởi vì khi bạn đã hiểu register, bạn sẽ bắt đầu hiểu:
HAL_GPIO_WritePin()
không còn là “phép thuật”.
Bạn sẽ hiểu:
HAL
↓
CMSIS
↓
Register
↓
Hardware
22. Một ví dụ cực đơn giản về register
Hãy tưởng tượng có:
REGISTER_A
bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0
0 0 0 0 0 0 0 0
Nếu bit 3 điều khiển một thiết bị:
00001000
thì thiết bị bật.
Nếu:
00000000
thì thiết bị tắt.
Đây là lý do bit manipulation cực kỳ quan trọng trong STM32.
23. Và đây là cây kiến thức bạn đang bắt đầu xây
C LANGUAGE
│
├── Variable
│
├── Data type
│
├── Address
│
├── Pointer
│
├── Bit
│
└── Operator
│
▼
MEMORY / REGISTER
│
▼
STM32 PERIPHERAL
│
┌─────┼─────┐
▼ ▼ ▼
GPIO ADC TIMER
│
▼
HARDWARE
Bạn đang đi đúng hướng để chuyển từ:
“lập trình STM32 bằng cách gọi hàm”
sang:
“hiểu STM32 thực sự hoạt động bên dưới.”
🧪 BÀI THỰC HÀNH 1
Bạn hãy chạy đoạn này trên Blue Pill bằng Arduino IDE:
int number = 123;
int *ptr = &number;
void setup()
{
Serial.begin(115200);
Serial.print("number = ");
Serial.println(number);
Serial.print("address = 0x");
Serial.println((uint32_t)&number, HEX);
Serial.print("*ptr = ");
Serial.println(*ptr);
*ptr = 456;
Serial.print("number after = ");
Serial.println(number);
}
void loop()
{
}
Bạn sẽ thấy đại khái:
number = 123
address = 0x2000....
*ptr = 123
number after = 456
Điều cần quan sát là:
number = 123
sau:
*ptr = 456;
thì:
number = 456
🧪 BÀI THỰC HÀNH 2 – RẤT QUAN TRỌNG
Thử chương trình:
uint8_t a = 10;
uint16_t b = 500;
uint32_t c = 100000;
void setup()
{
Serial.begin(115200);
Serial.print("a address = 0x");
Serial.println((uint32_t)&a, HEX);
Serial.print("b address = 0x");
Serial.println((uint32_t)&b, HEX);
Serial.print("c address = 0x");
Serial.println((uint32_t)&c, HEX);
}
void loop()
{
}
Bạn sẽ bắt đầu thấy rằng:
a → một địa chỉ
b → một địa chỉ khác
c → một địa chỉ khác
và mỗi biến chiếm số byte khác nhau.
🎯 6 điều bạn cần nắm sau Bài 2
Đừng cố học thuộc cả bài. Chỉ cần hiểu chắc 6 câu này:
① &
&x
→ lấy địa chỉ của x
② *
Nếu p là con trỏ:
*p
→ truy cập dữ liệu tại địa chỉ mà p đang trỏ tới
③ Con trỏ
int *p;
→ p là biến dùng để chứa địa chỉ của một int.
④ Register
Register là các vùng thanh ghi mà CPU/peripheral sử dụng để điều khiển và theo dõi phần cứng.
⑤ Memory-mapped I/O
Peripheral STM32 được ánh xạ vào không gian địa chỉ, nên CPU có thể giao tiếp với chúng thông qua các thao tác đọc/ghi địa chỉ.
⑥ volatile
Dùng để báo compiler rằng giá trị có thể thay đổi ngoài luồng thực thi thông thường, đặc biệt quan trọng với hardware register và biến liên quan đến interrupt.
🔜 BÀI 3
Nếu tiếp tục theo lộ trình này, Bài 3 mình đề nghị học:
“BIT – BYTE – HEX – toán tử & | ^ ~ << >> và cách STM32 bật/tắt đúng 1 bit của một register”
Đây sẽ là bài cực kỳ quan trọng.
Sau Bài 3, chúng ta sẽ có thể tự tay phân tích kiểu:
GPIOC->ODR |= (1 << 13);
và bạn sẽ hiểu từng ký tự trong dòng lệnh đó có ý nghĩa gì, thay vì học thuộc lòng.