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

Асинхронный двигатель фирмы 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 — это хорошее решение когда надо скрестить быстрые процессы типа управления электромагнитными процессами преобразователя и медленные типа интерфейсов.

Комментарии (5)


  1. rukhi7
    13.09.2025 15:33

    Когда управляешь инвертором двигателя, real time измеряется 10-30мкс.

    Что же вы самое интересное пропустили? Откуда у вас взялись такие цифры 10-30мкс?

    Я правда делал управление коллекторным двигателем, там слот времени принятия решения определялся полупериодом сетевого напряжения (50 Гц, 220В) это

    1000 миллисекунд / 50Гц / 2 полупериода = 10 миллисекунд.

    Семистр открывается до следующего пересечения нуля сетевым напряжением. Но некоторые вещи тем не менее требуют почти абсолютной точности расчетов по времени, это правда, как говорится "это real time детка!" :). Причем проблема не только минимальное разрешение, есть еще проблема больших интервалов для которых должна поддерживаться точность, например однажды выставленные 10мс должны колебаться вокруг именно 10 мс, а не 9.9 мс(например) на протяжении потенциально бесконечного времени работы драйвера двигателя...

    В этом случае разработчики начинают использовать прерывания, и распихивать код по ним. После определенного количества прерываний, можно получить ситуацию, что одно будет мешать другому в непредсказуемых комбинациях которые отладить бывает очень трудно.

    Эта задача решается не в языках программирования! В языках программирования нет такого понятия: "прерывания", ни в одном! Надо построить схему или диаграмму прерываний - то есть надо придумать как(!) конкретную схему нарисовать, для определенного набора прерываний и необходимого времени на их обработку так, чтобы они не накладывались непредсказуемым образом, а в коде надо просто контролировать что схема работает, то есть не происходит не продуманных наложений. Это проверяется на этапе отладки-тестирования, когда схема проверена это превращается в простую математическую задачу уровня 5-7 класса. А вот придумать как грамотно изобразить-начертить схему и как ее проверить - это действительно сложно.

    Если интересно я могу подробно, с примерами, рассказать как создавать такие, поддающиеся проверке, схемы прерываний. Я думаю на эту тему можно даже написать диссертацию, если руководитель найдется, который не постесняется руководить изучением такого нестандартного вопроса в нашей научной традиции.


  1. kovserg
    13.09.2025 15:33

    // Задержка в мс
    async me_delay(struct async *pt, uint32_t ticks)
    {
        async_begin(pt);
        static uint32_t timer_delay; // <<<--- если забыть static будут прикольно
        timer_delay = timer + ticks;
        while(!LoopCmp(timer, timer_delay)) {
    ...
    

    У себя использовал похожую схему. Я использовал - "постепенно исполняемые функции" (afn - async fn). Вообще любая такая функция просто набор 3х указателей: 1-состояние асинхронной функции (ctx), 2-обработчик сигналов (setup), 3-обработчик итерации (loop)

    afn.h
    typedef struct afn_t {
      void *ctx;
      int (*setup)(void *ctx,int sev);
      int (*loop)(void *ctx);
    } afn_t;
    
    enum SetupEvents { sevInit, sevDone };
    

    Если функция loop вернула 0 то она закончила, если 1-еще рабоает, остальные коды прерывания можно использовать для запросов. Функция setup тоже 0=ok. Фунция гарантируется что если вызвали setup(sevInit) то обязательно будет вызван setup(sevDone). И то что функция loop быполняет итерацию за конечное время. И самое главное предача кванта управления выполняется явно.

    А вот для реализации итераций в обработчике loop была такая же схема как и тут на switch-е. Но не на LINE а на COUNTER. И все переменные которые использует асинхронная функция она явно описывает в своём состоянии. Примерно так:

    example.c
    /* example.c */
    #include <stdio.h>
    #include "afn.h"
    #include "loop-fn.h"
    
    typedef struct fn1_ctx {
    	loop_t loop;
    	int i;
    } fn1_ctx;
    
    int fn1_setup(fn1_ctx *my,int sev) {
    	if (sev==sevInit) LOOP_RESET(my->loop);
    	return 0;
    }
    int fn1_loop(fn1_ctx *my) {
    	LOOP_BEGIN(my->loop)
    	printf(" fn1.begin");
    	LOOP_POINT
    	printf(" fn1.step1");
    	LOOP_POINT
    	for(my->i=0;my->i<3;my->i++) {
    		printf(" fn1.i=%d",my->i);
    		LOOP_POINT
    	}
    	printf(" fn1.end");
    	LOOP_END
    }
    
    int main(int argc,char** argv) {
    	fn1_ctx fn1[1]; int it,rc;
    	fn1_setup(fn1,sevInit);
    	for(it=0;it<10;it++) {
    		printf("it=%2d fn1=%2d",it,fn1->loop);
    		rc=fn1_loop(fn1);
    		if (!rc) printf(" [fn1 is done]");
    		printf("\n");
    		if (!rc) break;
    	}
    	fn1_setup(fn1,sevDone);
    	return 0;
    }
    

    output:

    it= 0 fn1= 0 fn1.begin
    it= 1 fn1= 1 fn1.step1
    it= 2 fn1= 2 fn1.i=0
    it= 3 fn1= 3 fn1.i=1
    it= 4 fn1= 3 fn1.i=2
    it= 5 fn1= 3 fn1.end [fn1 is done]
    
    loop-fn.h
    /* loop-fn.h - sequential execution function */
    #ifndef __LOOP_FN_H__
    #define __LOOP_FN_H__
    
    typedef int loop_t;
    
    #define LOOP_RESET(loop) { loop=0; }
    #if defined(__COUNTER__) && __COUNTER__!=__COUNTER__
    #define LOOP_BEGIN(loop) { enum { __loop_base=__COUNTER__ }; \
        loop_t *__loop=&(loop); __loop_switch: int __loop_rv=1; \
        switch(*__loop) { default: *__loop=0; case 0: {
    #define LOOP_POINT { enum { __loop_case=__COUNTER__-__loop_base }; \
        *__loop=__loop_case; goto __loop_leave; case __loop_case:{} }
    #else
    #define LOOP_BEGIN(loop) {loop_t*__loop=&(loop);__loop_switch:int __loop_rv=1;\
        switch(*__loop){ default: case 0: *__loop=__LINE__; case __LINE__:{
    #define LOOP_POINT { *__loop=__LINE__; goto __loop_leave; case __LINE__:{} }
    #endif
    #define LOOP_END { __loop_end: *__loop=-1; case -1: return 0; \
    	{ goto __loop_end; goto __loop_switch; } } \
    	}} __loop_leave: return __loop_rv; }
    #define LOOP_SET_RV(rv) { __loop_rv=(rv); } /* rv must be non zero */
    #define LOOP_INT(n) { __loop_rv=(n); LOOP_POINT } /* interrupt n */
    #define LOOP_POINT_(name) { *__loop=name; goto __loop_leave; case name:{} }
    #define LOOP_INT_(n,name) { __loop_rv=(n); LOOP_POINT_(name) }
    
    #endif /* __LOOP_FN_H__ */
    

    Такие функции могут итерироваться супервизорорами. Они очень удобно отслеживают и реагируют на аварийные ситуации и итерируют подопечную функцию. А для выполнения множеста таких функций используются планировщики. Они довольно разнообразны и это целая отдельная тема.


  1. LexaK
    13.09.2025 15:33

    Мне одному это сильно напоминает Protothreads от Адама нашего Дакнкелса?


  1. EmCreatore
    13.09.2025 15:33

    Когда управляешь инвертором двигателя, real time измеряется 10-30мкс. 

    Хорошо живете, столько времени иметь в запасе

    Однако чтобы прицизионно контроллером измерить напряжения и токи во всех фазах и еще внутренности разные измерить, делая это синхронно с шимом, то останется всего пара микросекунд. Это половинное время минимального пульса ШИМ.
    Потому что уложиться надо от середины пульса и пока не возникнет фронт или спад на какой либо фазе. На это, кстати, указывается во всех мануалах по управлению двигателями на однокристальных SoC.
    Поэтому всяческие коооперативные переключатели, как в этой статье, никак уже не подходят.
    Приходится применять вложенные прерывания с приоритетностью, и обычную RTOS типа FreeRTOS.
    Ничего лучшего человечество пока не придумало.


  1. kapojko
    13.09.2025 15:33

    Интересный подход! По названию, я подумал, что вы будете говорить о реализации асинхронный логики путем использования аппаратных контроллеров периферии и памяти. Но тут настоящий python/js async :) Выглядит впечатляюще!

    Хотя я бы так не писал. Во многих задачах, где мне доводилось применять микроконтроллеры, нужно так или иначе реальное время - а том смысле, что время реакции системы на входные сигналы должно быть однозначно предсказуемо. Применение async этому препятствует - очень трудно однозначно сказать, будет в конкретном месте когда yield или не будет, и не получится ли, что при каком-то редком сочетании факторов время реакции будет неожиданно сильно большим, чем в остальных случаях.

    Ну и к preprocessor hell это уже очень близко. :) Напоминает труды Александреску по C++, очень красиво, но лучше не применять в реальной жизни)