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

Недавно возникла потребность в быстром погружении в язык Chisel. Чтобы попробовать новый язык, не хотелось писать счетчик или сумматор, а что-то приближенное к рабочим моментам. И так, сформируем задание на разрабатываемый блок:
интерфейс получения данных — AXI-Stream;
интерфейс передачи данных — AXI-Stream;
максимальный размер обрабатываемого пакета — 1024 байта;
последнее слово пакета содержит значение контрольной суммы от пакета, посчитанное по алгоритму crc8;
необходимо выполнить проверку значения контрольной суммы. Если контрольная сумма корректна, передать пакет в интерфейс передачи данных, иначе, удалить пакет.
Содержание
1 Общая информация
2 Реализация блока на SystemVerilog
3 Реализация тестового окружения
4 Реализация блока на Chisel
5 Выводы
1 Общая информация
Алгоритм работы блока будет следующим:
Прием пакета из интерфейса получения данных, подсчет контрольной суммы, запись пакета в FIFO. Прием пакета заканчивается при поступлении признака конца пакета (tlast = 1, когда установлены сигналы tvalid = 1 и tready = 1). Переход в состояние анализа контрольной суммы.
Проверка значения контрольной суммы. Если контрольная сумма корректна (значение контрольной суммы равно 0), то переход в состояние передачи пакета из FIFO в интерфейс передачи данных. Иначе, переход в состояние удаления пакета.
Удаление пакета. Выполняется сброс FIFO, переход в состояние приема пакета из интерфейса получения данных.
Передача пакета из FIFO в интерфейс передачи данных. Передача заканчивается, когда будет выдан конец пакета (tlast = 1, когда установлены сигналы tvalid = 1 и tready = 1). Переход в состояние приема пакета из интерфейса получения данных
Алгоритм не особо сложный, чтобы просто пощупать новый язык.
2 Реализация блока на SystemVerilog
Сначала будет выполнена реализация на SystemVerilog, чтобы на этой реализации опробовать тестовое окружение, а потом с помощью этого тестового окружения верифицировать реализацию на Chisel.
Для начала потребуется блок для хранения пакета. Будем использовать FIFO, код простого FIFO приведен ниже под спойлером и на гитхаб (sync_fifo.sv)
`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)
// 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):
`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):
`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)
`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):
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)
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 Выводы
Субъективное мнение таково:
Смена языка вызывает ломку, поначалу всегда будет сопротивление новому, это нормально.
Не понравилась в Chisel работа со структурами данных. По работе часто приходится реализовывать разбор каких-то структур данных, struct и union в SystemVerilog с этим помогают.
Не понравилось, что в результате трансляции в verilog, все содержимое файла размещается в одном always блоке, и сигналы у которых используется сигнал сброса, и сигналы у которых не используется сигнал сброса.
Количество строк в блоке на Chisel меньше, чем в блоке на SystemVerilog.
В общем, мой выбор на текущий момент — SystemVerilog.