Как избежать глобальных переменных при использовании прерываний во встроенных системах


13

Есть ли хороший способ реализации связи между ISR и остальной частью программы для встроенной системы, которая избегает глобальных переменных?

Кажется, что общая схема состоит в том, чтобы иметь глобальную переменную, которая совместно используется ISR и остальной частью программы и используется в качестве флага, но это использование глобальных переменных идет вразрез со мной. Я включил простой пример с использованием ISR в стиле avr-libc:

volatile uint8_t flag;

int main() {
    ...

    if (flag == 1) {
        ...
    }
    ...
}

ISR(...) {
    ...
    flag = 1;
    ...
}

Я не могу видеть вокруг того, что по существу является проблемой определения объема; Любые переменные, доступные как ISR, так и остальной части программы, по своей природе должны быть глобальными? Несмотря на это, я часто видел, как люди говорят что-то вроде «глобальные переменные - это один из способов реализации связи между ISR и остальной частью программы» (выделено мной), что, по-видимому, подразумевает, что существуют другие методы; если есть другие методы, каковы они?



1
Это не обязательно правда, что ВСЕ остальная часть программы будет иметь доступ; если вы объявите переменную как статическую, ее увидит только файл, в котором она была объявлена. Нетрудно иметь переменные, которые видны во всем одном файле, но не в остальной части программы, и это может помочь.
DiBosco

1
кроме того, флаг должен быть объявлен как volatile, потому что вы используете / меняете его вне обычного программного потока. Это заставляет компилятор не оптимизировать какие-либо операции чтения / записи для флага и выполнять фактическую операцию чтения / записи.
следующий взлом

@ next-hack Да, это абсолютно правильно, извините, я просто пытался быстро привести пример.

Ответы:


18

Существует де-факто стандартный способ сделать это (при условии программирования на C):

  • Прерывания / ISR являются низкоуровневыми и поэтому должны быть реализованы только внутри драйвера, связанного с оборудованием, которое генерирует прерывание. Они не должны быть расположены нигде, кроме как внутри этого водителя.
  • Вся связь с ISR осуществляется только водителем и водителем. Если другим частям программы требуется доступ к этой информации, она должна запросить ее у драйвера через функции установщика / получателя или аналогичные функции.
  • Вы не должны объявлять «глобальные» переменные. Глобальные значения переменных области видимости файла с внешней связью. То есть: переменные, которые могут быть вызваны с externключевым словом или просто по ошибке.
  • Вместо этого, чтобы принудительно установить частную инкапсуляцию внутри драйвера, должны быть объявлены все такие переменные, общие для драйвера и ISR static. Такая переменная не является глобальной, но ограничена файлом, в котором она объявлена.
  • Чтобы предотвратить проблемы оптимизации компилятора, такие переменные также должны быть объявлены как volatile. Примечание: это не дает атомарного доступа и не разрешает повторный вход!
  • В драйвере часто требуется механизм повторного входа, если ISR выполняет запись в переменную. Примеры: отключение прерывания, глобальная маска прерывания, семафор / мьютекс или гарантированное атомарное чтение.

Примечание: вам, возможно, придется выставить прототип функции ISR через заголовок, чтобы поместить его в векторную таблицу, расположенную в другом файле. Но это не проблема, если вы задокументируете, что это прерывание и не должно вызываться программой.
Лундин

Что бы вы сказали, если бы контраргументом были повышенные издержки (и дополнительный код) использования функций установки / получения? Я сам обдумывал это, думая о стандартах кода для наших 8-битных встроенных устройств.
Leroy105

2
@ Leroy105 Язык C уже давно поддерживает встроенные функции. Хотя даже использование inlineстановится устаревшим, так как компиляторы становятся умнее и умнее при оптимизации кода. Я бы сказал, что беспокойство об издержках - это «преждевременная оптимизация» - в большинстве случаев эти затраты не имеют значения, если они вообще присутствуют в машинном коде.
Лундин

2
При этом, в случае написания драйверов ISR, около 80-90% всех программистов (не преувеличивая здесь) всегда что-то неправильно понимают. Результатом являются незаметные ошибки: неправильно очищенные флаги, неправильная оптимизация компилятора из-за отсутствия энергозависимости, условий гонки, паршивой производительности в реальном времени, переполнения стека и т. Д. И т. Д. Если ISR неправильно инкапсулирован в драйвере, вероятность таких тонких ошибок дальше увеличилось. Сосредоточьтесь на написании драйвера без ошибок, прежде чем беспокоиться о вещах, представляющих периферийный интерес, например, о том, что сеттер / геттер вносят незначительные накладные расходы.
Лундин

