Volatile

Материал из eeWiki - открытая энциклопедия по электронике
Перейти к: навигация, поиск

volatile для "чайников"

Вступление

Данная статья предназначена для программистов встраиваемых систем, в основном - для начинающих работать с языком Си, но опытные программисты, надеюсь, тоже смогут что-то для себя почерпнуть. При написании статьи я, возможно, чего-то не учел или где-то ошибся. Буду рад любым поправкам и любой критике.

Разбирая чужие исходники, часто натыкаюсь на ошибки программистов, связанные с недопониманием назначения квалификатора volatile. Результатом такого недопонимания является код, который дает редкие, совершенно непредсказуемые и, зачастую, очень разрушительные и необратимые сбои. Это особенно актуально для микроконтроллерных систем, где, во-первых, обработчики прерываний являются частью прикладного кода, а, во-вторых, регистры управления периферией отображаются в RAM общего назначения. Ошибки, связанные с неиспользованием (или неправильным использованием) квалификатора volatile трудно поддаются отладке из-за их непредсказуемости и неповторяемости. Бывает, что Си-код, в котором не предусмотрено использование volatile там, где его надо бы использовать, прекрасно работает, будучи собранным одним компилятором, и сбоит (или не работает вовсе), когда собирается другим.

Большинство примеров, приведенных в этой статье, написаны для компилятора GCC (Mplab C30), потому что, учитывая архитектуру ядра PIC24 и особенности компилятора, для него проще всего синтезировать маленькие наглядные примеры, в которых будет проявляться неправильное обращение с volatile. Многие из этих примеров будут собираться совершенно корректно на других (более простых) компиляторах, таких как PICC или MicroC. Но это не значит, что при работе с этими компиляторами ошибки неиспользования volatile не проявляются вовсе. Просто код демонстрации для этих компиляторов выглядел бы намного больше и сложнее.

Определение

(volatile в переводе с английского означает "нестабильный", "изменчивый")

Итак, volatile в языке Си - это квалификатор переменной, говорящий компилятору, что значение переменной может быть изменено в любой момент и что часть кода, которая производит над этой переменной какие-то действия (чтение или запись), не должна быть оптимизирована.

Что это значит? Известно, что одной из характеристик компиляторов, говорящих за их качество, является способность оптимизировать генерируемый объектный код. Для этого они объединяют повторяющиеся конструкции, сохраняют в регистрах общего назначения промежуточные результаты вычислений, выстраивают последовательность команд так, чтобы минимизировать долго выполняющиеся фрагменты кода (например, обращение через косвенную адресацию), и т.д. Выполняя такую оптимизацию, они немного преобразует наш код, подменяя его идентичным с точки зрения алгоритма, но более быстрым и/или компактным. Но такую подмену можно делать не всегда. Рассмотрим пример:

  1.  
  2. char a = 0;
  3. ...
  4. a |= 1;
  5. a |= 2;
  6. ...
  7.  

С точки зрения алгоритма устанавливаются два младших разряда в переменной a. Оптимизатор может сделать подмену такого кода одним оператором:

  1.  
  2. a |= 3;
  3.  

выиграв таким образом пару тактов и пару ячеек ROM. Но представим себе, что эти же действия мы выполняем не над какой-то абстрактной переменной, а над периферийным регистром:

  1.  
  2. PORTB |= 1;
  3. PORTB |= 2;
  4.  

Вот в этом случае оптимизация с заменой на "PORTB |= 3" нас может не устроить! Управляя напрямую состояниями выводов контроллера, нам часто бывает важна последовательность изменения сигналов. Например, мы формируем сигналы SPI, и один вывод (PORTB.0) - это данные, а другой (PORTB.1) - синхроимпульсы. В этом случае нам нельзя изменять состояния этих выводов одновременно, т.к. при этом нельзя гарантировать, что управляемая микросхема по синхроимпульсу получит правильные данные. И уж тем более, нам бы не хотелось, чтобы оптимизации подвергся код, формирующий синхроимпульс длительностью в один такт:

  1.  
  2. PORTB |= 2;
  3. PORTB &= ~2;
  4.  

