Stavový automat pro Arduino

Zápisník experimentátora

Když si vytváříte nějaký projekt pomocí Arduina a používáte v něm tlačítka a LED diody, obvykle vytváříte nějaký typ stavového automatu. Stavový automat reaguje pomocí tlačítek na signály zvenku a pomocí výstupů zase odesílá signály ven. Možná si ani neuvědomujete, že vytváříte automat, který musí velmi přesně dodržet mnoho pravidel. Obvykle máte omezené množství tlačítek a tak jedno tlačítko provádí různé úkoly. Stavový automat vám usnadňuje definice stavů a definice akcií, které jsou k těmto stavům přiřazeny. A díky těmto přesným pravidlům bude váš program reagovat v daném stavu tak jak potřebujete. Pojďme se naučit, jak pohodlně vytvářet stavové automaty.

arduino-fsm

Na vytváření stavových automatů použijeme hotové řešení. Knihovnu arduino-fsm si nainstalujete pomocí správce knihoven. Ve třech příkladech si ukážeme její možnosti. Při vytváření příkladů jsem vycházel z příkladů, které jsou součástí knihovny. I tato knihovna ale trpí neduhem špatné dokumentace a příklady obsahují chyby. Já jsem tyto chyby opravil a přidal jsem ještě jeden příklad, který lépe ukáže možnosti knihovny.

Blink

První příklad napodobuje typický program Blink, na kterém začíná každý uživatel Arduina. Program je delší, než původní Blink, protože demonstruje možnosti volání callback funkci v každém možném okamžiku. V tomto případě většina funkcí nedělá nic, pouze vypisuje informaci na sériový port. Díky tomu je dobrým úvodem k pochopení činnosti stavového automatu.

Každý stav (state_light_on a state_light_off) je definován pomocí tří callback funkcí.

  • První se zavolá při aktivaci stavu. Funkcí lze chápat jako inicializaci stavu. V tomto případě je callback funkce využita k přepnutí HIGH / LOW pinu, na kterém je připojena LED dioda.
  • Druhá se zavolá stále během setrvávání ve stavu. V tomto případě nemá využití a je místo ní použit pointer NULL. Pro praktické využití se podívejte příklad 2, kde se ve funkci kontroluje stisknutí tlačítka.
  • Poslední funkce se zavolá při opouštění stavu a přechodu na nový stav. Funkci můžete využít tehdy, když opouštíte stav a potřebujete v tom okamžiku něco udělat. Pro praktické využití se podívejte příklad číslo 3.

Aby bylo možné mezi stavy procházet, je třeba definovat povolené cesty mezi stavy. K tomu se používá funkce add_transition. Její parametry jsou

  • počáteční stav
  • koncový stav
  • událost
  • callback při události

Pokud je v systému definována cesta mezi stavy a je signalizována událost, stavový automat se přepne do nového stavu. Přepnutí je doprovázeno zavoláním všech callback funkcí ve správném pořadí. To pořadí najdete v zdrojem kódu ve funkci make_transition. Událost signalizujete stavovému automatu pomocí funkce trigger.

#include "Fsm.h"

// Used pins
#define LED_PIN LED_BUILTIN

// State machine variables
#define FLIP_LIGHT_SWITCH 1

State state_light_on(on_light_on_enter, NULL, &on_light_on_exit);
State state_light_off(on_light_off_enter, NULL, &on_light_off_exit);
Fsm fsm(&state_light_off);

// Transition callback functions
void on_light_on_enter() {
  Serial.println(__FUNCTION__);
  digitalWrite(LED_PIN, HIGH);
}

void on_light_on_exit() {
  Serial.println(__FUNCTION__);
  digitalWrite(LED_PIN, LOW);
}

void on_light_off_enter() {
  Serial.println(__FUNCTION__);
}

void on_light_off_exit() {
  Serial.println(__FUNCTION__);
}

void on_trans_light_on_light_off() {
  Serial.println(__FUNCTION__);
  Serial.println("  Transitioning from LIGHT_ON to LIGHT_OFF");
}

void on_trans_light_off_light_on() {
  Serial.println(__FUNCTION__);
  Serial.println("  Transitioning from LIGHT_OFF to LIGHT_ON");
}

