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


17

Я почав писати прошивку для свого продукту, і я тут новачок. Я ознайомився з багатьма статтями про не використання глобальних змінних чи функцій. Чи є обмеження на використання глобальних змінних у 8-бітовій системі чи це повне "Ні-Ні". Як я повинен використовувати глобальні змінні у своїй системі чи я повинен їх повністю уникати?

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


Це питання не характерне для вбудованих систем. Дублікат можна знайти тут .
Лундін

@Lundin З вашого посилання: "Ці дні мають значення лише у вбудованих середовищах, де пам'ять досить обмежена. Щось потрібно знати, перш ніж припустити, що вбудована така ж, як і в інших середовищах, і припустити, що правила програмування однакові у всіх планах".
ендоліт

Обсяг staticфайлу @endolith - це не те саме, що "глобальний", див. мою відповідь нижче.
Лундін

Відповіді:


31

Ви можете успішно використовувати глобальні змінні, якщо ви пам’ятаєте про вказівки @ Phil. Однак ось кілька приємних способів уникнути їх проблем, не роблячи складений код менш компактним.

  1. Використовуйте локальні статичні змінні для стійкого стану, до якого ви хочете отримати доступ лише в одній функції.

    #include <stdint.h>
    void skipper()
    {
        static uint8_t skip_initial_cycles = 5;
        if (skip_initial_cycles > 0) {
            skip_initial_cycles -= 1;
            return;
        }
        /* ... */
    }
  2. Використовуйте структуру для збереження пов'язаних змінних разом, щоб було зрозуміліше, де вони повинні використовуватися, а де ні.

    struct machine_state {
         uint8_t level;
         uint8_t error_code;
    } machine_state;
    
    struct led_state {
        uint8_t red;
        uint8_t green;
        uint8_t blue;
    } led_state;
    
    void machine_change_state()
    {
        machine_state.level += 1;
        /* ... */
        /* We can easily remember not to use led_state in this function. */
    }
    
    void machine_set_io()
    {
        switch (machine_state.level) {
        case 1:
            PIN_MACHINE_IO_A = 1;
            /* ... */
        }
    }
  3. Використовуйте глобальні статичні змінні, щоб зробити змінні видимими лише в поточному файлі C. Це запобігає випадковому доступу за кодом до інших файлів через конфлікти імен.

    /* time_machine.c */
    static uint8_t current_time;
    /* ... */
    
    /* delay.c */
    static uint8_t current_time; /* A completely separate variable for this C file only. */
    /* ... */

На завершення, якщо ви змінюєте глобальну змінну в процесі переривання і читаєте її в іншому місці:

  • Позначте змінну volatile.
  • Переконайтеся, що він атомарний для процесора (тобто 8-бітний для 8-бітного процесора).

АБО

  • Використовуйте механізм блокування для захисту доступу до змінної.

мінливі та / або атомні перешкоди не допоможуть вам уникнути помилок, вам потрібна якась фіксація / семафор або коротко маскування переривань під час запису до змінної.
Джон У

3
Це досить вузьке визначення "добре працювати". Моя думка полягала в тому, що декларування чогось мінливого не запобігає конфліктам. Крім того, ваш 3-й приклад не є чудовою ідеєю - наявність двох окремих глобальних місць з однаковою назвою , як мінімум, ускладнює розуміння / підтримку коду.
Джон У

1
@JohnU Ви не повинні використовувати мінливі для запобігання гоночних умов, адже це не допоможе. Ви повинні використовувати летючі для запобігання небезпечних помилок оптимізації компілятора, які часто зустрічаються у вбудованих системних компіляторах.
Лундін

2
@JohnU: Нормальне використання volatileзмінних полягає в тому, щоб дозволити коду, що працює в одному контексті виконання, щоб повідомити коду в іншому контексті виконання, знати, що щось сталося. У 8-бітовій системі буфером, який має вмістити два байти потужністю не більше 128, можна керувати одним мінливим байтом, який вказує загальну кількість життєвих ресурсів байтів, введених у буфер (мод 256) та інший, що вказує кількість вийнятих байтів протягом усього життя, за умови, що лише один контекст виконання вводить дані в буфер, і лише один виймає дані з нього.
supercat

2
@JohnU: Хоча для управління буфером можливо використовувати якусь форму блокування або тимчасово відключити переривання, це дійсно не потрібно чи корисно. Якщо буфер повинен містити 128-255 байт, кодування мало б змінитись, і якщо він повинен утримувати більше цього, можливо, знадобиться відключення переривань, але для 8-бітових системних буферів може бути мало; системи з більшими буферами можуть, як правило, робити атомні записи величиною більше 8 біт.
supercat

24

Причини, через які ви не хочете використовувати глобальні змінні в 8-бітовій системі, такі ж, як і ви не хочете використовувати їх в будь-якій іншій системі: вони ускладнюють міркування про поведінку програми.

Лише погані програмісти завішують правила типу "не використовувати глобальні змінні". Хороші програмісти розуміють причину, що стоїть за правилами, а потім ставляться до правил, як до вказівок.

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


1
Що сказав @MichaelKaras - важливо зрозуміти, що означають ці речі та як вони впливають на речі (і як вони можуть вас вкусити).
Джон У

5

Вам не слід повністю уникати використання глобальних змінних (коротких "глобальних"). Але, слід використовувати їх розумно. Практичні проблеми із надмірним використанням глобальних мереж:

  • Глобали видно по всій одиниці компіляції. Будь-який код у блоці компіляції може змінювати глобальний. Наслідки модифікації можуть виникнути в будь-якому місці, де ця глобальна оцінка оцінюється.
  • В результаті глобалісти ускладнюють читання та розуміння коду. Програміст завжди повинен пам’ятати про всі місця, де оцінюється та призначається глобальний.
  • Надмірне використання глобалів робить код більш схильним до дефектів.

Доречно додавати префікс g_до імені глобальних змінних. Наприклад, g_iFlags. Коли ви бачите змінну з префіксом у коді, ви відразу визнаєте, що вона глобальна.


2
Прапор не повинен бути глобальним. ISR може викликати, наприклад, функцію, яка має статичну змінну.
Філ Мороз

+1 Я про подібний трюк раніше не чув. Я вилучив цей абзац з відповіді. Як staticпрапор став би видно main()? Ви маєте на увазі, що та сама функція, яка має, staticможе повернути її main()пізніше?
Нік Алексєєв

Це один із способів зробити це. Можливо, функція приймає новий стан для встановлення та повертає старий стан. Існує маса інших способів. Можливо, у вас є один вихідний файл з функцією встановлення прапора, а інший для його отримання зі статичною глобальною змінною, що містить стан прапора. Хоча технічно це "глобальна" терміналогія C, сфера її застосування обмежена лише тим файлом, який містить лише функції, які потрібно знати. Тобто, його сфера застосування не є "глобальною", що справді є проблемою. C ++ забезпечує додаткові механізми інкапсуляції.
Філ Мороз

4
Деякі методи уникнення глобальних даних (наприклад, доступ до обладнання лише через драйвери пристроїв) можуть бути занадто неефективними для 8-бітового середовища з сильним голодом.
Spehro Pefhany

1
@SpehroPefhany Хороші програмісти розуміють причину правил, а потім ставляться до правил, як до вказівок. Коли вказівки суперечать, хороший програміст ретельно зважує рівновагу.
Філ Мороз

4

Перевага глобальних структур даних у вбудованій роботі полягає в тому, що вони є статичними. Якщо кожна змінна, яка вам потрібна, є глобальною, то вам ніколи випадково не вистачить пам’яті, коли введені функції та буде зроблено простір для них у стеці. Але тоді, на той момент, чому є функції? Чому б не одна велика функція, яка обробляє всю логіку та процеси - на зразок програми BASIC без дозволу GOSUB. Якщо ви докладете цю ідею досить далеко, у вас з'явиться типова програма мовної збірки з 1970-х. Ефективна та неможлива підтримка та зйомка проблем.

