Volatile для - чайників - pic24


Розбираючи чужі вихідні, часто натикаюся на помилки програмістів, пов'язані з нерозумінням призначення кваліфікатора volatile. Результатом такого непорозуміння є код, який дає рідкісні, абсолютно непередбачувані і, найчастіше, дуже руйнівні і незворотні збої. Це особливо актуально для мікроконтролерних систем, де, по-перше, обробники переривань є частиною прикладного коду, а, по-друге, регістри управління периферією відображаються в RAM загального призначення. Помилки, пов'язані з невикористанням (або неправильним використанням) кваліфікатора volatile важко піддаються налагодженні через їх непередбачуваності і неповторяемости. Буває, що Сі-код, в якому не передбачено використання volatile там, де його треба б використовувати, прекрасно працює, будучи зібраним одним компілятором, і дає збої (або не працює зовсім), коли збирається іншим.

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

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

Отже, volatile в мові Сі - це кваліфікатор змінної, що говорить компілятору, що значення змінної може бути змінено в будь-який момент і що частина коду, яка виробляє над цієї змінної якісь дії (читання або запис), не повинна бути оптимізована.

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

вигравши таким чином пару тактів і пару осередків ROM. Але уявімо собі, що ці ж дії ми виконуємо немає над якоюсь абстрактною змінної, а над периферійним регістром:

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

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

(В цьому можна переконатися, заглянувши в заголовки для конкретного контролера, що поставляється з компілятором). Кваліфікатор volatile забороняє виробляти оптимізацію коду, що виконує дії над регістром PORTB. Тому навіть взаємодоповнюючі дії залишаться недоторканими оптимізатором, і ми можемо бути впевнені в тому, що на виході сформується імпульс.

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

Є три основних типи помилок, що стосуються кваліфікатора volatile.

невикористання volatile там, де потрібно

зазвичай відбувається програмістами, які не знають про існування volatile. або бачили, але не розуміють, що це таке;

використання volatile там, де потрібно, але не так, як потрібно

властива програмістам, які знають, наскільки важливий volatile при програмуванні паралельних процесів або при доступі до периферійних регістрів, але не враховують деякі його нюанси;

використання volatile там, де не потрібно (буває і таке)

таке роблять ті, хто одного разу обпікся на перших двох помилках. Це не помилка і вона не призведе до неправильного поводження програми, але створить свої неприємності.

Розглянемо приклад для PIC24:

При відключеною оптимізації даний код буде працювати. Але варто включити оптимізацію, як програма почне зависати в функції wait (). Що відбувається? Компілятор, транслюючи функцію wait (), не знає про те, що змінна Counter може змінитися в будь-який момент при виникненні переривання. Він бачить тільки те, що ми її Обнуляємо, а потім відразу порівнюємо з параметром Time. Іншими словами компілятор припускає, що змінна Time завжди порівнюється з нулем, і лістинг функції wait () при включеній оптимізації буде виглядати так:

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

Що ми бачимо в цьому коді: проводиться обнуління змінної Counter, а потім параметр функції Time, переданий в неї через регістр w0, порівнюється з нулем, більше не звертаючи уваги на істинне значення змінної Coutner, яка справно збільшується при кожному перериванні по таймеру. Іншими словами, ми потрапили в вічний цикл. Як вже говорилося, справа в тому, що компілятор не припускає, що функція буде перервана якимось кодом, який буде виробляти операції над змінними, які беруть участь в роботі функцій. Тут нам на допомогу і приходить кваліфікатор volatile.

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

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

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

Часто для створення невеликих затримок користуються такими функціями:

Однак, деякі компілятори бачать в цих функціях даремний код і взагалі не включають його в результуючий об'єктний код. Якщо ця затримка застосовувалася для зниження швидкості програмного i2c (або SPI) під компілятором, наприклад, HT-PICC, то при перенесенні на ядро ​​AVR з компілятором WinAVR програма перестане працювати, вірніше, вона буде працювати так швидко, що керована мікросхема не встигатиме обробляти сигнали з-за того, що всі виклики функції Delay будуть скасовані.

Щоб цього не відбувалося, потрібно використовувати кваліфікатор volatile.

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

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

Звернемо увагу на те, що компілятор "викинув" установку біта 1 ( "* Port | = 2"), вважаючи її зайвою, тому що майже відразу ж за нею цей біт знову обнуляється, тобто він виконав оптимізацію без урахування особливостей регістру, на який вказувала змінна Port, а зробив він так тому, що програміст не пояснив компілятору, що регістр непростий. Для виправлення помилки потрібно було оголосити змінну Port як покажчик на volatile змінну:

Тепер компілятор знає, що оптимізацію над змінної, на яку вказує Port, виробляти не можна. І новий лістинг це відображає:

В іншому випадку ми отримаємо всі ті ж помилки, що і в попередньому прикладі.

звернення до багатобайтові змінної;

читання / модифікація / запис через акумулятор.

Розглянемо приклад звернення до змінної, що займає більш ніж одну комірку пам'яті (для 16-розрядних контролерів це int32, int64, float, double; для 8-розрядних - ще й int16). Я часто приводив цей приклад, наведу ще раз (для HT-PICC18):

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

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

значення ADCValue = 0x0FF;

проведено порівняння старших байтів: 0x00 і 0x00;

виникло переривання по ADIF, в якому значення змінної ADCValue оновилося на 0x100;

проводиться порівняння молодших байтів: 0x64 і вже 0x00!

тому програма думає, що було порівняння 0x000 <0x064, то она вызывает функцию Alarm.

І кваліфікатор volatile тут не рятує. Тут врятує тільки заборона переривань на час виконання порівнянь.

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

Ось тут-то volatile і стане в нагоді! Звернемо увагу на рядок перед вічним циклом - присвоювання значення змінної ADCValue змінної Temp. А заодно подивимося лістинг:

Як бачимо, всередині циклу взагалі немає звернення до змінної ADCValue, а замість цього порівняння проводиться з регістром W1, куди була скопійована змінна ADCValue ще перед циклом. Тому, як не буде змінюватися ADCValue, наша програма цього не помітить. Так що volatile в даному випадку потрібен обов'язково, просто не слід забувати, що цей кваліфікатор не гарантує нам атомарности операцій над оголошеної змінної.

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

Правильне визначення виглядає так:

Якщо ж потрібен volatile -вказівник на volatile -змінного, то він оголошується так:

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

глобальні змінні, використовувані і в переривання, і в програмі (або в переривання різних пріоритетів), потрібно оголошувати як volatile;

глобальні змінні, які обчислюються двома і більше завданнями при роботі під багатозадачною ОС, потрібно оголошувати як volatile;

покажчики на периферійні регістри, а також на змінні, оголошені як volatile. потрібно оголошувати як покажчики на volatile;

все, що не підпадає під перші три правила і не пов'язане з периферією, рекомендується писати, абстрагуючись від заліза. Тоді стає зрозуміло, що цикли виду "for (i = 0; i<100; i++) <>; "Не несуть на собі ніякої алгоритмічної навантаження і можуть бути видалені. А якщо вони повинні бути залишені, то змінні слід оголошувати як volatile.

у всіх інших випадках volatile буде зайвим.

Ще кілька зауважень:

Схожі статті