// standard arduino functions
void setup() {
  Serial.begin(9600);
  Serial.println("FSM Light Switch");
  pinMode(LED_PIN, OUTPUT);

  fsm.add_transition(&state_light_on, &state_light_off,
                     FLIP_LIGHT_SWITCH,
                     &on_trans_light_on_light_off);
  fsm.add_transition(&state_light_off, &state_light_on,
                     FLIP_LIGHT_SWITCH,
                     &on_trans_light_off_light_on);
  fsm.run_machine();
}

void loop() {
  Serial.println("-- loop --");
  delay(2000);
  fsm.trigger(FLIP_LIGHT_SWITCH);
}

Načasovaný přechod

Druhý příklad používá stavová automat na přepínání stavu pomocí tlačítka. První stisk přepne stav state_led_on. Druhé stisknutí přepne stav state_led_off. Stisknutí tlačítka i s ošetřením zákmitů nájdete ve funkcii check_button.

Demo obsahuje ještě jeden zajímavý přechod mezi stavy. Tím je načasovaný přechod, který se zavolá automaticky v případě, že je zapnutý stav state_led_on a uplynulo 5 sekund. O to se postará funkce add_timed_transition. Toto je velmi dobrá pomůcka ve světě mikrokontrolérů, kde takové časování má mnoho využití. Nemusíte to složitě programovat, pouze si přidáte časovaný přechod. Všimněte si, že ve funkci loop se zavolá pouze jediná funkce run_machine. Ta interně vola v aktivním stavu funkci check_button, která se postará o vyvolání události v okamžiku, když je detekováno stisknutí tlačítka.

#include "Fsm.h"

// Used pins
#define LED_PIN     LED_BUILTIN
#define BUTTON_PIN  10

// Events
#define BUTTON_EVENT  0

// Button states and debounce
int buttonState = 0;
int lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;

// States
State state_led_off(&led_off, &check_button, NULL);
State state_led_on(&led_on, &check_button, NULL);
Fsm fsm(&state_led_off);

void led_off() {
  Serial.println(__FUNCTION__);
  digitalWrite(LED_PIN, LOW);
}

void led_on() {
  Serial.println(__FUNCTION__);
  digitalWrite(LED_PIN, HIGH);
}

// Check button and debounce
void check_button() {
  int reading = digitalRead(BUTTON_PIN);
  if (reading != lastButtonState)
    lastDebounceTime = millis();

  if ((millis() - lastDebounceTime) > 50) {
    if (reading != buttonState) {
      buttonState = reading;
      if (buttonState == LOW) {
        Serial.println("button_pressed");
        fsm.trigger(BUTTON_EVENT);
      }
    }
  }

  lastButtonState = reading;
}

