Ще раз про затримки у вихідному коді проекту FPGA або просте питання для співбесіди на вакансію розробника FPGA


Деякий час тому при обговоренні в компанії професійних розробників FPGA виникла дискусія про проходження співбесіди. Які питання там ставлять, і що можна було б поставити. Я запропонував два питання:

  1. Наведіть приклад синхронного коду без використання затримок, який дасть різні результати при моделюванні і при роботі в реальній апаратурі
  2. Виправте цей код при допомоги затримок.

Після цього питання зав’язалася жвава дискусія, в результаті якої я вирішив більш детально розглянути це питання.

Я вже трохи торкався цього питання в попередній статті: habr.com/post/309162
Зараз більш докладно. Ось текст прикладу:

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, а цього не відбувається. Він спрацьовує раніше.

Читайте також  Записки IoT-провайдера. Девайси і перекупки

Давайте згадаємо що таке дельта затримка. Це принципове поняття, на ньому заснована робота подієвих симуляторів, якими ми користуємося при моделюванні логічних схем.

Симулятор має поняття модельного часу. До цього модельного часу прив’язані всі події в системі. Давайте подивимося на формування тактової частоти:

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

1

true
false



1
1
1
false
true
1


2
1

false
false
1

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

1

true
false



25
1
1
1
false
true



26

1
1
false
false
1


35

1

true
false
1


35
1
1
1
false
true
1


36

1
1
false
false
1
1
1
Читайте також  Побудова орбіт небесних тіл засобами Python

Видно, що значення 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

// Синтезуються частина - перепризначення тактового сигналу ---
завжди @(clk1) clk2 <= clk1; 

// Варіант 1 - Синтезируемая частину без затримок 

завжди @(posedge clk2) d <= b; 

завжди @(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 можна написати й по іншому:

завжди @(posedge clk1)
begin 
 c = b; 
 b = a;
end 

А можна так:

завжди @(posedge clk1)
begin 
 b = a;
 c = b; 
end

Залежно від порядку рядків результат буде різний.

Повертаючись до теми співбесіди хочу ще раз підкреслити, що ці питання на розуміння суті проблеми. А вже з розуміння проблеми можна робити різні висновки, наприклад якою використовувати стиль коду. Особисто я завжди використовую призначення затримок.

Файли прикладів доступні тут:
https://github.com/dsmv/fpga_components

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *