Sử dụng không ổn định trong phát triển nhúng C


44

Tôi đã đọc một số bài viết và câu trả lời Stack Exchange về việc sử dụng volatiletừ khóa để ngăn trình biên dịch áp dụng bất kỳ tối ưu hóa nào trên các đối tượng có thể thay đổi theo cách mà trình biên dịch không thể xác định được.

Nếu tôi đang đọc từ một ADC (hãy gọi biến adcValue) và tôi đang khai báo biến này là toàn cục, tôi có nên sử dụng từ khóa volatiletrong trường hợp này không?

  1. Không sử dụng volatiletừ khóa

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
    
  2. Sử dụng volatiletừ khóa

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    volatile uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
    

Tôi đang hỏi câu hỏi này bởi vì khi gỡ lỗi, tôi có thể thấy không có sự khác biệt giữa cả hai cách tiếp cận mặc dù các thực tiễn tốt nhất nói rằng trong trường hợp của tôi (một biến toàn cục thay đổi trực tiếp từ phần cứng), thì việc sử dụng volatilelà bắt buộc.


1
Một số môi trường gỡ lỗi (chắc chắn là gcc) không áp dụng tối ưu hóa. Một bản dựng sản xuất thông thường sẽ (tùy thuộc vào sự lựa chọn của bạn). Điều này có thể dẫn đến sự khác biệt 'thú vị' giữa các bản dựng. Nhìn vào bản đồ đầu ra liên kết là thông tin.
Peter Smith

22
"trong trường hợp của tôi (Biến toàn cục thay đổi trực tiếp từ phần cứng)" - Biến toàn cục của bạn không bị thay đổi bởi phần cứng mà chỉ bởi mã C của bạn, trong đó trình biên dịch biết. - Sổ đăng ký phần cứng trong đó ADC cung cấp kết quả của nó, tuy nhiên, phải được ổn định, bởi vì trình biên dịch không thể biết nếu / khi giá trị của nó sẽ thay đổi (nó thay đổi nếu / khi phần cứng ADC kết thúc chuyển đổi.)
JimmyB

2
Bạn đã so sánh trình biên dịch được tạo bởi cả hai phiên bản? Điều đó sẽ cho bạn thấy những gì đang xảy ra dưới mui xe
Mawg

3
@stark: BIOS? Trên vi điều khiển? Không gian I / O được ánh xạ bộ nhớ sẽ không thể lưu vào bộ nhớ cache (nếu kiến ​​trúc thậm chí có bộ đệm dữ liệu ở vị trí đầu tiên, không được đảm bảo) bởi tính nhất quán thiết kế giữa các quy tắc bộ đệm và bản đồ bộ nhớ. Nhưng dễ bay hơi không có gì để làm với bộ điều khiển bộ nhớ cache.
Ben Voigt

1
@Davislor Tiêu chuẩn ngôn ngữ không cần phải nói gì thêm. Việc đọc vào một đối tượng dễ bay hơi sẽ thực hiện tải thực (ngay cả khi trình biên dịch gần đây đã thực hiện và thường sẽ biết giá trị là gì) và ghi vào đối tượng đó sẽ thực hiện lưu trữ thực (ngay cả khi cùng một giá trị được đọc từ đối tượng ). Vì vậy, trong văn if(x==1) x=1;bản có thể được tối ưu hóa cho một không dễ bay hơi xvà không thể được tối ưu hóa nếu xkhông ổn định. OTOH nếu cần các hướng dẫn đặc biệt để truy cập các thiết bị bên ngoài, bạn cần thêm các thiết bị đó (ví dụ: nếu một phạm vi bộ nhớ cần phải được ghi qua).
tò mò

Câu trả lời:


87

Một định nghĩa về volatile

volatilebáo cho trình biên dịch rằng giá trị của biến có thể thay đổi mà trình biên dịch không biết. Do đó trình biên dịch không thể giả sử giá trị không thay đổi chỉ vì chương trình C dường như không thay đổi.

Mặt khác, điều đó có nghĩa là giá trị của biến có thể được yêu cầu (đọc) ở một nơi khác mà trình biên dịch không biết, do đó phải đảm bảo rằng mọi phép gán cho biến thực sự được thực hiện như một thao tác ghi.

Trường hợp sử dụng

volatile được yêu cầu khi

  • đại diện cho các thanh ghi phần cứng (hoặc I / O được ánh xạ bộ nhớ) dưới dạng các biến - ngay cả khi thanh ghi sẽ không bao giờ được đọc, trình biên dịch không được bỏ qua thao tác ghi suy nghĩ "Lập trình viên ngu ngốc. Cố gắng lưu trữ một giá trị trong một biến mà anh ấy / cô ấy sẽ không bao giờ đọc lại. Anh ấy / cô ấy thậm chí sẽ không chú ý nếu chúng tôi bỏ qua bài viết. " Ngược lại, ngay cả khi chương trình không bao giờ ghi giá trị vào biến, giá trị của nó vẫn có thể bị thay đổi bởi phần cứng.
  • chia sẻ các biến giữa các bối cảnh thực hiện (ví dụ: ISR / chương trình chính) (xem câu trả lời của @ kkramo)

Ảnh hưởng của volatile

Khi một biến được khai báo volatile, trình biên dịch phải đảm bảo rằng mọi phép gán cho nó trong mã chương trình được phản ánh trong một thao tác ghi thực tế và mỗi lần đọc trong mã chương trình sẽ đọc giá trị từ bộ nhớ (mmapped).

Đối với các biến không biến động, trình biên dịch giả định rằng nó biết nếu / khi giá trị của biến thay đổi và có thể tối ưu hóa mã theo các cách khác nhau.

