FPGA для програміста, прості рецепти

Пріоритетна структура коду
У розробці електронних пристроїв грань між розробником-схемотехником і розробником-програмістом дуже розмита. Що вже говорить про те, хто повинен писати RTL під FPGA.
З одного боку, RTL — це територія схем, з іншого боку, ресурси FPGA дешевшають, синтезатори розумнішають. Ціна помилки RTL дизайнера для FPGA не перевищує ціни помилки програміста, а створені схеми також можна оновлювати та нарощувати по функціональності, як звичайну прошивку процесора.
Виробники мікросхем теж не відстають, почали пакувати ПЛІС в один корпус з процесором, навіть Intel випустила процесор для PC з FPGA всередині, купивши для цього відомого виробника ПЛІС Altera.
Думаю всім істинним програмістам Всесвіт шле сигнали, що їм просто необхідно вивчити RTL і почати писати код для FPGA не гірше, ніж під їх звичні процесори.
Колись давно, я проходив цей шлях і дозволю собі дати кілька порад для прискорення.

Для початку, потрібно вибрати мову опису. На поточний момент використання мов типу System Verilog, SystemC і т. п., саме для створення схем, більше схоже на угоду з дияволом, ніж на роботу. Тому ще в строю старовинні і базові VHDL і Verilog. Я спробував обидва і раджу використовувати останній. Verilog більш дружній з синтаксису програмістам, так і в цілому як-то посучасніше.
Якщо ви твердо вирішили пройти цей шлях, то я думаю, що ви вже знаєте ключові слова і стандартні конструкції Verilog. Ви витратили якийсь час і розумієте, що в описі апаратури все відбувається одночасно, а не по черзі, як у програмах.
Ми поки залишимо питання мета-стабільність і гонки сигналів, для цього обмежимося тільки синхронними схемах з синхронним скиданням, а всяку комбінаторику та асинхроньщину залишимо старій школі.
В описах схем дуже важлива структура коду, про організацію якого і піде мова далі. Структура не тільки покращує читаність і поддерживаемость коду, але також впливає на результат роботи підсумкової схеми.
Справжні RTL-дизайнери мислять «схемно», вони організують код в блоки і цим визначають його структуру. Ми не будемо відразу міняти спосіб мислення, а будемо створювати «программисткие» опису. Ми зосередимося на тому, що хочемо отримати, а створення відповідної для цього схеми, залишимо синтезатору. Все як з мовами високого рівня, пишемо код, а оптимізацію і переклад в машинні коди вішаємо на компілятор.
Плата за такий підхід приблизно та ж, трохи менш оптимальний з точки зору ресурсів результат, але як сказано вище ціна ресурсів знижується, тому не будемо шкодувати патрони. Синтезатори до поточного моменту здорово порозумнішали, але все ж деякі проблеми є, розглянемо приклад:
input clk; //тактовий сигнал
input data_we; //сигнал дозволу запису від зовнішнього модуля
input [7:0] data; //дані від зовнішнього модуля

reg [7:0] Data; //дані 
reg DataRdy; //прапор готовності даних
reg [7:0] ProcessedData; //оброблені дані

