
Содержание курса |
1. Что такое ПЛИС | 3. Процесс сборки и системы симуляции
Мы уже упоминали, что "программы" для ПЛИС пишутся на языках описания аппаратуры (HDL - hardware description language). Строго говоря, описания проектов ПЛИС, конечно, не являются программами (это именно описания схем, реализуемых на базе ПЛИС), а данные языки не являются языками программирования. Однако для простоты, далее здесь (как часто и в реальной практике) мы иногда будем опускать эти различия и говорить о программистах, компиляции и различных терминах, относящихся к программированию, если это не будет вводить в заблуждение.
Основными языками являются Verilog и VHDL. Также существует ряд других языков, например, AHDL - язык фирмы Altera, обеспечивает гораздо более низкий уровень абстракции, чем два вышеназванных. Есть приложения, использующие для решения задач моделирования и верификации SystemC. Различные средства сборки проектов предоставляют также возможность рисовать схему вручную, без использования языков. Кроме того, постепенно развиваются средства высокоуровневого синтеза (High-Level Synthesis - HLS), которые позволяют генерировать схемы на базе описания на одном из высокоуровневых языков, например C/C++. Однако все эти возможности используются гораздо реже нежели основные языки.
Оба языка появились в 80е годы как средства моделирования и верификации цифровых схем. С момента появления возможности прямого синтеза схем из описания они фактически стали основными инструментами проектирования ASIC и разработки проектов для ПЛИС. Verilog был разработан компанией Gateway Design Automation как проприетарный язык. В 90м году его опубликовали как открытый стандарт. В 95 году вышел официальный стандарт IEEE Standard 1364-1995. Обновление вышло в 2001, после чего в 2005 его объединили с SystemVerilog - расширенным множеством языка, содержащим продвинутые опции для моделирования, верификации и повторного использования кода. С того времени официальным стандартом языка считается IEEE 1800, последней модификацией которого на момент написания курса был IEEE 1800-2017. В дальнейшем мы тоже не будем разделять Verilog и SystemVerilog и будем говорить уже о втором, в частности, как руководящий документ будем использовать IEEE 1800-2012. Впрочем, здесь нужно помнить, что SystemVerilog - очень обширный язык и компиляторы сред разработки поддерживают только его часть, которую будем называть синтезируемым подмножеством языка. В последствии при работе с SystemVerilog нужно в первую очередь ориентироваться на используемый инструмент синтеза проекта и исходя уже из его возможностей пользоваться теми или иными конструкциями. Verilog построен на базе синтаксиса языка C, что обуславливает простоту взаимодействия с ним для тех, кто уже был знаком с одним из C-подобных языков.
VHDL разработан министерством обороны США как составная часть программы VHSIC (Very High Speed Integrated Circuit). В 1987 году опубликован первый официальный стандарт языка IEEE 1076. Значимые обновления стандарта выходили в 1993, 2000, 2002 и 2008 годах. Текущей версией стандарта на момент написания курса является IEEE 1076-2019. VHDL широко распространен во многих европейских странах, а также в военной и аэрокосмической промышленности США. В России, по крайней мере у меня сложилось такое впечатление, Verilog пользуется большей популярностью, даже некоторые знакомые специалисты, которые начинали работать на VHDL, потом переходили на Verilog. VHDL построен на базе языка программирования Ada с некоторым влиянием Pascal. Существует мнение, что строгая типизация, более подробное описание элементов на VHDL и другие его особенности уменьшают возможность "выстрелить себе в ногу" и обеспечивают более удобный доступ к низкоуровневым компонентам схемы. Кроме того, что концепция языка ближе для схемотехников, которые пришли к разработке проектов для ПЛИС, в то время как Verilog ближе для программистов. Но для меня данные преимущества не могут оправдать избыточности языка. Будучи изначально ближе к схемотехнике, чем к программированию мне, тем не менее гораздо больше по душе пришелся лаконичный Verilog. В своей работе я неоднократно сталкивался с проектами на VHDL, однако обычно это ограничивалось, либо вставкой моих модулей на Verilog в VHDL-проекты, либо вставкой сторонних VHDL-модулей в проекты на Verilog. Вы вольны сами решать, какой язык использовать для обучения и последующей работы, ведь если вы впоследствии свяжете свою жизнь с этим, вам фактически придётся научиться общаться на этом языке. Однако я не смогу рассказать о конструкциях VHDL так же подробно и с такой же степенью достоверности как о языке Verilog, а значит в рамках данного курса речь мы будем вести о Verilog.
Базовой концепцией языка является модульный принцип построения проектов. Модуль - это блок кода на языке Verilog, который описывает некоторую структурную единицу схемы, обладающую определенной функциональностью. Так у вас есть топ-модуль, в котором описываются внешние сигналы, которым вы присваиваете выводы ПЛИС. Внутри модуля может находиться пользовательская логика, работающая с сигналами, подаваемыми на ПЛИС, а также внутренние модули, которые также могут включать в себя другие модули. Уровень вложенности в обычных проектах может достигать нескольких десятков, а вообще ограничен только вашей фантазией и аппаратными возможностями ПЛИС. Таким образом выстраивается многоуровневая иерархия проекта, где на низких уровнях вы можете управлять отдельными регистрами и мультиплексорами, а на более высоких уже описывать сложные функциональные блоки и системы.
Встроенная логика может быть как комбинационной, так и синхронной, причем комбинационные элементы чаще встречаются как составные части синхронных схем. Правилом хорошего тона считается описывать каждый модуль в отдельном файле. Давайте рассмотрим пару примеров такого файла:
module module_1 //Название модуля
( //Список сигналов модуля
input wire input_signal ,
output wire output_signal
);
//Внутренняя логика
//Конец модуля
endmodule
Пример 1. Необходимый минимум при описании модуля.
/*
* Заголовок
*/
//Список подключаемых библиотек, файлов и пакетов
`include "external_file.v"
import external_pkg::*;
//////////////////////////////////////////////////////////////////////////
module module_1 //Название модуля
#( //Список параметров модуля
parameter PARAMETER_1 = 32'h89AB_CDEF,
parameter PARAMETER_2 = "yes",
parameter PARAMETER_3 = (PARAMETER_2 == "no")?(PARAMETER_1/8):'1,
parameter PARAMETER_4 = 32
)
( //Список сигналов модуля
input wire clk ,
input wire input_signal ,
output wire [PARAMETER_4-1:0] output_signal ,
inout wire inout_signal
);
//Внутренние параметры
localparam INTERNAL_PARAMETER_1 = 128;
localparam INTERNAL_PARAMETER_2 = 32;
//Внутренние сигналы модуля
wire [INTERNAL_PARAMETER_1-1:0] internal_bus;
reg [INTERNAL_PARAMETER_1-1:0] internal_reg;
logic [7:0] internal_logic;
//Непосредственно архитектура модуля и пользовательская логика
//В том числе подключение других модулей
module_2
#( //Подключение параметров модуля module_2
.IN_DATA_WIDTH (PARAMETER_3),
.OUT_DATA_WIDTH (64 )
)
module_2_inst //Имя конкретного экземпляра модуля module_2
( //Подключение сигналов модуля module_2
.clk (clk ),//input wire
.reset_n (internal_logic[0] ),//input wire
.data_in (internal_reg[INTERNAL_PARAMETER_1/2-1:0]),//input wire
.data_out (internal_reg[INTERNAL_PARAMETER_1-1:
INTERNAL_PARAMETER_1/2] ) //output wire
);
assign output_signal = internal_bus[128:96];
//Конец модуля
endmodule
Пример 2. Пример расширенного описания модуля на языке SystemVerilog
Во-первых, в наших примерах мы встречаемся с конструкциями типа // и /* */. Данные символы обозначают комментарии. Комментарии являются важной частью любого языка программирования, включая Verilog. Они позволяют разработчикам оставлять заметки и объяснения в коде, которые не влияют на его функционирование, но помогают в понимании структуры и логики программы. В Verilog существует два типа комментариев:
Однострочные комментарии: Они начинаются с двойного слеша // и продолжаются до конца строки. Все, что написано после //, Verilog игнорирует.
Пример:
// Это однострочный комментарий
assign a = b; // Другой комментарий справа от кода
Многострочные (блочные) комментарии: Они начинаются с /* (открывающий) и заканчиваются на */ (закрывающий). Все, что находится между этими символами, является частью комментария, независимо от количества строк. К сожалению, вложенность блочных комментариев не поддерживается и комментарий будет заканчиваться на первом закрывающем символе, вне зависимости от того, сколько было открывающих. Т.е. закомментировать участок кода, в котором уже были блочные комментарии без дополнительных действий не получится.
Пример:/* Это многострочный комментарий, который продолжается на несколько строк */ assign a = b;
Важность комментариев.
Документирование: Комментарии используются для описания того, что делает определенный участок кода, особенно если логика за ним не очевидна.
Упрощение понимания: Они помогают другим разработчикам (или вам в будущем) быстрее понять намерения и функционирование кода.
Отладка: Иногда комментарии используются для временного исключения части кода во время отладки или тестирования.
Лучшие практики.
Краткость и ясность: Комментарии должны быть лаконичными и точно передавать намерения кода.
Актуальность: Устаревшие комментарии могут вводить в заблуждение, поэтому важно поддерживать их актуальность.
Избегание избыточности: Не следует комментировать очевидные вещи, так как это может загромождать код.
Важно помнить, что хорошо написанный код часто сам по себе является самодокументируемым, а комментарии должны добавлять ценность, а не просто дублировать то, что и так ясно из кода.
В примере 1 приведено описание простейшего модуля. Границы описания модуля задаются двумя ключевыми словами module и endmodule. Указывается название модуля (module_1) и в области, ограниченной скобками, задаются входные и выходные сигналы. В расширенной версии примера до начала описания модуля появляется область с подключением внешних элементов. К списку сигналов модуля добавляется список параметров. Отображены определения внутренних сигналов и параметров, а также показан пример подключения другого модуля внутри нашего. Более подробно отдельные элементы описания мы рассмотрим позже.
Начнем с существующих простейших типов данных. В первую очередь следует отметить, что в Verilog (так же, как и в большинстве языков программирования) разделяются понятия объекта и типа данных (хотя в нашем случае под данными может пониматься и некоторая физическая сущность, элемент схемы). Тип данных - это категоризация абстрактного множества возможных значений, характеристик и набор операций для некоторого атрибута. Объект - это именованный экземпляр, имеющий свою совокупность значений и соответствующий ему тип данных. Самому страшно от такого определения, поэтому другими словами, тип данных - это правило, описывающее формат некоторой совокупности данных, а объект - это экземпляр, созданный по этому правилу, имеющий свое имя и заданную совокупность значений. Так в примере 2 мы описываем тип данных module_1 и в соответствии с нашим описанием экземпляр (часто употребляется instance - инстанс) данного модуля будет включать в себя, например, объекты internal_bus и module_2_inst. Тип данных объекта internal_bus - wire, объекта module_2_inst - module_2.
Наиболее часто встречающимися простейшими типами данных являются wire (провод) и reg (регистр). wire - задает связь между элементами схемы, что может рассматриваться как реальный провод между подключенными частями. Так в примере 2 мы создаем шину internal_bus, разрядность которой равна 128 (о localparam мы поговорим позже), и старшую половину шины подключаем к выходу data_out модуля module_2_inst. Потом с помощью ключевого слова assign старшие 32 разряда шины подключаем к выходной шине output_signal модуля module_1. reg - тип данных, задающий элемент памяти, хранящий указанное количество бит информации. Чаще всего, при синтезе схемы, реализуется в виде физического регистра (D-триггер (Flip-Flop, FF) или массив из них) из состава ПЛИС. В нашем примере мы определяем регистр internal_reg с разрядностью 128. Пока что мы не указали, где на этот регистр присваивается значение, но отметили, что выход младшей половины регистра подключен к порту data_in модуля module_2_inst. Также стоит упомянуть тип logic, читал, что он дает более широкие возможности компилятору решать во что физически синтезируется наша величина, может использоваться с assign, однако в стандарте IEEE Std 1800-2012 указывается, что это просто другое наименование того же типа данных, что и reg. В новых проектах рекомендуют использовать именно logic, но, учитывая, что серьезной разницы между ними нет, будем использовать то, что в конкретной ситуации нам кажется предпочтительнее. Кроме того, следует помнить, что сами по себе ни reg, ни logic не гарантируют наличие физического регистра. Регистр будет создан, когда синтезатор обнаружит в вашем коде описание последовательной схемы, которая по тактовому сигналу сохраняет значение и передает его для дальнейшего использования.
По умолчанию разрядность wire и reg равна одному (1 провод или элемент памяти на 1 бит - скаляр). Чтобы указать требуемую разрядность после ключевого слова типа данных ставятся квадратные скобки в которых указываются старший и младший разряды, разделенные двоеточием. В этом случае определение будет задавать вектор. Классически первым указывается старший разряд. Этот момент немного отличается от концепции в программировании, когда отсчет начинается с нулевого адреса и иногда может приводить к небольшим коллизиям, например, когда сопоставляешь две структуры, одна из которых находится в области программного обеспечения, а другая на ПЛИС. Чтобы обратиться к конкретному элементу вектора нужно указать его номер в квадратных скобках, например internal_bus[5]. Также можно обратиться к части вектора: internal_bus[128:96]. Кроме того, компиляторы, как правило, понимают подключение к разрядной области переменного значения, например: internal_bus[internal_logic] и в этом случае реализуют на схеме мультиплексор. Однако, конструкции в виде internal_bus[internal_logic+8:internal_logic] у меня синтезировать уже не получалось.
Один разряд может хранить/передавать одно из четырех возможных состояний. Во-первых, это всем хорошо известные и, я надеюсь, понятные '1' и '0', а так же 'z' и 'x'. X - неопределенное состояние линии, например, состояние на ней, до того, как линия была проинициализирована. Физически, конечно, на ней будет какое-то определенное напряжение, но 'x' показывает нам, что оно нам не известно и мы им не управляем в данный момент. Z-состояние - это высокоимпедансное состояние, означающее, что линия не подключена к какому-либо драйверу ("висит в воздухе"). Два этих состояния применяются при симуляции работы схем, кроме того, X-состояние может применяться в некоторых условных конструкциях для игнорирования отдельных разрядов значения, а Z-состояние применяется в реальных схемах, например при коммутации двунаправленных шин (inout), передатчик выставляет на выходе 'z', чтобы приемник мог получить внешний сигнал.
Числа, которые мы можем передавать по нашим шинам, мы можем записывать в следующем формате: 32'h89AB_CDEF, где 32 - это количество бит, занимаемых числом (старшие биты заполняются нулями, если число занимает меньшее количество разрядов), 'h' - указатель на то, что число будет записано в шестнадцатиричной системе, кроме этого есть также 'd' - десятичная система, 'o' - восмиричная система, 'b' - двоичная система, например: 8'b11100101 и 78'd145389. Нижнее подчеркивание не влияет на само число и его можно использовать для удобства визуализации числа. Если число записывается без указания разрядности и типа системы счисления, то по умолчанию число считается десятичным 32-х разрядным. Для некоторых вопросов моделирования может быть удобным использовать тип данных integer - 32-разрядная целочисленная переменная. Также возможна запись чисел с плавающей точкой в формате 0.0. Для работы с ними существует тип данных real. В некоторых случаях данные могут вводится в виде строки символов, например: "String_Example".
В Verilog, помимо основных типов данных, таких как wire и reg, существуют конструкции для определения констант и константных выражений. Эти элементы языка предназначены для упрощения проектирования и увеличения гибкости кода, позволяя использовать повторно одни и те же блоки кода с различными параметрами. Двумя основными конструкциями, используемыми для этой цели, являются parameter и localparam.
parameter - Этот тип используется для определения значений, которые могут быть изменены при вызове модуля. Параметры parameter делают модуль более универсальным, позволяя настраивать его поведение без изменения основного кода. Например, parameter WIDTH = 8; определяет параметр WIDTH, который можно использовать для задания размера шины или количества битов в регистре внутри модуля. Параметры могут быть переопределены при инстанцировании модуля, что обеспечивает дополнительную гибкость.
localparam - В отличие от parameter, localparam используется для определения локальных констант внутри модуля, которые не могут быть изменены извне. Эти параметры полезны для создания внутренних констант, упрощающих понимание и поддержку кода. Например, localparam IDLE_STATE = 0; может определять константное значение для представления состояния покоя в конечном автомате. Значение localparam фиксируется в момент компиляции и не может быть изменено в процессе работы или в других модулях.
Как parameter, так и localparam повышают читаемость кода, позволяя заменить магические числа описательными именами, что облегчает понимание логики модуля и упрощает его модификацию. Они также полезны для определения важных характеристик модуля, таких как размеры шин, тайминги, специфические коды состояний и т.д.
Если у нас есть числа, то логично иметь возможность совершать операции над ними. Verilog предусматривает наличие следующих операций:
Знак операции | Название | Тип данных операндов |
---|---|---|
= | Бинарный оператор присваивания | Любой |
+= -= /= *= | Бинарные операторы арифметического присваивания | Integral, real, shortreal |
%= | Бинарный оператор присваивания арифметического остатка | Integral |
&= |= ^= | Бинарные операторы побитового присваивания | Integral |
>>= <<= | Бинарные операторы присваивания логического сдвига | Integral |
>>>= <<<= | Бинарные операторы присваивания арифметического сдвига | Integral |
?: | Условный оператор | Любой |
+ - | Унарные арифметические операторы | Integral, real, shortreal |
! | Унарный оператор логического отрицания | Integral, real, shortreal |
~ & ~& | ~| ^ ~^ ^~ | Унарные операторы логической редукции | Integral |
+ - * / ** | Бинарные арифметические операторы | Integral, real, shortreal |
% | Бинарный оператор арифметического остатка | Integral |
& | ^ ^~ ~^ | Бинарные побитовые операторы | Integral |
>> << | Бинарные операторы логического сдвига | Integral |
>>> <<< | Бинарные операторы арифметического сдвига | Integral |
&& || –> <–> | Бинарные логические операторы | Integral, real, shortreal |
< <= > >= | Бинарные операторы сравнения | Integral, real, shortreal |
=== !== | Бинарные операторы сравнения с учетом регистрового значения | Любой, кроме real и shortreal |
== != | Бинарные операторы логического сравнения | Любой |
==? !=? | Бинарные операторы сравнения с учетом шаблонов | Integral |
++ -- | Унарные операторы инкремента и декремента | Integral, real, shortreal |
inside | Бинарный оператор проверки принадлежности множеству | Единственность левого операнда |
dist | Бинарный оператор генерации случайных значений с заданным распределением | Integral |
{} {{}} | Операторы конкатенации и репликации | Integral |
{<<{}} {>>{}} | Потоковые операторы | Integral |
Таблица 1. Операции и типы данных в SystemVerilog. |
Оператор | Ассоциативность | Приоритет |
---|---|---|
| Слева направо | Высший |
| ||
| Слева направо | |
| Слева направо | |
| Слева направо | |
| Слева направо | |
| Слева направо | |
== != === !== ==? !=? | Слева направо | |
| Слева направо | |
| Слева направо | |
| (бинарный) | Слева направо | |
| Слева направо | |
|| | Слева направо | |
| Справа налево | |
| Справа налево | |
| Нет | |
| Самый низкий | |
Таблица 2. Приоритет действий в SystemVerilog. |
Операции могут быть унарными, бинарными и тернарными.
Унарные операции выполняются над одним операндом. К унарным операциям относятся, например, логическое НЕ (~), инкремент (++) и декремент (--).
Бинарные операции включают в себя операции, которые применяются к двум операндам. Это могут быть арифметические операции (сложение +, вычитание -, умножение *, деление /, модуль %), битовые операции (И &, ИЛИ |, исключающее ИЛИ ^), сдвиги (<< для сдвига влево и >> для сдвига вправо) и другие.
Тернарные операции - это операции с тремя операндами, наиболее известной из которых является условная операция (?:), которая работает как упрощенный аналог конструкции if-else.
Кроме того, Verilog определяет приоритет операций, который важен для понимания порядка выполнения операций в сложных выражениях. Например, арифметические операции имеют более высокий приоритет, чем логические, что означает, что они будут выполнены первыми. Добавлю, не стесняйтесь использовать скобки, это часто позволяет избежать непредвиденного поведения схемы.
Давайте подробнее рассмотрим некоторые из этих операций:
Логические операции: Используются для манипулирования булевыми значениями. Например, A && B (логическое И) возвращает 1, если и A, и B истинны.
Арифметические операции: Позволяют выполнять математические расчеты. Например, A + B складывает A и B.
Битовые операции: Применяются к каждому биту операндов отдельно. Например, A | B (битовое ИЛИ) устанавливает в 1 те биты в результате, которые установлены в 1 в A или B.
Операции сдвига: Используются для сдвига битов в переменной влево или вправо. Например, A << 2 сдвигает все биты в A на две позиции влево.
Условная операция: A ? B : C возвращает B, если A истинно (не ноль), и C в противном случае.
Операция конкатенации: {A,B} объединяет две шины в одну, в виде {A{B}}, где A – константа, формирует новую шину/значение из B взятого A раз.
Эти операции являются фундаментальными в Verilog и позволяют реализовывать сложную логику обработки данных в проектах ПЛИС.
Мы упомянули про непрерывное присваивание assign для wire. assign просто подсоединяет к шине другую шину или присваивает ей константное значение. В случае с reg присваивание значения чаще всего происходит синхронно с некоторым тактирующим сигналом. Давайте рассмотрим пример 3.
reg [3:0] reg_a = 8'h00;
reg [3:0] reg_b = 8'h00;
wire [7:0] wire_b;
always @(posedge clk)
begin
if(!reset_n)
begin
reg_a <= 8'h00;
reg_b <= 8'h00;
end
else
begin
case(reg_a)
0: begin reg_a <= reg_a + 1'b1; reg_b <= reg_a; end
1: begin reg_a <= reg_a + 1'b1; reg_b <= reg_a; end
2: begin reg_a <= reg_a + 1'b1; reg_b <= reg_a; end
3: begin reg_a <= reg_a + 1'b1; reg_b <= reg_a; end
4: begin reg_a <= reg_a + 1'b1; reg_b <= reg_a; end
5: begin reg_a <= reg_a + 1'b1; reg_b <= reg_a; end
6: begin reg_a <= reg_a + 1'b1; reg_b <= reg_a; end
7: begin reg_a <= 8'h00 ; reg_b <= reg_a; end
end
end
assign wire_b = {reg_a+8'd4, reg_b};

Пример 3. Пример использования always-блока. P.S. В реальном коде следует подбирать осмысленные наименования сигналов. Конструкция "case" в данном виде избыточна, так как практически все ветви выполняют одно и то же, здесь она приведена просто для иллюстрации синтаксиса.
В примере 3 приведена одна из важнейших конструкций языка – always-блок. Здесь по событию, которое описывается в списке чувствительности (заключен в скобки после символа @), т.е. по переднему фронту тактового сигнала, выполняется участок кода, заключенный между ключевыми словами begin-end.
Список чувствительности определяет события, на которые должен реагировать блок. Это могут быть изменения уровня или фронта на сигналах. Например:
@(posedge clk) реагирует на передний фронт тактового сигнала clk.
@(negedge reset) активируется по заднему фронту сигнала reset.
@(a or b or c) срабатывает при изменении состояния любого из сигналов a, b, или c.
Конструкция begin-end используется в Verilog для группировки нескольких операторов в один блок. Это важно в ситуациях, где нужно, чтобы несколько операторов рассматривались как единое целое, особенно в условных выражениях (if, else) и циклах (for, while). Также как и в случае со скобками begin-end можно активно использовать для выделения участков кода, например, можно выделить даже одну операцию, хотя оператор if и не принуждает использовать begin-end если операция одна. Это нужно, чтобы избежать путаницы и в последствии вы могли бы спокойно добавить еще одну операцию под if. Впрочем, лучше всего пользоваться begin-end, отталкиваясь от каждой конкретной ситуации. Учитывая распространенность конструкции, на мой взгляд, использование данных ключевых слов является избыточным и можно было бы, например, использовать для этого фигурные скобки, а под конкатенацию выделить какое-то ключевое слово. Впрочем, это один из немногих избыточных моментов в Verilog. Конструкция begin-end широко применяется как в синтезируемых конструкциях, так и при моделировании. Помимо нее существует конструкция fork-join. Она используется для моделирования параллельных процессов. В блоке fork-join, все операторы выполняются параллельно, что отличается от последовательного выполнения в begin-end.
Последовательность выполнения операций в begin-end является весьма условной и в большей степени зависит от используемого типа присваивания.
Типы присваиваний: Внутри always-блока обычно используются два типа присваиваний:
Блокирующее присваивание (=): Выполняет присваивание последовательно.
Неблокирующее присваивание (<=): Позволяет одновременное присваивание нескольких значений.
Блокирующее присваивание выполняется последовательно. Когда компилятор Verilog встречает блокирующее присваивание, он синтезирует схему, которая ожидает завершения присваивания, прежде чем переходить к следующему оператору. Это похоже на присваивание в большинстве традиционных языков программирования.
Пример:
a = b;
c = a + 1;
Здесь сначала выполняется a = b, и только после его завершения начинается выполнение c = a + 1. Примером такого поведения, может быть поведение сигналов в комбинационных схемах, где сигналы сначала проходят через один блок логики, а после попадают на следующий.
Неблокирующее присваивание позволяет выполнять операторы параллельно. Когда встречается неблокирующее присваивание, Verilog не ждет его завершения для выполнения следующего оператора. Это особенно полезно при моделировании поведения синхронных цифровых схем, где изменения состояния происходят на границах тактового сигнала.
Пример:
always @(posedge clk)
begin
a <= b;
c <= a + 1;
end
Здесь a <= b и c <= a + 1 выполняются параллельно. Значение a в выражении c <= a + 1 будет старым значением до тактового сигнала, так как все неблокирующие присваивания рассматриваются как происходящие одновременно, в момент, описанный в списке чувствительности.
Использование:
В последовательных схемах: Неблокирующее присваивание предпочтительно для описания поведения синхронных цифровых схем, таких как регистры и счетчики. Это помогает предотвратить ошибки в таймингах и обеспечивает более четкое и предсказуемое поведение.
В комбинационных схемах: Блокирующее присваивание обычно используется в комбинационных схемах, так как порядок выполнения операций важен для их правильной работы.
Важность порядка: В блоках с блокирующими присваиваниями порядок операторов критичен. В неблокирующих присваиваниях порядок менее значим, так как все они рассматриваются как выполняющиеся одновременно.
Вернемся к примеру 3. Внутри блока у нас находится условие if-else, в котором по событию проверяется состояние сигнала сброса, при отсутствии сброса выполняется участок кода внутри else, где в свою очередь стоит конструкция case. Здесь проверяется состояние регистра reg_a и в зависимости от его значения выполняется одна из ветвей конструкции. Внутри каждой ветви двум регистрам присваивается новое значение. Далее значение reg_b посредством постоянного присваивания assign передается на шину wire_b, в старшие разряды которой посредством конкатенации присваивается выход комбинационной схемы, прибавившей к значению reg_a число 4. Попробуем синтезировать наш модуль и получим схему, изображенную рядом на примере.
В синтезируемом коде я наиболее часто использую конструкцию if-else. Конструкция if-else в Verilog работает аналогично ее версиям в большинстве языков программирования (естественно, с поправкой на особенности hdl). Она позволяет выполнять определенные действия в зависимости от истинности или ложности заданного условия.
Пример:
if (условие)
begin
// Действия, если условие истинно
end
else
begin
// Действия, если условие ложно
end
Реже применяется конструкция case. Конструкция case используется для создания множественных условных ветвлений. Подобна оператору switch в других языках программирования. Удобна, например, для реализации конечных автоматов (finite state machine - fsm).
Пример:
case (условие)
value1: // Действия для value1
value2: // Действия для value2
default: // Действия по умолчанию
endcase
Помимо условных конструкций я иногда применяю цикл for. Чаще всего это происходит в генеративных блоках для создания повторяющихся структур. О генеративных структурах и ключевом слове generate мы поговорим позже.
Пример:
for (начальное значение; условие выхода; инкремент)
begin
// Тело цикла
end
Помимо вышеуказанных существует еще довольно много различных конструкций, таких как while, do-while, forever, foreach, repeat, но они уже чаще используются при симуляции и в синтезируемом коде крайне редки.
Кроме вышеобозначенного применения always-блока существует версия always @(*) или always @*, которая реагирует на изменения любого сигнала в его теле. Он автоматически определяет список чувствительности, включая все сигналы, используемые внутри блока. Это полезно для создания комбинационной логики, где выход изменяется всякий раз, когда изменяется любой из входов.
SystemVerilog, как расширение Verilog, вводит дополнительные возможности для always блоков, делая их более гибкими и мощными для разработки аппаратуры. Вот основные виды always блоков в SystemVerilog:
1. always_comb
Этот блок предназначен для описания чистой комбинационной логики. Он автоматически задает список чувствительности, поэтому разработчику не нужно его указывать вручную. Это улучшает читаемость и облегчает поддержку кода.
Пример:
always_comb
begin
out = a & b;
end
2. always_ff
Этот блок предназначен для реализации синхронных схем, таких как регистры или счетчики. Он обеспечивает более четкое разделение между синхронной и комбинационной логикой и повышает надежность кода, предотвращая случайное создание лэтчей.
Пример:
always_ff @(posedge clk or negedge reset)
begin
if (!reset)
reg <= 0;
else
reg <= reg + 1;
end
3. always_latch
Этот блок используется для описания уровневых лэтчей. Хотя лэтчи редко используются в современном цифровом проектировании из-за их потенциальной нестабильности, always_latch обеспечивает явный способ их описания.
Пример:
always_latch
begin
if (enable)
latch_reg = data;
end
Важные Замечания
Явная Семантика: always_comb, always_ff, и always_latch явно указывают на намерения разработчика, делая код более понятным и предсказуемым при синтезе.
Автоматический Список Чувствительности: always_comb и always_latch автоматически определяют список чувствительности, уменьшая вероятность ошибок и упрощая поддержку кода.
Повышение Надежности: Использование этих блоков по их прямому назначению помогает избежать случайного создания нежелательных элементов аппаратуры, таких как лэтчи или нестабильные комбинационные цепи.
SystemVerilog предлагает более структурированный и безопасный подход к описанию логики, что особенно важно в сложных системах на кристалле и крупных проектах.
В данной главе мы ознакомились с историей и основными возможностями языка описания аппаратуры Verilog, рассмотрели базовые принципы построения проектов на ПЛИС. Настоятельно рекомендую самостоятельно пролистать один из последних стандартов языка, например, IEEE 1800-2012. Тогда вы убедитесь, что мы не прошли даже 1% от всех возможностей языка. Впрочем, пусть это вас не пугает, начинать работать можно с достаточно ограниченным количеством конструкций и постепенно расширять свой кругозор, по мере приобретения опыта.
Задание 2.
Написать модуль, мигающий светодиодом (HelloWorld-проект для ПЛИС).
Топ-модуль проекта, содержит один входной сигнал – тактовый сигнал (клок) и один выходной сигнал – сигнал светодиода. Тактовый сигнал представляет собой меандр с частотой 200 МГц. Необходимо с помощью always-конструкции реализовать счетчик, который будет досчитывать до некоторого значения Т, инвертировать сигнал светодиода и сбрасываться в «0», начиная новый цикл. Значение Т должно быть таким, чтобы светодиод моргал с периодом 1 секунда. После реализации можно поэкспериментировать с режимами мигания.
Содержание курса |
1. Что такое ПЛИС | 3. Процесс сборки и системы симуляции