Đối với một, trình biên dịch có thể giảm số lần đọc / ghi vào bộ nhớ, bằng cách giữ giá trị trong các thanh ghi CPU.

Thí dụ:

void uint8_t compute(uint8_t input) {
  uint8_t result = input + 2;
  result = result * 2;
  if ( result > 100 ) {
    result -= 100;
  }
  return result;
}

Ở đây, trình biên dịch có thể sẽ không phân bổ RAM cho resultbiến và sẽ không bao giờ lưu trữ các giá trị trung gian ở bất cứ đâu ngoài một thanh ghi CPU.

Nếu resultkhông ổn định, mọi lần xuất hiện resulttrong mã C sẽ yêu cầu trình biên dịch thực hiện quyền truy cập vào RAM (hoặc cổng I / O), dẫn đến hiệu suất thấp hơn.

Thứ hai, trình biên dịch có thể sắp xếp lại các hoạt động trên các biến không biến động cho hiệu suất và / hoặc kích thước mã. Ví dụ đơn giản:

int a = 99;
int b = 1;
int c = 99;

có thể được đặt hàng lại để

int a = 99;
int c = 99;
int b = 1;

có thể lưu một lệnh trình biên dịch vì giá trị 99sẽ không phải tải hai lần.

Nếu a, bckhông ổn định, trình biên dịch sẽ phải phát ra các lệnh chỉ định các giá trị theo thứ tự chính xác như được đưa ra trong chương trình.

Một ví dụ kinh điển khác là như thế này:

volatile uint8_t signal;

void waitForSignal() {
  while ( signal == 0 ) {
    // Do nothing.
  }
}

Nếu trong trường hợp này signallà không volatile, trình biên dịch sẽ 'nghĩ' while( signal == 0 )có thể là một vòng lặp vô hạn (vì signalsẽ không bao giờ bị thay đổi bởi mã bên trong vòng lặp ) và có thể tạo ra tương đương với

void waitForSignal() {
  if ( signal != 0 ) {
    return; 
  } else {
    while(true) { // <-- Endless loop!
      // do nothing.
    }
  }
}

Cân nhắc xử lý các volatilegiá trị

Như đã nêu ở trên, một volatilebiến có thể đưa ra một hình phạt hiệu suất khi nó được truy cập thường xuyên hơn so với yêu cầu thực sự. Để giảm thiểu vấn đề này, bạn có thể "không biến động" giá trị bằng cách gán cho một biến không biến động, như

volatile uint32_t sysTickCount;

void doSysTick() {
  uint32_t ticks = sysTickCount; // A single read access to sysTickCount

  ticks = ticks + 1; 

  setLEDState( ticks < 500000L );

  if ( ticks >= 1000000L ) {
    ticks = 0;
  }
  sysTickCount = ticks; // A single write access to volatile sysTickCount
}

Điều này có thể đặc biệt có lợi trong ISR, nơi bạn muốn nhanh nhất có thể không truy cập cùng một phần cứng hoặc bộ nhớ nhiều lần khi bạn biết điều đó là không cần thiết vì giá trị sẽ không thay đổi trong khi ISR ​​của bạn đang chạy. Điều này là phổ biến khi ISR ​​là 'nhà sản xuất' các giá trị cho biến, như sysTickCounttrong ví dụ trên. Trên một AVR, sẽ rất khó khăn khi chức năng doSysTick()truy cập cùng bốn byte trong bộ nhớ (bốn hướng dẫn = 8 chu kỳ CPU cho mỗi lần truy cập sysTickCount) năm hoặc sáu lần thay vì chỉ hai lần, bởi vì lập trình viên biết rằng giá trị sẽ không được thay đổi từ một số mã khác trong khi anh ấy / cô ấy doSysTick()chạy.

Với thủ thuật này, về cơ bản, bạn thực hiện chính xác điều tương tự trình biên dịch thực hiện đối với các biến không biến động, tức là chỉ đọc chúng từ bộ nhớ khi nó phải, giữ giá trị trong một thanh ghi một thời gian và chỉ ghi lại vào bộ nhớ khi nó phải ; nhưng lần này, bạn biết rõ hơn trình biên dịch nếu / khi đọc / ghi phải xảy ra, vì vậy bạn giải phóng trình biên dịch khỏi tác vụ tối ưu hóa này và tự thực hiện.

Hạn chế của volatile

Truy cập phi nguyên tử

volatilekhông không cung cấp truy cập nguyên tử để biến nhiều từ. Đối với những trường hợp đó, bạn sẽ cần cung cấp loại trừ lẫn nhau bằng các phương tiện khác, ngoài việc sử dụng volatile. Trên AVR, bạn có thể sử dụng ATOMIC_BLOCKtừ <util/atomic.h>hoặc các cli(); ... sei();cuộc gọi đơn giản . Các macro tương ứng cũng hoạt động như một rào cản bộ nhớ, điều này rất quan trọng khi nói đến thứ tự truy cập:

Lệnh thực hiện

volatileáp đặt thứ tự thực hiện nghiêm ngặt chỉ đối với các biến dễ bay hơi khác. Điều này có nghĩa là, ví dụ

volatile int i;
volatile int j;
int a;

...

i = 1;
a = 99;
j = 2;

được đảm bảo trước tiên gán 1 cho isau đó gán 2 cho j. Tuy nhiên, nó không được đảm bảo asẽ được chỉ định ở giữa; trình biên dịch có thể thực hiện việc gán đó trước hoặc sau đoạn mã, về cơ bản bất cứ lúc nào cho đến lần đọc đầu tiên (hiển thị) a.

Nếu nó không dành cho hàng rào bộ nhớ của các macro được đề cập ở trên, trình biên dịch sẽ được phép dịch

uint32_t x;

cli();
x = volatileVar;
sei();

