Verilog代码生成功能
导言
在当今的数字世界中,计算性能和能效通常由硬件平台决定。 中央处理器(Cpu)功能多样,但对于实时信号处理、计算机视觉、通信协议或神经网络等专门任务来说并不总是最佳的。 在这里,他们脱颖而出Fpga(可编程逻辑集成电路)。
Fpga是一类独特的微电路,其内部结构(逻辑元件,存储块,专用节点的连接)可以在制造后由开发人员自己反复重新编程。 这允许您创建不是软件,而是物理硬件,理想情况下为特定算法量身定制。 因此,实现了前所未有的性能:并行性、可预测的响应时间和最小的功耗。
然而,FPGA编程与为处理器编写代码有着根本的不同。 在Verilog(RTL级)上进行设计需要特定的、基于硬件的思维、对时间图、时序和资源优化的深刻理解。 这创造了一个高的进入门槛,并增加了复杂算法模块的开发时间。 代替顺序指令,这里有必要描述硬件—由寄存器,加法器,多路复用器和并行操作的有限自动机组成的数字电路。 为此,有硬件描述语言(HDL-Hardware Description Language)。 两个主要代表是VHDL和Verilog。
Verilog是开发人员描述元素应该如何连接以及它们应该如何随着时间的推移表现的语言。 编译器(合成器)将此描述转换为FPGA的配置(比特流),实际上将您创建的电路"拼接"到其中。 所以你不是在写一个程序,而是在设计一个数字设备。
但是从数学算法到Verilog上的描述是一个复杂而耗时的过程,需要对数字电路有深入的了解。 这就是Engee等高级合成工具来拯救的地方。
Fpga的关键结构要素:DSP、FF、LUT和BRAM
FPGA架构基于可配置逻辑块(Clb,或ALTERA/Intel术语中的LAB)。 它们是基本的建筑元素,由较小的切片模块组成。 每个切片包括用于实现组合逻辑的可编程真值表(lut)和用于存储数据的触发器(FF)。 正是这些元件执行基本逻辑和寄存器功能,构成任何数字电路的核心。
对于高性能计算,专用DSP模块集成到FPGA中。 它们的关键特性是乘法累加(MAC)运算的优化。 这样的块不仅能够快速乘以数字,而且还可以将第三个值添加到结果中,然后在累加器中累加该量。 它们通常还包括一个预加法器,允许在乘法之前执行加法,例如,使用公式(A+D)×B+C。这使得它们对于数字信号处理,滤波器实现和神经网络来说是必不可少的。
内置块存储器(BRAM)为FPGA提供数据存储资源。 它是一个同步双端口存储器,允许您在同一时间独立读取和写入数据到两个地址。 BRAM可以以各种格式配置,并且通常支持内置功能,例如用于排队数据的FIFO控制器,以及错误控制机制(ECC),以提高信息存储的可靠性。
1. LUT(查找表—-设置表
-可编程真值表
-实现组合逻辑的主要元素
功能:
-任何布尔函数
-小型分布式内存
-可用作移位寄存器(SRL)
2. FF(触发器)-触发器
-D-用于存储1位的触发器
-通过时钟信号同步
种类:
-FDCE:异步复位(清除)
-FDPE:具有异步安装(预设)
-FDRE:具有同步复位
计划:
3. DSP(数字信号处理器—-DSP单元
-数学运算的专用块
-优化乘法-积累
典型DSP块的结构:
该图显示了DSP单元的典型架构。 它不仅执行快速乘法(A×B),还具有用于在乘法之前计算(A+D)的预加法器,以及用于乘法-累加运算(MAC)的累加器,这对于数字滤波器和卷积至关重要。
基本操作:
*P=A×B+C-MAC操作
*P=(A+D)×B+C-带预求和器
*模式检测-与样本比较
4. Bram(块RAM)-块内存
-内置RAM
-双端口同步存储器
特征:
-真正的双端口-两个端口都是独立的
—同步操作-输入/输出寄存器
-内置FIFO控制器-在某些型号
-ECC支持-错误控制
主要调查结果:
- LUT-数字逻辑的基础,取代了数千个单独的逻辑门
- FF-提供同步性和状态存储
- DSP-加速数学计算10-100倍
- BRAM-提供快速本地存储器,无需外部芯片
正是这四个元素的结合,使Fpga能够高效地实现复杂的数字系统,从简单的控制器到信号处理过程。
Verilog如何使用XOR的例子工作
1. Verilog上的源代码
module half_adder(
input a, b,
output y
);
assign y = a ^ b; // XOR операция
endmodule
-最简单的半蓄能器
-使用XOR运算符(^)
-计算总和,而不考虑转移
2. 综合与优化
-Verilog编译器分析代码
-创建架构的中间表示
-优化逻辑以尽量减少延迟
FPGA中通过LUT实现XOR的方案
**本质:**所有XOR逻辑被打包成4个LUT配置位,这些配置位被加载到FPGA中并作为查找表工作-输入成为地址,输出成为来自相应存储器位置的值。
文章回顾
本文是一个实用指南,将指导您完成从算法模型在Verilog上创建经过验证的硬件模块的整个周期。 我们不仅会向您展示如何,还会详细解释原因:
*为什么我们需要一个固定点而不是浮点?-答案在于使用FPGA资源的效率。
*矢量代码如何变成流水线方案?-这是高生产率的基础。
*如何自动生成可读和优化的Verilog代码?-使用模板和系统设置。
*如何确定创建的方案是否正常工作?-使用Verilator和Icarus Verilog的两步验证。
作为一个简单但说明性的例子,我们将采用梯形参数计算器。 这个训练示例非常适合演示面向模型设计的整个周期:我们将看到这个想法如何变成算法,算法变成定点流水线模型,模型变成硬件实现,准备好通过严 通过他的例子,我们将清楚地展示构成真实DSP算法基础的典型运算(加法,乘法),揭示该方法的关键细微差别。
辅助功能
让我们从描述辅助库的功能开始:
-
run_model(name)-加载并执行Engee模型。 -
read_v(filename)-读取并输出指定文件的内容,方便调试。 -
extract_verilog_parameters(output)-分析Verilog仿真的输出,提取数值参数值。
*库末尾的代码检查文件 tb.v 并生成它,如果文件丢失。
include("helper_lib.jl")
在Engee中使用定点数
在硬件设计和数字信号处理中,由于几个关键原因,使用定点数字而不是浮点数:
-这种方法在硬件中实现时需要更少的逻辑元素和内存。
-具有这种类型的数据的操作在固定数量的时钟周期中执行
-没有规范化的简单操作比具有浮号的类似操作执行得快得多。
在Engee中,函数用于与固定点一起工作 fi(),其具有以下语法:
x = fi(Value, Sign, Total_bits, Fractional_bits)
功能参数:
/参数/描述/示例|
|:---|:---|:---|
|Value/原始十进制数|128.9|
|Sign/标志类型: 1 -标志性的, 0 -未签名|1|
|Total_bits/总位数(位)|16|
|Fractional_bits/小数位数|7|
一个实际的例子:
Value = 128.9
Sign = 1;
Total_bits = 16;
Fractional_bits = 7;
x = fi(Value, Sign, Total_bits, Fractional_bits)
println("fi: $x")
如下图所示,创建定点数的过程涉及三个主要步骤:
-
计算范围-确定所选格式可以表示的最小值和最大值
-
量化过程是考虑到量化步长的初始数的变换
-
位表示-最终二进制字的形成
当在Engee中使用定点数执行操作时,结果的位深度会自动更改,这对于优化资源使用非常重要。
display(x + fi(2.8,0,8,6))
display(3x)
正如我们所看到的,当添加不同格式的数字时,系统会自动将位深度增加1位以防止溢出。 当乘以类型的整数时,会发生更显着的扩展 Int64 —在这种情况下,64位被添加到整个部分,这导致结果的位深度显着增加(从16位到80位)。
位深度的自动扩展,虽然防止了溢出错误,但常常导致资源的过度使用。 要创建内存优化的硬件解决方案,建议使用带位控制的显式类型转换,并改用定点数。 Int64 为常数并在必要时手动调整结果的位深度。
一种梯形参数计算算法的系统模型
为了最初测试算法,我们实现了一个Julia系统模型,用于计算梯形的基本几何参数:周长(P),中线(m)和面积(S)对于不同的高度值。
a, b, c, d = 5, 7, 3, 4
h = [4,5]
P = a + b + c + d
m = (a + b) / 2
S = m .* h
println("周长P=△P")
println("中间线是m=△m")
println("面积S=△S")
此实现是矢量化模型,其中对高度值数组执行面积计算操作。 h = [4, 5] 使用运算符 .*. 该模型适用于浮点数据类型(Float64),保证了高精度和调试的方便性。
虽然这种方法可以让你快速验证算法的正确性并获得参考结果,但它不支持硬件代码生成。 矢量化操作和浮点类型不能直接转换为高效的硬件实现,这需要创建单独的定点模型和流水线以用于后续Verilog代码生成。
生成Verilog代码的模型
要转向硬件实现,您需要创建一个支持代码生成的模型。 此过程的第一步是使用固定点定义输入参数。
与浮点系统模型不同,我们使用单个数据类型设置输入参数以生成Verilog代码。 fixdt(1, 16, 8) -带符号的16位格式,具有8个小数位,相同的数据类型仅用于简化,如有必要,您可以选择最佳类型。
因为在我们的例子中,高度是 h 由向量类型表示 [4, 5] 对于硬件实现,有必要组织这些值的顺序处理。 在Engee模型中,这是使用流水线方法实现的,其中每个值在单独的时钟周期中处理。
对于顺序数据供应,使用来自块的信号,它将值的向量拆分为以固定时间间隔进入模型的单独样本。 在这种情况下,两个高度值将在两个连续的计算周期中被处理。 我们的块的设置如下所示。
现在让我们来看看模型的整个顶层,它保留了与原始脚本相同的计算逻辑,但数据类型存在关键差异。 输入参数 a, b, c, d 匹配初始测试中的值(5, 7, 3, 4),但是,它们现在以定点格式表示。 fixdt(1, 16, 8).
还启用了以下设置来调试模型。
计算模型支持与系统版本相同的功能,但采用适合硬件实现的格式。
模型的主要特点:
-所有计算都是用固定点进行的
-保留原始数学公式
-增加了矢量操作的流水线
-输出信号可用于Verilog代码生成
为了验证创建的模型的正确性,运行它,然后将结果与原始示例进行比较。
run_model("model")
println("")
P_sim = (collect(simout["model/P"])).value[end]
m_sim = (collect(simout["model/m"])).value[end]
S_sim = (collect(simout["model/S"])).value[end]
S_sim_2 = (collect(simout["model/S"])).value[end-1]
println("周长P=$P_sim")
println("中线m=$m_sim")
println("面积S=$([S_sim,S_sim_2])")
获得的值与系统模型的结果完全匹配。 结果的成功重合证实了定点模型正确实现了计算梯形参数的算法,并准备好生成硬件代码。
代码生成和自定义模板
自定义模板
在开发硬件模型时,可能会出现使用的块不支持自动代码生成的情况。 这可能由于各种原因而发生:块是自定义的,具有特定功能,或者根本没有在目标代码生成系统中实现。
对于这种情况,Engee提供了一个机制自定义生成模板与扩展 .cgt. 这些模板允许您定义如何将特定块类型转换为目标硬件描述语言。
方法的主要特点:
- 宏替换 —
$(...)заменяются на реальные значения из модели - Условная генерация — логика зависит от свойств сигналов (разрядность, тип)
- Отдельные функции для повторяющихся операций
- Явное управление типами данных
Этот механизм обеспечивает гибкость при работе со сложными или специализированными блоками, позволяя создавать эффективные аппаратные реализации даже для компонентов, не имеющих встроенной поддержки генерации кода, теперь рассмотрим конкретный пример шаблона для блока умножения (Product), шапка для любого шаблона идентичная.
read_v("$(@__DIR__)/product.cgt")
Заголовок
//! BlockType = :Product
//! TargetLang = :Chisel
BlockType = :Product— указывает, что шаблон применяется к блокам типа "Product" (умножение)TargetLang = :Chisel— генерирует код на языке Chisel (Scala-based HDL), он является промежуточным звеном при генерации Verilog из моделей Engee.
Секция определений (@Definitions)
//! @Definitions
val $(output(1)) = Wire($(show_chisel_type(output(1))))
- Создаёт выходной провод (
Wire) с типом, соответствующим выходному сигналу $(output(1))-宏替换第一个输出端口的名称$(show_chisel_type(...))— функция, возвращающая тип данных в синтаксисе Chisel
Секция шага генерации (@Step)
/*! @Step
function cast(sig)
".asTypeOf(FixedPoint($(sig.ty.bits).W,$(sig.ty.fractional_bits).BP))"
end
- Определяет вспомогательную функцию
cast() - Добавляет приведение типа для сигналов фиксированной точки в Chisel
$(sig.ty.bits)\text{和}$(sig.ty.fractional_bits)— извлекают разрядность из метаданных сигнала
Условное приведение типов результата
function maybe_cast_res_begin()
output(1).ty.fractional_bits == 0 ? "(" : ""
end
function maybe_cast_res_end()
output(1).ty.fractional_bits == 0 ? ").asSInt" : ""
end
- Условные функции для обработки целочисленных результатов
- Если дробная часть равна 0, результат преобразуется в знаковое целое (
.asSInt) - Оборачивает выражение в скобки при необходимости приведения типа
Основное выражение генерации
$(output(1)) := \
$(maybe_cast_res_begin())\
$(input(1))$(cast(input(1)))*$(input(2))$(cast(input(2)))\
$(maybe_cast_res_end())
-生成乘法运算: выход := вход1 * вход2
-如果需要,为两个输入添加类型转换
-处理结果类型的条件转换
代码生成
要从模型生成代码,您需要选择生成器的目标语言,在我们的例子中它是Verilog。
.png)
下一阶段是用Verilog语言生成硬件代码。 为此,请使用函数 engee.generate_code(),具有以下参数:
/参数/目的/示例|
|:---|:---|:---|
/第一个/模型文件的路径|"$(@__DIR__)/model.engee"|
|Второй|Выходная директория для кода|"$(@__DIR__)/V_Code"|
/正在生成的子系统的第三个/名称|subsystem_name="trapeze_calculator"|
您还可以通过右键单击子系统块来生成代码。
.png)
engee.generate_code(
"$(@__DIR__)/model.engee",
"$(@__DIR__)/V_Code",
subsystem_name="trapeze_calculator"
)
read_v("$(@__DIR__)/V_Code/model_trapeze_calculator.v")
生成的模块 model_trapeze_calculator 它是计算梯形参数的算法的紧凑硬件实现。 该代码具有纯组合逻辑,在一个时钟周期内计算所有三个参数。
该模块接受五个16位输入(底座,高度和侧面),并生成三个16位输出(中心线,周长,面积)。 所有的计算都是优化的:基数的总和在重复使用的情况下计算一次,通过移位执行除以2,并且通过将位深度扩展到24位来实现区域乘法,以防止溢出。
代码生成器有效地将算法模型转化为硬件描述,优化了资源的使用,保持了定点计算的准确性,并确保了最小的信号延迟。
原子子系统
原子子系统是Engee模型中的功能模块,在生成代码时,被视为独立的,自给自足的模块,具有清晰的I/O接口。
与以前的单片版本不同,所有计算都集中在一个模块中,原子方法将功能划分为逻辑上独立的组件。 下图显示了如何创建这样的子系统。
该算法分为两个专门的模块:
Area-计算基数,中线和面积的总和model_atomic_trapeze_calculator-上层,增加了周长计算
engee.generate_code(
"$(@__DIR__)/model_atomic.engee",
"$(@__DIR__)/V_Code_atomic",
subsystem_name="trapeze_calculator"
)
read_v("$(@__DIR__)/V_Code_atomic/model_atomic_trapeze_calculator.v")
read_v("$(@__DIR__)/V_Code_atomic/Area.v")
尽管结构上存在差异,但两个版本生成的硬件结果完全相同。 划分为模块是一个纯粹的组织改进,而不会影响性能。
.png)
在开发复杂系统时,模块化方法变得至关重要,因为不同的单元可以并行处理单个组件。 它还允许对每个单元进行隔离测试,从而简化了验证,并且由于组件之间的责任界限清晰,便于调试。
测试生成的代码:验证工具
生成硬件代码后,必须对其进行彻底检查,以确保操作正确。 Engee中有两种主要的验证工具。 这两种工具相辅相成:Verilator非常适合在早期阶段快速验证功能,而Icarus Verilog对于详细的时间验证和合成前的最终测试准备是必不可少的。 使用这两种方法可以保证生成代码的最大可靠性。
验证器,验证器
Verilator是一款高性能模拟器,可将Verilog代码转换为优化的C++/c模型。 其主要特点:
-C代码生成:Verilog被翻译成等效的C程序
-与模型集成:测试直接在Engee环境中进行
-自动化:不需要手动编写测试环境
-高速:通过编译的C代码进行模拟比解释的解决方案快得多
Verilator的优点是能够快速验证生成的代码,而不需要创建额外的测试环境。
要自动生成C函数,我们需要在代码生成参数中启用此设置。
接下来,在生成代码时,会自动创建一个脚本来生成一个带有C-function块的模型,在我们的例子中,该文件被调用 model_trapeze_calculator_verification.jl 让我们运行它并查看结果。
include("$(@__DIR__)/V_Code/model_trapeze_calculator_verification.jl")
正如我们所看到的,模型是自动创建的,然后我们从原始模型中复制输入参数并比较结果。
run_model("model_verification")
println("")
P_sim_C = (collect(simout["model_verification/test.P"])).value[end]
m_sim_C = (collect(simout["model_verification/test.m"])).value[end]
S_sim_C = (collect(simout["model_verification/test.S"])).value[end]
S_sim_2_C = (collect(simout["model_verification/test.S"])).value[end-2]
println("周长P=$P_sim_C")
println("中线m=$m_sim_C")
println("面积S=$([S_sim_C,S_sim_2_C])")
正如我们所看到的,结果与原始模型相同,从中我们可以得出结论,生成的代码与原始模型完全相同。
伊卡洛斯Verilog
Icarus Verilog是一个解释性模拟器,直接执行Verilog代码。 其独特的特点:
-直接解释:代码执行时无需中间转换
-需要一个测试平台:有必要在Verilog上创建一个完整的测试环境
-详细调试:允许深度分析时间图表
-标准兼容性:严格遵守Verilog标准
这种方法提供了与实际硬件行为更精确的匹配,但需要额外的努力来创建测试环境。
read_v("$(@__DIR__)/tb.v")
此测试台对生成的模块进行全面检查。 model_trapeze_calculator 使用Icarus Verilog模拟器。\
测试台使用活动复位和定点格式输入数据(小数部分为8位移位)初始化系统。 在移除复位之后,在15个时间单位之后,对两个梯形高度值执行顺序测试:首先对于h=4,然后对于h=5。
移位用于处理定点数。 <<8,并将结果转换回十进制格式是通过 $itor($signed(...))/256.0. 时钟信号以10个时间单位的周期产生,时间延迟控制测试动作的顺序。
测试台允许您验证不同输入数据的计算正确性,检查系统对动态参数变化的响应,并验证定点格式转换是否正确。 所有结果都通过系统功能以方便的格式化形式显示 $display.
测试过程包括编译和执行Verilog代码,然后处理输出数据。 功能 extract_verilog_parameters 分析仿真输出并提取两个高度的计算梯形参数值。
run(`iverilog -o sim tb.v model_trapeze_calculator.v`)
output = read(`vvp sim`, String)
m_verilog, P_verilog, S4_verilog, S5_verilog = extract_verilog_parameters(output);
println(output)
获得的值与系统模型的参考结果完全匹配,这证实了生成的Verilog代码的正确性。
结论
此示例演示了使用Engee平台以Verilog语言生成经过验证的硬件代码的实用方法。 显示了从算法模型到生成代码的运行状况检查的完整开发周期。 下面是一个比较这项工作的所有结果的脚本。
using DataFrames
df = DataFrame(
Тест = ["茱莉亚剧本", "工程师模型", "Verilator的C代码", "Verilog测试台"],
P = [P, P_sim, P_sim_C, P_verilog],
m = [m, m_sim, m_sim_C, m_verilog],
S_h4 = [S[1], S_sim, S_sim_C, S4_verilog],
S_h5 = [S[2], S_sim_2, S_sim_2_C, S5_verilog]
)
根据该表,我们可以得出以下结论:
-
从Julia脚本中的浮点数到硬件实现中的固定点的转换是在不损失该算法的精度的情况下执行的。
-
所有四种方法都产生了相同的结果,这证实了代码生成器和验证工具的正确性。
-
通过中间C表示(Verilator)和本机Verilog模拟(Icarus)成功完成测试,消除了意外的巧合。
所考虑的面向模型设计的方法可以应用于数字信号处理系统或控制算法的开发,其中需要高性能硬件实现与算法建模的便利性相结合。
使用Engee和类似工具可显着降低入门门槛并加快Fpga的开发速度。 算法师和系统工程师有机会直接参与硬件加速器的创建,专注于算法的逻辑和优化,而不是算法到HDL的常规和容易出错的转换。 所示方法是高级建模和高效硬件之间的桥梁,通过自动生成和严格验证提供可靠性。
注
在这个例子中,故意省略了合成和实现等步骤,因为它们直接依赖于硬件平台。
合成**(合成)**是将HDL(Verilog/VHDL)中的数字电路描述转换为技术网表的过程—特定逻辑元素(LUT,触发器,内存块)的列表以及它们之间的连接。 合成器分析代码,优化逻辑表达式并将其映射到目标FPGA架构的基元。 在这个阶段,确定将使用晶体的哪些资源以及它们在逻辑上如何连接。
实现**(Implementation)**-下一阶段是将合成的网表物理放置在特定FPGA晶振上时。 该过程包括放置,这是将逻辑元素分配到芯片的特定物理单元,以及路由,这是通过可编程开关建立真正的连接。 实现直接取决于特定的FPGA模型,并生成用于编程器件的最终比特流。
Fpga和开发环境制造商
/制造商/主要开发环境/关键FPGA系列|
|:---|:---|:---|
/AMD/Xilinx/Vivado设计套件(适用于7系列及以上)、ISE(适用于旧系列)/Spartan、Artix、Kintex、Virtex、Zynq、Versal|
/Intel/Quartus Prime Design Suite/MAX,Cyclone,Arria,Stratix,Agilex|
/晶格半导体/晶格金刚石,晶格辐射/iCE40,MachXO,ECP5,交联|
/Microchip/Libero SoC Design Suite/IGLOO,ProASIC3,PolarFire,RTG4|
/开源/Yosys+nextpnr(独立工具)/支持莱迪思iCE40/ECP5,Xilinx7系列|
Vivado和Quartus Prime是最常见的专业环境,提供完整的开发周期,从代码输入到比特流生成。 莱迪思为其Fpga提供免费工具,而Yosys/nextpnr开放堆栈允许您在没有专有软件的情况下使用某些架构。 环境的选择由具体的FPGA决定。