Оглавление

  • Введение

  • Комбинационный сумматор

  • Сумматор с добавлением регистров

  • Сумматор с входным сигналом валидности данных

  • Сумматор с выходным сигналом валидности данных

  • Тестовое окружение

  • Заключение

Введение

В данном цикле статей будет представлен процесс разработки и тестирования RTL-модулей на языке Verilog. В качестве примера будет рассмотрен целочисленный сумматор с AXI-Stream интерфейсами. Мы разберем некоторые приемы и паттерны, часто используемые при проектировании цифровых устройств. Также мы покажем типовую структуру тестового окружения для проверки RTL-модулей.

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

Комбинационный сумматор

Реализуем параметризованный комбинационный сумматор. Слагаемые поступают на входы data1_i и data2_i. С помощью параметра WIDTH можно настраивать их разрядность. Сумматор будет складывать беззнаковые числа, поэтому, чтобы избежать переполнения, ширина шины выходного сигнала data_o должна быть на один бит больше ширины входных данных. Описание сумматора на Verilog состоит всего из одного оператора assign:

//! Простой комбинационный параметризируемый беззнаковый сумматор.
//! Разрядность результата суммирования на один бит больше разрядности слагаемых.

module adder_comb #(
    parameter integer WIDTH = 4     //! разрядность слагаемых
) (
    input  [WIDTH-1:0] data1_i,     //! первое слагаемое
    input  [WIDTH-1:0] data2_i,     //! второе слагаемое
    output [  WIDTH:0] data_o       //! результат сложения
);

  //! непрерывное присваивание, реализующее суммирование
  assign data_o = data1_i + data2_i;

endmodule

Упрощенная схема комбинационного сумматора представлена ниже. В виде облака показаны входящие в состав сумматора комбинационные логические вентили.

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

Сумматор с добавлением регистров

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

Для примера рассмотрим случай, когда мы решили не добавлять регистр на одном из выходов модуля. Мы отдельно провели статический временной анализ и убедились, что укладываемся в требования по тактовой частоте. Наш коллега выполняет разработку блока, который будет подключаться к выходу нашего модуля. Так совпало, что он реализовал свой блок без входных регистров.

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

После добавления регистров схема сумматора примет вид:

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

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

Сумматор с входным сигналом валидности данных

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

Добавим дополнительный однобитный входной сигнал valid_i, который будет указывать, являются ли данные валидными. Этот сигнал будет подключен ко входам clock enable (ce) входных регистров. Таким образом, слагаемые будут записываться во входные регистры только при высоком уровне сигнала valid_i, и на выходе сумматора будет появляться сумма только валидных данных.

Схема сумматора теперь будет выглядеть так:

Можно увидеть, что для реализации данного модуля можно воспользоваться написанным ранее комбинационным сумматором, добавив к нему три регистра. При этом два входных регистра должны иметь входной порт clock enable. Описание модуля на Verilog представлено ниже. Блоки always с именами data1_i_reg, data2_i_reg реализуют входные регистры. Блок always с именем data_o_reg описывает выходной регистр.

//! Параметризируемый беззнаковый сумматор с входным строб-сигналом.

module adder_valid_i #(
    parameter integer WIDTH = 4  //! разрядность слагаемых
) (
    input clk,                      //! тактовый сигнал
    input reset,                    //! сигнал сброса, активный уровень - 1
    input valid_i,                  //! входной строб-сигнал
    input [WIDTH-1:0] data1_i,      //! первое слагаемое
    input [WIDTH-1:0] data2_i,      //! второе слагаемое
    output reg [WIDTH:0] data_o     //! результат сложения
);

  reg  [WIDTH-1:0] adder_in_1;  //! первый вход комбинационного сумматора
  reg  [WIDTH-1:0] adder_in_2;  //! второй вход комбинационного сумматора
  wire [  WIDTH:0] adder_out;   //! выход комбинационного сумматора

  //! входный регистр для первого слагаемого
  always @(posedge clk) begin : data1_i_reg
    if (reset) adder_in_1 <= '0;
    else if (valid_i) adder_in_1 <= data1_i;
  end

  //! входный регистр для второго слагаемого
  always @(posedge clk) begin : data2_i_reg
    if (reset) adder_in_2 <= '0;
    else if (valid_i) adder_in_2 <= data2_i;
  end

  //! комбинационный сумматор
  adder_comb #(
      .WIDTH(WIDTH)
  ) adder_comb (
      .data1_i(adder_in_1),
      .data2_i(adder_in_2),
      .data_o (adder_out)
  );

  //! выходной регистр
  always @(posedge clk) begin : data_o_reg
    if (reset) data_o <= '0;
    else data_o <= adder_out;
  end


