Chisel, первый взгляд RTL-разработчика

от автора

Недавно возникла потребность в быстром погружении в язык Chisel. Чтобы попробовать новый язык, не хотелось писать счетчик или сумматор, а что-то приближенное к рабочим моментам. И так, сформируем задание на разрабатываемый блок:

  • интерфейс получения данных — AXI-Stream;

  • интерфейс передачи данных — AXI-Stream;

  • максимальный размер обрабатываемого пакета — 1024 байта;

  • последнее слово пакета содержит значение контрольной суммы от пакета, посчитанное по алгоритму crc8;

  • необходимо выполнить проверку значения контрольной суммы. Если контрольная сумма корректна, передать пакет в интерфейс передачи данных, иначе, удалить пакет.

Содержание

1 Общая информация

2 Реализация блока на SystemVerilog

3 Реализация тестового окружения

4 Реализация блока на Chisel

5 Выводы

1 Общая информация

Алгоритм работы блока будет следующим:

  1. Прием пакета из интерфейса получения данных, подсчет контрольной суммы, запись пакета в FIFO. Прием пакета заканчивается при поступлении признака конца пакета (tlast = 1, когда установлены сигналы tvalid = 1 и tready = 1). Переход в состояние анализа контрольной суммы.

  2. Проверка значения контрольной суммы. Если контрольная сумма корректна (значение контрольной суммы равно 0), то переход в состояние передачи пакета из FIFO в интерфейс передачи данных. Иначе, переход в состояние удаления пакета.

  3. Удаление пакета. Выполняется сброс FIFO, переход в состояние приема пакета из интерфейса получения данных.

  4. Передача пакета из FIFO в интерфейс передачи данных. Передача заканчивается, когда будет выдан конец пакета (tlast = 1, когда установлены сигналы tvalid = 1 и tready = 1). Переход в состояние приема пакета из интерфейса получения данных

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

2 Реализация блока на SystemVerilog

Сначала будет выполнена реализация на SystemVerilog, чтобы на этой реализации опробовать тестовое окружение, а потом с помощью этого тестового окружения верифицировать реализацию на Chisel.

Для начала потребуется блок для хранения пакета. Будем использовать FIFO, код простого FIFO приведен ниже под спойлером и на гитхаб (sync_fifo.sv)

sync_fifo
`default_nettype none  module sync_fifo #(     parameter int FIFO_DEPTH            = 8,     parameter int DATA_WIDTH            = 32 )(     input  wire logic                   CLK_I,     input  wire logic                   RST_I,      input  wire logic                   WR_EN_I,     input  wire logic [DATA_WIDTH-1:0]  WR_DATA_I,     output var  logic                   FULL_O,      input  wire logic                   RD_EN_I,     output var  logic [DATA_WIDTH-1:0]  RD_DATA_O,     output var  logic                   EMPTY_O );  localparam int ADDR_WIDTH = $clog2(FIFO_DEPTH);  logic [ADDR_WIDTH:0]    wr_ptr; logic [ADDR_WIDTH:0]    rd_ptr; logic [DATA_WIDTH-1:0]  fifo_cell [FIFO_DEPTH-1:0];  always_comb begin : pc_full_o     FULL_O = (  (wr_ptr[ADDR_WIDTH] != rd_ptr[ADDR_WIDTH]) &&                 (wr_ptr[ADDR_WIDTH - 1:0] == rd_ptr[ADDR_WIDTH - 1:0]) ) ? 1'b1 : 1'b0; end : pc_full_o  always_comb begin : pc_empty_o     EMPTY_O = (wr_ptr == rd_ptr) ? 1'b1 : 1'b0; end : pc_empty_o  always_ff @(posedge CLK_I) begin : ps_wr_ptr     if (RST_I) begin         wr_ptr <= '0;     end     else begin         if (FULL_O == 1'b0 && WR_EN_I == 1'b1) begin             wr_ptr <= wr_ptr + 1'b1;         end     end end : ps_wr_ptr  always_ff @(posedge CLK_I) begin : ps_rd_ptr     if (RST_I) begin         rd_ptr <= '0;     end     else begin         if (EMPTY_O == 1'b0 && RD_EN_I == 1'b1) begin             rd_ptr <= rd_ptr + 1'b1;         end     end end : ps_rd_ptr  always_ff @(posedge CLK_I) begin : ps_fifo_cell     if (FULL_O == 1'b0 && WR_EN_I == 1'b1) begin         fifo_cell[wr_ptr[ADDR_WIDTH-1:0]] <= WR_DATA_I;     end end : ps_fifo_cell  always_comb begin : pc_rd_data_o     RD_DATA_O = fifo_cell[rd_ptr[ADDR_WIDTH-1:0]]; end : pc_rd_data_o  endmodule  `resetall

Два указателя, на чтение и на запись, массив регистров.

