Разработка и тестирование целочисленного сумматора с AXI-Stream интерфейсами, часть 2

Автор: https://github.com/VSHEV92

Исходные коды на GitHub:
https://github.com/pcbproj/AXI-Stream-Adder/tree/axis-adder-v1
https://github.com/pcbproj/AXI-Stream-Adder/tree/axis-adder-v2

Часть 2

Оглавление

  • Введение

  • AXI-Stream интерфейс

  • Сумматор с AXI-Stream интерфейсами

  • Описание блока управления

  • Реализация блока управления

  • Модернизация блока управления

  • Оптимизированный сумматор

  • Заключение

Введение

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

AXI-Stream интерфейс

В своем самом простом виде AXI-Stream интерфейс состоит всего из трех сигналов:

  • tdata — данные от передатчика;

  • tvalid — сигнал валидности данных от передатчика;

  • tready — сигнал готовности получить данные, формируемый приемником.

Также модули с AXI-Stream интерфейсом должны иметь еще два обязательных сигнала:

  • aclk — тактовый сигнал;

  • aresetn — асинхронный сигнал сброса с нулевым активным уровнем.

Считается, что обмен данными между передатчиком и приемником происходит по фронту сигнала aclk при условии, что сигналы tvalid и tready имеют единичное значение. Такую процедуру называют рукопожатием (handshake), так как взаимодействующие модули как бы договариваются о моменте передачи данных.

Для примера рассмотрим ситуацию, когда у передатчика есть данные, но приемник еще не готов их получить. В этом случае передатчик выставляет данные на шину tdata, устанавливает сигнал tvalid в единицу и ждет ответа от приемника. Когда приемник будет готов, он устаналивает сигнал tready и защелкивает данные на шине tdata. На временных диаграммах эта процедура показана ниже:

yiboc-xibz2-neis6opeuo1w3-o.png

Стрелкой отмечен момент времени, когда происходит handshake. Заметим, что если у передатчика есть еще данные, он сразу может выставлять их на шину tdata, не сбрасывая в ноль сигнал tvalid. Также у AXI-Stream есть одно важное требование: если передатчик выставил сигналы tdata и tvalid, он не может изменять их значения до тех пор, пока не произойдет handshake.

Рассмотрим противоположную ситуацию, когда приемник готов, а передатчик еще нет. На диаграммах ниже можно увидеть, что приемник выставил сигнал tready и ждет данные от передатчика. Когда передатчик устанавливает сигнал tvalid в единицу, по фронту сигнала aclk происходит handshake.

fatfehizg3eezddxfotltumavb8.png

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

c5tbvqgcgdyncscn4hkmomo1nhk.png

В AXI-Stream интерфейсе могут присутствовать еще и другие служебные сигналы, но в нашем модуле мы ограничимся только сигналами tdata, tvalid и tready. Всем желающим поподробней познакомиться с данным протоколом рекомендуем прочитать его стандарт.

Сумматор с AXI-Stream интерфейсами

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

Наш модуль становится сложнее, поэтому имеет смысл постараться разбить его на более простые части, которые можно реализовать по отдельности. Одним из стандартных подходов является разделение модуля на тракт данных (datapath) и блок управления (control unit). Тракт данных представляет из себя конвейер, состоящий из слоев логики, разделенных регистрами. В datapath выполняются все вычисления и преобразования, связанные с данными. Control unit отвечает за управление конвейером и внешними интерфейсами. Он формирует все необходимые управляющие сигналы. При таком разбиении схему цифрового устройства можно представить в следующем виде:

ovibnqqopkvmemnefgfxr6qj-ce.png

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

Теперь опишем требования к control unit. Во-первых он должен управлять загрузкой данных в регистры datapath. Для этого он будет формировать три однобитных сигнала, которые будут поступать на входы clock enable регистров datapath. Во-вторых control unit должен управлять работой трех AXI-Stream интерфейсов, то есть выставлять и считывать значения сигналов tvalid и tready.

