Портативная ISR-безопасная передача данных

Во встроенном C я пытаюсь создать общий способ безопасной передачи значения из моего ISR (передний план) в мой основной цикл (фон). Под «общим» я подразумеваю, что я не хочу приостанавливать прерывания (потому что это зависит от компилятора/ЦП и может иметь побочные эффекты), просто хочу делать это с флагами занятости и т. д. Для этого конкретного механизма мне не нужно очередь, я просто хочу получить самое последнее значение, сообщенное ISR.

Таким образом, шаблон, который я использую, представляет собой структуру и несколько функций, которые работают со структурой. Проблема, конечно, в том, что функция «записи» основана на ISR и может прервать функцию «чтения» в любое время, и я хочу исключить возможность повреждения данных. Подход представляет собой двухслотовую систему и пару флагов занятости.

Это будет работать? И/или есть более простой способ? (Помните, что это встроенный C, и я пытаюсь быть универсальным/переносимым.) Спасибо!

typedef struct 
{
    uint8_t busy;
    int32_t valueA; 
    int32_t valueB;     
    uint8_t reading_from_A;
    uint8_t last_wrote_to_B;
} sSafeI32_Fore2Back;

void SafeI32_InitFore2Back(sSafeI32_Fore2Back * si)
{
    si->busy = 0;    
    si->valueA = 0;
    si->valueB = 0;
    si->reading_from_A = 0;
    si->last_wrote_to_B = 1;
}

int32_t SafeI32_ReadFromBack(sSafeI32_Fore2Back * si)
{
    int32_t rtn;

    si->busy = 1;
    if (si->last_wrote_to_B)
    {
        rtn = si->valueB;
    }
    else
    {
        si->reading_from_A = 1;
        rtn = si->valueA;
        si->reading_from_A = 0;
    }
    si->busy = 0;

    return rtn;
}

void SafeI32_WriteFromFore(sSafeI32_Fore2Back * si, int32_t v)
{
    if (si->busy == 0)
    {
        si->valueA = v;
        si->last_wrote_to_B = 0;      
    }
    else
    {
        if (si->reading_from_A)
        {
            si->valueB = v;
            si->last_wrote_to_B = 1;
        }
        else
        {
            si->valueA = v;
            si->last_wrote_to_B = 0;            
        }
    }    
}

person Chris    schedule 21.11.2017    source источник
comment
Обычно используется круговая очередь вещей. ISR загружает данные в индекс «сохранения», а основной цикл извлекает из индекса «загрузки», когда индекс «сохранения» отличается. ISR считывает индекс «сохранения», перемещает его вокруг одного, загружает новые данные в этот index и, когда все это безопасно сделано, сохраняет новое значение индекса обратно в индекс «сохранить». В этом случае основной цикл никогда не сможет увидеть новое значение индекса «сохранить», пока новые данные не будут полностью доступны. Индексы должны быть волатильными. Если бы у вас была RTOS, вы, вероятно, сигнализировали бы семафору, на котором поток обработчика ожидает вместо опроса.   -  person Martin James    schedule 21.11.2017
comment
Две вещи: я думаю, что очередь обертывания по своей сути не является потокобезопасной, например, если она шире 256 слотов, тогда ваши индексы больше байта, и для их чтения или записи потребуется две операции сборки (т.е. это неатомарно). Следовательно, ваша модификация индекса может произойти после того, как он будет наполовину прочитан, что приведет к случайному результату и плохому поведению.   -  person Chris    schedule 21.11.2017
comment
Во-вторых, очередь будет вести себя немного иначе, чем то, что я собираюсь сделать выше. Мне нужно самое последнее значение из ISR, история не нужна — если предыдущее еще не было прочитано, я хочу, чтобы оно было перезаписано.   -  person Chris    schedule 21.11.2017
comment
Также спасибо за ответ :)   -  person Chris    schedule 21.11.2017
comment
Очередь переноса безопасно использовать между одним ISR и одним потоком/основным циклом, если соблюдается описанный выше протокол. Индекс увеличивается и, возможно, помещается во временную, автоматическую переменную ISR, и данные загружаются в этот новый индекс до того, как будет установлена ​​изменчивая переменная «сохранить», поэтому поток/основной не может видеть никаких изменений в «сохранить», в то время как увеличение/тестирование/настройка происходит в автоматическом var. Ничего страшного - я много раз пользовался такой схемой и, пока настройка "сохранения" делается последней, все ок. Возможен сбой с а) вложенными ISR б) ожиданием более одного потока.   -  person Martin James    schedule 21.11.2017


Ответы (3)


Один из простых и распространенных способов сделать это — заставить функцию чтения гарантировать, что она не была вытеснена во время чтения. ISR обновляет счетчик при каждом выполнении, а функция чтения гарантирует, что счетчик не изменился во время доступа. Если счетчик изменился, чтение выполняется снова.

typedef struct
{
    int32_t value;
    uint32_t counter;
} sSafeI32_Fore2Back;

void SafeI32_InitFore2Back(volatile sSafeI32_Fore2Back * si)
{
    si->value = 0;
    si->counter = 0;
}

int32_t SafeI32_ReadFromBack(volatile sSafeI32_Fore2Back * si)
{
    int32_t rtn;
    uint32_t ctr;

    do {
        ctr = si->counter;
        rtn = si->value;
    } while(ctr != si->counter);

    return rtn;
}