Такой код мог бы быть воспринят компилятором как два взаимообратных действия, и первая строка могла бы не попасть в результирующий объектный код. Однако на практике мы видим, что такой оптимизации не производится. Так происходит именно потому, что переменная, на которую отображается регистр PORTB, объявлена с квалификатором volatile, например:

  1.  
  2. extern volatile unsigned char PORTB @ 0x06; // В HT-PICC
  3. extern volatile near unsigned char PORTB; // В MCC18
  4. extern volatile near unsigned int PORTB __attribute__((__sfr__)); // В MCC30
  5. и т.д.
  6.  

(в этом можно убедиться, заглянув в заголовочный файл для конкретного контроллера, поставляемый с компилятором). Квалификатор volatile запрещает производить оптимизацию кода, выполняющего действия над регистром PORTB. Поэтому даже взаимообратные действия останутся нетронутыми оптимизатором, и мы можем быть уверенны в том, что на выходе сформируется импульс.

Итак, выполняя оптимизацию, компиляторы стремятся помочь нам сделать наш код максимально быстрым и максимально компактным. Однако, если не использовать volatile, в некоторых случаях оптимизация может сыграть с программистом злую шутку. Причем, надо сказать, чем умнее и мощнее компилятор, тем больше проблем он может создать при неграмотном использовании volatile.

Ошибки, связанные с volatile

Есть три основных типа ошибок, касающихся квалификатора volatile:

  • неиспользование volatile там, где нужно
    • обычно совершается программистами, которые не знают про существование volatile, или видели, но не понимают, что это такое;
  • использование volatile там, где нужно, но не так, как нужно
    • присуща программистам, знающим, насколько важен volatile при программировании параллельных процессов или при доступе к периферийным регистрам, но не учитывающие некоторые его нюансы;
  • использование volatile там, где не нужно (бывает и такое)
    • такое делают те, кто однажды обжегся на первых двух ошибках. Это не ошибка и она не приведет к неправильному поведению программы, но создаст свои неприятности.

Неиспользование volatile

Глобальные переменные

Самая частая ошибка, связанная с volatile, это просто неиспользование этого квалификатора там, где это нужно. Работая с микроконтроллерами, программист почти всегда сам является автором кода основной программы и кода для обработки прерываний. Причем основная программа и прерывания должны обмениваться данными, а самым распространенным способом для этого является использование глобальных переменных (будь то счетчики, привязанные к периоду переполнения таймера, или буферы для хранения входных/выходных данных, или переменные состояния, или еще что-то).

Модификация переменной в прерывании

Рассмотрим пример для PIC24:

  1.  
  2. unsigned char Counter;
  3.  
  4. void __attribute__((__interrupt__, __auto_psv__)) _T1Interrupt (void)
  5. {
  6. IFS0bits.T1IF = 0;
  7. Counter++;
  8. }
  9.  
  10. void wait (unsigned char Time)
  11. {
  12. Counter = 0;
  13. while (Counter < Time) continue;
  14. }
  15.  

При отключенной оптимизации данный код будет работать. Но стоит включить оптимизацию, как программа начнет зависать в функции wait(). Что происходит? Компилятор, транслируя функцию wait(), не знает про то, что переменная Counter может измениться в любой момент при возникновении прерывания. Он видит только то, что мы ее обнуляем, а затем сразу сравниваем с параметром Time. Другими словами компилятор предполагает, что переменная Time всегда сравнивается с нулем, и листинг функции wait() при включенной оптимизации будет выглядеть так:

  1.  
  2. 0x5B4 wait: clr.b Counter
  3. 0x5B6 cp0.b w0 ; <-----
  4. 0x5B8 bra nz, 0x5B6
  5. 0x5BA return
  6.  

(Примечание: компилятор Си единственный однобайтовый аргумент передает в функцию через регистр w0)

Что мы видим в этом коде: производится обнуление переменной Counter, а затем параметр функции Time, переданный в нее через регистр w0, сравнивается с нулем, больше не обращая внимания на истинное значение переменной Coutner, которая исправно увеличивается при каждом прерывании по таймеру. Другими словами, мы попали в вечный цикл. Как уже говорилось, дело в том, что компилятор не предполагает, что функция будет прервана каким-то кодом, который будет производить операции над переменными, участвующими в работе функций. Здесь нам на помощь и приходит квалификатор volatile:

  1.  
  2. volatile unsigned char Counter;
  3.  
  4. void __attribute__((__interrupt__)) _T1Interrupt (void)
  5. {
  6. IFS0bits.T1IF = 0;
  7. Counter++;
  8. }
  9.  
  10. void wait (unsigned char Time)
  11. {
  12. Counter = 0;
  13. while (Counter < Time) continue;
  14. }
  15.  

