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

8fd918f29c6bca5bd633edca29182943.jpg

Недавно возникла потребность в быстром погружении в язык 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.(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.

© Habrahabr.ru