Python 与 CIRCT 的融合:HLS新视角

引言

随着人工智能、高性能计算等领域对定制化硬件需求的日益增长,传统的手工编写 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 设计中发挥越来越重要的作用,推动硬件设计的创新和发展。

参考资料

  1. Elastic Silicon Interconnects Abstracting Communication in Accelerator Design

  2. ESI Dialect

  3. Elastic Silicon Interconnect (ESI) modeling

  4. 高层次综合技术原理浅析

  5. HLS与RTL语言使用情况调查

  6. PyCDE Basics

  7. The Elastic Silicon Interconnect dialect

  8. The ESI System Construction Compiler in 2024

  9. What’s The Real Benefit Of High-Level Synthesis?

1 Like