🌱 Avoid Compiler Optimize với Volatile Keyword trong C
Trong lập trình C nhúng mình hay gặp khai báo biến với từ khóa volatile. Việc khai báo biến volatile rất cần thiết để tránh những lỗi sai khó phát hiện do tính năng optimization của compiler. Bài viết này sẽ cùng bạn tìm hiểu về từ khóa volatile trong lập trình C nhúng.
Đối với ngôn ngữ lập trình C áp dụng cho các vi điều khiển, thường yêu cầu lập trình viên quản lý các thanh ghi phần cứng và phản hồi các sự kiện không đồng bộ như ngắt. Trong bối cảnh này, từ khóa volatile trở thành một công cụ không thể thiếu trong kho vũ khí của lập trình viên C, đảm bảo hoạt động chính xác và ngăn ngừa các lỗi tinh vi có thể phát sinh từ quá trình tối ưu hóa trình biên dịch.
Trong quá trình compile chương trình vi điều khiển thường bật các option tối ưu hóa để giảm size code, nhưng từ đó cũng có thể gây ra các lỗi do compiler "không hiểu" về phần cứng!
Dưới đây là 3 trường hợp trong chương trình nhúng bị ảnh hưởng bởi compiler optimize, và cần được sử dụng của từ khóa volatile để chương trình hoạt động chính xác.
Thanh ghi ngoại vi có ánh xạ đến ô nhớ
Các thiết bị ngoại vi (GPIO, UART, ...) chứa các thanh ghi mà giá trị của nó có thể thay đổi ngoài ý muốn của dòng chương trình, đặc biệt là những thanh ghi trạng thái. Trong quá trình compile, compiler nhìn thấy thanh ghi đó chỉ là một giá trị địa chỉ cố định, nó sẽ nhận biết đây là một giá trị thay vì một ô nhớ.
Ví dụ: đợi một nút bấm, với địa chỉ thanh ghi GPIO tương ứng nút bấm được định nghĩa:
- /* Input Data Register Address */
- volatile uint32_t* GPIO_REGISTER = (volatile uint32_t*)0x40020810;
- /* When use the register */
- while( !(*GPIO_REGISTER & (1U << 13)) );
- Ở đây nếu không có Volatile, con trỏ GPIO_REGISTER sẽ đọc giá trị tại địa chỉ 0x40020810 (và giá trị này mặc định đang là 0).
- Khi đó compiler tối ưu - optimize điều kiện trong câu lệnh sau trở thành
while ( !(0 & (1 << 13)) ); - Tức là while (1);
- Và đương nhiên chương trình sẽ hoạt động sai.
Lưu ý: Còn một vấn đề nữa là các thanh ghi của PORT / GPIO thường chỉ cho phép access theo Word, nên khi chúng ta cố gắng access theo Byte mà không có keyword "volatile", chúng sẽ bị optimize thành lệnh không được cho phép (là access theo byte), do đó sẽ không ghi được giá trị vào thanh ghi IO.
Chương trình con phục vụ ngắt (ISR)
Ngắt - interrupt thường xuyên được sử dụng trong nhúng, ngắt ngoài, ngắt UART, SPI, Timer, ... và một thao tác quan trọng để giao tiếp giữa chương trình ngắt và chương trình chính là sử dụng biến global.
Trường hợp biến toàn cục (global) bị thay đổi trong chương trình con phục vụ ngắt, và trong main có sử dụng biến này. Compiler sẽ nhận biết chương trình chính không chạy vào ngắt (vì không có lệnh gọi hàm), và nếu biến global đó không được thay đổi ở vị trí nào khác trong chương trình, compiler sẽ coi biến đó không bị thay đổi và sẽ thay thế giá trị của biến trực tiếp vào các vị trí sử dụng biến đó!
Ví dụ sử dụng volatile với biến global trong ngắt.
- volatile uint32_t flag = 0U;
- void ISR_Routine(void)
- {
- flag = 1;
- }
- int main(void)
- {
- /* Initialize Program */
- while(1)
- {
- if( flag )
- {
- flag = 0U;
- /* Perform action when flag is set */
- }
- }
- }
- Một biến flag bị thay đổi trong ngắt, trong hàm main() đang dùng nó cho vòng while.
- Nếu không sử dụng volatile thì compiler không biết được biến flag này có thể thay đổi trong ngắt, và sẽ mặc định coi flag là một giá trị không đổi (luôn bằng 0).
- Chương trình sẽ không bao giờ nhảy vào câu lệnh if trong vòng lặp while(1).
Các ứng dụng multithread
Giống như trường hợp ISR, các biến toàn cục dùng trong nhiều tác vụ trong các ứng dụng đa luồng có thể bị thay đổi trong các Task khác nhau.
Ví dụ biến Global_Check có thể bị optimize nếu không được khai báo Volatile.
- #include <stdio.h>
- #include <pthread.h>
- #include <unistd.h>
- volatile int stop = 0; // Variables used to control loops
- void* thread_func(void* arg)
- {
- printf("Thread starts...\n");
- sleep(2); // Simulate work in 2 seconds
- stop = 1; // Stop the loop
- printf("Thread set stop = 1\n");
- return NULL;
- }
- int main(void)
- {
- pthread_t thread;
- // Create thread
- pthread_create(&thread, NULL, thread_func, NULL);
- // The main loop runs until stop == 1
- printf("Main loop begins...\n");
- while( stop == 0 )
- {
- // Without volatile, the compiler could optimize the loop to infinite.
- }
- printf("The main loop ends.\n");
- // Wait for thread to end
- pthread_join(thread, NULL);
- return 0;
- }
Các vi điều khiển 8-bits sẽ ít gặp các trường hợp này, có thể test bằng cách vào IDE và bật Optimize lên, hoặc sửa Compiler Option trong Makefile. Các dòng 32-bits thì cần chú ý dùng rất nhiều!
Cách sử dụng từ khóa volatile
Khai báo một biến với từ khóa volatile
:
volatile int data;
Hoặc với con trỏ:
volatile int *ptr;
>>>>>> Follow ngay <<<<<<<
Để nhận được những bài học miễn phí mới nhất nhé 😊
Chúc các bạn học tập tốt 😊
background nó là non-cache đúng hk nhỉ, đối với bare-metal ko cache thì trước khi action data nó sẽ update lại data đó.
Trả lờiXóa