void setup() {
  Serial.begin(9600);

  pinMode(LED_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  fsm.add_transition(&state_led_off, &state_led_on,
                     BUTTON_EVENT, NULL);
  fsm.add_timed_transition(&state_led_on, &state_led_off, 5000, NULL);
  fsm.add_transition(&state_led_on, &state_led_off, BUTTON_EVENT, NULL);
}

void loop() {
  fsm.run_machine();
}

Načasovaný přechod s LED modulem

V posledním příkladu jsem se rozhodl otestovat možnosti knihovny na větším stavovém automatu. Z předchozího příkladu jsem nechal zapnutí a vypnutí a doplnil jsem další stavy. Ty jsou využity pro ovládání osmi LED diod. Mám je zapojené podle obrázku. Využívám na to modul, na kterém je 8 LED diod spolu s rezistory. Toto je jeden z mých vlastních modulů a používám ho na breadboardu. Zapojení vidíte na ilustračním obrázku.

8 LED diod tvoří pomyslný proužek LED světel, které se postupně všechny zapínají. Periodickým stlačováním tlačítek postupně zapnete všechny LED diody. Pokud budete pokračovat v stlačení tlačítka, LED diody se vypnou a celý cyklus se bude opakovat. Všimněte si, jak jsem využil funkci led_leave na posledním stavu. Zde se mi hodila možnost provést akci při opuštění stavu a resetovat počítadlo LED.

Aby byl příklad komplikovanější, doplnil jsem i časované přechody. Délka setrvání pásku diod v svítícím stavu je přímo úměrná počtu zapnutých LED. Pokud v definovaném čase opět nestisknete tlačítko, celý stavový automat se přepne do úvodního stavu.

#include "Fsm.h"

// Used pins
#define LED_PIN     LED_BUILTIN
#define BUTTON_PIN  10

const int LED_BAR[] = {2, 3, 4, 5, 6, 7, 8, 9};
const int LED_BAR_LENGTH = sizeof(LED_BAR) / sizeof(int);
int bar_position = 0;

// Events
#define BUTTON_EVENT  0

// Button states and debounce
int buttonState = 0;
int lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;

// States
State state_led_off(&led_off, &check_button, NULL);
State state_led_on(&led_on,   &check_button, NULL);
State state_led1(&led_enter,  &check_button, NULL);
State state_led2(&led_enter,  &check_button, NULL);
State state_led3(&led_enter,  &check_button, NULL);
State state_led4(&led_enter,  &check_button, NULL);
State state_led5(&led_enter,  &check_button, NULL);
State state_led6(&led_enter,  &check_button, NULL);
State state_led7(&led_enter,  &check_button, NULL);
State state_led8(&led_enter,  &check_button, &led_leave);
Fsm fsm(&state_led_off);

void led_off() {
  Serial.println(__FUNCTION__);
  digitalWrite(LED_PIN, LOW);
  for (int i = 0; i < LED_BAR_LENGTH; i++)
    digitalWrite(LED_BAR[i], LOW);
  bar_position = 0;
}

void led_on() {
  Serial.println(__FUNCTION__);
  digitalWrite(LED_PIN, HIGH);
}

void led_enter() {
  Serial.println(__FUNCTION__);
  if (bar_position < LED_BAR_LENGTH)
    bar_position++;
  for (int i = 0; i < bar_position; i++)
    digitalWrite(LED_BAR[i], HIGH);
  for (int i = bar_position; i < LED_BAR_LENGTH; i++)
    digitalWrite(LED_BAR[i], LOW);
}

void led_leave() {
  Serial.println(__FUNCTION__);
  bar_position = 0;
}

// Check button and debounce
void check_button() {
  int reading = digitalRead(BUTTON_PIN);
  if (reading != lastButtonState)
    lastDebounceTime = millis();

  if ((millis() - lastDebounceTime) > 50) {
    if (reading != buttonState) {
      buttonState = reading;
      if (buttonState == LOW) {
        Serial.println("button_pressed");
        fsm.trigger(BUTTON_EVENT);
      }
    }
  }

  lastButtonState = reading;
}

void setup() {
  Serial.begin(9600);

  pinMode(LED_PIN, OUTPUT);
  for (int i = 0; i < LED_BAR_LENGTH; i++)
    pinMode(LED_BAR[i], OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  // on/off
  fsm.add_transition(      &state_led_off, &state_led_on, BUTTON_EVENT, NULL);
  fsm.add_timed_transition(&state_led_on,  &state_led_off, 5000, NULL);

  // LED bar button
  fsm.add_transition(      &state_led_on,  &state_led1, BUTTON_EVENT, NULL);
  fsm.add_transition(      &state_led1,    &state_led2, BUTTON_EVENT, NULL);
  fsm.add_transition(      &state_led2,    &state_led3, BUTTON_EVENT, NULL);
  fsm.add_transition(      &state_led3,    &state_led4, BUTTON_EVENT, NULL);
  fsm.add_transition(      &state_led4,    &state_led5, BUTTON_EVENT, NULL);
  fsm.add_transition(      &state_led5,    &state_led6, BUTTON_EVENT, NULL);
  fsm.add_transition(      &state_led6,    &state_led7, BUTTON_EVENT, NULL);
  fsm.add_transition(      &state_led7,    &state_led8, BUTTON_EVENT, NULL);
  fsm.add_transition(      &state_led8,    &state_led1, BUTTON_EVENT, NULL);
  // timed transitions
  fsm.add_timed_transition(&state_led1,    &state_led_off, 1000, NULL);
  fsm.add_timed_transition(&state_led2,    &state_led_off, 2000, NULL);
  fsm.add_timed_transition(&state_led3,    &state_led_off, 3000, NULL);
  fsm.add_timed_transition(&state_led4,    &state_led_off, 4000, NULL);
  fsm.add_timed_transition(&state_led5,    &state_led_off, 5000, NULL);
  fsm.add_timed_transition(&state_led6,    &state_led_off, 6000, NULL);
  fsm.add_timed_transition(&state_led7,    &state_led_off, 7000, NULL);
  fsm.add_timed_transition(&state_led8,    &state_led_off, 8000, NULL);
}

void loop() {
  fsm.run_machine();
}

Zdrojový kód

Zdrojový kód se nachází na serveru GitHub.


13.02.2018


Menu