C State-Design

184

Я создаю небольшой проект в смешанных C и С++. Я строю одну маленькую государственную машину в основе одной из моих рабочих потоков.

Мне было интересно, будут ли вы, гуру на SO, делиться вашими методами проектирования государственной машины.

ПРИМЕЧАНИЕ. Я прежде всего пробовал и проверял методы внедрения.

ОБНОВЛЕНО: Основываясь на всех больших вводах, собранных на SO, я решил эту архитектуру:

alt text

  • 3
    Ответы здесь очень хорошие. Также посмотрите на этот повторяющийся вопрос, на который также есть несколько хороших ответов: stackoverflow.com/questions/1371460/state-machines-tutorials
  • 2
    Это также интересно: stackoverflow.com/questions/133214/…
Показать ещё 8 комментариев
Теги:
architecture
design
state-machines

24 ответа

161
Лучший ответ

Государственные машины, которые я разработал ранее (C, а не С++), все сводились к массиву struct и циклу. Структура в основном состоит из состояния и события (для поиска) и функции, которая возвращает новое состояние, например:

typedef struct {
    int st;
    int ev;
    int (*fn)(void);
} tTransition;

Затем вы определяете свои состояния и события с помощью простых определений (тегами ANY являются специальные маркеры, см. ниже):

#define ST_ANY              -1
#define ST_INIT              0
#define ST_ERROR             1
#define ST_TERM              2
: :
#define EV_ANY              -1
#define EV_KEYPRESS       5000
#define EV_MOUSEMOVE      5001

Затем вы определяете все функции, вызываемые переходами:

static int GotKey (void) { ... };
static int FsmError (void) { ... };

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

Использование глобальных переменных не так плохо, как кажется, поскольку FSM обычно заперт внутри одного блока компиляции, и все переменные являются статическими для этого устройства (именно поэтому я использовал кавычки вокруг "global" выше - они больше разделяемых в рамках FSM, чем по-настоящему глобальный). Как и во всех глобальных переменных, это требует осторожности.

Затем массив переходов определяет все возможные переходы и функции, вызываемые для этих переходов (включая последнюю последнюю):

tTransition trans[] = {
    { ST_INIT, EV_KEYPRESS, &GotKey},
    : :
    { ST_ANY, EV_ANY, &FsmError}
};
#define TRANS_COUNT (sizeof(trans)/sizeof(*trans))

Что это значит: если вы находитесь в состоянии ST_INIT и получаете событие EV_KEYPRESS, сделайте вызов GotKey.

Затем работа FSM становится относительно простой:

state = ST_INIT;
while (state != ST_TERM) {
    event = GetNextEvent();
    for (i = 0; i < TRANS_COUNT; i++) {
        if ((state == trans[i].st) || (ST_ANY == trans[i].st)) {
            if ((event == trans[i].ev) || (EV_ANY == trans[i].ev)) {
                state = (trans[i].fn)();
                break;
            }
        }
    }
}

Как упоминалось выше, обратите внимание на использование ST_ANY в качестве wild-cards, позволяя событию вызывать функцию независимо от текущего состояния. EV_ANY также работает аналогично, позволяя любому событию в конкретном состоянии вызывать функцию.

Это также может гарантировать, что, если вы достигнете конца массива переходов, вы получите сообщение об ошибке, когда ваш FSM не был построен правильно (используя комбинацию ST_ANY/EV_ANY.

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

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


И, как говорится в ldog в комментарии, вы можете вообще избегать глобальных переменных, передав указатель структуры ко всем функциям (и используя это в цикле событий). Это позволит нескольким конечным машинам работать бок о бок без помех.

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

Причина, по которой я редко делал это, - это просто потому, что большинство состояний, которые я написал, были однотипными (одноразовый, в процессе запуска, чтение файла конфигурации, например), не нужно запускать больше чем один экземпляр. Но это имеет значение, если вам нужно запустить более одного.

  • 0
    Это выглядит красиво и практично. Я бы ожидал, что вы будете использовать гигантский оператор switch вместо итерации массива переходов ... Я думаю, что вижу такое преимущество в этом, но я был бы признателен, если бы вы объяснили, почему вы делаете это вместо этого. гигантского выключателя. Кроме того, небольшой минус: все компиляторы, которые я использую в эти дни, поддерживают перечисления, поэтому я бы предпочел использовать перечисления для таких вещей, как состояния. Я всегда определяю первое и последнее перечисление, которые не являются допустимыми состояниями, поэтому я могу написать проверку диапазона следующим образом: Assert(stateInvalid < state && state < stateMax);
  • 24
    Гигантский переключатель смешивает код с FSM. Даже если в переходе есть только вызов функции, есть некоторый код, и кому-то легко злоупотребить этим, просто добавив небольшой 4-строчный переход в строке. курица из десяти строк. Тогда это выходит из-под контроля. С массивом struct FSM остается чистым - вы можете видеть каждый переход и эффект (функцию). И я начал, когда перечисления были в мгновение ока в глазах ISO, написание кода для 6809 встроенных платформ с компиляторами, которые, скажем так, были менее чем идеальными :-)
Показать ещё 13 комментариев
71

Другие ответы хороши, но очень "легкая" реализация, которую я использовал, когда машина состояния очень проста выглядит:

enum state { ST_NEW, ST_OPEN, ST_SHIFT, ST_END };

enum state current_state = ST_NEW;

while (current_state != ST_END)
{
    input = get_input();

    switch (current_state)
    {
        case ST_NEW:
        /* Do something with input and set current_state */
        break;

        case ST_OPEN:
        /* Do something different and set current_state */
        break;

        /* ... etc ... */
    }
}

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

29

Извините меня за нарушение всех правил в области компьютерных наук, но конечный автомат является одним из немногих (я могу считать только два раза), где оператор goto не только более эффективен, но и делает ваш код более чистым и легче читать. Поскольку операторы goto основаны на таблицах, вы можете назвать свои состояния вместо того, чтобы отслеживать беспорядок чисел или использовать перечисление. Это также делает намного более чистый код, так как вам не нужны все лишние круговые указатели функций или огромные операторы switch и while. Я уже упоминал об этом более эффективно?

Здесь может выглядеть пользовательский автомат:

void state_machine() {
first_state:
    // Do some stuff here
    switch(some_var) {
    case 0:
        goto first_state;
    case 1:
        goto second_state;
    default:
        return;
    }

second_state:
    // Do some stuff here
    switch(some_var) {
    case 0:
        goto first_state;
    case 1:
        goto second_state;
    default:
        return;
    }
}

Вы получаете общую идею. Дело в том, что вы можете эффективно использовать государственный автомат, а тот, который относительно легко читать, и кричит читателю, что он смотрит на конечный автомат. Обратите внимание: если вы используете операторы goto, вы все равно должны быть осторожны, так как очень легко стрелять себе в ногу, делая это.

  • 4
    Это работает, только если конечный автомат находится в объекте верхнего уровня. В тот момент, когда какой-то другой объект, который иногда опрашивается / отправляется сообщения, должен иметь состояние, вы застряли с этим подходом (или вам придется сделать его намного более сложным)
  • 1
    Это действительно заставляет вас использовать упреждающую многозадачность во всех случаях, кроме самых простых.
