Программирование Xilinx Введение в Verilog Mon, December 09 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


Введение в Verilog Печать
Добавил(а) microsin   

Написание кода на Verilog позволяет нам сфокусироваться на поведении аппаратуры с точки зрения высокого уровня вместо того, чтобы описывать схему на низком уровне с помощью низкоуровневых элементов логики. Существует множество качественной литературы по программированию на HDL-языках Verilog и VHDL. Однако, к сожалению, часто такие книжки велики по объему и довольно трудны для освоения и понимания. Это руководство (перевод [1]) делает попытку ускоренного знакомства с Verilog для тех, кто хочет быстро начать программировать на практике.

Разработка цифровой схемы на Verilog обычно включает в себя два основных процесса - тест на основе симуляции и реализация рабочего кода.  На рисунке ниже показаны эти процессы. Сначала нужно передать исходные файлы Verilog в инструментарий симуляции, что показано на рисунке слева. Инструмент симуляции программно эмулирует реальное поведение аппаратуры схемы для определенных входных воздействий, что описывается в специально написанном тесте (testbench). Поскольку компиляция нашего кода Verilog для инструмента симуляции осуществляется относительно быстро, то этот шаг используется главным образом для оценки общей работоспособности дизайна.

Примечание: на рисунке слева показана симуляция с помощью пакета ModelSim, однако симуляцию можно реализовать и с помощью бесплатного симулятора iSIM, входящего в пакет разработки Xilinx ISE Webpack. Процесс установки этого пакета и описание симуляции в iSIM показаны в статье [3].

Simulation flow left and Synthesis flow right

На рисунке справа упрощенно показан процесс реализации проекта после успешного прогона теста в симуляторе. После того, как мы удостоверились в корректности высокоуровневого исходного кода Verilog, мы используем инструментарий синтеза для превращения этого кода в низкоуровневый список логических вентилей (gate netlist). Затем утилита отображения (mapping tool) привязывает netlist к соответствующим ресурсам используемого кристалла логики (микросхема CPLD или FPGA). На заключительном шаге мы загружаем конфигурационный поток данных (bitstream) в выбранную микросхему логики (это делается с помощью кабеля JTAG). В результате получается готовая схема, выполняющая нужные логические функции.

[Философия Verilog]

У Verilog синтаксис похож на язык C. Однако концептуально Verilog отличается от большинства языков программирования, поскольку Verilog описывает аппаратуру, а не программу. В частности:

• Операторы Verilog по своей природе изначально конкурентны (они выполняются в реальном времени параллельно). Это означает, что кроме кода, который появляется между ограничителями блока begin и end, нет заранее определенного порядка, в каком выполняются операторы. Такое поведение отличается от большинства языков программирования наподобие C, в которых подразумевается, что операторы выполняются последовательно, друг за другом. Первая строка в функции main() будет выполнена первой, затем вторая строка, и так далее.
• Синтезируемый код Verilog в конечном счете отображается (map) на реальные аппаратные вентили логики. С другой стороны, компилируемый код C отображается на некие биты в памяти, которые CPU будет или не будет интерпретировать как команды и/или данные при выполнении программы.

[Синтаксис синтезируемой комбинаторной логики Verilog]

Модули. Базовый блок программы Verilog это оператор module. Он в чем-то является аналогом определения функции на языке C:

module < имямодуля >(< список_входов_и_выходов >[, ..., ...]);
   // Список входов и выходов:
   input < имявхода >;
   output < имявыхода >; 
 
   // Далее идет список переменных, если он есть, и код модуля:
   ...
endmodule

Ниже в качестве примера приведен модуль, у которого 3 входа: два 5-разрядных операнда a и b, вход разрешения работы en, и также есть выход a_gt_b. Модуль называется comparator (для простоты код логики модуля не приведен).

module comparator(a, b, en, a_gt_b);
   input [4:0] a, b;
   input en;
   output a_gt_b;
endmodule

В таком состоянии код модуля не производит никаких действий - по двум причинам. Во-первых, в теле модуля нет описания логики для действий, которые из входных сигналов генерировали бы сигнал на выходе. Во-вторых, определение модуля само по себе недостаточно для реализации работы логики (кроме случая так называемого модуля верхнего уровня), и нам нужно сделать так называемую инстанциацию модуля, т. е. нужно создать экземпляр модуля, который будет делать реальную работу.

Инстанциация модулей. Для создания экземпляра модуля в коде другого модуля используется следующий синтаксис:

< имямодуля > < имяэкземпляра >(< списокпортов >);

Портами называются сигналы, с которыми работает модуль. Например, для инстанциации модуля компаратора с именем экземпляра comparator1, входными сигналами in1, in2 и en, и выходом gt (они будут составлять списокпортов), мы должны написать следующий код:

comparator comparator1(in1, in2, en, gt);

Эта инстанциация зависит от порядка следования портов в списке аргументов модуля comparator. Также есть альтернативный синтаксис инстанциации модулей, не зависящий от порядка следования портов, часто использующийся по этой причине:

< имямодуля > < имяэкземпляра >(.< имяпорта >(ioname), ...);

Для предыдущего примера получится следующий код:

comparator comparator1(.b(in2), .a(in1), .en(en), .a_gt_b(gt));

Обратите внимание, что несмотря на то, что порядок следования портов в аргументах модуля изменен (b теперь идет перед a), эта инстанциация теперь все еще будет работать корректно, потому что дополнительно указаны конкретные сигналы, к которым подключаются порты.

Комментарии. Комментарии Verilog добавляются по таким же правилам, что и на языке C.

// Это однострочный комментарий.
/* Комментарий, состоящий
из нескольких строк. */

Числовые литералы. Многие модули будут содержать в коде числовые литералы. По умолчанию на Verilog числовые литералы обрабатываются как 32-битные числа без знака, однако следует выработать в себе привычку декларировать ширину (разрядность) каждого такого числового литерала. Это приведет к меньшему количеству догадок, каким образом нужно связывать сигнал (wire) и числовой литерал (что показано ниже).

Примеры числовых литералов:

/* Общий синтаксис:
   < количествобит >'< системасчисления >< число >
   Здесь системасчисления указывается символами:
      b двочиная (binary
      d десятичная (decimal)
      h шестнадцатиричная (hexadecimal) */
 
wire [2:0] a = 3'b111;  // 3-разрядное двоичное число, в котором все разряды в лог. 1
wire [4:0] b = 5'd31;   // 5-разрядное десятичное число 31
wire [31:0] c = 32'hdeadbeef; // 32-битное шестнадцатеричное число 0xdeadbeef

Мы пока не определили, что что такое сигнал (wire), сделаем это позже.

Константы. Мы можем использовать оператор `define, чтобы определить глобальные константы в коде, наподобие директивы препроцессора #define на языке C. Обратите внимание, что в отличие от языка C, когда осуществляется ссылка на константу, нам нужно добавить символ обратной кавычки (backtick) перед константой: например, в нашем случае мы должны использовать `FRI вместо FRI. Также (как и на C) не добавляйте точку с запятой к к оператору `define.

`define RED 2'b00    // в оператор `define точку с запятой добавлять не нужно,
`define WHITE 2'b01  // как и в операторе #define на языке C
`define BLUE 2'b10
 
wire [1:0] color1 = `RED;
wire [1:0] color2 = `WHITE;
wire [1:0] color3 = `BLUE;

Сигналы (wire). Для начала разберемся, как декларировать 2 типа данных в наших модулях: сигналы (wire) и регистры (reg). Вы можете думать о сигналах как о физических проводах - их можно подключать либо к другому wire, к входному или выходному порту, или к логическому значению константы. Чтобы декларировать сигнал, мы используем оператор wire:

wire a_wire;
wire [1:0] two_bit_wire;
wire [4:0] five_bit_wire;

Для подключения сигнала к чему-нибудь мы используем оператор assign. Предположим, что у наш модуль принимает двухбитный сигнал и именем two_bit_input, тогда нам нужно сделать следующее:

assign two_bit_wire = two_bit_input;
// Подключим сигнал a_wire к младшему биту two_bit_wire:
assign a_wire = two_bit_wire[0];
/* {} обозначает конкатенацию - в 3 старших битах будет значение 101,
   а 2 младших бита будут подключены к two_bit_wire */
assign five_bit_wire = {3'b101, two_bit_wire};
// Эта строка приведет к ошибке, потому что нельзя применять assign
// дважды к одному и тому же wire:
// assign a_wire = 1’b1;

Обратите внимание, что assign выполняет НЕПРЕРЫВНОЕ (continuous) присваивание. Это означает в предыдущем примере, что всегда, когда меняется входной сигнал two_bit_input, то также поменяется значение two_bit_wire, a_wire и five_bit_wire. Нет никакого порядка очередности для этих изменений сигналов - все они происходят одновременно. Именно по этой причине нельзя несколько раз применять assign к одному и тому же wire - сигнал wire не может управляться разными выходами одновременно. Одновременное действие операторов assign это именно то, что мы подразумеваем, говоря о Verilog, что он "изначально параллелен".

В заключение рассмотрим краткое выражение, которое иногда используется для декларирования и назначения за один раз:

// Декларируется сигнал gnd и одновременно назначается ему значение 0:
wire gnd = 1'b0;

Регистры. Еще один тип данных, который мы будем использовать, это регистр. Несмотря на имя типа, регистры не подразумевают, что это память. Это просто конструкция языка, обозначающая переменные, которые могут появляться на левой стороне блока always (и в коде симуляции в блоках initial и forever). Вы декларируете регистры наподобие сигналов wire, на верхнем уровне модуля, и Вы не можете делать назначение (assign) для сигналов (wire) внутри блока always.

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

В простейшем случае регистр в операторе always работает наподобие типа данных wire, как в этом простом модуле:

module bitwise_not(a_in, a_out);
   input [1:0] a_in;
   output [1:0] a_out;
 
   // Декларируется 2-битный выход a_out как регистр, поскольку
   // он используется в левой части выражения блока always:
   reg [1:0] a_out;
 
   // Здесь лучше было бы использовать @*, см. следующий пример:
   always @(a_in)
   begin
      a_out = ~a_in;    // out = побитная инверсия входа a_in
   end
endmodule

В этом модуле описывается, что всегда, когда меняется входной сигнал a_in, будет обрабатываться блок always, в результате чего a_out получает значение, вычисленное из значения a_in. Это как бы если мы декларировали a_out как wire, и назначили ему значение побитной инверсии a_in.

Операторы if и case. Еще более интересно, что мы можем поместить операторы case и if внутрь блоков always. Об операторах case и if мы можем думать, что они синтезируются в некий вид мультиплексора. В сущности if и case работают одинаково, однако они существуют отдельно в основном только для того, чтобы сделать код более удобочитаемым.

Ниже показана простая схема, использующая оператор case внутри оператора if:

module alarm_clock(day_i, hour_o, minute_o);
   input [2:0] day_i;
   output [4:0] hour_o;
   output [5:0] minute_o;
 
   wire [2:0] day_i;
 
   /* Декларируются регистры hour_o и minute_o, поскольку они
    * используются в левой части выражения блока always */
   reg [4:0] hour_o;
   reg [5:0] minute_o;
 
   // Сигнал is_weekday получает значение компаратора:
   wire is_weekday;
   assign is_weekday = (day_i <= `FRI);
 
   always @* begin
      if (is_weekday) begin
         hour_o = 5'd8;
         minute_o = 6'd30;
      end
      else begin
         case (day_i)
            `SAT: {hour_o, minute_o} = {5'd11, 6'd15};
            `SUN: {hour_o, minute_o} = {5'd12, 6'd45};
            default: {hour_o, minute_o} = 11'd0;
         endcase
      end
   end
endmodule

В частности, имейте в виду:

• Операторы case и if должны размещаться в блоке always.
• По поводу использования always @*: это новая конструкция Verilog-2001, которая автоматически заполняет список чувствительности всеми переменными, перечисленными в правой части выражений блока always. Если специально не указано нечто другое, Вы должны всегда использовать always @* для списков чувствительности блоков always, что может экономить многие часы отладки.
• Операторы begin end используются для обозначения блоков из нескольких строк (то же самое, для чего используют фигурные скобки {} на языке C). Можно опустить конструкцию begin/end, если присваивания в операторах case или if состоят только из одной строки.
• Фактически каждый оператор case имеет оператор default, и каждый оператор if имеет соответствующий else. Не забывайте об этом, или Вы сгенерируете защелки (latch)!

Параметры. Часто мы хотим создать стандартный модуль, который можно настроить несколькими параметрами, когда для этого модуля создается экземпляр (инстанциация модуля). Это то место, где появляется оператор parameter. Ниже показан пример параметризованного ALU, который по умолчанию 32-разрядный, если при инстанциации модуля не был предоставлен параметр:

`define ADD 3'd0
`define LESS 3'd1
`define EQ 3'd2
`define OR 3'd3
`define AND 3'd4
`define NOT 3'd5
 
module ALU(opcode, op_a, op_b, result);
   parameter N = 32;
   input [2:0] opcode;
   input [N-1:0] op_a, op_b;
   output [N-1:0] result;
 
   // result используется в левой части блока always, следовательно
   // он должен быть определен как регистр:
   reg [N-1:0] result;
 
   always @* begin
      case (opcode)
         `ADD: result = op_a + op_b;
         `LESS: result = op_a < op_b;
         `EQ: result = op_a == op_b;
         `OR: result = op_a | op_b;
         `AND: result = op_a & op_b;
         `NOT: result = ~op_a;
         default: result = 0;
      endcase
   end
endmodule

Тогда для инстанциации ALU внутри другого модуля можно использовать символы #() в строке инстанциации, чтобы применить параметр. Для нашего примера, вот так нужно инстанциировать 16-битное ALU:

ALU #(16) alu1(...)

[Симуляция, тестирование кода]

Код для поддержки тестирования (testbench). До настоящего момента мы писали только синтезируемый код Verilog, т. е. тот код, который синтезируется, транслируется и отображается в реальную аппаратуру (например в вентили FPGA). Однако перед тем, как пробовать нашу логику на реальном устройстве, нам следует удостовериться, что она функционирует корректно. Проверка делается с помощью написания на Verilog кода теста (testbench) и применения программного обеспечения симуляции, чтобы запустить тест.

Verilog testbench это специальный файл, который инстанциирует модуль (или модули), которые нам нужны для прогона теста. Этот testbench не синтезируется в аппаратуру. Вместо этого он предоставляет входные сигналы (стимул) в инстанциированных модулях, благодаря чему мы можем запустить testbench в программном симуляторе нашего проектируемого дизайна логики. Из-за того, что этот код testbench не синтезируется, он может быть написан в другом стиле, отличающемся от обычного синтезируемого модуля Verilog.

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

// Общий синтаксис оператора: #< n >, здесь n задает задержку в единицах времени.
 
#5;   // задержка в 5 единиц времени
#100; // задержка в 100 единиц времени
 
// Также оператор задержки можно скомпоновать с другим оператором,
// чтобы задержать его выполнение:
#3 $display("hi");   // подождать 3 единицы времени, затем отобразить "hi"

Единица времени задается в начале файла Verilog строкой вида:

`timescale 1ns / 1ps

1ns означает шкалу времени для симулятора, а 1ps означает точность учета времени для него. Единицы времени, указанные здесь, применяются при создании тестовых сигналов, подаваемых на вход тестируемой в симуляторе схемы.

Блоки initial. Блоки initial и forever подобны блокам always, которые выполняются в определенном порядке, когда задано их срабатывание. Точно так же в левой части этих блоков можно использовать только регистры. Однако в отличие от блоков always, которые срабатывают всякий раз, когда истинно условие списка чувствительности, блок initial выполняется только один раз в начале программы. Блок forever, судя по своему названию, предназначен для создания бесконечного зацикливания куска кода.

Следующий код устанавливает регистры opcode, op_a и op_b в значения 0, 10 и 20 соответственно в момент времени симуляции t=0, и затем через задержку 5 их назначения меняются на 2, 10 и 20 соответственно (момент времени симуляции t=5):

reg [2:0] opcode;
reg [4:0] op_a, op_b;
 
initial begin
   opcode = 3'b000;
   op_a = 5'd10;
   op_b = 5'd20;
 
   #5 opcode = 3'b010;
   op_a = 5'd10;
   op_b = 5'd20;
end

Оператор display. Используется для отображения значения переменной, для форматирования результата используется printf-подобный синтаксис. При выводе оператор автоматически добавляет в конец строки переход на новую строку.

wire [3:0] ten = 4'd10;
$display("10 in hex: %h, dec: %d, bin: %b", ten, ten, ten);

[Пример testbench]

Используя эти операторы, мы можем создать очень сырой тест для модуля ALU.

module ALU_test;
   /* Декларация регистров, поскольку мы меняем эти значения
      в блоках always */
   reg [2:0] opcode;
   reg [15:0] op_a, op_b;
   wire [15:0] result;  // просто подключено к модулю
 
   // Инстанциация модуля ALU
   ALU #(16) alu(.opcode(opcode), .op_a(op_a),
                 .op_b(op_b), .result(result));
 
   initial begin
      opcode = `ADD;
      {op_a, op_b} = {16'd32, 16'd5};
      // Ожидание 1 единицу времени для отображения установки result:
      #1 $display("%b + %b = %b", op_a, op_b, result);
      #5;
 
      opcode = `OR;
      {op_a, op_b} = {16'd8, 16'd7};
      #1 $display("%b | %b = %b", op_a, op_b, result);
 
      // и т. д.
   end
endmodule
 
/* Вывод в симуляторе:
# 0000000000100000 + 0000000000000101 = 0000000000100101
# 0000000000001000 | 0000000000000111 = 0000000000001111 */

Этот тест далек от завершения. Здесь мы протестировали только установку входов только для двух функций устройства. Также нужно проверить больше случаев, особенно крайние. Также автоматизированный тест лучше, чем ручной тест наподобие этого. Однако приведенный пример просто показывает начальную точку, от которой Вы можете начать писать программу более сложного теста.

[Ссылки]

1. Introduction to Verilog site:cva.stanford.edu.
2. Introduction to Verilog site:lsi.upc.edu.
3. WebPack ISE: быстрый старт.

 
Top of Page