đến

x = volatileVar;
cli();
sei();

hoặc là

cli();
sei();
x = volatileVar;

(Để hoàn thiện, tôi phải nói rằng các rào cản bộ nhớ, giống như các rào cản được ngụ ý bởi các macro sei / cli, có thể thực sự cản trở việc sử dụng volatile, nếu tất cả các truy cập được đặt trong các rào cản này.)


7
Thảo luận tốt về việc không biến động cho hiệu suất :)
awjlogan

3
Tôi luôn muốn đề cập đến định nghĩa về tính không ổn định trong ISO / IEC 9899: 1999 6.7.3 (6): An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. Nhiều người nên đọc nó.
Jeroen3

3
Có thể đáng nói đến cli/ đó seilà giải pháp quá nặng nếu mục tiêu duy nhất của bạn là đạt được một rào cản bộ nhớ, không ngăn chặn sự gián đoạn. Các macro này tạo ra các lệnh thực tế cli/ seihướng dẫn và bộ nhớ clobber bổ sung, và chính việc ghi đè này dẫn đến rào cản. Để chỉ có một rào cản bộ nhớ mà không vô hiệu hóa các ngắt, bạn có thể xác định macro của riêng mình với phần thân như __asm__ __volatile__("":::"memory")(ví dụ mã lắp ráp trống với bộ nhớ đệm).
Ruslan

3
@NicHartley số C17 5.1.2.3 §6 định nghĩa hành vi có thể quan sát được : "Truy cập vào các đối tượng dễ bay hơi được đánh giá đúng theo các quy tắc của máy trừu tượng." Nhìn chung, tiêu chuẩn C không thực sự rõ ràng về những rào cản bộ nhớ cần thiết. Ở cuối một biểu thức sử dụng volatilecó một điểm thứ tự và mọi thứ sau nó phải được "giải trình tự sau". Có nghĩa là biểu hiện đó một rào cản bộ nhớ của các loại. Các nhà cung cấp trình biên dịch đã chọn truyền bá tất cả các loại huyền thoại để đặt trách nhiệm của các rào cản bộ nhớ lên lập trình viên nhưng điều đó vi phạm các quy tắc của "cỗ máy trừu tượng".
Lundin

2
@JimmyB Biến động cục bộ có thể hữu ích cho mã như volatile data_t data = {0}; set_mmio(&data); while (!data.ready);.
Maciej Piechotka

13

Từ khóa dễ bay hơi cho trình biên dịch biết rằng việc truy cập vào biến có tác dụng quan sát được. Điều đó có nghĩa là mỗi khi mã nguồn của bạn sử dụng biến, trình biên dịch PHẢI tạo quyền truy cập vào biến đó. Có thể là một truy cập đọc hoặc viết.

Tác động của điều này là bất kỳ thay đổi nào đối với biến ngoài luồng mã thông thường cũng sẽ được mã quan sát. Ví dụ: nếu một trình xử lý ngắt thay đổi giá trị. Hoặc nếu biến thực sự là một số thanh ghi phần cứng thay đổi.

Lợi ích tuyệt vời này cũng là nhược điểm của nó. Mỗi lần truy cập vào biến đều đi qua biến và giá trị không bao giờ được giữ trong một thanh ghi để truy cập nhanh hơn trong bất kỳ khoảng thời gian nào. Điều đó có nghĩa là một biến động sẽ chậm. Độ phóng đại chậm hơn. Vì vậy, chỉ sử dụng dễ bay hơi khi thực sự cần thiết.

Trong trường hợp của bạn, theo như bạn đã hiển thị mã, biến toàn cục chỉ được thay đổi khi bạn tự cập nhật nó adcValue = readADC();. Trình biên dịch biết khi nào điều này xảy ra và sẽ không bao giờ giữ giá trị của adcValue trong một thanh ghi trên một cái gì đó có thể gọi readFromADC()hàm. Hoặc bất kỳ chức năng nào nó không biết về. Hoặc bất cứ điều gì sẽ thao túng con trỏ có thể trỏ đến adcValuevà như vậy. Thực sự không cần biến động vì biến không bao giờ thay đổi theo những cách không thể đoán trước.


6
Tôi đồng ý với câu trả lời này nhưng "cường độ chậm hơn" nghe có vẻ quá thảm khốc.
kkrambo

6
Một thanh ghi CPU có thể được truy cập trong ít hơn một chu kỳ cpu trên các CPU siêu thanh hiện đại. Mặt khác, quyền truy cập vào bộ nhớ không bị chặn thực tế (hãy nhớ rằng một số phần cứng bên ngoài sẽ thay đổi điều này, do đó không cho phép lưu trữ CPU) trong phạm vi 100-300 chu kỳ CPU. Vì vậy, vâng, cường độ. Sẽ không quá tệ trên một bộ điều khiển vi mô hoặc tương tự nhưng câu hỏi không chỉ định phần cứng.
Goswin von Brederlow

7
Trong các hệ thống nhúng (vi điều khiển), hình phạt cho truy cập RAM thường ít hơn nhiều. Ví dụ, các AVR chỉ mất hai chu kỳ CPU để đọc hoặc ghi vào RAM (di chuyển thanh ghi đăng ký mất một chu kỳ), vì vậy tiết kiệm tối đa việc giữ mọi thứ trong phương pháp đăng ký (nhưng không bao giờ thực sự đạt được). 2 chu kỳ đồng hồ cho mỗi lần truy cập. - Tất nhiên, nói một cách tương đối, việc lưu một giá trị từ thanh ghi X vào RAM, sau đó tải lại ngay giá trị đó vào thanh ghi X để tính toán thêm sẽ mất 2x2 = 4 thay vì 0 chu kỳ (khi chỉ giữ giá trị trong X) và do đó là vô hạn chậm hơn :)
JimmyB