Теперь при трансляции компилятор сгенерит код, который будет каждый раз обращаться к переменной Counter:

  1.  
  2. 0x5B4 wait: mov.b w0, w1
  3. 0x5B6 clr.b Counter
  4. 0x5B8 mov.b Counter, w0 ; <-----
  5. 0x5BA sub w0, w1, [w15]
  6. 0x5BC bra nc, 0x5B8
  7. 0x5BE return
  8.  
Модификация переменной в параллельной задаче

Если программа работает под управлением ОСРВ, т.е. выполнение функции может быть прервано в какой-то момент, а потом опять передано ей с того места, где она прервалась. Не важно, кооперативный ли планировщик у ОС (т.е. сам программист решает, где функции быть прерванной) или вытесняющий (тут программист вообще ничего не решает, и его функция может быть прервана абсолютно в любой момент времени более приоритетной задачей). Важно то, что компилятор не знает о том, что в середине функции может быть выполнен какой-то посторонний код. Вот фрагмент кода из одной реальной программы:

  1.  
  2. unsigned char Button;
  3.  
  4. void Task_Button (char NewLevel)
  5. {
  6. unsigned int Temp;
  7.  
  8. for (;;)
  9. {
  10. //...
  11. // Переключение контекста
  12. //...
  13.  
  14. Temp = PORTB;
  15. //...
  16. // Обработка дребезга
  17. //...
  18.  
  19. Button = 0;
  20. if (Temp & 1) Button = 1;
  21. if (Temp & 2) Button = 2;
  22. if (Temp & 4) Button = 3;
  23. }
  24. }
  25.  
  26. void Task_Work ()
  27. {
  28. if (!Button) PIN_RED_LED = 1;
  29. for (;;)
  30. {
  31. //...
  32. // Переключение контекста
  33. // и пр.
  34. //...
  35.  
  36. switch (Button)
  37. {
  38. case 1: //... Действия по кнопке 1
  39. break;
  40. case 2: //... Действия по кнопке 2
  41. break;
  42. case 3: //... Действия по кнопке 3
  43. break;
  44. }
  45. }
  46. }
  47.  

Программа хорошо работала, будучи собранной компилятором HT-PICC18, но при переносе этого же кода на PIC24 (компилятор MCC30) работать перестала, т.е. совсем не реагировала на кнопки. Проблема была в том, что оптимизатор MCC30, в отличие от оптимизатора HT-PICC18, учел, что на момент выполнения switch значение переменной Button уже хранится в одном из регистров общего назначения (w0) (в PIC18 всего 1 аккумулятор, поэтому при работе с ним такое поведение менее вероятно):

  1.  
  2. ; if (!Button) PIN_RED_LED = 1;
  3. mov.b Button, w0
  4. bra nz, 1f
  5. bset.b PORTB, 0
  6. 1:
  7. ...
  8. ...
  9. ; switch (Button)
  10. sub.b w0, #2, [w15]
  11. bra z, Case2
  12. sub.b w0, #3, [w15]
  13. bra z, Case3
  14. sub.b w0, #1, [w15]
  15. bra nz, SwithEnd
  16. Case1:
  17. ...
  18. Case2:
  19. ...
  20. Case3:
  21. ...
  22. SwithEnd:
  23.  

Автор этого кода рассказал, что при отладке сломал кнопку, пытаясь нажать ее сильнее, чтобы она хоть как-то сработала :). Его ошибка заключалась в том, что он не использовал volatile при объявлении переменной Button. Сделай он это - и компилятор знал бы, что при каждом обращении к переменной Button нужно выполнять инструкции для фактического обращения к ячейке памяти, а не использовать для ускорения промежуточный результат.

После правильного объявления переменной:

  1.  
  2. volatile unsigned char Button;
  3.  
  4. void Task_Button (char NewLevel)
  5. {
  6. ...
  7. }
  8.  
  9. void Task_Work ()
  10. {
  11. ...
  12. }
  13.  

проблема исчезла:

  1.  
  2. ; if (!Button) PIN_RED_LED = 1;
  3. mov.b Button, w0
  4. bra nz, 1f
  5. bset.b PORTB, 0
  6. 1:
  7. ...
  8. ...
  9. ; switch (Button)
  10. mov.b Button, w0
  11. sub.b w0, #2, [w15]
  12. bra z, Case2
  13. mov.b Button, w0
  14. sub.b w0, #3, [w15]
  15. bra z, Case3
  16. mov.b Button, w0
  17. sub.b w0, #1, [w15]
  18. bra nz, SwithEnd
  19. Case1:
  20. ...
  21. Case2:
  22. ...
  23. Case3:
  24. ...
  25. SwithEnd:
  26.  

Локальные переменные

Часто для создания небольших задержек пользуются такими функциями:

  1.  
  2. void Delay (char D)
  3. {
  4. char i;
  5. for (i = 0; i < D; i++) continue;
  6. }
  7.  

Однако, некоторые компиляторы видят в этих функциях бесполезный код и вообще не включают его в результирующий объектный код. Если эта задержка применялась для снижения скорости программного i2c (или SPI) под компилятором, например, HT-PICC, то при переносе на ядро AVR с компилятором WinAVR программа перестанет работать, вернее, она будет работать так быстро, что управляемая микросхема не будет успевать обрабатывать сигналы из-за того, что все вызовы функции Delay будут упразднены.

Чтобы этого не происходило, нужно использовать квалификатор volatile:

  1.  
  2. void Delay (char D)
  3. {
  4. volatile char i;
  5. for (i = 0; i < D; i++) continue;
  6. }
  7.  

Указатели на volatile

Также распространенной ошибкой является использование обычного (не volatile) указателя на volatile-переменную. Чем это может нам навредить? Рассмотрим пример (из реальной программы), в котором был использован указатель на регистр порта ввода/вывода. Программа по SPI управляла двумя одинаковыми микросхемами, подключенными на разные выводы микроконтроллера, порт, к которому подключена активная микросхема, выбирался через указатель:

  1.  
  2. unsigned int *Port;
  3.  
  4. void SPI_Send (unsigned char Data)
  5. {
  6. char i = 8;
  7.  
  8. do
  9. {
  10. if (Data & 0x80) *Port |= 1; // Set data bit
  11. else *Port &= ~1;
  12.  
  13. *Port |= 2; // Make clock pulse
  14. Data <<= 1; // Shift data
  15. *Port ^= ~2;
  16. } while (--i);
  17. }
  18.  
  19. void main (void)
  20. {
  21. //...
  22. Port = &PORTB;
  23. //...
  24. SPI_Send(0x55);
  25. SPI_Send(0x66);
  26. //...
  27. }
  28.  

Произошла та же ситуация, что и в предыдущем примере: под одним компилятором (MCC18) код работал, а под другим (MCC30) - перестал. Причина крылась в неправильно объявленном указателе. Дизассемблер функции SPI_Send() выглядел так:

  1.  
  2. 0050C SPI_Send: mov.w port,w1
  3. 0050E mov.b #0x8,w2 ; i = 8
  4. 00510 While: cp0.b w0,#0 ; if (Data & 0x80)
  5. 00512 bra ges, 0x000518
  6. 00514 bset [w1],#0 ; *Port |= 1
  7. 00516 bra 0x00051a
  8. 00518 bclr [w1],#0 ; *Port &= ~1
  9. ; *Port |= 2 <==
  10. 0051A add.b w0,w0,w0 ; Data <<= 1
  11. 0051C bclr [w1],#1 ; *Port &= ~2
  12. 0051E dec.b w2,w2 ; while (--n)
  13. 00520 bra nz, 0x000510
  14. 00522 return
  15.  

