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

Асинхронный двигатель фирмы AEG 1890-ых
Асинхронный двигатель фирмы AEG 1890-ых

И несправедливости, что возит, поит, греет творение М.О. Доливо-Добровольского, а поминают Н. Тесла. Причем АД второго, невероятно плох. Двухфазный (привет асимметрии линий электропередачи), концентрированная обмотка на полюсах (прощай КПД), ну и низкий пусковой момент. А вот Михаила Осиповича творение с первого включения показало КПД в районе 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 — это хорошее решение когда надо скрестить быстрые процессы типа управления электромагнитными процессами преобразователя и медленные типа интерфейсов.