1
Đó là "cường độ chậm hơn" trong bối cảnh "viết hoặc đọc từ một biến cụ thể", vâng. Tuy nhiên, trong bối cảnh của một chương trình hoàn chỉnh có khả năng thực hiện nhiều hơn đáng kể so với việc đọc từ / ghi thành một biến nhiều lần, không, không thực sự. Trong trường hợp đó, sự khác biệt tổng thể có thể là 'nhỏ đến không đáng kể'. Cần thận trọng, khi đưa ra các xác nhận về hiệu suất, để làm rõ nếu khẳng định đó liên quan đến một op cụ thể hoặc toàn bộ chương trình. Làm chậm một op được sử dụng không thường xuyên bởi hệ số ~ 300x gần như không bao giờ là vấn đề lớn.
aroth

1
Ý bạn là, câu cuối cùng đó? Điều đó có nghĩa nhiều hơn theo nghĩa "tối ưu hóa sớm là gốc rễ của mọi tội lỗi". Rõ ràng là bạn không nên sử dụng volatilecho tất cả mọi thứ chỉ vì , nhưng bạn cũng không nên né tránh nó trong trường hợp bạn nghĩ rằng nó được gọi một cách hợp pháp vì lo lắng về hiệu suất được ưu tiên.
aroth

9

Việc sử dụng chính của từ khóa dễ bay hơi trên các ứng dụng C được nhúng là để đánh dấu một biến toàn cục được ghi trong một trình xử lý ngắt. Nó chắc chắn không phải là tùy chọn trong trường hợp này.

Không có nó, trình biên dịch không thể chứng minh rằng giá trị được ghi vào sau khi khởi tạo, bởi vì nó không thể chứng minh trình xử lý ngắt được gọi. Do đó, nó nghĩ rằng nó có thể tối ưu hóa các biến ra khỏi sự tồn tại.


2
Chắc chắn các ứng dụng thực tế khác tồn tại, nhưng imho đây là phổ biến nhất.
Abbeyatcu

1
Nếu giá trị chỉ được đọc trong ISR (và được thay đổi từ main ()), bạn có khả năng phải sử dụng biến động cũng như để đảm bảo quyền truy cập ATOMIC cho các biến đa byte.
Rev1.0

15
@ Rev1.0 Không, dễ bay hơi không đảm bảo tính thơm. Mối quan tâm đó phải được giải quyết một cách riêng biệt.
Chris Stratton

1
Không có đọc từ phần cứng cũng như bất kỳ gián đoạn trong mã được đăng. Bạn đang giả định những điều từ câu hỏi không có ở đó. Nó không thể thực sự được trả lời ở dạng hiện tại của nó.
Lundin

3
"đánh dấu một biến toàn cục được ghi vào một trình xử lý ngắt" không. Đó là để đánh dấu một biến; toàn cầu hoặc cách khác; rằng nó có thể được thay đổi bởi một cái gì đó bên ngoài sự hiểu biết của trình biên dịch. Ngắt không bắt buộc. Nó có thể được chia sẻ bộ nhớ hoặc ai đó gắn đầu dò vào bộ nhớ (sau này không được khuyến nghị cho bất kỳ thứ gì hiện đại hơn 40 năm)
UKMonkey

9

Có hai trường hợp bạn phải sử dụng volatiletrong các hệ thống nhúng.

  • Khi đọc từ một thanh ghi phần cứng.

    Điều đó có nghĩa là, chính thanh ghi ánh xạ bộ nhớ, một phần của các thiết bị ngoại vi phần cứng bên trong MCU. Nó có thể sẽ có một số tên khó hiểu như "ADC0DR". Thanh ghi này phải được xác định bằng mã C, thông qua một số bản đồ đăng ký được cung cấp bởi nhà cung cấp công cụ hoặc bởi chính bạn. Để tự làm, bạn sẽ làm (giả sử đăng ký 16 bit):

    #define ADC0DR (*(volatile uint16_t*)0x1234)

    trong đó 0x1234 là địa chỉ nơi MCU đã ánh xạ thanh ghi. Vì volatileđã là một phần của macro trên, mọi quyền truy cập vào nó sẽ đủ điều kiện dễ bay hơi. Vì vậy, mã này là tốt:

    uint16_t adc_data;
    adc_data = ADC0DR;
  • Khi chia sẻ một biến giữa ISR và mã liên quan bằng kết quả của ISR.

    Nếu bạn có một cái gì đó như thế này:

    uint16_t adc_data = 0;
    
    void adc_stuff (void)
    {
      if(adc_data > 0)
      {
        do_stuff(adc_data);
      } 
    }
    
    interrupt void ADC0_interrupt (void)
    {
      adc_data = ADC0DR;
    }

    Sau đó, trình biên dịch có thể nghĩ: "adc_data luôn là 0 vì nó không được cập nhật ở bất cứ đâu. Và hàm ADC0_interrupt () không bao giờ được gọi, vì vậy biến không thể thay đổi". Trình biên dịch thường không nhận ra rằng các ngắt được gọi bằng phần cứng, không phải bằng phần mềm. Vì vậy, trình biên dịch đi và loại bỏ mã if(adc_data > 0){ do_stuff(adc_data); }vì nó nghĩ rằng nó không bao giờ có thể là sự thật, gây ra một lỗi rất lạ và khó gỡ lỗi.

    Bằng cách khai báo adc_data volatile, trình biên dịch không được phép đưa ra bất kỳ giả định nào như vậy và nó không được phép tối ưu hóa việc truy cập vào biến.


