📘 BÀI 6 – CON TRỎ + MẢNG: HIỂU CÁCH STM32 TRUY CẬP DỮ LIỆU TRONG RAM
Ở Bài 5, chúng ta đã học con trỏ: biến con trỏ không giữ trực tiếp dữ liệu mà giữ địa chỉ của dữ liệu.
Bài 6 sẽ ghép hai thứ lại:
Mảng + con trỏ = nền tảng cực kỳ quan trọng khi lập trình STM32 bằng C.
Đây cũng là bước chuẩn bị để sau này bạn hiểu được buffer UART, ADC, DMA, GPIO register, struct, chuỗi ký tự…
1. Mảng là gì?
Ví dụ:
uint8_t data[5];
Ta có 5 phần tử:
data[0]
data[1]
data[2]
data[3]
data[4]
Giả sử địa chỉ bắt đầu của data là:
0x20000000
Vì uint8_t = 1 byte nên RAM có thể hình dung:
Địa chỉ Dữ liệu
0x20000000 data[0]
0x20000001 data[1]
0x20000002 data[2]
0x20000003 data[3]
0x20000004 data[4]
2. Điều rất quan trọng: tên mảng gần giống một con trỏ
Xét:
uint8_t data[5];
Khi viết:
data
trong hầu hết các biểu thức, nó biểu diễn địa chỉ phần tử đầu tiên:
&data[0]
Vì vậy:
data == &data[0]
về mặt địa chỉ là tương đương.
Ví dụ:
printf("%p\n", data);
printf("%p\n", &data[0]);
sẽ cho cùng một địa chỉ.
3. data[0] thực chất liên quan đến con trỏ
Ta có:
data[0]
và:
*(data + 0)
là tương đương.
Tương tự:
data[1]
tương đương:
*(data + 1)
và:
data[2]
tương đương:
*(data + 2)
Có thể nhớ:
data[i] <=> *(data + i)
Đây là một trong những công thức quan trọng nhất của C.
4. Nhưng tại sao data + 1 lại tăng đúng kích thước kiểu dữ liệu?
Đây là điểm rất quan trọng.
Ví dụ:
uint8_t data[5];
thì:
data + 1
tăng:
1 × sizeof(uint8_t)
= 1 byte
Nhưng:
uint32_t data[5];
thì:
data + 1
tăng:
1 × sizeof(uint32_t)
= 4 byte
Ví dụ địa chỉ:
uint32_t data[5]
data
↓
0x20000000
data + 1
↓
0x20000004
data + 2
↓
0x20000008
data + 3
↓
0x2000000C
Con trỏ hiểu kích thước của kiểu dữ liệu mà nó trỏ tới.
5. Thí nghiệm rất quan trọng
Bạn hãy thử đoạn C này:
#include <stdio.h>
#include <stdint.h>
int main()
{
uint32_t data[5];
printf("%p\n", (void*)data);
printf("%p\n", (void*)(data + 1));
printf("%p\n", (void*)(data + 2));
return 0;
}
Bạn sẽ thấy các địa chỉ cách nhau khoảng:
4 byte
vì:
sizeof(uint32_t) == 4
6. Con trỏ có thể duyệt qua mảng
Ví dụ:
uint8_t data[5] = {10, 20, 30, 40, 50};
uint8_t *p = data;
Ban đầu:
p
│
▼
data[0]
10
Nếu:
p++;
thì:
p
│
▼
data[1]
20
Tiếp tục:
p++;
thì:
p
│
▼
data[2]
30
Vì vậy ta có thể duyệt mảng bằng con trỏ.
7. Ví dụ thực tế
#include <stdio.h>
#include <stdint.h>
int main()
{
uint8_t data[5] = {10, 20, 30, 40, 50};
uint8_t *p = data;
for(int i = 0; i < 5; i++)
{
printf("%d\n", *p);
p++;
}
return 0;
}
Kết quả:
10
20
30
40
50
8. Đây chính là điều STM32 sử dụng rất nhiều
Ví dụ sau này bạn đọc UART:
UART
│
▼
RAM
│
▼
buffer[]
Ví dụ:
uint8_t rxBuffer[64];
STM32 nhận 64 byte vào vùng RAM:
rxBuffer[0]
rxBuffer[1]
rxBuffer[2]
...
rxBuffer[63]
Bạn có thể dùng con trỏ để xử lý:
uint8_t *p = rxBuffer;
Sau đó:
p++;
để chuyển sang byte tiếp theo.
9. Một ví dụ cực kỳ quan trọng với STM32
Giả sử ADC tạo ra 10 giá trị:
uint16_t adc[10];
Có thể hình dung:
RAM
adc
↓
┌────────┬────────┬────────┬────────┬─────
│ ADC0 │ ADC1 │ ADC2 │ ADC3 │ ...
└────────┴────────┴────────┴────────┴─────
Mỗi phần tử uint16_t chiếm 2 byte.
Nếu:
uint16_t *p = adc;
thì:
*p
là ADC0.
Sau:
p++;
thì:
*p
là ADC1.
Sau:
p++;
thì:
*p
là ADC2.
10. Một điều rất quan trọng: p++ không phải lúc nào cũng tăng 1 byte
Ví dụ:
uint8_t *p;
thì:
p++;
→ tăng 1 byte.
Nhưng:
uint16_t *p;
thì:
p++;
→ tăng 2 byte.
Và:
uint32_t *p;
thì:
p++;
→ tăng 4 byte.
Cho nên:
Con trỏ tăng theo kích thước kiểu dữ liệu mà nó trỏ tới.
11. Bài tập 1 – bạn hãy tự suy nghĩ
Cho:
uint8_t a[4] = {10,20,30,40};
uint8_t *p = a;
Hỏi:
Sau câu lệnh:
printf("%d", *p);
in ra gì?
Sau:
p++;
printf("%d", *p);
in ra gì?
Sau:
p++;
p++;
printf("%d", *p);
in ra gì?
Bạn hãy tự trả lời trước, chưa cần chạy chương trình.
12. Bài tập 2 – rất quan trọng
Cho:
uint16_t adc[4] = {1000,2000,3000,4000};
uint16_t *p = adc;
Hãy xác định:
*p
bằng bao nhiêu?
Sau:
p++;
thì:
*p
bằng bao nhiêu?
Sau:
p++;
thì:
*p
bằng bao nhiêu?
13. Bài tập 3 – liên quan trực tiếp đến RAM
Giả sử:
uint32_t data[3];
và:
data bắt đầu tại địa chỉ:
0x20000000
Hãy xác định địa chỉ của:
&data[0]
&data[1]
&data[2]
Biết:
sizeof(uint32_t) = 4
14. Kiến thức bạn cần nhớ sau Bài 6
Bạn chưa cần học thuộc hàng đống câu lệnh.
Chỉ cần thật sự hiểu 5 điều này:
① Mảng chứa nhiều phần tử cùng kiểu.
② data thường biểu diễn địa chỉ phần tử đầu tiên.
③ data[i] tương đương *(data + i).
④ p++ đưa con trỏ sang phần tử kế tiếp.
⑤ p++ tăng theo sizeof(kiểu dữ liệu).
Nếu bạn hiểu chắc 5 điều này thì bạn đã bắt đầu chuyển từ:
“lập trình Arduino bằng cách gọi hàm”
sang:
“hiểu cách C thực sự làm việc với RAM của STM32.”
🔜 Bài 7
Sau Bài 6, chúng ta nên học:
BÀI 7 – const, con trỏ tới const, const + con trỏ và tại sao STM32 có rất nhiều dòng const uint8_t *...
Phần này cực kỳ hữu ích trước khi chúng ta đi vào struct, function, UART, GPIO register và DMA.