Эксперимент 35. Конечные автоматы

Коне́чный автома́т — абстрактный автомат, число возможных внутренних состояний которого конечно. Если говорить проще, то с помощью конечного автомата описываются состояния какого либо объекта и переходы между этими состояниями. Например, светофор можно описать с помощью конечного автомата.

Видно, что из состояния 1 (красного сигнала) светофор может перейти только в состояние 2 (красный + желтый), означающий скорое включение зеленого. Из состояния 2 светофор может перейти только в состояние 3 (зеленый сигнал). После зеленого всегда идет желтый сигнал (состояние 4), который сменяется красным (состояние 1). Главное, что у данного конечного автомата есть 4 состояния и мы знаем из какого состояния в какое он может переходить.

Конечные автоматы иногда очень полезны для описания состояний электроники. Возьмем тот же инкрементальный энкодер. Снова посмотрим на график сигналов от него:

По графику видно, что состояния у энкодера не меняются хаотично. Они меняются только последовательно. Если крутить ручку в одну сторону, то состояния сменяются 0-1-2-3-0…, а если в другую, то 0-3-2-1-0…

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

Схема эксперимента

Рисунок 1. Электрическая принципиальная схема эксперимента

Рисунок 2. Монтажная схема эксперимента

Программный код эксперимента

Exp35.ino
  1. #include <LCDI2C_Multilingual.h>
  2.  
  3. #define ENC_A 13
  4. #define ENC_B 12
  5. #define DEFAULT_I2C_ADDR 0x3F // Или 0x27 в зависимости от твоей платы IoT
  6.  
  7. LCDI2C_Generic lcd(DEFAULT_I2C_ADDR, 16, 2);
  8.  
  9. bool states[4][2] = {
  10. {0,0},
  11. {1,0},
  12. {1,1},
  13. {0,1}
  14. };
  15.  
  16. bool value_a = 0;
  17. bool value_b = 0;
  18.  
  19. int count = 0;
  20. int state = 0;
  21. int state_old = 0;
  22.  
  23. void printLcd(int number) {
  24. lcd.clear();
  25. lcd.print(number);
  26. }
  27.  
  28. int index(bool a, bool b){
  29. for (int i = 0; i < 4; i++){
  30. if (states[i][0] == a and states[i][1] == b) {
  31. return i;
  32. }
  33. }
  34. return -1;
  35. }
  36.  
  37. void setup() {
  38. Serial.begin(9600);
  39. lcd.init();
  40. lcd.setBacklight(0);
  41. pinMode(ENC_A, INPUT);
  42. pinMode(ENC_B, INPUT);
  43. }
  44.  
  45. void loop() {
  46. value_a = digitalRead(ENC_A);
  47. value_b = digitalRead(ENC_B);
  48.  
  49. state = index(value_a, value_b);
  50.  
  51. if ((state - state_old == 1) or (state == 0 and state_old == 3)) {
  52. count++;
  53. Serial.println("+");
  54. printLcd(count);
  55. state_old = state;
  56. }
  57. else if ((state - state_old == -1) or (state == 3 and state_old == 0)) {
  58. count--;
  59. Serial.println("-");
  60. printLcd(count);
  61. state_old = state;
  62. }
  63. }

Описываем возможные состояния конечного автомата:

  1. bool states[4][2] = {
  2. {0,0},
  3. {1,0},
  4. {1,1},
  5. {0,1}
  6. };

Двумерный массив 4×2. Первая цифра это состояние сигнала A, вторая — сигнала B. Номер состояния — это индекс элемента списка. Логика переключения состояний автомата простая — состояние может смениться только на соседнее: состояние 0 на 1, 1 на 2, 2 на 3, 3 на 0. И в обратном направлении. Состояние не может измениться «перескочив» через другое. Эту логику мы опишем в программе далее.

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

В основном цикле программы мы получаем данные о текущем состоянии линий А и B:

  1. value_a = digitalRead(ENC_A);
  2. value_b = digitalRead(ENC_B);

И определяем номер состояния конечного автомата, соответствующего такому состоянию А и B с помощью написанной нами функцией index():

  1. state = index(value_a, value_b);

Функция index(a, b) принимает два параметра типа bool, осуществляет поиск соответствующего элемента в массиве states и возвращает индекс найденного элемента. Ключевое слово return прерывает работу функции и возвращает указанное после него значение.

  1. int index(bool a, bool b){
  2. for (int i = 0; i < 4; i++){
  3. if (states[i][0] == a and states[i][1] == b) {
  4. return i;
  5. }
  6. }
  7. return -1;
  8. }

Зачем же нужна строка return -1; в конце функции, если в цикле for в любом случае будет найден и возвращен индекс? Компилятор не на столько умный и не знает об этом, но для всех функций обязательно возвращение значения, иначе программа не скомпилируется. Поэтому мы возвращаем заведомо нереальное значение, т.к. в нашем случае строка return -1; никогда не будет выполнена.

Теперь мы знаем индекс только что измеренного состояния линий. А индекс последнего состояния конечного автомата хранится в переменной state_old. Теперь нам нужно понять можем ли мы из состояния записанного в state_old переключиться в новое состояние, индекс которого мы определили и записали в state.

  1. if ((state - state_old == 1) or (state == 0 and state_old == 3)) {

Если индекс нового состояния на 1 больше старого или, если новое состояние 0, а старое 3, то регистрируем переход в новое состояние. Переводим конечный автомат в новое состояние state_old = state. Увеличиваем на 1 счетчик count, печатаем + в терминал и отображаем счетчик на дисплее.

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

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