Як використовувати ключове слово volatile на мові c, avr, programming

Чи знайомі Ви з наступними ситуаціями в Вашому коді C або C ++ вбудованих систем?

• Код працює добре - поки не будуть вирішені оптимізації для компілятора.
• Код працює добре - поки не будуть дозволені переривання.
• дикувато працюють апаратні драйвера.
• Завдання RTOS добре працюють в ізоляції - поки не буде породжена деяка інша задача.

Якщо відповідь "так" на будь-який з цих питань, то швидше за все Ви не використали ключове слово volatile. Але Ви не один такий. Мало хто з програмістів добре розуміє, як правильно використовувати volatile. На жаль, багато книг по мові програмування C мало приділяють уваги кваліфікаторів volatile.

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

[Синтаксис ключового слова volatile]

Щоб декларувати змінну як volatile, додайте ключове слово volatile перед або після типу даних у визначенні змінної. Наприклад, нижче наведені рівноцінні визначення змінної foo як цілого числа з властивістю volatile:

Те ж саме відноситься і до покажчиків на volatile-змінні, особливо пов'язаних з відображеними на пам'ять регістрами введення / виведення (memory-mapped I / O registers). Обидві ці декларації однаково визначають pReg як покажчик на беззнакое 8-бітове ціле число (байт) з властивістю volatile:

Volatile-покажчики на НЕ-volatile дані зустрічаються дуже рідко, але варто все ж згадати і такий синтаксис, як можливий:

І просто для повноти, якщо Ви раптом насправді повинні отримати volatile-покажчик на volatile-змінну, то напишете це так:

І нарешті, якщо Ви застосували volatile до структури (struct) або об'єднання (union), то весь вміст (всі поля) структури / об'єднання отримає властивості volatile. Якщо Вам не потрібна така поведінка, то слід причепити кваліфікатор volatile до окремих полів (членам) структури / об'єднання.

[Правильне використання ключового слова volatile]

Мінлива повинна бути декларована як volatile щоразу, коли можливо її несподівана зміна. На практиці це можуть бути тільки 3 типи таких змінних:

1. Відображені на пам'ять регістри процесора або його периферійного пристрою (memory-mapped peripheral registers).
2. Глобальні змінні, модифікуються в коді обробника переривання (interrupt service routine, ISR).
3. Глобальні змінні, до яких звертаються різні завдання в многопоточного додатки (зазвичай маються на увазі різні потоки RTOS).

Розглянемо докладніше кожен з цих трьох випадків.

Зазвичай такий код призведе до помилки, як тільки Ви включите оптимізацію компілятора, коли з включеною оптимізацією буде згенеровано приблизно такий код:

Тепер код асемблера стане таким:

Необхідну поведінку програми досягнуто.

Підступні проблеми схильні виникати з регістрами, у яких є спеціальні властивості. Наприклад, деякі периферійні пристрої містять регістри, які обнуляються простим їх читанням. Додаткова кількість їх читань (або меншу кількість їх читань), що відрізняється від очікуваного, можуть привести до абсолютно непередбачуваних результатів.

Обробники переривань. Обробники переривань (interrupt service routines, ISR) часто встановлюють змінні, які перевіряються в основному коді (працюючому поза переривання) або в коді іншого переривання. Наприклад, ISR послідовного порту може перевіряти кожен прийнятий символ на предмет появи символу ETX (імовірно позначає кінець повідомлення). Якщо зустрівся символ ETX, то ISR може встановити якийсь глобальний прапор. Некоректна реалізація може бути наступною:

Коли оптимізація компілятора вимкнена (так зазвичай надходять при налагодженні), цей код може працювати. Однак практично будь-який більш-менш гідний оптимізатор "поламає" цей код. Проблема в тому, що компілятор немає ніякої інформації про те, що змінна etx_rcvd може бути змінена в ISR. Поки компілятор вважає так, то для нього вираз! Ext_rcvd завжди істина, так що цикл ніколи не завершиться. Отже, весь код після тіла циклу оптимізатором може бути просто знищений оптимізатором. Якщо Вам пощастить, то компілятор повідомить про це. Якщо не пощастить (або Ви ще не навчилися звертати увагу на повідомлення компілятора), то робота коду буде порушена. Природно, вина відразу буде покладено на "паршивий оптимізатор".

Рішенням в цій ситуації буде оголосити змінну etx_rcvd як volatile. Тоді всі Ваші проблеми (або принаймні проблеми, пов'язані з цим випадком) зникнуть.

Багатопотокові програми. Незважаючи на наявність черг (queues), каналів (pipes), і інших механізмів обміну в багатопотоковому оточенні (RTOS), все одно звичайною практикою залишається обмін інформацією між двома потоками за допомогою загальної пам'яті (глобальної змінної). Навіть коли Ви додасте витісняє планувальник в свій код, у компілятора все одно немає ніяких припущень, що спрацює перемикання контексту, і що це може статися. У цьому випадку, коли інша задача модифікувала загальну глобальну змінну, може статися те ж саме, як і з обробником переривання, що вже обговорювалося вище. Як і в попередньому прикладі, загальна змінна повинна бути оголошена як volatile. Наприклад, тут можуть бути проблеми в коді при доступі до загальної змінної cntr з різних завдань:

Цей код швидше за все не буде працювати, коли оптимізація буде дозволена. Декларування cntr як volatile буде правильним шляхом вирішення проблеми.

Доступ до інструкції на пам'ять регістрів (memory-mapped register, або MMR) здійснюється за допомогою покажчиків. Тут наведено ще один приклад, де були проблеми з відсутністю ключового слова коду C для мікроконтролерів PIC. Код повинен робити перестановку вмісту між двома периферійними регістрами:

Деякі компілятори дозволяють Вам неявно декларувати всі змінні як volatile. Не піддавайтеся на цю спокусу, бо воно по суті відучує думати (як втім, і відключення оптимізації). Це також потенційно призводить до менш оптимизированному коду.

Також відмовтеся від спокуси завжди вимикати оптимізатор. Сучасні оптимізатори настільки гарні, що неможливо пригадати, щоб оптимізація приводила до помилок. Швидше навпаки - включення оптимізації дозволяє виявити всі потенційні помилки в коді, які Ви інакше б не помітили.

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

[А як бути зі структурами? ]

Як використовувати volatile зі структурами і покажчиками на структури? Чи повинні ми визначити покажчик на структуру, яка volatile, або ж слід кожен окремий елемент структури визначити як volatile або НЕ-volatile? Може бути, для деяких периферійних регістрів не потрібно використовувати volatile, оскільки їх значення ніколи не змінюється, але статус, маска переривань, буфер прийому і т. Д. Можуть змінюватися, і отже повинні бути визначені як volatile?

Куди слід помістити ключове слово volatile в цих визначення, щоб правильно використовувати структуру з моїм дороговказом?

У цьому прикладі передовим досвідом буде наступне визначення структури з регістрами I / O MMR:

Схожі статті