引言
随着人工智能、高性能计算等领域对定制化硬件需求的日益增长,传统的手工编写 RTL 代码的设计方法面临着巨大的挑战。高层级综合(High-Level Synthesis, HLS)作为一种能够将高级语言描述转化为硬件电路的技术,受到了越来越多的关注。本文将深入探讨一种新颖的 HLS 视角,即如何利用 Python 这种解释性语言与 CIRCT 工具链的强大组合来实现 HLS 流程。我们将展示如何将 Python 代码转化为可在 FPGA 或 ASIC 上运行的硬件电路描述,从而加速硬件设计的迭代和验证过程。此外,本文还将介绍 PyCDE(Python CIRCT Design Entry),这是一个用于 FPGA 设计和验证的 Python API,以及 ESI (Elastic Silicon Interconnect) Dialect,它能够辅助加速器系统的构建。通过结合二者,读者可以轻松地使用 Python 与 CIRCT 进行交互,构建各种硬件设计,从而开启 HLS 的新篇章。
HLS 概述
HLS 是一种将算法级描述(如 C/C++、Python 等高级语言代码)自动转换为硬件描述语言(如 Verilog、VHDL)的技术。与传统的寄存器传输级(RTL)设计方法不同,HLS 允许开发者专注于功能逻辑而非底层硬件时序,通过编译器自动完成状态机生成、资源分配和时序优化等复杂任务。
HLS VS 传统 RTL 设计
传统硬件设计通常使用 SystemVerilog 或 VHDL 编写 RTL 代码,需要手动实现时钟周期级的状态机、数据路径和时序约束。这种方法的局限性在于:
- 开发周期长:需逐行编写硬件控制逻辑,代码量庞大;
- 验证成本高:功能修改需重新调整时序和接口;
- 算法移植困难:算法工程师与硬件工程师存在语言鸿沟。
HLS 通过提高抽象层级解决了这些问题:
- 提高开发效率:开发者可使用高级语言描述硬件行为,无需关注底层的时序细节。这种抽象提高了代码密度,减少了代码量,并降低了出错的可能性;
- 快速迭代:功能验证可在高级语言层面完成,减少硬件仿真时间,从而加快了设计迭代的速度从而加快了设计迭代的速度;
- 跨领域协作:算法工程师可以使用熟悉的编程语言表达算法,HLS 工具可以将算法代码直接转换为硬件实现,消除了算法工程师与硬件工程师之间的语言障碍。
虽然 HLS 能显著提升硬件开发效率,但在实际应用中,相较于传统的 RTL 设计仍存在一些不足之处:
- 复杂性应对不足:在处理复杂控制逻辑(如嵌套条件分支)或非规则算法(如递归、动态内存操作)时,可能生成效率较低甚至错误的代码;
- 优化空间受限:HLS 工具通常通过编译指令指导或 API 驱动优化,但选项有限,难以覆盖所有硬件场景。例如,手动调整数据通路或存储器架构可能比工具自动优化更高效;
- 功耗优化薄弱:对于低功耗设计和特定架构(如异构计算单元)的支持相对有限,难以实现精细的时序控制和定制化的功耗优化。
HLS 与 RTL 特性对比如下列表 1 所示:
特性 | SystemVerilog (RTL) | HLS (C/SystemC) | HLS (Python) |
---|---|---|---|
代码密度 | 低(需描述时序细节) | 中等 | 高 |
开发速度 | 慢 | 快 | 极快 |
硬件控制粒度 | 精细(手动优化) | 中等(指令引导) | 粗粒度(API 驱动) |
学习曲线 | 陡峭 | 中等 | 平缓 |
错误检测 | 弱 | 强 | 强 |
灵活性 | 更为灵活 | 较弱 | 较弱 |
表 1:特性对比 HLS vs RTL
HLS 输入语言演进:C → SystemC → Python
早期 HLS 工具主要支持 C/C++ 和 SystemC,但不同语言存在显著差异:
- C 语言:通用性强,但缺乏硬件原语(如并行性、接口协议),需通过 Pragma 指令标注硬件特性;
- SystemC:扩展了 C++的硬件建模能力,支持模块化设计和事务级建模(TLM),但语法复杂,学习成本高;
- Python:凭借简洁语法和丰富生态(如 NumPy、AI 框架),成为新兴 HLS 输入语言。例如,PyCDE 通过 Python 绑定 CIRCT 工具链,可直接将 Python 数据结构映射为硬件模块,同时利用 Python 的交互性实现实时硬件调试。
Python+HLS 的独特优势
在 AI 加速器、信号处理等场景中,Python 的 HLS 价值尤为突出:
- 算法即硬件:AI 模型(如 PyTorch)可直接转换为 FPGA/ASIC 加速器;
- 动态优化:利用 Python 解释器实时调整硬件参数(如并行度、流水线深度);
- 生态融合:结合 Jupyter Notebook 实现可视化硬件设计流程;
- 跨层级验证:同一套 Python 代码既可用于软件仿真,也可生成可综合的 RTL。
PyCDE 概述
PyCDE 是一个 Python API,旨在简化硬件相关活动。它的主要目的是使 CIRCT 的功能更容易暴露给 Python 开发人员。PyCDE 通过少量语法糖将代码映射到 CIRCT 操作,其核心工作实际上完全由 CIRCT 底层完成。功能特点如下:
- 硬件模块:硬件模块继承自 pycde.Module,通过设置类成员来定义任意数量的类型化输入和输出;
- 生成器:@generator 装饰器用于表示模块构造代码,在每个模块创建时调用一次;
- 类型和信号:“由于 CIRCT 主要面向硬件而非软件,因此它定义了自己的类型系统来描述硬件信号。PyCDE 通过 pycde.types.Type 类层次结构将这些硬件类型暴露给 Python 开发者。PyCDE 中的信号代表了目标硬件上的值,每个信号都具有特定的 Type (即硬件类型,与 Python 对象自身的 type 不同),该 Type 存储在信号对象的 type 成员中;
- 参数化模块:为了“参数化”一个模块,只需从一个函数返回一个模块。该函数必须使用 modparams 进行装饰,以告知 PyCDE 返回的模块是一个参数化模块。modparams 装饰器执行多项操作,包括存储参数化函数以及自动派生包含参数值的模块名称(用于模块名称唯一性)。
Python Bindings 介绍
Python Bindings 是一种允许 Python 代码调用其他语言编写的库或函数技术。因为 Python 是解释型语言,计算密集型任务(如数值计算、图像处理)时效率较低,但可通过调用 C/C++ 等编译型语言的代码,以大幅提升性能;或将已有的 C/C++ 库(如 OpenCV、TensorFlow)暴露给 Python,进而实现代码复用;亦可直接调用操作系统或硬件相关的底层接口(如 GPU 加速库 CUDA)。
CIRCT 中的 Python Bindings 工具:pybind11
pybind11 是一个用于在 Python 和 C++ 之间进行互操作的轻量级、仅头文件的库。它允许 C++ 代码被 Python 调用,反之亦然。pybind11 简化了创建 Python 绑定的过程,减少了所需的样板代码。其主要优势如下:
- 简洁的接口和代码:pybind11 利用 C++11 及更高版本的特性,如自动类型转换、lambda 表达式和可变参数模板,提供了简洁的接口和代码;
- 高性能:pybind11 使用 C++ 的编译器来生成 Python 的 C 扩展,因此性能非常高;
- STL 集成:pybind11 可以无缝处理 STL 数据结构,如 vectors 和 dictionaries,自动在 C++ 和 Python 等价物之间进行转换;
ESI 项目的发展历程
在 FPGA/ASIC 设计领域,芯片内部模块间的互连和通讯通常是采用临时 (ad-hoc) 的方式,线缆信号协议也常常没有标准,这会导致许多问题,例如信号监听和手动调整时容易产生混淆和错误。虽然目前有一些标准化信号协议的尝试,但各种变体依然层出不穷。此外,由于 FPGA/ASIC 设计内部中的各个逻辑块之间,互连所传输的数据类型常没有正式、统一的定义方式,而是通过数据表等非正式的文档来约定,间接表明 RTL 支持的基本类型系统存在不足。
而为了解决上述问题,微软于 2019 年 7 月发起了 ESI 项目,该项目旨在提高数据类型和接口的标准。
- 在数据类型方面:ESI 定义了一个丰富的、以硬件为中心的类型系统,以允许更正式的数据类型定义和强大的静态类型安全;
- 在 ABI 方面:ESI 提供简单的、延迟不敏感的标准化接口,以便开发者使用,并负责处理信号细节和转换。
其根本目的是解耦物理信号层与消息层,以解决 FPGA/ASIC 设计中接口和数据类型方面的挑战,提高设计的可靠性和效率。
于 2020 年 10 月,该项目核心开发者 John Demme 在 LLVM 论坛上对 ESI 建模进行了讨论,其中提到 ESI 项目被定义为一个高级硅 (FPGA/ASIC) 互连生成器,旨在提高 inter/intra/CPU 通信的抽象级别。并且 ESI 使用类型化的、延迟不敏感的片上连接,连接各个启用了 ESI 的模块;它还桥接片外,并使用类型数据创建高级 API。在默认情况下,延迟不敏感(弹性)连接允许编译器做出优化决策。此外,ESI 还支持许多自动化任务,如下:
- 跨语言通信;
- 类型检查以减少接口边界处的错误;
- 通信结构(包括时钟域交叉)的正确构建;
- 自动决策模块之间的物理信号;
- 自动生成软件 API,桥接 PCIe、网络或仿真;
- 自动进行 endian 转换;
- 基于模块之间的布局规划自动流水线,以减少时序收敛压力;
- 兼容不同带宽的模块(自动变速);
- 在通信结构中提供类型和信号感知的调试器/监视器;
- 通用板级支持包接口;
- 可扩展服务以支持全局资源(例如,遥测);
讨论中还提到该项目正在逐步与 CIRCT 生态进行融合,CIRCT 官方最新 Python 转换流程图如下:
于 2024 年,项目核心开发者在 Cornell 大学发表了有关 ESI 项目的论文,名为The ESI System Construction Compiler in 2024,文中主要表述了在部署 FPGA 加速器时,仍然存在的一些问题,如片上信号问题、复位信号与时钟信号问题以及主机连接问题。
Python 细节举例
如上图图 1 所示,Python 转换路线中涉及许多方言,下面将会以附带例子的形式进行一一介绍。值得注意的是,如果想要输出 IR,需要实例化 System 类,然后调用该类下的 print()方法。以下是针对 Python 源码通过导入 PyCDE API 转换到不同方言的举例。
- ESI 方言:ESI 方言是在将 ESI 项目融入 CIRCT 生态进而产生的,其核心目的与 ESI 项目保持一致(同上 ESI 发展历程中所提)。
Python 源码:
from pycde import Input, Output, Module, generator
from pycde import esi
from pycde.types import Bits, Bundle, BundledChannel, Channel, ChannelDirection
import pycde
class CoerceBundleTransform(Module):
'''
定义了一个名为 b_in 的输入端口。这个端口的类型是 Bundle,表示它是由多个信号组合在一起的。
BundledChannel("req", ChannelDirection.TO, Channel(Bits(32))):
定义了名为 req 的 Bundle 通道;方向 TO 表明数据从“发送者”到“接收者”;且通道宽度为32bits
BundledChannel("resp", ChannelDirection.FROM, Channel(Bits(8))):
定义了名为 resp 的 Bundle 通道;方向 FROM 表明数据从“接收者”到“发送者”;且通道宽度为8bits
'''
b_in = Input(
Bundle([
BundledChannel("req", ChannelDirection.TO, Channel(Bits(32))),
BundledChannel("resp", ChannelDirection.FROM, Channel(Bits(8))),
]))
''' 下列的输出端口与上面的输入端口类似 '''
b_out = Output(
Bundle([
BundledChannel("arg", ChannelDirection.TO, Channel(Bits(24))),
BundledChannel("result", ChannelDirection.FROM, Channel(Bits(16))),
]))
@generator
def build(ports):
'''
调用coerce方法,将双向通道转换为另一种双向通道
lambda x : x[0:24] 表明取 reg 通道的 0-23bits,并赋给 arg
lambda x : x[0:8] 表明取 resp 通道的 0-7bits,并赋给 result 的 0-7位
'''
ports.b_out = ports.b_in.coerce(CoerceBundleTransform.b_out.type,
lambda x: x[0:24], lambda x: x[0:8])
system = pycde.System([CoerceBundleTransform], name="CBTDemo")
system.generate()
system.print()
ESI IR:使用 python3 xxx.py
即可
# RUN: python3 %s
module {
esi.manifest.sym @CoerceBundleTransform name "CoerceBundleTransform"
hw.module @CoerceBundleTransform(in %b_in : !esi.bundle<[!esi.channel<i32> to "req", !esi.channel<i8> from "resp"]>,
out b_out : !esi.bundle<[!esi.channel<i24> to "arg", !esi.channel<i16> from "result"]>)
attributes {output_file = #hw.output_file<"CoerceBundleTransform.sv", includeReplicatedOps>} {
'''从 ESI 通道中解包数据和 valid 信号 '''
%rawOutput, %valid = esi.unwrap.vr %result, %ready : i16
'''从0位开始提取 8bits 数据'''
%0 = comb.extract %rawOutput from 0 : (i16) -> i8
'''将数据和 valid 信号封装到 ESI 通道中,用于发送数据'''
%chanOutput, %ready = esi.wrap.vr %0, %valid : i8
'''将多个 ESI 通道打包到一起'''
%req = esi.bundle.unpack %chanOutput from %b_in : !esi.bundle<[!esi.channel<i32> to "req", !esi.channel<i8> from "resp"]>
%rawOutput_0, %valid_1 = esi.unwrap.vr %req, %ready_3 : i32
%1 = comb.extract %rawOutput_0 from 0 : (i32) -> i24
%chanOutput_2, %ready_3 = esi.wrap.vr %1, %valid_1 : i24
%bundle, %result = esi.bundle.pack %chanOutput_2 : !esi.bundle<[!esi.channel<i24> to "arg", !esi.channel<i16> from "result"]>
hw.output %bundle : !esi.bundle<[!esi.channel<i24> to "arg", !esi.channel<i16> from "result"]>
}
}
Core IR:使用circt-opt --esi-clean-metadata --lower-esi-bundles --lower-esi-ports --lower-esi-to-hw
--esi-clean-metadata
用于删掉esi.manifest.sym @xxx
,该 IR 在 core 层是非法的;--lower-esi-bundles
用于处理esi.bundle.(un)pack
;--lower-esi-ports
用于将 port 列表中的 esi 方言中的类型转换为能被 core 层方言识别的类型;--lower-esi-to-hw
用于将剩余的 esi 方言转换为 core 层方言。
# RUN: circt-opt --esi-clean-metadata --lower-esi-bundles --lower-esi-ports --lower-esi-to-hw %s
module {
hw.module @CoerceBundleTransform(in %b_in_req : i32, in %b_in_req_valid : i1, in %b_out_result : i16, in %b_out_result_valid : i1, in %b_in_resp_ready : i1, in %b_out_arg_ready : i1, out b_in_req_ready : i1, out b_out_result_ready : i1, out b_in_resp : i8, out b_in_resp_valid : i1, out b_out_arg : i24, out b_out_arg_valid : i1) attributes {output_file = #hw.output_file<"CoerceBundleTransform.sv", includeReplicatedOps>} {
%0 = comb.extract %b_out_result from 0 : (i16) -> i8
%1 = comb.extract %b_in_req from 0 : (i32) -> i24
hw.output %b_out_arg_ready, %b_in_resp_ready, %0, %b_out_result_valid, %1, %b_in_req_valid : i1, i1, i8, i1, i24, i1
}
}
- Hwarith 方言:专用于表示处理不同 bit 类型的算术运算,其中仅包含四则运算、比较运算以及类型转换。
参考 SystemVerilog 在 CIRCT 上的初步探索文中的 Comb 方言,其中也包含这些算数运算。二者的区别在于,Hwarith 方言不要求运算中的 operands 的类型保持一致,但是 Comb 方言要求 operands 的类型必须相同。
Python 源码:
from pycde import Input, Output, generator, Module, System
from pycde.types import types, UInt
class Arith(Module):
in0 = Input(types.si15)
in1 = Input(types.ui16)
@generator
def construct(ports):
add = ports.in0 + ports.in1
sub = ports.in0 - ports.in1
mul = ports.in0 * ports.in1
div = ports.in0 / ports.in1
system = System([Arith], name="Arith")
system.generate()
system.print()
Hwarith IR:使用 python3 xxx.py 即可
# RUN: python3 %s
module {
esi.manifest.sym @Arith name "Arith"
hw.module @Arith(in %in0 : si15, in %in1 : ui16) attributes {output_file = #hw.output_file<"Arith.sv", includeReplicatedOps>} {
'''这里的结果为有符号 18bits,是因为考虑了进位以及符号位'''
%0 = hwarith.add %in0, %in1 {sv.namehint = "in0_plus_in1"} : (si15, ui16) -> si18
%1 = hwarith.sub %in0, %in1 {sv.namehint = "in0_minus_in1"} : (si15, ui16) -> si18
%2 = hwarith.mul %in0, %in1 {sv.namehint = "in0_mul_in1"} : (si15, ui16) -> si31
%3 = hwarith.div %in0, %in1 {sv.namehint = "in0_div_in1"} : (si15, ui16) -> si15
hw.output
}
}
Core IR:使用 circt-opt --lower-hwarith-to-hw xxx.mlir
用于将 Hwarith 方言转换为 Core 层方言
#RUN: circt-opt --lower-hwarith-to-hw %s
module {
esi.manifest.sym @Arith name "Arith"
hw.module @Arith(in %in0 : i15, in %in1 : i16) attributes {output_file = #hw.output_file<"Arith.sv", includeReplicatedOps>} {
'''
add = ports.in0 + ports.in1
下列的comb IR是为了保证操作数有相同的类型宽度
'''
%0 = comb.extract %in0 from 14 : (i15) -> i1
%1 = comb.replicate %0 : (i1) -> i3
%2 = comb.concat %1, %in0 : i3, i15
%c0_i2 = hw.constant 0 : i2
%3 = comb.concat %c0_i2, %in1 : i2, i16
%4 = comb.add %2, %3 {sv.namehint = "in0_plus_in1"} : i18
'''sub = ports.in0 - ports.in1'''
%5 = comb.extract %in0 from 14 : (i15) -> i1
%6 = comb.replicate %5 : (i1) -> i3
%7 = comb.concat %6, %in0 : i3, i15
%c0_i2_0 = hw.constant 0 : i2
%8 = comb.concat %c0_i2_0, %in1 : i2, i16
%9 = comb.sub %7, %8 {sv.namehint = "in0_minus_in1"} : i18
'''mul = ports.in0 * ports.in1'''
%10 = comb.extract %in0 from 14 : (i15) -> i1
%11 = comb.replicate %10 : (i1) -> i16
%12 = comb.concat %11, %in0 : i16, i15
%c0_i15 = hw.constant 0 : i15
%13 = comb.concat %c0_i15, %in1 : i15, i16
%14 = comb.mul %12, %13 {sv.namehint = "in0_mul_in1"} : i31
'''div = ports.in0 / ports.in1'''
%15 = comb.extract %in0 from 14 : (i15) -> i1
%16 = comb.replicate %15 : (i1) -> i2
%17 = comb.concat %16, %in0 : i2, i15
%false = hw.constant false
%18 = comb.concat %false, %in1 : i1, i16
%19 = comb.divs %17, %18 {sv.namehint = "in0_div_in1"} : i17
%20 = comb.extract %19 from 0 {sv.namehint = "in0_div_in1_0_to_15"} : (i17) -> i15
hw.output
}
}
- FSM(Finite-state machine)方言:对电路中的有限状态机进行建模
Python 源码:
from pycde import System, Input, Output, generator, Module
from pycde.common import Clock
from pycde.dialects import comb
from pycde import fsm
from pycde.types import types
class F0(fsm.Machine):
a = Input(types.i1)
b = Input(types.i1)
c = Input(types.i1)
def maj3(ports):
'''采用异或操作实现与非门'''
def nand(*args):
return comb.XorOp(comb.AndOp(*args), types.i1(1))
'''构建三级与非门组合电路'''
c1 = nand(ports.a, ports.b)
c2 = nand(ports.b, ports.c)
c3 = nand(ports.a, ports.c)
return nand(c1, c2, c3)
idle = fsm.State(initial=True)
(A, B, C) = fsm.States(3)
'''
配置状态转移规则:
idle -> A (无条件)
A -> B (当输入a有效时)
B -> C (根据maj3计算结果)
C -> idle(基于c)或A (基于b的取反运算)
'''
idle.set_transitions((A,))
A.set_transitions((B, lambda ports: ports.a))
B.set_transitions((C, maj3))
C.set_transitions((idle, lambda ports: ports.c),
(A, lambda ports: comb.XorOp(ports.b, types.i1(1))))
class FSMUser(Module):
a = Input(types.i1)
b = Input(types.i1)
c = Input(types.i1)
clk = Clock()
rst = Input(types.i1)
is_a = Output(types.i1)
is_b = Output(types.i1)
is_c = Output(types.i1)
@generator
def construct(ports):
fsm = F0(a=ports.a, b=ports.b, c=ports.c, clk=ports.clk, rst=ports.rst)
ports.is_a = fsm.is_A
ports.is_b = fsm.is_B
ports.is_c = fsm.is_C
system = System([FSMUser])
system.generate()
system.print()
FSM IR:使用 python3 xxx.py
即可
# RUN: python3 %s
module {
esi.manifest.sym @FSMUser name "FSMUser"
hw.module @FSMUser(in %a : i1, in %b : i1, in %c : i1, in %clk : !seq.clock, in %rst : i1, out is_a : i1, out is_b : i1, out is_c : i1) attributes {output_file = #hw.output_file<"FSMUser.sv", includeReplicatedOps>} {
%0:4 = fsm.hw_instance "F0" @F0(%a, %b, %c), clock %clk, reset %rst : (i1, i1, i1) -> (i1, i1, i1, i1)
hw.output %0#1, %0#2, %0#3 : i1, i1, i1
}
fsm.machine @F0(%arg0: i1, %arg1: i1, %arg2: i1) -> (i1, i1, i1, i1) attributes {clock_name = "clk", in_names = ["a", "b", "c"], initialState = "idle", out_names = ["is_idle", "is_A", "is_B", "is_C"], reset_name = "rst"} {
fsm.state @idle output {
%true = hw.constant true
%false = hw.constant false
%false_0 = hw.constant false
%false_1 = hw.constant false
fsm.output %true, %false, %false_0, %false_1 : i1, i1, i1, i1
} transitions {
'''无条件跳转到 A'''
fsm.transition @A
}
fsm.state @A output {
%false = hw.constant false
%true = hw.constant true
%false_0 = hw.constant false
%false_1 = hw.constant false
fsm.output %false, %true, %false_0, %false_1 : i1, i1, i1, i1
} transitions {
'''根据 arg0(也就是a)的值进行判断是否跳转到B'''
fsm.transition @B guard {
fsm.return %arg0
}
}
fsm.state @B output {
%false = hw.constant false
%false_0 = hw.constant false
%true = hw.constant true
%false_1 = hw.constant false
fsm.output %false, %false_0, %true, %false_1 : i1, i1, i1, i1
} transitions {
'''根据 maj3 的计算结果进行判断是否跳转到C'''
fsm.transition @C guard {
%0 = comb.and bin %arg0, %arg1 : i1
%true = hw.constant true
%1 = comb.xor bin %0, %true : i1
%2 = comb.and bin %arg1, %arg2 : i1
%true_0 = hw.constant true
%3 = comb.xor bin %2, %true_0 : i1
%4 = comb.and bin %arg0, %arg2 : i1
%true_1 = hw.constant true
%5 = comb.xor bin %4, %true_1 : i1
%6 = comb.and bin %1, %3, %5 : i1
%true_2 = hw.constant true
%7 = comb.xor bin %6, %true_2 : i1
fsm.return %7
}
}
fsm.state @C output {
%false = hw.constant false
%false_0 = hw.constant false
%false_1 = hw.constant false
%true = hw.constant true
fsm.output %false, %false_0, %false_1, %true : i1, i1, i1, i1
} transitions {
'''根据 arg2(c)的值进行判断是否跳转到 idle 初始状态'''
fsm.transition @idle guard {
fsm.return %arg2
}
'''根据 arg1(b)的值取反进行判断是否跳转到 A'''
fsm.transition @A guard {
%true = hw.constant true
%0 = comb.xor bin %arg1, %true : i1
fsm.return %0
}
}
}
}
- Core IR + SV IR:使用
circt-opt --convert-fsm-to-sv xxx.mlir
用于将 hwarith 方言转换为 Core 层方言 + SV 方言
# RUN: circt-opt --convert-fsm-to-sv %s
module {
'''定义一个类型域,用于管理有限状态机(FSM)的状态枚举类型定义'''
hw.type_scope @fsm_enum_typedecls {
hw.typedecl @F0_state_t : !hw.enum<idle, A, B, C>
}
'''将类型域 @fsm_enum_typedecls 的定义输出到文件 "fsm_enum_typedefs.sv"'''
emit.file "fsm_enum_typedefs.sv" {
emit.ref @fsm_enum_typedecls
}
'''定义一个 emit 片段,用于在生成的 SystemVerilog 代码中包含'''
emit.fragment @FSM_ENUM_TYPEDEFS {
sv.verbatim "`include \22fsm_enum_typedefs.sv\22"
}
esi.manifest.sym @FSMUser name "FSMUser"
hw.module @FSMUser(in %a : i1, in %b : i1, in %c : i1, in %clk : !seq.clock, in %rst : i1, out is_a : i1, out is_b : i1, out is_c : i1) attributes {output_file = #hw.output_file<"FSMUser.sv", includeReplicatedOps>} {
%F0.out0, %F0.out1, %F0.out2, %F0.out3 = hw.instance "F0" @F0(in0: %a: i1, in1: %b: i1, in2: %c: i1, clk: %clk: !seq.clock, rst: %rst: i1) -> (out0: i1, out1: i1, out2: i1, out3: i1)
hw.output %F0.out1, %F0.out2, %F0.out3 : i1, i1, i1
}
hw.module @F0(in %in0 : i1, in %in1 : i1, in %in2 : i1, out out0 : i1, out out1 : i1, out out2 : i1, out out3 : i1, in %clk : !seq.clock, in %rst : i1) attributes {emit.fragments = [@FSM_ENUM_TYPEDEFS]} {
'''
定义枚举常量 idle,类型为 F0_state_t,并定义一个寄存器 %to_idle, 并初始化为idle
类似地,定义枚举常量 A,类型为 F0_state_t,以及对应的寄存器 %to_A
定义枚举常量 B,类型为 F0_state_t,以及对应的寄存器 %to_B
定义枚举常量 C,类型为 F0_state_t,以及对应的寄存器 %to_C
'''
%idle = hw.enum.constant idle : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
%to_idle = sv.reg sym @idle : !hw.inout<typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>>
sv.assign %to_idle, %idle : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
%0 = sv.read_inout %to_idle : !hw.inout<typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>>
%A = hw.enum.constant A : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
%to_A = sv.reg sym @A : !hw.inout<typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>>
sv.assign %to_A, %A : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
%1 = sv.read_inout %to_A : !hw.inout<typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>>
%B = hw.enum.constant B : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
%to_B = sv.reg sym @B : !hw.inout<typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>>
sv.assign %to_B, %B : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
%2 = sv.read_inout %to_B : !hw.inout<typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>>
%C = hw.enum.constant C : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
%to_C = sv.reg sym @C : !hw.inout<typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>>
sv.assign %to_C, %C : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
%3 = sv.read_inout %to_C : !hw.inout<typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>>
%state_next = sv.reg : !hw.inout<typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>>
%4 = sv.read_inout %state_next : !hw.inout<typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>>
'''
定义一个名为 state_reg 的 compreg, 它的输出为 %4,时钟为 %clk,复位信号为 %rst,复位值为 %0 (idle)
'''
%state_reg = seq.compreg sym @state_reg %4, %clk reset %rst, %0 : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
%true = hw.constant true
%false = hw.constant false
%false_0 = hw.constant false
%false_1 = hw.constant false
%false_2 = hw.constant false
%true_3 = hw.constant true
%false_4 = hw.constant false
%false_5 = hw.constant false
'''状态机状态转移逻辑'''
%5 = comb.mux %in0, %2, %1 : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
%false_6 = hw.constant false
%false_7 = hw.constant false
%true_8 = hw.constant true
%false_9 = hw.constant false
%6 = comb.and bin %in0, %in1 : i1
%true_10 = hw.constant true
%7 = comb.xor bin %6, %true_10 : i1
%8 = comb.and bin %in1, %in2 : i1
%true_11 = hw.constant true
%9 = comb.xor bin %8, %true_11 : i1
%10 = comb.and bin %in0, %in2 : i1
%true_12 = hw.constant true
%11 = comb.xor bin %10, %true_12 : i1
%12 = comb.and bin %7, %9, %11 : i1
%true_13 = hw.constant true
%13 = comb.xor bin %12, %true_13 : i1
%14 = comb.mux %13, %3, %2 : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
%false_14 = hw.constant false
%false_15 = hw.constant false
%false_16 = hw.constant false
%true_17 = hw.constant true
%true_18 = hw.constant true
%15 = comb.xor bin %in1, %true_18 : i1
%16 = comb.mux %15, %1, %3 : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
%17 = comb.mux %in2, %0, %16 : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
'''定义用于存储输出的寄存器'''
%output_0 = sv.reg : !hw.inout<i1>
%output_1 = sv.reg : !hw.inout<i1>
%output_2 = sv.reg : !hw.inout<i1>
%output_3 = sv.reg : !hw.inout<i1>
'''描述状态机跳转关系'''
sv.alwayscomb {
sv.case %state_reg : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
case idle: {
sv.bpassign %state_next, %1 : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
sv.bpassign %output_0, %true : i1
sv.bpassign %output_1, %false : i1
sv.bpassign %output_2, %false_0 : i1
sv.bpassign %output_3, %false_1 : i1
}
case A: {
sv.bpassign %state_next, %5 : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
sv.bpassign %output_0, %false_2 : i1
sv.bpassign %output_1, %true_3 : i1
sv.bpassign %output_2, %false_4 : i1
sv.bpassign %output_3, %false_5 : i1
}
case B: {
sv.bpassign %state_next, %14 : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
sv.bpassign %output_0, %false_6 : i1
sv.bpassign %output_1, %false_7 : i1
sv.bpassign %output_2, %true_8 : i1
sv.bpassign %output_3, %false_9 : i1
}
case C: {
sv.bpassign %state_next, %17 : !hw.typealias<@fsm_enum_typedecls::@F0_state_t, !hw.enum<idle, A, B, C>>
sv.bpassign %output_0, %false_14 : i1
sv.bpassign %output_1, %false_15 : i1
sv.bpassign %output_2, %false_16 : i1
sv.bpassign %output_3, %true_17 : i1
}
default: {
}
}
%18 = sv.read_inout %output_0 : !hw.inout<i1>
%19 = sv.read_inout %output_1 : !hw.inout<i1>
%20 = sv.read_inout %output_2 : !hw.inout<i1>
%21 = sv.read_inout %output_3 : !hw.inout<i1>
hw.output %18, %19, %20, %21 : i1, i1, i1, i1
}
}
上面所举例子中均涉及 Core 层方言,可分为以下三种方言:
- Seq:用于表示带复位信号和使能信号的寄存器逻辑。例如
seq.comreg.ce
用于描述具有使能信号和复位信号的同步时序逻辑,而seq.firreg
则表示带有复位信号的同步或异步时序逻辑。 - HW:专用于表示硬件设计中的公共特性。该方言定义的 Operations 不涉及组合逻辑、时序逻辑和线网之间的连接逻辑,因此具有高度的通用性,并能够与其他方言灵活结合使用。这使得 HW 方言不依赖于特定的硬件语言。例如,它可以有效地表示硬件中的 module、instance 和 port;
- Comb:与 HW 方言类似,提供一种不依赖于特定硬件语言的通用表示方式。专注于表示硬件电路中的组合电路,其设计理念强调简洁性,力求用更少的 Operations 表达更多的功能。例如,通过将 x 与全 1 进行异或运算,可以有效地表示一元位运算符(~x)。
此外,在 FSM 转换路线中还涉及到 SV 方言,该方言旨在直接对 SystemVerilog 语言的语法进行建模,并通过 ExportVerilog pass 轻松生成 SystemVerilog。由于该方言专注于输出 SystemVerilog,因此生成的 IR 采用“AST”风格的表示形式。此外,该方言可以与 Core 层的其他方言混合使用,因此它并不具备独立的组合逻辑、时序逻辑以及模块等常见功能。
后续对 Core 层方言的转换逻辑可以参考 SystemVerilog 在 CIRCT 上的初步探索或 Chisel 与 CIRCT 的无缝集成中对 Johnson 计数器的举例。
总结
本文深入探讨了 Python 与 CIRCT 的融合在 HLS 领域带来的全新视角。通过 PyCDE 提供的 Python API,开发者可以方便地利用 Python 语言进行硬件设计描述和验证,从而加速 HLS 流程。 ESI Dialect 则为构建复杂加速器系统提供了强大的互连支持。 这种 Python + CIRCT 的组合不仅降低了硬件设计的门槛,还充分利用了 Python 语言的灵活性和丰富的生态系统,为 HLS 带来了前所未有的便利性和效率。 随着相关技术的不断发展,我们有理由相信,Python 与 CIRCT 的融合将在未来的 FPGA 和 ASIC 设计中发挥越来越重要的作用,推动硬件设计的创新和发展。
参考资料