Конечные автоматы играют очень важную роль при разработке прошивок ПЛИС. Все слышали о двух классических типах автоматов: автомат Мили и автомат Мура, которые были предложены ещё до эпохи ПЛИС. Однако специфика построения ПЛИС вносит свои коррективы и в процессе работы у меня сложился вполне определённый стиль описания автомата.

В литературе предлагаются различные реализации автоматов. Такая статья есть и на Хабре — «Описание цифровых автоматов на VHDL» от canny. Однако при практической реализации на ПЛИС, тем более на высоких частотах такая реализация неудобна. Предлагается следующий вариант.

Описание компонента:

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 );

Результат работы представлен на рисунке:

image

На диаграмме визуально видно, что сигналы тактовой частоты clk1 и clk2 совпадают, но на самом деле clk2 задержан относительно clk1 на величину дельта задержки.Сигнал c отстаёт от сигнала b на один такт. Это правильно. Но вот сигнал d должен совпадать с сигналом c, а этого не происходит. Он срабатывает раньше. Это происходит из-за того, что он защёлкивается частотой clk2. При работе в аппаратуре clk2 будет полностью совпадать с clk1, это будет один и тот же сигнал на глобальной тактовой линии. В реальных проектах присваивания типа clk2<=clk1 встречаются достаточно часто. Конечно можно попробовать всем разработчикам строго настрого запретить это делать, но я сильно сомневаюсь в результате. Вместо присваивания можно сделать описание типа alias:

alias clk2 is clk1;

В этом случае clk2 будет просто ещё одним именем clk1 и результаты будут правильными. Но есть ещё один момент. Чисто визуально, на временной диаграмме непонятно когда происходит изменение сигналов относительно тактовой частоты.

А вот что происходит при добавлении after 1 ns:

image

Сигналы 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. Однако в подавляющем количестве случаев это несущественно.

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

Комментарии (13)


  1. nerudo
    04.09.2016 20:43
    +1

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


  1. Uint32
    05.09.2016 01:50

    >> after 1 ns – для облегчения моделирования
    Весьма спорно, т.к. сразу попадаем в не синтезируемое подмножество.
    >> Наличие такой записи позволяет быть уверенным что результаты моделирования и результаты работы в реальной аппаратуре будут одинаковыми.
    А что мешает провести верификацию прогнав tb после P&R?


    1. dsmv2014
      05.09.2016 09:49

      По стандарту языка слово after синтезатором игнорируется. Когда ПЛИС были маленькими я проводил моделирование с реальными задержками после стадии P&R. Для современных ПЛИС это не реально. Слишком долго будет формироваться модель с задержками и будет слишком большое время моделирования.


  1. b_oberon
    05.09.2016 10:18

    Использование after в синтезируемом коде — это плохо, потому что:

    1. Вы смешиваете functional simulation и gate-level simulation (который после P&R). Задача первого — убедиться, что Ваш алгоритм правильный, второго — что суровая физическая реальность не разрушит Ваш правильный алгоритм. Если gate-level simulation выполняется слишком медленно, то стоит посмотреть в сторону timing analysis (у Altera это, если не ошибаюсь, TimeQuest): он укажет Вам, в каких именно местах могут быть проблемы с временными характеристиками (плюс компилятор может использовать эту информацию для оптимизации схемы).

    2. Возможности повторного использования Вашего кода сильно ограниченны, поскольку он не инвариантен относительно частоты: при 10 МГц задержка 1 ns — это почти дельта-задержка (в том плане, что она мала по сравнению с периодом), на частоте 100 МГц — уже чувствительно, а на 1 ГГц — целый такт. В зависимости от заданной частоты работы схемы functional simulation будет давать очень разные результаты, и это явно не то, к чему Вы стремитесь. Ну и никакого физического обоснования для константы 1 ns нет, все будет зависеть от модели ПЛИС.


    1. dsmv2014
      05.09.2016 11:17

      На современных проектах слишком медленно выполняется даже functional simulation. Например мне приходится при помощи параметров удалять целые блоки из проекта ПЛИС. Это касается например блоков PCE Express и контроллеров DDR. Timig Analysis конечно используется.
      По поводу повторного использования — этот код у нас постоянно повторно используется. До 300 МГц after 1 ns прекрасно выглядит, а выше уже можно писать after 0.1 ns.
      Ещё раз, задача after 1 ns — показать что есть задержка. Она должна быть меньше периода. С этой точки зрения неважно сколько получиться в реальной аппаратуре: 0.1 или 2.2 ns. Если получиться меньше периода — результаты моделирования и реальной работы совпадут. Остальное неважно.


  1. kurilkin
    05.09.2016 11:09

    а можно попросить хоть один пример необходимости такого переназначения «clk2 <= clk1»?


    1. dsmv2014
      05.09.2016 11:20

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


      1. kurilkin
        05.09.2016 11:43
        +1

        а компилятор дает ли гарантию прямого объединения цепей, есть ли уверенность, что он не засадит их через буфер, скромно сообщив об этом в одном из варнингов?


        1. Khort
          06.09.2016 10:40

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


          1. kurilkin
            06.09.2016 11:07

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


      1. firegurafiku
        05.09.2016 21:15

        Я не суперспец в ПЛИС, но с каких пор копипаста называется повторным использованием кода? Разве не следует вынести общую функциональность в отдельный именованный модуль?


        1. dsmv2014
          05.09.2016 21:19

          Выносить в отдельный модуль это не всегда разумно.


  1. dsmv2014
    05.09.2016 12:23

    Это проверено, получается один и тот же сигнал тактовой частоты