endmodule

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

Сумматор с выходным сигналом валидности данных

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

Это опять же может создать проблемы для блока, который будет подключен к выходу нашего сумматора, если ему требуется защелкивать только актуальные, а не старые результаты сложения. Можно, конечно, воспользоваться сигналом valid_i и учесть, что выходной сигнал обновиться через два такта, но это не самое правильное решение. Если мы изменим внутреннюю реализацию сумматор, например, добавим еще регистров для создания конвейера, то задержка распространения сигнала через сумматор изменится.

Лучше добавим в наш сумматор однобитный выходной порт valid_o, который будет указывать на валидность выходной суммы. Блок, стоящий после нашего сумматора, будет ориентироваться на уровень сигнала valid_o при защелкивании актуального значения результата суммирования. Временные диаграммы работы будут иметь следующий вид:

Логику работы данного порта можно реализовать, просто задержав на два такта сигнал valid_i с помощью триггеров. Упрощенная схема сумматора с выходным сигналом валидности данных представлена ниже:

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

Ниже представлено описание сумматор с выходным сигналом валидности данных на Verilog. Модуль adder_valid_i реализует сумматор с входным сигналом валидности. Блок always с именем valid_i_shift_reg описывает линию задержки на два такта на основе регистра сдвига. Выход регистра сдвига формирует сигнал валидности выходных данных valid_o с помощью оператора assign.


//! Параметризируемый беззнаковый сумматор с входным и выходным строб-сигналом.

module adder_valid_io #(
    parameter integer WIDTH = 4  //! разрядность слагаемых
) (
    input              clk,      //! тактовый сигнал
    input              reset,    //! сигнал сброса, активный уровень - 1
    input              valid_i,  //! входной строб-сигнал
    input  [WIDTH-1:0] data1_i,  //! первое слагаемое
    input  [WIDTH-1:0] data2_i,  //! второе слагаемое
    output             valid_o,  //! выходной строб-сигнал
    output [  WIDTH:0] data_o    //! результат сложения
);

  //! регистр сдвига для задержки строб-сигнала
  reg [1:0] valid_shift_reg;

  //! сумматор с входным строб-сигналом
  adder_valid_i #(
      .WIDTH(WIDTH)
  ) adder_valid_i (
      .clk    (clk),
      .reset  (reset),
      .data1_i(data1_i),
      .data2_i(data2_i),
      .valid_i(valid_i),
      .data_o (data_o)
  );

  //! задерживаем входной строб-сигнал на два такта для
  //! выравнивая его с выходными данными
  always @(posedge clk) begin : valid_i_shift_reg
    if (reset) valid_shift_reg <= 2'b00;
    else valid_shift_reg <= {valid_shift_reg[0], valid_i};
  end
  assign valid_o = valid_shift_reg[1];


endmodule

Тестовое окружение

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

Для начала объявим все сигналы, которые будут использоваться в нашем тестовом окружении. Входные слагаемые будут формироваться внутри цикла с помощью функции $urandom() . Переменная trans_cnt - это счетчик итераций в цикле, а trans_number - число суммирований, которое будет выполнено во время теста. Функция $urandom() является генератором положительных псевдослучайных целых чисел. Начальное состояние генератора можно задать с помощью переменной seed. Изменяя ее значение, мы будем получать разные последовательности случайных слагаемых.

