Уникання глобальних змінних при використанні переривань у вбудованих системах


13

Чи існує хороший спосіб здійснення зв’язку між ISR та рештою програми для вбудованої системи, яка дозволяє уникнути глобальних змінних?

Здається, що загальною схемою є глобальна змінна, яка поділяється між ISR та рештою програми та використовується як прапор, але це використання глобальних змінних йде проти мене. Я включив простий приклад з використанням ISRs стилю avr-libc:

volatile uint8_t flag;

int main() {
    ...

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

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

Я не бачу осторонь того, що є, по суті, питанням обстеження; будь-які змінні, доступні як для МСБ, так і для решти програми, повинні бути, власне, глобальними? Незважаючи на це, я часто бачив, як люди говорять про те, що "глобальні змінні є одним із способів здійснення комунікації між ISR та рештою програми" (наголос мій), який, мабуть, означає, що існують інші методи; якщо є інші методи, які вони?



1
Не обов’язково правда, що ВСІ інші програми мали б доступ; якщо ви оголосили змінну статичною, її бачив би лише файл, в якому була оголошена змінна. Зовсім не важко мати змінні, які видно у всьому одному файлі, але не в решті програми, і це може допомогти.
DiBosco

1
поруч прапор повинен бути оголошений непостійним, оскільки ви використовуєте / змінюєте його поза нормальним потоком програми. Це змушує компілятор не оптимізувати жодне читання / запис до прапора, а виконувати фактичну операцію читання / запису.
наступний хак

@ next-hack Так, це абсолютно правильно, вибачте, що я просто намагався швидко знайти приклад.

Відповіді:


18

Існує фактично стандартний спосіб зробити це (якщо припустити програмування на С):

  • Переривання / 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 мс мають розмір слова, яке можна прочитати за одну атомну операцію. Якщо ви використовуєте мову високого рівня, не забудьте сказати компілятору, що ці змінні можуть змінюватися асинхронно. Наприклад, в C ви оголошуєте їх зовнішніми непостійними . Звичайно, це щось, що входить до складу консервованого файлу, тому вам не потрібно пам’ятати про це для кожного проекту.

    Я іноді роблю лічильник 1 галочок загальним лічильником часу, що минув, тому роблю 32 біта в ширину. Це не можна прочитати в одній атомній операції на багатьох малих мікро, які я використовую, щоб вони не стали глобальними. Натомість передбачено розпорядок роботи, який читає багатослівне значення, розглядає можливі оновлення між прочитаними та повертає результат.

    Звичайно, могли бути підпрограми, щоб отримати менші 1 мс, 10 мс тощо, лічильники галочок. Однак це насправді дуже мало для вас, додає багато інструкцій замість читання одного слова та використовує інше місце стека викликів.

    Який мінус? Я припускаю, що хтось міг зробити помилку, яка випадково записується в один із лічильників, що потім може зіпсувати інші терміни в системі. Навмисно писати до лічильника не було б сенсу, тому такий вид помилок повинен бути чимось ненавмисним, як помилка друку. Здається, це дуже малоймовірно. Я не пам'ятаю, щоб коли-небудь траплялося у понад 100 маленьких мікроконтролерів.

  2. Кінцеві відфільтровані та відрегульовані значення A / D. Загальне, що потрібно зробити - це читання рутинного переривання ручки з A / D. Зазвичай я читаю аналогові значення швидше, ніж це потрібно, потім застосовую невелику фільтрацію з низьким проходом. Також часто застосовуються масштабування та компенсація.

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

    Решта системи просто бачить актуальне оновлене значення із зазначенням напруги живлення. Він не знає і не повинен дбати про те, коли саме це оновлено, тим більше, що він оновлюється набагато частіше, ніж час осідання фільтра низьких частот.

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


Я ходив на терапію, на повільному тижні, дуже намагаючись вивести свій код. Я бачу думку Лундіна щодо обмеження змінного доступу, але я дивлюся на мою реальну систему і думаю, що це така віддалена можливість, ЩО ЛЮБИМА дійсно зірве критичну для глобальної системи змінну. Функції Getter / Setter в кінцевому рахунку коштують вам накладні
гроші,

3
@ Leroy105 Проблема не в тому, що "терористи" навмисно зловживають глобальною змінною. Забруднення простору імен може бути проблемою у великих проектах, але це можна вирішити за допомогою хорошого називання. Ні, справжня проблема полягає в тому, що програміст намагається використовувати глобальну змінну за призначенням, але не вдається зробити це правильно. Або тому, що вони не усвідомлюють проблеми перегонів, які існують з усіма ISR, або тому, що вони заплутують реалізацію механізму обов'язкового захисту, або просто тому, що вони розкривають використання глобальної змінної у всьому коді, створюючи щільну зв'язок і нечитабельний код.
Лундін

Ваші бали є дійсними Олін, але навіть у цих прикладах заміна extern int ticks10msна inline int getTicks10ms()абсолютно не змінить складену збірку, в той час як з іншого боку буде важко випадково змінити її значення в інших частинах програми, а також дозволить вам спосіб "підключити" цей виклик (наприклад, знущатися над часом під час тестування одиниць, записувати доступ до цієї змінної чи будь-чого іншого). Навіть якщо ви стверджуєте, що шанс Сан-програміста змінити цю змінну на нуль, вартість вбудованого геттера не коштує.
Груо

@Groo: Це справедливо лише в тому випадку, якщо ви використовуєте мову, яка підтримує вбудовані функції, і це означає, що визначення функції getter має бути видимим для всіх. Насправді, використовуючи мову високого рівня, я більше використовую функції getter і менше глобальні змінні. У збірці просто набагато простіше схопити значення глобальної змінної, ніж турбуватися з функцією getter.
Олін Латроп

Звичайно, якщо ви не можете вступити, то вибір не такий простий. Я хотів сказати, що з вбудованими функціями (і багатьма компіляторами попереднього C99 вже підтримуються вбудовані розширення), продуктивність не може бути аргументом проти Getters. З розумним оптимізуючим компілятором, ви повинні закінчити таку ж виготовлену збірку.
Гроо

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);
}

