Ещё раз о задержках в исходном коде проекта FPGA или простой вопрос для собеседования на вакансию разработчика FPGA +9




Некоторое время назад при обсуждении в компании профессиональных разработчиков FPGA возникла дискуссия о прохождении собеседования. Какие вопросы там задают, и что можно было бы задать. Я предложил два вопроса:

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

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

Я уже немного касался этого вопроса в предыдущей статье. Сейчас более подробно. Вот текст примера:

library IEEE;
use IEEE.STD_LOGIC_1164.all;

entity delta_delay is
end delta_delay;

architecture delta_delay of delta_delay is

signal	clk1		: std_logic:='0';
signal	clk2		: std_logic;
alias	clk3		: std_logic is clk1;	-- назначение другого имени clk1

signal	a			: std_logic;
signal	b			: std_logic;
signal	c			: std_logic;
signal	d			: std_logic;
begin							
--- Формирование тестовых сигналов ---
clk1 <= not clk1 after 5 ns;

pr_a: process begin
	a <= '0' after 1 ns;
	wait until rising_edge( clk1 );
	wait until rising_edge( clk1 );
	a <= '1' after 1 ns;
	wait until rising_edge( clk1 );
	wait until rising_edge( clk1 );
	wait until rising_edge( clk1 );
	wait until rising_edge( clk1 );
end process;	
	
--- Синтезируемая часть - переназначение тактового сигнала  ---
clk2 <= clk1; -- вот в этом проблема, не надо так делать без крайней необходимости

--- Вариант 1 - Синтезируемая часть без задержек  ---

b <= a when rising_edge( clk1 );
c <= b when rising_edge( clk1 );
d <= b when rising_edge( clk2 );

--- Вариант 2 - Синтезируемая часть с задержками  ---
--
--clk2 <= clk1;
--b <= a after 1 ns when rising_edge( clk1 );
--c <= b after 1 ns when rising_edge( clk1 );
--d <= b after 1 ns when rising_edge( clk2 );

--- Вариант 3 - Синтезируемая часть без задержек но с переназначением сигнала через alias  ---
--b <= a when rising_edge( clk1 );
--c <= b when rising_edge( clk1 );
--d <= b when rising_edge( clk3 );

end delta_delay;

Для упрощения весь код размещён в одном компоненте.

Сигналы clk1 и a это сигналы тестового воздействия. clk1 это тактовая частота 100 MHz, Сигнал а держится два такта в 0 и четыре такта в 1. Сигнал a формируется с задержкой 1 nc относительно нарастающего фронта clk1. Этих двух сигналов достаточно для описания проблемы.

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

Вот результаты моделирования для варианта 1:



На диаграмме визуально видно, что сигналы тактовой частоты clk1 и clk2 совпадают, но на самом деле clk2 задержан относительно clk1 на величину дельта задержки. Сигнал c отстаёт от сигнала b на один такт. Это правильно. Но вот сигнал d должен совпадать с сигналом c, а этого не происходит. Он срабатывает раньше.

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

Симулятор имеет понятие модельного времени. К этому модельному времени привязаны все события в системе. Давайте посмотрим на формирование тактовой частоты:

clk1 <= not clk1 after 5 ns;

Предположим что сейчас мы моделируем только clk1, других сигналов нет.
В начальный момент времени clk1 равен 0, это задано при объявлении сигнала. Симулятор видит требование инвертировать сигнал. Ключевое слово after даёт инструкцию провести назначение нового значение через 5 ns относительно текущего модельного времени. Симулятор это видит и делает отметку, что в момент времени 5 ns значение clk1 будет равно 1. Пока это модельное будущее, оно кстати ещё может измениться. Далее симулятор просматривает остальные сигналы. Симулятор увидит что для данного момента модельного времени всё выполнено и он может рассчитывать следующий момент. Возникает вопрос – а какой момент следующий? В принципе возможны разные варианты. Например Simulink имеет режим с фиксированным шагом. В этом случае произойдёт приращении модельного времени на какую то величину и вычисления продолжаться.

Системы моделирования цифровых схем делают по другому. Они переходят к ближайшему событию, которые они уже разместили в будущем на своей оси модельного времени. В данном случае это будет момент 5 нс. Симулятор увидит что clk1 изменился и просчитает для него новое значение, это будет 0 который также будет размещён с задержкой в 5 нс на временной оси. Т.е. это будет момент 10 нс. И так процесс будет продолжаться пока не закончиться заданное время моделирования.

Теперь давайте добавим сигналы a и b.

Сигнал a назначается в процессе. Для сигнала b используется условная конструкция when; Функция rising_edge(clk1) анализирует clk1 и возвращает true когда зафиксирован фронт, т.е. предыдущее значение равно 0 а текущее равно 1.

В момент модельного времени 5 ns произойдёт изменение clk1. Он станет равным 1 и для момента 10 ns будет создано событие установки его в 0. Но это потом. Пока мы ещё в моменте 5 ns и продолжаем вычисления. Симулятор переходит к строчке
b<=a when rising_edge(clk1);
Поскольку есть функция которая зависит от clk1 то симулятор вычислит значение функции, увидит что она вернула true и произведёт присваивание
b<=a;


Вот здесь начинается самое интересное — когда надо изменить значение b. Казалось бы надо изменить его сейчас, в этот момент времени. Но у нас параллельные процессы. Может быть, нам ещё понадобиться значение b для расчёта других сигналов. И вот здесь появляется понятие дельта задержки. Это минимальная величина, на которую смещается модельное время. Эта величина даже не имеет размерности времени. Это просто дельта. Но их может быть много. Причём настолько много что симулятор просто останавливается по ошибке или зависает.
Итак, новое значение b будет установлено для момента 5 ns + 1 (1 – это первая дельта задержка). Симулятор увидит, что рассчитывать для момента 5 ns уже нечего и перейдёт к следующему моменту, а это будет 5 ns + 1; В этот момент rising_edge(ckl1) не срабатывает. А значение b будет установлено в 1. После этого симулятор перейдёт к моменту 10 nc.