// Testbench для проверки сумматора с входным и выходным строб-сигналом
module adder_valid_io_tb ();

  localparam integer WIDTH = 4;  // разрядность входных данных

  integer seed = 0;           // начальное значение генератора случайных чисел
  integer trans_number = 10;  // число суммирований
  integer trans_cnt;          // счетчик суммирований

  reg clk = 1'b0;    // тактовый сигнал
  reg reset = 1'b1;  // сигнал сброса, активный уровень - 1

  // входной и выходной строб-сигналы
  reg  valid_i;
  wire valid_o;

  // входные слагаемые и результат суммы
  reg  [WIDTH-1:0] data1_i;
  reg  [WIDTH-1:0] data2_i;
  wire [WIDTH:0]   data_o;

Добавим в тестовое окружение сумматор с выходным сигналом валидности данных и подключим все его порты.

  // проверяемый модуль
  adder_valid_io #(
      .WIDTH(WIDTH)
  ) dut (
      .clk(clk),
      .reset(reset),
      .valid_i(valid_i),
      .data1_i(data1_i),
      .data2_i(data2_i),
      .data_o(data_o),
      .valid_o(valid_o)
  );

С помощью блока always будем формировать тактовый сигнал. Значение сигнала будет изменяться на противоположное каждые 5 ns, поэтому период тактового сигнала будет равен 10 ns. Сигнал сброса удобно создать с помощью блока initial. Используя цикл repeat (3), будем дожидаться появления трех фронтов тактового сигнала @(posedge clk), после чего установим сигнал сброса в неактивное нулевое состояние.

 // формирование тактового сигнала
  always #5 clk = ~clk;

  // удержание сигнала сброса в активном уровне в течение 3 тактов
  initial begin
    repeat (3) @(posedge clk);
    reset <= 1'b0;
  end

Далее следует блок формирования входных слагаемых и сигнала валидности. Сначала дожидаемся спада сигнала сброса @(negedge reset), после чего наш сумматор будет готов принимать входные данные. Внутри внешнего цикла for присваиваем сигналу valid_i нулевое значение. С помощью внутреннего цикла repeat(2) ожидаем два такта @(posedge clk). После этого устанавливаем сигнал valid_i в единицу и ждем еще один такт. Таким образом, сигнал валидности входных данных будет поступать на сумматор каждый третий такт. На каждом такте с помощью функции $urandom() присваиваем входным слагаемым data1_i и data2_i случайные значения. После завершения заданного числа trans_number суммирований завершаем тест с помощью функции $finish.

  // Формирование входных воздействий. 
  initial begin
    // ожидание снятия сброса
    @(negedge reset);

    for (trans_cnt = 0; trans_cnt < trans_number; trans_cnt = trans_cnt + 1) begin
      // задержка формирования строба в два такта
      // в момент отсутстия строб формируются фиктивные данные
      valid_i <= 1'b0;
      repeat(2) begin
          data1_i <= $urandom(seed);
          data2_i <= $urandom(seed);
          @(posedge clk);
      end
      // формировние валидных данных и строба
      valid_i <= 1'b1;
      data1_i <= $urandom(seed);
      data2_i <= $urandom(seed);
      @(posedge clk);
    end
    // завершение теста
    $finish;
  end

В конец тестового окружения добавим еще один initial блок, который будет сохранять все временные диаграммы сигналов в файл в формате VCD.

  // дамп waveforms в VCD формате
  initial begin
    $dumpfile("wave_dump.vcd");
    $dumpvars(0);
  end
endmodule

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

Видно, что сразу после старта моделирования сигнал сброса reset устанавливается в единицу, и затем на третьем фронте тактового сигнала принимает нулевое значение. Далее на вход сумматора начинают поступать случайные входные слагаемые и каждые три такта формируется сигнал валидности данных. Как и ожидалось, выходной сигнал валидности данных задержан на два такта относительно входного сигнала. Суммирование слагаемых выполняется корректно.

Заключение

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

Автор: Шевцев Владимир Андреевич, FPGA разработчик

Комментарии (2)


  1. KeisN13
    13.10.2023 14:41
    +1

    Кажется тут не хватает парочки сигналов xD


  1. checkpoint
    13.10.2023 14:41
    +1

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