//прийом даних по фронту клока ----------------------
завжди @(posedge clk)
begin
if(data_we == 1'b1) //якщо є сигнал запису 
begin
Data <= data; //зберігаємо дані
DataRdy <= 1'b1; //ставимо прапор готовності даних
end 
end
приклад коду №1

Поки ніяких проблем немає, прийом виділили в окремий блок, все зручно і зрозуміло. Тепер припустимо, далі у нас йде робота з отриманими даними, і нам хочеться зняти прапор DataRdy по закінченню обробки даних, щоб розуміти, коли прийдуть нові дані.
//обробка по фронту клока ----------------------
завжди @(posedge clk)
begin
if(DataRdy == 1'b1) //якщо є нові дані
begin
//обробка даних
ProcessedData <= Data;
DataRdy <= 1'b0; //знімаємо прапор, дані оброблені 
end
end
приклад коду №2

Ось тепер починаються проблеми, у любителів Xilinx точно, але думаю, що і інші синтезатори будуть солідарні. Синтезатор скаже, що в сигналу DataRdy два джерела змінюють його значення, він змінюється по фронту сигналу в 2 блоках і неважливо, що один тактовий сигнал.
Може здатися, що синтезатор не знає яке значення задати, якщо виконуються умови зміни в обох блоку одночасно, коли DataRdy має значення 1
//в першому блоці
if(data_we == 1'b1)
DataRdy <= 1'b1;

...

//у другому блоці
if(DataRdy == 1'b1)
DataRdy <= 1'b0;
приклад коду №3

Але модифікація коду вирішальна цей конфлікт не допоможе.
//прийом даних по фронту клока ----------------------
завжди @(posedge clk)
begin
//якщо є сигнал запису, і знятий прапор даних 
if((data_we == 1'b1)&&(DataRdy == 1'b0)) 
begin
Data <= data; //зберігаємо дані
DataRdy <= 1'b1; //ставимо прапор готовності даних
end 
end
приклад коду №4

Логічно все вірно, ніяких конфліктів немає, але синтезатор наполегливо буде скаржитися на подвійний джерело сигналу, і домовитися з ним не вийде. Не можна в різних блоках змінювати один сигнал, щоб усе вийшло, треба і прийом і обробку помістити в один блок.
І тут перше речення, а давайте у нас в модулі буде взагалі всього 1 блок always, і все, що робить наш модуль, ми розмістимо в цьому блоці, наш приклад стане виглядати так
input clk; //тактовий сигнал
input data_we; //сигнал дозволу запису від зовнішнього модуля
input [7:0] data; //дані від зовнішнього модуля

reg [7:0] Data; //дані 
reg DataRdy; //прапор готовності даних
reg [7:0] ProcessedData; //оброблені дані

//---------------------------------------------------
// обробка основного клока
//---------------------------------------------------
завжди @(posedge clk)
begin
//якщо є сигнал запису, і знятий прапор даних 
if((data_we == 1'b1)&&(DataRdy == 1'b0)) 
begin
Data <= data; //зберігаємо дані
DataRdy <= 1'b1; //ставимо прапор готовності даних
end 
else if(DataRdy == 1'b1) //якщо є прапор даних 
begin
//обробка даних
ProcessedData <= Data;
DataRdy <= 1'b0; //знімаємо прапор, дані оброблені 
end
end
приклад коду №5

Тепер все працює, але модуль став вже не таким зрозумілим, явного поділу на прийом і обробку немає, все в одну купу. Тут нам на допомогу приходити одне дуже приємне властивість мови Verilog. Якщо в одному блоці ви робите кілька присвоєнь однієї змінної (говоримо про неблокирующих присвоєння), то виконається останні з них (Стандарт Verilog HDL IEEE Std 1364-2001). Правильніше сказати, що виконуються вони всі в описаному порядку, але так як всі такі присвоєння відбуваються одночасно, то змінна прийме останні присвоєне значення.

Тобто, якщо написати так:
input B;
reg [2:0] A;
завжди @(posedge clk)
begin
A <= 1;
A <= 2;
A <= 3;
if(B) A <= 4;
end
приклад коду №6

A прийме значення 3 у разі, якщо У ні, а якщо все ж У істина, то А прийме значення 4, в цьому можна переконатися на наступному зображенні

Рис1. Тимчасова діаграма симуляції поведінки опису №6
Це повністю описана стандартом і синтезуються конструкція, що дає нам цікаві можливості, немає необхідності робити складні ланцюжки конструкції ifelse if розділяючи, коли змінній присвоїти одне значення, а коли інше. Ви можете просто написати умову і значення змінної, написати це не думаючи про інших умовах і присвоєння цієї змінної, написати це як би ізольовано від іншого коду.
Далі залишиться розташувати такі присвоєння в правильному порядки, тим самим визначити їх пріоритети на випадок одночасного виконання, і все вийде само. Це дуже зручний спосіб керування кодом, при цьому контрольований синтезатором, а не людиною.
У наступному прикладі показано, як це може виглядати
//---------------------------------------------------
// обробка основного клока
//---------------------------------------------------
завжди @(posedge clk)
begin
//прийом даних, найменший пріоритет ----- 
if(...) Data <= data;

//обробка даних, середній пріоритет ----
if(...) Data <= Func(Data);

//скидання, найвищий пріоритет -------------
if(reset_n == 1'b0)
Data <= 0; 
end
приклад коду №7

Куди б вас не завела крива створення модуля, ви можете бути впевненими, що стан скидання, перекриє все що ви накоїли вище, ви можете зробити скільки завгодно помилок у логіці, скидання відбудеться і задасть змінної описане в блоці скидання значення.
Також ви можете бути впевненими, що якщо у вас раптом співпадуть в один момент часу умови обробки і прийому даних, то ви обробите дані, а не затрете їх новими прийшли. Це відбудеться тому, що обробка в нашому коді стоїть нижче, вона більш пріоритетна. Якщо раптом у якийсь момент ви зрозумієте, що важливіше не втратити приходять дані, поміняйте блоки місцями і тим самим змінити пріоритети.
Якщо у вас кілька інтерфейсів, які можуть змінити дані, ви знову ж просто маєте ділянки коду реалізують інтерфейс один за одним, і тим самим розставляєте пріоритети доступу до даних.

//---------------------------------------------------
// обробка основного клока
//---------------------------------------------------
завжди @(posedge clk)
begin

//прийом даних по 1 інтерфейсу, 
//найменший пріоритет ------------------- 
if(master1_we) 
Data <= data1; 

//прийом даних по 2 інтерфейсу, 
//пріоритет вище першого ----------------- 
if(master2_we) 
Data <= data2;

//обробка даних, середній пріоритет ----
if(need_process) 
Data <= (Data << 1);

//скидання, найвищий пріоритет -------------
if(reset_n == 1'b0)
Data <= 0;
end
приклад коду №8

Симуляція роботи опису можна бачити на малюнку нижче

Рис2. Тимчасова діаграма симуляції поведінки опису №8
Ця система управляється кількома провідними пристроями і арбітраж між ними вийшов автоматично. Коли майстри керують схемою по черзі (фаза 1 і 2, рис. 2), вона отримує дані від кожного з них, але якщо раптом кілька майстрів видадуть дані одночасно (фаза 3, рис. 2), то схема використовує дані від більш пріоритетного майстра, інтерфейс якого описаний нижче, від другого в нашому прикладі.
При цьому скидання схеми перекриває всі сигнали (фаза 5, рис. 2), а обробка вище пріоритету будь-якого з майстрів, але нижче скиду (фаза 4, рис. 2).
Повернемося до початкового наприклад, і покажемо його кінцевий варіант опису:
input clk; //тактовий сигнал
input data_we; //сигнал дозволу запису від зовнішнього модуля
input [7:0] data; //дані від зовнішнього модуля

reg [7:0] Data; //дані 
reg DataRdy; //прапор готовності даних
reg [7:0] ProcessedData; //оброблені дані

//---------------------------------------------------
// обробка основного клока
//---------------------------------------------------
завжди @(posedge clk)
begin
//прийом даних -------------------------------
//пріоритет 0
if(data_we == 1'b1)//якщо є сигнал запису 
begin
Data <= data; //зберігаємо дані
DataRdy <= 1'b1; //ставимо прапор готовності даних
end 

//обробка даних --------------------------
//пріоритет 1
if(DataRdy == 1'b1) //якщо є прапор даних 
begin
//обробка даних
ProcessedData <= Data;
DataRdy <= 1'b0; //знімаємо прапор, дані оброблені 
end
end
приклад коду №9

Навіть не потрібно в блоці прийому перевіряти, що DataRdy має нульове значення, блок обробки перекриє по пріоритету блок прийому, і скине прапор DataRdy, навіть якщо під час обробки надійдуть нові дані. А помінявши блоки місцями, ми не пропустимо ніяких нових даних.
input clk; //тактовий сигнал
input data_we; //сигнал дозволу запису від зовнішнього модуля
input [7:0] data; //дані від зовнішнього модуля

reg [7:0] Data; //дані 
reg DataRdy; //прапор готовності даних
reg [7:0] ProcessedData; //оброблені дані 

//---------------------------------------------------
// обробка основного клока
//---------------------------------------------------
завжди @(posedge clk)
begin
//обробка даних --------------------------
//пріоритет 0
if(DataRdy == 1'b1) //якщо є прапор даних 
begin
//обробка даних
ProcessedData <= Data; 
DataRdy <= 1'b0; //знімаємо прапор, дані оброблені 
end

//прийом даних -------------------------------
//пріоритет 1
if(data_we == 1'b1)//якщо є сигнал запису 
begin
Data <= data; //зберігаємо дані
DataRdy <= 1'b1; //ставимо прапор готовності даних
end
end
приклад коду №10

Після обробки даних скидається прапор DataRdy, але якщо одночасно з цим моментом до нас приходять нові дані, блок прийому перекриє пріоритет скидання і знову поставить прапор DataRdy, і дані (факт їх оновлення) не загубляться, дані будуть оброблені в наступному циклі.
Що дає така організація коду?
Код розділений на зрозумілі блоки, перед ними можна дати розлогі коментарі, що робить кожен блок. Ми маємо можливо ставити пріоритети блокам, перекриваючи присвоєння одного блоку іншим, при це не пов'язуємо їх у величезні незручні списки ifelse if else if. Можна видалити або «закоментувати» блок, вставити між будь-якими блоками ще один, інша частина коду продовжить працювати без правок.
Оскільки у нас єдиний always, то немає конфліктів подвійних джерел сигналу, якщо в якийсь момент ми вирішимо змінювати сигнали в різних структурних блоках. Ми просто змінюємо сигнал де і коли нам треба. Не треба організовувати ніяких «хэндшейков» і «прокидати» додаткові сигнали, як у випадку окремих always.
Код управляємо, читаємо, змінюємо, існує зрозумілим законам, вам не треба збирати пріоритетні шифратори і подавати їх на мультиплексори інтерфейсів, збираючи всі сигнали шини і прикидати всі умови зміни сигналу.
Все що вам треба, просто описати поведінку схеми, що ви від неї хочете, задати пріоритети розташуванням блоків опису і віддати все це на обробку синтезатору. Можете бути впевнені, що він чудово впорається з поставленим завданням і видасть схему з бажаним поведінкою, вже синтезатор Xilinx точно, але думаю і інші будуть солідарні.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.