Jump to content

    
Arlleex

'volatile' или, все-таки, нет?

Recommended Posts

Приветствую!

 

Вот интересно... Допустим, у нас есть переменная, к которой осуществляется доступ как из фоновой задачи, так и из прерывания

volatile u32 GVar;

TmrISR()
{
  TmrOff();
  GVar = 1;
}

int main(void)
{
  GVar = 1;
  
  while(1)
  {
    if(GVar)
      GVar = 0, TmrOn();
  }
}

 

Все понятно, все логично - volatile нужен, так как сами обращения к переменной GVar есть в двух местах. И одно из них - код ISR. Код чисто как пример, это формальность.

 

А если, допустим, есть функция, работающая с глобальной переменной, и я использую эту функцию и в фоновом процессе, и в прерывании, должен ли я ставить volatile?
С точки зрения логики, ИМХО, нет. Так как вызов функции должен выполнять всегда одно и то же, а значит все операции с переменной будут одинаковы и в ISR, и в фоновом процессе.

 

Так ли это? Или, все-таки, оптимизация рано или поздно выстрелит в ногу?

 

Писал драйвер, и FSM этого драйвера находится в одной глобальной статической структуре.

Сначала нужные поля были volatile (там где обращения к переменным происходили напрямую из прерываний и из API-функций пользователя).

А теперь доступ к этим переменным осуществляется через промежуточные функции, а функции вызываются из прерывания и API-функций.

Вот и думаю, оставлять volatile или нет.

 

ИМХО, если доступ в функции однократен, то volatile можно убрать.

Если в функции присутствует > 1 обращения к разделяемой переменной, она должна быть volatile.

Share this post


Link to post
Share on other sites

volatile нужно когда двойная запись в ячейку памяти даёт принципиально другой результат отличный от просто последней записи.
Т.е. если мы ногодрыгом делаем клок записывая 1 0 1 0 и т.д. то volatile обязательно. Если это просто ячейка памяти хранящяя последнюю температуру то не надо. 

Т.е. volatile это про то что все операции записи будут выполнены как бы бессмысленно с точки зрения оптимизатора они выглядели. И совершенно не имеет отношения к тому со скольки мест вы будете править переменную.

Share this post


Link to post
Share on other sites
5 минут назад, MegaVolt сказал:

Т.е. volatile это про то что все операции записи будут выполнены как бы бессмысленно с точки зрения оптимизатора они выглядели. И совершенно не имеет отношения к тому со скольки мест вы будете править переменную.

Это я понимаю. Но смотрите, допустим, есть вот такие функции

f1()
{
  GVar |= 1 << 0;
}

f2()
{
  GVar |= 1 << 1;
}

f3()
{
  GVar |= 1 << 2;
}

 

Разве обязан я тут объявлять GVar с volatile? ИМХО, нет.

Каждая из функций должна выполнить операцию доступа к памяти (чтение + запись).

 

А если я теперь, например, слинкую f1() как обработчик прерывания, разве что-то поменяется? ИМХО нет.

Компилятор по-прежнему должен осуществлять доступ на чтение + запись в каждой из функций.

А вот корректность результата такого кода на плечах программиста - это другой вопрос.

Т.е. по сути, формально, GVar стала изменчивой извне (в прерывании), но volatile тут, вроде бы, не нужен.

Share this post


Link to post
Share on other sites
2 минуты назад, Arlleex сказал:

Разве обязан я тут объявлять GVar с volatile? ИМХО, нет.

По моему тоже. Если нету реально многопоточности то проблем быть не должно.

Share this post


Link to post
Share on other sites

Для синхронизации между потоками исполнения и прерываниями больше подходят барьеры. В исходном примере достаточно добавить memory fence в бесконечный цикл. Только данном случае важен не порядок обращения к памяти а то, что компилятор после барьера должен прочитать значения глобальных переменных снова, не помню как это правильно называется.

Share this post


Link to post
Share on other sites
6 минут назад, MegaVolt сказал:

По моему тоже. Если нету реально многопоточности то проблем быть не должно.

Даже если многопоточность будет.

Ну, например, f1(), f2() и f3() вызываются каждая из своего потока.

Каждая из них выполнит чтение + запись. И все.

Конечно, с точки зрения конечного результата это может работать не правильно, но это вопросы атомарности доступа, а не разделяемости.

Если же процесс читает переменную в каком-нибудь цикле внутри потока и эта переменная может измениться другим потоком (в том числе ISR), то да, volatile нужен.

Иначе, в целях оптимизации, чтение произойдет один раз в регистр, и цикл застрянет навечно.

Share this post


Link to post
Share on other sites
33 минуты назад, Arlleex сказал:

А если, допустим, есть функция, работающая с глобальной переменной, и я использую эту функцию и в фоновом процессе, и в прерывании, должен ли я ставить volatile?
С точки зрения логики, ИМХО, нет. Так как вызов функции должен выполнять всегда одно и то же, а значит все операции с переменной будут одинаковы и в ISR, и в фоновом процессе.

