2. Комбинационные схемы

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

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

Разберёмся же с концепцией комбинационных схем на примере сумматора. Нужно сложить два бита с переполнением (оно же — перенос разряда). Разберём код, реализующий эту функциональность:

src/02-combination-logic/adder_plus_1_bit.sv
module adder_plus_1_bit (
    input logic a,  (1)
    input logic b,  (2)
    input logic c_in,  (3)
    output logic c_out,  (4)
    output logic sum  (5)
);  (6)
  assign {c_out, sum} = a + b + c_in;  (7)
endmodule
1 Первый входной бит.
2 Второй входной бит.
3 Бит, отвечающий за переполнение на входе.
4 Бит, отвечающий за переполнение на выходе.
5 Результат сложения.
6 Не забывайте ставить ;.
7 Сложение работает как привычное сложение двоичных чисел. Соответственно, сначала будет один бит переполнения, а потом — результирующий бит. Отловить это можно с помощью оператора конкатенации[2] через фигурные скобки. Тогда мы явно свяжем шину, состоящую из двух битов (c_out и sum) с результатом сложения. Также важно отметить использование непрерывного присваивания через assign[3]. Оно "сцепляет" элементы друг с другом, то есть при любом изменении одного элемента, изменится и другой.
Непрерывное присваивание — это единственный способ "положить" что-то в результирующие переменные. То есть выражения для них могут быть очень сложными, со сложной логикой, но всё сведётся к assign output_var = expr.

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

src/02-combination-logic/adder_logic_1_bit.sv
module adder_logic_1_bit (
    input  logic a,
    input  logic b,
    input  logic c_in,
    output logic c_out,
    output logic sum
);

  assign sum   = a ^ b ^ c_in;

  assign c_out = (a & b) | (c_in & (a ^ b));
endmodule

Можно воспользоваться конкатенацией, как в прошлом примере, это избавит нас от лишнего assign:

assign {sum, c_out} = {a ^ b ^ c_in, (a & b) | (c_in & (a ^ b))};

Однако, как видно, это несколько ухудшает читаемость кода.

Важно не забывать специфику: все модули (если в них не используются некоторые специальные конструкции), написанные на SystemVerilog, должны синтезироваться в принципиальную схему. Существуют механизмы, позволяющие сделать это. Подробнее, как получить схему в виде картинки, можно почитать тут. Проделав необходимые действия с нашим сумматором, получаем схему:

adder logic 1 bit
Рисунок 1. Синтезированная схема сумматора

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

src/02-combination-logic/adder_logic_1_bit_tb.sv
module adder_logic_1_bit_tb;

  logic a, b, c_in, c_out, sum; (1)

  adder_logic_1_bit add ( (2)
      .a(a), (3)
      .b(b),
      .c_in(c_in),
      .c_out(c_out),
      .sum(sum)
  );

  initial (4)
    begin
      a = 0;
      b = 1;
      c_in = 0;
      #10; (5)
      $display("%b (из переполнения) + %b + %b = %b (%b в переполнении)",
              c_in, a, b, sum, c_out);
      a = 1;
      b = 1;
      c_in = 0;
      #10;
      $display("%b (из переполнения) + %b + %b = %b (%b в переполнении)",
              c_in, a, b, sum, c_out);
      a = 0;
      b = 0;
      c_in = 1;
      #10;
      $display("%b (из переполнения) + %b + %b = %b (%b в переполнении)",
              c_in, a, b, sum, c_out);
    end
endmodule
1 Объявляем всё, что нам потребуется для работы.
2 Задаём тестируемый модуль[4]. Тут почти то же самое, что и вызов функции, но результат записывается в выделенные переменные и изменяется непрерывно с изменением входных данных.
3 Передавать аргументы можно и просто по порядку, но их зачастую больше, чем мы привыкли, легко запутаться.
4 Выполняем блок в начальный момент времени[5]. Этот блок ломает синтезируемость модуля, так что обычно его используют только в тестбенчах.
5 В SystemVerilog всё довольно сложно с последовательностью выполнения команд, здесь серьёзное отличие с привычными нам языками, потому что тут мы можем управлять ходом выполнения программы потактово. Так что, чтобы быть уверенным в том, в какой момент исполнятся команды, нужно задавать задержку[6] (либо делать что-то хитрее, о чём мы поговорим позже). Именно это мы и сделали. У нас задержка в 10 наносекунд (это изменяемый параметр, называемый time_unit[7]), можно взять и другую, но исторически повелось брать именно 10.

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

$ iverilog -s adder_logic_1_bit_tb adder_logic_1_bit.sv adder_logic_1_bit_tb.sv -g2012 -o build/adder_logic_1_bit_tb

Флаг -s позволяет явно указать верхний модуль, в нашем случае это adder_logic_1_bit_tb. Конкретно здесь можно обойтись и без него, но если проект достаточно сложный, то лучше явно прописывать. Подробнее о том, какие есть флаги и что они позволяют сделать, можно почитать здесь.

После запустим его:

$ build/adder_logic_1_bit_tb

Вывод должен выглядеть так:

0 (из переполнения) + 0 + 1 = 1 (0 в переполнении)
0 (из переполнения) + 1 + 1 = 0 (1 в переполнении)
1 (из переполнения) + 0 + 0 = 1 (0 в переполнении)

В следующем разделе мы подробнее поговорим про тестбенчи и настроим CI.

После прохождения обязательно ознакомьтесь с упражнениями.