void SafeI32_WriteFromFore(sSafeI32_Fore2Back * si, int32_t v)
{
    si->value = v;
    si->counter++;
}
person D Krueger    schedule 21.11.2017
comment
Зачем использовать счетчик? Кажется излишне сложным, так как вы можете просто использовать любую изменчивую логическую переменную. Читатель: changed = false; x = read; if(changed).... - person Lundin; 22.11.2017
comment
@Lundin Я согласен, что ваше решение чище, когда есть только одна фоновая задача. Но ограничение изменений состояния выполнением ISR делает чтение потокобезопасным. - person D Krueger; 22.11.2017
comment
Если у вас есть несколько потоков, взаимодействующих напрямую с одним и тем же ISR, дизайн плохой. Это должен быть ISR ‹-› драйвер ‹-› установщик/получатель драйвера ‹-› несколько потоков. - person Lundin; 22.11.2017
comment
@Lundin Разве функция чтения не является реализацией геттера драйвера? - person D Krueger; 22.11.2017

Я выполнил обход кода и не смог найти каких-либо явных проблем с реализацией. Я верю, что это будет работать безопасно. Тем не менее, это, вероятно, слишком сложно с использованием двух флагов (занято и чтение_из_А), что немного усложняет просмотр. Важная часть заключается в том, что только фоновая функция записывает в флаги (reading_from_A и Busy), а не передняя функция.

ниже показано возможное упрощение, которое, как мне кажется, также будет безопасно работать. Я использовал аналогичную реализацию на процессоре 8051.

typedef struct
{
    uint8_t pendingData;
    uint8_t index;
    int32_t value[2];
} sSafeI32_Fore2Back;

void SafeI32_InitFore2Back(sSafeI32_Fore2Back * si)
{
    si->pendingData = 0;
    si->index = 0;
    si->value[0] = 0;
    si->value[1] = 0;
}

int32_t SafeI32_ReadFromBack(sSafeI32_Fore2Back * si)
{
    if (si->pendingData) {
        if (si->index == 0) {
            si->index = 1;
        } else {
            si->index = 0;
        }
        si->pendingData = 0;
    }
    return si->value[index];


    // Note 1: you could change the above if;then;else to
    //  index = (index+1) % 2;
    // but this is not an atomic operation and the ISR could
    // fire after the sum and before the modulo 2.
    // Perhaps index would take on an intermediate value of 2 before
    // the modulo and lead to a memory corruption by indexing out of
    // range when the ISR fires.  The ISR could mask the upper bits out
    // to ensure an out of range condition does not occur, but I think
    // the if;then;else is inherently safer.

    // Note 2: You need to assume that the ISR has fired at least once before this
    // function is called. Otherwise the function will return the initialized
    // value of 0. I believe the original post would do the same....

    // Note 3: I believe the original post is correct (at least I didn't find any
    // glaring issue with it.)  The important part is that only the background
    // function writes to the flags (reading_from_A and the busy) and not the
    // foreground function. Likewise, in this implemention, only the background
    // write to index and the forground function only reads it.However, the
// pendingData flag is read and written by both functions. Note that it is
// only written if its in a particular state which is mutually exclusive
}

void SafeI32_WriteFromFore(sSafeI32_Fore2Back * si, int32_t v)
{
    si->value[(si->index + 1) % 2] = v;  // write to the other location than what is
                                         // is being read from.
    if (!si->pendingData) {
        si->pendingData = 1;
    }
}
person dernst    schedule 21.11.2017
comment
Потрясающий! Выглядит отлично, спасибо сравню/сопоставлю. - person Chris; 21.11.2017
comment
Вопрос, скажем, ISR записывает в него «3», тогда я читаю, что он получит «3». Затем я прочитаю его еще раз, я получу «0», верно? Это немного отличается от намерения (которое я не очень хорошо объяснил, извините) всегда получать самое последнее значение. (Другими словами, мы хотим получить «3», пока оно не будет перезаписано, как простая переменная.) - person Chris; 21.11.2017
comment
да. Я думаю, что вы получите 0 при следующем чтении, если ISR не сработает между ними. - person dernst; 22.11.2017
comment
Вероятно, потребуется добавить счетчик, как и другое предложение, чтобы узнать, было ли записано новое значение. - person dernst; 22.11.2017
comment
Я исправил указанную вами ошибку в отношении множественного чтения. Но я добавил еще один флаг, который возвращает его к тому, что вы реализовали. Он немного другой и, может быть, немного проще. - person dernst; 22.11.2017

Поскольку вы знаете, что прерывание не может быть прервано в свою очередь (если только у вас нет особого экзотического сценария), это довольно просто:

static volatile bool changed = false;
static uint32_t value;

void isr (void) // it is an interrupt; it can't be interrupted by the caller
{
  changed = true;
  value = something;
}

void caller (void)
{
  ...
  changed = false;
  uint32_t val = value;
  if(changed)
  {
    // handle error, discard val
  }
}

Переменная changed даже не должна иметь атомарного доступа.

Обратите внимание, однако, что все переменные, совместно используемые ISR и вызывающей стороной, должны быть volatile, иначе могут возникнуть всевозможные странные и очень тонкие ошибки оптимизации.

person Lundin    schedule 22.11.2017