Lưu ý quan trọng:

  • Một ISR sẽ luôn được khai báo bên trong trình điều khiển phần cứng. Trong trường hợp này, ADC ISR phải nằm trong trình điều khiển ADC. Không ai khác ngoài tài xế nên giao tiếp với ISR ​​- mọi thứ khác là lập trình spaghetti.

  • Khi viết C, tất cả thông tin liên lạc giữa ISR và chương trình nền phải được bảo vệ trước các điều kiện chủng tộc. Luôn luôn , mọi lúc, không có ngoại lệ. Kích thước của bus dữ liệu MCU không thành vấn đề, bởi vì ngay cả khi bạn thực hiện một bản sao 8 bit duy nhất bằng C, ngôn ngữ không thể đảm bảo tính nguyên tử của các hoạt động. Không trừ khi bạn sử dụng tính năng C11 _Atomic. Nếu tính năng này không khả dụng, bạn phải sử dụng một số cách sử dụng semaphore hoặc vô hiệu hóa ngắt trong khi đọc, vv Trình biên dịch nội tuyến là một tùy chọn khác. volatilekhông đảm bảo tính nguyên tử.

    Điều gì có thể xảy ra là thế này:
    -Tải giá trị từ ngăn xếp vào thanh ghi
    -Xuất hiện xảy ra
    -Sử dụng giá trị từ thanh ghi

    Và sau đó, không có vấn đề gì nếu phần "giá trị sử dụng" chỉ là một lệnh. Đáng buồn thay, một phần đáng kể của tất cả các lập trình viên hệ thống nhúng đều không biết điều này, có lẽ làm cho nó trở thành lỗi hệ thống nhúng phổ biến nhất từ ​​trước đến nay. Luôn luôn ngắt quãng, khó khiêu khích, khó tìm.


Một ví dụ về trình điều khiển ADC được viết chính xác sẽ trông như thế này (giả sử C11 _Atomickhông khả dụng):

quảng cáo

// adc.h
#ifndef ADC_H
#define ADC_H

/* misc init routines here */

uint16_t adc_get_val (void);

#endif

quảng cáo

// adc.c
#include "adc.h"

#define ADC0DR (*(volatile uint16_t*)0x1234)

static volatile bool semaphore = false;
static volatile uint16_t adc_val = 0;

uint16_t adc_get_val (void)
{
  uint16_t result;
  semaphore = true;
    result = adc_val;
  semaphore = false;
  return result;
}

interrupt void ADC0_interrupt (void)
{
  if(!semaphore)
  {
    adc_val = ADC0DR;
  }
}
  • Mã này giả định rằng một ngắt không thể bị gián đoạn trong chính nó. Trên các hệ thống như vậy, một boolean đơn giản có thể hoạt động như semaphore và nó không cần phải là nguyên tử, vì sẽ không có hại nếu ngắt xảy ra trước khi boolean được đặt. Mặt trái của phương pháp đơn giản hóa ở trên là nó sẽ loại bỏ các lần đọc ADC khi điều kiện cuộc đua xảy ra, sử dụng giá trị trước đó để thay thế. Điều này cũng có thể tránh được, nhưng sau đó mã trở nên phức tạp hơn.

  • Ở đây volatilebảo vệ chống lại lỗi tối ưu hóa. Nó không liên quan gì đến dữ liệu bắt nguồn từ một thanh ghi phần cứng, chỉ có điều dữ liệu được chia sẻ với ISR.

  • staticbảo vệ chống lại lập trình spaghetti và ô nhiễm không gian tên, bằng cách biến biến cục bộ thành trình điều khiển. (Điều này tốt trong các ứng dụng đơn lõi, đơn luồng, nhưng không phải trong các ứng dụng đa luồng.)


Khó gỡ lỗi là tương đối, nếu mã bị xóa, bạn sẽ nhận thấy rằng mã có giá trị của bạn đã biến mất - đó là một tuyên bố khá táo bạo rằng có gì đó không ổn. Nhưng tôi đồng ý, có thể có các hiệu ứng gỡ lỗi rất lạ và khó.
Arsenal

@Arsenal Nếu bạn có một trình gỡ lỗi đẹp, sắp xếp trình biên dịch chương trình với C và bạn biết ít nhất một chút asm, thì có thể dễ dàng nhận ra. Nhưng đối với mã phức tạp lớn hơn, một khối lớn asm do máy tạo ra không phải là chuyện nhỏ. Hoặc nếu bạn không biết asm. Hoặc nếu trình gỡ lỗi của bạn là tào lao và không hiển thị asm (hoeclipsecough).
Lundin

Có thể tôi hơi hư hỏng khi sử dụng trình gỡ lỗi Lauterbach sau đó. Nếu bạn cố gắng thiết lập một điểm dừng trong mã được tối ưu hóa, nó sẽ đặt nó ở một nơi khác và bạn biết điều gì đó đang diễn ra ở đó.
Arsenal

@Arsenal Yep, loại C / asm hỗn hợp mà bạn có thể nhận được trong Lauterbach không có nghĩa là tiêu chuẩn. Hầu hết các trình sửa lỗi hiển thị mã asm trong một cửa sổ riêng biệt, nếu có.
Lundin

semaphorechắc chắn sẽ có volatile! Trong thực tế, đó là các trường hợp sử dụng cơ bản nhất Mà đòi hỏi volatile: Tín hiệu điều gì đó từ một bối cảnh thực hiện khác. - Trong ví dụ của bạn, trình biên dịch có thể bỏ qua semaphore = true;vì nó 'thấy' rằng giá trị của nó không bao giờ được đọc trước khi nó bị ghi đè bởi semaphore = false;.
JimmyB

5