10
это использование глобальных переменных идет вразрез со мной

Это настоящая проблема. Преодолей это.

Теперь, перед тем, как колени будут сразу же разглагольствовать о том, что это нечисто, позвольте мне немного уточнить это. Существует определенная опасность в использовании глобальных переменных для чрезмерного. Но они также могут повысить эффективность, что иногда имеет значение в небольших системах с ограниченными ресурсами.

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

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

  1. Счетчики тактов, управляемые системным прерыванием часов. У меня обычно есть периодическое прерывание тактовой частоты, которое запускается каждые 1 мс. Это часто полезно для различного времени в системе. Один из способов получить эту информацию из подпрограммы прерывания, где остальная система может использовать ее, - это сохранить глобальный счетчик тактов. Процедура прерывания увеличивает счетчик каждый такт. Код переднего плана может прочитать счетчик в любое время. Часто я делаю это за 10 мс, 100 мс и даже за 1 секунду.

    Я уверен, что тики 1 мс, 10 мс и 100 мс имеют размер слова, который можно прочитать за одну атомарную операцию. При использовании языка высокого уровня обязательно сообщите компилятору, что эти переменные могут изменяться асинхронно. Например, в Си вы объявляете их внешне изменчивыми . Конечно, это то, что входит в стандартный файл включения, поэтому вам не нужно помнить это для каждого проекта.

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

    Конечно, могли быть процедуры для получения меньших 1 мс, 10 мс и т. Д., А также счетчики тиков. Однако это действительно очень мало для вас, добавляет много инструкций вместо чтения одного слова и использует другое расположение стека вызовов.

    Какой минус? Я предполагаю, что кто-то может сделать опечатку, которая случайно записывает данные на один из счетчиков, что затем может испортить другие временные характеристики в системе. Записывать счетчику преднамеренно не имеет смысла, поэтому такого рода ошибка должна быть чем-то непреднамеренным, например опечатка. Кажется очень маловероятным. Я не помню, чтобы это когда-либо происходило в более чем 100 небольших проектах микроконтроллеров.

  2. Окончательно отфильтрованные и скорректированные A / D значения. Обычная вещь, которую нужно сделать, это иметь показания обработчика прерываний от A / D. Я обычно читаю аналоговые значения быстрее, чем необходимо, затем применяю небольшую фильтрацию нижних частот. Часто также применяется масштабирование и смещение.

    Например, A / D может считывать выходной сигнал от 0 до 3 В делителя напряжения для измерения напряжения 24 В. Многие показания проходят через некоторую фильтрацию, затем масштабируются так, чтобы окончательное значение было в милливольтах. Если напряжение питания составляет 24,015 В, то окончательное значение составляет 24015.

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

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


В медленную неделю я ходил на терапию, действительно пытаясь придираться к своему коду. Я вижу точку зрения Лундина об ограничении доступа к переменным, но я смотрю на свои настоящие системы и думаю, что это такая отдаленная возможность, что ЛЮБОЙ ЧЕЛОВЕК фактически может поставить под угрозу критическую для системы глобальную переменную. Функции Getter / Setter в конечном итоге обходятся вам дороже, чем просто использование глобальных и принятие этих довольно простых программ ...
Leroy105

3
@ Leroy105 Проблема не в том, что «террористы» намеренно злоупотребляют глобальной переменной. Загрязнение пространства имен может быть проблемой в более крупных проектах, но это можно решить с помощью правильного именования. Нет, настоящая проблема заключается в том, что программист пытается использовать глобальную переменную по назначению, но не может сделать это правильно. Либо потому, что они не осознают проблему состояния гонки, которая существует со всеми ISR, либо потому, что они испортили реализацию механизма обязательной защиты, либо просто потому, что они извергают использование глобальной переменной во всем коде, создавая тесную связь и нечитаемый код
Лундин

