4. Последовательностная логика

Немного теории

До сих пор мы рассматривали только комбинационные схемы. В них выходные сигналы непрерывно зависят от входных, однако возможность сохранять состояние отсутствует. На помощь нам приходит последовательностная (секвенциальная) логика.

Пока что сигналы в наших схемах изменялись "мгновенно", но логика подсказывает, что так ничего запомнить не получится, поэтому нам нужен некий управляющий сигнал. Главное требование к управляющему сигналу (чаще называемому clock или clk) — циклическое изменение уровня сигнала с заданным периодом.

Управляющий сигнал выглядит так:

clk

Момент со стрелочкой вверх называется восходящим фронтом волны, а момент со стрелочкой вниз — нисходящим фронтом волны.

Для сохранения же нам нужна какая-то ячейка памяти. Такой ячейкой обычно является регистр (обычно реализуемый через D-триггер) — элемент, который по заданному условию сохраняет значение приходящего в него сигнала. Условием срабатывания триггера чаще всего является восходящий фронт волны управляющего сигнала.

Ещё стоит сказать про сигнал сброса, обычно называемый rst. Он служит для сброса состояния секвенциальной схемы. Например для зануления используемых регистров.

Первый пример

После знакомства с новыми терминами возьмёмся за пример. В упражнениях к предыдущей главе предлагалось написать свёртку. Тогда можно было написать свёртку только при условии, что все элементы поступают одновременно. Если же мы хотим "сворачивать" элементы последовательно, то нам необходимо сохранять предыдущее состояние. Тут нас и спасут регистры.

Рассмотрим код модуля, который последовательно суммирует числа.

src/04-sequential-logic/sum_reduce/sum_reduce.sv
module sum_reduce #(
    parameter int COUNT_OF_BITS = 4
) (
    input logic clk,  (1)
    input logic rst,  (2)
    input logic [COUNT_OF_BITS-1:0] num,
    output logic [COUNT_OF_BITS-1:0] sum
);

  logic [COUNT_OF_BITS-1:0] acc;  (3)

  always_comb begin  (4)
    sum = acc + num;  (5)
  end

  always_ff @(posedge clk) begin  (6)
    if (rst) begin  (7)
      acc <= 'b0;  (8)
    end else begin
      acc <= sum;  (9)
    end
  end
endmodule
1 Подключаем управляющий сигнал.
2 И сигнал сброса.
3 "Переменная" (на самом деле регистр) для сохранения накопленной суммы.
4 always_comb позволяет описывать комбинационную логику. В данном случае код эквивалентен коду: assign sum = acc + num;. В общем же случае always_comb блоки позволяют описывать более сложную комбинационную логику, например, с ветвлениями[1].
5 Складывает накопленную сумму с новым числом.
6 always_ff описывает последовательностную логику. ff указывает на то, что мы хотим использовать именно регистр (flip-flop)[2]. @(posedge clk) означает, что блок будет исполняться при каждом переднем фронте (posedge) сигнала clk.
7 Проверяем наличие сигнала сброса.
8 В случае наличия сигнала сброса, обнуляем наш аккумулятор.
9 В ином случае запоминаем накопленную сумму. Здесь появляется новый оператор <= — неблокирующее присваивание. Все такие присваивания в одном always_ff блоке будут выполнять "одновременно". Существует так же оператор блокирующего присваивания =, который позволяет в каком-то смысле выстроить порядок вычислений. Подробнее можно почитать по ссылке.

Схема нашего модуля выглядит так:

sum reduce

Регистр на схеме отображается как прямоугольник, к которому явно подключается сигнал clk в "треугольный" порт.

Проверка работоспособности

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

src/04-sequential-logic/sum_reduce/sum_reduce_tb.sv
module sum_reduce_tb;
  initial begin  (1)
    $dumpfile("sum_reduce_tb.vcd");
    $dumpvars(0, sum_reduce_tb);
  end

  localparam int CountOfBits = 4;
  localparam int ClkPeriod = 10;

  logic clk, rst;  (2)

  initial begin  (3)
    rst <= '1;
    #(ClkPeriod / 2);
    rst <= '0;
  end

  initial begin  (4)
    clk <= '0;
    forever begin  (5)
      #(ClkPeriod / 2) clk <= ~clk;
    end
  end

  logic [CountOfBits-1:0] num, sum;

  sum_reduce #(
      .COUNT_OF_BITS(CountOfBits)
  ) DUT (  (6)
      .clk(clk),
      .rst(rst),
      .num(num),
      .sum(sum)
  );

  initial begin
    $monitor("clk=%d, rst=%d, num=%d, sum=%d", clk, rst, num, sum);  (7)

    wait (!rst) num = 1;  (8)
    @(posedge clk) num = 2;  (9)
    @(posedge clk) num = 3;
    @(posedge clk) begin
      num = 0;
      $finish();
    end
  end
endmodule
1 Блок необходимый для просмотра сигналов, пока что пропустим.
2 Создаем управляющий сигнал и сигнал сброса.
3 Блок, отвечающий за выставление сигнала сброса в начальный момент времени и снятие через время равное ClkPeriod / 2.
4 Блок, отвечающий за обновление управляющего сигнала.
5 forever указывает на бесконечный цикл.
6 DUT — Device Under Testing — одно из стандартных названий тестируемого модуля в тестбенче.
7 Встроенная процедура $monitor позволяет указать за какими "переменными" следить. И при каждом обновлении любой из них будет выводить текст в консоль.
8 Данное выражение читается как: "Дождись пока rst не станет 0, после этого присвой num значение 1".
9 Данное выражение читается как: "В момент, когда наступит передний фронт clk, присвой num значение 2".

Визуализация работы

Отлаживать код на SystemVerilog можно не только с помощью тестов, но и при помощи визуализации значений внутри модуля.

Для данных целей существует программа GTKWave. К счастью для нас, она в более-менее актуальном состоянии есть в большинстве дистрибутивов Linux[3], поэтому её достаточно лишь установить.

Чтобы получить необходимые данные при работе модуля нужен тот самый блок из предыдушего примера:

  initial begin
    $dumpfile("sum_reduce_tb.vcd"); (1)
    $dumpvars(0, sum_reduce_tb); (2)
  end
1 Указывает файл для выгрузки переменных.
2 Указывает параметры выгрузки переменных. Второй аргумент отвечает за интересующий нас модуль. А первый за глубину. 0 — выгружает переменные всей иерархии модулей. 1 — выгрузит только переменные модуля из второго аргумента.

После работы примера мы сможем найти файл с расширением .vcd. Его нужно открыть в GTKWave, перетащив файл в окно программы или из терминала:

$ gtkwave sum_reduce_tb.vcd

После запуска нужно добавить необходимые сигналы. Для этого необходимо раскрыть дерево модулей и выбрать нужный нам модуль — DUT.

gtkwave dut

Далее, двойным нажатием на сигналы слева можно добавлять их в правую рабочую область. В данном случае удобно добавлять сигналы в следующем порядке: clk, rst, num, acc, sum. После этого будет удобно проследить как работал наш модуль.

gtkwave signals
Не забывайте про упражнения.