📘 BÀI 15 – static, const, volatile: 3 TỪ KHÓA CỰC KỲ QUAN TRỌNG TRONG STM32
Đây là một bài rất quan trọng.
Nếu struct, enum, typedef, pointer giúp bạn tổ chức dữ liệu, thì:
static
const
volatile
giúp bạn kiểm soát cách biến tồn tại, có được phép thay đổi hay không, và cách compiler phải xử lý nó.
Đặc biệt:
volatilelà kiến thức bắt buộc khi bạn bắt đầu làm STM32 thực tế.
1. const – biến không được phép thay đổi
Ví dụ:
const int MAX_SPEED = 1000;
Sau đó:
MAX_SPEED = 2000;
❌ Không hợp lệ.
Bởi vì MAX_SPEED đã được khai báo là const.
Ví dụ với STM32
Giả sử động cơ của bạn tối đa 1000 RPM:
const uint16_t MOTOR_MAX_SPEED = 1000;
Trong chương trình:
if (speed > MOTOR_MAX_SPEED)
{
speed = MOTOR_MAX_SPEED;
}
Điều này rất tốt.
Bạn đang nói với compiler:
“Giá trị này không được chương trình thay đổi.”
2. const không có nghĩa là “không nằm trong RAM”
Đây là điểm bạn cần nhớ.
Khi học C trên STM32, đừng hiểu đơn giản:
const = chắc chắn nằm trong Flash
Không phải lúc nào cũng vậy.
const chủ yếu mang ý nghĩa:
Code của bạn không được phép sửa giá trị thông qua đối tượng đó.
Compiler/linker có thể đặt dữ liệu const ở vùng nhớ phù hợp, thường có thể nằm trong Flash đối với dữ liệu chỉ đọc.
3. const với pointer
Đây là phần quan trọng hơn.
Có:
int value = 10;
int *ptr = &value;
Ta có thể:
*ptr = 20;
Nhưng:
const int *ptr = &value;
thì:
*ptr = 20;
❌ Không được.
Ta có thể đọc:
printf("%d", *ptr);
nhưng không được sửa thông qua ptr.
4. static – biến sống lâu hơn phạm vi của nó
Ví dụ:
void Counter(void)
{
int count = 0;
count++;
printf("%d", count);
}
Mỗi lần gọi:
Counter();
Counter();
Counter();
count lại được tạo lại.
Kết quả:
1
1
1
Nhưng nếu:
void Counter(void)
{
static int count = 0;
count++;
printf("%d", count);
}
thì:
Counter();
→ 1
Counter();
→ 2
Counter();
→ 3
Tại sao?
Vì:
static int count;
không bị mất sau khi hàm kết thúc.
5. Hãy hình dung RAM
Biến bình thường:
void Function(void)
{
int x = 10;
}
Có thể hình dung:
Function()
↓
Stack
┌─────────┐
│ x = 10 │
└─────────┘
↓
Function kết thúc
↓
x không còn tồn tại
Còn:
static int x = 10;
thì:
RAM
┌─────────────┐
│ static x=10 │
└─────────────┘
↑
│
Function()
Hàm kết thúc nhưng x vẫn tồn tại.
6. static rất hữu ích trong firmware
Ví dụ muốn tạo bộ đếm:
void Task_1ms(void)
{
static uint32_t counter = 0;
counter++;
if (counter >= 1000)
{
counter = 0;
// làm một việc
}
}
Nếu Task_1ms() được gọi mỗi 1 ms:
1 ms → counter = 1
2 ms → counter = 2
3 ms → counter = 3
...
1000ms → counter = 1000
Sau đó:
counter = 0;
và bắt đầu lại.
Đây là kỹ thuật bạn sẽ gặp rất nhiều trong STM32.
7. static còn có một ý nghĩa khác
Ví dụ:
static int motorSpeed;
ở phạm vi file .c.
Nó có nghĩa rằng biến này chỉ được sử dụng trong file đó.
Ví dụ:
Motor.c
static int motorSpeed;
thì các file khác không thể truy cập trực tiếp motorSpeed.
Điều này giúp đóng gói module.
8. Ví dụ module STM32
Giả sử:
Motor.c
Motor.h
Trong Motor.c:
static uint16_t motorSpeed = 0;
Bạn có:
void Motor_SetSpeed(uint16_t speed)
{
motorSpeed = speed;
}
và:
uint16_t Motor_GetSpeed(void)
{
return motorSpeed;
}
Bên ngoài chỉ cần:
Motor_SetSpeed(500);
không cần biết biến motorSpeed nằm ở đâu.
Đây là tư duy encapsulation rất quan trọng.
9. volatile – phần quan trọng nhất
Bây giờ đến từ khóa quan trọng nhất:
volatile
volatile nói với compiler:
“Giá trị của biến này có thể thay đổi từ bên ngoài luồng code bình thường. Đừng tự ý tối ưu hóa việc đọc nó.”
Đây chính là tình huống xảy ra rất nhiều trong STM32.
10. Ví dụ đơn giản
volatile uint8_t flag = 0;
Sau đó chương trình:
while (flag == 0)
{
}
Compiler phải hiểu rằng:
flag
↓
có thể thay đổi bất kỳ lúc nào
↓
phải đọc lại từ bộ nhớ
11. Tại sao STM32 cần volatile?
Vì giá trị có thể thay đổi bởi:
Interrupt
Timer
DMA
Peripheral
Hardware register
chứ không nhất thiết do câu lệnh thông thường trong main() thay đổi.
12. Ví dụ Interrupt
Giả sử:
volatile uint8_t buttonPressed = 0;
Trong main():
while (1)
{
if (buttonPressed)
{
buttonPressed = 0;
// xử lý nút
}
}
Trong interrupt:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
buttonPressed = 1;
}
Ta có:
INTERRUPT
│
▼
buttonPressed = 1
│
▼
RAM
│
▼
main()
│
▼
if(buttonPressed)
buttonPressed có thể thay đổi ngoài luồng thực thi thông thường của main().
Vì vậy:
volatile uint8_t buttonPressed;
là rất phù hợp.
13. Nếu không có volatile thì sao?
Compiler có thể tối ưu hóa code dựa trên giả định rằng một biến bình thường không tự nhiên thay đổi giữa các lần đọc nếu không có code tương ứng ghi vào nó.
Ví dụ:
uint8_t flag = 0;
while (flag == 0)
{
}
Nếu compiler thấy không có đoạn code thông thường nào thay đổi flag, nó có thể tối ưu mạnh đoạn code này.
Nhưng với:
volatile uint8_t flag = 0;
compiler phải tôn trọng khả năng flag bị thay đổi bởi cơ chế bên ngoài như interrupt hoặc phần cứng.
14. Hardware Register và volatile
Đây là nơi bạn sẽ thấy volatile rất nhiều.
STM32 có các thanh ghi phần cứng.
Ví dụ khái niệm:
volatile uint32_t *REG = ...;
CPU đọc:
value = *REG;
Phần cứng có thể làm thay đổi giá trị thanh ghi.
Do đó compiler không được giả định rằng giá trị luôn giữ nguyên.
Đây là lý do các thanh ghi peripheral thường được khai báo theo cơ chế tương đương volatile.
15. volatile KHÔNG làm code “an toàn tuyệt đối”
Đây là lỗi hiểu rất phổ biến.
Nhiều người nghĩ:
volatile
có nghĩa:
“Biến này an toàn khi dùng trong interrupt.”
❌ Không hoàn toàn đúng.
Ví dụ:
volatile uint32_t counter;
volatile đảm bảo compiler không tối ưu hóa các lần truy cập theo cách làm mất ý nghĩa volatile.
Nhưng nó không tự biến phép đọc/ghi thành một thao tác nguyên tử trong mọi trường hợp, và không giải quyết mọi vấn đề đồng bộ giữa main, ISR, DMA hoặc các ngữ cảnh khác.
Đây là kiến thức chúng ta sẽ học sâu hơn sau.
16. So sánh 3 từ khóa
| Từ khóa | Ý nghĩa chính |
|---|---|
const |
Code không được phép sửa giá trị qua đối tượng đó |
static |
Điều khiển thời gian sống/phạm vi liên kết của biến |
volatile |
Báo compiler rằng giá trị có thể thay đổi ngoài luồng code thông thường |
Hãy nhớ:
const
↓
KHÔNG CHO PHÉP CODE SỬA
static
↓
GIỮ TRẠNG THÁI / GIỚI HẠN PHẠM VI
volatile
↓
GIÁ TRỊ CÓ THỂ THAY ĐỔI BẤT NGỜ
17. Kết hợp cả 3
Bạn hoàn toàn có thể gặp:
static volatile uint8_t flag;
Nghĩa là:
static
↓
chỉ dùng trong module này
và tồn tại lâu dài
volatile
↓
giá trị có thể bị thay đổi
bởi interrupt/hardware
Ví dụ:
static volatile uint8_t uartRxComplete = 0;
Đây là kiểu khai báo rất thực tế trong firmware.
18. Một ví dụ STM32 hoàn chỉnh
Giả sử UART nhận dữ liệu.
static volatile uint8_t rxComplete = 0;
ISR/callback:
void UART_RxComplete(void)
{
rxComplete = 1;
}
Trong main():
while (1)
{
if (rxComplete)
{
rxComplete = 0;
// xử lý dữ liệu
}
}
Ở đây:
static
→ biến chỉ thuộc module.
volatile
→ ISR có thể thay đổi nó.
19. Một ví dụ const rất thực tế
Giả sử STM32 điều khiển động cơ:
#define MOTOR_MAX_SPEED 1000
hoặc:
const uint16_t MOTOR_MAX_SPEED = 1000;
Sau đó:
if (speed > MOTOR_MAX_SPEED)
{
speed = MOTOR_MAX_SPEED;
}
Nếu đây là một tham số không thay đổi trong chương trình, const giúp thể hiện rõ ý định.
20. Kết hợp với STRUCT của Bài 14
Bây giờ chúng ta nâng cấp kiến thức.
typedef enum
{
MOTOR_STOP,
MOTOR_RUN,
MOTOR_ERROR
} MotorState_t;
typedef struct
{
MotorState_t state;
uint16_t speed;
uint8_t errorCode;
} Motor_t;
Có thể có:
static Motor_t motor1;
Nghĩa là motor1 tồn tại suốt thời gian chương trình chạy và nếu đặt ở phạm vi file thì chỉ file đó sử dụng trực tiếp nó.
21. Một bài tập rất quan trọng
Hãy nhìn đoạn code:
static volatile uint8_t buttonFlag = 0;
void EXTI_Callback(void)
{
buttonFlag = 1;
}
void Main_Task(void)
{
if (buttonFlag == 1)
{
buttonFlag = 0;
// xử lý nút nhấn
}
}
Bạn hãy tự phân tích:
static làm gì?
→ buttonFlag có thời gian sống suốt chương trình; nếu ở phạm vi file thì nó còn được giới hạn trong file đó.
volatile làm gì?
→ Báo compiler rằng buttonFlag có thể thay đổi ngoài luồng code bình thường, ví dụ bởi interrupt.
uint8_t làm gì?
→ Chọn kiểu dữ liệu số nguyên không dấu 8 bit.
🧠 Bài tập Bài 15
Bài 1
Cho:
const uint16_t MAX_SPEED = 1000;
Câu hỏi:
MAX_SPEED = 2000;
có hợp lệ không?
Bài 2
Đoán kết quả:
void Counter(void)
{
static int count = 0;
count++;
printf("%d\n", count);
}
Nếu gọi:
Counter();
Counter();
Counter();
Counter();
Kết quả là gì?
Bài 3
Tại sao biến được thay đổi trong interrupt thường nên được xem xét khai báo:
volatile
?
Bài 4 – quan trọng nhất
Giải thích từng từ khóa trong:
static volatile uint8_t adcReady = 0;
Bài 5 – STM32 thực tế
Giả sử:
volatile uint8_t buttonFlag = 0;
Trong interrupt:
buttonFlag = 1;
Trong while(1):
if (buttonFlag == 1)
{
buttonFlag = 0;
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
Hãy giải thích luồng hoạt động:
Nút nhấn
↓
GPIO Interrupt
↓
buttonFlag = 1
↓
main()
↓
if(buttonFlag)
↓
Toggle PC13
🎯 Sau Bài 15 bạn cần thuộc lòng 3 câu
const→ Không cho code sửa giá trị qua đối tượng đó.
static→ Giữ trạng thái/lifetime và có thể giới hạn phạm vi của biến hoặc hàm.
volatile→ Giá trị có thể thay đổi ngoài luồng code bình thường; compiler phải thực hiện các truy cập theo yêu cầu của volatile.
Và từ Bài 16, chúng ta có thể bước sang một phần cực kỳ quan trọng để lập trình STM32:
BÀI 16 – BIT, BYTE, MASK, &, |, ^, ~, <<, >>: CÁCH STM32 THỰC SỰ ĐIỀU KHIỂN THANH GHI.
Đây sẽ là bài nối trực tiếp kiến thức C của bạn với GPIO, RCC, Timer, UART, ADC và các thanh ghi của STM32F103C8T6.