Показать ещё 3 комментария
29

Вы можете рассмотреть компилятор State Machine http://smc.sourceforge.net/

Эта великолепная утилита с открытым исходным кодом принимает описание конечного автомата на простом языке и компилирует его на любой из десятков языков, включая C и С++. Сама утилита написана на Java и может быть включена как часть сборки.

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

Извините, я испытываю чрезмерную энтузиазм и, несомненно, увольняю всех. Но это первоклассная утилита и хорошо документирована.

19

Обязательно проверьте работу Miro Samek (блог State Space, веб-сайт Государственные машины и инструменты), чьи статьи в журнале пользователей C/С++ были замечательными.

Веб-сайт содержит полную (C/С++) реализацию как с открытым исходным кодом, так и с коммерческой лицензией системы государственных машин (QP Framework), обработчика событий (QEP), базовый инструмент моделирования (QM) и инструмент трассировки (QSpy), который позволяет рисовать конечные машины, создавать код и отлаживать их.

Книга содержит обширное объяснение того, что/почему реализации и как ее использовать, а также является отличным материалом для понимания основ иерархических и конечных автоматов.

На веб-сайте также содержатся ссылки на несколько пакетов поддержки плат для использования программного обеспечения со встроенными платформами.

  • 0
    Я изменил название вопроса в соответствии с вашим каламбуром.
  • 0
    @jldupont: Я просто имел в виду, что было бы лучше уточнить. Я удалил ненужные части моего ответа сейчас.
Показать ещё 3 комментария
10

Я сделал что-то похожее на то, что описывает paxdiablo, только вместо массива переходов состояний/событий я настроил двумерный массив указателей на функции, значение события как индекс одной оси и текущего как другое. Тогда я просто звоню state = state_table[event][state](params), и правильная вещь случается. Ячейки, представляющие недействительные комбинации состояний/событий, получают указатель на функцию, которая это говорит, конечно.

Очевидно, что это работает только в том случае, если значения состояния и события являются смежными диапазонами и начинаются с 0 или достаточно близко.

  • 1
    Похоже, что это решение не очень хорошо масштабируется: слишком много заполнения стола, нет?
  • 2
    +1. Проблема масштабирования в данном случае связана с памятью. У моего собственного решения есть проблема масштабирования, т. Е. Время, затрачиваемое на сканирование таблицы переходов (хотя вы можете оптимизировать вручную для наиболее распространенных переходов). Этот жертвует памятью ради скорости - это просто компромисс. Вам, вероятно, понадобятся проверки на наличие границ, но это не плохое решение.
Показать ещё 3 комментария
8

Очень симпатичная основанная на шаблонах "фреймворк" состояния языка С++ дается Стефаном Хайнцманом в статье .

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

Основное нововведение заключается в том, что компилятор генерирует очень эффективный код. Пустые действия входа/выхода не требуют затрат. Непустые действия входа/выхода включены. Компилятор также проверяет полноту государственной диаграммы. Отсутствующие действия генерируют ошибки связывания. Единственное, что не удалось поймать, это отсутствие Top::init.

Это очень хорошая альтернатива реализации Miro Samek, если вы можете жить без того, что отсутствует - это далеко не полная реализация UML Statechart, хотя она правильно реализует семантику UML, тогда как код Samek по дизайну не обрабатывает выхода/перехода/ввода в правильном порядке.

Если этот код работает для того, что вам нужно сделать, и у вас есть достойный компилятор С++ для вашей системы, он, вероятно, будет работать лучше, чем реализация Miro C/С++. Компилятор генерирует для вас сжатую, O (1) переходную машину. Если проверка сборки сборок подтверждает, что оптимизация работает по желанию, вы приближаетесь к теоретической производительности. Лучшая часть: это относительно крошечный, понятный код.

#ifndef HSM_HPP
#define HSM_HPP

// This code is from:
// Yet Another Hierarchical State Machine
// by Stefan Heinzmann
// Overload issue 64 december 2004
// http://accu.org/index.php/journals/252

/* This is a basic implementation of UML Statecharts.
 * The key observation is that the machine can only
 * be in a leaf state at any given time. The composite
 * states are only traversed, never final.
 * Only the leaf states are ever instantiated. The composite
 * states are only mechanisms used to generate code. They are
 * never instantiated.
 */

// Helpers

// A gadget from Herb Sutter GotW #71 -- depends on SFINAE
template<class D, class B>
class IsDerivedFrom {
    class Yes { char a[1]; };
    class No  { char a[10]; };
    static Yes Test(B*); // undefined
    static No Test(...); // undefined
public:
    enum { Res = sizeof(Test(static_cast<D*>(0))) == sizeof(Yes) ? 1 : 0 };
};

template<bool> class Bool {};

// Top State, Composite State and Leaf State

template <typename H>
struct TopState {
    typedef H Host;
    typedef void Base;
    virtual void handler(Host&) const = 0;
    virtual unsigned getId() const = 0;
};

template <typename H, unsigned id, typename B>
struct CompState;

template <typename H, unsigned id, typename B = CompState<H, 0, TopState<H> > >
struct CompState : B {
    typedef B Base;
    typedef CompState<H, id, Base> This;
    template <typename X> void handle(H& h, const X& x) const { Base::handle(h, x); }
    static void init(H&); // no implementation
    static void entry(H&) {}
    static void exit(H&) {}
};

template <typename H>
struct CompState<H, 0, TopState<H> > : TopState<H> {
    typedef TopState<H> Base;
    typedef CompState<H, 0, Base> This;
    template <typename X> void handle(H&, const X&) const {}
    static void init(H&); // no implementation
    static void entry(H&) {}
    static void exit(H&) {}
};

template <typename H, unsigned id, typename B = CompState<H, 0, TopState<H> > >
struct LeafState : B {
    typedef H Host;
    typedef B Base;
    typedef LeafState<H, id, Base> This;
    template <typename X> void handle(H& h, const X& x) const { Base::handle(h, x); }
    virtual void handler(H& h) const { handle(h, *this); }
    virtual unsigned getId() const { return id; }
    static void init(H& h) { h.next(obj); } // don't specialize this
    static void entry(H&) {}
    static void exit(H&) {}
    static const LeafState obj; // only the leaf states have instances
};

template <typename H, unsigned id, typename B>
const LeafState<H, id, B> LeafState<H, id, B>::obj;

// Transition Object