Далее, потребуется блок для вычисления контрольной суммы. Возьмем по первой ссылке в гугле. Код ниже под спойлером и на гитхаб (calc_crc.v)

calc_crc
// vim: ts=4 sw=4 expandtab  // THIS IS GENERATED VERILOG CODE. // https://bues.ch/h/crcgen //  // This code is Public Domain. // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted. //  // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY // SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER // RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, // NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE // USE OR PERFORMANCE OF THIS SOFTWARE.  `ifndef CALC_CRC_V_ `define CALC_CRC_V_  // CRC polynomial coefficients: x^8 + x^2 + x + 1 //                              0x7 (hex) // CRC width:                   8 bits // CRC shift direction:         left (big endian) // Input word width:            8 bits  module calc_crc (     input [7:0] CRC_I,     input [7:0] DATA_I,     output [7:0] CRC_O );     assign CRC_O[0] = CRC_I[0] ^ CRC_I[6] ^ CRC_I[7] ^ DATA_I[0] ^ DATA_I[6] ^ DATA_I[7];     assign CRC_O[1] = CRC_I[0] ^ CRC_I[1] ^ CRC_I[6] ^ DATA_I[0] ^ DATA_I[1] ^ DATA_I[6];     assign CRC_O[2] = CRC_I[0] ^ CRC_I[1] ^ CRC_I[2] ^ CRC_I[6] ^ DATA_I[0] ^ DATA_I[1] ^ DATA_I[2] ^ DATA_I[6];     assign CRC_O[3] = CRC_I[1] ^ CRC_I[2] ^ CRC_I[3] ^ CRC_I[7] ^ DATA_I[1] ^ DATA_I[2] ^ DATA_I[3] ^ DATA_I[7];     assign CRC_O[4] = CRC_I[2] ^ CRC_I[3] ^ CRC_I[4] ^ DATA_I[2] ^ DATA_I[3] ^ DATA_I[4];     assign CRC_O[5] = CRC_I[3] ^ CRC_I[4] ^ CRC_I[5] ^ DATA_I[3] ^ DATA_I[4] ^ DATA_I[5];     assign CRC_O[6] = CRC_I[4] ^ CRC_I[5] ^ CRC_I[6] ^ DATA_I[4] ^ DATA_I[5] ^ DATA_I[6];     assign CRC_O[7] = CRC_I[5] ^ CRC_I[6] ^ CRC_I[7] ^ DATA_I[5] ^ DATA_I[6] ^ DATA_I[7]; endmodule  `endif // CALC_CRC_V_

Опишем управляющий автомат. Объявление возможных состояний:

enum logic [1:0] {ST_RECEIVE, ST_CHECK, ST_RESET, ST_SEND} checker_st;

Реализация управляющего автомата:

always_ff @(posedge CLK_I) begin : ps_checker_st     if (RST_I) begin         checker_st <= ST_RECEIVE;     end     else begin         case (checker_st)             ST_RECEIVE : begin                 if (packet_received) begin                     checker_st <= ST_CHECK;                 end             end             ST_CHECK : begin                 if (crc_in == '0) begin                     checker_st <= ST_SEND;                 end                 else begin                     checker_st <= ST_RESET;                 end             end             ST_RESET : begin                 checker_st <= ST_RECEIVE;             end             ST_SEND : begin                 if (packet_sended) begin                     checker_st <= ST_RECEIVE;                 end             end         endcase     end end : ps_checker_st

Признаки окончания приема пакета и окончания передачи пакета:

always_comb begin : pc_packet_received     packet_received = AXIS_SLV_IF.tvalid & AXIS_SLV_IF.tready & AXIS_SLV_IF.tlast; end : pc_packet_received  always_comb begin : pc_packet_sended     packet_sended = AXIS_MST_IF.tvalid & AXIS_MST_IF.tready & AXIS_MST_IF.tlast; end : pc_packet_sended

Подсчет контрольной суммы:

always_ff @(posedge CLK_I) begin : ps_crc_in     if (RST_I) begin         crc_in <= '1;     end     else begin         if (AXIS_SLV_IF.tvalid & AXIS_SLV_IF.tready) begin             crc_in <= crc_out;         end         else if (checker_st == ST_CHECK) begin             crc_in <= '1;         end     end end : ps_crc_in  calc_crc u_calc_crc(     .CRC_I  (crc_in),     .DATA_I (AXIS_SLV_IF.tdata),     .CRC_O  (crc_out) );

Сброс FIFO в случае несовпадения контрольной суммы:

always_ff @(posedge CLK_I) begin : ps_fifo_rst     if (RST_I) begin         fifo_rst <= 1'b1;     end     else begin         fifo_rst <= (checker_st == ST_RESET) ? 1'b1 : 1'b0;     end end : ps_fifo_rst

Для удобства записи/чтение в/из FIFO объявим структуру и необходимые сигналы:

 localparam int AXIS_DW = $bits(AXIS_SLV_IF.tdata);  enum logic [1:0] {ST_RECEIVE, ST_CHECK, ST_RESET, ST_SEND} checker_st;  typedef struct packed {     logic               tlast;     logic [AXIS_DW-1:0] tdata; } fifo_data_t;  fifo_data_t fifo_data_w; logic       fifo_write; logic       fifo_empty; logic       fifo_read; fifo_data_t fifo_data_r;

Запись данных в FIFO:

always_comb begin : pc_fifo_data_w     fifo_data_w.tdata = AXIS_SLV_IF.tdata;     fifo_data_w.tlast = AXIS_SLV_IF.tlast; end : pc_fifo_data_w  always_comb begin : pc_fifo_write     fifo_write = AXIS_SLV_IF.tvalid & AXIS_SLV_IF.tready; end : pc_fifo_write

Чтение из FIFO и выдача в интерфейс передачи данных:

always_comb begin : pc_fifo_read     fifo_read = (checker_st == ST_SEND && AXIS_MST_IF.tready == 1'b1) ? 1'b1 : 1'b0; end : pc_fifo_read  always_comb begin : pc_axis_mst_if_tvalid     AXIS_MST_IF.tvalid = (checker_st == ST_SEND && fifo_empty == 1'b0) ? 1'b1 : 1'b0; end : pc_axis_mst_if_tvalid  always_comb begin : pc_axis_mst_if_out     AXIS_MST_IF.tdata = (AXIS_MST_IF.tvalid) ? fifo_data_r.tdata : 'x;     AXIS_MST_IF.tlast = (AXIS_MST_IF.tvalid) ? fifo_data_r.tlast : 'x; end : pc_axis_mst_if_out

Итоговый блок, под спойлером и на гитхаб (axis_crc_checker.sv):

axis_crc_checker
`default_nettype none  module axis_crc_checker (     input wire logic    CLK_I,     input wire logic    RST_I,      AXIS_Bus.slave      AXIS_SLV_IF,     AXIS_Bus.master     AXIS_MST_IF );  localparam int AXIS_DW = $bits(AXIS_SLV_IF.tdata);  enum logic [1:0] {ST_RECEIVE, ST_CHECK, ST_RESET, ST_SEND} checker_st;  typedef struct packed {     logic               tlast;     logic [AXIS_DW-1:0] tdata; } fifo_data_t;  logic       packet_received; logic       packet_sended; logic [7:0] crc_in; logic [7:0] crc_out; logic       fifo_rst; fifo_data_t fifo_data_w; logic       fifo_write; logic       fifo_empty; logic       fifo_read; fifo_data_t fifo_data_r;  always_comb begin : pc_packet_received     packet_received = AXIS_SLV_IF.tvalid & AXIS_SLV_IF.tready & AXIS_SLV_IF.tlast; end : pc_packet_received  always_comb begin : pc_packet_sended     packet_sended = AXIS_MST_IF.tvalid & AXIS_MST_IF.tready & AXIS_MST_IF.tlast; end : pc_packet_sended  always_ff @(posedge CLK_I) begin : ps_checker_st     if (RST_I) begin         checker_st <= ST_RECEIVE;     end     else begin         case (checker_st)             ST_RECEIVE : begin                 if (packet_received) begin                     checker_st <= ST_CHECK;                 end             end             ST_CHECK : begin                 if (crc_in == '0) begin                     checker_st <= ST_SEND;                 end                 else begin                     checker_st <= ST_RESET;                 end             end             ST_RESET : begin                 checker_st <= ST_RECEIVE;             end             ST_SEND : begin                 if (packet_sended) begin                     checker_st <= ST_RECEIVE;                 end             end         endcase     end end : ps_checker_st  always_ff @(posedge CLK_I) begin : ps_axis_slv_if_tready     if (RST_I) begin         AXIS_SLV_IF.tready <= 1'b0;     end     else begin         if (packet_received == 1'b1) begin             AXIS_SLV_IF.tready <= 1'b0;         end         else if (checker_st == ST_RECEIVE) begin             AXIS_SLV_IF.tready <= 1'b1;         end     end end : ps_axis_slv_if_tready  always_ff @(posedge CLK_I) begin : ps_crc_in     if (RST_I) begin         crc_in <= '1;     end     else begin         if (AXIS_SLV_IF.tvalid & AXIS_SLV_IF.tready) begin             crc_in <= crc_out;         end         else if (checker_st == ST_CHECK) begin             crc_in <= '1;         end     end end : ps_crc_in  calc_crc u_calc_crc(     .CRC_I  (crc_in),     .DATA_I (AXIS_SLV_IF.tdata),     .CRC_O  (crc_out) );  always_ff @(posedge CLK_I) begin : ps_fifo_rst     if (RST_I) begin         fifo_rst <= 1'b1;     end     else begin         fifo_rst <= (checker_st == ST_RESET) ? 1'b1 : 1'b0;     end end : ps_fifo_rst  always_comb begin : pc_fifo_data_w     fifo_data_w.tdata = AXIS_SLV_IF.tdata;     fifo_data_w.tlast = AXIS_SLV_IF.tlast; end : pc_fifo_data_w  always_comb begin : pc_fifo_write     fifo_write = AXIS_SLV_IF.tvalid & AXIS_SLV_IF.tready; end : pc_fifo_write  sync_fifo #(     .FIFO_DEPTH (1024),     .DATA_WIDTH ($bits(fifo_data_t)) ) u_sync_fifo (     .CLK_I      (CLK_I),     .RST_I      (fifo_rst),     .WR_EN_I    (fifo_write),     .WR_DATA_I  (fifo_data_w),     .FULL_O     (),     .RD_EN_I    (fifo_read),     .RD_DATA_O  (fifo_data_r),     .EMPTY_O    (fifo_empty) );  always_comb begin : pc_fifo_read     fifo_read = (checker_st == ST_SEND && AXIS_MST_IF.tready == 1'b1) ? 1'b1 : 1'b0; end : pc_fifo_read  always_comb begin : pc_axis_mst_if_tvalid     AXIS_MST_IF.tvalid = (checker_st == ST_SEND && fifo_empty == 1'b0) ? 1'b1 : 1'b0; end : pc_axis_mst_if_tvalid  always_comb begin : pc_axis_mst_if_out     AXIS_MST_IF.tdata = (AXIS_MST_IF.tvalid) ? fifo_data_r.tdata : 'x;     AXIS_MST_IF.tlast = (AXIS_MST_IF.tvalid) ? fifo_data_r.tlast : 'x; end : pc_axis_mst_if_out  endmodule  `resetall

3 Реализация тестового окружения

Реализация тестового окружения будет базироваться на том же принципе, который был описан мной в статье Тестирование целочисленного сумматора с интерфейсами AXI-Stream на SystemVerilog

Транзакция для отправки в блок формируется следующим образом: формируется буфер случайного размера, содержимое буфера заполняется случайными данными, в последнее слово буфера помещается вычисленное от содержимого буфера значение контрольной суммы. Иногда формируется пакет, который будет содержать некорректную контрольную сумму, для этого в случайное место в пакете записывается случайное значение. Реализация ниже под спойлером и на гитхаб (transaction_cls_pkg.sv):

transaction_cls_pkg
`ifndef TRANSACTION_CLS_PKG__SV `define TRANSACTION_CLS_PKG__SV  package transaction_cls_pkg;      import test_param_pkg::*;      class transaction_cls;          localparam int MIN_PACK_SIZE = 10;         localparam int MAX_PACK_SIZE = 1024;          rand bit [$clog2(MAX_PACK_SIZE)-1:0] pack_size;         rand bit bad_pack;          constraint c_transaction {             pack_size inside {[MIN_PACK_SIZE : MAX_PACK_SIZE]};         }          logic [DATA_WIDTH-1:0] data_buf [];         logic [DATA_WIDTH-1:0] crc_field;          function void post_randomize ();             int select_index;             data_buf = new[pack_size];              crc_field = '1;             for (int i = 0; i < data_buf.size() - 1; i++) begin                 data_buf[i] = $urandom_range(0, 2**DATA_WIDTH - 1);                 crc_field = calc_crc(.CRC_I(crc_field), .DATA_I(data_buf[i]));             end             data_buf[data_buf.size() - 1] = crc_field;              if (bad_pack === 1'b1) begin                 select_index = $urandom_range(0, data_buf.size());                 data_buf[select_index] = $urandom_range(0, 2**DATA_WIDTH - 1);             end          endfunction : post_randomize      endclass : transaction_cls  endpackage : transaction_cls_pkg  `endif //TRANSACTION_CLS_PKG__SV

Проверка транзакции в тестовом окружении выполняется в блоке scoreboard. Принимается буфер с данными, проверяется контрольная сумма от буфера, если контрольная сумма корректна (равна 0) то содержимое буфера копируется в выходную транзакцию и отправляется из scoreboard. Реализация ниже под спойлером на гитхаб (scoreboard_cls_pkg.sv)

scoreboard_cls_pkg
`ifndef SCOREBOARD_CLS_PKG__SV `define SCOREBOARD_CLS_PKG__SV  package scoreboard_cls_pkg;      import transaction_cls_pkg::*;     import test_param_pkg::*;      class scoreboard_cls;          mailbox #(transaction_cls)  mbx_agt2scb, mbx_scb2chk;         transaction_cls             input_transaction, output_transaction;         int                         cnt_good;         int                         cnt_bad;          function new (             input mailbox #(transaction_cls) mbx_agt2scb, mbx_scb2chk         );             this.mbx_agt2scb = mbx_agt2scb;             this.mbx_scb2chk = mbx_scb2chk;             cnt_good = 0;             cnt_bad = 0;         endfunction : new          task run (             input int count         );             logic [DATA_WIDTH-1:0] crc;              repeat (count) begin                 mbx_agt2scb.get(input_transaction);                 crc = '1;                  foreach (input_transaction.data_buf[i]) begin                     crc = calc_crc(.CRC_I(crc), .DATA_I(input_transaction.data_buf[i]));                 end                  if (crc === '0) begin                     output_transaction = new;                     output_transaction.data_buf = input_transaction.data_buf;                     mbx_scb2chk.put(output_transaction);                     cnt_good++;                 end                 else begin                     cnt_bad++;                 end             end         endtask : run      endclass : scoreboard_cls  endpackage : scoreboard_cls_pkg  `endif //SCOREBOARD_CLS_PKG__SV

Запускаем тест в симуляторе, получаем результат:

# ------------------------------------------------------------ #                    TEST PARAMS #                    TEST SYSTEM VERILOG SOURCES # Simulation run with default random seed # ------------------------------------------------------------ # [ENV] Run count = 1447 # [ENV] Socreboard good packet =  691, scoreboard bad packet =  756 # >>>>> SUCCESS

4 Реализация блока на Chisel

Быстрый поиск привел на сайт chisel-lang.org, где есть необходимые ссылки на документацию и примеры.

Сайт довольно таки хорошо структурирован и быстро была найдена рекомендуемая книга для начала — Digital Design with Chisel. Книга написана хорошо, с примерами. Также в книге есть ссылка на Cheatsheet с кратким описанием основных конструкций и ссылка на репозиторий, который можно использовать в качестве примера.

И так, для начала необходимо реализовать FIFO для хранения пакетов. Так как, Chisel это язык с поддержкой ООП, то все разрабатываемые модули должны расширять класс Module. Входные и выходные интерфейсы оборачиваются в вызов IO.

Расширим класс Bundle для создания 2 интерфейсов: для записи и для чтения:

class WriterIO (DataWidth: Int) extends Bundle {   val WriteEn   = Input(Bool())   val WriteData = Input(UInt(DataWidth.W))   val Full      = Output(Bool()) }  class ReaderIO (DataWidth: Int) extends Bundle {   val ReadEn   = Input(Bool())   val ReadData = Output(UInt(DataWidth.W))   val Empty   = Output(Bool()) }

