Được bạn. Chúng ta tiếp tục Bài 3 trong lộ trình học C cho STM32 nhé.
🟦 BÀI 3 – CON TRỎ, ĐỊA CHỈ VÀ CÁCH STM32 “ĐỤNG” VÀO PHẦN CỨNG
Ở Bài 1, chúng ta học kiểu dữ liệu và RAM.
Ở Bài 2, chúng ta học biến, toán tử, if, for, while, hàm…
Bây giờ đến một thứ cực kỳ quan trọng:
Con trỏ (
pointer) và địa chỉ bộ nhớ.
Nếu bạn muốn chuyển từ kiểu lập trình Arduino sang lập trình STM32/C nhúng thực sự, đây là một trong những bài quan trọng nhất.
1. Trước hết: biến thực sự nằm ở đâu?
Ví dụ:
uint8_t led = 1;
Bạn có thể hình dung RAM như một dãy hộp:
RAM STM32
Địa chỉ Giá trị
--------------------------------
0x20000000 ...
0x20000001 ...
0x20000002 1 ← led
0x20000003 ...
Biến led không phải chỉ là con số 1.
Nó gồm:
Tên biến : led
Kiểu dữ liệu : uint8_t
Giá trị : 1
Địa chỉ : một địa chỉ nào đó trong RAM
Ví dụ tưởng tượng:
led = 1
địa chỉ của led = 0x20000010
2. Toán tử & – lấy địa chỉ
Trong C:
&led
có nghĩa:
Lấy địa chỉ của biến
led.
Ví dụ:
uint8_t led = 1;
sau đó:
printf("%p", &led);
có thể cho ra đại loại:
0x20000010
Không cần quan tâm địa chỉ cụ thể lúc này.
Điều quan trọng là:
led
↓
giá trị
&led
↓
địa chỉ
3. Con trỏ là gì?
Con trỏ là một biến dùng để chứa địa chỉ của một biến khác.
Ví dụ:
uint8_t led = 1;
uint8_t *p = &led;
Ta có:
led
┌──────────────┐
│ 1 │
└──────────────┘
↑
│
│ &led
│
p ────┘
p không chứa số 1.
p chứa địa chỉ của led.
4. Ý nghĩa của dấu *
Đây là chỗ người mới học C thường bị rối.
uint8_t *p;
Ở đây * có nghĩa:
plà một con trỏ tớiuint8_t.
Nhưng:
*p
lại có nghĩa:
Đi tới địa chỉ mà
pđang chứa và lấy giá trị ở đó.
Ví dụ:
uint8_t led = 1;
uint8_t *p = &led;
thì:
p
là:
địa chỉ của led
còn:
*p
là:
giá trị của led
5. Đây là công thức bạn nên nhớ
& biến
↓
lấy địa chỉ
* con_trỏ
↓
lấy giá trị tại địa chỉ
Ví dụ:
uint8_t x = 10;
uint8_t *p = &x;
Ta có:
x = 10
&x = địa chỉ của x
p = địa chỉ của x
*p = 10
6. Điều cực kỳ thú vị
Ta có thể thay đổi x thông qua p.
uint8_t x = 10;
uint8_t *p = &x;
*p = 20;
Sau đó:
x
sẽ bằng:
20
Tại sao?
Vì:
p
│
│ chứa địa chỉ của x
▼
┌─────────────┐
│ x = 10 │
└─────────────┘
Khi viết:
*p = 20;
ta nói với CPU:
“Hãy tới địa chỉ mà
pđang giữ và ghi số 20 vào đó.”
Kết quả:
┌─────────────┐
│ x = 20 │
└─────────────┘
7. Đây chính là nền tảng của lập trình STM32
Bây giờ mới đến phần rất quan trọng.
Trong STM32, rất nhiều phần cứng được điều khiển bằng cách đọc/ghi vào các địa chỉ bộ nhớ đặc biệt.
Ví dụ khái niệm:
CPU
│
│ ghi dữ liệu
▼
địa chỉ đặc biệt
│
▼
GPIO
│
▼
chân PA0
Vì vậy lập trình STM32 thường xuyên gặp:
*address = value;
hoặc:
value = *address;
8. Ví dụ giả lập thanh ghi GPIO
Giả sử một thanh ghi nằm tại:
0x4001080C
Ta có thể hình dung:
volatile uint32_t *GPIO_REG =
(volatile uint32_t *)0x4001080C;
Sau đó:
*GPIO_REG = 0x01;
CPU sẽ ghi:
0x00000001
vào địa chỉ:
0x4001080C
Đây chính là kiểu tư duy Register-level programming.
9. volatile là gì?
Bạn sẽ gặp từ này cực kỳ nhiều trong STM32:
volatile
Ví dụ:
volatile uint32_t *GPIO_REG;
Hiểu đơn giản trước:
volatilenói với compiler rằng giá trị này có thể thay đổi bởi một nguồn bên ngoài luồng code bình thường, nên compiler không được tự ý tối ưu việc đọc/ghi nó như một biến thông thường.
Đặc biệt quan trọng đối với:
- GPIO
- Timer
- UART
- ADC
- Interrupt
- DMA
- các peripheral register
10. Tại sao STM32 cần volatile?
Ví dụ CPU đang đọc một thanh ghi:
while (UART_STATUS == 0)
{
}
Compiler có thể suy luận:
“UART_STATUS chưa thay đổi trong đoạn code này.”
Nhưng thực tế phần cứng UART có thể thay đổi thanh ghi đó bất cứ lúc nào.
Vì vậy ta dùng:
volatile
để compiler không đưa ra những tối ưu sai đối với phần cứng.
11. Một ví dụ rất dễ hiểu với LED
Giả sử:
uint8_t led = 0;
Ta tạo con trỏ:
uint8_t *p = &led;
Sau đó:
*p = 1;
thì:
led = 1
Tiếp tục:
*p = 0;
thì:
led = 0
Vậy:
p
│
│
▼
led
Con trỏ giống như một địa chỉ nhà.
12. Hãy tưởng tượng RAM là một khu phố
Ví dụ:
RAM
────────────────────────────
Nhà 1000
┌───────────┐
│ x = 25 │
└───────────┘
Nhà 2000
┌───────────┐
│ p=1000 │
└───────────┘
p chứa:
1000
nên:
*p
nghĩa là:
Đến nhà số 1000 lấy giá trị.
Do đó:
*p
→ 25
Còn:
*p = 50;
→ đến nhà 1000 và đổi thành 50.
13. Con trỏ và hàm
Đây là một lý do cực kỳ quan trọng khiến con trỏ xuất hiện nhiều trong C.
Ví dụ:
void set_value(uint8_t *p)
{
*p = 100;
}
Sau đó:
uint8_t x = 10;
set_value(&x);
Kết quả:
x = 100
Vì ta truyền:
&x
tức là địa chỉ của x vào hàm.
Hàm nhận:
uint8_t *p
và thay đổi:
*p
14. So sánh với Arduino
Trong Arduino bạn có thể viết:
digitalWrite(LED_BUILTIN, HIGH);
Bạn không cần quan tâm:
GPIO register nằm ở đâu?
bit nào điều khiển output?
địa chỉ thanh ghi là gì?
Arduino đã che giấu phần đó.
STM32 HAL cũng che giấu rất nhiều thứ:
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
Nhưng bên dưới:
HAL
↓
GPIO driver
↓
Register
↓
Memory address
↓
Phần cứng GPIO
↓
PC13
Cho nên nếu bạn hiểu con trỏ + địa chỉ + register, bạn sẽ bắt đầu hiểu được STM32 thực sự hoạt động như thế nào.
15. Một bài tập rất quan trọng
Bạn hãy tự viết đoạn C này:
#include <stdint.h>
int main(void)
{
uint8_t a = 10;
uint8_t *p = &a;
*p = 50;
return 0;
}
Hãy tự trả lời 5 câu:
Câu 1
a
có giá trị bao nhiêu?
Câu 2
&a
là gì?
Câu 3
p
chứa gì?
Câu 4
*p
bằng bao nhiêu?
Câu 5
Sau:
*p = 50;
thì:
a
bằng bao nhiêu?
Nếu bạn trả lời được 5 câu này mà không cần nhìn tài liệu, bạn đã bắt đầu thực sự hiểu pointer.
16. Bài thực hành trên STM32 Blue Pill
Với Blue Pill của bạn, chúng ta sẽ chưa vội lao ngay vào register.
Hãy đi theo 3 tầng:
TẦNG 1
C thuần
↓
biến
con trỏ
địa chỉ
TẦNG 2
STM32 HAL
↓
GPIO
ADC
Timer
UART
TẦNG 3
STM32 Register
↓
RCC
GPIOx_CRL/CRH
GPIOx_ODR
GPIOx_IDR
ADC
TIM
USART
Mục tiêu cuối cùng là bạn nhìn thấy:
HAL_GPIO_WritePin(...)
và hiểu được:
“À, bên dưới nó cuối cùng cũng phải thao tác với các thanh ghi phần cứng.”
🎯 Sau Bài 3 bạn cần nhớ 7 thứ
1. Biến nằm trong bộ nhớ.
2. &x = địa chỉ của x.
3. Con trỏ là biến chứa địa chỉ.
4. *p = giá trị tại địa chỉ mà p trỏ tới.
5. *p = value → ghi value vào vùng nhớ mà p trỏ tới.
6. STM32 dùng địa chỉ bộ nhớ để truy cập peripheral/register.
7. volatile rất quan trọng khi làm việc với phần cứng.
Và có một câu cực kỳ đáng nhớ:
C cho phép bạn không chỉ thao tác với dữ liệu, mà còn thao tác trực tiếp với nơi dữ liệu đang nằm.
Đó chính là một trong những lý do C trở thành ngôn ngữ quan trọng trong embedded/STM32.
Bài tiếp theo
Bài 4 – “BIT, BYTE và REGISTER: Vì sao chỉ 1 bit cũng có thể bật/tắt một chân GPIO?”
Bài 4 sẽ nối trực tiếp kiến thức hôm nay với PC13 trên Blue Pill của bạn, và chúng ta sẽ bắt đầu nhìn một GPIO STM32 dưới dạng từng bit trong thanh ghi.