Конечный автомат php. Конечный автомат: теория и реализация

В статье рассмотрены простые конечные автоматы и их реализация на C++ с помощью switch-конструкций, таблиц времени исполнения и библиотеки Boost Statechart .

Введение

Грубо говоря, конечный автомат (Finite State Machine), глазами пользователя – это черный ящик, в который можно что-то передать и что-то оттуда получить. Это очень удобная абстракция, которая позволяет скрыть сложный алгоритм, кроме того конечные автоматы очень эффективны.

Конечные автоматы изображают в виде диаграмм состоящих из состояний и переходов. Поясню на простом примере:

Как вы наверное догадались – это диаграмма состояний лампочки. Начальное состояние обозначается черным кружком, переходы стрелками, некоторые стрелки подписаны – это события после которых автомат переходит в другое состояние. Итак, сразу из начального состояния, мы попадаем в состояние Light Off – лампа не горит. Если нажать кнопку, то автомат изменит свое состояние и перейдет по стрелке помеченной Push Button , в состояние Light On – лампа горит. Перейти из этого состояния можно опять же по стрелке, после нажатия кнопки, в состояние Light Off .

Также широко используются таблицы переходов:

Практическое применение автоматов

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

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

Для примера мы реализуем автомат для подсчета чисел и слов в тексте. Для начала договоримся, что числом будет считаться последовательность цифр от 0 до 9 произвольной длины, окруженная пробельными символами (пробел, табуляция, перевод строки). Словом будет считаться последовательность произвольной длины состоящая из букв и также окруженная пробельными символами.

Рассмотрим диаграмму:

Из начального состояния мы попадаем в состояние Start . Проверяем текущий символ, и если он является буквой, то переходим в состояние Word по стрелке помеченной как Letter . Это состояние предполагает, что в данный момент мы рассматриваем слово, анализ дальнейших символов, либо подтвердит данное предположение, либо опровергнет. Итак, рассматриваем следующий символ, если он является буквой, то состояние не изменяется (обратите внимание на круговую стрелку помеченную как Letter ). Если символ не является буквой, а соответствует пробельному символу, то это означает, что предположение было верным и мы обнаружили слово (делаем переход по стрелке Space в состояние Start ). Если символ не является, ни буквой, ни пробелом, значит мы ошиблись в предположении и последовательность которую мы рассматриваем, не является словом (переходим по стрелке Unknown в состояние Skip ).

В состоянии Skip мы находимся до тех пор, пока не будет встречен пробельный символ. После того как пробел был обнаружен мы переходим по стрелке Space в состояние Start . Это нужно, чтобы полностью пропустить строку не соответствующую нашему шаблону поиска.

После перехода в состояние Start , поиск цикл повторяется с начала. Ветвь с распознаванием чисел аналогична ветви распознавания слов.

Автомат с использованием switch-инструкций

Первое – возможные состояния:

После чего мы итерируем по строке подсовывая автомату текущий символ. Сам автомат представляет собой инструкцию switch сначала выполняющей переход в секцию текущего состояния. Внутри секции есть конструкция if-else, которая в зависимости от события (пришедшего символа) изменяет текущее состояние:

const size_t length = text.length () ; for (size_t i = 0 ; i ! = length; ++ i) { const char current = text[ i] ; switch (state) { case State_Start: if (std:: isdigit (current) ) { state = State_Number; } else if (std:: isalpha (current) ) { state = State_Word; } break ; case State_Number: if (std:: isspace (current) ) {

Здесь смотрим на диаграмму – текущее состояние Number , событие Space (встречен пробельный символ), а значит найдено число:

FoundNumber() ; state = State_Start; } else if (! std:: isdigit (current) ) { state = State_Skip; } break ; case State_Word: if (std:: isspace (current) ) { FoundWord() ; state = State_Start; } else if (! std:: isalpha (current) ) { state = State_Skip; } break ; case State_Skip: if (std:: isspace (current) ) { state = State_Start; } break ; } }

Итог:

Высокая эффективность

Простота реализации для простых алгоритмов

— Тяжело поддерживать

Интерпретация времени выполнения

Идея данного подхода проста – надо создать таблицу переходов, заполнить ее, и потом, при наступлении события, найди в таблице следующее состояние и сделать переход:

enum Events { Event_Space, Event_Digit, Event_Letter, Event_Unknown } ; void ProcessEvent(Events event) ; ... struct Transition { States BaseState_; Events Event_; States TargetState_; } ; void AddTransition(States fromState, Events event, States toState) ; ... typedef std:: vector < transition> TransitionTable; TransitionTable Transitions_; States CurrentState_;

Заполнение таблицы:

AddTransition(State_Start, Event_Digit, State_Number) ; AddTransition(State_Start, Event_Letter, State_Word) ; AddTransition(State_Number, Event_Space, State_Start) ; AddTransition(State_Number, Event_Letter, State_Skip) ; AddTransition(State_Number, Event_Unknown, State_Skip) ; AddTransition(State_Word, Event_Space, State_Start) ; AddTransition(State_Word, Event_Digit, State_Skip) ; AddTransition(State_Word, Event_Unknown, State_Skip) ; AddTransition(State_Skip, Event_Space, State_Start) ;

Получилось довольно наглядно. Платой за наглядность будет более медленная работа автомата, что впрочем, часто не имеет значения.

Чтобы автомат, при наступлении некоторых событий, мог оповестить некоторый код, можно добавить в структуру с информацией о переходе (Transition ) указатель на функцию (Action ), которая будет вызываться:

typedef void (DynamicMachine:: * Action) () ; struct Transition { States BaseState_; Events Event_; States TargetState_; Action Action_; } ; ... void AddTransition(States fromState, Events event, States toState, Action action) ; ... AddTransition (State_Number, Event_Space, State_Start, & DynamicMachine:: FoundNumber ) ;

Итог:

Гибкость и наглядность

Проще поддерживать

— Меньшая производительность по сравнению с switch-блоками

Интерпретация времени исполнения. Оптимизация по скорости

Можно ли объединить наглядность со скоростью? Сделать автомат настолько же эффективным, как автомат на switch-блоках вряд ли удастся, но сократить разрыв можно. Для этого надо из таблицы, построить матрицу, такую, чтобы для получения информации о переходе не выполнять поиск, а сделать выборку по номеру состояния и события:

Достигается результат, за счет расхода памяти.

Итог:

Гибкость, наглядность

Эффективен

— Расход памяти (скорее всего несущественно)

Boost Statechart

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

Итак, сначала определим события:

namespace Events { struct Digit : boost:: statechart :: event < Digit> { } ; struct Letter : boost:: statechart :: event < Letter> { } ; struct Space : boost:: statechart :: event < Space> { } ; struct Unknown : boost:: statechart :: event < Unknown> { } ; }

Саму машину (обратите внимание, что второй параметр шаблона – начальное состояние):

struct Machine : boost:: statechart :: state_machine < Machine, States:: Start > { } ;

И собственно сами состояния. Внутри состояний требуется определить тип описывающий переходы (reactions ), а если переходов несколько, то перечислить их в списке типов boost::mpl::list . Второй параметр шаблона simple_state – тип описывающий машину. Переходы описываются параметрами шаблона transition, парой Событие — Следующее состояние :

namespace States { struct Start : boost:: statechart :: simple_state < Start, Machine> < boost:: statechart :: transition < Events:: Digit , States:: Number >< Events:: Letter , States:: Word > > reactions; } ; struct Number : boost:: statechart :: simple_state < Number, Machine> { typedef boost:: mpl :: list < boost:: statechart :: transition < Events:: Space , States:: Start > , boost:: statechart :: transition < Events:: Letter , States:: Skip > , boost:: statechart :: transition < Events:: Unknown , States:: Skip > > reactions; } ; struct Word : boost:: statechart :: simple_state < Word, Machine> { typedef boost:: mpl :: list < boost:: statechart :: transition < Events:: Space , States:: Start > , boost:: statechart :: transition < Events:: Digit , States:: Skip > , boost:: statechart :: transition < Events:: Unknown , States:: Skip > > reactions; } ; struct Skip : boost:: statechart :: simple_state < Skip, Machine> { typedef boost:: statechart :: transition < Events:: Space , States:: Start > reactions; } ; }

Машина построена, осталось только инициализировать ее и можно использовать:

Machine machine; machine.initiate () ; ... machine .process_event (Events:: Space () ) ;

Итог:

Гибкость, расширяемость

— Эффективность

Быстродействие

Я написал тестовую программу для проверки быстродействия построенных автоматов. Через автоматы я прогнал текст размером ~17 Мб. Вот результаты прогона:

Loading text Text length: 17605548 bytes 0.19 s Running BoostMachine Words: 998002, numbers: 6816 0.73 s Running DynamicMachine Words: 998002, numbers: 6816 0.56 s Running FastDynamicMachine Words: 998002, numbers: 6816 0.29 s Running SimpleMachine Words: 998002, numbers: 6816 0.20 s

Что осталось не рассмотренным

Неохваченными остались еще несколько реализаций конечных автоматов (рекомендую http://www.rsdn.ru/article/alg/Static_Finite_State_Machine.xml и http://www.rsdn.ru/article/alg/FiniteStateMachine.xml), генераторы строящие автоматы из описаний, библиотека Meta State Machine из Boost версии 1.44.0, а также формальные описания конечных автоматов. Со всем перечисленным любопытный читатель может ознакомиться самостоятельно.

Теория конечных автоматов

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

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

· работа конечного автомата отличается высоким быстродействием.

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

· существует ряд теорем и алгоритмов, позволяющих конструировать и упрощать конечные автоматы.

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

Определение: Конечный автомат - это формальная система, которая задается с помощью следующих объектов :

· конечным множеством входных символов;

· конечным множеством состояний;

· функцией переходов, которая каждой паре (текущее состояние, входной символ) ставит в соответствие новое состояние;

· начальным состоянием;

· подмножеством состояний, выделенных в качестве допускающих или заключительных.

Пример. Разберем работу контроллера, проверяющего четно или нечетно число единиц в произвольной цепочке, состоящей из нулей и единиц. Допустим, что соответствующий конечный автомат должен « допускать» все цепочки, содержащие нечетное число единиц и « отвергать » цепочки с четным их числом. Назовем этот автомат « контроллером четности ». Считаем, что символы, отличные от 0 и 1 нельзя подавать на вход автомата. Итак, входной алфавит контроллера есть множество {0, 1} . Считаем, что в конкретный момент времени конечный автомат имеет дело лишь с одним входным символом, а информацию о предыдущих символах входной цепочки сохраняет с помощью конечного множества состояний. В качестве множества состояний будем рассматривать множество { чет, нечет }, одно из этих состояний должно быть выбрано в качестве начального. Пусть им будет состояние {чет}, поскольку на первом шаге число прочитанных единиц равно нулю, а нуль есть четное число. При чтении очередного входного символа состояние автомата либо меняется, либо сохраняется прежним причем новое его состояние зависит от входного символа и текущего состояния. Такое изменение состояния называется переходом.



Работа автомата может описываться математически функцией переходов вида d(Sтек., x) = Sнов. Иначе это можно записать следующим образом:

d (чет., 0) = чет. d (чет., 1) = нечет.

d (нечет., 0) = нечет. d (нечет., 1) = чет.

Контроллер имеет единственное допускающее состояние НЕЧЕТ, а ЧЕТ есть « отвергающее » состояние. Отразим последовательность переходов автомата при подаче на его вход цепочки 1101.

ЧЕТ ® НЕЧЕТ ® ЧЕТ® ЧЕТ® НЕЧЕТ

Таблица переходов такого автомата имеет вид:

чет чет нечет
нечет нечет чет

Определение. Конечный автомат - это формальная система

S = { A, Q, d, l,V },

объекты которой следующие:

* A - конечное множество входных символов (множество

терминалов);

* Q - конечное множество внутренних состояний автомата

(множество нетерминалов);

* V - конечное множество выходных символов (выходной алфавит);

* d - функция переходов, для которой характерно A ´ Q ® Q;

* l - функция выходов, определяющая отображение вида.

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

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

Приведём несколько примеров конечных автоматов.

Пример 1. Элемент задержки (элемент памяти).

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

Предположим, что входной и, следовательно, выходной алфавит есть X ={0, 1}, Y ={0, 1}. Тогда Q ={0, 1}. Под состоянием элемента задержки в момент времени t понимается содержание элемента памяти в данный момент. Таким образом q (t )= X (t 1), a Y (t )= q (t )=X (t 1).

Зададим элемент задержки таблицей, где а 1 =0, а 2 =1, q 1 =0, q 2 =1,

(a 1 , q 1)= (0, 0)=0, (a 1 , q 1)= (0, 0)=0;

(a 1 , q 2)= (0, 1)=0, (a 1 , q 2)= (0, 1)=1;

(a 2 , q 1)= (1, 0)=1, (a 2 , q 1)= (1, 0)=0;

(a 2 , q 2)= (1, 1)=1, (a 2 , q 2)= (1, 1)=1;

q

a

=0, =0

=0, =1

=1, =0

=1, =1

Диаграмма Мура изображена на рис. 3

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

Обратим внимание на то, что т=п=р =2. Тогда k =r =s =1, и поэтому элемент задержки задается двумя функциями и . Таблица истинности этих функций содержит 2 k + r =2 2 =4 строки и k +r +r +s =4 столбца:

x

z

Пример 2. Двоичный сумматор последовательного действия.

Данный сумматор последовательного действия представляет собой устройство, осуществляющее сложение двух чисел в двоичной системе исчисления. На входы сумматора подаются числа х 1 и x 2 , начиная с младших разрядов. На выходе формируется последовательность, соответствующая записи числа х 1 +x 2 в двоичной системе исчисления (рис. 4).

Входной и выходной алфавиты определены однозначно: X ={00; 01; 10; 11}, Y ={0,1}. Множество состояний определяется значением пере­носа при сложении соответствующих разрядов чисел х 1 и x 2 . Если при сложении некоторых разрядов образовался перенос, то будем считать, что сумматор перешел в состояние q 1 . При отсутствии переноса будем считать, что сумматор находится в состоянии q 0 .

Сумматор задается таблицей.

q

a

q 0

q 1

q 0 , 0

q 0 , 1

q 0 , 1

q 1 , 0

q 0 , 1

q 1 , 0

q 1 , 0

q 1 , 1

Диаграмма Мура сумматора последовательного действия изображена на рис. 5.

Заметим, что входные и выходные символы уже закодированы. Состояния закодируем следующим образом: (q 0)=0, (q 1)=1. Поэтому сумматор последовательного действия задается двумя булевыми функциями, таблица истинности которых следующая:

x 1

x 2

z

Пример 3. Схема сравнения на равенство.

Схема сравнения на равенств представляет собой устройство, срав­нивающее два числа х 1 и x 2 , заданное в двоичной системе исчисления. Это устройство работает следующим образом. На вход устройства после­довательно, начиная со старших, подаются разряды чисел х 1 и x 2 . Эти разряды сравниваются. При совпадении разрядов на выходе схемы формируется выходной сигнал 0, в противном случае на выходе появляется сигнал 1. Ясно, что появление 1 в выходной последовательности означает, что сравниваемые числа х 1 и x 2 различны. Если же выходная последовательность является нулевой и ее длина совпадает с числом разрядов сравниваемых чисел, то х 1 и x 2 .

Для этого автомата X ={00, 01, 10, 11}; Y ={0,1}.

Функционирование схемы определяется двумя состояниями. Состояние q 0 соответствует равенству сравниваемых в данный момент разрядов. При этом автомат остается в этом же состоянии. Если в следующий момент сравниваемые разряды будут различны, то автомат перейдет в новое состояние q 1 и в нем остается, так как это означает, что числа различны. Таким образом, схему сравнения можно задать таблицей:

q

x

q 0

q 1

q 0 , 0

q 1 , 1

q 1 , 1

q 1 , 1

q 1 , 1

q 1 , 1

q 0 , 0

q 1 , 1

Диаграмма Мура схемы сравнения на равенство изображена на рис. 6.

Кодирование состояний произведем следующим образом: (q 0)=0, (q 1)=1. Автомат будет задаваться двумя функциями.

x 1

x 2

z

Пример 4. Схема сравнения на неравенство.

Схема сравнения на неравенство представляет собой устройство, позволяющее выяснить, равны ли сравниваемые х 1 и x 2 , и если они не равны, выяснить, какое из них больше другого. Это устройство имеет два входа и два выхода. Выходные сигналы y 1 (t ) и y 2 (t )определяются по следующим правилам:

y 1 (t )=y 2 (t )=0, если x 1 (t )=x 2 (t );

y 1 (t )=1, y 2 (t )=0, если x 1 (t )>x 2 (t ), то есть x 1 (t )=1, x 2 (t )=0;

y 1 (t )=0, y 2 (t )=1, если x 1 (t )<x 2 (t ), то есть x 1 (t )=0, x 2 (t )=1.

Таким образом, при подаче на вход схемы сравнения на неравенство чисел x 1 =x 1 (l)…x 1 (t ) и x 2 =x 2 (l)…x 2 (t )последовательно сравниваются разряды этих чисел, начиная со старших. Выходные сигналы формулируются согласно вышеуказанным правилам. При этом, если выходная последовательность состоит из нулевых пар, то x 1 =x 2 . Если первая, отличная от нулевой, пара имеет вид , () то x 1 >x 2 (x 1 <x 2).

Из описания схемы следует, что

X ={00, 01, 10, 11}, Y ={00, 01, 10}.

Состояние схемы определяется следующим образом. Предположим, что в начальный момент времени t =1 автомат находится в состоянии q 1 . Если сравниваемые разряды чисел х 1 и х 2 совпадают, то автомат остается в этом состоянии. Заметим, что на выходе при этом появится сигнал 00. Если же разряд числа х 1 будет меньше (больше) соответствующего разряда числа х 2 , то автомат перейдет в состояние q 2 (q 3). При этом на выходе появится сигнал 01 (10). В дальнейшем при подаче оставшихся разрядов чисел х 1 и х 2 на входы автомата автомат будет оставаться в состоянии q 2 (q 3) и вырабатывать выходной символ 10 (01). Из вышеизложенного следует, что схему сравнения на неравенство можно задать таблицей:

q

x

q 1

q 2

q 3

q 1 , 00

q 2 , 01

q 3 , 10

q 2 , 01

q 2 , 01

q 3 , 10

q 3 , 10

q 2 , 01

q 3 , 10

q 1 , 00

q 2 , 01

q 3 , 10

Соответствующая диаграмма Мура изображена на рис. 7.

Входной и выходной алфавиты здесь уже закодированы. Состояния q 1 , q 2 и q 3 закодируем: 1 (q 1)=00, (q 2)=01, (q 3)=10.

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

x 1

x 2

z 1

z 2

В таблице символами * отмечены наборы переменных x 1 , x 2 , z 1 , z 2 , на которых функции 1 , 2 , 1 , 2 не определены. Положим значения функций 1 , 2 , 1 , 2 на этих наборах равными 1.

Теория автоматов

Определение автомата и его разновидности. Таблицы и графы переходов и выходов. Подавтоматы. Теорема о приведенном автомате

Операции с автоматами

Преобразование автомата Мили в автомат Мура и автомата Мура в автомат Мили. Эквивалентность автоматов. Различимость состояний автоматов. Минимизация автоматов. Синтез автоматов. Распознающие автоматы

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

1) техническом,

2) математическом.

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

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

Частным и важным случаем автомата выступает цифровой автомат (ЦА), в котором полностью автоматизированы процессы приема, преобразования, хранения и выдачи цифровой информации.

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

ЦА считается конечным, если конечны множества входных сигналов X, состояний S и выходных сигналов Y. Конечный автомат можно поставить в соответствие такому устройству, как компьютер. Компьютер перерабатывает поступающие входные данные в выходные данные (результат), но этот результат соответствует не только входным данным, но и текущему состоянию компьютера, т.е. тем данным, которые хранятся в памяти компьютера, например, результаты предыдущих вычислений, программы вычислений.

Работа ЦА осуществляется в автоматном времени, определяемом числом периодов поступления входных сигналов.

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

Слова входного языка можно представить символами множества X={x 1 ,x 2 ,...x n }, который называют входным алфавитом , а слова выходного языка - символами множества Y={y 1 ,y 2 ,...y p }, который называют выходным алфавитом . Множество состояний автомата S={s 1 ,s 2 ,...s m } называют алфавитом состояний .


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

Работу абстрактного автомата следует рассматривать применительно к конкретным интервалам времени, т.к. каждому интервалу дискретности t будет соответствовать свой выходной сигнал y(t). Следовательно, функционирование автомата рассматривается через дискретные интервалы времени конечной продолжительности. В абстрактной теории цифровых автоматов считается, что входные сигналы воздействуют на синхронный автомат в момент начала каждого i -того интервала (кванта) времени, выделенного соответствующим синхроимпульсом (тактом), а изменение внутренних состояний автомата происходит в интервалы времени между смежными синхроимпульсами, когда нет воздействия входных сигналов.

Понятие «состояние» используют для того, чтобы установить функциональную зависимость генерируемых автоматом символов и/или слов выходного языка от символов и/или слов входного языка при реализации автоматом заданного алгоритма. Для каждого состояния автомата sÎS и для каждого символа xÎX в момент дискретного времени [t] на выходе устройства генерируется символ yÎY. Эту зависимость определяет функция выходов автомата j. Для каждого текущего состояния автомата sÎS и для каждого символа xÎX в момент дискретного времени [t] автомат переходит в очередное состояние sÎS. Эту зависимость определяет функция переходов автомата y. Функционирование автомата состоит в порождении двух последовательностей: последовательности очередных состояний автомата (s 1[ s 2 s 3 ...) и последовательности выходных символов (y 1 y 2 y 3 ...), которые для последовательности символов (x 1 x 2 x 3 ...) разворачиваются в моменты дискретного времени t = 1,2,3,.... В прямоугольных скобках указывают моменты дискретного времени, которые называют иначе тактами, в круглых скобках - последовательности символов алфавитов X, Y и S.

Итак, математическая модель конечного автомата есть трехосновная алгебра, носителями которой являются три множества X, Y и S, а операциями - две функции j и y.

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

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

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

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

Программа под микроконтроллер не может просто закончить свое исполнение. Вот представьте себе, что вы включили плеер или магнитофон. Вы нажали кнопочку 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. Автоматное программирование.

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

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

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

Реализация многозадачности в 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.

Файл 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 #include //Стандартная библиотека включающая в себя прерывания #define LED1 0 #define LED2 1 #define LED3 2 #define LED4 3 #define Komparator ACSR //компаратор #define ViklKomparator 1<

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

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

#include "hal.h" void INIT_PEREF(void) { //Инициализация портов ввода-вывода //################################################################################### Komparator = ViklKomparator; //инициализация компаратора - выключение DDRD = 1<

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

Шаг 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<

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

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

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

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

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

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

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

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

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

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

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