С учетом всего вышесказанного, схему сумматора с AXI-Stream интерфейсами можно представить в виде:

kq6w-qsg--c9048pzemfz_rmcl8.png

Блок управления сделаем в виде отдельного модуля и разберем подробности его внутреннего устройства в следующем разделе статьи. Пока же реализуем на языке Verilog все остальные компоненты сумматора.

Для начала опишем все внешние и внутренние сигналы. Как упоминалось ранее модуль должен содержать тактовый сигнал aclk и сигнал асинхронного сброса с низким активным уровнем aresetn. Также можно увидеть три набора сигналов для AXI-Stream интерфейсов.

//! Простой сумматор с AXI-Stream интерфейсами.
//! Разрядность результата суммирования на один бит больше разрядности слагаемых.
//! В соответствие со стандартом на AXI-Stream ширина шины tdata должна быть кратна 8.

module adder_axis_naive #(
    parameter integer ADDER_WIDTH = 4,                                        //! разрядность слагаемых
    parameter integer IN_AXIS_WIDTH  = $ceil($itor(ADDER_WIDTH) / 8) * 8,     //! разрядность шины tdata для слагаемых
    parameter integer OUT_AXIS_WIDTH  = $ceil($itor(ADDER_WIDTH+1) / 8) * 8   //! разрядность шины tdata для суммы
) (
    input                           aclk,            //! тактовый сигнал
    input                           aresetn,         //! асинхронный сброс. активный уровень - 0
    //! @virtualbus data1_i @dir in
    input       [IN_AXIS_WIDTH-1:0] data1_i_tdata,   //! входные данные
    input                           data1_i_tvalid,  //! сигнал валидности данных
    output                          data1_i_tready,  //! сигнал готовности принять данные @end
    //! @virtualbus data2_i @dir in
    input       [IN_AXIS_WIDTH-1:0] data2_i_tdata,   //! входные данные
    input                           data2_i_tvalid,  //! сигнал валидности данных
    output                          data2_i_tready,  //! сигнал готовности принять данные @end
    //! @virtualbus data_o @dir out
    output reg [OUT_AXIS_WIDTH-1:0] data_o_tdata,    //! выходные данные
    output                          data_o_tvalid,   //! сигнал валидности данных
    input                           data_o_tready    //! сигнал готовности принять данные @end
);

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

  wire data1_i_ce; //! сигнал загрузки во входной регистр первого слагаемого
  wire data2_i_ce; //! сигнал загрузки во входной регистр второго слагаемого
  wire data_o_ce;  //! сигнал загрузки в выходной регистр результата суммирования

Обратим внимание на один важный момент, о котором не упоминалось ранее. В соответствии со стандартом шина tdata должна иметь ширину в битах, кратную 8. То есть, в шину должно укладываться целое число байт. Чтобы наш сумматор мог работать с произвольной шириной слагаемых, мы будем выполнять округление в большую сторону до кратности 8. Для этого у нас будет два новых параметра. Как и ранее, ADDER_WIDTH — это ширина слагаемых, для которых вычисляется сумма. Параметры IN_AXIS_WIDTH и OUT_AXIS_WIDTH задают ширину сигналов tdata для входных и выходных AXI-Stream интерфейсов. Чтобы выполнить округление, преобразуем ширину шины из целого числа в вещественное ($itor), разделим на 8, округлим до целого в большую сторону ($ceil) и умножим на 8. При этом дополнительные старшие биты, полученные в результате округления, при вычислении суммы будут просто игнорироваться.

Тракт данных почти полностью совпадает с тем, что было реализовано в сумматоре с регистровыми входами. Единственное отличие — это, то что сигнал сброса является асинхронным и низкого уровня, а также выходной регистр теперь тоже имеет входной порт clock enable.

  //! входной регистр первого слагаемого
  always @(posedge aclk or negedge aresetn) begin : data1_i_reg
    if (!aresetn) adder_in_1 <= '0;
    else if (data1_i_ce) adder_in_1 <= data1_i_tdata;
  end

  //! входной регистр второго слагаемого
  always @(posedge aclk or negedge aresetn) begin : data2_i_reg
    if (!aresetn) adder_in_2 <= '0;
    else if (data2_i_ce) adder_in_2 <= data2_i_tdata;
  end

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

  //! выходной регистр результата суммирования
  always @(posedge aclk or negedge aresetn) begin : data_o_reg
    if (!aresetn) data_o_tdata <= '0;
    else if (data_o_ce) data_o_tdata <= adder_out;
  end

