Được 👍 Chúng ta tiếp tục BÀI 5 – Lập trình C cho STM32.
Ở các bài trước, ta đã đi từ kiểu dữ liệu → biến → cách dữ liệu nằm trong RAM → toán tử/câu lệnh cơ bản. Bài 5 tôi muốn đưa bạn sang một phần cực kỳ quan trọng khi lập trình STM32:
🔥 BÀI 5 – CON TRỎ (POINTER) TRONG C VÀ TẠI SAO STM32 RẤT CẦN CON TRỎ
Nếu học C để làm STM32 mà chưa hiểu pointer thì sau này bạn sẽ rất khó đọc HAL, thanh ghi GPIO, UART, DMA, cấu trúc dữ liệu…
1. Pointer thực chất là gì?
Hãy tưởng tượng RAM của STM32 giống như một dãy tủ:
Địa chỉ RAM
0x20000000 ──┐
│
0x20000001 ──┤
│
0x20000002 ──┤
│
0x20000003 ──┘
Mỗi ô nhớ có một địa chỉ.
Ví dụ:
int x = 100;
Giả sử STM32 đặt x tại:
0x20000010
Ta có:
Tên biến Giá trị Địa chỉ
x 100 0x20000010
2. Toán tử &
Trong C:
&x
có nghĩa:
“Cho tôi địa chỉ của biến x.”
Ví dụ:
int x = 100;
printf("%d", x);
→ 100
Còn:
printf("%p", &x);
→ địa chỉ của x.
Ví dụ:
0x20000010
3. Pointer là gì?
Pointer là một biến dùng để lưu địa chỉ của một biến khác.
Ví dụ:
int x = 100;
int *p;
p = &x;
Bây giờ:
x
│
│ giá trị = 100
│
▼
RAM
0x20000010
▲
│
│
p
p không chứa 100.
Nó chứa:
0x20000010
4. Dấu * có hai ý nghĩa
Đây là điểm rất nhiều người mới học C bị nhầm.
Khi khai báo:
int *p;
nghĩa là:
plà pointer tớiint.
Khi sử dụng:
*p
nghĩa là:
lấy giá trị tại địa chỉ mà
pđang trỏ tới.
Ví dụ:
int x = 100;
int *p = &x;
Ta có:
x = 100
p = địa chỉ của x
*p = 100
5. Ví dụ cực kỳ quan trọng
int x = 100;
int *p = &x;
x = 200;
Khi đó:
x = 200
*p = 200
Bởi vì p đang trỏ vào x.
Ngược lại:
*p = 500;
thì:
x = 500
Đây chính là sức mạnh của pointer.
6. Hãy nhớ sơ đồ này
RAM
┌───────────────┐
│ │
0x2000 │ x │
│ 100 │
└───────────────┘
▲
│
│
│
┌────┴─────┐
│ p │
│ 0x2000 │
└──────────┘
p chứa địa chỉ.
*p lấy giá trị tại địa chỉ đó.
7. Pointer với các kiểu dữ liệu
Không phải pointer nào cũng giống nhau.
Ví dụ:
char *p1;
int *p2;
float *p3;
Ý nghĩa:
char *p1 → trỏ tới char
int *p2 → trỏ tới int
float *p3 → trỏ tới float
Ví dụ:
char a = 10;
char *p = &a;
và:
int b = 1000;
int *q = &b;
8. Tại sao kiểu pointer lại quan trọng?
Vì CPU cần biết bao nhiêu byte dữ liệu cần đọc.
Ví dụ STM32F103:
char
thường:
1 byte
Trong khi:
int
thường:
4 byte
Do đó:
char *p;
khi p++ thì địa chỉ thường tăng:
+1 byte
Còn:
int *p;
khi p++ thì địa chỉ thường tăng:
+4 byte
Đây gọi là pointer arithmetic.
9. Pointer arithmetic
Ví dụ:
int a[3] = {10,20,30};
int *p = a;
Ban đầu:
p → a[0]
Nếu:
p++;
thì:
p → a[1]
Tiếp tục:
p++;
thì:
p → a[2]
Sơ đồ:
a:
┌────┬────┬────┐
│ 10 │ 20 │ 30 │
└────┴────┴────┘
▲
p
p++
┌────┬────┬────┐
│ 10 │ 20 │ 30 │
└────┴────┴────┘
▲
p
p++
┌────┬────┬────┐
│ 10 │ 20 │ 30 │
└────┴────┴────┘
▲
p
10. Pointer và mảng
Đây là một kiến thức cực kỳ quan trọng:
int a[5];
Tên a trong nhiều biểu thức có thể được sử dụng như địa chỉ phần tử đầu tiên:
a
tương đương về mặt địa chỉ với:
&a[0]
Do đó:
int *p = a;
có thể hiểu là:
int *p = &a[0];
11. Một điều rất quan trọng đối với STM32
Sau này bạn sẽ gặp những thứ kiểu:
uint32_t *address;
hoặc:
volatile uint32_t *reg;
Ví dụ một thanh ghi phần cứng nằm tại một địa chỉ cụ thể:
0x4001080C
Ta có thể hình dung:
volatile uint32_t *GPIO_REG =
(volatile uint32_t *)0x4001080C;
Sau đó:
*GPIO_REG = 0x00000020;
Nghĩa là:
Ghi dữ liệu vào vùng nhớ tại địa chỉ
0x4001080C.
🔥 Đây chính là một trong những cơ chế nền tảng để C điều khiển phần cứng STM32.
12. volatile là gì?
Bạn sẽ gặp nó rất nhiều trong STM32.
Ví dụ:
volatile uint32_t GPIO_REG;
volatile nói với compiler rằng:
Giá trị này có thể thay đổi bởi phần cứng hoặc một cơ chế bên ngoài chương trình; đừng tự ý tối ưu hóa việc đọc/ghi nó.
Ví dụ thanh ghi phần cứng:
volatile uint32_t *REG;
Điều này cực kỳ quan trọng khi làm:
- GPIO
- UART
- Timer
- ADC
- Interrupt
- DMA
- Peripheral registers
13. Pointer và hàm – cực kỳ quan trọng
Đây là phần bạn cần đặc biệt chú ý.
Giả sử:
void Tang(int x)
{
x++;
}
Gọi:
int a = 10;
Tang(a);
Sau đó:
a = 10
Tại sao?
Vì hàm nhận bản sao của a.
Nhưng nếu dùng pointer:
void Tang(int *x)
{
(*x)++;
}
Gọi:
int a = 10;
Tang(&a);
Sau đó:
a = 11
Sơ đồ:
main()
a = 10
│
│ &a
▼
Tang()
x ─────────────► a
│
▼
10
│
(*x)++
│
▼
11
Đây là một ứng dụng pointer bạn sẽ gặp rất thường xuyên trong lập trình STM32.
14. Tại sao HAL của STM32 dùng pointer rất nhiều?
Bạn sẽ thường thấy các hàm dạng:
HAL_UART_Transmit(&huart1, ...);
Hãy chú ý:
&huart1
Đây chính là:
địa chỉ của biến
huart1.
HAL nhận địa chỉ đó để có thể làm việc với cấu trúc cấu hình UART tương ứng.
Tương tự:
HAL_GPIO_WritePin(...)
và nhiều API khác cũng sử dụng pointer, cấu trúc và địa chỉ bộ nhớ.
15. Ba ký hiệu bạn phải thuộc
Nếu chỉ chọn 3 thứ quan trọng nhất của bài hôm nay, hãy nhớ:
&
→ lấy địa chỉ.
*
→ lấy giá trị tại địa chỉ.
int *p;
→ khai báo pointer p trỏ tới int.
Ví dụ:
int x = 100;
int *p = &x;
*p = 200;
Kết quả:
x = 200
🧠 BÀI TẬP BÀI 5
Bạn hãy tự trả lời trước, đừng chạy code.
Câu 1
int x = 10;
int *p = &x;
*p = 20;
Giá trị cuối cùng của x là bao nhiêu?
Câu 2
int x = 50;
int *p = &x;
x = 100;
Giá trị của:
*p
là bao nhiêu?
Câu 3
int a[3] = {10,20,30};
int *p = a;
p++;
*p bằng bao nhiêu?
Câu 4 – quan trọng
Giải thích bằng lời của bạn:
int *p = &x;
p, *p, &x lần lượt có ý nghĩa gì?
Câu 5 – STM32
Tại sao trong lập trình STM32 chúng ta phải hiểu pointer trước khi học sâu về:
GPIO → UART → Timer → ADC → Interrupt → DMA → thanh ghi phần cứng?
Bạn trả lời 5 câu này, tôi sẽ chấm và sửa từng câu, sau đó chúng ta sang BÀI 6 – STRUCT, ENUM, UNION: 3 thứ cực kỳ quan trọng để đọc được code STM32/HAL.