Обратим внимание на то, что компилятор "выкинул" установку бита 1 ("*Port |= 2"), посчитав ее лишней, т.к. почти сразу же за ней этот бит снова обнуляется, т.е. он выполнил оптимизацию без учета особенностей регистра, на который указывала переменная Port, а сделал он так потому, что программист не объяснил компилятору, что регистр непростой. Для исправления ошибки нужно было объявить переменную Port как указатель на volatile переменную:

  1.  
  2. volatile unsigned int *Port;
  3.  
  4. void SPI_Send (unsigned char Data)
  5. {
  6. ...
  7. }
  8.  

Теперь компилятор знает, что оптимизацию над переменной, на которую указывает Port, производить нельзя. И новый листинг это отражает:

  1.  
  2. 0050C SPI_Send: mov.w port,w1
  3. 0050E mov.b #0x8,w2 ; i = 8
  4. 00510 While: cp0.b w0 ; if (Data & 0x80)
  5. 00512 bra ges, 0x000518
  6. 00514 bset [w1],#0 ; *Port |= 1
  7. 00516 bra 0x00051a
  8. 00518 bclr [w1],#0 ; *Port &= ~1
  9. 0051A bset [w1],#1 ; *Port |= 2 <-----------
  10. 0051C add.b w0,w0,w0 ; Data <<= 1
  11. 0051E bclr [w1],#1 ; *Port &= ~2
  12. 00520 dec.b w2,w2 ; while (--n)
  13. 00522 bra nz, 0x000510
  14. 00524 return
  15.  

Аргумент функции

Если бы в функцию SPI_Send из предыдущего примера нужно было бы передавать адрес порта (т.е. не держать его в глобальной переменной, а передавать в качестве аргумента), то не нужно забывать, что сам аргумент функции должен быть также описан с квалификатором volatile:

  1.  
  2. void SPI_Send (unsigned char Data, volatile unsigned int *Port)
  3. {
  4. ...
  5. }
  6.  
  7.  
  8. void main (void)
  9. {
  10. //...
  11. SPI_Send(0x55, &PORTB);
  12. SPI_Send(0x66, &PORTB);
  13. //...
  14. }
  15.  

В противном случае мы получим все те же ошибки, что и в предыдущем примере.

Неправильное использование volatile

volatile != атомарность

Существует некоторое заблуждение насчет защищенности переменных, объявленных как volatile. Т.е. программист, используя volatile-переменную, уверен, что компилятор сам позаботится о том, чтобы операции с переменной защищались запретами прерываний или еще какими-то хитрыми действиями. Однако, это не так. Объявление переменной как volatile совсем не гарантирует нам атомарность операций с ней. Тут программистов подстерегают две проблемы:

  1. обращение к многобайтовой переменной;
  2. чтение/модификация/запись через аккумулятор.

Рассмотрим пример обращения к переменной, занимающей более чем одну ячейку памяти (для 16-разрядных контроллеров это int32, int64, float, double; для 8-разрядных - еще и int16). Я часто приводил этот пример, приведу еще раз (для HT-PICC18):

  1.  
  2. volatile unsigned int ADCValue; // Результат чтения АЦП (производится в прервании)
  3.  
  4. void interrupt isr (void)
  5. {
  6. if (ADIF && ADIE)
  7. {
  8. ADIF = 0;
  9. ADCValue = (ADRESH << 8) | ADRESL; // Читаем последнее измерение
  10. ADGO = 1; // Запускаем следующее
  11. }
  12. }
  13.  
  14. void main ()
  15. {
  16. //...
  17. while (1)
  18. {
  19. //...
  20. if (ADCValue < 100) Alarm(); // Сравнение напряжения с пороговыми
  21. if (ADCValue > 900) Alarm(); // значениями и вызов тревоги
  22. //...
  23. }
  24. }
  25.  

Обратим внимание, что переменная ADCValue имеет размерность 2 байта (для хранения 10-битного результата АЦП). Чем опасен данный код? Рассмотрим листинг одного из сравнений (допустим, первого):

  1.  
  2. ; if (ADCValue < 100) Alarm();
  3. MOVLW 0
  4. SUBWF ADCValue + 1, W
  5. MOVLW 100
  6. BTFSC STATUS, Z
  7. SUBWF ADCValue, W
  8. BTFSS STATUS, C
  9. RCALL Alarm
  10.  

