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

Генерация Verilog из моделей Engee с фиксированной точкой

Рассмотрим генерацию Verilog из четырёх моделей Engee с фиксированной точкой. На вычислении площади и периметра покажем влияние разрядности, переполнение и добавление регистра. Затем проверим сгенерированные модули с помощью тестовых стендов.

Арифметика с фиксированной точкой

Тип задаёт знак, разрядность и число дробных битов. Значение хранится как . Для знакового типа диапазон равен

Сравним форматы fixdt(1,16,8) и fixdt(1,6,2). В моделях используется округление Floor, насыщение отключено: переполнение приводит к циклическому переходу значения.

Описание моделей

Первые три модели вычисляют среднюю линию, периметр и площадь трапеции:

Заданы , , , , ; при проверке регистра также используем .

  • model_old — вычисления в формате fixdt(1,16,8).
  • model_new — тот же расчёт в формате fixdt(1,6,2).
  • model_delay — формат fixdt(1,16,8) и Unit Delay на выходе площади.
  • model_triangle — отдельный расчёт при , .

Модели используют библиотечные Add, Gain и Product. Для генерации выбран TargetHardware = Verilog.

Подготовка

Модели и тестовые стенды находятся в папке примера. Сгенерированный код сохраняется во временную папку внутри /user. Подготовим функции запуска и проверки.

In [ ]:
using Main: engee, plot, plot!, histogram, hline!, savefig, Animation, frame, gif
root = @__DIR__
generated = mktempdir("/user"; prefix=".verilog_demo_")
println("Сгенерированные файлы: ", generated)

function run_demo_model(root::String, name::String)
    path = joinpath(root, name * ".engee")
    @assert isfile(path) "Нет модели: $path"
    model = engee.load(path; force=true)
    try
        return engee.run(model; verbose=false)
    finally
        engee.close(model; force=true)
    end
end

function logged_values(result, signal)
    return Float64.((collect(result[signal])).value)
end

for file in ("model_old.engee", "model_new.engee", "model_delay.engee",
             "model_triangle.engee", "tb_trapezoid_delay.v", "tb_triangle.v")
    @assert isfile(joinpath(root, file)) "Нет исходного файла: $file"
end
println("Исходные модели и тестбенчи найдены")
Исходные модели и тестбенчи найдены

Проверим наличие файлов четырёх моделей и двух тестовых стендов.

Шаг 1. Разрядность и переполнение

Запустим две модели трапеции. При достаточной разрядности ожидаем периметр 19 и площадь 24.

In [ ]:
old_sim = run_demo_model(root, "model_old")
new_sim = run_demo_model(root, "model_new")
old_P = logged_values(old_sim, "P")
new_P = logged_values(new_sim, "P")
old_S = logged_values(old_sim, "S")
new_S = logged_values(new_sim, "S")
@assert !isempty(old_P) && !isempty(new_P)
@assert isapprox(old_P[end], 19.0; atol=1/256)
println("P: 16 бит = $(old_P[end]), 6 бит = $(new_P[end])")
println("S: 16 бит = $(old_S[end]), 6 бит = $(new_S[end])")
P: 16 бит = 19.0, 6 бит = 3.0
S: 16 бит = 24.0, 6 бит = -8.0

В формате fixdt(1,16,8) получаем P = 19 и S = 24. В формате fixdt(1,6,2) сумма оснований 12 выходит за диапазон и превращается в −4. Поэтому m = −2, P = 3 и S = −8.

Сгенерируем Verilog и проверим разрядность портов: 16 и 6 бит соответственно.

In [ ]:
old_dir = joinpath(generated, "01_trapezoid_16bit")
new_dir = joinpath(generated, "02_trapezoid_6bit")
mkpath(old_dir)
mkpath(new_dir)
engee.generate_code(joinpath(root, "model_old.engee"), old_dir,
                    subsystem_name="trapeze_calculator")
engee.generate_code(joinpath(root, "model_new.engee"), new_dir,
                    subsystem_name="trapeze_calculator")
old_v = joinpath(old_dir, "model_old_trapeze_calculator.v")
new_v = joinpath(new_dir, "model_new_trapeze_calculator.v")
@assert isfile(old_v) && isfile(new_v)
@assert occursin(r"input\s+\[15:0\]", read(old_v, String))
@assert occursin(r"input\s+\[5:0\]", read(new_v, String))
println("RTL: 16-битные и 6-битные входы подтверждены")
RTL: 16-битные и 6-битные входы подтверждены

В сгенерированном коде Gain реализуется сдвигом, а Product — умножением с выбором нужных разрядов результата. Размеры сигналов отражают форматы фиксированной точки в модели.

