Документация Engee
Notebook

Счётчик

В данном примере мы разработаем и протестируем модель счётчика, поддерживающую генерацию кода 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.

Далее рассмотрим саму реализованную модель и её настройки генерации. image.png

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

  1. шаг, с которым выполняется приращение счётчика;
  2. предел максимально допустимого значения счётчика;
  3. сброс счётчика.

Теперь, когда мы разобрались с моделью, сгенерируем её код и проведём верификацию полученного кода на основе сгенерированной C function и на основе рукописного тест-бенча Verilog, предварительно добавив папку с compare.cgt в путь Engee.

image_3.png

In [ ]:
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)
Building...
Progress 0%
Progress 100%
Progress 100%
Out[0]:

Как мы видим, результаты работы С кода и исходной модели идентичны, что свидетельствует о корректной работе генератора кода. Теперь перейдём к написанию тестовой обвязки нашего сгенерированного кода.

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 предназначен для проверки работы счётчика. Ниже перечислим действия, которые он выполняет.

  1. Инициализирует сигналы:
    • clock = 0;
    • reset = 1;
    • io_Step = 3;
    • io_Limit_max = 27;
    • io_Reset = 0.
  2. Генерирует тактовый сигнал с периодом 10 тактов.
  3. Через 10 тактов деактивирует сигнал reset.
  4. Выводит значения io_Step, io_Limit_max и io_Cnt для наблюдения за поведением счётчика.
  5. Завершает симуляцию через 200 тактов.

Теперь запустим наш тест, используя ​сайт JDoodle Verilog Online. Это удобная онлайн-платформа для написания, компиляции и запуска кода на языке Verilog прямо в браузере.

In [ ]:
# display(MIME("text/html"), 
#                 """<iframe 
#                 src="https://www.jdoodle.com/execute-verilog-online" 
#                 width="750" height="500" 
#                 style="border: none;">
#                 </iframe>""")

image.png

Вывод

Результаты симуляции говорят о том, что код работает корректно. И он аналогичен результатам, полученным в модели. Это означает, что сгенерированный счётчик может применяться в работе на ПЛИС.