Счётчик¶
В данном примере мы разработаем и протестируем модель счётчика, поддерживающую генерацию кода Verilog, а также протестируем её работоспособность с применением облачного компилятора.
Начнём с анализа реализованной модели. Это простая модель, сделанная с помощью базовых блоков. Она поддерживает генерацию кода, в том числе в данной модели применяется шаблон генерации кода для блока операторов сравнения. Сам шаблон генерации представлен ниже.
Данный код — это генератор Chisel-кода, предназначенный для генерации блока логического оператора (например, ==
, !=
, <
, >
). Он использует встроенные вспомогательные функции и метки (например, //!
, /*!
) для генерации Verilog-кода.
//! BlockType = :RelationalOperator
//! TargetLang = :Chisel
/*! #----------------------------------------------------# */
/*! @Definitions
function show_chisel_type(x::typ) :: String
if x.bits == 1 && x.fractional_bits == 0
out = "Bool()"
elseif x.fractional_bits == 0
out = (x.is_unsigned ? "U" : "S") * "Int($(x.bits).W)"
else
out = "FixedPoint($(x.bits).W,$(x.fractional_bits).BP)"
end
out
end
function show_chisel_type(x::signal) :: String
base_type = show_chisel_type(x.ty)
len = x.rows * x.cols
len == 1 ? base_type : "Vec($(len), $(base_type))"
end
*/
val $(output(1)) = Wire($(show_chisel_type(output(1))))
/*! #----------------------------------------------------# */
/*! @Step
function patch_op(op)
op == "==" ? "===" : op == "~=" ? "=/=" : op
end
function get_idx(len, blen)
if len == 1
return ""
elseif len == blen
return "(i)"
else
return "(i % $(len))"
end
end
function maybe_zext(sig, sig2)
if sig.ty.is_unsigned && !sig2.ty.is_unsigned
return ".zext"
end
""
end
function impl(sig1, sig2, op, out)
dim0 = dim(out)
dim1 = dim(sig1)
dim2 = dim(sig2)
blen = lcm(dim1, dim2) # broadcasted length
if blen == 1
loop_decl = ""
else
loop_decl = "for (i <- 0 until $(blen)) "
end
"$(loop_decl)$(output(1))$(get_idx(dim0, blen)) := \
$(sig1)$(get_idx(dim1, blen))$(maybe_zext(sig1, sig2)) \
$(patch_op(op)) \
$(sig2)$(get_idx(dim2, blen))$(maybe_zext(sig2, sig1))"
end
*/
$(impl(input(1), input(2), param.Operator, output(1)))
/*! #----------------------------------------------------# */
Код представляет собой поэтапное описание блока.
1. Определение функций (блок @Definitions
)
show_chisel_type(x::typ)
Определяет, как представить тип x
в синтаксисе Chisel:
Bool()
, если 1-битное логическое значение.UInt(...)
илиSInt(...)
, если целочисленный тип (без дробной части).FixedPoint(...)
, если есть дробная часть (фиксированная точка).
show_chisel_type(x::signal)
Если сигнал — массив (Vec
), то возвращает Vec(n, base_type)
, иначе просто base_type
.
2. Объявление выходного сигнала
chisel
val $(output(1)) = Wire($(show_chisel_type(output(1))))
Создаёт выходной сигнал как Wire(...)
, тип которого определяется по описанным выше функциям.
3. Генерация тела блока (блок @Step
)
patch_op(op)
Преобразует операторы в синтаксис Chisel:
"=="
→"==="
"~="
→"=/="
(неравенство)
get_idx(len, blen)
Используется для обращения к индексам при векторной (массивной) логике:
- возвращает
""
, если скаляр. (i)
, если длины совпадают.(i % len)
, если требуется broadcasting.
maybe_zext(sig, sig2)
Если один из сигналов — unsigned
, а другой — signed
, возможно, требуется zext
(расширение нулями до совместимого формата).
impl(sig1, sig2, op, out)
Основная функция генерации строки:
- определяет размеры сигналов и их broadcast длину;
- при необходимости оборачивает в
for
-цикл. - формирует присваивание вида
chisel
output(i) := input1(i) <op> input2(i)
с обработкой zext
, индексами и переводом операторов.
Этот код реализует универсальный генератор Chisel-блока для логических операций с поддержкой векторных сигналов, broadcasting и приведения типов. Он компилируется в Chisel-код для аппаратного описания блоков, например, для FPGA или ASIC.
Далее рассмотрим саму реализованную модель и её настройки генерации.
Как мы видим, модель достаточно простая и имеет три входных порта:
- шаг, с которым выполняется приращение счётчика;
- предел максимально допустимого значения счётчика;
- сброс счётчика.
Теперь, когда мы разобрались с моделью, сгенерируем её код и проведём верификацию полученного кода на основе сгенерированной C function и на основе рукописного тест-бенча Verilog, предварительно добавив папку с compare.cgt
в путь Engee.
function run_model(name_model)
Path = string(@__DIR__) * "/" * name_model * ".engee"
if name_model in [m.name for m in engee.get_all_models()] # Проверка условия загрузки модели в ядро
model = engee.open( name_model ) # Открыть модель
model_output = engee.run( model, verbose=true ); # Запустить модель
else
model = engee.load( Path, force=true ) # Загрузить модель
model_output = engee.run( model, verbose=true ); # Запустить модель
engee.close( name_model, force=true ); # Закрыть модель
end
return model_output
end
run_model("Counter_C_test")
Cnt_ref = simout["Counter_C_test/Cnt_ref"].value;
Cnt_C = simout["Counter_C_test/Cnt_C"].value;
plot(Cnt_ref)
plot!(Cnt_C)
Как мы видим, результаты работы С кода и исходной модели идентичны, что свидетельствует о корректной работе генератора кода. Теперь перейдём к написанию тестовой обвязки нашего сгенерированного кода.
module count_Count(
input clock,
reset,
input [31:0] io_Step,
io_Limit_max,
input io_Reset,
output [31:0] io_Cnt
);
reg [31:0] UnitDelay_state;
wire [31:0] Switch =
$signed(UnitDelay_state) > $signed(io_Limit_max) | io_Reset ? 32'h0 : UnitDelay_state;
always @(posedge clock) begin
if (reset)
UnitDelay_state <= 32'h0;
else
UnitDelay_state <= io_Step + Switch;
end // always @(posedge)
assign io_Cnt = Switch;
endmodule
module testbench;
reg clock;
reg reset;
reg [31:0] io_Step;
reg [31:0] io_Limit_max;
reg io_Reset;
wire [31:0] io_Cnt;
// Instantiate the module under test
count_Count dut (
.clock(clock),
.reset(reset),
.io_Step(io_Step),
.io_Limit_max(io_Limit_max),
.io_Reset(io_Reset),
.io_Cnt(io_Cnt)
);
// Clock generation
always 5 clock = ~clock;
// Test sequence
initial begin
// Initialize signals
clock = 0;
reset = 1;
io_Step = 3;
io_Limit_max = 27;
io_Reset = 0;
// Display header
$display("tStep\tLimit\tCnt");
$monitor(io_Step, io_Limit_max, io_Cnt);
// Apply reset
10 reset = 0;
// Run simulation until counter wraps around
200;
$finish;
end
endmodule
Модуль testbench
предназначен для проверки работы счётчика. Ниже перечислим действия, которые он выполняет.
- Инициализирует сигналы:
clock
= 0;reset
= 1;io_Step
= 3;io_Limit_max
= 27;io_Reset
= 0.
- Генерирует тактовый сигнал с периодом 10 тактов.
- Через 10 тактов деактивирует сигнал
reset
. - Выводит значения
io_Step
,io_Limit_max
иio_Cnt
для наблюдения за поведением счётчика. - Завершает симуляцию через 200 тактов.
Теперь запустим наш тест, используя сайт JDoodle Verilog Online. Это удобная онлайн-платформа для написания, компиляции и запуска кода на языке Verilog прямо в браузере.
# display(MIME("text/html"),
# """<iframe
# src="https://www.jdoodle.com/execute-verilog-online"
# width="750" height="500"
# style="border: none;">
# </iframe>""")
Вывод¶
Результаты симуляции говорят о том, что код работает корректно. И он аналогичен результатам, полученным в модели. Это означает, что сгенерированный счётчик может применяться в работе на ПЛИС.