Шаг 2. Регистр и тактовая проверка

Запустим модель с Unit Delay. На первом шаге площадь должна быть нулевой, далее — равной результату предыдущего шага.

In [ ]:
delay_sim = run_demo_model(root, "model_delay")
delay_P = logged_values(delay_sim, "P")
@assert length(delay_P) >= 5 && length(old_P) >= 5
@assert isapprox(delay_P[1], 0.0; atol=1/256)
@assert all(isapprox.(delay_P[2:5], old_P[1:4]; atol=1/256))
println("P без задержки: ", old_P[1:5])
println("P с задержкой: ", delay_P[1:5])
P без задержки: [19.0, 19.0, 19.0, 19.0, 19.0]
P с задержкой: [0.0, 19.0, 19.0, 19.0, 19.0]

Результат моделирования подтверждает задержку площади на один шаг.

Сгенерируем модуль с регистром и выполним тактовую проверку в Icarus Verilog. Тестовый стенд проверяет сброс и результаты для двух значений высоты. Для запуска нужны iverilog и vvp.

In [ ]:
delay_dir = joinpath(generated, "03_trapezoid_delay")
mkpath(delay_dir)
engee.generate_code(joinpath(root, "model_delay.engee"), delay_dir,
                    subsystem_name="trapeze_calculator")
delay_v = joinpath(delay_dir, "model_delay_trapeze_calculator.v")
@assert isfile(delay_v)
delay_rtl = read(delay_v, String)
@assert occursin(r"\breg\b", delay_rtl)
@assert occursin("posedge clock", delay_rtl)

delay_tb = joinpath(root, "tb_trapezoid_delay.v")
delay_exe = joinpath(delay_dir, "sim_delay")
run(`iverilog -o $delay_exe $delay_tb $delay_v`)
delay_output = read(`vvp $delay_exe`, String)
@assert occursin("h=4: m=6.00 P=19.00 S=24.00", delay_output)
@assert occursin("h=5: m=6.00 P=19.00 S=30.00", delay_output)
println(delay_output)
h=4: m=6.00 P=19.00 S=24.00
h=5: m=6.00 P=19.00 S=30.00

Тестовый стенд подтверждает m = 6, P = 19 и площади 24 и 30. Unit Delay преобразуется в регистр, обновляемый по фронту тактового сигнала.

Шаг 3. Отдельный вычислительный модуль

В отдельной модели вычислим площадь треугольника при a = 8 и h = 5. Ожидаемый результат — 20.

In [ ]:
triangle_sim = run_demo_model(root, "model_triangle")
triangle_S = logged_values(triangle_sim, "S")
@assert !isempty(triangle_S)
println("Значения S модели Engee: ", triangle_S)
Значения S модели Engee: [20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0]

Модель выдаёт S = 20.

Сгенерируем модуль S_triangle и проверим три набора входных данных. Ожидаемые площади — 10, 10.5 и 30.

In [ ]:
triangle_dir = joinpath(generated, "04_triangle")
mkpath(triangle_dir)
engee.generate_code(joinpath(root, "model_triangle.engee"), triangle_dir,
                    subsystem_name="S_triangle")
triangle_v = joinpath(triangle_dir, "model_triangle_S_triangle.v")
@assert isfile(triangle_v)
triangle_rtl = read(triangle_v, String)
@assert occursin("module model_triangle_S_triangle", triangle_rtl)

triangle_tb = joinpath(root, "tb_triangle.v")
triangle_exe = joinpath(triangle_dir, "sim_triangle")
run(`iverilog -o $triangle_exe $triangle_tb $triangle_v`)
triangle_output = read(`vvp $triangle_exe`, String)
for expected in ("a=5, h=4: S=10.00", "a=7, h=3: S=10.50",
                 "a=10, h=6: S=30.00")
    @assert occursin(expected, triangle_output) "Не совпал RTL-результат: $expected"
end
println(triangle_output)
a=5, h=4: S=10.00
a=7, h=3: S=10.50
a=10, h=6: S=30.00

Все три проверки совпадают с ожидаемыми значениями. Выбор разрядов произведения учитывает дробную часть формата и деление на два.

Закроем модели, если они ещё открыты.

In [ ]:
for name in ("model_old", "model_new", "model_delay", "model_triangle")
    if name in [m.name for m in engee.get_all_models()]
        engee.close(name; force = true)
    end
end
"Verilog demo passed; four models and two RTL testbenches verified"

Вывод

Сгенерированный Verilog сохраняет особенности вычислений модели: разрядность, переполнение и задержку на один такт. Проверки в Icarus Verilog подтвердили ожидаемые результаты для рассмотренных вычислительных модулей.