Программирование конечных автоматов (без delay).

Arhat109

★★★★✩✩✩
9 Июн 2019
473
203
Выложу в отдельную тему, если не возражаете..

Чутка теории (очень): Конечный Автомат (КА) это такая организация программы, когда она делает нечто строго по своему состоянию (в википедию, за большим). Например "кнопка нажата - делаем это", "кнопка отпущена" - не делаем это или делаем что-то ишо.
Или парсер текста (анализатор компилятора): "пришел такой символ" - о, это начало имени переменной, делаем это. "пришел такой символ" - упс, его не ждали, ошибка.
КА может переходить из одного состояния в другое, в зависимости от тех или иных внешних событий - "нажата кнопка", "нашли очередной символ в строке" и т.д., в т.ч. и по времении "прошло столько-то лет/минут/секунд .. от ..".
В общем, у КА есть "слушатель входного потока событий", который может быть как службой времени (millis), аппаратным событием - "прерывание", программным событием "код выполнил это" (нажата кнопка).. в традиционной реализации, "слушатель" не выделяется из кода КА, и является его частью (прочитать очередной символ из строки, проверить состояние кнопки и т.д., проверить закончился ли интервал времени).
И также есть "матрица состояний" между которыми КА "путешествует" по мере появления входных событий: пока нажата кнопка - повторяем делать это (состояние) или обнаружено нажатие кнопки - сделать это (действие по переходу между состояниями) и перейти в режим ожидания (состояние). Или прочитан символ (перейти в состояние чтения имени переменной)
Состояние - то действие (или бездействие) которое длится существенный промежуток времени МЕЖДУ входными событиями .. часто тупо "ожидание события".

Соответственно, можно различать 2 больших класса КА: автоматы, исполняющие код на переходе между состояниями (Милли) и те, которые исполняют код пока длится состояние (Мура).

Ну и конечно же есть код, исполняющийся в том или ином состоянии - "исполнитель". Также часто пишется вместе с кодом "слушателя" и очень часто в виде switch() оператора:
case "состяние1": "делай это" и "перейди в состояниеN"
...
и часто код КА отличается по наличию "переменной текущего состояния" ..

То есть, все задачи видов:
1. Сделай это, через 5мсек сделай так, а через 100лет сделай тут .. (КА Времени - разного рода "часовые реле" - например "мигалка двоеточием в часах" - включи/выключи каждые 0.5сек);
2. Запусти замер, как закончится (через столько) выведи на экран (вариант предыдущего .. интервал замера существенная трата времени и часто сделан на delay);
3. Если случилось тут, то включи это и покажи тут, потом включи тут и выключи тут ..
4. Пришло прерывание надо принять по SPI кучку байтов, и в коде "пришла команда по SPI - надо сделать это"
и мн. другие классы задач легко решаются как раз программированием в стиле "конечных автоматов".

Более того! У микроконтроллера, принимающего внешние события, управляющего по ним теми или иными устройствами и сигнализирующего об их состоянии на то или иное устройство вывода .. и нет иных "задач", кроме как в виде "конечных автоматов". Он предназначен в общем-то "исключительно для этого". :)

В стиле КА легко встраиваются и асинхронные обработчики прерываний, т.к. по сути это такой же "слушатель" внешнего потока аппаратного события. Не больше.
 

Arhat109

★★★★✩✩✩
9 Июн 2019
473
203
1. КА "Времени". (блинк без делай) ..

Тут первые грабли, это работа по определению интервала времени. Особенности беззнаковой целочисленной арифметики компьютеров позволяют использовать такой код (но и ТОЛЬКО в такой форме!!!):
C:
if( millis() - prevTime > timeInterval){ /* do something */ }
Не переживая за вопрос переполнения счетчика времени внутри millis(). Главное, чтобы все операнды были одного и БЕЗзнакового типа. ;)

Использовать эту конструкцию, к сожалению не очень удобно в большом проекте, поэтому рекомендую организовать её в такой макрос (скопипастил с соседней темы):
C:
// вырезано из arhat/ka.h:
//
// Укорачиваем место хранения времени и экономим память:
// переопределение интервалов до 65535 мсек
typedef uint16_t KaTime;

// ****************** 1. Автоматы, управляемые временем ***************** //

// переопределяем функцию получения времени: укорачивем возвращаемое значение до 2 байт
// максимально допустимый интервал между исполнениями 65.535 сек.
#define kaMillis()   ((KaTime)millis())

// Простейший конечный автомат сначала(!) исполняющий действие command и
// затем ожидающий его повторения через interval тиков таймера кратно 1 миллисекунде.
// В качестве команды может быть даже последовательность команд через ; или прямой вызов функции!
// интервалы можно задавать константно и переменными (выражениями).
// При первом вызове сразу исполняет действие и настраивает интервал до следующего исполнения
//
// @example прямая вставка исполняемого кода в фигурных скобках:
//
// everyMillis(1000,
// {
//   digitalWrite(13, 1-digitalRead(13));
//   analogWrite(10, analogRead(A0)>>4);
// });
//
// @example или прямой вызов функции: everyMillis(1000, blink(); );
//
#define everyMillis(interval, action)         \
do{                                           \
  static KaTime t = 0U;                       \
  if( kaMillis() - t > ((KaTime)(interval)) ) \
  {                                           \
    t = kaMillis();                           \
    { action }                                \
  }                                           \
}while(0)
Этого достаточно, чтобы реализовывать простейшие "автоматы времени", типа мигания двоеточием в часах, независимо ни от чего (заодно, как признак что программа работает). Решение для 13-й ноги, прямо тут же в примере (только убрать вторую строчку про чтение) .. ;)
 

Arhat109

★★★★✩✩✩
9 Июн 2019
473
203
Часто, автоматы времени требуют поведения несколько сложнее (разные реле-таймеры в управлении): "сделай так, потом вот так и ещё это" .. то есть автомат по истечении серии интервалов делает некие действия.

Самый первый пример - светофор: включи красный на 30сек, затем желтый на 3сек, затем зеленый на 60сек, после чего помигай зеленым 3 раза (выключи и включи по 0.5сек) и .. перейди в началу программы.
Тут можно "автоматизировать" все, кроме самой программы. Последнюю можно представить как массив состояний со своим исполнителем и длительностью интервала до следующего состояния:
C:
// *.h часть:
//---------------------------------------------------------------------------------------

// Выделенные коды состояний для всех типов конечных автоматов тут:
// автомат "выключен":
#define KA_STATE_OFF -1
// нет нового состояния перехода (не изменять!)
#define KA_STATE_NO  -2

// ****************** 2. Автоматы времени, управляемые таблицей состояний(переходов) ***************** //

// Все действия автомата определяются интервалами между ними - разного рода "мигалки".
// Порядок: сначала вызывается команда, а потом(!) выдерживается указанная в строке пауза до указанной строки
//   "Выполнить текущую команду и ждать этот интервал до заданной строки"
// Нумерация состояний с 0
// Слушатель входного потока - тут встроен и занят только ожиданием завершения таймаута.
// Исполнитель команды возвращает bool - откуда брать новое состояние {stateOk|stateNo}

typedef struct _ka_time_ctrl KaTimeControl;

// Функции - исполнители действий автоматов времени (моргалок)
// Получает структуру своего КА на всякий случай..
// Возвращает bool, выбор следующей строки по результату из колонки: nextOk или nextNo
typedef int8_t (*KaTimeCommand)(KaTimeControl *_ka);

// Преобразователь указателей к типу "команда автомата времени"
#define ptrKaTimeCommand(ptr) ((KaTimeCommand)(ptr))

// Тип для строки таблицы состояний автомата:
typedef struct {
  KaTimeCommand command;        // команда (метод) для исполнения действий состояния
  KaTime        timeout;        // временной интервал до(!) следующего состояния (мсек)
  int8_t        stateOk;        // успешно: номер следующей команды в таблице состояний КА
  int8_t        stateNo;        // ошибка: номер следующей команды в таблице состояний КА
} KaTimeStep;

// Преобразователь указателей к строке таблицы
#define ptrKaTimeStep(ptr) ((KaTimeStep *)(ptr))

// Собственно КА, управляемый таблицей состояний и интервалов
typedef struct _ka_time_ctrl {
  KaTime       started_at;      // момент начала текущего интервала ожидания
  KaTime       timeout;         // текущий интервал до запуска строки state
  int8_t       nextState;       // ожидаемый к исполнению номер или выключен
  KaTimeStep * table;           // таблица состояний и переходов этого КА
} KaTimeControl;

