Хабрахабр

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

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

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

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

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

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

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

clk1 <= not clk1 after 5 ns;

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

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

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

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

Он станет равным 1 и для момента 10 ns будет создано событие установки его в 0. В момент модельного времени 5 ns произойдёт изменение clk1. Пока мы ещё в моменте 5 ns и продолжаем вычисления. Но это потом. Симулятор переходит к строчке

b<=a when rising_edge(clk1);

Поскольку есть функция которая зависит от clk1 то симулятор вычислит значение функции, увидит что она вернула true и произведёт присваивание

b<=a;

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

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

Ведь мы просто переназначили сигнал 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

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

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

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

Теги
Показать больше

Похожие статьи

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

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

Кнопка «Наверх»
Закрыть