В конце добавим блок управления и подключим все его внешние сигналы. Наш сумматор с AXI-Stream интерфейсами готов.

  //! блок управления загрузкой регистров и сигналами AXI-Stream интерфейса
  adder_axis_cu adder_axis_cu (
      .aclk(aclk),
      .aresetn(aresetn),
      .data1_i_ce(data1_i_ce),
      .data2_i_ce(data2_i_ce),
      .data_o_ce(data_o_ce),
      .data1_i_tready(data1_i_tready),
      .data1_i_tvalid(data1_i_tvalid),
      .data2_i_tready(data2_i_tready),
      .data2_i_tvalid(data2_i_tvalid),
      .data_o_tready(data_o_tready),
      .data_o_tvalid(data_o_tvalid)
  );

endmodule

Реализация блока управления

Теперь рассмотрим один из возможных вариантов реализации блока управления. Control unit должен формировать сигналы clock enable для регистров в datapath, а также управлять сигналами tvalid и tready для AXI-stream интерфейсов. Для входных слагаемых сумматор выступает в качестве приемника, поэтому блок управления получает сигналы tvalid и формирует сигналы tready. Для выходного интерфейса сумматор является передатчиком, поэтому он управляет сигналом tvalid и получает от приемника сигнал tready.

Внешние интерфейсы control unit можно описать следующим образом:

//! Конечный автомат управления служебными сигналами сумматора
//! с AXI-Stream интерфейсами. Модуль управляет загрузкой данных
//! во внутренние регистры сумматора и формирует tvalid и tready
//! сигналы
module adder_axis_cu (
    input aclk,    //! тактовый сигнал
    input aresetn, //! асинхронный сброс. активный уровень - 0

    output data1_i_ce,  //! загрузка первого слагаемого во внутренний регистр
    output data2_i_ce,  //! загрузка второго слагаемого во внутренний регистр
    output data_o_ce,   //! загрузка результата в выходной регистр

    input  data1_i_tvalid,  //! сигнал валидности данных
    output data1_i_tready,  //! сигнал готовности принять данные
    
    input  data2_i_tvalid,  //! сигнал валидности данных
    output data2_i_tready,  //! сигнал готовности принять данные

    output data_o_tvalid,  //! сигнал валидности данных
    input  data_o_tready   //! сигнал готовности принять данные
);

Реализуем весь блок управления в виде одного конечного автомата. Для начала рассмотрим, какие состояния мы можем выделить. Во-первых, определим состояние сброса IDLE. Автомат попадает в него при активном низком уровне сигнала aresetn. В этом состоянии сумматор не готов получать слагаемые и ничего не выдает в качестве результата суммы. После снятия сигнала сброса сумматор ожидает входные слагаемые — это будет еще одно состояние (WAIT_INPUT_DATA). Входные слагаемые могут приходит в произвольном порядке, например, сначала на порт data1_i_tdata, потом на порт data2_i_tdata, или наоборот. Выделим два состояния: получено первое слагаемое и ждем второе (WAIT_DATA2_INPUT), получено второе слагаемое и ждем первое (WAIT_DATA1_INPUT). После получения обоих слагаемых, мы будем попадать в состояние вычисления суммы и загрузки результата в выходной регистр (LOAD_OUTPUT_DATA). Последнее состояние — это ожидание готовности приемника получить результат сложения (WAIT_OUTPUT_READY).

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

  localparam IDLE = 3'b000;               //! состояние сброса устройства
  localparam WAIT_INPUT_DATA = 3'b001;    //! состояние готовности получать слагаемые
  localparam WAIT_DATA1_INPUT = 3'b010;   //! состояние после получения второго слагаемого и ожидания первого
  localparam WAIT_DATA2_INPUT = 3'b011;   //! состояние после получения первого слагаемого и ожидания второго
  localparam LOAD_OUTPUT_DATA = 3'b100;   //! состояние после получения слагаемых и формирования суммы
  localparam WAIT_OUTPUT_READY = 3'b101;  //! состояние ожидания, когда следующий блок примет результат суммы

  //! состояние конечного автомата
  reg [2:0] state;