// Преобразователь указателей к управляющей структуре КА
#define ptrKaTimeControl(ptr) ((KaTimeControl *)(ptr))

// setup() или динамическая смена таблицы без изменения состояния
inline void kaTimeSetup( KaTimeControl *_ka, KaTimeStep * _table){ _ka->table = _table; }

// Принудительное изменение шага: и сразу исполняет его команду.
void kaTimeGo( KaTimeControl *_ka, uint8_t _toStep );

// В loop(): исполнение шага, если прошел интервал с предыдущего (его помним)
//   Если таблица не задана - ничего не выполняет (отключение автомата)
//   Если в таблице нет команды (==0) производит задержку до следующего выполнения, ничего не вызывает
//   Время исполнения текущей команды входит в интервал ожидания следующей!
void kaTimeRun( KaTimeControl *_ka );

// оно же для таблицы управления, размещенной во flash
void pgmTimeGo( KaTimeControl *_ka, uint8_t _toStep );
void pgmTimeRun( KaTimeControl *_ka );

// *.c часть (реализация функций)
//------------------------------------------------------------------------------------
// ************ 2. КА по времени с таблицей состояний (мигалки) *********** //

// Принудительное изменение шага: и сразу исполняет его команду.
//   Если в таблице нет команды (==0) производит задержку до следующего выполнения, ничего не вызывает
//   Время исполнения текущей команды входит в интервал ожидания следующей!
//   Если таблица не задана или next<0 - ничего не выполняет (отключение автомата)
void kaTimeGo( KaTimeControl *_ka, uint8_t _toStep )
{
  if( _ka->table )                                               // есть таблица программы КА
  {
    int8_t res = 1;                                              // нет команды - значит "всё ок".

    _ka->started_at = kaMillis();                                // время ожидания: начало тут
    KaTimeStep * ptrStep = _ka->table + _toStep;                 // идем к строке
    _ka->timeout = ptrStep->timeout;                             // ждать будем столько

    if( ptrStep->command ){ res = (ptrStep->command)(_ka); }     // исполняем команду сразу тут

    _ka->nextState = (res? ptrStep->stateOk : ptrStep->stateNo); // следующий шаг
  }
}

// В loop(): исполнение шага, если прошел интервал с предыдущего (его помним)
void kaTimeRun( KaTimeControl *_ka )
{
  KaTime skippedTime = kaMillis() - _ka->started_at; // сколько уже прошло времени

  if( (_ka->nextState != KA_STATE_OFF) && (skippedTime >= _ka->timeout) )
  {
    kaTimeGo(_ka, _ka->nextState);
  }
}

// ====== 2.1 версии для размещения таблицы управления во flash ====== //

void pgmTimeGo( KaTimeControl *_ka, uint8_t _toStep )
{
  if( _ka->table )
  {
    _ka->started_at = kaMillis();   // время ожидания пошло
    {
      int8_t          res=1;
      KaTimeStep    * ptrStep = _ka->table + _toStep;  // flash-адрес!
      KaTimeCommand   command = ptrKaTimeCommand(pgm_read_word_near( &(ptrStep->command) ));

      _ka->timeout = pgm_read_word_near( &(ptrStep->timeout) );
      if( command ){ res=(command)(_ka); }
      _ka->nextState = res?
            pgm_read_byte_near( &(ptrStep->stateOk) )
          : pgm_read_byte_near( &(ptrStep->stateNo) )
      ;
    }
  }
}

void pgmTimeRun( KaTimeControl *_ka )
{
  KaTime skippedTime = kaMillis() - _ka->started_at; // сколько уже прошло времени

  if( (_ka->nextState != KA_STATE_OFF) && (skippedTime >= _ka->timeout) )
  {
    pgmTimeGo(_ka, _ka->nextState);
  }
}
Собственно тут, "автоматизирован" весь процесс работы такого автомата, принимающего "на входе" таблицу своего поведения и только. Сама таблица может быть как "изменяемой" в процессе работы кода (лежать в SRAM) так и фиксированного поведения (во FLASH).

Все, что надо воткнуть в цикл loop() - это запуска исполнения программы автомата функцией xxxTimeRun(), где xxx - откуда берем программу и в параметре указать "программу управления".
 
Изменено:

Arhat109

★★★★✩✩✩
9 Июн 2019
473
203
Программировать автоматы Времени можно прямо "по ТЗ", типа так (на примере того же светофора). Тут шаги разработки - пронумерованы и весь пример писался "снизу вверх":
C:
// пример2 -- RGB моргалка (светофор) со сложной программой по времени (пишем "снизу-вверх")

#include "ka.h"

// 4. Назначем пины светофору и делаем функцию инициализации:
#define pinRed    2
#define pinYellow 3
#define pinGreen  4

void rgbSetup(){
  pinMode(pinRed,    OUTPUT);
  pinMode(pinYellow, OUTPUT);
  pinMode(pinGreen,  OUTPUT);
}

// 3. Создаем "команды" светофора:
int8_t doRGB_Off(KaTimeControl *_ka)
{
  digitalWrite(pinRed, LOW);
  digitalWrite(pinYellow, LOW);
  digitalWrite(pinGreen, LOW);
  return 1;
}
int8_t doRGB_Red(KaTimeControl *_ka)
{
  doRGB_Off(_ka);
  digitalWrite(pinRed, HIGH);
  return 1;
}
int8_t doRGB_Yellow(KaTimeControl *_ka)
{
  doRGB_Off(_ka);
  digitalWrite(pinYellow, HIGH);
  return 1;
}
int8_t doRGB_Green(KaTimeControl *_ka)
{
  doRGB_Off(_ka);
  digitalWrite(pinGreen, HIGH);
  return 1;
}
int8_t doRGB_All(KaTimeControl *_ka)
{
  digitalWrite(pinRed, HIGH);
  digitalWrite(pinYellow, HIGH);
  digitalWrite(pinGreen, HIGH);
  return 1;
}

// генератор ошибки (примера для)
int8_t doRGB_Error(KaTimeControl *_ka)
{
  return (rand()%100 > 10? 1 : 0);
}

// 2. Создаем "программу" для светофора
// Закомментирован вариант объявления (и запуска) с размещением программы во flash
// Возврат любой командой "ошибки" (0) приведет к аварии "морганию всеми светодиодами"

// const KaTimeStep rgbProg[] PROGMEM = {
KaTimeStep rgbProg[] = {
// поля:  command,    timeout,       nextOk,      nextNo
       { doRGB_Off,       0,KA_STATE_OFF,KA_STATE_OFF}, //  0: светофор выключен и стоит в этом шаге
       { doRGB_Red,    3000,  2,  9}, //  1: включить красный на 3сек
       { doRGB_Yellow,  500,  3,  9}, //  2: включить желтый на 0.5сек
       { doRGB_Green,  1000,  4,  9}, //  3: включить зеленый на 1 сек
       { doRGB_Off,     250,  5,  9}, //  4: выключить
       { doRGB_Green,   250,  6,  9}, //  5: мигаем зеленым
       { doRGB_Off,     250,  7,  9}, //  6: выключить
       { doRGB_Green,   250,  8,  9}, //  7: мигаем зеленым
       { doRGB_Error,     0,  1,  9}, //  8: ошибка? повторяем программу или переходим на обработку ошибки

       { doRGB_All,     250, 10, 10}, //  9: Ошибка светофора! Включаем все на 0.25сек
       { doRGB_Off,     250, 11, 11}, // 10: Ошибка светофора! Выключаем всё на 0.25сек или стоп.
       { doRGB_Error,     0,  1,  0}, // 11: ошибка? повторная ошибка выключает светофор..
};

// 1. Определяем глобальную структуру для светофора (тут задан старт с первой, а не нулевой записи!):
KaTimeControl rgb = {
// поля: started_at, timeout, nextState, table
              0,        0,        1,    ptrKaTimeStep(rgbProg) // желательно явное приведение при размещении во flash
};

// ============== собственно скетч: ===================

void setup() {
  rgbSetup();
}

void loop() {
  kaTimeRun( &rgb );
//  pgmTimeRun( &rgb );
}
Компилятор "ругается" на этот пример, выкладывая Warning, поскольку внутри исполнителей передаваемый параметр НЕ использован .. ну не нужен он тут или не везде.. ;)

В программе управления есть в каждом шаге 2 ветки: успешное и ошибочное поведение на шаге. Понятно, что оно не всегда требуется, но .. вдруг Ваш светофор "умеет" определять что "лампочка перегорела"? ;)
 
  • Лойс +1
Реакции: svl

Arhat109

★★★★✩✩✩
9 Июн 2019
473
203
Файлы из моей библиотеки, кому понравилось (там есть и другие виды конечных автоматов)
 

Вложения

  • 9.6 KB Просмотры: 28
  • 16.3 KB Просмотры: 27

Arhat109

★★★★✩✩✩
9 Июн 2019
473
203
Для разнообразия. Вариант этого же макроса, на случай "отложенного старта" первого исполнения :)

C:
#define everyMillis(start, interval, action)  \
do{                                           \
  static KaTime t = (start));                 \
  if( kaMillis() - t > ((KaTime)(interval)) ) \
  {                                           \
    t = kaMillis();                           \
    { action }                                \
  }                                           \
}while(0)
! Добавлен параметр start, который задает начальное время (мсек) от запуска millis() в начале исполнения кода. Поскольку он присваивается статической внутренней переменной t, доступа извне к которой не существует, то такое присваивание исполняется ровно 1 раз .. об этом надо помнить .. лучше, если при компиляции программы.
:)

Также можно развить макрос в сторону idle исполнения - "что делать, если время ещё не вышло?" .. :)
.. или добавить "флаг включения автомата" в условие if(..), что позволит управлять исполнением "извне" - флаг сброшен = автомат не работает, например так:
C:
#define everyMillis(interval, isRun, action)  \
do{                                           \
  static KaTime t = 0U;                       \
  if(                                         \
      (isRun) &&                              \
      (kaMillis() - t > ((KaTime)(interval))) \
  ){                                          \
    t = kaMillis();                           \
    { action }                                \
  }                                           \
}while(0)

/* и где-нибудь в коде (ТЗ: если сработал датчик пожара, то включить оралку с частотами 1 и 2кГц по 2сек каждая) */
  if( /* сработал пожарный датчик*/ ){ alarmFire = 1 /* запускаем оралку */ }
  // ..
  // определяем константы оралки:
  #define ALARM_TONE_TIME 2000
  #define ALARM_PIN 13
  // ..
  // работа автомата в loop():
  everyMillis( ALARM_TONE_TIME, alarmFire, {
    static int curTone = 1000;
    curTone = 3000 - curTone;
    tone(ALARM_PIN, curTone, ALARM_TONE_TIME);
  });
  // ..
*/
(* просто как пример, куда и как это можно дорабатывать *)
 
Изменено:
  • Лойс +1
Реакции: kostyamat

Arhat109

★★★★✩✩✩
9 Июн 2019
473
203
@kalobyte, Спасибо. Видео хорошо разжевывает тему. Тут как раз набор примитивов, для такого подхода к построению ПО, тс. "джентельменский набор", конкретику допиливать самостоятельно. :)

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

Ещё тут как-бы "объединены" не только сами КА, но ещё и разного рода "таймеры Времени", когда надо исполнить определенную последовательность согласно "временной программе".

И Последнее достоинство на мой взгляд - это отсутствие в ПО развесистых switch - case операторов, которыми так сильно грешат все остальные предложения.

P.S. Ну и кстати, это можно достаточно просто переложить на С++ и наследие классов, но уже с виртуальными функциями (станет дороже в размере и скорости кода). Но .. пусть этим Алекс сам занимается .. мне - лениво. :)
 
Изменено:

kalobyte

★★★✩✩✩✩
1 Янв 2020
726
146
ну я решил накинуть картинок для представления образа

вот еще картинка


все это никто в своих уроках не рассказывает
везде только и видиш как помигать светодиодом или как подключить какой экран

это все равно что показали бы как забить гвоздь или как покрасить стены, но никто не говорит, как построить дом и чтобы он не развалился через год

Слушатель это обработчик прерывания
ну да, так будет более четче и ровнее, нежели в главном цикле опрашивать
 

Arhat109

★★★★✩✩✩
9 Июн 2019
473
203
Ну вот ещё пример нашей работы с моторами ведущих колес спортивной тележки (Vmax ~3м/сек по прямой!) в стиле конечного автомата Времени.
Мотор обладает собственной инерцией и управлять им непрерывно - в общем случае бесполезно. Редуктор, колесо, масса гоночной машинки, хоть и небольшая, но все равно вносят свою лепту в инерционность управления. Кроме этого ШИМ Ардуино крутится с частотой ок. 500гц, что ограничивает ещё время управления. Менять ШИМ чаще чем его период - просто "нонсенс".

Писано также "снизу-вверх", шаги написания кода тоже пронумерованы (этот текст не тестил, только как пример):
C:
/**
* Модуль работы с моторами и драйвером к ним "Ардуино как Лего".
* Часть 3. Конечный автомат времени управления мотором или группой моторов "по расписанию"
*
* Функция ABS-торможения рассчитана на нашу тележку по линии. Параметры - подобраны экспериментально.
* Исключительно как пример .. нам над было тормозить без потери времени .. :)
* Писать похожее СНИЗУ-ВВЕРХ по номерам! :)
*/

#include "ka.h"
#include "motorsGrp.h"

#ifdef __cplusplus
  extern "C" {
#endif

// Шаг 4. Создаем нужные глобалы для команд:
GrpMotors * mTimerGroup  = 0; // Группа задается при активации автомата времени!
int8_t     mTimerRepeat = 0;     // останов если не задано кол-во повторов ABS торможения

// Шаг 3. "Команды" автомата: "выключить", "включить"
int8_t mTimerStop(KaTimeControl *_ka)
{
  if( mTimerRepeat <= 0 || mTimerGroup == 0 ){ return 0; } // false нечем управлять..

  grpStopMotors(mTimerGroup);
  mTimerRepeat--;

  return mTimerRepeat? 1 : 0; // повторы закончились? останов автомата..
}

int8_t mTimerBack(KaTimeControl *_ka)
{
  grpSetSpeed(mTimerGroup, -160); // крутим назад, подбираем тут
  grpRunMotors(mTimerGroup);
  return 1; // успешно
}

int8_t mTimerMove(KaTimeControl *_ka)
{
  grpRunMotors(mTimerGroup);
  return 1; // успешно
}

// Шаг 2. Создаем "программу" для управления группой (ABS-торможение: выкл - назад - выкл)
KaTimeStep mTimerProg[] /*PROGMEM*/ = {
// поля:     command, timeout, nextOk,nextNo
       { mTimerMove,       4,    0,   0}, //  0: управлять согласно скоростям моторов в группе каждые 4мсек остаемся на этом шаге.
       { mTimerStop,      15,     2,   0}, //  1: выключить на 15мсек
       { mTimerBack,      15,     1,   0}, //  2: включить на 15мсек, повторить цикл
};

// Шаг 1. Определяем глобальную структуру для КА-времени управления группой (старт - выключен!):
KaTimeControl mTimer = {
// поля: started_at, timeout, nextState, table
              0,        0,        0,    mTimerProg
};

#ifdef __cplusplus
  }
#endif
Тут:
состояние 0 -- управление моторами по вычисленным скоростям в программе с периодом управления 4 миллисекунды;
сост 1 -- режим ABS торможения, стадия "мотор выключен" на 15мсек;
соответственно состояние 2 -- он же вторая часть: мотор крутим обратно в течении 15мсек.

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

P.S. Подправил некоторые косяки в коде .. код переписан под групповое управление моторами после чего не тестировался ещё .. не было случая.
 
Изменено:

Arhat109

★★★★✩✩✩
9 Июн 2019
473
203
Ну и ещё, по поводу "как построить дом". Взято из реальной соревновательной задачи по Wordskill - фактически написание программы пожарно-охранной сигнализации за 4 часа соревновательного времени.

Там всего около 15 листов ТЗ .. выдержки:
"Датчик пожара. При его срабатывании надо включить "сирену" с тональностью 1кГц и 2кГц длительностью по 1 секунде каждая. Сирена выключается или по отсутствию срабатывания датчика или по кнопке охранника"
..
"Датчик газа. При его срабатывании надо .. ага, включить сирену с тональностью 2кГц и дительностью 0.5сек и 1кГц длительностью 1.5сек. Сирена выключается при нажатии кнопки охранника или по отсутствию срабатывания датчика."
..
"Датчик препятствия состоит из двух датчик на входе и подсчитывает количество зашедших и вышедших посетителей. Охранная система включается только в случае отсутствия посетителей в помещении по кнопке охранника"
..

Читаем "ТЗ" и .. строим конечные автоматы на каждую задачу. Пример включения "оралки" датчиком пожара - приводил выше. Остальное - аналогично по сути. Код замера состояния всех датчиков может быть и единым блоком, все управление конечными автоматами сводится или к включению "флага срабатывания" как выше или к принудительному переводу КА в требуемое состояние, ну или .. с проверкой "можно ли перевести КА в нужное состояние?" :)

4 часа на задание - жесть конечно. Как сказали организаторы - это специально, чтобы никто не успел выполнить задание целиком. Так проще сравнивать кто сколько успел и за каждый пункт ТЗ начисляются фиксированные баллы - считать проще. :)
Зная такую библиотеку, можно решить всё ТЗ за примерно 12-16 часов...

P.S. Несколько отдельно про реализацию работы с устройством вывода средствами конечных автоматов.

В том задании есть такие пункты:
а) вывод часов на экран LCD1602. Двоеточие между часами и минутами должно моргать с периодом 1 сек. (0.5сек выкл + 0.5сек вкл) если часы не находятся в режиме "настройки". Режим настройки - это ввод времени часов и минут с клавиатуры 16х16 .. в режиме настройки должна моргать текущая набираемая цифра часов или минут..
б) В нижней строке, в случае срабатывания датчика должно выводится "Alarm! Fire detected." где "Fire" отражает сработавший датчик (Smoke, Perimeter - для датчиков препятствий в/из помещения и т.д.).

То есть конечный автомат первой части должен периодически выводить на экран в заданной позиции или пробел или двоеточие или цифру.
А второй частью, надо вывести некий текст, который собирается из константных строк "до" и "после" и текста, соответствующего событию ..
Но, библиотека вывода на LCD1602 она как-бы едина и ни разу не в стиле КА..

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

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

Arhat109

★★★★✩✩✩
9 Июн 2019
473
203
В целом, подход решения задач управления чем-либо в стиле "конечные автоматы" - это разделение ТЗ на независимые участки, поиск и формирование состояний каждого блока (аппаратуры) и .. в общем-то ВСЁ. Остальное становится тривиальным. :)
 

kalobyte

★★★✩✩✩✩
1 Янв 2020
726
146
кстати вспомнил еще про конечные автоматы
собственно это больше тема относится к программированию плк и есть один крутой мужик из этой области, который запилил как раз прогу, где быстро можно накидать конечный автомат без строчки кода

вот реализация реального проекта самим автором с 18:37
 

Arhat109

★★★★✩✩✩
9 Июн 2019
473
203
Решил разобрать тут вопрос соседней темы про "фоторезистор, пищалку и игралку", т.к. там появился хоть и примитивный, но все-таки полноценный автомат Милли.
C:
/**
* Фоторезистор как конечный автомат Милли, состояния:
* 1. "превышен порог освещенности" -- включаем "пищалку" (выключится сама), выключаем "игралку"
* 2. "освещенность меньше или равна порогу" -- включаем "игралку", если ещё не включена.
*/
#include "arhat/ka.h"

// 4 шаг: функции автомата "игралка" (тут можно через #define)
// mps.stop(), mp3.play(file) -- тут условно, как библиотечные вызовы..

int8_t kaPlayerState = 0; // переменная-состояние КА "игралка" как глобал..

#define kaPlayerStop()     do{ kaPlayerState=0; mp3.stop(); }while(0)
#define kaPlayerPlay(file) do{ kaPlayerState=1; mp3.play(file); }while(0)

// 3 шаг: создаем функции слушателя и исполнителей автомата:
// константа нашего "порога срабатывания" (можно и в переменную, тогда его можно будет менять по ходу пьессы)
#define KA_PHOTOREZ_LEVEL 512

// 3.1 Слушатель: вариант с сохранением события в автомате (можно тупо возращать код события)
KaListener listenerPhotoRez(KaControl* _ka)
{
    // тут производим "замер" согласно конструкции нашего фоторезистора..
    int photoVal = 0; // вместо 0 втыкаем возврат результата замера ..

    if( photoVal > KA_PHOTOREZ_LEVEL ){
      _ka.event = 1; // произошло событие превышения порога освещенности
    }else{
      _ka.event = 2; // произошло событие освещение ниже или равно порогу
    }
}
/** типа такого (замер - чтение АЦП с ноги фоторезистора:
* { return adcRead(pinPhotoRes)>KA_PHOTOREZ_LEVEL? 1 : 2; }
*/

// 3.2 "Исполнитель превышения порога":
KaControl highPhotoRez( KaControl* _ka)
{
  if( _ka.state != 1 ){                 // если пришли из другого состояния (1 раз!)
    if( kaPlayerState == PLAYER_PLAYING ){
        kaPlayerStop();                 // "игралка" играла файл - выключить!
    }
    tone(/* с параметрами для пищалки */);
  }
  return 1;                             // в любом раскладе возвращаем текущее состояние..
}

// 3.3 "Исполнитель освещенность ниже порога":
KaControl lowPhotoRez( KaControl* _ka)
{
  if( _ka.state != 2 ){                 // если пришли из другого состояния (1 раз!)
    if( kaPlayerState == PLAYER_STOPPED ){
        kaPlayerPlay(/*файл?*/);        // "игралка" - играй!
    }
  }
  return 2;                             // в любом раскладе возвращаем текущее состояние..
}

// 3.4. Пустышка, если оно вообще надо ..
KaControl emptyPhotoRez( KaControl* _ka){ return 0; }

// 2.5 шаг: создаем таблицу строк состояний (выделено из шага 2)
// !автомат не меняет своего состояния исполнителями ни при удачном ни при ошибочном завершении!
#define KA_PHOTOREZ_STATES 3
KaMilliStep photoRowsControl[KA_PHOTOREZ_STATES] = {
    {0, emptyPhotoRez, 0, 0}, // состояние 0 -- стартовое, ничего не знаю, замера ещё нет
    {1, highPhotoRes,  1, 1}, // состояние 1 -- "превышен порог"
    {2, lowPhotoRez,   2, 2}  // состояние 2 -- "равно или ниже порога"
};

// 2 шаг: определяем таблицу состояний (там всего 3 состояния):
KaMilliRow photoTableControl = {
    KA_PHOTOREZ_STATES, photoRowsControl
};

// 1 шаг: создаем структуру КА "фоторезистор" как автомат Милли (общий исполнитель типовой!):
KaMilliControl kaPhotoRez = {
    // общая подструктура .base (listener, command, state, event):
    {listenerPhotoRez, _kaMilliDo, 0, 0},
    // таблица состояний этого КА .table:
    photoTableControl
};

// шаг 5 -- как это все в loop():

void loop(){
  // сам вызовет "слушателя", сам вызовет главного исполнителя, который посмотрит событие
  // и вызовет соотв. "строчного исполнителя события" из таблицы состояний.
  // для расширения кода достаточно добавлять события (строчки) в табличку и создавать к ним новых исполнителей.
  kaRun( kaPhotoRez );
  // остальной код .. если нужен .. :)
}
Исключительно как пример применения тутошнего "автомата Милли". Забавно, что чтобы оно стало именно Милли, пришлось в исполнителей вставить код проверки измененности состояний .. новое или повтор?

P.S. В целом, фоторезистор есть, пищалка и игралка тоже .. надо будет потестить .. писалось просто "в лоб по ТЗ" без какой либо компиляции... исключительно как пошаговый пример.
 

Arhat109

★★★★✩✩✩
9 Июн 2019
473
203
P.P.S. Н-да, код конечно не компилируется, но это мелочи .. ;)

Хуже другое: автомат kaRun() в лупе вызывается "при каждом проходе" и, если в лупе ничего нет или мало, то будет вызываться достаточно часто, что, скорее всего, по ТЗ не требуется. Думаю достаточно проверять освещенность скажем .. каждую секунду.
Как вариант:
C:
void loop(){
    everyMillis(1000, { kaRun( ptrKaControl(&kaPhotoRez)); }); // фоторезистор (и все обработки его собюытий!) отрабатывают раз в 1с...
}
 

Arhat109

★★★★✩✩✩
9 Июн 2019
473
203
Моего там никода и не было.. Это обязательно, "иметь хаб на гитхабе"? ;)
 
  • Ахах! +1
Реакции: PiratFox