Ваши очки действительны для Olin, но даже в этих примерах замена extern int ticks10msна не inline int getTicks10ms()будет иметь абсолютно никакого значения в скомпилированной сборке, в то время как с другой стороны будет трудно случайно изменить ее значение в других частях программы, а также позволит вам способ «привязки» к этому вызову (например, для проверки времени во время модульного тестирования, для регистрации доступа к этой переменной или чего-либо еще). Даже если вы утверждаете, что шанс программиста san изменить эту переменную на ноль, встроенный метод получения не будет стоить дорого.
Groo

@Groo: Это верно только в том случае, если вы используете язык, который поддерживает встроенные функции, и это означает, что определение функции getter должно быть видимым для всех. На самом деле, когда я использую язык высокого уровня, я больше использую функции геттера и меньше глобальные переменные. В сборке гораздо проще получить значение глобальной переменной, чем использовать функцию получения.
Олин Латроп

Конечно, если вы не можете встроить, то выбор не так прост. Я хотел сказать, что с встроенными функциями (и многие компиляторы до C99 уже поддерживали встроенные расширения), производительность не может быть аргументом против геттеров. С разумным оптимизирующим компилятором у вас должна получиться та же самая сборка.
Groo

2

Любое конкретное прерывание будет глобальным ресурсом. Однако иногда бывает полезно, чтобы несколько прерываний использовали один и тот же код. Например, система может иметь несколько UART, каждый из которых должен использовать аналогичную логику отправки / получения.

Хороший подход к обработке, который заключается в том, чтобы поместить объекты, используемые обработчиком прерываний или указатели на них, в объект структуры, а затем получить фактические аппаратные обработчики прерываний, которые будут выглядеть примерно так:

void UART1_handler(void) { uart_handler(&uart1_info); }
void UART2_handler(void) { uart_handler(&uart2_info); }
void UART3_handler(void) { uart_handler(&uart3_info); }

Объекты uart1_info, uart2_infoи т.д. бы глобальные переменные, но они были бы только глобальные переменные , используемые обработчики прерываний. Все остальное, к чему будут обращаться обработчики, будет обрабатываться внутри них.

Обратите внимание, что все, к чему обращается как обработчик прерываний, так и код основной линии, должно быть квалифицировано volatile. Возможно, проще всего просто объявить volatileвсе, что будет использоваться обработчиком прерываний, но, если важна производительность, можно написать код, который копирует информацию во временные значения, оперирует ими, а затем записывает их обратно. Например, вместо того, чтобы писать:

if (foo->timer)
  foo->timer--;

записывать:

uint32_t was_timer;
was_timer = foo->timer;
if (was_timer)
{
  was_timer--;
  foo->timer = was_timer;
}

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


0

Вот три идеи:

Объявите переменную флага как статическую, чтобы ограничить область действия одним файлом.

Сделайте переменную флага частной и используйте функции getter и setter для доступа к значению флага.

Используйте сигнальный объект, такой как семафор, вместо переменной-флага. ISR установит / опубликует семафор.


0

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

volatile bool *flag;  // must be initialized before the interrupt is enabled

ISR(...) {
    *flag = true;
}

или объектно-ориентированный код с «виртуальной» функцией:

HandlerObject *obj;

ISR(...) {
    obj->handler_function(obj);
}

… Первый шаг должен включать фактическую глобальную (или, по крайней мере, статическую) переменную для достижения этих других данных.

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


Вы должны объявить флаг как volatile int *.
следующий взлом

0

Сейчас я пишу для Cortex M0 / M4, и подход, который мы используем в C ++ (тега C ++ нет, поэтому этот ответ может быть не по теме), заключается в следующем:

Мы используем класс, CInterruptVectorTableкоторый содержит все подпрограммы обработки прерываний, которые хранятся в фактическом векторе прерываний контроллера:

#pragma location = ".intvec"
extern "C" const intvec_elem __vector_table[] =
{
  { .__ptr = __sfe( "CSTACK" ) },           // 0x00
  __iar_program_start,                      // 0x04

  CInterruptVectorTable::IsrNMI,            // 0x08
  CInterruptVectorTable::IsrHardFault,      // 0x0C
  //[...]
}

Класс CInterruptVectorTableреализует абстракцию векторов прерываний, поэтому вы можете связывать различные функции с векторами прерываний во время выполнения.

Интерфейс этого класса выглядит следующим образом:

class CInterruptVectorTable  {
public :
    typedef void (*IsrCallbackfunction_t)(void);                      