Далее опишем логику перехода между состояниями. Из состояния сброса IDLE, мы попадаем в состояние ожидания слагаемых WAIT_INPUT_DATA. Если в этом состоянии мы получим сигнал data1_i_tvalid, то это означает, что было принято первое слагаемое. Соответственно, автомат переходит в состояние ожидания второго слагаемого WAIT_DATA2_INPUT. Также возможно, что сначала будет получен сигнал data2_i_tvalid, то есть принято второе слагаемое. При этом мы переходим в состояние ожидания первого слагаемого WAIT_DATA1_INPUT. Находясь в состояниях WAIT_DATA1_INPUT или WAIT_DATA2_INPUT, мы ожидаем оставшееся слагаемое и при получении соответствующего сигнала tvalid переходим в состояние вычисления суммы и загрузки результата в выходной регистр LOAD_OUTPUT_DATA. Мы также можем попасть в это состояние сразу из состояния ожидания слагаемых WAIT_INPUT_DATA, если слагаемые появятся на входах сумматора одновременно. В состоянии LOAD_OUTPUT_DATA мы находимся один такт, после чего переходим в состояние ожидания готовности приемника WAIT_OUTPUT_READY. Если сигнал data_o_tready равен единице, то это означает, что приемник готов получить данные, и на следующем такте на выходном интерфейсе сумматора произойдет handshake. После этого автомат переходит в состояние ожидания новых слагаемых WAIT_INPUT_DATA и цикл повторяется.

Логика переходов автомата реализована с помощью блока always и оператора case:

//! логика перехода между состояниями автомата
  always @(posedge aclk or negedge aresetn) begin : FSM_State_Transition
    if (!aresetn) state <= IDLE;
    else
      case (state)
        // после снятия сигнала сброса ожидаем входные слагаемые
        IDLE:
          state <= WAIT_INPUT_DATA;

        // в каждом такте можем получить первое слагаемое,
        // второе слагаемое или сразу оба. от этого зависит следующее
        // состояние. наличие входных данных определяется по
        // сигналу tvalid
        WAIT_INPUT_DATA:
          if (data1_i_tvalid && data2_i_tvalid) state <= LOAD_OUTPUT_DATA;
          else if (data1_i_tvalid) state <= WAIT_DATA2_INPUT;
          else if (data2_i_tvalid) state <= WAIT_DATA1_INPUT;
          
        // второе слагаемое получили, ждем первое слагаемое
        WAIT_DATA1_INPUT:
          if (data1_i_tvalid) state <= LOAD_OUTPUT_DATA;

        // первое слагаемое получили, ждем второе слагаемое
        WAIT_DATA2_INPUT:
          if (data2_i_tvalid) state <= LOAD_OUTPUT_DATA;

        // получили оба слагаемых. считаем сумму и загружаем
        // результат в выходной регистр
        LOAD_OUTPUT_DATA:
          state <= WAIT_OUTPUT_READY;

        // ожидаем готовность приемника получить результат суммы
        WAIT_OUTPUT_READY:
          if (data_o_tready) state <= WAIT_INPUT_DATA;
          
        default:
          state <= WAIT_INPUT_DATA;

      endcase
  end

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

l-vyypvj00lg4ksfndcfzvtfxdc.png

В завершение рассмотрим логику формирования сигналов управления. Сначала рассмотрим запись во входные регистры datapath. Мы будем действовать следующим образом: пока слагаемое не получено, мы на каждом такте будем записывать в соответствующий регистр все, что появляется на шине tdata. Когда слагаемое появится на входном интерфейсе и защелкнется в регистр, мы перестанем в него что-либо записывать. Например, мы будем сохранять данные из шины data1_i_tdata во входной регистр первого слагаемого до тех пор пока оно не получено, то есть пока автомат находится в состоянии WAIT_INPUT_DATA или WAIT_DATA1_INPUT. При это сигнал clock enable data1_i_ce будет установлен в единицу. Когда автомат выйдет из одного из этих состояний, мы заканчиваем запись в регистр и сбрасываем сигнал data1_i_ce в ноль.

Аналогично, для второго слагаемого сигнал data2_i_ce будет устанавливаться в единицу, когда автомат находится в состоянии WAIT_INPUT_DATA или WAIT_DATA2_INPUT.

Сигнал готовности получить первое слагаемое data1_i_tready будет совпадать с сигналом data1_i_ce, разрешающим запись в регистр. То есть, пока первое слагаемое не получено, сигнал data1_i_ce будет установлен в единицу. При этом сигнал data1_i_tready также будет иметь единичное значение, указывая на готовность принять первое слагаемое. Когда слагаемое будет получено, сигнал data1_i_ce сбросится в ноль, а в месте с ними и сигнал data1_i_tready. Для второго слагаемого аналогичным образом получаем, что сигнал data2_i_tready будет совпадать с сигналом data2_i_ce.

Когда оба слагаемых получены, мы можем посчитать сумму и записать результат в выходной регистр. Поэтому находясь в состоянии LOAD_OUTPUT_DATA, мы устанавливаем в единицу сигнал data_o_ce, поступающий на порт clock enable выходного регистра datapath.

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

  // пока не получено первое слагаемое его можно получить по AXI-Stream
  // и загрузить во входной внутренний регистр сумматора
  assign data1_i_ce = (state == WAIT_INPUT_DATA) || (state == WAIT_DATA1_INPUT);
  assign data1_i_tready = data1_i_ce;

  // пока не получено второе слагаемое его можно получить по AXI-Stream
  // и загрузить во входной внутренний регистр сумматора
  assign data2_i_ce = (state == WAIT_INPUT_DATA) || (state == WAIT_DATA2_INPUT);
  assign data2_i_tready = data2_i_ce;

  // загружаем результат суммы, после получения обоих слагаемых
  assign data_o_ce = (state == LOAD_OUTPUT_DATA);

  // после загрузки суммы в выходной регистр устанавливаем сигнал валидности
  assign data_o_tvalid = (state == WAIT_OUTPUT_READY);

endmodule

Ниже представлены временные диаграммы работы нашего сумматора с AXI-Stream интерфейсами. Стрелками показаны моменты, когда происходит handshake на соответствующих интерфейсах.

cwx5qjqqqdzbng_icrhyraycuzi.png

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

Например, рассмотрим вычисление второй суммы. Сначала приходит первое слагаемое data1_i_tdata. Его значение, равное нулю, записывается во входной регистр. После этого приходит второе слагаемое data2_i_tdata, равное 12. Оно также записывается в регистр, после чего вычисляется сумма и через такт результат, равный 12 появляется на шине data_o_tdata. В тот же момент времени устанавливается сигнал валидности результата сложения data_o_tvalid. Приемник не готов получить данные, на что указывает равное нулю значение сигнала data_o_tready. Чуть позже появляется следующее слагаемое на шине data1_i_tdata, однако так как результат суммы еще не передан в приемник, сумматор не записывает новое слагаемое во входной регистр. Но это явный недостаток. Значение во входном регистре для первого слагаемого уже было использовано для расчета суммы и его не нужно больше хранить. Если записывать следующие слагаемые во входные регистры, еще до того как результат сложения будет передан приемнику, то мы сможем увеличить быстродействие нашего сумматора. Давайте займемся этим в следующем разделе.

Модернизация блока управления

