Атомарный доступ к переменным

23/09/2009 - 21:00
   При отладке встраиваемых приложений, наиболее сложно отловить ошибки,  проявляющие себя не постоянно, а лишь время от времени. Одна из причин подобных багов: переменные, доступ к которым осуществляется асинхронно. Такие переменные должны быть правильно определены, и иметь соответствующую защиту. 

   Определение должно включать ключевое слово 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. Красиво, да?

   Вообщем принцип везде один, а вариантов реализации много. Можно, например, при доступе к переменной запрещать не все прерывания, а только те, в которых используется эта переменная.

На этом все. Всем удачной отладки.
 

Comments   

# Guest 2010-02-05 12:39
Проблема возможна и с восьмибитной переменной. Операция вроде system_timer -= 100 компилится в несколько ассемблерных инструкций и в основном коде также может быть прервана между чтением system_timer и записью результата.
---------
Есть еще один способ чтения многобайтовых асинхронных счетчиков (без запрета прерываний) - считать переменную два раза и сравнить все байты кроме младшего. Если байты в копиях равны - берем последнее считанное значение, если не равны - считываем до тех пор, пока в двух последних считанных значениях байты не будут равны. Младший байт счетчика между чтениями может успеть измениться без переноса, поэтому он в проверке не участвует.
# Guest 2010-05-23 09:29
Как видно из примеров, код приведен для компилятора IAR. В WinAVR подобная проблема решается включением файла , в котором определены макросы для реализации атомарного доступа.
например так :
.....
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
// блок кода с запрещенными прерываниями
}
....
# Guest 2010-05-23 09:31
в пред идущем посте не вписал имя файла для включения. Файл .
# Guest 2010-05-23 09:34
Файл util/atomic.h
P.S. Всетаки имя файла вписывал, просто оно в угловых скобках не отображается. Если админ может подправить мой первый пост, то удалите уточняющие.
# Guest 2010-09-07 14:35
Quote:
Мониторная функция – это функция, которая при входе сохраняет регистр SREG, запрещает прерывания на время своего выполнения, а перед выходом восстанавливает содержимое SREG.
Возможно глупый вопрос, но разве обычная, не мониторная, функция не делает то-же самое - сохраняет SREG и запрещает прерывания?
# Pashgan 2010-09-12 19:25
Нет, не делает.
Вот в этой статье написано, что происходит при вызове функции - chipenable.ru/index.php/programming-c/52-isr-calling-function.html
# Garoldy 2011-06-14 07:22
Можно ли защищать участки кода от прерываний, помещая их между функциями cli(); и sei(); в WinAVR ?
# Pashgan 2011-09-21 20:39
Да, так многие делают.
Более грамотный вариант с сохранением и восстановлением регистра SREG

Code:
unsigned char save = SREG;
cli();

....какой то код

SREG = save;
# Evgeniy 2013-11-26 07:01
Объясните почему это более грамотный вариант, пожалуйста?
# САБ 2013-11-26 08:55
Потому что может находиться внутри такого же блока. И после окончания вместо тупого включения прерываний восстанавливает их состояние на момент начала блока. Такой блок может находиться внутри функции, которая может в свою очередь вызываться откуда угодно и все будет работать корректно.
# Evgeniy 2013-11-26 09:55
Теперь понятно. Т.е. мы восстанавливаем весь SREG только ради флага I?
Если восстанавливать не весь SREG, а только флаг I, то суть от этого не поменяется, так? (просто это дольше, чем SREG целиком?)
# САБ 2013-11-27 08:05
да, верно
# SeNiMal 2012-12-30 12:24
Объясните, пожалуйста, зачем нужно сохранять SREG перед запретом прерываний и восстанавливать его снова перед разрешением прерываний. Об этом везде пишут, но не понятно когда может измениться SREG, если прерывания запрещены.

У вас недостаточно прав для комментирования.