Як спеціаліста з силової електроніки та електроприводу, асоціації з «асинхронний» ось такі:
Асинхронний двигун фірми AEG 1890-х
І несправедливість, що возить, поїть, гріє творіння М. О. Доліво-Добровольського, а поминають Н. Теслу. Причому АД другого [Тесли] неймовірно поганий. Двофазний (привіт асиметрії ліній електропередачі), концентрована обмотка на полюсах (прощай КПД), ну і низький пусковий момент. А ось творіння Михаїла Осиповича з першого включення показало КПД у районі 90 % і подвійний пусковий момент, який став де-факто стандартом з 1891 р. і по сьогодні.
Коли керуєш інвертором двигуна, час реального часу вимірюється 10–30 мкс. Це швидко, навіть для сучасних мікросхем. Архітектуру програми треба планувати без зайвих пожирачів часу ядра. Однак є вхідні сигнали, які треба обробляти повільно. Ще є інтерфейси, де виникають колізії та тайм-аути.
Наведу класичний повільний приклад — робота з GSM-модемом. Цей чорний ящик вимагає заклинань у вигляді AT-команд, потрібно почекати відповіді AT OK/ERROR, а відповідь може й не прийти (тайм-аут 100 мс), а іноді треба «підштовхнути» його ще раз, а буває, він взагалі зависає і його треба перезавантажити через живлення.
Зазвичай там, де повільні процеси або пристрої, розробники схиляються до використання RTOS. Тут усе зрозуміло. Є окремі завдання, виконувані паралельно з переключенням контексту та налаштованими пріоритетами. Недоліки: складність використання, треба пам’ятати про переключуваний контекст. Наприклад, виклик printf у різних завданнях може призвести до непередбачуваних наслідків. Не забуваємо й про збільшення розміру коду.
Існує ще один улюблений підхід програмістів мікроконтролерів. А давайте ми зробимо приблизно так:
Суть криється в функції delay, яка є просто циклом із командами nop або через таймер, гальмує програму в цьому місці на потрібний час. Підхід як би нормальний, за винятком того, що використовуваної периферії завжди багато в реальних проєктах, і опитування 1-Wire може тривати до кількох секунд, а на запити майстра Modbus треба відповідати з мінімальною затримкою. У цьому випадку розробники починають використовувати переривання й розподіляти код по них. Після певної кількості переривань можна отримати ситуацію, що одне буде заважати іншому в непередбачуваних комбінаціях, які відлагодити буває дуже важко. За законом Мерфі це починається в продукті. З власного досвіду: сміттєвозка, дощ, сморід, олія від гідравліки, бруд, з дебагером і ноутбуком на колінах шукати голку в «сміттєвому кузові» вантажівки. А в людей нерви й плани, куди приїхати треба. У замовників — скільки поставок зробити. Від керівництва питання: «А чому ваша машина не надсилає координати GPS?». Також з’являються петлі в умовній printf, у якої є внутрішні змінні, і її викликом в основному циклі програми та в якомусь перериванні. Може хаотично впасти й на столі лабораторії, буває комбінацію не повторити.
Еволюційно я прийшов до асинхронності. Загальний принцип такий. Контекст не переключається: коли потрібен delay або подія, перевіряємо лічильник або наявність події й стрибаємо на іншу ділянку коду. Основний недолік даного підходу — це контроль часу життя змінних. Мова C не забезпечує це автоматично, як, наприклад, Rust, тож теж можна вистрелити собі в ногу. Переваги: не треба думати про розподіл пам’яті, як в RTOS, і відносно легко контролювати наявність вільних ресурсів на відміну від підходу «давайте тут загальмуємо за допомогою циклу з NOP». Архітектура програми в цьому випадку будується на мінімальному використанні переривань, усю периферію максимально на DMA, і далі три шляхи.
Шлях перший: трохи ассемблера. З часів (десь 2007–2009 рр.), коли використовував AVR8-AVR32:
Використання в програмі: la_c_jmp(i_jmp); // перед асинхронною ділянкою. Від даного місця будемо стрибати на мітку.
Таймер можна збільшувати в sys_tick. Усе було цікаво, але зоопарк проєктів і мікроконтролерів ріс, копирсатися в ассемблері кожного, а також забезпечувати сумісність із компілятором змусило шукати шляхи на C.
Шлях другий: вказівники на функції C (приблизно з 2010–2012 рр.).
Усе працює, але… Недолік даного підходу — жахлива читабельність коду. Тому довелося шукати далі.
Шлях третій: async.h. Посилання на рішення https://github.com/naasking/async.h. Це красива комбінація вказівників на функцію та переключення на звичайному switch().
Виглядає і працює елегантно:
Недоліки:
- Час життя змінних компілятор може пропустити.
- Змінних як правило вимагається більше.
- Не працює всередині switch() {case …}.
Переваги:
- Красивий читабельний код.
- Повністю сумісне з C.
- Не треба занурюватися в роботу компілятора.
- Точно контролюємо, куди йде ресурс і скільки ще залишилося часу/тактів на обчислення.
За власним досвідом, async.h — це хороше рішення, коли треба скрестити швидкі процеси типу керування електромагнітними процесами перетворювача і повільні типу інтерфейсів.
Коментарі