А вот теперь давайте добавим сигналы c, d и разберёмся почему они разные.
Лучше всего это рассмотреть момент модельного времени 25 ns с учётом дельта задержек

delta clk1 clk2 re(clk1) re(clk2) b c d
0 1 0 true false 0 0 0
1 1 1 false true 1 0 0
2 1 0 false false 1 0 1

Примечание: re — rising_edge

Из таблицы видно что в момент срабатывания функции rising_edge(clk2) значение b уже равно 1. И поэтому оно будет присвоено сигналу d.

Исходя из здравого смысла это не то поведение, которое мы ожидали от кода. Ведь мы просто переназначили сигнал clk1 на clk2 и ожидали, что сигналы c и d будут одинаковыми. Но следуя логике работы симулятора это не так. Это ПРИНЦИПИАЛЬНАЯ особенность. Эту особенность конечно надо знать разработчикам FPGA проектов и поэтому это хороший и нужный вопрос для собеседования.

Что же произойдет при синтезе? А вот синтезатор проследует здравому смыслу, он сделает сигналы clk2 и clk1 одним сигналом и поэтому c и d тоже будут одинаковыми. А при определённых настройках синтезатора они тоже будут объединены в один сигнал.

Это как раз случай, когда моделирование и работа в реальной аппаратуре приведут к разным результатам. Хочу обратить внимание, что причина разных результатов – это разная логика симулятора и синтезатора. Это ПРИНЦИПИАЛЬНАЯ разница. Это не имеет ничего общего с временными ограничениями. И если ваш проект в модели и в железе показывает разные результаты то проверьте, может быть там закралась конструкция подобная

clk2 <= clk1 

Теперь второй вопрос – исправьте этот код при помощи задержек.
Это вариант 2. Его можно раскомментировать и промоделировать.
Вот результат.



Результат правильный. Что же произошло? Давайте ещё раз составим таблицу для интервала 25 – 36 нс
time delta clk1 clk2 re(clk1) re(clk2) b c d
25 0 1 0 true false 0 0 0
25 1 1 1 false true 0 0 0
26 0 1 1 false false 1 0 0
35 0 1 0 true false 1 0 0
35 1 1 1 false true 1 0 0
36 0 1 1 false false 1 1 1

Видно, что значение b не меняется в моменты фронтов clk1, clk2. Задержка в 1 нс уводит момент изменения сигналов за зону срабатывания фронтов. Этот код становиться ближе к реальности. В реальной схеме существует какое то время на срабатывание триггера и на распространение сигнала. Это время должно быть меньше периода тактовой частоты, собственного говоря, именно этого добивается трассировщик и именно это проверяет временной анализ.

Причина возникновения ошибки это переназначение тактового сигнала обычным присваиванием при котором появляется дельта задержка. Однако язык VHDL имеет конструкцию alias. Это позволяет получить другое имя для сигнала. Вот объявление:

alias clk3 	: std_logic is clk1;

В тексте примера можно раскомментировать вариант 3 – он будет работать правильно.

Данный пример написан на языке VHDL. Может быть это проблемы только этого языка? Но вот те же варианты на языке Verilog.

Скрытый текст
`timescale 1 ns / 1 ps

module delta_delay_2 ();
	
reg  clk1 = 1'b0;	 
reg  clk2;
wire clk3;		

reg a = 1'b0;
reg b;
reg c;
reg d;

initial begin
forever clk1 = #5 ~clk1;
end 

initial begin
repeat(10)
begin

#20 a = 1'b1;
#60 a = 1'b0;
end
end

// Синтезируемая часть - переназначение тактового сигнала  ---
always @(clk1) clk2 <= clk1;	
	
// Вариант 1 - Синтезируемая часть без задержек  
	
always 	@(posedge clk2)	d <= b;			
	
always 	@(posedge clk1)
begin	
	c <= b;		
	b <= a;
end				  

// Вариант 2 - Синтезируемая часть с задержеками  
    
//always 	@(posedge clk1)	b = #1 a;	
//	
//always 	@(posedge clk1)	c = #1 b;		
//	
//always 	@(posedge clk2)	d = #1 b;			
	
// Вариант 3 - Синтезируемая часть без задержек 
// но с переназначением сигнала через assign  

//assign clk3 = clk1;		
//
//always 	@(posedge clk3)	d <= b;			
//	
//always 	@(posedge clk1)
//begin	
//	c <= b;		
//	b <= a;
//end	
endmodule



  • Вариант 1 – без задержек. Работает неправильно.
  • Вариант 2 – с задержками. Работает правильно.
  • Вариант 3 – переназначение через wire. Работает правильно.

В языке Verilog есть понятие reg и wire. В данном случае переназначение тактового сигнала через wire выглядит более естественным. Это является аналогом присвоения через alias в языке VHDL. Это несколько снимает напряжённость проблемы, но всё равно это надо знать.
Также в языке Verilog есть понятие блокирующего и неблокирующего присваивания. Назначение сигналов b и c можно написать и по другому:

always 	@(posedge clk1)
begin	
	c = b;		
	b = a;
end	

А можно так:

always 	@(posedge clk1)
begin	
	b = a;
	c = b;		
end

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

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

Файлы примеров доступны здесь




К сожалению, не доступен сервер mySQL