При отладке встраиваемых приложений, наиболее сложно отловить ошибки,
проявляющие себя не постоянно, а лишь время от времени. Одна из причин
подобных багов: переменные, доступ к которым осуществляется асинхронно.
Такие переменные должны быть правильно определены, и иметь
соответствующую защиту.
Определение должно включать ключевое
слово volatile. Оно информирует
компилятор, о том, что переменная может быть изменена не только из
текущего выполняемого кода, но и из других мест. Тогда компилятор будет
избегать определенных оптимизаций этой переменной.
Чтобы
защитить общую переменную, каждая операция доступа к ней должна быть
атомарной. То есть не должна прерываться до своего окончания.
Например, доступ к 32 или 16 разрядной переменной на 8-ми разрядной
архитектуре не атомарный, поскольку операции чтения или записи требуют
больше одной инструкции.
Рассмотрим типичный пример
общедоступной переменной – программный таймер. В обработчике прерывания
его значение изменяется, а в основном коде - считывается. Если в
обработчике другие прерывания запрещены, как, например, по дефолту
сделано в микроконтроллерах AVR, то операция изменения переменной
атомарна и никаких косяков не случится.
volatile unsigned long system_timer = 0;
#pragma vector = TIMER0_COMP_vect
__interrupt void
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.
__monitor unsigned
long get_system_timer(void)
{
return system_timer;
}
...
if (get_system_timer() >=
next_cycle)
{
next_cycle += 100;
do_ something();
}
Мониторная функция – это функция, которая при входе сохраняет
регистр SREG, запрещает прерывания на время своего выполнения, а перед
выходом восстанавливает содержимое SREG.
Если
требуется, чтобы прерывания запрещались в каком-то конкретном участке
кода, можно использовать intrinsic функции.
#include <intrinsics.h>
…
unsigned long tmp;
unsigned char oldState;
oldState = __save_interrupt();//сохраняем регистр SREG
__disable_interrupt();
//запрещаем прерывания
tmp =
system_timer; //считываем значение
system_timer во временную переменную __restore_interrupt(oldState);//восстанавливаем SREG
if (tmp >= next_cycle)
{
next_cycle += 100;
do_ something();
}
Средства
Си++ позволяют встроить эту логику в класс.
#include <intrinsics.h>
class Mutex
{
public:
Mutex ()
{
current_state = __save_interrupt();
__disable_interrupt();
}
~Mutex ()
{
__restore_interrupt(current_state);
}
private:
unsigned char
current_state;
};
….
unsigned
long tmp;
{
Mutex m; //создаем объект класса, теперь
доступ будет атомарным
tmp = system_timer; //сохраняем system_timer во
временной переменой
}
if (tmp >= next_cycle)
{
next_cycle += 100;
do_ something();
}
При
создании объекта m конструктор сохранит регистр SREG, и запретит
прерывания. По окончанию блока – деструктор восстановит содержимое SREG.
Красиво, да?
Вообщем принцип везде один, а вариантов
реализации много. Можно, например, при доступе к переменной запрещать
не все прерывания, а только те, в которых используется эта переменная.
Проблема возможна и с восьмибитной переменной. Операция вроде
system_timer -= 100 компилится в несколько ассемблерных инструкций и в
основном коде также может быть прервана между чтением system_timer и
записью результата. Есть еще один способ чтения
многобайтовых асинхронных счетчиков (без запрета прерываний) - считать
переменную два раза и сравнить все байты кроме младшего. Если байты в
копиях равны - берем последнее считанное значение, если не равны -
считываем до тех пор, пока в двух последних считанных значениях байты не
будут равны. Младший байт счетчика между чтениями может успеть
измениться без переноса, поэтому он в проверке не участвует.
Как видно из примеров, код приведен для компилятора IAR. В WinAVR
подобная проблема решается включением файла , в котором определены
макросы для реализации атомарного доступа.
например так:
#include <util/atomic.h>
...
ATOMIC_BLOCK(AT OMIC_RESTORESTA TE)
{
//
блок кода с запрещенными прерываниями
}
...
Статья взята с http://chipenable.ru/index.php/programming-c/16-volatile-critical-section.html.
|