Sunday, June 12, 2016

Sử dụng từ khóa Volatile trong C như thế nào?



Bạn lập trình nhúng C?  Bạn đã gặp trường hợp khi build với chế độ debug thì code chạy đúng , nhưng khi bạn build release ( hoặc build để tối ưu) thì chương trình lại chạy lung ta lung tung chưa? Nếu bạn đã gặp rồi thì vấn đề có thể do bạn không khai báo volatile cho biến. Bài viết dưới đây sẽ cho bạn biết khi nào cần khai báo volatile.

Volatile là gì?

Volatile có nghĩa là không dự đoán được. Một biến sử dụng với volatile qualifier có nghĩa là nói với compiler là biến này có thể sẽ được thay đổi ở bất kì chỗ nào

Cú pháp khai báo
- Biến: int volatile var = 0;
- Con trỏ: int volatile *ptr;

Sử dụng Volatile

Chúng ta sẽ sử dụng volatile khi khai báo biến trong các trường hợp dưới đây:
- Con trỏ trỏ tới địa chỉ của các thanh ghi trong bộ nhớ (Memory - mapped peripheral register)
- Biến toàn cục dùng chung cả trong và ngoài ngắt (ISR Routine)
- Biến toàn cục dùng chung trong nhiều task trong ứng dụng multi-thread

Memory - mapped peripheral register

Xem xét đoạn code sau
//gan con tro ptr tới thanh ghi trạng thái có địa chỉ 0x1234
uint32_t *ptr = (uint32_t *) (0x1234);
//vòng lặp kiểm tra nếu giá trị thanh ghi khác 0 thì thoát
while (*ptr == 0);

Dòng code đầu tiên khai báo con trỏ ptr. Giả sử giá trị thanh ghi tại thời điểm này là 0 tức *ptr = 0. Khi build tối ưu, compiler có thể sẽ không nhìn thấy giá trị *ptr được sửa ở đâu nữa nên nó sẽ coi như đây là vòng lặp vô hạn luôn (do giá trị *ptr lúc đầu = 0). Do đó để tránh trường hợp trên chúng ta cần khai báo volatile cho biến con trỏ ptr


uint32_t volatile *ptr;


Nếu bạn mở file .h định nghĩa các thanh ghi của một vi điều khiển sẽ thấy họ sử dụng từ khóa volatile


ISR Routine

Xem xét đoạn code
uint8_t flag  = 0;
void main(void) {
while (!flag) {
//do something
}
}

void ISR_UART(void) {

//nhan ki tu tu UART
char c = UART_GetChar();
if (c = 'A') {
flag = 1;
}
}

Chúng ta thường hay sử dụng đoạn code như trên khi có ngắt sẽ set một cờ (flag) và sau đó trong hàm main() sẽ kiểm tra cờ để thực hiện một hành động nào đó. Tuy nhiên compiler không hề nhìn thấy là giá trị của flag có thể bị thay đổi ở trong ngắt, do đó nó tối ưu dẫn đến while(!flag) có thể trở thành một vòng lặp vô hạn. Do vậy trong trường hợp này chúng ta cũng nên thêm volatile vào trước biến flag

uint8_t volatile flag = 0;

Multi-thread

Xem xét đoạn code
uint8_t cnt;
void taskA() {
    cnt = 0;
    while (!cnt) {
    //do something
    }
}
void taskB() {
    cnt++;
}

Tương tự như 2 phần trên, compiler không biết là giá trị của biến cnt có thể thay đổi ở bên task B nên khi tối ưu sẽ dẫn đến việc chạy không đúng như ý muốn.

Một nhầm lẫn là tưởng sử dụng mutex để bảo vệ dữ liệu dùng chung sẽ giải quyết được vấn đề này.

int cnt ; 

task_a() { 
take(mutex); 
cnt += 1; 
give(mutex); 
}

task_b() { 

cnt = 0; 
take(mutex); 
if (cnt == 0) ... else ...; 
give(mutex); 
}

Trong đoạn code trên, compiler không biết cnt có thể thay đổi nên nó có thể lờ đi cả đoàn code if....else.

Tổng kết
Những lỗi gây ra do không sử dụng volatile có thể phát hiện ra sớm, nhưng với những trường hợp như sử dụng biến trong ngắt thì có thể rất lâu sau mới phát sinh lỗi. Vì vậy hãy sử dụng volatile bất cứ khi nào có thể :)


Tài liệu tham khảo

[1]. http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka3750.html
[2]. http://www.barrgroup.com/Embedded-Systems/How-To/C-Volatile-Keyword

8 comments:

  1. Cám ơn anh hôm nay em mới biết volatile.
    Cho em hỏi thêm là:
    Cái ví dụ dùng cờ flag trong hàm ngắt kia. Có thể khai báo extern int volatile flag; trong main.c và khai báo int volatile trong file xxx_it.c, khai báo vậy đúng ko ạ. Do các hàm ngắt hay để ở file khác!

    ReplyDelete
    Replies
    1. Biến toàn cục mà nằm ở nhiều file thì em phải thêm extern khi khai báo nhé. Phân biệt khai báo (declare) và định nghĩa (define) nhé

      Delete
    2. Cám ơn ơn!
      Bây giờ một trương trình có rất nhiều biến và e muốn gom chúng lại vào một file sau đó vào trương trình thì gọi include, nhưng trong file main.c e đã gọi include "var.h" rồi , tới một file khác e cũng gọi inclue "var.h" thì trương trình lại báo lỗi là khai báo trùng lặp. E chưa tìm được cách xử lý!

      Delete
  2. Em phân biệt khái niệm define và declare nhé.
    Trong var.h em sẽ khai báo (declare) int varibale;
    Trong một trong các file.c em phải định nghĩa nó (define) ví dụ int variable = 0; nếu em define ở nhiều file.c compiler sẽ báo là redefined

    ReplyDelete
  3. Hi bạn,

    Có lẽ bạn đã hiểu sai hoàn toàn volatile là gì. Sau khi đọc bài viết của bạn mình thấy có vấn đề nên mình đọc link reference của bài viết của bạn, và nó có ý nghĩa hoàn toàn khác.

    ReplyDelete
    Replies
    1. http://www.barrgroup.com/Embedded-Systems/How-To/C-Volatile-Keyword. Mình hiểu sai ở mục nào nhỉ. Nếu sai mình còn update lại

      Delete
  4. volatile dùng để nói với compiler rằng đây là 1 biến "khá" quan trọng, làm ơn đừng có optimize lung tung nhé. Bạn có thể viết 1 đoạn code delay dùng vòng lặp for để test. Khi không dùng volatile thì thời gian "có thể" sẽ khác mỗi 1 lần compiler hoặc khi bạn viết thêm code. Đối với các MCU 8 bits, điều này không quá quan trọng. Tuy nhiên với các MCU có SRAM lớn, việc truy cập các biến được lưu trong SRAM mà ở địa chỉ thấp hơn sẽ nhanh hơn các biến ở địa chỉ cao.

    ReplyDelete
    Replies
    1. Em chưa hiểu lắm tại sao việc truy cập các biến được lưu trong SRAM ở địa chỉ thấp lại nhanh hơn địa chỉ cao ạ? A có tài liệu nào nói về chỗ này không ạ

      Delete