А какая разница, как происходит обращение - через функцию или без? Скажем, компилятор в начале функции прочитает переменную в регистр, и будет работать с ним. А в середине функции - бац, прерывание, вызов функции, записали новое значение. Потом возврат, и функция записывает значение из регистра. И мы потеряли изменения, сделанные в прерывании.

Share this post


Link to post
Share on other sites
1 минуту назад, AHTOXA сказал:

А какая разница, как происходит обращение - через функцию или без? Скажем, компилятор в начале функции прочитает переменную в регистр, и будет работать с ним. А в середине функции - бац, прерывание, вызов функции, записали новое значение. Потом возврат, и функция записывает значение из регистра. И мы потеряли изменения, сделанные в прерывании.

Поэтому я и говорю, что volatile не нужен, если в функции одно обращение. Если как минимум несколько - то нужен (чтобы гарантировать физический доступ к памяти, а не оптимизированный, который может работать с предварительно загруженным регистром). Вернее не говорю, а предполагаю:smile:

Share this post


Link to post
Share on other sites
1 минуту назад, Arlleex сказал:

Поэтому я и говорю, что volatile не нужен, если в функции одно обращение. Если как минимум несколько - то нужен (чтобы гарантировать физический доступ к памяти, а не оптимизированный, который может работать с предварительно загруженным регистром). Вернее не говорю, а предполагаю:smile:

Да, просто volatile здесь не поможет. Здесь нужен atomic (который обычно подразумевает volatile + атомарность изменений).

volatile - это для регистров периферии.

Share this post


Link to post
Share on other sites

Добавлю, что в случае флага варианты типа

while (flag) {...};

и

bool getFlag() { return globalFlag; }

while (getFlag()) {...};

одинаковы - и там и там без volatile можно пролететь.

Share this post


Link to post
Share on other sites
36 минут назад, AHTOXA сказал:

Добавлю, что в случае флага варианты типа

...

одинаковы - и там и там без volatile можно пролететь.

В случае while(flag) {...}; (и любых вырожденных форм этой конструкции) понятно - в пределах одной функции будет множественный доступ. Тут volatile нужен.

Share this post


Link to post
Share on other sites

критическую секцию на край можно поставить (не в прерывании, а в фоне кода).

да, конечно это - костыль, но для подобного построения кода это вполне допустимо, особенно в крохотных и примитивных проектиках

тем более, если переменная имеет размер больше, чем слово проца, т. е. атомарно за раз его не прочитать или не записать

Share this post


Link to post
Share on other sites
14 минут назад, Forger сказал:

критическую секцию на край можно поставить (не в прерывании, а в фоне кода).

да, конечно это - костыль, но для подобного построения кода это вполне допустимо, особенно в крохотных и примитивных проектиках

тем более, если переменная имеет размер больше, чем слово проца, т. е. атомарно за раз его не прочитать или не записать

Так речь не об атомарности - для ее обеспечения соответствующие методы есть, в том числе критические секции.

Речь шла насчет корректности применения volatile к изменяемому объекту, когда изменяется переменная не напрямую, а через вызов функции.

И в этой функции осуществляется однократный доступ к этой переменной.

 

Я сейчас в одном проекте реализую менеджер доступа к шине I2C. На ней висит пока что FRAM, но в будущем, возможно, что-то навесить еще придется.

Так вот чтобы осуществить транзакцию (из любого места, в том числе из прерывания), пользовательский код запрашивает свободный 'transfer slot'.

Эти слоты организованы в кольцевую очередь. По завершению транзакции автоматом происходит вычитка из этой очереди следующей транзакции (если есть).

 

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

static struct
{
#define MNGACT_STOP    0
#define MNGACT_WORK    1
  volatile u8 act;
  volatile u8 mode;
  u8          serv;
  volatile u8 rw;
  u8          addr;
#define MNGFSM_SB      0
#define MNGFSM_RSB     1
#define MNGFSM_ADDRACK 2
#define MNGFSM_DATAACK 3
  volatile u8       fsm;
  volatile sI2CSOp *sop;
  
  struct
  {
    sI2CSDFRAM  tinf;
    volatile u8 addrw;
  }fram;
}BusMng;

 

Когда код немного разросся из обычных прямых обращений к полям этой структуры в выполнения законченных действий (функций), захотел обрезать volatile там, где это неуместно.

 

Собственно, вопрос считаю решенным, всем спасибо.

Share this post


Link to post
Share on other sites
2 hours ago, Arlleex said:

Разве обязан я тут объявлять GVar с volatile? ИМХО, нет.

Каждая из функций должна выполнить операцию доступа к памяти (чтение + запись).

А если я теперь, например, слинкую f1() как обработчик прерывания, разве что-то поменяется? ИМХО нет.

 

если оптимизатор никогда не заинлайнит f1, а вдруг так бывает?

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.