Где Input/Output задают направление сигналов для модуля. Bool описывает сигнал шириной 1 бит, UInt(DataWidth.W)) описывает сигнал шириной DataWidth бит, задаваемый параметром интерфейса.

Тогда, объявление модуля с интерфейсами будет выглядеть следующим образом:

class SyncFifo (DataWidth: Int, FifoDepth: Int) extends Module {   val io = IO(new Bundle {     val Writer = new WriterIO(DataWidth)     val Reader = new ReaderIO(DataWidth)   } )

У модуля есть 2 параметра: DataWidth — ширина записываемых данных, FifoDepth — глубина FIFO. Синхросигнал и сигнал сброса объявлять не требуется, они будут подключены автоматически.

Дальше нас поджидает следующие отличие от языка SystemVerilog: вычисление параметров.

localparam int ADDR_WIDTH = $clog2(DEPTH);
val ADDR_WIDTH = unsignedBitLength(depth);

Если глубина FIFO равна 8 элементам, то вызов $clog2 вернет значение 3, то есть нужно 3 бита чтобы закодировать 8 значений. Вызов unsignedBitLength от значения 8 вернет значение 4. Необходимо помнить об этом.

Создаем указатели для записи и чтения:

val WritePtr = RegInit(0.U((ADDR_WIDTH).W)) val ReadPtr = RegInit(0.U((ADDR_WIDTH).W))

Здесь воспользовались объектом RegInit. Он создает регистр с начальным значением 0 (которое устанавливается по сигналу сброса) и шириной ADDR_WIDTH бит.

Сформируем признаки full и empty:

io.Writer.Full  := (  (WritePtr(ADDR_WIDTH - 1) =/= ReadPtr(ADDR_WIDTH - 1)) &&                        (WritePtr(ADDR_WIDTH - 2, 0) === ReadPtr(ADDR_WIDTH - 2, 0))) io.Reader.Empty := (WritePtr === ReadPtr)

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

Создаем массив регистров, в которых будем хранить данные:

val FifoCell = Reg(Vec((FifoDepth), UInt(DataWidth.W)))

Здесь воспользовались объектом Reg — регистр без начального значения. Таких регистров необходимо установить FifoDepth штук, для этого используется вызов Vec. Размер каждого из FifoDepth регистров должен быть DataWidth бит.

Полная реализация представлена ниже под спойлером и на гитхаб (FifoPkg.scala):

FifoPkg
package fifo_pkg  import chisel3._ import chisel3.util._  class WriterIO (DataWidth: Int) extends Bundle {   val WriteEn   = Input(Bool())   val WriteData = Input(UInt(DataWidth.W))   val Full      = Output(Bool()) }  class ReaderIO (DataWidth: Int) extends Bundle {   val ReadEn   = Input(Bool())   val ReadData = Output(UInt(DataWidth.W))   val Empty   = Output(Bool()) }  class SyncFifo (DataWidth: Int, FifoDepth: Int) extends Module {   val io = IO(new Bundle {     val Writer = new WriterIO(DataWidth)     val Reader = new ReaderIO(DataWidth)   } )    val ADDR_WIDTH = unsignedBitLength(FifoDepth);    val WritePtr = RegInit(0.U((ADDR_WIDTH).W))   val ReadPtr = RegInit(0.U((ADDR_WIDTH).W))   val FifoCell = Reg(Vec((FifoDepth), UInt(DataWidth.W)))    io.Writer.Full  := (  (WritePtr(ADDR_WIDTH - 1) =/= ReadPtr(ADDR_WIDTH - 1)) &&                          (WritePtr(ADDR_WIDTH - 2, 0) === ReadPtr(ADDR_WIDTH - 2, 0)))   io.Reader.Empty := (WritePtr === ReadPtr)      when (io.Writer.Full === 0.U && io.Writer.WriteEn === 1.U) {     WritePtr := WritePtr + 1.U   }    when (io.Reader.Empty === 0.U && io.Reader.ReadEn === 1.U) {     ReadPtr := ReadPtr + 1.U   }      when (io.Writer.Full === 0.U && io.Writer.WriteEn === 1.U) {     FifoCell(WritePtr(ADDR_WIDTH - 2, 0)) := io.Writer.WriteData;   }    io.Reader.ReadData := FifoCell(ReadPtr(ADDR_WIDTH - 2, 0)) }

Блок вычисления контрольной суммы будет использоваться тот же, что и в реализации на SystemVerilog, так как Chisel позволяет подключать внешние блоки как blackbox:

class calc_crc extends HasBlackBoxResource {   val io = IO (new Bundle {     val CRC_I   = Input(UInt(8.W))     val DATA_I  = Input(UInt(8.W))     val CRC_O   = Output(UInt(8.W))   } )   addResource("calc_crc.v") }

Для реализации интерфейса AXI-Stream в Chisel есть класс DecoupledIO, который содержит сигналы valid и ready, а также поле bits для данных.

Расширим класс Bundle, добавив в него сигнал Tdata (шириной 8 бит) и сигнал Tast (шириной 1 бит):

class AxisBus extends Bundle {   val Tlast = Bool()       val Tdata = UInt(8.W) }

Тогда, объявление модуля будет выглядеть следующим образом:

class AxisCrcChecker extends Module {    val io = IO(new Bundle {                     val AxisSlv = Flipped(new DecoupledIO(new AxisBus))     val AxisMst = new DecoupledIO(new AxisBus)   } ) 

По умолчанию, при добавлении класса DecoupledIO, модуль считывает его выходным интерфейсом, то есть ready вход, valid выход. Чтобы сделать его входным интерфейсом (ready выход, valid вход) необходимо добавить вызов Flipped.

Объявление конечного автомата и реализация:

  object State extends ChiselEnum {     val ST_RECEIVE, ST_CHECK, ST_RESET, ST_SEND = Value   }   import State._   val CheckerStReg = RegInit(ST_RECEIVE)      switch (CheckerStReg) {     is (ST_RECEIVE) {       when (PacketReceived === 1.U) {         CheckerStReg := ST_CHECK       }     }     is (ST_CHECK) {       when (CrcInReg === 0.U) {         CheckerStReg := ST_SEND       }       .otherwise {         CheckerStReg := ST_RESET       }     }     is (ST_RESET) {       CheckerStReg := ST_RECEIVE     }     is (ST_SEND) {       when (RacketSended === 1.U) {         CheckerStReg := ST_RECEIVE       }     }   }

Создается новый тип, с помощью RegInit создается регистр со значением после сброса ST_RECEIVE.

Признаки окончания приема пакета и окончания передачи пакета:

val PacketReceived  = Wire(Bool()) val RacketSended    = Wire(Bool())  PacketReceived := io.AxisSlv.valid & io.AxisSlv.ready & io.AxisSlv.bits.Tlast  RacketSended := io.AxisMst.valid & io.AxisMst.ready & io.AxisMst.bits.Tlastt

Подсчет контрольной суммы:

val CrcInReg        = RegInit("hFF".U(8.W)) val CrcOut          = Wire(UInt(8.W))  when (io.AxisSlv.valid & io.AxisSlv.ready) {   CrcInReg := CrcOut } .elsewhen (CheckerStReg === ST_CHECK) {   CrcInReg := "hFF".U }  val u_calc_crc = Module(new calc_crc())  u_calc_crc.io.CRC_I   := CrcInReg u_calc_crc.io.DATA_I  := io.AxisSlv.bits.Tdata CrcOut                := u_calc_crc.io.CRC_O

Здесь, регистр CrcInReg инициализируется значением 0xFF после сигнала сброса, CrcOut представляет собой шину для получения результата из модуля подсчета crc. Выполняется установка модуля подсчета crc и подключение сигналов к его портам.

Сброс FIFO в случае несовпадения контрольной суммы:

val FifoRstReg      = RegInit(1.U(1.W))  FifoRstReg := (CheckerStReg === ST_RESET)  val u_sync_fifo = Module(new SyncFifo(9, 1024))    u_sync_fifo.reset := (FifoRstReg === 1.U)

Регистр FifoRstReg устанавливается в значение 1 при сигнале сброса или при нахождении управляющего автомата в состоянии ST_RESET. Выполняется подключение к неявному сигналу сброса у модуля u_sync_fifo.

В Chisel есть две конструкции, которые позволяет группировать сигналы: Bundle и Vec. Bundle группирует сигналы разных типов как именованные поля. Vec представляет собой индексируемый набор сигналов одного типа. Vec не подходит, так как сигналы разного типа: 8 бит данных и 1 бит признака last. Также, запрещается заполнение массива бит с прямым указанием индекса. Пример из книги, который приведет к ошибке:

val assignWord = Wire(UInt(16.W)) assignWord(7, 0) := lowByte assignWord(15, 8) := highByte

В книге предлагается следующее решение проблемы: создание дополнительной структуры на основе Bundle:

val assignWord = Wire(UInt(16.W)) class Split extends Bundle {   val high = UInt(8.W)   val low = UInt(8.W) }  val split = Wire(new Split()) split.low := lowByte split.high := highByte assignWord := split.asUInt()

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

Был написан следующий код:

class FifoDataT extends Bundle {   val Last = UInt(1.W)   val Data = UInt(8.W) }  val FifoDataW = Wire(new FifoDataT()) val Rifo_data_r = Wire(new FifoDataT())  FifoDataW.data := io.AxisSlv.bits.Tdata FifoDataW.last := io.AxisSlv.bits.Tlast  u_sync_fifo.io.Writer.WriteData := FifoDataW.asUInt fifo_data_r := u_sync_fifo.io.reader.rd_data; 

И получена ошибка при попытке подключить порт прочитанных данных из FIFO к экземпляру созданного класса:

error] chisel3.package$ChiselException: Connection between sink (AxisCrcChecker.fifo_data_r: Wire[FifoDataT]) and source (SyncFifo.io.reader.rd_data: IO[UInt<9>]) failed @: Sink (FifoDataT) and Source (UInt<9>) have different types. [error]         at ... () [error]         at eht_frame_filter_pkg.AxisCrcChecker.<init>(AxisCrcChecker.scala:116) [error]         at eht_frame_filter_pkg.AxisCrcCheckerMain$.$anonfun$new$46(AxisCrcChecker.scala:124) [error]         at ... () [error]         at ... (Stack trace trimmed to user code only. Rerun with --full-stacktrace to see the full stack trace) [error] stack trace is suppressed; run last Compile / run for the full output [error] (Compile / run) chisel3.package$ChiselException: Connection between sink (AxisCrcChecker.fifo_data_r: Wire[FifoDataT]) and source (SyncFifo.io.reader.rd_data: IO[UInt<9>]) failed @: Sink (FifoDataT) and Source (UInt<9>) have different types. 

Понятно, что типы не совпадают, но что с этим делать — не понятно. В итого перепишем по старинке:

u_sync_fifo.io.Writer.WriteData := io.AxisSlv.bits.Tlast ## io.AxisSlv.bits.Tdata u_sync_fifo.io.Writer.WriteEn := io.AxisSlv.valid & io.AxisSlv.ready  u_sync_fifo.io.Reader.ReadEn := (CheckerStReg === ST_SEND && io.AxisMst.ready === 1.U)   io.AxisMst.valid := (CheckerStReg === ST_SEND && u_sync_fifo.io.Reader.Empty === 0.U)  io.AxisMst.bits.Tdata := u_sync_fifo.io.Reader.ReadData(7, 0) io.AxisMst.bits.Tlast := u_sync_fifo.io.Reader.ReadData(8)

Здесь, операнд ## это конкатенация шин, аналог {} в SystemVerilog.

Код итоговый модуля приведен ниже и на гитхаб (AxisCrcChecker.scala)

AxisCrcChecker
package eht_frame_filter_pkg  import chisel3._ import chisel3.util._  import fifo_pkg._  class calc_crc extends HasBlackBoxResource {   val io = IO (new Bundle {     val CRC_I   = Input(UInt(8.W))     val DATA_I  = Input(UInt(8.W))     val CRC_O   = Output(UInt(8.W))   } )   addResource("calc_crc.v") }  class AxisBus extends Bundle {   val Tlast = Bool()       val Tdata = UInt(8.W) }  class AxisCrcChecker extends Module {    val io = IO(new Bundle {                     val AxisSlv = Flipped(new DecoupledIO(new AxisBus))     val AxisMst = new DecoupledIO(new AxisBus)   } )     val PacketReceived  = Wire(Bool())   val RacketSended    = Wire(Bool())   val CrcInReg        = RegInit("hFF".U(8.W))   val CrcOut          = Wire(UInt(8.W))   val FifoRstReg      = RegInit(1.U(1.W))    object State extends ChiselEnum {     val ST_RECEIVE, ST_CHECK, ST_RESET, ST_SEND = Value   }   import State._   val CheckerStReg = RegInit(ST_RECEIVE)    PacketReceived := io.AxisSlv.valid & io.AxisSlv.ready & io.AxisSlv.bits.Tlast    RacketSended := io.AxisMst.valid & io.AxisMst.ready & io.AxisMst.bits.Tlast      switch (CheckerStReg) {     is (ST_RECEIVE) {       when (PacketReceived === 1.U) {         CheckerStReg := ST_CHECK       }     }     is (ST_CHECK) {       when (CrcInReg === 0.U) {         CheckerStReg := ST_SEND       }       .otherwise {         CheckerStReg := ST_RESET       }     }     is (ST_RESET) {       CheckerStReg := ST_RECEIVE     }     is (ST_SEND) {       when (RacketSended === 1.U) {         CheckerStReg := ST_RECEIVE       }     }   }    val AxisSlvTready = RegInit(0.U(1.W))   when (PacketReceived === 1.U) {     AxisSlvTready := 0.U   }   .elsewhen (CheckerStReg === ST_RECEIVE) {     AxisSlvTready := 1.U   }    io.AxisSlv.ready := AxisSlvTready    when (io.AxisSlv.valid & io.AxisSlv.ready) {     CrcInReg := CrcOut   }   .elsewhen (CheckerStReg === ST_CHECK) {     CrcInReg := "hFF".U   }    val u_calc_crc = Module(new calc_crc())    u_calc_crc.io.CRC_I   := CrcInReg   u_calc_crc.io.DATA_I  := io.AxisSlv.bits.Tdata   CrcOut                := u_calc_crc.io.CRC_O    FifoRstReg := (CheckerStReg === ST_RESET)    val u_sync_fifo = Module(new SyncFifo(9, 1024))      u_sync_fifo.reset := (FifoRstReg === 1.U)    u_sync_fifo.io.Writer.WriteData := io.AxisSlv.bits.Tlast ## io.AxisSlv.bits.Tdata   u_sync_fifo.io.Writer.WriteEn := io.AxisSlv.valid & io.AxisSlv.ready    u_sync_fifo.io.Reader.ReadEn := (CheckerStReg === ST_SEND && io.AxisMst.ready === 1.U)     io.AxisMst.valid := (CheckerStReg === ST_SEND && u_sync_fifo.io.Reader.Empty === 0.U)    io.AxisMst.bits.Tdata := u_sync_fifo.io.Reader.ReadData(7, 0)   io.AxisMst.bits.Tlast := u_sync_fifo.io.Reader.ReadData(8) }  object AxisCrcCheckerMain extends App {   println("Generating the hardware")   emitVerilog(new AxisCrcChecker(), Array("--target-dir", "generated")) }

Запустив трансляцию, будет получен файл AxisCrcChecker.v, содержащий код на verilog. В целом, файл имеет читаемый вид, каждый описанный модуль на языке Chisel представляет собой один always блок.

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

# ------------------------------------------------------------ #                    TEST PARAMS #                    TEST CHISEL SOURCES # Simulation run with default random seed # ------------------------------------------------------------ # [ENV] Run count = 1447 # [ENV] Socreboard good packet =  691, scoreboard bad packet =  756 # >>>>> SUCCESS

Значит модуль, написанный на языке Chisel разработан корректно.

5 Выводы

Субъективное мнение таково:

  1. Смена языка вызывает ломку, поначалу всегда будет сопротивление новому, это нормально.

  2. Не понравилась в Chisel работа со структурами данных. По работе часто приходится реализовывать разбор каких-то структур данных, struct и union в SystemVerilog с этим помогают.

  3. Не понравилось, что в результате трансляции в verilog, все содержимое файла размещается в одном always блоке, и сигналы у которых используется сигнал сброса, и сигналы у которых не используется сигнал сброса.

  4. Количество строк в блоке на Chisel меньше, чем в блоке на SystemVerilog.

  5. В общем, мой выбор на текущий момент — SystemVerilog.


ссылка на оригинал статьи https://habr.com/ru/articles/891966/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *