Конечные автоматы играют очень важную роль при разработке прошивок ПЛИС. Все слышали о двух классических типах автоматов: автомат Мили и автомат Мура, которые были предложены ещё до эпохи ПЛИС. Однако специфика построения ПЛИС вносит свои коррективы и в процессе работы у меня сложился вполне определённый стиль описания автомата.
В литературе предлагаются различные реализации автоматов. Такая статья есть и на habrahabr: canny
Описание цифровых автоматов на VHDL
Однако при практической реализации на ПЛИС, тем более на высоких частотах такая реализация неудобна.
Предлагается следующий вариант:
Описание компонента:
entity ex_user is
port (
reset : in std_logic: -- 1 – сброс
clk : in std_logic; -- тактовая частота
a : in std_logic; -- вход автомата
q : out std_logic -- выход автомата
)
end ex_user;
Описание типа и сигналов:
type stp_type is ( s0, s1, s2, s3 );
signal stp : stp_type;
signal rstp0 : std_logic;
signal rstp : std_logic;
Синхронизация сброса:
rstp0 <= reset after 1 ns when rising_edge( clk );
rstp <= rstp0 after 1 ns when rising_edge( clk );
Собственно автомат, в данном случае – очень простой
pr_stp: process( clk ) begin
if( rising_edge( clk ) ) then
case( stp ) is
when s0 => -- это начальное значение
q <= ‘0’ after 1 ns;
if( a=’1’ ) then
stp <= s1 after 1 ns;
end if;
when s1 => -- что то делаем, например ждём a=0
if( a=’0’ ) then
stp <= s2 after 1 ns;
end if;
when s2 =>
q <= ‘1’ after 1 ns;
stp <= s0 after 1 ns;
end case;
--- А вот это сброс, он действует только на stp ---
if( rstp=’1’ ) then
stp <= s0 after 1 ns; -- действие только на stp
end if;
end if;
end process;
Особенности:
- Описание отдельного типа для сигнала состояния
- Синхронизация сигнала reset
- Reset действует только на сигнал состояния
- Все входные сигналы должны быть синхронны с тактовой частотой автомата
- Переходы и выходные сигналы описаны в одном процессе
- Наличие after 1 ns – для облегчения моделирования
- Может быть реализован как автомат Мура, так и автомат Мили
Здесь нет ничего революционного, всё это давно известно. Но всё вместе это как раз и образует стиль описания.
Более подробно про особенности описания.
В первую очередь это введение отдельного типа для сигнала состояния. В данном случае описан тип перечисление. Это даёт возможность синтезатору выбрать тип сигнала. Это может быть one-hot, может быть двоичный счётчик или счётчик Грея. Тип реализации может быть указан директивами синтезатора. Это отделяет описание от реализации. Если кому-то это не нравиться, то вполне возможно задание типа std_logic_vector и отдельных констант s0, s1 и т.д. Иногда мне высказывают претензии, что названия s0, s1 не информативны, и лучше бы использовать константы связанные по смыслу с конкретным состоянием. Так я тоже пытался делать, но в процессе разработки очень часто логика состояния меняется но название остаётся, а это только запутывает дело.
Синхронизация сигнала reset – очень часто reset является асинхронным относительно тактовой частоты автомата. Что бы не проверять откуда он идёт лучше всегда поставить два триггера. В любом случае это облегчит трассировку. Сброс действует только на сигнал состояния. Это также облегчает трассировку, особенно при большом количестве выходных сигналов, но это требует что бы в начальном состоянии все выходные сигналы были описаны.
Автомат является синхронной схемой поэтому все входные сигналы должны быть синхронный с тактовой частотой, здесь без вариантов. Требуется обязательно знать откуда приходит сигнал. Если это сигнал формируется на другой тактовой частоте, то в обязательном порядке требуется поставить два триггера (также как для сигнала reset).
То что переходы и выходные сигналы описаны в одном процессе это визуально облегчает процесс разработки и повышает наглядность. Если сделать автомат в двух (а то и в трёх процессах) как нам советуют учебники, то это сложно охватить взглядом. Хотя бы потому что большой автомат на одном экране компьютера не поместится.
Самым спорным утверждением является запись after 1 ns при присваивании сигнала. На одном из форумов меня очень сильно за это критиковали. А также писали, что по мере накопления опыта я избавлюсь от этой вредной привычки. Мой опыт показывает что это очень полезная привычка. Наличие такой записи позволяет быть уверенным что результаты моделирования и результаты работы в реальной аппаратуре будут одинаковыми. Всё дело в понятии дельта задержки.
Рассмотрим простую конструкцию языка:
clk2 <= clk1;
b <= a when rising_edge( clk1 );
c <= b when rising_edge( clk1 );
d <= b when rising_edge( clk2 );
Результат работы представлен на рисунке:
На диаграмме визуально видно, что сигналы тактовой частоты clk1 и clk2 совпадают, но на самом деле clk2 задержан относительно clk1 на величину дельта задержки.Сигнал c отстаёт от сигнала b на один такт. Это правильно. Но вот сигнал d должен совпадать с сигналом c, а этого не происходит. Он срабатывает раньше. Это происходит из-за того, что он защёлкивается частотой clk2. При работе в аппаратуре clk2 будет полностью совпадать с clk1, это будет один и тот же сигнал на глобальной тактовой линии. В реальных проектах присваивания типа clk2<=clk1 встречаются достаточно часто. Конечно можно попробовать всем разработчикам строго настрого запретить это делать, но я сильно сомневаюсь в результате. Вместо присваивания можно сделать описание типа alias:
alias clk2 is clk1;
В этом случае clk2 будет просто ещё одним именем clk1 и результаты будут правильными. Но есть ещё один момент. Чисто визуально, на временной диаграмме непонятно когда происходит изменение сигналов относительно тактовой частоты.
А вот что происходит при добавлении after 1 ns:
Сигналы c и d формируются правильно. Изменение сигнала b прекрасно видно относительно фронта тактовой частоты.
Однажды много лет назад я потратил очень много времени на поиск причины различного поведения в модели и в реальной аппаратуре. Сдвиг был всего лишь на один такт. А причина именно эта – присваивание тактовой частоты и отсутствие after 1 ns. С тех пор я всегда добавляю задержку и ни разу об этом не пожалел.
И напоследок, в примере приведён автомат Мура. Выходные сигналы зависят только от состояния. Но код всегда можно изменить, например так:
when s1 => -- что то делаем, например ждём a=0
if( a=’0’ ) then
stp <= s2 after 1 ns;
q <= ‘1’ after 1 ns;
end if;
В этом случае сигнал q будет сформирован на один такт раньше, т.е. он будет зависеть от состояния и входного сигнала. А это уже автомат Мили.
В статье Описание цифровых автоматов на VHDL также приведёт вариант описания в одном процессе. На первый взгляд всё совпадает.
PROCESS (clk, reset)
BEGIN
IF reset = '1'
THEN state <= Init;
ELSIF (rising_edge(clk)) THEN
CASE state IS
WHEN Init =>
IF cnt = '1'
THEN state <= R;
ELSE
state <= Init;
END IF;
output <= "000";
WHEN R =>
IF cnt = '1'
THEN state <= RG;
ELSE
state <= R;
END IF;
output <= "100";
WHEN RG =>
IF cnt = '1'
THEN state <= G;
ELSE
state <= RG;
END IF;
output <= "010";
WHEN G =>
IF cnt = '1'
THEN state <= GR;
ELSE
state <= G;
END IF;
output <= "001";
WHEN GR =>
IF cnt = '1'
THEN state <= R;
ELSE
state <= GR;
END IF;
OUTPUT <= "010";
END CASE;
END IF;
END PROCESS;
В данном описании есть очень серьёзная ошибка. Во время действия сигнала reset выходной сигнал OUTPUT не определён, что бы его определить требуется внести назначение сигнала внутри сброса:
IF reset = '1'
THEN state <= Init;
OUTPUT <= "000";
ELSIF
В данном случае добавляются три линии сброса. При большом количестве выходных сигналов это ухудшает трассировку ПЛИС.
В моём случае сигнал reset действует только на сигнал состояния, но поскольку внутри процесса он находиться после case то в соответствии с правилами языка он имеет приоритет над назначением состояния внутри case. Особенностью этого решения является то, что перевод выходных сигналов в исходное состояние произойдёт только на второй такт сигнала reset. Однако в подавляющем количестве случаев это несущественно.
В заключение хочу отметить что такой стиль описания очень хорошо себя зарекомендовал, автомат хорошо разводится в ПЛИС. Легко получаются каскадные соединения нескольких автоматов и соединения с другими схемами внутри ПЛИС.
Автор: dsmv2014