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

И несправедливости, что возит, поит, греет творение М.О. Доливо-Добровольского, а поминают Н. Тесла. Причем АД второго, невероятно плох. Двухфазный (привет асимметрии линий электропередачи), концентрированная обмотка на полюсах (прощай КПД), ну и низкий пусковой момент. А вот Михаила Осиповича творение с первого включения показало КПД в районе 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)
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__ */
Такие функции могут итерироваться супервизорорами. Они очень удобно отслеживают и реагируют на аварийные ситуации и итерируют подопечную функцию. А для выполнения множеста таких функций используются планировщики. Они довольно разнообразны и это целая отдельная тема.
EmCreatore
13.09.2025 15:33Когда управляешь инвертором двигателя, real time измеряется 10-30мкс.
Хорошо живете, столько времени иметь в запасе
Однако чтобы прицизионно контроллером измерить напряжения и токи во всех фазах и еще внутренности разные измерить, делая это синхронно с шимом, то останется всего пара микросекунд. Это половинное время минимального пульса ШИМ.
Потому что уложиться надо от середины пульса и пока не возникнет фронт или спад на какой либо фазе. На это, кстати, указывается во всех мануалах по управлению двигателями на однокристальных SoC.
Поэтому всяческие коооперативные переключатели, как в этой статье, никак уже не подходят.
Приходится применять вложенные прерывания с приоритетностью, и обычную RTOS типа FreeRTOS.
Ничего лучшего человечество пока не придумало.
kapojko
13.09.2025 15:33Интересный подход! По названию, я подумал, что вы будете говорить о реализации асинхронный логики путем использования аппаратных контроллеров периферии и памяти. Но тут настоящий python/js async :) Выглядит впечатляюще!
Хотя я бы так не писал. Во многих задачах, где мне доводилось применять микроконтроллеры, нужно так или иначе реальное время - а том смысле, что время реакции системы на входные сигналы должно быть однозначно предсказуемо. Применение async этому препятствует - очень трудно однозначно сказать, будет в конкретном месте когда yield или не будет, и не получится ли, что при каком-то редком сочетании факторов время реакции будет неожиданно сильно большим, чем в остальных случаях.
Ну и к preprocessor hell это уже очень близко. :) Напоминает труды Александреску по C++, очень красиво, но лучше не применять в реальной жизни)
rukhi7
Что же вы самое интересное пропустили? Откуда у вас взялись такие цифры 10-30мкс?
Я правда делал управление коллекторным двигателем, там слот времени принятия решения определялся полупериодом сетевого напряжения (50 Гц, 220В) это
1000 миллисекунд / 50Гц / 2 полупериода = 10 миллисекунд.
Семистр открывается до следующего пересечения нуля сетевым напряжением. Но некоторые вещи тем не менее требуют почти абсолютной точности расчетов по времени, это правда, как говорится "это real time детка!" :). Причем проблема не только минимальное разрешение, есть еще проблема больших интервалов для которых должна поддерживаться точность, например однажды выставленные 10мс должны колебаться вокруг именно 10 мс, а не 9.9 мс(например) на протяжении потенциально бесконечного времени работы драйвера двигателя...
Эта задача решается не в языках программирования! В языках программирования нет такого понятия: "прерывания", ни в одном! Надо построить схему или диаграмму прерываний - то есть надо придумать как(!) конкретную схему нарисовать, для определенного набора прерываний и необходимого времени на их обработку так, чтобы они не накладывались непредсказуемым образом, а в коде надо просто контролировать что схема работает, то есть не происходит не продуманных наложений. Это проверяется на этапе отладки-тестирования, когда схема проверена это превращается в простую математическую задачу уровня 5-7 класса. А вот придумать как грамотно изобразить-начертить схему и как ее проверить - это действительно сложно.
Если интересно я могу подробно, с примерами, рассказать как создавать такие, поддающиеся проверке, схемы прерываний. Я думаю на эту тему можно даже написать диссертацию, если руководитель найдется, который не постесняется руководить изучением такого нестандартного вопроса в нашей научной традиции.