Trong các đoạn mã được trình bày trong câu hỏi, vẫn chưa có lý do để sử dụng biến động. Không có liên quan rằng giá trị adcValueđến từ một ADC. Và mang tính adcValuetoàn cầu sẽ khiến bạn nghi ngờ về việc có adcValuenên biến động hay không nhưng đó không phải là lý do.

Trở thành toàn cầu là một đầu mối vì nó mở ra khả năng adcValuecó thể được truy cập từ nhiều hơn một bối cảnh chương trình. Một bối cảnh chương trình bao gồm một trình xử lý ngắt và một nhiệm vụ RTOS. Nếu biến toàn cục bị thay đổi bởi một bối cảnh thì bối cảnh chương trình khác không thể cho rằng họ biết giá trị từ lần truy cập trước. Mỗi bối cảnh phải đọc lại giá trị biến mỗi lần họ sử dụng vì giá trị có thể đã bị thay đổi trong ngữ cảnh chương trình khác nhau. Một bối cảnh chương trình không nhận biết khi nào xảy ra gián đoạn hoặc chuyển đổi tác vụ, do đó, nó phải giả định rằng bất kỳ biến toàn cục nào được sử dụng bởi nhiều bối cảnh có thể thay đổi giữa mọi truy cập của biến do chuyển đổi ngữ cảnh có thể. Đây là những gì tuyên bố dễ bay hơi là cho. Nó báo cho trình biên dịch rằng biến này có thể thay đổi bên ngoài ngữ cảnh của bạn, vì vậy hãy đọc nó mỗi lần truy cập và đừng cho rằng bạn đã biết giá trị.

Nếu biến được ánh xạ bộ nhớ đến một địa chỉ phần cứng, thì những thay đổi được thực hiện bởi phần cứng thực sự là một bối cảnh khác bên ngoài bối cảnh của chương trình của bạn. Vì vậy, bản đồ bộ nhớ cũng là một đầu mối. Ví dụ: nếu readADC()hàm của bạn truy cập giá trị ánh xạ bộ nhớ để lấy giá trị ADC thì biến ánh xạ bộ nhớ đó có thể sẽ không ổn định.

Vì vậy, quay trở lại câu hỏi của bạn, nếu có nhiều mã hơn và adcValueđược truy cập bởi mã khác chạy trong một ngữ cảnh khác, thì có, adcValuesẽ không ổn định.


4

"Biến toàn cầu thay đổi trực tiếp từ phần cứng"

Chỉ vì giá trị đến từ một số thanh ghi ADC phần cứng, không có nghĩa là nó được "thay đổi" trực tiếp bởi phần cứng.

Trong ví dụ của bạn, bạn chỉ cần gọi readADC (), trả về một số giá trị thanh ghi ADC. Điều này là tốt đối với trình biên dịch, biết rằng adcValue được gán một giá trị mới tại thời điểm đó.

Sẽ khác nếu bạn sử dụng thói quen ngắt ADC để gán giá trị mới, được gọi khi giá trị ADC mới sẵn sàng. Trong trường hợp đó, trình biên dịch sẽ không có manh mối về thời điểm ISR tương ứng được gọi và có thể quyết định rằng adcValue sẽ không được truy cập theo cách này. Đây là nơi dễ bay hơi sẽ giúp.


1
Vì mã của bạn không bao giờ "gọi" hàm ISR, Compiler thấy biến đó chỉ được cập nhật trong một hàm mà không ai gọi. Vì vậy, trình biên dịch tối ưu hóa nó.
Swanand

1
Nó phụ thuộc vào phần còn lại của mã, nếu adcValue không được đọc ở bất cứ đâu (như chỉ đọc qua trình gỡ lỗi) hoặc nếu nó chỉ đọc một lần ở một nơi, trình biên dịch sẽ có khả năng tối ưu hóa nó.
Damien

2
@Damien: Nó luôn "phụ thuộc", nhưng tôi đã nhắm đến việc giải quyết câu hỏi thực tế "Tôi có nên sử dụng từ khóa không ổn định trong trường hợp này không?" Càng ngắn càng tốt.
Rev1.0

4

Hành vi của volatileđối số phần lớn phụ thuộc vào mã của bạn, trình biên dịch và tối ưu hóa được thực hiện.

Có hai trường hợp sử dụng mà cá nhân tôi sử dụng volatile:

  • Nếu có một biến tôi muốn xem xét với trình gỡ lỗi, nhưng trình biên dịch đã tối ưu hóa nó (có nghĩa là nó đã xóa nó vì nó không cần phải có biến này), việc thêm volatilesẽ buộc trình biên dịch giữ nó và do đó có thể được nhìn thấy trên gỡ lỗi.

  • Nếu biến có thể thay đổi "ngoài mã", thông thường nếu bạn có một số phần cứng truy cập vào nó hoặc nếu bạn ánh xạ biến trực tiếp đến một địa chỉ.

Trong nhúng cũng đôi khi có một số lỗi trong trình biên dịch, thực hiện tối ưu hóa mà thực sự không hoạt động và đôi khi volatilecó thể giải quyết các vấn đề.

Cho bạn biến của bạn được khai báo trên toàn cầu, nó có thể sẽ không được tối ưu hóa, miễn là biến đó được sử dụng trên mã, ít nhất là được viết và đọc.

Thí dụ:

void test()
{
    int a = 1;
    printf("%i", a);
}

Trong trường hợp này, biến có thể sẽ được tối ưu hóa thành printf ("% i", 1);

void test()
{
    volatile int a = 1;
    printf("%i", a);
}

sẽ không được tối ưu hóa

Một số khác:

void delay1Ms()
{
    unsigned int i;
    for (i=0; i<10; i++)
    {
        delay10us( 10);
    }
}

Trong trường hợp này, trình biên dịch có thể tối ưu hóa bằng (nếu bạn tối ưu hóa tốc độ) và do đó loại bỏ biến