На данный момент проблема в блоке управления состоит в том, что его состояния изменяются последовательно, и мы не можем записать новые данные во входные регистры, до тех пор пока из выходного регистра не считан результат суммирования. Главное преимущество FPGA заключается в возможности распараллеливания процессов. Давайте этим воспользуемся и реализуем control unit таким образом, чтобы каждый регистр управлялся параллельно и независимо.

Для этого в виде отдельного модуля axis_inf_cu создадим небольшой автомат, который будет управлять одним из регистров datapath. Модуль будет формировать сигнал load_en, поступающий на входной порт clock enable регистра. Автомат будет проверять сигнал валидности данных upstream_tvalid от передатчика и формировать сигнал готовности upstream_tready. При считывании данных из регистра автомат будет формировать сигнал валидности данных downstream_tvalid для приемника и получать от него сигнал готовности downstream_tready. Входные и выходные порты автомата представлены на схеме:

c4nbxg18iw5-mqm_wkwnh-2dyto.png

Теперь рассмотрим реализацию данного модуля на Verilog. Для начала опишем его внешние порты:

//! Конечный автомат управления загрузкой в регистр сумматора

module axis_inf_cu (
  input aclk,     //! тактовый сигнал
  input aresetn,  //! асинхронный сброс. активный уровень - 0

  output load_en, //! сигнал разрешения загрузки регистра

  //! @virtualbus upstream @dir in
  input  upstream_tvalid,  //! сигнал валидности данных
  output upstream_tready,  //! сигнал готовности принять данные @end

  //! @virtualbus upstream @dir out
  output downstream_tvalid,  //! сигнал валидности данных
  input  downstream_tready   //! сигнал готовности принять данные @end
);

Автомат будет иметь всего два состояния. Первое состояние EMPTY указывает, что регистр, которым управляет автомат, на текущий момент не содержит данных, которые требуется хранить и в него можно записать новые данные. Другое состояние FULL говорит, что регистр содержит данные и запись в него запрещена. Описание состояний показано ниже:

  localparam EMPTY = 1'b0; //! состояние свободного регистра
  localparam FULL  = 1'b1; //! состояние занятого регистра

  //! состояние конечного автомата
  reg state;

С помощью блока always и оператора case опишем логику перехода между состояниями. После сброса регистр, которым управляет автомат, свободен, поэтому мы попадаем в состояние EMPTY. О появлении валидных входных данных передатчик сигнализирует установкой сигнала upstream_tvalid в единицу. Мы записываем эти данные в регистр, после чего переходим в состояние FULL.

Теперь мы ждем, когда приемник будет готов считать данные из регистра. Для этого мы проверяем сигнал downstream_tready. Если он равен единице, значит при появлении следующего фронта тактового сигнала данные будут считаны и их больше не требуется хранить в регистре. При этом возможны два варианта. Если у передатчика есть еще данные (установлен сигнал upstream_tvalid), то они сразу запишутся в регистр, и автомат останется в состоянии FULL. Если же новых данных нет, мы переходим в состояние EMPTY.

  //! логика перехода между состояниями автомата
  always @(posedge aclk or negedge aresetn) begin : FSM_State_Transition
    if (!aresetn) state <= EMPTY;
    else
      case (state)

        // если регистр пустой и данные валидны, то записываем данные
        EMPTY:
          if (upstream_tvalid) state <= FULL;

        // если в регистре есть данные и приемник их готов принять,
        // то данные считываются. Если одновременно на входе есть
        // валидные данные, то они записываются в регистр, и он
        // остается в заполненном состоянии
        FULL:
          if (downstream_tready && !upstream_tvalid) state <= EMPTY;

        default:
          state <= EMPTY;
      endcase
  end

Ниже представлена диаграмма состояний автомата. Можно увидеть, что она получилась совсем простой.

yils0oocr8sodaslgbddisbnhja.png

