Нужен ли квалификатор volatile, если общая переменная не может быть изменена извне во время полного вызова функции?

isr

Рассмотрите следующий пример:

int i { 0 };

void incInt() {
    ++i;
}

int readInt() {
    return i;
}

setup() {
    Serial.begin(9600);
    attachInterrupt(digitalPinToInterrupt(D1), incInt, CHANGE);
}

loop() {
    noInterrupts();
    int currI = readInt();
    interrupts();

    Serial.println(currI);
}

Благодаря заключению вызова readInt() в noInterrupts() и interrupts() гарантируется, что i нельзя изменить извне во время выполнения readInt(). i также не может быть изменен извне во время выполнения incInt(), пока он вызывается только как ISR (процедура обслуживания прерывания). Нужно ли мне по-прежнему объявлять i как volatile?

Изменяется ли ситуация, если доступ к i осуществляется непосредственно в основном цикле:

loop() {
    noInterrupts();
    int currI = i;
    interrupts();

    Serial.println(currI);
}

Другими словами: в каком контексте компилятор ожидает, что переменная не будет изменяться извне, если она не объявлена volatile?

, 👍0


2 ответа


Лучший ответ:

1

Объявление переменной как volatile сообщает компилятору, что он всегда должен считывать реальную переменную и что он не может использовать кэшированное значение. Также компилятор не может оптимизировать эту переменную, так как неизвестно, когда переменная изменится. Для этого не имеет значения, как вы на самом деле читаете переменную в основном скетче, важно только то, что она читается. Также это не имеет прямого отношения к атомарному чтению (с помощью noInterrupts()) (хотя при этом вы гарантируете только правильные значения)

Так что да, вам нужно объявить его volatile, иначе компилятор может подумать, что значение не меняется, и каким-то образом оптимизировать его.

В каком контексте компилятор ожидает, что переменная не будет изменяться извне, если она не объявлена volatile?

Внешнее здесь означает вне основной процедуры. Это относится к переменным, которые изменяются в ISR, а также к регистрам, которые изменяются аппаратно (поэтому определение регистра в используемом ядре также является volatile). Компилятор не может знать, когда произойдут эти изменения, потому что они вызваны оборудованием, а не кодом. Он попытается оптимизировать ваш скетч (что является довольно сложной процедурой) и может увидеть, что ваша переменная никогда не меняется в коде (обработчик прерывания не вызывается из основного скетча, поэтому компилятор не видит его вызова). ) и постараюсь его оптимизировать. Чтобы убедиться, что ваш скетч работает корректно, ваша задача — сообщить компилятору, что эта переменная может измениться в любой момент (чтобы он не думал иначе). Вы делаете это, объявляя его как volatile

,

1

Давайте на время забудем об атомарности, которая является отдельной проблемой, и предположим, что i является атомарным типом (возможно, byte или чем-то эквивалентным). Рассмотрим следующий код:

void loop()
{
    Serial.println(i);
}

Не так давно я ожидал, что это будет отлично работать на Arduino, так как существует не так много способов скомпилировать эту функцию... самостоятельно! Однако, начиная с версии 1.6.10, среда IDE позволяет оптимизация времени. Это означает, что компилятор (точнее, оптимизатор, работающий внутри компоновщик), может увидеть всю программу и заметить, что setup() и loop() вызываются только из одной функции, принадлежащей Arduino. ядро:

int main()  // немного упрощено
{
    init();      // Инициализация ядра Arduino
    setup();     // Инициализация пользователя
    for (;;)
        loop();  // Пользовательский цикл
}

Это делает их идеальными кандидатами для встраивания:

int main()
{
    init();      // скорее всего тоже будет встроено

    // void setup():
    Serial.begin(9600);
    attachInterrupt(digitalPinToInterrupt(D1), incInt, CHANGE);    

    for (;;) {
        // Встроенный цикл():
        Serial.println(i);
    }
}

Теперь должно стать понятнее, что повторное чтение i из памяти выглядит пустой тратой времени, которое можно оптимизировать. Затем цикл for становится:

register uint8_t cached_i = i;  // сохранить `i' во внутреннем регистре ЦП
for (;;) {
    // Встроенный цикл():
    Serial.println(cached_i);
}

И теперь программа печатает только нули. Вот почему вам нужен volatile для предотвратить эту оптимизацию.

,