Допустим, значение напряжения на входе АЦП такое, что результат преобразования равен 255 (0x0FF). И последнее значение переменной ADCValue, соответственно, тоже = 0x0FF. С этим значением начинает выполняться код сравнения со значением 100 (0x064). Сначала сравниваются старшие байты переменной и константы (0x00 с 0x00), а затем - младшие (0x64 и 0xFF). Результат, казалось бы, очевиден. Однако, здесь кроется неприятность. Хоть результат АЦ-преобразования и равен 0xFF, на него влияют несколько факторов: стабильность напряжения питания (или опорного напряжения), стабильность входного сигнала, близость уровня измеряемого напряжения к порогу смены единицы младшего разряда, наводки, шумы и пр. Поэтому результат АЦ-преобразования имеет некий джиттер в одну-две единицы младшего разряда. Т.е. результат АЦП может скакать между значениями 0xFF и 0x100. И если между выполнением сравнений возникнет прерывание, то может произойти следующее:

  1. значение ADCValue = 0x0FF;
  2. произведено сравнение старших байтов: 0x00 и 0x00;
  3. возникло прерывание по ADIF, в котором значение переменной ADCValue обновилось на 0x100;
  4. производится сравнение младших байтов: 0x64 и уже 0x00!
  5. т.к. программа думает, что было сравнение 0x000 < 0x064, то она вызывает функцию Alarm.

И квалификатор volatile здесь не спасает. Здесь спасет только запрет прерываний на время выполнения сравнений.

  1.  
  2. ADIE = 0;
  3. if (ADCValue < 100) { ADIE = 1; Alarm();}
  4. if (ADCValue > 900) { ADIE = 1; Alarm();}
  5. ADIE = 1;
  6. ...
  7.  
  8. void Alarm (void)
  9. {
  10. ADIE = 1; // Не забыть включить прерывание при выполнении условий
  11. ...
  12.  

Так может быть, volatile вообще не нужен? Прерывания-то все равно запрещаются? Да, прерывания запрещаются, но volatile, все-таки нужен. Зачем? Рассмотрим почти такой же код для компилятора C30:

  1.  
  2. unsigned int ADCValue;
  3.  
  4. void main (void)
  5. {
  6. //...
  7. Temp = ADCValue;
  8.  
  9. while (1)
  10. {
  11. //...
  12. IEC0bits.ADIE = 0;
  13. if (ADCValue < 100) { IEC0bits.ADIE = 1; Alarm();}
  14. if (ADCValue > 900) { IEC0bits.ADIE = 1; Alarm();}
  15. IEC0bits.ADIE = 1;
  16. //...
  17. }
  18. }
  19.  

Вот здесь-то volatile и пригодится! Обратим внимание на строчку перед вечным циклом - присваивание значения переменной ADCValue переменной Temp. А заодно посмотрим листинг:

  1.  
  2. 00536 mov.w ADCValue, w1
  3. 00538 mov.w w1, Temp
  4. ; while (1)
  5. ; {
  6. ; IEC0bits.AD1IE = 0;
  7. 0053A bclr.b 0x0095,#5
  8. ; if (ADCValue < 100) { IEC0bits.AD1IE = 1; Alarm();}
  9. 0053C mov.w #0x63,w0
  10. 0053E sub.w w1,w0,[w15]
  11. 00540 bra leu, 0x000548
  12. ; if (ADCValue > 900) { IEC0bits.AD1IE = 1; Alarm();}
  13. 00542 mov.w #0x384,w0
  14. 00544 sub.w w1,w0,[w15]
  15. 00546 bra leu, 0x00054c
  16. 00548 bset.b 0x0095,#5
  17. 0054A rcall Alarm
  18. ; IEC0bits.AD1IE = 1;
  19. 0054C bset.b 0x0095,#5
  20.  

Как видим, внутри цикла вообще нет обращения к переменной ADCValue, а вместо этого сравнение производится с регистром W1, куда была скопирована переменная ADCValue еще перед циклом. Поэтому, как ни будет изменяться ADCValue, наша программа этого не заметит. Так что volatile в данном случае нужен обязательно, просто не следует забывать, что этот квалификатор не гарантирует нам атомарности операций над объявленной переменной.

volatile указатели

Иногда бывает такое, что сам указатель на какую-то переменную должен быть защищен от оптимизации (например, есть его модификация в теле прерывания). Часто программисты совершают одну и ту же ошибку, т.е. при объявлении такого указателя ставят volatile не туда, куда нужно:

  1.  
  2. volatile char *p;
  3. или
  4. char volatile *p;
  5.  

Обе записи идентичны, но они не делают саму переменную p volatile-переменной. Обе записи означают "переменная p является указателем на volatile char". Последствия такого определения очевидны: при изменении значения указателя в прерывании или в параллельной задаче программа этого может не заметить, т.к. будет работать с указателем через РОН.

Правильное определение выглядит так:

  1.  
  2. char * volatile p;
  3.  

Если же нужен volatile-указатель на volatile-переменную, то он объявляется так:

  1.  
  2. volatile char * volatile p;
  3.  


Использование volatile не к месту

Скажу два слова об этой проблеме. У некоторых однажды нарвавшихся на проблемы, связанные с отсутствием volatile там, где он должен был быть, появляется какой-то комплекс volatile-мании (или оптимизация-фобии), когда в страхе, что "а вдруг компилятор этот код выкинет", чуть ли не все переменные объявляются с квалификатором volatile. В принципе, ничего криминального в таком использовании volatile нет, код будет работать правильно, но он будет громоздкий и неповоротливый. volatile не позволит компилятору применять оптимизацию, и некоторые фрагменты, которые, будучи оптимизированы, выполнились бы за 50-100 тактов, будут выполняться за 1000-2000 тактов. То же самое касается объемов кода (в десятки раз он, конечно, не вырастет, а вот раза в два - запросто). Другими словами появится неудовлетворенность оттого, что "и контроллер быстрый, и компилятор хороший, а код все равно тормозит".

Здесь четких рекомендаций я дать не смогу. В большинстве случаев при написании программ на Си я стараюсь придерживаться следующих правил:

  • глобальные переменные, используемые и в прерываниях, и в программе (или в прерываниях различных приоритетов), нужно объявлять как volatile;
  • глобальные переменные, обрабатываемые двумя и более задачами при работе под многозадачной ОС, нужно объявлять как volatile;
  • указатели на периферийные регистры, а также на переменные, объявленные как volatile, нужно объявлять как указатели на volatile;
  • все, что не попадает под первые три правила и не связано с периферией, рекомендуется писать, абстрагируясь от железа. Тогда становится понятно, что циклы вида "for (i = 0; i<100; i++) {};" не несут на себе никакой алгоритмической нагрузки и могут быть удалены. А если они должны быть оставлены, то переменные следует объявлять как volatile.
  • во всех остальных случаях volatile будет лишним.

Дополнительно

Еще несколько замечаний:

volatile можно ставить в любое место в типе

Как уже указывалось выше, следующие записи эквивалентны:

  1.  
  2. volatile static int N;
  3. static volatile int N;
  4. static int volatile N;
  5.  

Лично я для наглядности volatile выношу вперед.

volatile и typedef

volatile может быть использован при определении типов по всем правилам:

  1.  
  2. typedef volatile char vchar;
  3.  

Теперь все переменные типа vchar будут volatile-переменными.

volatile и структуры

volatile может быть применен как ко всей структуре целиком, так и к отдельным ее полям. В зависимости от этого компилятор будет производить оптимизацию по-разному. Например, в этой структуре все поля будут volatile:

  1.  
  2. typedef volatile struct
  3. {
  4. unsigned char Counter;
  5. unsigned char *Buffer;
  6. } T_BUFFER;
  7.  

(Обращу ваше внимание на то, что здесь Buffer является volatile-указателем на не-volatile переменную. Т.е. эквивалент определения: "unsigned char * volatile Buffer", а не "volatile unsigned char *Buffer".)

А в этой - только счетчик:

  1.  
  2. typedef struct
  3. {
  4. volatile unsigned char Counter;
  5. unsigned char *Butter;
  6. } T_BUFFER;
  7.  
volatile и extern

Если переменная во внешнем модуле объявлена как volatile:

  1.  
  2. volatile int Counter;
  3.  

, то и в заголовочном файле она должна быть объявлена с квалификатором volatile:

  1.  
  2. extern volatile int Counter;
  3.  




Виктор Тимофеев, июнь, 2010

osa@pic24.ru