... перший крок повинен включати фактичну глобальну (або принаймні статичну) змінну для досягнення цих інших даних.

Усі ці механізми додають опосередкованість, тому зазвичай цього не робиться, якщо ви хочете вичавити останній цикл з обробника переривання.


ви повинні оголосити прапор нестабільним 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-показник, оскільки векторна таблиця не є об'єктом. Тож для подолання цієї проблеми у нас є статичний pThis-показник всередині CInterruptVectorTable. Після введення однієї зі статичних функцій переривання, він може отримати доступ до pThis-показника, щоб отримати доступ до членів одного об'єкта CInterruptVectorTable.


Тепер у програмі ви можете використовувати SetIsrCallbackfunctionдля надання вказівника на staticфункцію, яку потрібно викликати, коли відбувається переривання. Покажчики зберігаються в InterruptVectorTable_t virtualVectorTable.

А реалізація функції переривання виглядає приблизно так:

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

Отже, викликме staticметод іншого класу (який може бути private), який потім може містити інший static this-показник для отримання доступу до змінних членів цього об'єкта (лише однієї).

Я думаю, ви могли б створити та інтерфейсувати, як IInterruptHandlerі зберігати покажчики на об’єкти, тож вам не потрібен static this-показник у всіх цих класах. (можливо, ми спробуємо це в наступній ітерації нашої архітектури)

Інший підхід для нас працює чудово, оскільки єдиними об'єктами, дозволеним реалізувати обробник переривань, є ті, що знаходяться в шарі апаратної абстракції, і ми зазвичай маємо лише один об'єкт для кожного апаратного блоку, тому добре працювати з static this-інструментами. А апаратний рівень абстракції забезпечує ще одну абстракцію переривань, називається ICallbackяка потім реалізується в шарі пристрою над апаратним забезпеченням.


Ви маєте доступ до глобальних даних? Зрозуміло, що ви робите, але ви можете зробити більшість необхідних глобальних даних приватними, як this-показчики та функції переривання.

Це не куленепробивна, і це додає накладні витрати. Ви будете намагатися реалізувати стек IO-Link, використовуючи такий підхід. Але якщо ви не дуже тісні з часом, це добре працює, щоб отримати гнучку абстракцію переривань і зв’язок в модулях без використання глобальних змінних, доступних звідусіль.


1
"так що ви можете прив'язувати різні функції до векторів переривання під час виконання" Це звучить як погана ідея. «Цикломатична складність» програми просто би пройшла через дах. Усі комбінації випадків використання повинні бути протестовані, щоб не було ні конфліктів часу, ні конфлікту використання. Багато болить голова за функцію з дуже обмеженою корисністю IMO. (Якщо у вас немає випадку завантажувача, це вже інша історія) Загалом це пахне метапрограмуванням.
Лундін

@Lundin Я насправді не бачу вашої точки зору. Ми використовуємо його для прив'язки, наприклад, перерви DMA до обробника переривання SPI, якщо DMA використовується для SPI, і до обробника переривання UART, якщо він використовується для UART. Обидва обробники повинні бути протестовані, але це не проблема. І це, безумовно, не має нічого спільного з метапрограмуванням.
Арсенал

DMA - одне, присвоєння векторів переривань під час виконання - це зовсім інше. Має сенс дозволити налаштування драйверів DMA бути змінними під час роботи. Векторний стіл, не так вже й багато.
Лундін

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