При налагодженні вбудованих додатків, найбільш складно відловити помилки, які виявляють себе не постійно, а лише час від часу. Одна з причин подібних багів: змінні, доступ до яких здійснюється асинхронно. Такі змінні повинні бути правильно визначені, і мати відповідний захист.
Визначення повинно включати ключове слово volatile. Воно інформує компілятор, про те, що змінна може бути змінена не тільки з поточного виконуваного коду, але і з інших місць. Тоді компілятор буде уникати певних оптимізацій цієї змінної.
Щоб захистити загальну змінну, кожен операція доступу до неї повинна бути атомарної. Тобто не повинна перериватися до свого закінчення. Наприклад, доступ до 32 або 16 розрядної змінної на 8-ми розрядної архітектурою не атомарний, оскільки операції читання або запису вимагають більше однієї інструкції.
Розглянемо типовий приклад загальнодоступною змінної - програмний таймер. У обробнику переривання його значення змінюється, а в основному коді - зчитується. Якщо в обробнику інші переривання заборонені, як, наприклад, по дефолту зроблено в мікроконтролерах AVR, то операція зміни змінної атомарний і ніяких косяків не трапиться.
volatile unsigned long system_timer = 0;
#pragma vector = TIMER0_COMP_vect
__interruptvoid Timer0CompVect (void)
system_timer ++;
>
З іншого боку в основному циклі програми переривання найчастіше дозволені, і варіант небезпечного коду міг би виглядати так:
if (system_timer> = next_cycle)
next_cycle + = 100;
do_ something ();
>
Цей код не безпечний, тому що операція читання змінної system_timer НЕ атомарний. У той час як ми читаємо один з байтів змінної system_timer, може виникнути переривання TIMER0_COMP і обробник змінить її значення. Тоді, після повернення в основну програму, ми прочитаємо частину змінної вже від її нового значення. У ряді випадків мікс зі старого і нового значення не викличе збоїв, але в інших може сильно вплинути на поведінку програми. Ну, наприклад, якщо старе значення system_timer було 0x00ffffff, а нове 0x01000000.
Щоб захистити доступ до змінної system_timer, можна використовувати моніторну функцію, для цього перед ім'ям функції вказується ключове слово__monitor.
if (get_system_timer ()> = next_cycle)
next_cycle + = 100;
do_ something ();
>
Мониторная функція - це функція, яка при вході зберігає регістр SREG, забороняє переривання на час свого виконання, а перед виходом відновлює вміст SREG.
Якщо потрібно, щоб переривання заборонялися в якомусь конкретному ділянці коду, можна використовувати intrinsic функції.
...
unsigned long tmp;
unsigned char oldState;
oldState = __save_interrupt (); // зберігаємо регістр SREG
__disable_interrupt (); // забороняємо переривання
tmp = system_timer; // зчитуємо значення system_timer в тимчасову змінну __restore_interrupt (oldState); // відновлюємо SREG
Засоби Сі ++ дозволяють вбудувати цю логіку в клас.
Mutex ()
__restore_interrupt (current_state);
>
unsigned long tmp;
Mutex m; // створюємо об'єкт класу, тепер доступ буде атомарним
tmp = system_timer; // зберігаємо system_timer в тимчасовій зміною
>
При створенні об'єкта m конструктор збереже регістр SREG, і заборонить переривання. По закінченню блоку - деструктор відновить вміст SREG. Красиво, да?
Вообщем принцип скрізь один, а варіантів реалізації багато. Можна, наприклад, при доступі до змінної забороняти не всі переривання, а тільки ті, в яких використовується ця змінна.
На цьому все. Всім вдалої налагодження.
Проблема можлива і з восьмібітной змінної. Операція на кшталт system_timer - = 100 Компільо в кілька ассемблерних інструкцій і в основному коді також може бути перервана між читанням system_timer і записом результату.
---------
Є ще один спосіб читання мультибайтних асинхронних лічильників (без заборони переривань) - вважати змінну два рази і порівняти всі байти крім молодшого. Якщо байти в копіях рівні - беремо останнім лічений значення, якщо не рівні - зчитуємо до тих пір, поки в двох останніх лічених значеннях байти НЕ будуть рівні. Молодший байт лічильника між читаннями може встигнути змінитися без переносу, тому він в перевірці не бере.
Як видно з прикладів, код наведено для компілятора IAR. У WinAVR подібна проблема вирішується включенням файлу. в якому визначені макроси для реалізації атомарного доступу.
наприклад так:
.
ATOMIC_BLOCK (ATOMIC_RESTORESTATE)
// блок коду із забороненими переривань
>
.