void delay1Ms()
{
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
}

Đối với trường hợp sử dụng của bạn, "nó có thể phụ thuộc" vào phần còn lại của mã của bạn, cách adcValuesử dụng ở nơi khác và cài đặt phiên bản / tối ưu hóa trình biên dịch bạn sử dụng.

Đôi khi có thể gây khó chịu khi có một mã hoạt động mà không tối ưu hóa, nhưng bị phá vỡ một khi được tối ưu hóa.

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
}

Điều này có thể được tối ưu hóa thành printf ("% i", readADC ());

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
  callAnotherFunction(adcValue);
}

-

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
}

void anotherFunction()
{
   // Do something with adcValue
}

Chúng có thể sẽ không được tối ưu hóa, nhưng bạn không bao giờ biết "trình biên dịch tốt như thế nào" và có thể thay đổi với các tham số của trình biên dịch. Thông thường trình biên dịch với tối ưu hóa tốt được cấp phép.


1
Ví dụ a = 1; b = a; và c = b; trình biên dịch có thể nghĩ rằng đợi một phút, a và b là vô ích, hãy đặt trực tiếp 1 đến c. Tất nhiên bạn sẽ không làm điều đó trong mã của mình, nhưng trình biên dịch tốt hơn bạn trong việc tìm kiếm những mã này, ngoài ra nếu bạn cố gắng viết mã được tối ưu hóa ngay lập tức thì sẽ không thể đọc được.
Damien

2
Một mã chính xác với trình biên dịch đúng sẽ không bị phá vỡ khi tối ưu hóa được bật. Tính chính xác của trình biên dịch là một vấn đề, nhưng ít nhất với IAR tôi đã không gặp phải tình huống tối ưu hóa dẫn đến phá mã ở nơi không nên.
Arsenal

5
Rất nhiều trường hợp tối ưu hóa phá vỡ mã là khi bạn mạo hiểm vào lãnh thổ UB ..
đường ống

2
Vâng, một tác dụng phụ của sự không ổn định là nó có thể hỗ trợ gỡ lỗi. Nhưng đó không phải là một lý do tốt để sử dụng dễ bay hơi. Bạn có thể nên tắt tối ưu hóa nếu gỡ lỗi dễ dàng là mục tiêu của bạn. Câu trả lời này thậm chí không đề cập đến ngắt.
kkrambo

2
Thêm vào đối số gỡ lỗi, volatilebuộc trình biên dịch lưu trữ một biến trong RAM và cập nhật RAM đó ngay khi một giá trị được gán cho biến đó. Hầu hết thời gian, trình biên dịch không 'xóa' các biến, bởi vì chúng ta thường không viết các bài tập mà không có hiệu lực, nhưng nó có thể quyết định giữ biến trong một số thanh ghi CPU và sau đó có thể hoặc không bao giờ ghi giá trị của thanh ghi đó vào RAM. Trình gỡ lỗi thường thất bại trong việc định vị thanh ghi CPU trong đó biến được giữ và do đó không thể hiển thị giá trị của nó.
JimmyB

1

Rất nhiều giải thích kỹ thuật nhưng tôi muốn tập trung vào ứng dụng thực tế.

Các volatilelực lượng từ khóa trình biên dịch để đọc hoặc ghi giá trị của biến từ bộ nhớ mỗi khi nó được sử dụng. Thông thường trình biên dịch sẽ cố gắng tối ưu hóa nhưng không thực hiện đọc và ghi không cần thiết, ví dụ bằng cách giữ giá trị trong thanh ghi CPU thay vì truy cập bộ nhớ mỗi lần.

Điều này có hai cách sử dụng chính trong mã nhúng. Đầu tiên, nó được sử dụng cho các thanh ghi phần cứng. Các thanh ghi phần cứng có thể thay đổi, ví dụ thanh ghi kết quả ADC có thể được viết bởi thiết bị ngoại vi ADC. Thanh ghi phần cứng cũng có thể thực hiện các hành động khi truy cập. Một ví dụ phổ biến là thanh ghi dữ liệu của UART, thường xóa các cờ ngắt khi đọc.

Trình biên dịch thường sẽ cố gắng tối ưu hóa việc đọc và ghi lặp lại của thanh ghi với giả định rằng giá trị sẽ không bao giờ thay đổi, do đó không cần phải tiếp tục truy cập nó, nhưng volatiletừ khóa sẽ buộc nó phải thực hiện thao tác đọc mỗi lần.

Việc sử dụng phổ biến thứ hai là cho các biến được sử dụng bởi cả mã ngắt và mã không ngắt. Các ngắt không được gọi trực tiếp, vì vậy trình biên dịch không thể xác định khi nào chúng sẽ thực thi và do đó giả định rằng mọi truy cập bên trong ngắt không bao giờ xảy ra. Bởi vì volatiletừ khóa buộc trình biên dịch truy cập vào biến mỗi lần, giả định này bị loại bỏ.

Điều quan trọng cần lưu ý là volatiletừ khóa không phải là giải pháp hoàn chỉnh cho những vấn đề này, và phải cẩn thận để tránh chúng. Ví dụ, trên hệ thống 8 bit, biến 16 bit yêu cầu hai lần truy cập bộ nhớ để đọc hoặc ghi, và do đó ngay cả khi trình biên dịch buộc phải thực hiện các truy cập đó, chúng xảy ra tuần tự và phần cứng có thể hoạt động trong lần truy cập đầu tiên hoặc một sự gián đoạn xảy ra giữa hai.


0

Trong trường hợp không có volatilevòng loại, giá trị của một đối tượng có thể được lưu trữ ở nhiều nơi trong một số phần nhất định của mã. Ví dụ, hãy xem xét một cái gì đó như:

int foo;
int someArray[64];
void test(void)
{
  int i;
  foo = 0;
  for (i=0; i<64; i++)
    if (someArray[i] > 0)
      foo++;
}

Trong những ngày đầu của C, một trình biên dịch sẽ xử lý câu lệnh

foo++;

thông qua các bước:

load foo into a register
increment that register
store that register back to foo

Tuy nhiên, các trình biên dịch phức tạp hơn sẽ nhận ra rằng nếu giá trị của "foo" được giữ trong một thanh ghi trong vòng lặp, thì nó sẽ chỉ cần được tải một lần trước vòng lặp và được lưu trữ một lần sau đó. Tuy nhiên, trong vòng lặp, điều đó có nghĩa là giá trị của "foo" đang được giữ ở hai nơi - trong kho lưu trữ toàn cầu và trong sổ đăng ký. Đây sẽ không phải là vấn đề nếu trình biên dịch có thể thấy tất cả các cách mà "foo" có thể được truy cập trong vòng lặp, nhưng có thể gây rắc rối nếu giá trị của "foo" được truy cập trong một số cơ chế mà trình biên dịch không biết về ( chẳng hạn như một trình xử lý ngắt).

Các tác giả của Tiêu chuẩn có thể bổ sung một vòng loại mới có thể mời rõ ràng trình biên dịch để tối ưu hóa như vậy, và nói rằng ngữ nghĩa lỗi thời sẽ áp dụng trong trường hợp không có, nhưng trường hợp tối ưu hóa rất hữu ích những nơi mà nó sẽ có vấn đề, vì vậy, Tiêu chuẩn thay vào đó cho phép các trình biên dịch cho rằng các tối ưu hóa như vậy là an toàn trong trường hợp không có bằng chứng cho thấy chúng không có. Mục đích của volatiletừ khóa là cung cấp bằng chứng như vậy.

Một vài điểm bất đồng giữa một số người viết trình biên dịch và lập trình viên xảy ra với các tình huống như:

unsigned short volatile *volatile output_ptr;
unsigned volatile output_count;

void interrupt_handler(void)
{
  if (output_count)
  {
    *((unsigned short*)0xC0001230) = *output_ptr; // Hardware I/O register
    *((unsigned short*)0xC0001234) = 1; // Hardware I/O register
    *((unsigned short*)0xC0001234) = 0; // Hardware I/O register
    output_ptr++;
    output_count--;
  }
}

void output_data_via_interrupt(unsigned short *dat, unsigned count)
{
  output_ptr = dat;
  output_count = count;
  while(output_count)
     ; // Wait for interrupt to output the data
}

unsigned short output_buffer[10];

void test(void)
{
  output_buffer[0] = 0x1234;
  output_data_via_interrupt(output_buffer, 1);
  output_buffer[0] = 0x2345;
  output_buffer[1] = 0x6789;
  output_data_via_interrupt(output_buffer,2);
}

Về mặt lịch sử, hầu hết các trình biên dịch sẽ cho phép khả năng viết một volatilevị trí lưu trữ có thể kích hoạt các tác dụng phụ tùy ý và tránh lưu trữ bất kỳ giá trị nào trong các thanh ghi trên một cửa hàng như vậy, nếu không chúng sẽ không lưu các giá trị lưu trữ trong các thanh ghi qua các lệnh gọi đến các hàm không đủ điều kiện "nội tuyến" và do đó sẽ ghi 0x1234 vào output_buffer[0], thiết lập mọi thứ để xuất dữ liệu, đợi dữ liệu hoàn tất, sau đó viết 0x2345 đến output_buffer[0]và tiếp tục từ đó. Tiêu chuẩn không yêu cầu triển khai để xử lý hành vi lưu trữ địa chỉ output_buffervàovolatileTuy nhiên, con trỏ đủ tiêu chuẩn là dấu hiệu cho thấy điều gì đó có thể xảy ra với nó thông qua nghĩa là trình biên dịch không hiểu, vì các tác giả nghĩ trình biên dịch các trình biên dịch dành cho các nền tảng khác nhau và mục đích sẽ nhận ra khi phục vụ các mục đích đó trên các nền tảng đó mà không cần phải nói Do đó, một số trình biên dịch "thông minh" như gcc và clang sẽ cho rằng mặc dù địa chỉ của output_buffernó được ghi vào một con trỏ đủ điều kiện dễ bay hơi giữa hai cửa hàng output_buffer[0], nhưng không có lý do gì để cho rằng mọi thứ có thể quan tâm đến giá trị được giữ trong đối tượng đó tại thời điểm đó

Hơn nữa, trong khi các con trỏ được truyền trực tiếp từ các số nguyên hiếm khi được sử dụng cho bất kỳ mục đích nào ngoài việc thao túng mọi thứ theo cách mà trình biên dịch không có khả năng hiểu, thì Tiêu chuẩn lại không yêu cầu trình biên dịch xử lý các truy cập như vậy volatile. Do đó, lần viết đầu tiên *((unsigned short*)0xC0001234)có thể bị bỏ qua bởi các trình biên dịch "thông minh" như gcc và clang, bởi vì những người duy trì các trình biên dịch đó thà tuyên bố rằng mã bỏ qua việc đủ điều kiện như vậy volatilelà "bị hỏng" hơn là nhận ra rằng tính tương thích đó là mã hữu ích . Rất nhiều tệp tiêu đề do nhà cung cấp cung cấp bỏ qua volatilevòng loại và trình biên dịch tương thích với các tệp tiêu đề do nhà cung cấp cung cấp sẽ hữu ích hơn các tệp tiêu đề không do nhà cung cấp cung cấp sẽ hữu ích hơn các tệp không có.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.