В завершение реализуем логику управления внешними сигналами. Мы будем выставлять сигнал разрешения записи новых данных load_en, если регистр не занят (состояние EMPTY), либо если занят (состояние FULL) и установлен сигнал downstream_tready, то есть, если на следующем такте данные будут считаны. По аналогии с автоматом из предыдущего раздела, сигнал готовности получить данные от передатчика upstream_tready будет совпадать с сигналом разрешения записи в регистр load_en.

Сигнал валидности данных downstream_tvalid устанавливается, когда эти данные записаны в регистр и автомат находится в состоянии FULL.

  // выставляем сигнал готовности к приему данных и сигнал записи в регистр,
  // если регистр не занят, или если занят, но приемник готов
  // из него считать данные
  assign load_en = (state == EMPTY) || (downstream_tready & state == FULL);
  assign upstream_tready = load_en;

  // выставляем сигнал валидности, если данные загружены в регистр
  assign downstream_tvalid = (state == FULL);

endmodule

Таким образом, мы реализовали автомат управления отдельным регистром datapath сумматора. У нас таких регистров три, поэтому мы можем объединить три модуля axis_inf_cu и получить блок управления для всего тракта данных.

Оптимизированный сумматор

Теперь мы можем собрать из отдельных модулей axis_inf_cu блок управления для всего сумматора. Сначала рассмотрим, как должны быть подключены порты upstream и downstream автоматов управления регистрами. Для входных регистров порты upstream_tvalid и upstream_tready нужно соединить непосредственно с сигналами tvalid и tready входных AXI-Stream интерфейсов. Аналогичным образом, для выходного регистра downstream порты подключаются к сигналам tvalid и tready выходного AXI-Stream интерфейса.

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

В выходной регистр должна быть записана сумма, которая вычисляется с помощью внутреннего комбинационного сумматора. Мы можем выполнить сложение и записать полученный результат, только при условии, что во входных регистрах находятся валидные данные и выходной регистр пуст. Для отслеживания этого условия создадим сигнал start_summation. Чтобы его сформировать, нам необходимо с помощью вентиля AND объединить сигналы валидности данных от входных регистров (downstream_tvalid) и сигнал готовности от выходного регистра (upstream_tready).

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

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

В итоге получаем следующую схему блока управления:

dwie7dxhslhebjvkf9svfc0lskm.png

Для краткости, на схеме были введены дополнительные обозначения:

  • u_v = upstream_tvalid

  • u_r = upstream_tready

  • d_v = downstream_tvalid

  • d_r = downstream_tready

Ниже представлено описание блока управления на Verilog. Можно увидеть, что согласно схеме, он состоит из трех экземпляров модуля axis_inf_cu, а также оператора assign, формирующего сигнал start_summation. Так как мы модифицировали только блок управления, то остальной код сумматора изменять не нужно.