template <typename C, typename S, typename T>
// Current, Source, Target
struct Tran {
    typedef typename C::Host Host;
    typedef typename C::Base CurrentBase;
    typedef typename S::Base SourceBase;
    typedef typename T::Base TargetBase;
    enum { // work out when to terminate template recursion
        eTB_CB = IsDerivedFrom<TargetBase, CurrentBase>::Res,
        eS_CB = IsDerivedFrom<S, CurrentBase>::Res,
        eS_C = IsDerivedFrom<S, C>::Res,
        eC_S = IsDerivedFrom<C, S>::Res,
        exitStop = eTB_CB && eS_C,
        entryStop = eS_C || eS_CB && !eC_S
    };
    // We use overloading to stop recursion.
    // The more natural template specialization
    // method would require to specialize the inner
    // template without specializing the outer one,
    // which is forbidden.
    static void exitActions(Host&, Bool<true>) {}
    static void exitActions(Host&h, Bool<false>) {
        C::exit(h);
        Tran<CurrentBase, S, T>::exitActions(h, Bool<exitStop>());
    }
    static void entryActions(Host&, Bool<true>) {}
    static void entryActions(Host& h, Bool<false>) {
        Tran<CurrentBase, S, T>::entryActions(h, Bool<entryStop>());
        C::entry(h);
    }
    Tran(Host & h) : host_(h) {
        exitActions(host_, Bool<false>());
    }
    ~Tran() {
        Tran<T, S, T>::entryActions(host_, Bool<false>());
        T::init(host_);
    }
    Host& host_;
};

// Initializer for Compound States

template <typename T>
struct Init {
    typedef typename T::Host Host;
    Init(Host& h) : host_(h) {}
    ~Init() {
        T::entry(host_);
        T::init(host_);
    }
    Host& host_;
};

#endif // HSM_HPP

Далее следует тестовый код.

#include <cstdio>
#include "hsm.hpp"
#include "hsmtest.hpp"

/* Implements the following state machine from Miro Samek's
 * Practical Statecharts in C/C++
 *
 * |-init-----------------------------------------------------|
 * |                           s0                             |
 * |----------------------------------------------------------|
 * |                                                          |
 * |    |-init-----------|        |-------------------------| |
 * |    |       s1       |---c--->|            s2           | |
 * |    |----------------|<--c----|-------------------------| |
 * |    |                |        |                         | |
 * |<-d-| |-init-------| |        | |-init----------------| | |
 * |    | |     s11    |<----f----| |          s21        | | |
 * | /--| |------------| |        | |---------------------| | |
 * | a  | |            | |        | |                     | | |
 * | \->| |            |------g--------->|-init------|    | | |
 * |    | |____________| |        | |-b->|    s211   |---g--->|
 * |    |----b---^       |------f------->|           |    | | |
 * |    |________________|        | |<-d-|___________|<--e----|
 * |                              | |_____________________| | |
 * |                              |_________________________| |
 * |__________________________________________________________|
 */

class TestHSM;

typedef CompState<TestHSM,0>     Top;
typedef CompState<TestHSM,1,Top>   S0;
typedef CompState<TestHSM,2,S0>      S1;
typedef LeafState<TestHSM,3,S1>        S11;
typedef CompState<TestHSM,4,S0>      S2;
typedef CompState<TestHSM,5,S2>        S21;
typedef LeafState<TestHSM,6,S21>         S211;

enum Signal { A_SIG, B_SIG, C_SIG, D_SIG, E_SIG, F_SIG, G_SIG, H_SIG };

class TestHSM {
public:
    TestHSM() { Top::init(*this); }
    ~TestHSM() {}
    void next(const TopState<TestHSM>& state) {
        state_ = &state;
    }
    Signal getSig() const { return sig_; }
    void dispatch(Signal sig) {
        sig_ = sig;
        state_->handler(*this);
    }
    void foo(int i) {
        foo_ = i;
    }
    int foo() const {
        return foo_;
    }
private:
    const TopState<TestHSM>* state_;
    Signal sig_;
    int foo_;
};

bool testDispatch(char c) {
    static TestHSM test;
    if (c<'a' || 'h'<c) {
        return false;
    }
    printf("Signal<-%c", c);
    test.dispatch((Signal)(c-'a'));
    printf("\n");
    return true;
}

int main(int, char**) {
    testDispatch('a');
    testDispatch('e');
    testDispatch('e');
    testDispatch('a');
    testDispatch('h');
    testDispatch('h');
    return 0;
}

#define HSMHANDLER(State) \
    template<> template<typename X> inline void State::handle(TestHSM& h, const X& x) const

HSMHANDLER(S0) {
    switch (h.getSig()) {
    case E_SIG: { Tran<X, This, S211> t(h);
        printf("s0-E;");
        return; }
    default:
        break;
    }
    return Base::handle(h, x);
}

HSMHANDLER(S1) {
    switch (h.getSig()) {
    case A_SIG: { Tran<X, This, S1> t(h);
        printf("s1-A;"); return; }
    case B_SIG: { Tran<X, This, S11> t(h);
        printf("s1-B;"); return; }
    case C_SIG: { Tran<X, This, S2> t(h);
        printf("s1-C;"); return; }
    case D_SIG: { Tran<X, This, S0> t(h);
        printf("s1-D;"); return; }
    case F_SIG: { Tran<X, This, S211> t(h);
        printf("s1-F;"); return; }
    default: break;
    }
    return Base::handle(h, x);
}

HSMHANDLER(S11) {
    switch (h.getSig()) {
    case G_SIG: { Tran<X, This, S211> t(h);
        printf("s11-G;"); return; }
    case H_SIG: if (h.foo()) {
            printf("s11-H");
            h.foo(0); return;
        } break;
    default: break;
    }
    return Base::handle(h, x);
}

HSMHANDLER(S2) {
    switch (h.getSig()) {
    case C_SIG: { Tran<X, This, S1> t(h);
        printf("s2-C"); return; }
    case F_SIG: { Tran<X, This, S11> t(h);
        printf("s2-F"); return; }
    default: break;
    }
    return Base::handle(h, x);
}

HSMHANDLER(S21) {
    switch (h.getSig()) {
    case B_SIG: { Tran<X, This, S211> t(h);
        printf("s21-B;"); return; }
    case H_SIG: if (!h.foo()) {
            Tran<X, This, S21> t(h);
            printf("s21-H;"); h.foo(1);
            return;
        } break;
    default: break;
    }
    return Base::handle(h, x);
}

HSMHANDLER(S211) {
    switch (h.getSig()) {
    case D_SIG: { Tran<X, This, S21> t(h);
        printf("s211-D;"); return; }
    case G_SIG: { Tran<X, This, S0> t(h);
        printf("s211-G;"); return; }
    }
    return Base::handle(h, x);
}

#define HSMENTRY(State) \
    template<> inline void State::entry(TestHSM&) { \
        printf(#State "-ENTRY;"); \
    }

HSMENTRY(S0)
HSMENTRY(S1)
HSMENTRY(S11)
HSMENTRY(S2)
HSMENTRY(S21)
HSMENTRY(S211)

#define HSMEXIT(State) \
    template<> inline void State::exit(TestHSM&) { \
        printf(#State "-EXIT;"); \
    }

HSMEXIT(S0)
HSMEXIT(S1)
HSMEXIT(S11)
HSMEXIT(S2)
HSMEXIT(S21)
HSMEXIT(S211)

#define HSMINIT(State, InitState) \
    template<> inline void State::init(TestHSM& h) { \
       Init<InitState> i(h); \
       printf(#State "-INIT;"); \
    }

HSMINIT(Top, S0)
HSMINIT(S0, S1)
HSMINIT(S1, S11)
HSMINIT(S2, S21)
HSMINIT(S21, S211)
  • 0
    Хм ... в вашем коде нет ничего. Прежде всего, вы включаете два заголовка, но предоставляете только первый. Когда я просто комментирую оператор include, я получаю эту ошибку при компиляции: d: \ 1 \ hsm> g ++ test.cpp test.cpp: 195: 1: error: специализация 'static void CompState <H, id, B> :: init (H &) [с H = TestHSM; без знака int id = 0u; B = CompState <TestHSM, 0u, TopState <TestHSM>>] 'после создания
  • 0
    Мне пришлось переместить определения всех HSMINIT () выше класса TestHSM, и он компилируется и работает нормально (; Единственное, что неправильно, это то, что все переходы являются «внешними», в то время как они должны быть «внутренними» - там было В статье обсуждались некоторые споры, и автор решил, что «extrenal» был прав, но использованные стрелки указывают на «внутренний».
5

Простейший случай

enum event_type { ET_THIS, ET_THAT };
union event_parm { uint8_t this; uint16_t that; }
static void handle_event(enum event_type event, union event_parm parm)
{
  static enum { THIS, THAT } state;
  switch (state)
  {
    case THIS:
    switch (event)
    {
      case ET_THIS:
      // Handle event.
      break;

      default:
      // Unhandled events in this state.
      break;
    }
    break;

    case THAT:
    // Handle state.
    break;
  }
}

Очки: Состояние является закрытым не только для компиляции, но и для event_handler. Специальные случаи могут обрабатываться отдельно от основного коммутатора, используя любую конструкцию, которая считается необходимой.

Более сложный случай

Когда коммутатор становится больше, чем на пару экранов, разделите его на функции, которые обрабатывают каждое состояние, используя таблицу состояний для непосредственного поиска функции. Состояние по-прежнему является приватным для обработчика событий. Функции обработчика состояния возвращают следующее состояние. При необходимости некоторые события могут по-прежнему получать специальное лечение в главном обработчике событий. Мне нравится бросать псевдо-события для входа и выхода из состояния и, возможно, запуска автомата:

enum state_type { THIS, THAT, FOO, NA };
enum event_type { ET_START, ET_ENTER, ET_EXIT, ET_THIS, ET_THAT, ET_WHATEVER, ET_TIMEOUT };
union event_parm { uint8_t this; uint16_t that; };
static void handle_event(enum event_type event, union event_parm parm)
{
  static enum state_type state;
  static void (* const state_handler[])(enum event_type event, union event_parm parm) = { handle_this, handle_that };
  enum state_type next_state = state_handler[state](event, parm);
  if (NA != next_state && state != next_state)
  {
    (void)state_handler[state](ET_EXIT, 0);
    state = next_state;
    (void)state_handler[state](ET_ENTER, 0);
  }
}

Я не уверен, что я прибил синтаксис, особенно в отношении массива указателей функций. Я не запускал ни одно из этого через компилятор. После обзора я заметил, что я забыл явно отказаться от следующего состояния при обработке псевдо-событий (скобки (void) перед вызовом state_handler()). Это то, что мне нравится делать, даже если компиляторы молча принимают молчание. Он сообщает читателям кода, что "да, я действительно имел в виду вызывать функцию без использования возвращаемого значения", и она может остановить инструменты статического анализа от предупреждения об этом. Это может быть особенным, потому что я не помню, чтобы кто-то другой это делал.

Баллы: добавление крошечной части сложности (проверка, если следующее состояние отличается от текущего), может избежать дублирования кода в другом месте, поскольку функции обработчика состояния могут пользоваться псевдо-событиями, которые возникают, когда состояние введено и осталось. Помните, что состояние не может измениться при обработке псевдо-событий, потому что результат обработчика состояния отбрасывается после этих событий. Разумеется, вы можете изменить поведение.

Обработчик состояния будет выглядеть так:

static enum state_type handle_this(enum event_type event, union event_parm parm)
{
  enum state_type next_state = NA;
  switch (event)
  {
    case ET_ENTER:
    // Start a timer to do whatever.
    // Do other stuff necessary when entering this state.
    break;

    case ET_WHATEVER:
    // Switch state.
    next_state = THAT;
    break;

    case ET_TIMEOUT:
    // Switch state.
    next_state = FOO;
    break;

    case ET_EXIT:
    // Stop the timer.
    // Generally clean up this state.
    break;
  }
  return next_state;
}

Более сложная

Когда блок компиляции становится слишком большим (независимо от того, что вы чувствуете, я должен сказать около 1000 строк), поместите каждый обработчик состояния в отдельный файл. Когда каждый обработчик состояния становится длиннее, чем несколько экранов, разделяйте каждое событие в отдельной функции, подобно тому, как разделялся переключатель состояния. Вы можете сделать это несколькими способами, отдельно от состояния или с помощью общей таблицы или объединения различных схем. Некоторые из них были охвачены другими. Сортируйте свои таблицы и используйте бинарный поиск, если требуется скорость.

Общее программирование

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

Двумерные таблицы

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

Отказ

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

  • 0
    Я бы избегал «this» в качестве имени переменной или символа только для ассоциации, даже если это на самом деле не зарезервированное слово.
5

Метод, который мне нравится для состояний машин (по крайней мере, для управления программой), заключается в использовании указателей функций. Каждое состояние представлено другой функцией. Функция принимает входной символ и возвращает указатель на функцию для следующего состояния. Центральные диспетчерские мониторы отправляют следующий вход, подают его в текущее состояние и обрабатывают результат.

Впечатывание на нем становится немного странным, так как C не имеет способа указывать типы возвращающих себя указателей функций, поэтому функции состояния возвращают void*. Но вы можете сделать что-то вроде этого:

typedef void* (*state_handler)(input_symbol_t);
void dispatch_fsm()
{
    state_handler current = initial_handler;
    /* Let assume returning null indicates end-of-machine */
    while (current) {
        current = current(get_input);
    }
 }

Затем ваши индивидуальные функции состояния могут включать свой вход для обработки и возвращать соответствующее значение.

  • 0
    +1 это действительно приятно, и предоставляет хорошие места для передачи функциональности внутри функций перехода
4

Другим интересным инструментом с открытым исходным кодом является Якинду. Инструменты Statechart на statecharts.org. Он использует диаграммы состояния Harel и, таким образом, обеспечивает иерархические и параллельные состояния и генерирует код C и С++ (а также Java). Он не использует библиотеки, но следует подходом "простого кода". Код в основном применяет структуры коммутационных шкафов. Генераторы кода также могут быть настроены. Кроме того, инструмент предоставляет множество других функций.

4

Чрезвычайно непроверенный, но забавный код, теперь в более изысканной версии, чем мой оригинальный ответ; обновленные версии можно найти на mercurial.intuxication.org:

sm.h

#ifndef SM_ARGS
#error "SM_ARGS undefined: " \
    "use '#define SM_ARGS (void)' to get an empty argument list"
#endif

#ifndef SM_STATES
#error "SM_STATES undefined: " \
    "you must provide a list of comma-separated states"
#endif

typedef void (*sm_state) SM_ARGS;
static const sm_state SM_STATES;

#define sm_transit(STATE) ((sm_state (*) SM_ARGS)STATE)

#define sm_def(NAME) \
    static sm_state NAME ## _fn SM_ARGS; \
    static const sm_state NAME = (sm_state)NAME ## _fn; \
    static sm_state NAME ## _fn SM_ARGS

example.c

#include <stdio.h>

#define SM_ARGS (int i)
#define SM_STATES EVEN, ODD
#include "sm.h"

sm_def(EVEN)
{
    printf("even %i\n", i);
    return ODD;
}

sm_def(ODD)
{
    printf("odd  %i\n", i);
    return EVEN;
}

int main(void)
{
    int i = 0;
    sm_state state = EVEN;

    for(; i < 10; ++i)
        state = sm_transit(state)(i);

    return 0;
}
  • 14
    Мне нравится "крайне непроверенный" комментарий. Кажется, указывает на то, что существуют степени непроверенности и что вы приложили немало усилий, чтобы не протестировать его :-)
  • 0
    @ Кристоф, ссылка в этом ответе не работает. Кроме того, вы проверяли этот код или нет? Если это было проверено и работает, вы должны удалить это из ответа. Также, возможно, покажите пример того, к какому коду это приводит после развертывания макросов. Мне нравится общая идея.
3

boost.org поставляется с двумя различными реализациями графиков состояний:

Как всегда, boost запустит вас в шаблонный ад.

Первая библиотека предназначена для более критически важных состояний. Вторая библиотека дает вам прямой путь перехода из UML Statechart в код.

Здесь SO вопрос, требующий сравнения между ними, где оба автора отвечают.

3

Alrght, я думаю, что я немного отличаюсь от всех остальных. Немного больше разделения кода и данных, чем я вижу в других ответах. Я действительно прочитал теорию, чтобы написать это, в котором реализуется полный регулярный язык (без регулярных выражений, к сожалению). Ульман, Минский, Хомский. Не могу сказать, что я все это понял, но я сделал из старых мастеров максимально возможное: через их слова.

Я использую указатель функции на предикат, который определяет переход в состояние "да" или состояние "нет". Это облегчает создание акцептора конечного состояния для обычного языка, который вы программируете более похожим на ассемблере образом. Пожалуйста, не путайте мои глупые имена. 'czek' == 'check'. 'grok' == [искать его в Hacker Dictionary].

Итак, для каждой итерации czek вызывает предикатную функцию с текущим символом в качестве аргумента. Если предикат возвращает true, символ потребляется (указатель продвинут), и мы следуем "y", чтобы выбрать следующее состояние. Если предикат возвращает false, символ НЕ потребляется, и мы следуем "n". Поэтому каждая инструкция - это двусторонняя ветка! Должно быть, я читал "Историю Мела" в то время.

Этот код поступает прямо из моего интерпретатора постскриптума и превратился в его текущую форму с большим руководством от стипендиатов на comp.lang.c. Поскольку postcript в принципе не имеет синтаксиса (требуется только сбалансированные скобки), такой же как и синтаксический анализатор, является обычным языком Accepter.

/* currentstr is set to the start of string by czek
   and used by setrad (called by israd) to set currentrad
   which is used by israddig to determine if the character
   in question is valid for the specified radix
   --
   a little semantic checking in the syntax!
 */
char *currentstr;
int currentrad;
void setrad(void) {
    char *end;
    currentrad = strtol(currentstr, &end, 10);
    if (*end != '#' /* just a sanity check,
                       the automaton should already have determined this */
    ||  currentrad > 36
    ||  currentrad < 2)
        fatal("bad radix"); /* should probably be a simple syntaxerror */
}

/*
   character classes
   used as tests by automatons under control of czek
 */
char *alpha = "0123456789" "ABCDE" "FGHIJ" "KLMNO" "PQRST" "UVWXYZ";
#define EQ(a,b) a==b
#define WITHIN(a,b) strchr(a,b)!=NULL
int israd  (int c) {
    if (EQ('#',c)) { setrad(); return true; }
    return false;
}
int israddig(int c) {
    return strchrnul(alpha,toupper(c))-alpha <= currentrad;
}
int isdot  (int c) {return EQ('.',c);}
int ise    (int c) {return WITHIN("eE",c);}
int issign (int c) {return WITHIN("+-",c);}
int isdel  (int c) {return WITHIN("()<>[]{}/%",c);}
int isreg  (int c) {return c!=EOF && !isspace(c) && !isdel(c);}
#undef WITHIN
#undef EQ

/*
   the automaton type
 */
typedef struct { int (*pred)(int); int y, n; } test;

/*
   automaton to match a simple decimal number
 */
/* /^[+-]?[0-9]+$/ */
test fsm_dec[] = {
/* 0*/ { issign,  1,  1 },
/* 1*/ { isdigit, 2, -1 },
/* 2*/ { isdigit, 2, -1 },
};
int acc_dec(int i) { return i==2; }

/*
   automaton to match a radix number
 */
/* /^[0-9]+[#][a-Z0-9]+$/ */
test fsm_rad[] = {
/* 0*/ { isdigit,  1, -1 },
/* 1*/ { isdigit,  1,  2 },
/* 2*/ { israd,    3, -1 },
/* 3*/ { israddig, 4, -1 },
/* 4*/ { israddig, 4, -1 },
};
int acc_rad(int i) { return i==4; }

/*
   automaton to match a real number
 */
/* /^[+-]?(d+(.d*)?)|(d*.d+)([eE][+-]?d+)?$/ */
/* represents the merge of these (simpler) expressions
   [+-]?[0-9]+\.[0-9]*([eE][+-]?[0-9]+)?
   [+-]?[0-9]*\.[0-9]+([eE][+-]?[0-9]+)?
   The complexity comes from ensuring at least one
   digit in the integer or the fraction with optional
   sign and optional optionally-signed exponent.
   So passing isdot in state 3 means at least one integer digit has been found
   but passing isdot in state 4 means we must find at least one fraction digit
   via state 5 or the whole thing is a bust.
 */
test fsm_real[] = {
/* 0*/ { issign,  1,   1 },
/* 1*/ { isdigit, 2,   4 },
/* 2*/ { isdigit, 2,   3 },
/* 3*/ { isdot,   6,   7 },
/* 4*/ { isdot,   5,  -1 },
/* 5*/ { isdigit, 6,  -1 },
/* 6*/ { isdigit, 6,   7 },
/* 7*/ { ise,     8,  -1 },
/* 8*/ { issign,  9,   9 },
/* 9*/ { isdigit, 10, -1 },
/*10*/ { isdigit, 10, -1 },
};
int acc_real(int i) {
    switch(i) {
        case 2: /* integer */
        case 6: /* real */
        case 10: /* real with exponent */
            return true;
    }
    return false;
}

/*
   Helper function for grok.
   Execute automaton against the buffer,
   applying test to each character:
       on success, consume character and follow 'y' transition.
       on failure, do not consume but follow 'n' transition.
   Call yes function to determine if the ending state
   is considered an acceptable final state.
   A transition to -1 represents rejection by the automaton
 */
int czek (char *s, test *fsm, int (*yes)(int)) {
    int sta = 0;
    currentstr = s;
    while (sta!=-1 && *s) {
        if (fsm[sta].pred((int)*s)) {
            sta=fsm[sta].y;
            s++;
        } else {
            sta=fsm[sta].n;
        }
    }
    return yes(sta);
}

/*
   Helper function for toke.
   Interpret the contents of the buffer,
   trying automatons to match number formats;
   and falling through to a switch for special characters.
   Any token consisting of all regular characters
   that cannot be interpreted as a number is an executable name
 */
object grok (state *st, char *s, int ns,
    object *src,
    int (*next)(state *,object *),
    void (*back)(state *,int, object *)) {

    if (czek(s, fsm_dec, acc_dec)) {
        long num;
        num = strtol(s,NULL,10);
        if ((num==LONG_MAX || num==LONG_MIN) && errno==ERANGE) {
            error(st,limitcheck);
/*       } else if (num > INT_MAX || num < INT_MIN) { */
/*           error(limitcheck, OP_token); */
        } else {
            return consint(num);
        }
    }

    else if (czek(s, fsm_rad, acc_rad)) {
        long ra,num;
        ra = (int)strtol(s,NULL,10);
        if (ra > 36 || ra < 2) {
            error(st,limitcheck);
        }
        num = strtol(strchr(s,'#')+1, NULL, (int)ra);
        if ((num==LONG_MAX || num==LONG_MIN) && errno==ERANGE) {
            error(st,limitcheck);
/*       } else if (num > INT_MAX || num < INT_MAX) { */
/*           error(limitcheck, OP_token); */
        } else {
            return consint(num);
        }
    }

    else if (czek(s, fsm_real, acc_real)) {
        double num;
        num = strtod(s,NULL);
        if ((num==HUGE_VAL || num==-HUGE_VAL) && errno==ERANGE) {
            error(st,limitcheck);
        } else {
            return consreal(num);
        }
    }

    else switch(*s) {
        case '(': {
            int c, defer=1;
            char *sp = s;

            while (defer && (c=next(st,src)) != EOF ) {
                switch(c) {
                    case '(': defer++; break;
                    case ')': defer--;
                        if (!defer) goto endstring;
                        break;
                    case '\\': c=next(st,src);
                        switch(c) {
                            case '\n': continue;
                            case 'a': c = '\a'; break;
                            case 'b': c = '\b'; break;
                            case 'f': c = '\f'; break;
                            case 'n': c = '\n'; break;
                            case 'r': c = '\r'; break;
                            case 't': c = '\t'; break;
                            case 'v': c = '\v'; break;
                            case '\'': case '\"':
                            case '(': case ')':
                            default: break;
                        }
                }
                if (sp-s>ns) error(st,limitcheck);
                else *sp++ = c;
            }
endstring:  *sp=0;
            return cvlit(consstring(st,s,sp-s));
        }

        case '<': {
            int c;
            char d, *x = "0123456789abcdef", *sp = s;
            while (c=next(st,src), c!='>' && c!=EOF) {
                if (isspace(c)) continue;
                if (isxdigit(c)) c = strchr(x,tolower(c)) - x;
                else error(st,syntaxerror);
                d = (char)c << 4;
                while (isspace(c=next(st,src))) /*loop*/;
                if (isxdigit(c)) c = strchr(x,tolower(c)) - x;
                else error(st,syntaxerror);
                d |= (char)c;
                if (sp-s>ns) error(st,limitcheck);
                *sp++ = d;
            }
            *sp = 0;
            return cvlit(consstring(st,s,sp-s));
        }

        case '{': {
            object *a;
            size_t na = 100;
            size_t i;
            object proc;
            object fin;

            fin = consname(st,"}");
            (a = malloc(na * sizeof(object))) || (fatal("failure to malloc"),0);
            for (i=0 ; objcmp(st,a[i]=toke(st,src,next,back),fin) != 0; i++) {
                if (i == na-1)
                (a = realloc(a, (na+=100) * sizeof(object))) || (fatal("failure to malloc"),0);
            }
            proc = consarray(st,i);
            { size_t j;
                for (j=0; j<i; j++) {
                    a_put(st, proc, j, a[j]);
                }
            }
            free(a);
            return proc;
        }

        case '/': {
            s[1] = (char)next(st,src);
            puff(st, s+2, ns-2, src, next, back);
            if (s[1] == '/') {
                push(consname(st,s+2));
                opexec(st, op_cuts.load);
                return pop();
            }
            return cvlit(consname(st,s+1));
        }

        default: return consname(st,s);
    }
    return null; /* should be unreachable */
}

/*
   Helper function for toke.
   Read into buffer any regular characters.
   If we read one too many characters, put it back
   unless it whitespace.
 */
int puff (state *st, char *buf, int nbuf,
    object *src,
    int (*next)(state *,object *),
    void (*back)(state *,int, object *)) {
    int c;
    char *s = buf;
    while (isreg(c=next(st,src))) {
        if (s-buf >= nbuf-1) return false;
        *s++ = c;
    }
    *s = 0;
    if (!isspace(c) && c != EOF) back(st,c,src); /* eat interstice */
    return true;
}

/*
   Helper function for Stoken Ftoken.
   Read a token from src using next and back.
   Loop until having read a bona-fide non-whitespace non-comment character.
   Call puff to read into buffer up to next delimiter or space.
   Call grok to figure out what it is.
 */
#define NBUF MAXLINE
object toke (state *st, object *src,
        int (*next)(state *, object *),
        void (*back)(state *, int, object *)) {
    char buf[NBUF] = "", *s=buf;
    int c,sta = 1;
    object o;

    do {
        c=next(st,src);
        //if (c==EOF) return null;
        if (c=='%') {
            if (DUMPCOMMENTS) fputc(c, stdout);
            do {
                c=next(st,src);
                if (DUMPCOMMENTS) fputc(c, stdout);
            } while (c!='\n' && c!='\f' && c!=EOF);
        }
    } while (c!=EOF && isspace(c));
    if (c==EOF) return null;
    *s++ = c;
    *s = 0;
    if (!isdel(c)) sta=puff(st, s,NBUF-1,src,next,back);

    if (sta) {
        o=grok(st,buf,NBUF-1,src,next,back);
        return o;
    } else {
        return null;
    }
}
  • 2
    Это то, что любой парсер или генератор лексеров с радостью выпустит для вас. Странно так. Хотите ли вы написать код вручную, сомнительно. Это имеет педагогическую ценность, конечно.
3

Придя к этому поздно (как обычно), но просматривая ответы на сегодняшний день, я думаю, что что-то важное отсутствует,

Я нашел в своих проектах, что очень полезно не иметь функции для каждой допустимой комбинации состояний/событий. Мне нравится идея эффективно иметь 2D-таблицу состояний/событий. Но мне нравится, что элементы таблицы являются более чем простым указателем на функцию. Вместо этого я пытаюсь организовать свой проект, поэтому в нем он содержит кучу простых атомных элементов или действий. Таким образом, я могу перечислить эти простые атомные элементы на каждом пересечении таблицы состояния/события. Идея состоит в том, что вам не нужно определять массу N квадратов (обычно очень простых) функций. Почему у кого-то такое склонное к ошибкам, трудоемкое, трудное для записи, трудно читаемое, вы называете его?

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

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

  • 2
    Может быть, пример был бы хорош, нет?
  • 1
    Реалистичный пример, который можно представить отдельно, - это сложная задача, которая потребует больше времени, чем я готов дать в данный момент. Есть ли в моем посте что-то особенно сложное для понимания? Может быть, я могу выразить это более четко. Идея очень проста; Не определяйте механизм состояний, который требует отдельной функции для каждой комбинации события / состояния, вы получаете слишком много функций таким образом. Вместо этого найдите другой способ описать функциональность, которую вы хотите для этой комбинации события / состояния, по крайней мере, в большинстве случаев.
Показать ещё 1 комментарий
2

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

Я загрузил свою реализацию на этот сайт, чтобы поделиться с сообществом. Он был протестирован с использованием IAR Embedded Workbench для ARM.

https://sourceforge.net/projects/compactfsm/

  • 0
    Нахождение этого в 2018 году и что это все еще применимо. Я читал ответ @paxdiablo и раньше успешно использовал этот тип реализации во встроенных системах. Это решение добавляет недостающие вещи из ответа paxdiablos :)
2

Учитывая, что вы подразумеваете, что можете использовать С++ и, следовательно, OO-код, я бы предложил оценить шаблон GoF'state (GoF = Gang of Four, ребята, написавшие книгу шаблонов дизайна, которая привнесла шаблоны дизайна в центр внимания).

Это не особенно сложно и широко используется и обсуждается, поэтому легко увидеть примеры и объяснения на линии.

Это также вполне вероятно будет узнано любым другим, поддерживающим ваш код позднее.

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

Ниже приведены некоторые ссылки на шаблон состояния Gof, как предлагает Крейг:

  • 0
    выглядит больше как комментарий: могу ли я предложить вам относиться к нему как таковой? т.е. не помещайте его в раздел «ответ».
  • 0
    Было бы хорошо, если бы вы могли предоставить хорошую URL-ссылку для «шаблона состояния GoF» для тех, кто не знаком с ним.
Показать ещё 2 комментария
2

Пила это где-то

#define FSM
#define STATE(x)      s_##x :
#define NEXTSTATE(x)  goto s_##x

FSM {
  STATE(x) {
    ...
    NEXTSTATE(y);
  }

  STATE(y) {
    ...
    if (x == 0)
      NEXTSTATE(y);
    else
      NEXTSTATE(x);
  }
}
  • 1
    Это интересно, но не возражайте, пока вы не приведете один или два примера (и, возможно, результат de-macro'ed) или некоторое обсуждение того, почему это может быть более практичным, чем другой. Интересное использование сиротских скобок и макросов. Я полагаю, что нечто подобное можно было бы сделать на языке, который выполняет некоторую оптимизацию хвостовой рекурсии; Вы можете использовать прямые вызовы функций и не беспокоиться о перегрузке стекового пространства мусором при вызове функций (что, я думаю, является тем, что макросы здесь по существу преодолевают)
  • 2
    Преимущества этого метода ...? Я вижу несколько недостатков, таких как запутывание макросов и использование goto которое создает зависимость от более ранней многозадачной ОС.
2

Эта серия статей Ars OpenForum о несколько сложной бит логики управления включает в себя очень простую в использовании реализацию в качестве конечного автомата в C.

1

Это старое сообщение с большим количеством ответов, но я подумал, что добавлю свой подход к машине конечного состояния в C. Я создал Python script для создания скелетного кода C для любого количества состояний. Этот script документирован на GituHub по адресу FsmTemplateC

Этот пример основан на других подходах, о которых я читал. Он не использует операторы goto или switch, а вместо этого имеет функции перехода в матрицу указателя (справочная таблица). Код опирается на большой многострочный макрос инициализатора и функции C99 (назначенные инициализаторы и сложные литералы), поэтому, если вам не нравятся эти вещи, вам может не понравиться этот подход.

Вот пример Python script пример турникета, который генерирует скелет C-кода, используя FsmTemplateC:

# dict parameter for generating FSM
fsm_param = {
    # main FSM struct type string
    'type': 'FsmTurnstile',
    # struct type and name for passing data to state machine functions
    # by pointer (these custom names are optional)
    'fopts': {
        'type': 'FsmTurnstileFopts',
        'name': 'fopts'
    },
    # list of states
    'states': ['locked', 'unlocked'],
    # list of inputs (can be any length > 0)
    'inputs': ['coin', 'push'],
    # map inputs to commands (next desired state) using a transition table
    # index of array corresponds to 'inputs' array
    # for this example, index 0 is 'coin', index 1 is 'push'
    'transitiontable': {
        # current state |  'coin'  |  'push'  |
        'locked':       ['unlocked',        ''],
        'unlocked':     [        '',  'locked']
    }
}

# folder to contain generated code
folder = 'turnstile_example'
# function prefix
prefix = 'fsm_turnstile'

# generate FSM code
code = fsm.Fsm(fsm_param).genccode(folder, prefix)

Сгенерированный выходной заголовок содержит typedefs:

/* function options (EDIT) */
typedef struct FsmTurnstileFopts {
    /* define your options struct here */
} FsmTurnstileFopts;

/* transition check */
typedef enum eFsmTurnstileCheck {
    EFSM_TURNSTILE_TR_RETREAT,
    EFSM_TURNSTILE_TR_ADVANCE,
    EFSM_TURNSTILE_TR_CONTINUE,
    EFSM_TURNSTILE_TR_BADINPUT
} eFsmTurnstileCheck;

/* states (enum) */
typedef enum eFsmTurnstileState {
    EFSM_TURNSTILE_ST_LOCKED,
    EFSM_TURNSTILE_ST_UNLOCKED,
    EFSM_TURNSTILE_NUM_STATES
} eFsmTurnstileState;

/* inputs (enum) */
typedef enum eFsmTurnstileInput {
    EFSM_TURNSTILE_IN_COIN,
    EFSM_TURNSTILE_IN_PUSH,
    EFSM_TURNSTILE_NUM_INPUTS,
    EFSM_TURNSTILE_NOINPUT
} eFsmTurnstileInput;

/* finite state machine struct */
typedef struct FsmTurnstile {
    eFsmTurnstileInput input;
    eFsmTurnstileCheck check;
    eFsmTurnstileState cur;
    eFsmTurnstileState cmd;
    eFsmTurnstileState **transition_table;
    void (***state_transitions)(struct FsmTurnstile *, FsmTurnstileFopts *);
    void (*run)(struct FsmTurnstile *, FsmTurnstileFopts *, const eFsmTurnstileInput);
} FsmTurnstile;

/* transition functions */
typedef void (*pFsmTurnstileStateTransitions)(struct FsmTurnstile *, FsmTurnstileFopts *);
  • enum eFsmTurnstileCheck используется для определения того, был ли заблокирован переход с EFSM_TURNSTILE_TR_RETREAT, разрешен для выполнения с помощью EFSM_TURNSTILE_TR_ADVANCE или вызову функции не предшествовал переход с EFSM_TURNSTILE_TR_CONTINUE.
  • enum eFsmTurnstileState - это просто список состояний.
  • enum eFsmTurnstileInput - это просто список входов.
  • Структура FsmTurnstile является основой конечного автомата с проверкой перехода, таблицей поиска функций, текущим состоянием, командным состоянием и псевдонимом основной функции, которая запускает машину.
  • Каждый указатель функции (псевдоним) в FsmTurnstile должен вызываться только из структуры и должен иметь свой первый вход как указатель на себя, чтобы поддерживать постоянное состояние, объектно-ориентированный стиль.

Теперь для объявлений функций в заголовке:

/* fsm declarations */
void fsm_turnstile_locked_locked (FsmTurnstile *fsm, FsmTurnstileFopts *fopts);
void fsm_turnstile_locked_unlocked (FsmTurnstile *fsm, FsmTurnstileFopts *fopts);
void fsm_turnstile_unlocked_locked (FsmTurnstile *fsm, FsmTurnstileFopts *fopts);
void fsm_turnstile_unlocked_unlocked (FsmTurnstile *fsm, FsmTurnstileFopts *fopts);
void fsm_turnstile_run (FsmTurnstile *fsm, FsmTurnstileFopts *fopts, const eFsmTurnstileInput input);

Имена функций находятся в формате {prefix}_{from}_{to}, где {from} - это предыдущее (текущее) состояние, а {to} - следующее состояние. Обратите внимание, что если таблица переходов не допускает определенных переходов, будет установлен указатель NULL вместо указателя функции. Наконец, магия происходит с макросом. Здесь мы строим таблицу переходов (матрицу перечислений состояний) и таблицу переходов состояний (таблица матриц указателей функций):

/* creation macro */
#define FSM_TURNSTILE_CREATE() \
{ \
    .input = EFSM_TURNSTILE_NOINPUT, \
    .check = EFSM_TURNSTILE_TR_CONTINUE, \
    .cur = EFSM_TURNSTILE_ST_LOCKED, \
    .cmd = EFSM_TURNSTILE_ST_LOCKED, \
    .transition_table = (eFsmTurnstileState * [EFSM_TURNSTILE_NUM_STATES]) { \
        (eFsmTurnstileState [EFSM_TURNSTILE_NUM_INPUTS]) { \
            EFSM_TURNSTILE_ST_UNLOCKED, \
            EFSM_TURNSTILE_ST_LOCKED \
        }, \
        (eFsmTurnstileState [EFSM_TURNSTILE_NUM_INPUTS]) { \
            EFSM_TURNSTILE_ST_UNLOCKED, \
            EFSM_TURNSTILE_ST_LOCKED \
        } \
    }, \
    .state_transitions = (pFsmTurnstileStateTransitions * [EFSM_TURNSTILE_NUM_STATES]) { \
        (pFsmTurnstileStateTransitions [EFSM_TURNSTILE_NUM_STATES]) { \
            fsm_turnstile_locked_locked, \
            fsm_turnstile_locked_unlocked \
        }, \
        (pFsmTurnstileStateTransitions [EFSM_TURNSTILE_NUM_STATES]) { \
            fsm_turnstile_unlocked_locked, \
            fsm_turnstile_unlocked_unlocked \
        } \
    }, \
    .run = fsm_turnstile_run \
}

При создании FSM необходимо использовать макрос FSM_EXAMPLE_CREATE().

Теперь в исходном коде должна быть заполнена каждая заявленная выше функция перехода состояния. Структуру FsmTurnstileFopts можно использовать для передачи данных в/из конечного автомата. Каждый переход должен установить fsm->check равным либо EFSM_EXAMPLE_TR_RETREAT, чтобы заблокировать его от перехода, или EFSM_EXAMPLE_TR_ADVANCE, чтобы он мог перейти в командное состояние. Рабочий пример можно найти на (FsmTemplateC) [https://github.com/ChisholmKyle/FsmTemplateC].

Вот очень простое фактическое использование в вашем коде:

/* create fsm */
FsmTurnstile fsm = FSM_TURNSTILE_CREATE();
/* create fopts */
FsmTurnstileFopts fopts = {
    .msg = ""
};
/* initialize input */
eFsmTurnstileInput input = EFSM_TURNSTILE_NOINPUT;

/* main loop */
for (;;) {
    /* wait for timer signal, inputs, interrupts, whatever */
    /* optionally set the input (my_input = EFSM_TURNSTILE_IN_PUSH for example) */
    /* run state machine */
    my_fsm.run(&my_fsm, &my_fopts, my_input);
}

Все, что заголовок бизнес и все эти функции просто иметь простой и быстрый интерфейс стоит в моем сознании.

1

Я успешно использовал State Machine Compiler в проектах Java и Python.

1

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

  • Внедрение встроенного государственного устройства

    В этой статье описывается простой подход к реализации конечного автомата для встроенной системы. Для целей этой статьи конечный автомат определяется как алгоритм, который может находиться в одном из небольшого числа состояний. Состояние - это условие, которое вызывает предписанную взаимосвязь входов и выходов и входов в следующие состояния. Разумный читатель быстро заметит, что государственные машины, описанные в этой статье, являются машинами Мили. Машина "Мили" - это машина состояний, где выходы являются функцией как текущего состояния, так и входа, в отличие от машины Мура, в которой выходы являются функцией только состояния.

    • Машины кодирования состояния на C и С++

      Моя озабоченность в этой статье связана с основными состояниями машины и некоторыми простыми правилами программирования для кодирования состояний машин на C или С++. Я надеюсь, что эти простые методы могут стать более распространенными, так что вы (и другие) можете легко увидеть структуру состояния-машины прямо из исходного кода.

  • 6
    Ссылки мертвы или не указывают на то, что предлагает пост.
0

Я лично использую самореляционные структуры в сочетании с массивами указателей. Я загрузил учебник по github некоторое время назад, ссылка:

https://github.com/mmelchger/polling_state_machine_c

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

0
void (* StateController)(void); 
void state1(void);
void state2(void);

void main()
{
 StateController=&state1;
 while(1)
 {
  (* StateController)();
 }
}

void state1(void)
{
 //do something in state1
 StateController=&state2;
}

void state2(void)
{
 //do something in state2
 //Keep changing function direction based on state transition
 StateController=&state1;
}
  • 0
    Вы можете дополнительно оптимизировать его для безопасности, используя массив постоянных указателей на функции
0

Вы можете использовать библиотеку с открытым исходным кодом OpenFST.

OpenFst - это библиотека для построения, комбинирования, оптимизации и поиска взвешенных конечных преобразователей (FST). Взвешенные конечные преобразователи - это автоматы, в которых каждый переход имеет входную метку, выходную метку и вес. Более знакомый конечный акцептор представлен как преобразователь, каждый из которых соответствует входному и выходному меткам перехода. Акцепторы конечного состояния используются для представления наборов строк (в частности, регулярных или рациональных множеств); преобразователи конечного состояния используются для представления двоичных отношений между парами строк (в частности, рациональных трансдукций). Веса могут использоваться для представления стоимости принятия конкретного перехода.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню