Конечные автоматы, как программировать без запарок

Всем привет, меня зовут Владимир Васильев и вот наконец я созрел для написания новой статьи, которой спешу с вами дорогие читатели поделиться.

SWITCH - tekhnologiia ili konechny`e avtomaty`

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

Хочу заметить что стимулом написания этого поста послужил [urlspan]цикл статей о SWITCH-технологии[/urlspan] Владимира Татарчевского.  Цикл статей называется «Применение SWITCH-технологии при разработке прикладного программного обеспечения для микроконтроллеров» Так что в этом статье я постараюсь  по большей части привести  пример рабочего  кода и его описание.

Кстати я запланировал ряд статей посвященных программированию, в которых буду подробно рассматривать приемы программирования под микроконтроллеры АВР, [urlspan]не пропустите[/urlspan]…. Ну что ж поехали!


Содержание статьи


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

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

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

А что здесь такого? Все нормально — контроллер, тот что в недрах вашего плеера просто закончил выполнение своей программы. Вот видите неудобненько как-то получается.

Так вот отсюда мы делаем вывод, что программа под микроконтроллер просто не должна останавливаться. По сути своей она должна представлять собой бесконечный цикл — только в этом случае наш плеер работал бы правильно. Дальше я вам покажу  какие бывают конструкции программного кода под микроконтроллеры, это даже не конструкции а некоторые стили программирования.

Стили программирования.

«Стили программирования» —  звучит как-то непонятно, ну да ладно. Что я хочу этим сказать?Представим, что человек никогда до этого не занимался программированием, то есть вообще полный чайник.

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

Так вот эти стили и являются ступеньками ведущими от простого уровня к более сложному, но и в тоже время более эффективному.

Поначалу  я не задумывался о каких-то конструктивных особенностях программы. Я просто формировал логику программы — чертил блок-схему и писал код. От чего постоянно натыкался на грабли. Но  это было  первое время когда  я не парился и использовал стиль «простое зацикливание», затем стал применять прерывания, далее были автоматы и пошло поехало…

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

void main(void)
{
initial_AL(); //инициализация периферии
       while(1)
       {
        Leds_BLINK(); //функция светодиодной мигалки
        signal_on(); //функция включения сигнала
        signal_off(); //функция выключения сигнала
        l=button(); //переменная отвечающая за нажатие кнопок
            switch(l) //В зависимости от величины переменной выполняется то или иное действие
            {
             case 1: 
                   {
                    Deistvie1(); //Вместо функции может быть условный оператор 
                    Deistvie2(); //или еще несколько веток switch case
                    Deistvie3();
                    Deistvie4();
                    Deistvie5();
                   };
            case 2:
                   {
                    Deistvie6();
                    Deistvie7();
                    Deistvie8();
                    Deistvie9();
                    Deistvie10();
                   }; 
                     . . . . . . . .
             } 
         }
}

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

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

С таким подходом я написал несколько программок, они были не большие и вполне рабочие но наглядность оставляла желать лучшего . Чтобы добавить какое-то новое условие , приходилось перелопачивать весь код, потому, что все было завязано. Это порождало много ошибок и головной боли. Компилятор ругался как только мог, отлаживание такой программы превращалось в ад.

2. Цикл + прерывания.

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

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

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

3. Автоматное программирование .

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

Программа написанная в автоматном стиле похожа на переключатель, который в зависимости от условий переключается в то или иное состояние. Количество состояний программисту изначально известно.

perecliuchatel`

В грубом представлении это как выключатель освещения. Есть два состояния включено и выключено, и два условия включить и выключить. Ну а обо всем по порядку.

Реализация многозадачности в switch-технологии.

Микроконтроллер способен управлять нагрузкой, моргать светодиодами, отслеживать нажатие клавиш и многое другое. Но как все это делать одновременно ? Для решения этого вопроса существует множество решений. Самый простой из них я уже упоминал это использование прерываний.

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

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

В программах написанных по switch — технологии подобная «иллюзия» многозадачности получается благодаря системе обмена сообщений. Я написал «иллюзия», потому, что так и есть на самом деле, ведь программа физически не может в одно и тоже время выполнять различные участки кода. О системе обмена сообщениями я расскажу немного дальше.

Система обмена сообщениями.

Разрулить многочисленные процессы и создать иллюзию многозадачности можно используя систему обмена сообщениями.

Допустим нам нужна программа в которой идет переключение светодиода. Вот у  нас есть два автомата, назовем их LEDON -автомат ответственный за включение светодиода и автомат LEDOFF — автомат ответственный за выключение светодиода.

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

 При активации одного автомата  происходит зажжение светодиода, при активации другого  светодиод гасится. Рассмотрим небольшой пример :

int main(void)
{
   INIT_PEREF(); //инициализация периферии (светодиоды)
   InitGTimers(); //инициализация таймеров
   InitMessages(); //инициализация механизма обработки сообщений
   InitLEDON(); //инициализация автомата LEDON
   InitLEDOFF(); //инициализация автомата LEDOFF
   SendMessage(MSG_LEDON_ACTIVATE); //активируем автомат LEDON
   sei(); //Разрешаем прерывания 
//Главный цикл программы
     while(1)
     {
      ProcessLEDON(); //итерация автомата LEDON
      ProcessLEDOFF(); //итерация автомата LEDOFF
      ProcessMessages(); //обработка сообщений
      };
 }

В строках 3 -7 происходят различные инициализации поэтому нас это сейчас не особо интересует. А вот дальше происходит следующее: перед запуском главного цикла (while(1)), мы отправляем сообщение автомату

Блок схема передачи сообщения при переключения светолиода

SendMessage(MSG_LEDON_ACTIVATE)

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

Небольшое отступление:

Сообщение имеет три состояния. А именно состояние сообщение может быть неактивно, установлено но неактивно  и активное состояние.

Три состояния сообщения

 

Получается, что сообщение изначально было неактивно, когда мы отправили сообщение,  оно получило состояние «установлено но неактивно». И это дает нам следующее. При последовательном выполнении программы автомат LEDON сообщение не получает. Происходит холостая итерация автомата LEDON при котором сообщение просто не может быть принято. Так как  сообщение имеет состояние «установлено но неактивно»  программа продолжает свое выполнение.

После того как все автоматы в холостую протикают,  очередь доходит до функции ProcessMessages(). Эта функция всегда ставится в конце цикла, после выполнения всех итераций автоматов.  Функция ProcessMessages(), просто переводит сообщение из состояния «установлено но неактивно» в состояние «активно».

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

Хочу заметить, что сообщения которые имеют состояние «активно», при встрече с функцией ProcessMessages уничтожаются. Поэтому сообщение может быть принято только одним автоматом. Есть еще один тип сообщений — это широковещательные сообщения, но я их рассматривать не буду,  в статьяхТатарчевского они также хорошо освещены.

Таймеры

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

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

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

Алгоритм будет следующим:

Схема конечного автомата с таймером

Можно кликнуть чтобы увеличить

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

1. Входим в состояние посредством принятия сообщения.

2. Проверяем показания таймера/счетчика, если дотикало, то выполняем действие, иначе просто отправляем сообщение самому себе.

3. Отправляем сообщение следующему автомату.

4. Выход

В следующем входе все повторяется.

Программа по SWITCH-технологии. Три шага. 

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

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

Программа  будет у нас модульной и поэтому будет разбита на несколько файлов. Модули у нас будут следующие:

  • Модуль основного цикла программы содержит файлы leds_blink.c, HAL.c, HAL.h
  • Модуль таймеров содержит файлы timers.c, timers.h
  • Модуль обработки сообщений содержит файлы messages.c, messages.h
  • Модуль автомата 1 содержит файлы ledon.c, ledon.h
  • Модуль автомата 2  содержит файлы ledoff.c, ledoff.h

Шаг 1.

Создаем проект и сразу подключаем к нему файлы наших статичных модулей:  timers.c, timers.h, messages.c, messages.h.

Шаг 2.

Далее пишем модуль основного цикла программы .

Файл leds_blink.c модуля основного цикла прогарммы.

#include "hal.h"
#include "messages.h" //модуль обработки сообщений
#include "timers.h" //модуль таймеров
//Прерывания по таймеру
//############################################################################################
ISR(TIMER0_OVF_vect) // переход по вектору прерывания (переполнение таймера счетчика T0)
    {
     ProcessTimers(); //Обработчик прерываний от таймера
    }
 //###########################################################################################
int main(void)
{
 INIT_PEREF(); //инициализация переферии (светодиоды)
 InitGTimers(); //инициализация таймеров
 InitMessages(); //инициализация механизма обработки сообщений
 InitLEDON(); //инициализация автомата LEDON
 InitLEDOFF();
 StartGTimer(TIMER_SEK); //Запуск таймера
 SendMessage(MSG_LEDON_ACTIVATE); //активируем автомат FSM1
 sei(); //Разрешаем прерывания 
//Главный цикл программы
         while(1)
          {
           ProcessLEDON(); //итерация автомата LEDON
           ProcessLEDOFF();
           ProcessMessages(); //обработка сообщений
           };
 }

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

Со строчки  int main (void) можно сказать начинается основная программа. И начинается она с инициализации всего и вся.  Здесь инициализируем периферию, то есть задаем начальные значения портам ввода вывода компаратору и всему остальному содержимому контроллера. Все это делает функция INIT_PEREF, здесь ее запускаем, хотя основное ее тело находится в файле hal.c.

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

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

StartGTimer(TIMER_SEK); //Запуск таймера
SendMessage(MSG_LEDON_ACTIVATE); //активируем автомат FSM1

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

Hal.h — это заголовочный файл модуля основного цикла программы.

#ifndef HAL_h
#define HAL_h
#include <avr/io.h>
#include <avr/interrupt.h> //Стандартная библиотека включающая в себя прерывания
#define LED1 0
#define LED2 1
#define LED3 2
#define LED4 3
#define Komparator ACSR //компаратор
#define ViklKomparator 1<<ACD //выключение компаратора
#define TimerDel1024 1<<CS00|0<<CS01|1<<CS02 // Предделитель на 1024 , если 1 то нет предделителя
#define one_sek 4
/************************************************************
Если частота контроллера 1МГц то одна секунда равна 4 переполнениям
Если частота контроллера 8МГц то одна секунда равна 30 переполнений
***************************************************************/
//Инициализация периферии
void INIT_PEREF(void);
#endiе

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

А вот файл Hal.c — это уже исполняемый файл, и как я уже упоминал, в нем содержится различный инициализации периферии.

#include "hal.h"
void INIT_PEREF(void)
{
//Инициализация портов ввода-вывода
//###################################################################################
Komparator = ViklKomparator; //инициализация компаратора - выключение 
DDRD =  1<<LED1| //устанавливаем порт светодиодов на Выход
        1<<LED2| 
        1<<LED3| 
        1<<LED4; 
PORTD = 0<<LED1| //гасим все светодиоды 
        0<<LED2| //Погашен
        0<<LED3| //Погашен
        0<<LED4; //Погашен
//###################################################################################
//Инициализация аппаратного таймера
//###################################################################################
TCCR0 = TimerDel1024;
TIMSK = 1<<TOIE0; //0b100 разрешение прерывания по переполнению таймера/счетчика
}

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

Шаг 3. 

Нам осталось написать модули конечных автоматов, в нашем случае автомата LEDON и автомата LEDOFF. Для начала приведу текст программы автомата зажигающего светодиод файл ledon.c.

//файл ledon.c
#include "ledon.h"
#include "timers.h"
#include "messages.h"
unsigned char ledon_state; //переменная состояния
void InitLEDON(void)
 {
  ledon_state=0;
  //здесь можно выполнить инициализацию других 
  //переменных автомата при их наличии
 }
void ProcessLEDON(void)
{
    switch(ledon_state)
    {
     case 0: //неактивное состояние
            if(GetMessage(MSG_LEDON_ACTIVATE)) //если сообщение есть то оно будет принято 
             {                                 //и пойдет проверка таймера
               if(GetGTimer(TIMER_SEK)==one_sek) //если таймер засек 1сек то выполняем 
                { 
                 StopGTimer(TIMER_SEK); 
                 PORTD = 1<<LED1| //зажигаем 
                 0<<LED2| //Погашен
                 0<<LED3| //Погашен
                 0<<LED4; //Погашен
                 StartGTimer (TIMER_SEK);
                 SendMessage(MSG_LEDOFF_ACTIVATE); //активируем автомат FSM1
                 } 
                 else SendMessage(MSG_LEDON_ACTIVATE); // иначе отправляем сообщение себе
              };
               break;
       case 1: //активное состояние 
               break;
      }
}

 Здесь в первых строчках как всегда подключаются библиотеки и объявляются переменные. Далее у нас пошли уже функции, с которыми мы уже встречались. Это функция инициализации автомата  InitLEDON и функция уже самого обработчика автомата ProcessLEDON.

В теле обработчика уже происходит отработка функций из таймерного модуля и модуля сообщений.  А сама логика автомата выполнена на основе конструкции switch-case. И здесь можно заметить что обработчик  автомата можно также усложнить добавив несколько переключателей case.

Заголовочный файл для автомата будет еще проще :

//файл fsm1
#ifndef LEDON_h
#define LEDON_h
#include "hal.h"
void InitLEDON(void);
void ProcessLEDON(void);
#endif

Здесь подключаем связующий файл hal.h а также указываем прототипы функций.

Файл ответственный за выключение светодиода будет выглядеть практически также только в зеркальном отражении, так, что здесь я его выводить не буду — неохота 🙂

Все файлы проекта вы можете скачать вот по этой ссылке ====>>>[urlspan]ССЫЛКА[/urlspan].

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

Кстати я запланировал ряд интересных проектов которые будут особенно интересны, так  что обязательно [urlspan]подпишитесь на новые статьи[/urlspan]. Также я планирую делать рассылку дополнительных материалов,  поэтому многие уже подписываются через основную страницу сайта. Подписаться можно и здесь .

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

С н/п Владимир Васильев

Лучший способ сказать СПАСИБО автору это отправить донат!


 

Читайте также:

комментария 3

  1. Волька:

    Жуть!

  2. Leo:

    Здравствуйте. Вот я полный чайник. Прочитав Вашу статью несколько раз, вникая, уловил лишь смысл, но как применить на практике, так и не понял. Вроде бы да, так писать и наглядней и проще, но кажется еще сложнее, чем обычно. Откуда и зачем берутся подключаемые библиотеки ? Чем отличаются hal.h и hal.c и почему у них именно такие названия?
    На данный момент мне всё же проще разобраться в чужом «неделимом» комке кода и внести туда необходимые мне коррективы, чем писать программу с нуля. Хотя и чувствую, что по Вашим рекомендациям это легче, чем разбираться… Чего то не хватает мне для осознания простоты. Мозгов, наверное. Но Вам всё равно спасибо за труд

  3. Crypto:

    Учитывая возраст статьи не уверен что получу ответ, но автор уверен что не получиться так, что условие if(GetMessage(MSG_LEDON_ACTIVATE)) выполнится на второй итерации, а if(GetGTimer(TIMER_SEK)==one_sek) еще нет. А к тому моменту, когда таймер до тикает и if(GetGTimer(TIMER_SEK)==one_sek) станет истинным программа уже не сможет войти в if(GetMessage(MSG_LEDON_ACTIVATE)), так как менеджер сообщений его уже подчистит и if(GetMessage(MSG_LEDON_ACTIVATE)) останется в ложном состоянии.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *