Stavový automat pre Arduino

Zápisník experimentátora

Keď si vytvárate nejaký projekt pomocou Arduina a používate v ňom tlačidlá a LED diódy, obvykle vytvárate nejaký typ stavového automatu. Stavový automat reaguje pomocou tlačidiel na signály zvonku a pomocou výstupov zase odosiela signály von. Možno si ani neuvedomujete, že vytvárate automat, ktorý musí veľmi presne dodržať veľa pravidiel. Obvykle máte obmedzené množstvo tlačidiel a tak jedno tlačidlo vykonáva rôzne úlohy. Stavový automat vám uľahčuje definície stavov a definície akcií, ktoré sú k týmto stavom priradené. A vďaka týmto presným pravidlám bude váš program reagovať v danom stave tak ako potrebujete. Poďme sa naučiť, ako pohodlne vytvárať stavové automaty.

arduino-fsm

Na vytváranie stavových automatov použijeme hotové riešenie. Knižnicu arduino-fsm si nainštalujete pomocou správcu knižníc. V troch príkladoch si ukážeme jej možnosti. Pri vytváraní príkladov som vychádzal z príkladov, ktoré sú súčasťou knižnice. Aj táto knižnica ale trpí neduhom zlej dokumentácie a príklady obsahujú chyby. Ja som tieto chyby opravil a pridal som ešte jeden príklad, ktorý lepšie ukáže možnosti knižnice.

Blink

Prvý príklad napodobňuje typický program Blink, na ktorom začína každý užívateľ Arduina. Program je dlhší, než pôvodný Blink, pretože demonštruje možnosti volania callback funkcii v každom možnom okamihu. V tomto prípade väčšina funkcií nerobí nič, iba vypisuje informáciu na sériový port. Vďaka tomu je dobrým úvodom k pochopeniu činnosti stavového automatu.

Každý stav (state_light_on a state_light_off) je definovaný pomocou troch callback funkcií.

  • Prvá sa zavolá pri aktivovaní stavu. Funkciou možno chápať ako inicializáciu stavu. V tomto prípade je callback funkcia využitá na prepnutie HIGH/LOW pinu, na ktorom je pripojená LED dióda.
  • Druhá sa volá stále počas zotrvávania v stave. V tomto prípade nemá využitie a je miesto nej použitý pointer NULL. Pre praktické využitie si pozrite príklad 2, kde sa vo funkcii kontroluje stlačenie tlačidla.
  • Posledná funkcia sa volá pri opúšťaní stavu a prechode na nový stav. Funkciu môžete využiť vtedy, keď opúšťate stav a potrebujete v tom okamihu niečo urobiť. Pre praktické využitie si pozrite príklad číslo 3.

Aby bolo možné medzi stavmi prechádzať, je potrebné definovať povolené cesty medzi stavmi. Na to sa používa funkcia add_transition. Jej parametrami sú

  • počiatočný stav
  • koncový stav
  • udalosť
  • callback pri udalosti

Pokiaľ je v systéme definovaná cesta medzi stavmi a je signalizovaná udalosť, stavový automat sa prepne do nového stavu. Prepnutie je sprevádzané zavolaním všetkých callback funkcií v správnom poradí. To poradie nájdete v zdrojom kóde vo funkcii make_transition. Udalosť signalizujete stavovému automatu pomocou funkcie 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ý prechod

Druhý príklad používa stavová automat na prepínanie stavu pomocou tlačidla. Prvé stlačenie prepne stav state_led_on. Druhé stlačenie prepne stav state_led_off. Stlačenie tlačidla aj s ošetrením zákmitov nájdete vo funkcii check_button.

Demo obsahuje ešte jeden zaujímavý prechod medzi stavmi. Tým je načasovaný prechod, ktorý sa zavolá automaticky v prípade, že je zapnutý stav state_led_on a uplynulo 5 sekúnd. O to sa postará funkcia add_timed_transition. Toto je veľmi dobrá pomôcka vo svete mikrokontrolérov, kde takéto časovanie má veľa využití. Nemusíte to zložito programovať, iba si pridáte časovaný prechod. Všimnite si, že vo funkcii loop sa volá iba jediná funkcia run_machine. Tá interne vola v aktívnom stave funkciu check_button, ktorá sa postará o vyvolanie udalosti v okamihu, ako je detekované stlačenie tlačidla.

#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ý prechod s LED modulom

V poslednom príklade som sa rozhodol otestovať možnosti knižnice na väčšom stavovom automate. Z predchádzajúceho príkladu som nechal zapnutie a vypnutie a doplnil som ďalšie stavy. Tie sú využité na ovládanie ôsmich LED diód. Mám ich zapojené podľa obrázku. Využívam na to modul, na ktorom je 8 LED diód spolu s rezistormi. Toto je jeden z mojich vlastných modulov a používam ho na breadboarde. Zapojenie vidíte na ilustračnom obrázku.

8 LED diód tvorí pomyselný prúžok LED svetiel, ktoré sa postupne všetky zapínajú. Periodickým stláčaním tlačidiel postupne zapnete všetky LED diódy. Ak budete pokračovať v stláčaní tlačidla, LED diódy sa vypnú a celý cyklus sa bude opakovať. Všimnite si, ako som využil funkciu led_leave na poslednom stave. Tu sa mi hodila možnosť vykonať akciu pri opustení stavu a resetnúť počítadlo LED.

Aby bol príklad komplikovanejší, doplnil som aj časované prechody. Dĺžka zotrvania pásika diód v svietiacom stave je priamo úmerná počtu zapnutých LED. Pokiaľ v definovanom čase opäť nestlačím tlačidlo, celý stavový automat sa prepne 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 sa nachádza na serveri GitHub.


13.02.2018


Menu