Как у специалиста по силовой электроники и электроприводу, ассоциации с «асинхронный» вот такие:

И несправедливости, что возит, поит, греет творение М.О. Доливо-Добровольского, а поминают Н. Тесла. Причем АД второго, невероятно плох. Двухфазный (привет асимметрии линий электропередачи), концентрированная обмотка на полюсах (прощай КПД), ну и низкий пусковой момент. А вот Михаила Осиповича творение с первого включения показало КПД в районе 90 и двойной пусковой момент, что и стало стандартом де-факто с 1891г. и по сей день.
Когда управляешь инвертором двигателя, real time измеряется 10-30мкс. Это быстро, даже для современных микросхем. Архитектуру программы надо планировать без лишних пожирателей времени ядра. Однако есть входящие сигналы которые надо обрабатывать медленно. Еще есть интерфейсы, где есть коллизии и тайм ауты.
Приведу классический медленный пример — работа с GSM модемом. Этот черный ящик, требует заклинаний в виде AT команд, нужно дождаться ответа AT OK/ERROR, а ответ может и не прийти (таймаут 100мс), а иногда надо «пнуть» его еще раз, а бывает он вообще виснет и его перезагрузить надо по питанию.
Обычно там где медленные процессы или устройства, то разработчики склоняются к RTOS. Тут все понятно. Есть отдельные задачи, выполняемые параллельно с переключаемым контекстом и настроенным приоритетом. Недостаток, сложность использования, надо помнить про переключаемый контекст. Условно вызов printf в разных задачах может привести к непредсказуемым последствиям. Не забываем и про увеличивающийся размер кода.
Имеется еще один любимый подход программистов микроконтроллеров. А давайте мы сделаем примерно так:
send_at(«AT»);
delay(100);
switch(get_at_result()) …
```
Соль кроется в функции delay, которая является просто циклом с командами nop, или через таймер, тормозит программу в этом месте на нужное время. Подход как бы нормальный, за исключением. Используемой периферии всегда много в реальных проектах, и опрос 1-wire может длиться до нескольких секунд, а на запросы мастера Modbus, надо отвечать с минимальной задержкой. В этом случае разработчики начинают использовать прерывания, и распихивать код по ним. После определенного количества прерываний, можно получить ситуацию, что одно будет мешать другому в непредсказуемых комбинациях которые отладить бывает очень трудно. По закону Мерфи это начинается в продукте. Из собственного опыта: мусоровозка, дождь, вонь, масло от гидравлики, грязь, с дебагером и ноутом на коленках искать иголку в «мусорном кузове» грузовика. А у людей нервы и планы куда приехать надо. У заказчиков, сколько надо поставок сделать. У верхнего уровня вопросы: "А что у вас машина координаты GPS не шлёт?". Также появляются петли в условной printf у которой есть внутренние переменные, и её вызовом в основном цикле программы и в неком прерывании. Может хаотично упасть и на столе лаборатории, бывает комбинацию не повторить.
Эволюционно я пришел собственно к асинхронности. Общий принцип таков. Контекст не переключается, нужен delay или событие, проверь счетчик или наличие событие и jump на другой участок кода. Основной недостаток данного подхода — это контроль времени жизни переменных. C — это не делает, как например Rust, тоже можно выстрелить в ногу. Из плюсов, не надо думать про распределение памяти как в RTOS и относительно легко контролировать наличие свободных ресурсов в отличие давайте здесь тормознём с помощью цикла с NOP. Архитектура программы в этом случае строится на минимальном использовании прерываний, всю периферию максимально на DMA, и далее три пути.
Путь первый: немного ассемблера. Из времен (где то 2007-9г.), когда использовал AVR8-AVR32:
#define la_set(lab) {asm volatile(#lab ": "::); }
#if (AVR == AVR32)
#define la_save(var, lab) { asm volatile("lda.w %[VAL], "#lab " \n\t" \
" ":[VAL]"=r" (var): "[VAL]" (var)); }
#define la_save_set(var, lab) { asm volatile("lda.w %[VAL], "#lab " \n\t" \
" ":[VAL]"=r" (var): "[VAL]" (var)); \
asm volatile(#lab ": "::); }
#define la_jmp(var) { asm volatile("mov pc, %[VAL]":: [VAL]"r"(var)); } // lddpc
typedef struct la_str_jmp {
U32 *timer;
U32 jmp;
} la_str_jmp;
```
Использование в программе: la_c_jmp(i_jmp); // перед асинхронным участком. От данного места будем прыгать на метку.
switch(step) {
case : x
*i_jmp->timer = 0; // сбросим таймер delay
la_save(i_jmp->jmp, l_td_t3); // запоминаем куда перепрыгнуть
la_set(l_td_t3); // ставим куда перепрыгнуть
if (*i_jmp->timer < 500) {
// подождали и что то здесь делаем
}
break;
}
```
таймер можно увеличивать в sys_tick. Все было прикольно, но зоопарк проектов и микроконтроллеров рос, копаться в ассемблере каждого, а также дружить это с компилятором вынудило искать пути на С.
Путь второй: указатели на функции C (примерно с 2010-2012г).
U8 (*_ij)(void); // указатель на функцию
U8 (*_ij_next)(void); // указатель на функцию
U32 _ij_delay = 0;
U8 f_hard_delay(void) {
if (gsm_j_timer < _ij_delay) {
return 0;
}
gsm_j_timer = 0;
return 1;
}
U8 f_turnon(void) {
// ….
gsm_j_timer = 0; _ij_delay = 1500;
_ij_next = f_turnon1;
_ij = f_hard_delay;
return 0x0;
}
U8 f_turnon1(void) {
// ….
_ij_next = f_turnon2;
gsm_j_timer = 0; _ij_delay = 3000;
_ij = f_hard_delay;
return 0x0;
}
while(1) {
// переключатель
if (_ij != NULL) {
if( (_ij)() ) {
_ij = _ij_next;
_ij_next = NULL;
}
}
}
```
Все работает, но… Недостаток у данного подхода — ужасная читаемость кода. Поэтому пришлось искать дальше.
Путь третий async.h. Ссылка на решение https://github.com/naasking/async.h. Это красивая комбинация указателей на функцию и переключение на обычном switch().
Выглядит и работает элегантно:
static struct async pt_me;
// Задержка в мс
async me_delay(struct async *pt, uint32_t ticks)
{
async_begin(pt);
static uint32_t timer_delay;
timer_delay = timer + ticks;
while(!LoopCmp(timer, timer_delay)) {
async_yield;
}
/* And we loop. */
async_end;
}
async read_device(struct async *pt, uint8_t *dev)
{
async_begin(pt);
satatic uint8_t ret;
async_init(&pt_me);
await(wait_signal(&pt_me, *ret)); // подождать некий внешний сигнал
// что то сделать
async_init(&pt_me);
await(me_delay(&pt_me, 100)); // подождем …
*dev = ret;
async_end;
}
static struct async pt_main;
static struct async pt_process;
async app_main( struct async *pt) {
async_begin(pt);
while (1) {
if (!signal1) { // сигнал поступил
async_yield;
}
async_init(&pt_process);
await( read_device(&pt_process, &dev));
async_init(&pt_process);
uint8_t result;
await( send_at_command(&pt_process, «AT», &result));
if (result) {
// To do …
}
}
async_end;
}
void main(void) {
async_init(&pt_main);
while (1) {
app_main(&pt_main);
}
}
```
Недостатки
Время жизни переменных компилятор может пропустить.
Переменных как правило требуется больше.
Не работает внутри switch() {case …}.
Преимущества:
Красивый читаемый код.
Полностью С совместимое.
Не надо погружаться в работу компилятора
Точно контролируем куда уходит ресурс и сколько еще осталось времени/тактов на вычисления.
По собственному опыту, async.h — это хорошее решение когда надо скрестить быстрые процессы типа управления электромагнитными процессами преобразователя и медленные типа интерфейсов.