Конечный автомат (он же машина состояний) на чистом С

Почти каждый микроконтроллерщик сталкивался с громадными switch-case и мучительно их отлаживал.
И много кто, начиная писать реализацию какого-либо протокола, задумывался как написать её красиво, изящно, так чтобы через месяц было понятно что ты имел в виду, чтобы она не отжирала всю память и вообще какала бабочками.
И вот тут на помощь приходят машины состояний, они же конечные автоматы (те самые которые используются в регулярных выражениях).

Собственно через регулярные выражения я к ним и пришёл.

Недавно мне потребовалось реализовать несложную функциональность устройства с 4-мя кнопками — выделение в потенциально хаотичных нажатиях кнопок двух кодовых последовательностей и выполнение определённых действий, когда последовательность найдена.

Поясню на примере: есть 4 кнопки — A, B, C, D и кодовая последовательность ACDA. Тогда при последовательном нажатии B, C, A, D, A, C, D, A, B, устройство должно зажечь лампочку после 8-го (предпоследнего) нажатия. С точки зрения регулярных выражений задача проста — ищем во входной строке подстроку «ACDA». Как только нашли — зажигаем лампочку. Но тащить в микроконтроллер какую-нибудь библиотеку по работе с regexp не хотелось. Я чувствовал что всё можно сделать самому и проще. Тогда я решил попробовать самостоятельно реализовать конечный автомат соответствующий поиску данной подпоследовательности.

А теперь — немного теории и практики:

Основная черта конечных автоматов — они описываются набором возможных состояний, набором сигналов (событий) и таблицей переходов. Таблица переходов — это сопоставление паре из текущего состояния и пришедшего сигнала нового состояния.

Представим, что у нас есть автомат с 3 состояниями и 3 сигналами, которые нужно обрабатывать. По приходу каждого сигнала автомат совершает переходит в новое состояние.
Наиболее интуитивный, но громоздкий код для подобной задачи:
enum states
{
    initial = 0,
    state_1,
    state_final
};

enum signals
{
    sign0,
    sign1,
    sign_N
};

enum signals getSignal();

void doFSM()
{
    enum states current_state = initial;
    while (1)
    {
        enum signals current_signal = getSignal();
        switch (current_state)
        {
            case initial:
                switch (current_signal)
                {
                    case sign0:
                        current_state = state_1;
                        break;
                    case sign1:
                        current_state = initial;
                        break;
                    case signN:
                        current_state = state_1;
                        break;
                }
                break;
            case state_1:
                switch (current_signal)
                {
                    case sign0:
                        current_state = initial;
                        break;
                    case sign1:
                        current_state = state_1;
                        break;
                    case signN:
                        current_state = state_final;
                        break;
                }
                break;
            case state_final:
                switch (current_signal)
                {
                    case sign0:
                        current_state = initial;
                        break;
                    case sign1:
                        current_state = initial;
                        break;
                    case signN:
                        current_state = initial;
                        break;
                }
                break;
        }
    }
}



Очевидно что данный код не очень удобочитаем и будет катастрофически разрастаться с увеличением количества состояний и сигналов.
Кроме того, если мы захотим в каждый переход добавить какое-либо однотипное действие (например логирование — что-то вроде
printf("Current = %d, signal = %d\n", current_state, current_signal);
), то это породит кучу изменений в коде. При редактировании таких изменений почти наверняка будет допущена какая-нибудь ошибка и отладка превратится в ад.

Заметим, что суть двух вложенных switch — это выбор из таблицы (по колонке и по столбцу) и вспомним что формально конечные автоматы удобно записывать в виде таблицы переходов где каждая строка соответствует сигналу, каждый столбец — состоянию, а на пересечении записано состояние в которое переходит автомат.

Попробуем упростить имеющийся код, задав таблицу переходов, извините за тафтологию, таблицей.

enum states FSM_table[3][3] = {
    [initial][sign0] = state_1,
    [initial][sign1] = initial,
    [initial][sign_N] = state_1,
    [state_1][sign0] = initial,
    [state_1][sign1] = state_1,
    [state_1][sign_N] = state_final,
    [state_final][sign0] = initial,
    [state_final][sign1] = initial,
    [state_final][sign_N] = initial
};

Продемонстрированый вид инициализации даннных известен как designated inits и был введён в С99.

теперь надо полученную структуру данных обработать — пишем функцию doFSM_table:
void doFSM_table()
{
    enum states current_state = initial;
    while (1)
    {
        enum signals current_signal = getSignal();
        current_state = FSM_table[current_state][current_signal];
    }
}

Код получился проще и понятнее, правда?
Теперь ещё несколько бонусов подобной записи:
  • нет ничего проще, чем добавить отладочный вывод к каждому шагу автомата:
    doFSM_table с отладочным выводом
    void doFSM_table()
    {
        enum states current_state = initial;
        while (1)
        {
            enum signals current_signal = getSignal();
            enum states new_state = FSM_table[current_state][current_signal];
            printf("Current = %d, signal = %d, new = %d\n", current_state, current_signal, new_state);
            current_state = new_state;
        }
    }
    


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


Чтобы сделать получившийся автомат ещё более универсальным и придать ему возможность совершать какие-то действия кроме перехода по состояниям добавим в таблицу указатели на функции-обработчики, соответствующие переходам:
Автомат с произвольными действиями на каждом переходе
typedef void (*transition_callback)(enum states state, enum signals signal);

struct transition
{
    enum states new_state;
    transition_callback worker;
};

void initial_1_fxn(enum states state, enum signals signal);
void initial_1N_fxn(enum states state, enum signals signal);
void fxn2(enum states state, enum signals signal);
void fxn3(enum states state, enum signals signal);
void to_initial_fxn(enum states state, enum signals signal);

struct transition FSM_table[3][3] = {
    [initial][sign0] = {state_1, initial_1_fxn},
    [initial][sign1] = {initial, NULL},
    [initial][sign_N] = {state_1, initial_1N_fxn},
    [state_1][sign0] = {initial, fxn2},
    [state_1][sign1] = {state_1, fxn3},
    [state_1][sign_N] = {state_final, NULL},
    [state_final][sign0] = {initial, to_initial_fxn},
    [state_final][sign1] = {initial, to_initial_fxn},
    [state_final][sign_N] = {initial, to_initial_fxn}
};

void doFSM_table()
{
    enum states current_state = initial;
    while (1)
    {
        enum signals current_signal = getSignal();
        enum states new_state = FSM_table[current_state][current_signal].new_state;
        transition_callback worker = FSM_table[current_state][current_signal].worker;
        if (worker != NULL)
        {
            worker(current_state, current_signal);
        }
        current_state = new_state;
    }
}


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

В заключение приведу таблицу, получившуюся для задача поиска подпоследовательности, с которой я начинал:
Получившаяся таблица состояний:
enum states FSM_table[9][4] = {
    [initial ... sACDA][bA ... bD] = initial, //для всех сигналов кроме описанных ниже возвращаемся в исходное состояние.
    [initial][bA] = sA, //распознали начало последовательности
    
    //ветка для последовательности ABADC
    [sA][bB] = sAB,
    [sAB][bA] = sABA,
    [sABA][bD] = sABAD,
    [sABAD][bC] = sABADC,
    