//! Конечный автомат управления служебными сигналами сумматора
//! с AXI-Stream интерфейсами. Модуль управляет загрузкой данных
//! во внутренние регистры сумматора и формирует tvalid и tready
//! сигналы
module adder_axis_cu (
    input aclk,    //! тактовый сигнал
    input aresetn, //! асинхронный сброс. активный уровень - 0

    output data1_i_ce,  //! загрузка первого слагаемого во внутренний регистр
    output data2_i_ce,  //! загрузка второго слагаемого во внутренний регистр
    output data_o_ce,   //! загрузка результата в выходной регистр

    //! @virtualbus data1_i @dir in
    input  data1_i_tvalid,  //! сигнал валидности данных
    output data1_i_tready,  //! сигнал готовности принять данные @end

    //! @virtualbus data2_i @dir in
    input  data2_i_tvalid,  //! сигнал валидности данных
    output data2_i_tready,  //! сигнал готовности принять данные @end

    //! @virtualbus data_o @dir out
    output data_o_tvalid,  //! сигнал валидности данных
    input  data_o_tready   //! сигнал готовности принять данные @end
);

  wire data1_i_reg_valid; //! сигнал готовности первого слагаемого во входном регистре
  wire data2_i_reg_valid; //! сигнал готовности второго слагаемого во входном регистре
  wire data_o_reg_ready;  //! сигнал готовности выходного регистра принять данные

  wire start_summation;   //! сигнал готовности выполнить суммирование слагаемых

  //! конечный автомат управления загрузкой входного регистра для первого слагаемого
  axis_inf_cu data1_i_cu (
    .aclk(aclk),
    .aresetn(aresetn),
    .load_en(data1_i_ce),
    .upstream_tready(data1_i_tready),
    .upstream_tvalid(data1_i_tvalid),
    .downstream_tready(start_summation),
    .downstream_tvalid(data1_i_reg_valid)
  );

  //! конечный автомат управления загрузкой входного регистра для второго слагаемого
  axis_inf_cu data2_i_cu (
    .aclk(aclk),
    .aresetn(aresetn),
    .load_en(data2_i_ce),
    .upstream_tready(data2_i_tready),
    .upstream_tvalid(data2_i_tvalid),
    .downstream_tready(start_summation),
    .downstream_tvalid(data2_i_reg_valid)
  );

  //! конечный автомат управления загрузкой выходного регистра
  axis_inf_cu data_o_cu (
    .aclk(aclk),
    .aresetn(aresetn),
    .load_en(data_o_ce),
    .upstream_tready(data_o_reg_ready),
    .upstream_tvalid(start_summation),
    .downstream_tready(data_o_tready),
    .downstream_tvalid(data_o_tvalid)
  );

  // выполнять сложение можно, если входные регистры содержат данные и выходной
  // регистр готов их принять
  assign start_summation = data1_i_reg_valid & data2_i_reg_valid & data_o_reg_ready;

endmodule

Временные диаграммы оптимизированного сумматора представлены ниже. По ним можно отследить распространение данных внутри сумматора. Например, рассмотрим, как выполняется второе сложение. На входной интерфейс приходит первое слагаемое, равное нулю. Чуть позже на другой вход поступает второе слагаемое, равное 12. Через такт на выходном интерфейсе появляется результат суммы, равный 12, и устанавливается сигнал валидности data_o_tvalid. Однако, приемник пока не готов получить данные от сумматора, на что указывает нулевое значение сигнала data_o_tready.

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

Спустя несколько тактов приемник устанавливает сигнал готовности data_o_tready в единицу и считывает подряд сразу два результата суммирования. Такая ситуация была бы невозможна при предыдущем варианте реализации блока управления. Мы действительно смогли ускорить работу сумматора.

fkein2p_m413nkbjgq8afyfdjmu.png

У нашего сумматора по-прежнему есть недостатки. Например, в модуле axis_inf_cu присутствует комбинационный путь между входом downstream_tready и выходом upstream_tready. При соединении последовательно нескольких таких модулей мы можем получить слишком много уровней логики и проблемы с временными ограничениями. На самом деле, если объединить наш автомат управления регистром и сам регистр в один модуль, то мы получим известную конструкцию, которая называется AXI-Stream Register Slice. Она также имеет проблему с комбинационным путем для сигналов tready, и для нее предложено решение за счет введения дополнительных регистров расширения. Интересующимся читателям рекомендуем поискать в интернете Pipelining AXI Buses with registered ready signals.

Заключение

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

Полученный нами сумматор существенно сложнее того, что был сделан в предыдущей статье, поэтому возникает вопрос:, а как его проверять? Мы, например, должны убедиться, что он корректно себя ведет для всех комбинаций сигналов управления на AXI-Stream интерфейсе. У нас таких сигналов два: tvalid и tready. Соответственно 4 возможных комбинации их значений. Интерфейсов у нас три, то есть, мы получаем уже 64 возможных состояния сигналов управления на входах и выходах сумматора. Очевидно, что генерировать все 64 состояния, запускать тест и просматривать временные диаграммы глазами слишком утомительно. Более того при каждом изменении кода сумматора просмотр диаграмм нужно повторять.

В следующей статье мы рассмотрим основные принципы проверки сложных модулей и представим тестовое окружение для нашего сумматора.

© Habrahabr.ru