Часто буває так, що програма повинна вести себе по-різному в залежності від настання якихось подій, від часу пройшов з початку роботи і т.д.
Наприклад, давайте розглянемо пристрій, подобу гірлянди. Припустимо ми хочемо, щоб до Arduino були підключені світлодіоди, режим світіння яких можна було б змінювати натисканням кнопки. Режиму може бути три:
Світлодіоди горять постійно
Світлодіоди то вимикаються, то включаються кожні 2 секунди
Світлодіоди плавно набирають яскравість від нуля до максимуму за 10 секунд, гаснуть і починають набирати яскравість заново
Натискаючи кнопку, ми хочемо переходити до чергового режиму: 1 → 2, 2 → 3, 3 → 1. Власне точно так же, як це відбувається в багатьох ялинкових гірляндах.
Ми не будемо займатися управлінням кожним світлодіодом окремо. Просто припустимо, що всі вони відразу підключені до одного з пинов Arduino через MOSFET-транзистор або інший комутатор. До якогось іншого піну при цьому підключена тактова кнопка для зміни режиму.
При такому розкладі, скетч, який робить всю роботу, може виглядати так:
Вийшла не найпростіша програма. Але і логіки у нас більш ніж достатньо. Крок за кроком розберемося що тут написано. Почнемо з деяких макроозначень.
Як вже говорилося, у нас є 3 режими: постійне світіння, мигання, наростання яскравості. Але як їх позначати, щоб сказати процесору який з них поточний, який наступний і т.п. Як ми вже знаємо, все, з чим працює процесор - цілі числа. Знаючи це, ми можемо всі можливі режими просто пронумерувати:
0 - режим постійного світіння, ми назвали його ім'ям STATE_SHINE
1 - режим миготіння, ми назвали його STATE_BLINK
2 - режим наростання яскравості, ми назвали його STATE_FADE
Використання загального префікса в назвах, звичайно, не обов'язково - це всього-на-всього макроозначення - але для угруповання пов'язаних за змістом значень префікси досить зручні і зустріти їх можна досить часто.
У кожному режимі наше пристрій робить щось унікальне, відмінне від того, що відбувається в інших режимах. Поточним може бути лише один режим. А можливі переходи чітко визначені: кожен наступний включається при натисканні кнопки. Такі системи називаються кінцевими автоматами. а їх режими називаються станами (англ. state).
Якщо говорити формально, кінцевий автомат - це система з кінцевим, відомим кількістю станів, умови переходів між якими фіксовані і відомі, а поточним завжди є рівно один стан.
Що ж, ми робимо кінцевий автомат - відмінно. Поточний стан, тобто режим світіння будемо зберігати в змінної з ім'ям ledState. яку спочатку встановимо в значення STATE_SHINE. Таким чином, при включенні Arduino система буде перебувати в режимі постійного світіння.
Про призначення інших макроозначень і змінних поговоримо по ходу справи.
Ланцюжок переходів між станами
Розберемо функцію loop. А точніше, її першу частину і ті визначення, які її стосуються:
Насамперед ми зчитуємо стан кнопки в логічну змінну isButtonDown. А відразу після цього перевіряємо умова того, що вона була натиснута тільки що, а не знаходиться в цьому стані з попереднього виклику loop. Ви могли дізнатися типовий прийом для визначення кліка кнопки. Це він і є, тому виклик delay і призначення wasButtonDown в кінці повинні бути вам зрозумілі. Зосередимося на тому, що відбувається всередині умовного виразу if.
оператор рівності
Його суть в тому, щоб встановити замість поточного стану наступне. За допомогою умов визначається поточний стан, а в коді веткок встановлюється відповідне наступний стан.
Зверніть увагу, що в C ++ перевірка на рівність здійснюється символом ==. тобто подвійним знаком рівності. Так записується оператор «дорівнює». Значення логічного виразу буде істинним, якщо значення зліва і праворуч від == рівні.
Типова помилка при програмуванні - переплутати оператор рівності == з оператором присвоювання =. Якщо написати:
програма буде абсолютно коректна з точки зору компілятора C ++, але відбуватися буде зовсім не те, що передбачається: спочатку змінної ledState буде присвоєно значення STATE_SHINE. а тільки потім буде перевірено чи істинне її значення.
Тому, наприклад, в нашому випадку, якби ми допустили таку помилку, при проходженні цього коду ledState завжди б перезаписувати значенням STATE_SHINE. яке в свою чергу оголошено як 0. 0 в умови еквівалентний false. а отже всередину блоку коду ми б ніколи не потрапили.
Отже, якщо розглядати нашу ланцюжок з if в цілому, можна бачити, що ми в умовах намагаємося зрозуміти який стан встановлено зараз і на основі цього, оновлюємо значення ledState. встановлюючи його в наступний за нашим задумом режим.
Своя логіка в кожному стані
Ми зробили все, що необхідно для того, щоб по змінної ledState можна було зрозуміти, що саме потрібно робити зі світлодіодами прямо зараз. Залишилося лише реалізувати цю логіку на практиці.
Розглянемо другу половину коду функції loop і визначення, які в ній використовуються.
У тілі loop ми бачимо ланцюжок умовних виразів, де в кожному з умов ми порівнюємо поточний стан ledState черзі з усіма можливими. Суть точно та ж, що і була раніше, при перемиканні станів після натискання кнопки: в залежності від поточного значення ledState виконати той чи інший блок коду.
У наведеному коді можна чітко побачити 3 блоку коду: по одному на кожен режим світіння. Перший з них виповнюється в разі, якщо ledState одно STATE_SHINE. тобто якщо поточний режим - безперервне світіння. Блок коду в цьому випадку примітивний, нам просто потрібно переконатися, що світлодіоди включені:
Зрозуміло, що проходячи loop в наступний раз, процесор знову потрапить в цю гілку і ще раз включить вже включені світлодіоди. Але в цьому немає нічого страшного, це абсолютно нормальне явище, без побічних ефектів.
режим миготіння
Далі цікавіше. Якщо поточний стан було встановлено в STATE_BLINK. тобто режим миготіння кожні 2 секунди, виконується гілка з кодом:
Ви знаєте, що для того, щоб просто моргнути світлодіодом на Arduino досить послідовно викликати digitalWrite і delay. то включаючи, то вимикаючи пін:
Але ми вчинили інакше і трохи складніше. Навіщо?
Справа в тому, що виклик delay призводить до приспання процесора, і він просто «застигає» на виклик на вказаний проміжок часу. Він не зможе займатися нічим іншим, крім як спати. А тепер згадаємо, що наша гірлянда завжди «одночасно» робить 2 речі:
Управляє світінням світлодіодів
Ловить момент натискання кнопки, щоб перемикати режими
Таким чином, якщо в режимі миготіння процесор засне на 2 секунди, а в цей проміжок часу ми кликнемо кнопку перемикання, її натискання залишиться непоміченим і перехід на наступний режим не відбудеться. А нам би цього зовсім не хотілося.
Як вирішити проблему? Не застосовувати препарат delay зовсім! Замість цього, ми кожен раз потрапляючи в нашу гілку можемо заново розраховувати: чи повинна гірлянда бути включена або виключена саме в поточний момент часу.
Для цього ми використовуємо невелику арифметичне вираз. (Millis () / BLINK_DELAY)% 2.
Далі ми просто беремо залишок від ділення на 2 цього проміжного результату на. Таким чином, підсумкове значення буде то 0. то 1. перемикаючись кожні 2 секунди. Те, що нам потрібно! І ми просто передаємо обчислене значення як аргумент при виклику digitalWrite.
Режим наростання яскравості
Якщо поточний стан нашої гірлянди - STATE_FADE. буде виконуватися третій блок, який змусить світлодіоди плавно набирати свою яскравість протягом 10 секунд, гаснути і набирати яскравість знову:
Суть приблизно така ж, що і для миготіння. Просто ми використовуємо трохи інший вираз для розрахунків і викликаємо analogWrite замість digitalWrite.
Наше завдання: змусити світлодіоди набирати яскравість від нуля до максимуму рівно за 10 секунд або, що те ж саме, 10000 мілісекунд. Функція analogWrite як параметр яскравості приймає значення від 0 до 255, тобто всього 256 градацій. Тому, для збільшення яскравості на одну градацію має пройти 10000 мс ÷ 256 ≈ 39 мс. Саме ці значення ми визначили на початку програми:
Так значення виразу millis () / FADE_STEP_DELAY ставатиме на одиницю більше кожен раз, коли проходить 39 мс.
Зверніть увагу на дужки у визначенні FADE_STEP_DELAY. Оскільки значення макроозначень підставляються в програму як є, ми отримуємо millis () / (10000/256); а якщо дужок б не було, вийшло б millis () / 10000 / 256. що абсолютно не одне і те ж з точки зору математики. Тому додавайте круглі дужки навколо будь-яких арифметичних виразів, коли використовуєте їх в якості значень макроозначень.
Нарешті, від проміжного значення millis () / FADE_STEP_DELAY ми отримуємо залишок ділення на 256. Таким чином, все буде починатися спочатку всякий раз, коли проміжне значення буде ставати кратним 256. Те, що потрібно!
арифметика станів
Ще раз поглянемо на код, який забезпечував нам включення чергового стану при кліці кнопки:
Якби станів у нас було не 3, а 33, код би розтягнувся на багато рядків, але при цьому нічого нового і унікального в програму б не додавав: він би залишався однотипним. Якщо вам при написанні скетчу здається, що якісь його місця зводяться до монотонного набору інструкцій, слабо відрізняються один від одного, майже напевно існує спосіб спростити цей код, зробити його простіше, зрозуміліше, компактніше. Варто лише подумати.
Що можна зробити з нашої ланцюжком? Згадаймо, що стану для процесора - це всього лише цілі числа. Ми визначили їх за допомогою макроозначень.
Ці числа ми визначили послідовно. Тому перемикання ledState на чергове значення - це ні що інше, як додаток одиниці. Єдина заковика - перехід з останнього стану в перше. Але нам вже знайомий оператор залишку від ділення, а за допомогою нього легко врахувати цей сценарій. Тоді весь код для цього документа включено таке стану можна написати без всякого розгалуження так:
Де TOTAL_STATES - загальна кількість станів, яке ми можемо визначити як:
перерахування enum
З точки зору компіляції це нічим не відрізняється від:
Але у випадку з enum нам не довелося явно прописувати значення 0, 1, 2, 3 і т.д. У перерахування, перша константа автоматично отримує значення 0, а кожна наступна на одиницю більше.
І чим ще добре перерахування: ми можемо використовувати його ім'я (State в нашому випадку) в якості типу даних, так само як int. bool і т.п. Тобто ми можемо визначити змінну поточного стану так:
За кадром ledState залишилася все тим же цілим числом, що і раніше, але сама програма тепер стала трохи зрозуміліше і наочніше: ми чітко позначили, що збираємося зберігати в нашій змінної.
Зібравши все разом, отримаємо оновлений варіант нашої програми:
Вираз вибору switch
Для того, щоб зрозуміти що робити зі світлодіодами гірлянди в даний конкретний момент ми використовували ланцюжок з виразів if. де по черзі порівнювали ledState з усіма можливими станами. Це досить поширений сценарій і для нього в C ++ існує вираз вибору switch. Ми можемо використовувати його для нашої мети:
Не сказати, що код став компактніше, але він став трохи наочніше.
Суть вираження switch така. Спочатку обчислюється значення арифметичного виразу, записаного в круглих дужках. У нашому випадку - це просто отримання значення ledState. Потім, в блоці коду між фігурними дужками шукається мітка case. чиє значення дорівнює обчисленому. Виконання коду починається з неї і йде послідовно до кінця блоку (не до наступного case). Виконання блоку можна завершити достроково виразом break.
Часта помилка від неуважності - забути поставити break. У цьому випадку процесор виконає код, що належить іншим мітках, а це найчастіше - не те, що потрібно. Так, це неприємна особливість C ++, але так вже склалося історія. Спочатку це було зроблено для того, щоб можна було перерахувати відразу кілька значень на одну гілку. Наприклад, якби для станів STATE_FADE і STATE_BLINK ми, за задумом, мали б були робити одне і те ж, ми могли б написати:
Також в switch. на останньому місці можна вказати мітку зі спеціальним ім'ям default. Вона буде виконана, якщо жодна інша мітка case не підійшла за значенням.
Отже, ви познайомилися з кінцевими автоматами, станами, переходами, принципами одночасного виконання декількох завдань. У нашому прикладі, для різних станів ми використовували досить просту логіку: один виклик digitalWrite з нехитрим арифметичним виразом; а для перемикання станів і зовсім використовували послідовні переходи по кліку кнопки.
Ніщо не заважає вам розвинути цю тему, щоб зробити набагато більше розумний пристрій. Наприклад, кімнатний робот може мати стану патрулювання для виявлення кота, стан переслідування кота, стан індикації про низький рівень заряду батареї. Перемикання відбуватимуться за сигналами з датчиків і вже не будуть настільки прямолінійні. Логіка самих станів при цьому може бути досить складною: потрібно отримати значення з декількох сенсорів, вибрати напрямок руху, покриття моторами і т.п.
Так само, як і з усім іншим, ніщо не заважає робити вкладені кінцеві автомати: тобто стану, які самі по собі є кінцевими автоматами. Головне підтримувати код струнким і читаним, тоді у вас все вийде!
За винятком випадків, коли вказано інше, вміст цієї Вікі підпадає під дію такої ліцензії: CC Attribution-Noncommercial-Share Alike 3.0 Unported