Тож використовуйте глобалі з розумом, як змінні стану (наприклад, якщо кожна функція повинна знати, чи система перебуває в інтерпретації чи запущеному стані) та інші структури даних, які потрібно бачити у багатьох функціях і як говорить @PhilFrost, - це поведінка ваші функції передбачувані? Чи є можливість заповнити стек вхідним рядком, який ніколи не закінчується? Це питання для проектування алгоритму.

Зауважте, що статика має різний зміст усередині і поза функцією. /programming/5868947/difference-between-static-variable-inside-and-outside-of-a-function

/programming/5033627/static-variable-inside-of-a-function-in-c


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

4

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

У типових комп’ютерних додатках часто немає жодної конкретної причини припускати, що ніколи не буде нічого, ніж щось. Однак у вбудованих системах такі припущення часто набагато доцільніші. Хоча можливо, що типова комп'ютерна програма може бути задіяна для підтримки декількох одночасних користувачів, користувальницький інтерфейс типової вбудованої системи буде розроблений для роботи одним користувачем, який взаємодіє з її кнопками та дисплеєм. Таким чином, він у будь-який момент часу матиме єдиний стан інтерфейсу користувача. Проектування системи таким чином, щоб кілька користувачів могли взаємодіяти з декількома клавіатурами та дисплеями, зажадало б набагато більше складності та потребуватимуть значно більше часу, ніж проектування для одного користувача. Якщо система ніколи не вимагається підтримувати декілька користувачів, будь-які додаткові зусилля, вкладені для полегшення такого використання, будуть марними. Якщо, швидше за все, не знадобиться підтримка багатьох користувачів, можливо, розумніше буде ризикувати відмовитись від коду, який використовується для однокористувацького інтерфейсу у випадку, якщо потрібна підтримка багатокористувача, ніж витрачати додатковий час на додавання багатофункціональних користувачів, підтримка користувачів, яка, ймовірно, ніколи не знадобиться.

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


Тому збереження глобальних змінних, які не зіткнуться між собою, не буде проблемою. наприклад: Day_cntr, week_cntr тощо для підрахунку днів та тижня відповідно. І я вірю, що навмисно не слід використовувати багато глобальних змінних, що нагадують ту саму мету і повинні чітко їх визначати. ​​Дякую багато за переважну відповідь. :)
Rookie91,

-1

Багато людей плутаються з цього приводу. Визначення глобальної змінної:

Щось доступне з будь-якої точки вашої програми.

Це не те саме, що змінні області файлу , які оголошуються ключовим словом static. Це не глобальні змінні, це локальні приватні змінні.

 int x; // global variable
 static int y; // file scope variable

 void some_func (void) {...} // added to demonstrate that the variables above are at file scope.

Чи варто використовувати глобальні змінні? Є кілька випадків, коли це добре:

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

Ви повинні прагнути писати незалежні, автономні модулі коду, розроблені для виконання конкретного завдання. Всередині цих модулів внутрішні змінні області файлів повинні містити приватні дані. Цей метод проектування відомий як об'єктно-орієнтована і широко визнаний як хороший дизайн.


Чому потік?
м.Алін

2
Я думаю, що "глобальна змінна" також може бути використана для опису виділень на глобальний сегмент (не текст, стек чи купу). У цьому сенсі статичні файли та функції статичних змінних є / можуть бути "глобальними". У контексті цього питання дещо зрозуміло, що глобальний має на увазі сегмент не розподілу сфери застосування (хоча, можливо, ОП цього не знала).
Пол А. Клейтон

1
@ PaulA.Clayton Я ніколи не чув офіційного терміна, який називався "глобальний сегмент пам'яті". Ваші змінні опиняться в одному з наступних місць: регістри або стек (розподіл часу виконання), купа (розподіл часу виконання), .даний сегмент (явно ініціалізовані статичні змінні зберігання), .bss сегмент (нульові статичні змінні зберігання), .rodata (читати -тільки константи) або .text (частина коду). Якщо вони закінчуються деінде, це певна настройка, що стосується конкретного проекту.
Лундін

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