    //ветка для последовательности ACDA
    [sA][bC] = sAC,
    [sAC][bD] = sACD,
    [sACD][bA] = sACDA
};

Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 48

    +7
    С точки зрения регулярных выражений задача проста — ищем во входной строке подстроку «ACDA».
    Если ваша задача звучит именно таким образом, то не очень понятно, зачем вам здесь регулярные выражения или собственная реализация конечного автомата. Задача поиска подстроки в строке является классической, известно множество алгоритмов её решения, в вашем случае, если строка-образец действительно такая короткая, подойдет тривиальный алгоритм, работающий за O(nm), где n — длина строки, в которой ищем, а m — длина образца.
      0
      Вообще-то — да. Регулярные выражения здесь вообще overkill.
      А вот с поиском подстроки в строке есть несколько нюансов:
      Так как нельзя дождаться окончания строки, а надо реагировать на каждый символ, то приходится хранить где-то уже пришедшую строку.
      Если образцов несколько — то надо при приходе каждого символа сравнивать получившуюся строку с каждым из образцов.
      Если экономить память — то нужно хранить последние m символов в кольцевом буфере и писать собственную реализацию алгоритма поиска подстроки в строке работающую не на подряд идущей строке, а на буфере. Если не писать свою реализацию — надо при приходе каждого символа накопленную строку из кольцевого буфера копировать в линейный.
      В принципе всё это решаемо, но по итогам я подумал что собственный ДКА будет компактнее.
        +1
        Если экономить память — то нужно хранить последние m символов в кольцевом буфере
        Не обязательно, можно банально каждый раз сдвигать символы на 1 элемент влево. Вообще, если дело обстоит так, как вы описали выше, и у вас всего 4 возможных символа, то любую строку длины 4 можно представить просто числом от 0 до 15 и сравнение строк свести к сравнению двух чисел.
        В принципе всё это решаемо, но по итогам я подумал что собственный ДКА будет компактнее.
        Мне кажется, тут вы ошиблись, тривиальный алгоритм займет меньше строк, чем одно лишь описание автомата для вашей строки.

        В чем минус вашего решения на мой взгляд? Представим себе, что мы захотели поменять шаблонную строку или добавить новую. В этом случае мы не можем просто где-то заменить константу, а вынуждены будем руками реализовывать конечный автомат для нашей новой строки. Дело это не такое уж и тривиальное, как кажется, представим, что у нас строка вида "ABAC" и мы находимся в состоянии «встречено первые 3 символа». Тогда для символа A мы должны перейти в состояние «встречен 1 символ», для B — «встречено 2 символа», для С — «найдена вся строка», для D — «ни один символ не встречен». Если хотите, можно, увеличив длину строки, построить ещё более сложный пример.
          +1
          Не обязательно, можно банально каждый раз сдвигать символы на 1 элемент влево. Вообще, если дело обстоит так, как вы описали выше, и у вас всего 4 возможных символа, то любую строку длины 4 можно представить просто числом от 0 до 15 и сравнение строк свести к сравнению двух чисел.
          Это уже посоветовали в комментариях ниже.

          Про сложность потенциальную сложность автомата — да, наверное вы правы. Надо совершенствоваться в умении не строить Hammer, когда достаточно телеги.
            +1
            Регулярки и автоматы хороши, когда вам требуется найти все подстроки, принадлежащие к некоторому классу (например, выделить из текста все даты в каком-то формате). Для вашей же задачи и в более общем случае (когда длина шаблона может быть большой) существуют более простые и при этом не менее эффективные алгоритмы, например алгоритм Кнута-Морриса-Пратта
          0
          Нет ничего такого что регулярные выражения не могли бы сделать хуже.
            0
            Мне кажется, что это справедливо для любого инструмента.
            В то же время есть много таких областей где без регулярных выражений гораздо тяжелее, чем с ними.
            Как и для любого другого хорошего инструмента.
        0
        Как раз думал над унификацией разбросанных по проектам частных решений с автоматами, и ваша статья как нельзя кстати. И да, как раз размышлял, как бы универсально обрабатывать переходы по эвентам. Двумерный массив переходов — оригинально :)
        К сожалению, не могу пока голосовать, но плюсик в уме
        Ещё, я бы таблицу размещал как const, в флеш-памяти. Экономия ОЗУ и надежность.
          +1
          И ещё кое-что.
          Мне больше нравится оформление в виде
          BEGIN_STATE_MASHINE(SM_Name)
              ADD_BRANCH(OldState, Event, NewState, CallBack)
          END_STATE_MASHINE(SM_Name)
          
            +1
            *MACHINE *Callback
              0
              Дальнейшее оформление — дело вкуса.
              В принципе лучше в функцию doFSM выделять отдельный шаг автомата, а не весь цикл. Тогда можно делать «вложенные» машины состояний, когда шаг одной вызывает событие в другой и т.д.
              А ещё можно к таблице переходов прикрутить имя автомата и текущее состояние и передавать эту структуру в doFSM. Тогда можно обойтись вообще одной функцией на все автоматы и несколькими структурами с таблицами переходов. Но я решил не перегружать статью, а то можно бесконечно писать.
              0
              Можно объявлять как const. Тогда уж сразу же надо заставить компилятор выделать под enum не int, а минимально необходимое число байт.
              Но тогда нельзя (сложно) будет переконфигурировать «на лету», т.е. в моём случае, менять кодовые последовательности. Это не всегда нужно, но иногда приятно — например при создании перепрограммируемого кодового замка.
                0
                Двумерный массив переходов — оригинально :)

                Извините, без подкола, — а как еще можно представить-то?
                  0
                  Можно (но не нужно) представить линейным массивом структур из {old_state, signal, new_state, callback}. Если его правильно отсортировать он окажется двумерным. Если не сортировать — поиск нужного перехода из O(1) станет O(n) и будет вызывать боль (кроме случая когда мы экономим память, а таблица разреженная).
                  здесь должен был быть монолог в пользу map<pair<state,event>, pair<state, callback>>, но я не люблю С++ в микроконтроллерах
                    0
                    А, понятно, спасибо.
                    Насколько ж по-разному головы работают… Мне бы в голову ничего, кроме двухмерного массива не пришло бы.
                      +1
                      Другой, гораздо более типичный способ — представлять в виде функции, вычисляющей новое состояние (это может быть как единая функция для всех состояний, так и по одной штуке для каждого состояния). Пример есть в статье под катом «Наиболее интуитивный, но громоздкий код для подобной задачи». В случае, когда у вас алфавит достаточно велик (а не 4 символа, как у автора), способы, честно хранящие новое состояние для каждой пары <исходное состояние, символ>, дают слишком большой memory overhead.
                  0
                  Если вы думаете над унификацией по проектам с коллективной разработкой, имеет смысл рассмотреть устоявшийся хорошо документированный фреймворк — про один из таких есть, на мой взгляд, толковая книга автора по имени Miro Samek «Practical UML Statecharts in C/C++, Second Editiion. Event-Driven Programming for Embedded Systems». Собственно, очевидное преимущество описываемого там фреймворка (автор назвал его QP, Quantum leaPs) — сам факт существования книги :) (я первое издание читал в бумажной копии, теперь имею pdf второго издания, прочту, когда реально понадобится).

                  (заглянул на сайт автора — у него, оказывается, графический инструмент ещё появился для моделирования, ещё один плюс...)
                  +6
                  По-русски — не «машина состояний», а «конечный автомат». Заголовок глаза режет.
                    0
                    Поправил заголовок.
                    –2
                    «Устройство с четырьмя кнопками» да на чистом С — это не Pebble случайно? ) Очень похоже!
                      +2
                      Полезная статейка, возьму на заметку.
                      Если у в вашем случае нужно проверять строку из четырех символов (байт) то реализацию можно сильно упростить:
                      current_signal = getSignal();
                      current_state = (current_state&0xffffffff << 8) + current_signal;
                      	if (current_signal==valid_state)
                      		// установлено нужное состояние
                      
                      
                        +3
                        Можно, но (при моём руководстве) боязно делать настолько узкозаточенные алгоритмы — а вдруг попросят сделать последовательность из 7 кнопок? И не говорите мне что если кнопок всего 4, то нужно 2 бита на сигнал и в Uint32 влезет последовательность из 16 кнопок.
                        Хотя идея тоже красивая в своей оптимизированности.
                        +2
                        Хотелось бы порекомендовать автору ragel: en.wikipedia.org/wiki/Ragel — генератор конечных автоматов на чистом C. Там, где борьба идет за каждый лишний байт кода, возможно, и не подойдет, но в общем его вывод весьма хорош (-G2).
                          0
                          Спасибо, покурю. Раньше пользовался только flex — но мне кажется что он слишком заточен на работу именно с текстом.
                            +1
                            Присоединяюсь к рекомендации Ragel. Хочу особо выделить некоторые фичи
                            • генерация быстрого Си кода (можно сгенерировать fsm целиком на goto)
                            • можно писать как в стиле регулярных выражений, так и писать явный fsm. Можно мешать то и другое.
                            • на состояния и переходы можно повесить произвольные действия, код подставляется inline.
                            +2
                            Для тех, кто испытывал чувство дежавю во время прочтения: описанное очень близко к алгоритму Кнута — Морриса — Пратта (жалко на Википедии нет примера ДКА, для полноты аналогии).
                              0
                              В указанном Вами алгоритме самая интересная часть, по моему мнению, это именно построение таблицы переходов, а не описание общих принципов работы.
                                +1
                                Если расстатривать алгоритм отдельно, то да. А если рассматривать решаемую задачу и алгоритм, то он прямо «просится» сюда. Во-первых, обработка данных при их поступлении без использования буфера. Во-вторых, заранее известный шаблон, идеальный случай для данного алгоритма.

                                После фразы про строки мысли идут в сторону соответствующих алгоритмов, дальше требования отсекают остальные, а потом в тексте описывается автомат в таком виде, какой был бы построен КМП. Если бы это было целью автора, то вышел бы отличный пример использования алгоритма.
                              0
                              Я вот так всегда делаю:
                              github.com/Traumflug/Teacup_Firmware/blob/master/gcode_parse.c
                              Т.е. если нужна последовательность «ACDA», то алгоритм такой: читаем символ, если он А и стейт «начало», идем в стейт «ждем С». Читаем символ, если он С — идем в стейт «ждем D», иначе откатываемся в начало. В случае приведенного примера не очень-то большой код получается, а вот с ростом количества сигналов и состояний можно немаленький огород вырастить.

                              Вроде чувствую, что в топике что-то интересное описано, а понять код не могу :-(
                                0
                                Так топик как раз об этом — как удобнее расположить грядки в огороде, чтобы в нём не запутаться.
                                С описанного вами подхода я как раз начинал — смотрите первый приведённый мною кусок кода «Наиболее интуитивный, но громоздкий код для подобной задачи».
                                А дальше я рассказываю как его сделать компактнее.
                                ОФФТОП: читая gcode_parse.c, на который вы ссылаетесь, задался вопросом: мне кажется или
                                if (DEBUG_ECHO && (debug_flags & DEBUG_ECHO))
                                полностью эквивалентна более компактной
                                if (debug_flags & DEBUG_ECHO)
                                ?
                                Казалось бы что если (debug_flags & DEBUG_ECHO), т.е. (debug_flags & DEBUG_ECHO) != 0, то и DEBUG_ECHO != 0.
                                  0
                                  Да, что-то странное с этим. Если бы это был питон, то можно было бы сказать, что это для ускорения :-)
                                  0
                                  читаем символ, если он А и стейт «начало», идем в стейт «ждем С». Читаем символ, если он С — идем в стейт «ждем D», иначе откатываемся в начало

                                  Ну так, блин, это и есть конечный автомат. Автор описал универсальное, короткое решение.
                                  Таблица легко строится по графу, методы минимизации автомата известны. Соответственно, решение топикстартера лекго поменять. А вот править стену кода…
                                    +1
                                    иначе откатываемся в начало

                                    Тут нюанс: если искомая строка имеет префикс, входящий в строку не только в её начале (например, букву А в ABADC или AB в строке ABABC), то в некоторых ситуациях нужно откатываться не в начало. Например, если искомая строка ABABC, и на входе имеем ABABD, то получив букву D нужно из стейта «ждём букву №5» откатываться не в начало, а в стейт «ждём бувку №3».

                                    И в общем виде, для произвольной искомой строки эта задача не решается таким простым алгоритмом, как вы предлагаете. Нужно построить префикс-функцию, как это делает алгоритм Кнута-Морриса-Пратта, и проще всего она выражается как раз таблицей переходов конечного автомата.
                                    +2
                                    В последнем примере автомата для последовательности ABADC забыли переход:
                                    [sABA][dB] = sAB

                                    Без него он не среагирует на входную строку ABABADC
                                      +1
                                      Ваша правда. Поэтому для чистого поиска строк лучше использовать КМП, а не городить огород, который я развёл. О чём уже указывалось в комментариях выше
                                      0
                                      Очень полезная статья. Помню, как я замучился с этим FSM, когда проходил курс «Embedded Systems — Shape the World» на edX. И скажу, что в плане синтеза, составления графа переходов и составление самой таблицы — тема не раскрыта (но это и не было целью статьи, как я понял).
                                      Кстати, курс стартует вновь — www.edx.org/course/utaustinx/utaustinx-ut-6-02x-embedded-systems-4806
                                        0
                                        А что у него с производительностью по сравнению с первым, громоздким вариантом. У меня парсинг HTTP-запросов делается через свич на несколько сотен строк. Предполагаю, что использование таблицы переходов плюс вызов отдельной функции на каждый переход должно сказаться на производительности, вопрос в том на сколько скажется.
                                          +1
                                          Это очень зависит от компилятора. Если компилятор развернёт switch в таблицу переходов по адресам — будет примерно так же по времени.
                                          Если switch будет развёрнут как последовательность if-else — то мой вариант будет быстрее, т.к. любой переход будет выполняться за фиксированное время, независимо зависимости от расположения в таблице. А у switch-case придётся выверять последовательность case, так чтобы наиболее частые шли первыми.
                                          Помню как у меня замена
                                          switch (argument)
                                          {
                                          case 0x10: ... break;
                                          case 0x20: ... break;
                                          case 0x50: ... break;
                                          default: ...
                                          }
                                          


                                          на
                                          switch (argument >> 4)
                                          {
                                          case 0x1: ... break;
                                          case 0x2: ... break;
                                          case 0x5: ... break;
                                          default: ...
                                          }
                                          

                                          дало очень существенный прирост.

                                          Про вызов функции — надо смотреть на соотношение трудоёмкости пролога/эпилога с обработкой switch-case. А ещё принимать во внимание как всё это будет кэшироваться. Вообщем без прямых тестов сказать невозможно.
                                          Вообще для оптиматльной производительности, с шансами, подойдёт как раз упомянутый выше Ragel с генерацией кода на goto.
                                            0
                                            Да, я понимаю, что правильный switch будет развёрнут в таблицу переходов и что вызов функции на современных процессорах не очень затратная задача, но это всё теория, потому и спросил, по тестам производительности. Вообще конечно интересная задача, надо будет потестировать.
                                          0
                                          Когда я писал КА на C++ я использовал немного другой подход. Я делал примерно так:

                                          ...
                                          int start();
                                          int state1();
                                          int stateN();
                                          ...
                                          parse = &TParser::start;
                                          ...
                                          while( getByte() != END){
                                                 if((this->*parse)() < 0)
                                                        break;
                                          }
                                          


                                          Для Си будет тоже самое, только вместо методов — функции
                                          Каждая из функций уже меняла состояние. Интересно сравнить такой подход с таблицей состояний по скорости.
                                            +2
                                            Подход тоже имеет право на жизнь, но:
                                            1. В случае если требуется только поменять состояние получается куцая функция перехода
                                            2. Непрозрачно (по крайней мере в вашем примере) соответствие графического представления КА с исходным текстом. Я ведь правильно понимаю, что под функцией state1() скрывается парсер входных данных и все переходы из состояния 1? Тогда сложно эти переходы наглядно увидеть в одном месте кода
                                            3. в случае добавления ещё одного состояния придётся менять очень много кода разбросанного по файлу — неудобно будет читать diff в системе контроля версий
                                            4. Если в рамках отладки захотелось добавить текстовый лог вида «Начальное состояние -> сигнал -> конечное состояние» то его придётся добавлять в каждую функцию, потому что нет момента выполнения где доступны все три значения одновременно.

                                            5. Про скорость ничего сказать не могу.
                                            –3
                                            Я вот никака не могу понять код :-( Объясни, плз:

                                            enum signals current_signal = getSignal();
                                            Я правильно понимаю, что enum signals — это тип? Т.е. можно сделать typedef enum signals signal_t;
                                            А где функция getSignal? Или это просто некая не относящаяся к посту функция, которая ловит сигнал… АААААААА
                                            ок

                                            enum states new_state = FSM_table[current_state][current_signal];

                                            Блин, понял. В цикле ловим сигнал и меняем стейт по таблице

                                            А чо, класс! Два дня всего отняло понимание :-) Супер пост!
                                              0
                                              Нет, для пяти состояний и четырех сигналов табличка получается достаточно компактная, не спорю. Но…

                                              Через какое-то время апологеты табличного задания КА сталкиваются с ситуацией сложных сигналов. Типа такого — если нажата кнопка 1 и температура меньше T1 и двигатель включен, то выключить двигатель. А если двигатель выключен, то зажечь лампу. И начинают выбирать — или плодить сигналы по комбинации условий, или добавлять три-пять промежуточных состояний. Опа, а табличка раздулась, состояний увеличилось и разбираться с ними не менее сложно, чем с ветвистым switch.

                                              И в этом плане мне гораздо удобнее читать

                                              if (blah-blah-blah) {// коммент
                                                  GOTO_STATE(POWER_OFF);
                                              } else if (nah-nah-nah) {// коммент
                                                  GOTO_STATE(POWER_ON);
                                              }
                                              
                                                0
                                                Это смотря что в качестве входного алфавита выбирать. Раздутая таблица — не проблема, есть способы поиска совместных состояний.
                                                  0
                                                  ну так и возвращаемся к теме — добавить левых символов, которые используются только в одном состоянии (а такой комбинации условий может больше и не быть) или наплодить состояний, оптимизировать автомат и потом пытаться понять, что же там не взлетело. И это только для того, чтобы сократить главный цикл до двух строк?

                                                  И вдогонку — вывод отладки во всех состояниях конечно делается легко, красиво и одной строкой, а если надо вывод сделать в половине состояний? А в другой половине состояний писать в UART?
                                                    0
                                                    или наплодить состояний, оптимизировать автомат и потом пытаться понять, что же там не взлетело

                                                    Зачем? Вполне по силам написать утилиту, которая поправит таблицу и проведет, если надо оптимизацию.
                                                    В конце концов, системы типа parser builder'а как раз по БНФ строят таблицы. Тут — аналогично.

                                                    а если надо вывод сделать в половине состояний? А в другой половине состояний писать в UART?

                                                    Вполне реализуемо.
                                                      0
                                                      и вот так по шагам придем к SFC и успокоимся ))
                                                        0
                                                        Каждой задаче — свой инструмент.

                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                              Самое читаемое