    enum InterruptId_t {
        INTERRUPT_ID_NMI,
        INTERRUPT_ID_HARDFAULT,
        //[...]
    };

    typedef struct InterruptVectorTable_t {
        IsrCallbackfunction_t IsrNMI;
        IsrCallbackfunction_t IsrHardFault;
        //[...]
    } InterruptVectorTable_t;

    typedef InterruptVectorTable_t* PinterruptVectorTable_t;


public :
    CInterruptVectorTable(void);
    void SetIsrCallbackfunction(const InterruptId_t& interruptID, const IsrCallbackfunction_t& isrCallbackFunction);

private :

    static void IsrStandard(void);

public :
    static void IsrNMI(void);
    static void IsrHardFault(void);
    //[...]

private :

    volatile InterruptVectorTable_t virtualVectorTable;
    static volatile CInterruptVectorTable* pThis;
};

Вам необходимо создать функции, которые хранятся в таблице векторов, staticпотому что контроллер не может предоставить this-pointer, поскольку таблица векторов не является объектом. Таким образом, чтобы обойти эту проблему, у нас есть статический pThisуказатель внутри CInterruptVectorTable. После входа в одну из статических функций прерывания он может получить доступ к pThis-pointer, чтобы получить доступ к членам одного объекта CInterruptVectorTable.


Теперь в программе вы можете использовать SetIsrCallbackfunctionуказатель функции для staticфункции, которая должна вызываться при возникновении прерывания. Указатели хранятся в InterruptVectorTable_t virtualVectorTable.

И реализация функции прерывания выглядит так:

void CInterruptVectorTable::IsrNMI(void) {
    pThis->virtualVectorTable.IsrNMI(); 
}

Так что это вызовет staticметод другого класса (который может бытьprivate ), который затем может содержать другой static this-pointer, чтобы получить доступ к переменным-членам этого объекта (только один).

Я думаю, вы могли бы построить и интерфейс, как IInterruptHandler и хранить указатели на объекты, поэтому вам не нужен static this-pointer во всех этих классах. (возможно, мы попробуем это на следующей итерации нашей архитектуры)

Другой подход хорошо работает для нас, так как единственные объекты, которым разрешено реализовывать обработчик прерываний, это те, которые находятся на уровне аппаратной абстракции, и у нас обычно есть только один объект для каждого аппаратного блока, так что это нормально работает с static this -pointers. И уровень аппаратной абстракции предоставляет еще одну абстракцию для прерываний, ICallbackкоторая называется, которая затем реализуется на уровне устройств над аппаратными средствами.


У вас есть доступ к глобальным данным? Конечно, но вы можете сделать большую часть необходимых глобальных данных частными, как, например,this -pointers и функции прерывания.

Это не пуленепробиваемый, и это добавляет накладные расходы. Вы будете бороться за реализацию стека IO-Link, используя этот подход. Но если вы не очень сильно привязаны к временным интервалам, это работает очень хорошо, чтобы получить гибкую абстракцию прерываний и связи в модулях без использования глобальных переменных, которые доступны везде.


1
«чтобы вы могли связать различные функции с векторами прерываний во время выполнения» Это звучит как плохая идея. «Цикломатическая сложность» программы просто прошла бы через крышу. Все комбинации вариантов использования должны были бы быть проверены, чтобы не было ни времени, ни конфликтов использования стека. Много болит голова за функцию с очень ограниченной полезностью IMO. (Если у вас нет случая с загрузчиком, это другая история) В целом это пахнет метапрограммированием.
Лундин

@ Лундин Я не совсем понимаю твою точку зрения. Мы используем его, например, для привязки прерывания DMA к обработчику прерываний SPI, если DMA используется для SPI, и к обработчику прерываний UART, если он используется для UART. Оба обработчика должны быть проверены, но это не проблема. И это, безусловно, не имеет ничего общего с метапрограммированием.
Арсенал

DMA - это одно, а присвоение векторов прерываний во время выполнения - это совсем другое. Имеет смысл разрешить настройку драйвера DMA во время выполнения. Таблица векторов, не так уж и много.
Лундин

@Lundin Я думаю, у нас разные взгляды на это, мы могли бы начать чат об этом, потому что я до сих пор не вижу твоей проблемы с этим - так что, возможно, мой ответ так плохо написан, что вся концепция неправильно понята.
Арсенал
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.