Віртуальні машини
Як тільки ми переходимо від простеньких програм у пару дій до більш складних, ми стикаємося з однією з основних проблем, що виникають під час програмування мікроконтролерів: проблема реалізації багатозадачності. У «великих» програмах зазвичай необхідно виконувати кілька дій одночасно: перевіряти вхідну інформацію (стан портів, дані по 1-Wire, I2C, UART тощо), стежити за відображенням вихідної інформації (дисплеєм та світлодіодами, наприклад) , виконувати якісь керуючі дії (включення-вимикання чогось, відсилання повідомлень) та ще багато чого! На комп'ютерах за одночасність виконання різних дій відповідає операційна система - саме вона стежить за тим, хто з програм отримає зараз процесорний час, вона виділяє кожному за потребами пам'ять. При цьому операційна система може керувати викликом програм по-різному: є багатозадачність, що витісняє і невитісняє. При невитіснюючій багатозадачності процеси допомагають операційній системі - вони самі визначають, коли варто закінчити зі своїми справами і запропонувати іншим процесам час і ресурси - тобто операційній системі залишається лише викликати підпрограми в якійсь певній послідовності. При витісняючої багатозадачності кожен процес вважає себе найважливішим і зазвичай готовий відхопити часу та ресурсів по максимуму, а операційна система може грубо переривати такі процеси та передавати ресурси іншим. Для того, щоб реалізувати багатозадачність, нам теж доведеться зробити щось на кшталт простенької операційної системи-перемикача завдань – будемо використовувати невитіснюючу багатозадачність. При цьому перше і найважливіше, що потрібно зробити, - це виділити так звані віртуальні машини.
Що таке віртуальна машина
Овали -це стани, переходи – відповідно стрілки; червоним виділено аналіз переходів, синім – дії, що ми виконуємо може. Усього у нас вийшло три стани: перший – це коли ми чекаємо натискання на кнопку; у цьому стані ми не робимо нічого, просто перевіряємо, чи не натиснута кнопка. Якщо кнопка натиснута, то переходимо в стан 2, а якщо ні - так і залишаємося в стані 1. Стан 2 - це саме відправлення повідомлення; після виконання дій у стані відбувається безумовний (незалежно від вхідних сигналів) перехід у стан 3. У стані 3 ми чекаємо, коли кнопка буде відпущена; так само, як і в стані 1 ми нічого не робимо, тільки перевіряємо кнопку. Як тільки кнопка відпущена, переходимо в стан 1. Тепер розглянемо автомат Милі – в ньому спочатку відбувається перевірка вхідних сигналів, і вже по переходах відбувається виконання дій. Ось як це виглядає схематично:
Однак у житті дуже часто буває так, що легше скомбінувати автомати Мура та Мілі: іноді потрібно при попаданні в стан спочатку зробити якісь дії, потім перевірити вхідні сигнали, і за результатами цієї перевірки теж зробити якісь дії і потім уже перейти до інший стан. Такі автомати називаються комбінованими, їх дуже зручно застосовувати – проте потрібно їх уважно проектувати!
А тепер повернемося до нашого прикладу – спробуємо продати віртуальну машину на базі схеми автомата Мура. Вся фішка в тому, що ми використовуємо змінну, що відповідає за поточний стан віртуальної машини - назвемо її, наприклад, ButtonMachineState. Вона може приймати три значення - наприклад, 1 - коли ми чекаємо натискання на кнопку, і 2 - коли знаємо, що кнопка натиснута, і відправляємо повідомлення, і 3 - коли чекаємо, коли кнопка будевідпущено. Тоді сама функція для цієї простенької машини буде такою:
У нашому випадку змінна, що відповідає за поточний стан машини, буде глобальною (через відсутність класів, як у С++) – хоча насправді стан машини можна передавати у функцію через параметр, а повертати новий стан через результат. Для спрощення розуміння коду зробили її у вигляді перерахування – щоб можна було коротко описати кожен стан. Виходить, що при кожному виклику функції нашої машини ми виконуємо пару-трійку дій і витрачаємо на це зовсім небагато часу: наприклад, потрібна кнопка натиснута, тоді в перший виклик функції ми тільки перевіримо, який стан у кнопки, і визначимо, що Значення ButtonMachineState має змінитися. А на другий виклик ми вже відправимо повідомлення, і знову змінимо ButtonMachineState. Якщо ми так само реалізуємо ще кілька машин, і будемо послідовно викликати їх в основному циклі, то вийде, що все працює практично одночасно. Звичайно, якщо у нас будуть десятки таких машин, то програма «підгальмовуватиме» - але це виникає навіть на компах, вірно?) Їдемо далі: в мікроконтролерах часто буває, що виконання деяких дій критично за часом: наприклад , перемикання відображення розрядів на семисегментному дисплеї, або робота з деякими інтерфейсами типу 1-Wire, зчитування значень АЦП і т.д. За всієї користі звичайні віртуальні машини що неспроможні гарантувати однакову тривалість виконання кожного зі своїх станів – у разі використовують переривання, які однозначно викликаються через рівні проміжки часу чи за будь-якої події. Наприклад, кожну секунду потрібно відображати час, і ми маємо переривання, яке викликається раз на секунду. Найголовніше, що потрібно запам'ятати- Те, що не можна всю машину запихати в переривання! Переривання - така страшна річ, яка однозначно постійно забирає шмат часу у програми - а значить, і у всіх "звичайних" віртуальних машин.
Тому в перериванні в цьому випадку у нас буде тільки відправлятися посилка - назвемо її OnShowTime.
Пару слів про прапори та посилки (повідомлення, якщо називати за аналогією з Windows): вважаємо, що прапор знімається і зводиться одним і тим же «пристроєм» або машиною – таким чином, якщо якась інша машина не встигла помітити зведення прапора до того , як він був знятий - то сам винен. Посилання ж, або повідомлення - це прапор, який зводиться однією машиною, але вона при цьому не спантеличується подальшою долею цієї посилки - чи потрібна ця інформація будь-якій іншій машині. Такий прапор може встановлюватися кілька разів поспіль, «не знімаючись» - натомість це гарантує, що інформацію цієї посилки – наприклад, настання певної події – точно встигнуть отримати, навіть якщо ця подія вже пройшла і навіть настала ще раз: наприклад, такою подією може бути "натискання на кнопку", або "необхідність відображення часу", як у прикладі вище - навіть якщо користувач кнопку вже відпустив, або секунда вже пройшла, відповідні події дії все одно потрібно виконати будь-що-будь. І тільки після виконання дій посилка очищається – програма знову готова чекати на її отримання. Використовуючи цей підхід, слід враховувати існування таких небезпек: при використанні прапора є небезпека пропустити його короткочасне зведення; а при використанні посилки є ймовірність, що ми не помітимо, як подія відбулася кілька разів. У такому разі, якщо кількість подій важлива, додають ще змінну-лічильник. Ну а найголовніше привикористання прапорів та посилок – це не забувати їх очищати!
Як виділяти віртуальні машини
Для того, щоб виділити в пристрої, що розробляється, віртуальні машини, насамперед потрібно його змоделювати в голові!
Взагалі для того, щоб почати працювати над якоюсь програмою чи пристроєм, якщо це не «Hello, world!», потрібно… погасити дисплей, відставити клавіатуру та мишку убік, взяти листочок та ручку, заплющити очі та уявити, що ми хочемо отримати в результаті: як програма буде спілкуватися з користувачем, що вона отримуватиме на вхід, які результати видаватиме. Варто намалювати, які стану будуть у програми – використовуючи ті самі кружечки і стрілочки; якщо програма спілкується з користувачем (а це відбувається практично завжди) – як користувач бачитиме цей процес; приклад моделювання представлений тут
Тільки представивши, як працюватиме програма і, якщо ми програмуємо мікроконтролер, як виглядатиме пристрій, можна переходити до виділення віртуальних машин. Зазвичай вже при моделюванні починають видно обриси окремих «цеглинок» - тут ми працюємо з кнопками, тут - керуємо дисплеєм, особняком стоїть управління через висновки і, наприклад, спілкування через 1-Wire ...