Chisel - 下一代 RTL 设计与验证语言

Chisel是建构在Scala语言之上的领域专用语言(Domain-specific language, DSL),支持高度参数化的硬件构造器。Chisel一直宣称自己既不是HDL(Hardware Description Language),也不是HLS(High-Level Synthesis),而是HCL(Hardware Constructed Language)。HCL本身不提供任何新的硬件抽象, 其本质仍是RTL建模, 但它可以利用宿主语言的特征使硬件设计变得更加参数化和模块化。相比HLS编程的软件思维,作为HCL的Chisel还是需要使用硬件思维编程去描述电路。

泛Chisel语言还有Spinal HDL,其语言完成度也很高。但同源自UC Berkeley名门的RISC-V生态与Chisel联系紧密,使用Chisel实现相应单元能较为便捷地接入现有的RISC-V生态之中,因此Spinal在当前的大环境中无法打败Chisel在学术界和工业界的流行地位。

Chisel内建了各种数据类型以支持在RTL层进行硬件建模, 通过设计模式、函数式编程等技术, 方便地表达设计、集成各种重用模块。Chisel可以为设计生成高效的周期精确的C++模拟模型, 或者使用标准工具生成适用于FPGA仿真或ASIC综合的Verilog描述。在验证方面, Chisel带来的优势是编写Testbench与设计描述所用的语法语义在同一个层次, 利用Scala提供的单元测试等支持, 从而加速设计验证。与Chisel相伴随的还有电路灵活中间表达FIRRTL用于描述电路,也是后文将要重点介绍的内容。

复旦大学高级综合课程中不少出现Chisel的身影:在2016年的课程中,张启晨首先报告了Chisel2的接口文档;2019年朱浩哲以Chisel3.2.0为例,详细介绍了早期的Chisel/FIRRTL,它现在被称作Scala-based FIRRTL Compiler (SFC)。Chisel仍是一门在不断发展和进化中的新型语言,如今的Chisel后端已更换为更加高效灵活的MLIR-based FIRRTL Compiler (MFC),可使Chisel的编译速度提升几十倍。与此同时,活跃的Chisel团队也采用了更激进的发版规则,最新以MFC作为编译后端的Chisel版本号已经飞升至6.0.0。

FIRRTL编译器可以把Chisel文件转换成.fir这种标准的中间交换格式。借助FIRRTL这一层中间表示可以让各种高级语言方便地转换到Verilog/VHDL,但它其实和verilog/VHDL属于同一层次。也就是说,Chisel其实是为现阶段的硬件设计生态做了妥协,没有让FIRRTL直接生成电路网表,而是借助Verilog再接入各种EDA工具进行后续的验证工作。

『关于Chisel的构建工具: Mill 和 SBT』

SBT 是 Scala 社区的默认构建工具,因此尽管 SBT 一直被程序员诟病(语法结构复杂、抽象层次过多…),但绝大多数人仍默认使用 SBT 构建 Scala 工程。 Mill 是一种类似于 Maven、SBT 的支持 Scala 的快速多语言 JVM 构建工具, 它的构建文件语法更简洁,直接使用的就是 Scala 语法,对于程序员而言更容易理解和维护。 尽管 Mill 诞生时间不长、社区资源不如 SBT 丰富,但其现代化的设计思想让它更适合与如 Chisel 这类新一代工具链生态集成。 国内领先使用 Chisel 作为开发语言的香山团队、一生一芯团队也选择了 Mill 作为构建工具。 「所以推荐尝试用 Mill 构建你的 Chisel 工程」

CIRCT - 基于 MLIR 的电路编译器和工具链

CIRCT 的核心目标是解决 EDA 工具的碎片化问题,构建一个统一的编译框架,将形式化验证、仿真、综合等功能集成到同一平台中。MLIR 原本是为了解决软件领域的编译难题,但其模块化和可扩展的设计使得它成为了硬件编译的理想工具。CIRCT通过为不同的硬件描述语言(如 Verilog、Sys-temVerilog、Chisel 等)定义特定的 Dialect,并利用 MLIR 的多层次表示和优化方法,为硬件设计提供了一条更加灵活和高效的设计流程。CIRCT 与 MLIR 相结合,做到硬件设计和软件编译的“同源”,旨在实现软件生态和硬件设计生态的一致化。

MLIR Dialect体系

首先,MLIR 中的 ML 不是 Machine Learning,而是 Multi-Level,但 Machine Learning 确实是 MLIR 的一个应用领域。MLIR 的诞生是希望为各种 DSL 提供一种中间表达形式,将他们集成为一套生态系统,使用一种一致性强的方式编译到特定硬件平台的汇编语言上。利用这样的形式,MLIR 就可以利用它模块化、可扩展的特点来解决 IR 之间相互配合的问题。一个软件前端的 IR,可以通过多级 IR,逐渐 lowering 到特定硬件上,这些 IR 的接口在 MLIR 中被称作 Dialect。利用多级 IR,可以分层处理问题,针对不同领域问题可以选择不同的专用 IR 进行多层次优化;高层次的 IR 可以不断复用,并相应地使用低层次的 IR 去适配不同的硬件后端。

水平方向上,Dialect 把完整中间表示打散成许多局部中间表示;垂直方向上,MLIR 可以对处于不同层级的概念进行建模。这对领域专用编译器是非常有用的。因为领域专用语言一般是高度抽象的声明式语言,只描述任务,需要编译器将其转换成具体的命令式机器指令。一步跨越这个巨大的抽象差距是非常难的,利用多级抽象和建模来进行渐进式 lowering 是更加适合的方式。分离各个层次关注的问题,会让整个系统更加易于开发和维护,这样设计人员就只需要专注于自己层级的优化,避免去做重复造轮子的优化工作。

在 MLIR 的设计初衷里,可扩展性是非常强烈的需求考量,要能够纵向支撑用户不同的场景,也要横向支撑不同硬件接入进来,低成本地编译优化。MLIR 为编译优化而生,分层 Lowering 是比较符合设计直觉的,在多硬件、多场景的 Dialect扩展性上具有天然优势。在 CIRCT 项目中,Dialect 的设计至关重要。高层次的 FIRRTL Dialect 可以直接映射 Chisel 中的特性。更低层次的 Core CIRCT Dialect 中还有 HW Dialect 用于抽象硬件结构,Seq Dialect 表示时序逻辑,Comb Dialect 表示组合逻辑。还有 SV Dialect 用于映射 SystemVerilog 的定义和结构。通过这些 Dialect 的定义,CIRCT 能够在硬件设计的不同抽象层次上进行优化。

这个思路就是 LLVM 的思路:LLVM 设计了一种描述清楚的中间表达 llir,编译器前端可以将任何语言转化为 llir,而所有的中间优化步骤会一级级的处理llir,优化过的内容仍然符合llir格式。最终的 llir 会被编译器后端转化为到目标机器汇编代码。

Chisel/FIRRTL 硬件编译器框架

在Chisel3.5.x及之前的版本中的FIRRTL称为Scala-based FIRRTL Compiler (SFC)。5.0.0及之后的Chisel/FIRRTL已更换为MLIR-based FIRRTL Compiler (MFC),并将其集成在了LLVM CIRCT的firtool中。往后,Chisel团队将不再维护SFC项目,而是直接将Chisel的编译后端链接至Chris Lattner带领维护的CIRCT项目。

Scala-based FIRRTL Compiler (SFC)

FIRRTL 是在 Chisel3 版本引入的框架中间代码层语言。在 Chisel 2 中,Chisel 编译器会直接将其内部的数据结构转换成 Verilog 输出,这种紧耦合的做法有着与LLVM类似的问题。为此,Chisel 3 重新设计了整个编译流程,独立出了 FIRRTL 后端。FIRRTL 一方面提供了标准化的中间表示层,另一方面提供了模块化的编译流程。此阶段的 FIRRTL 主要包括三个部分:FIRRTL IR、FIRRTL 编译器(FIRRTL Compiler)、电路级变换(Circuit-level Transformations)。

FIRRTL 内部的数据结构是一棵描述数字电路的抽象语法树(Abstract Syntax Tree,AST),它的整个结构都可以以可读性较好的文本形式(concrete syntax)写下来,这个文本形式的表示就是 FIRRTL IR。Chisel 编译器以 FIRRTL IR 格式将电路写入若干文件,作为 FIRRTL Compiler 的输入。FIRRTL Compiler 读取 FIRRTL IR 输入,按照一定顺序依次对电路做若干变换。这里的变换可以是逻辑简化、时序优化、形式化验证、转换成 Verilog 等,FIRRTL 内置了一些常用的变换,用户也可以根据自己的需求开发自己的变换。

此阶段的 FIRRTL 已经参考 MLIR 引入了 lowering 变换:将FIRRTL IR中的高阶语法用等价(但一般更加冗长且难以阅读)的低级语法代替。FIRRTL制定了三种不同Level的电路,称为Form:High Form囊括了所有的FIRRTL IR语法;Mid Form要求所有位宽必须显式地给出,不允许使用条件语句,所有组件只允许连接一次;Low Form在Mid Form基础上进一步要求信号类型必须是有符号整型、无符号整型、时钟三种类型中的一种,不允许信号部分连接。Low Form的FIRRTL IR代码可以很容易地翻译到Verilog,甚至逻辑上可以直接做Techmap到网表。FIRRTL 这种应用于硬件设计领域的多层次的编译优化方案参考于 MLIR,也为 CIRCT 的多级优化提供了解决思路。

以下面这段Chisel3的代码为例,展示了由此生成三级FIRRTL IR的格式,可以看出每一级Form的优化都发生了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package example

import chisel3._
import chisel3.stage._
import chisel3.stage.ChiselStage

class counter(bitWidth: Int) extends Module {
  val io = IO(new Bundle {
    val out = Output(UInt(bitWidth.W))
    val opcode = Input(UInt(2.W))
  })

  val counter1 = RegInit(UInt(bitWidth.W), 0.U)

  when(io.opcode === 0.U) {
    counter1 := counter1 + 1.U
  }.elsewhen(io.opcode === 1.U) {
    counter1 := counter1 - 1.U
  }.elsewhen(io.opcode === 2.U) {
    counter1 := counter1
  }.otherwise {
    counter1 := counter1
  }

  io.out := counter1;

}

object counterMain extends App {
  val verilogString =
    (new chisel3.stage.ChiselStage).emitVerilog(new counter(32))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// High Form FIRRTL
circuit counter :
  module counter :
    input clock : Clock
    input reset : UInt<1>
    output io : { out : UInt<32>, flip opcode : UInt<2>}

    reg counter1 : UInt<32>, clock with :
      reset => (reset, UInt<1>("h0")) @[counter.scala 13:25]
    node _T = eq(io.opcode, UInt<1>("h0")) @[counter.scala 15:18]
    when _T : @[counter.scala 15:27]
      node _counter1_T = add(counter1, UInt<1>("h1")) @[counter.scala 16:26]
      node _counter1_T_1 = tail(_counter1_T, 1) @[counter.scala 16:26]
      counter1 <= _counter1_T_1 @[counter.scala 16:14]
    else :
      node _T_1 = eq(io.opcode, UInt<1>("h1")) @[counter.scala 17:24]
      when _T_1 : @[counter.scala 17:33]
        node _counter1_T_2 = sub(counter1, UInt<1>("h1")) @[counter.scala 18:26]
        node _counter1_T_3 = tail(_counter1_T_2, 1) @[counter.scala 18:26]
        counter1 <= _counter1_T_3 @[counter.scala 18:14]
      else :
        node _T_2 = eq(io.opcode, UInt<2>("h2")) @[counter.scala 19:24]
        when _T_2 : @[counter.scala 19:33]
          counter1 <= counter1 @[counter.scala 20:14]
        else :
          counter1 <= counter1 @[counter.scala 22:14]
    io.out <= counter1 @[counter.scala 25:10]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Mid Form FIRRTL
circuit counter :
  module counter :
    input clock : Clock
    input reset : UInt<1>
    output io : { out : UInt<32>, flip opcode : UInt<2>}

    reg counter1 : UInt<32>, clock with :
      reset => (reset, UInt<1>("h0")) @[counter.scala 13:25]
    node _T = eq(io.opcode, UInt<1>("h0")) @[counter.scala 15:18]
    node _counter1_T = add(counter1, UInt<1>("h1")) @[counter.scala 16:26]
    node _counter1_T_1 = tail(_counter1_T, 1) @[counter.scala 16:26]
    node _T_1 = eq(io.opcode, UInt<1>("h1")) @[counter.scala 17:24]
    node _counter1_T_2 = sub(counter1, UInt<1>("h1")) @[counter.scala 18:26]
    node _counter1_T_3 = tail(_counter1_T_2, 1) @[counter.scala 18:26]
    node _T_2 = eq(io.opcode, UInt<2>("h2")) @[counter.scala 19:24]
    node _GEN_0 = mux(_T_2, counter1, counter1) @[counter.scala 19:33 counter.scala 20:14 counter.scala 22:14]
    node _GEN_1 = mux(_T_1, _counter1_T_3, _GEN_0) @[counter.scala 17:33 counter.scala 18:14]
    node _GEN_2 = mux(_T, _counter1_T_1, _GEN_1) @[counter.scala 15:27 counter.scala 16:14]
    io.out <= counter1 @[counter.scala 25:10]
    counter1 <= _GEN_2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Low Form FIRRTL
circuit counter :
  module counter :
    input clock : Clock
    input reset : UInt<1>
    output io_out : UInt<32>
    input io_opcode : UInt<2>

    reg counter1 : UInt<32>, clock with :
      reset => (UInt<1>("h0"), counter1) @[counter.scala 13:25]
    node _T = eq(io_opcode, UInt<1>("h0")) @[counter.scala 15:18]
    node _counter1_T = add(counter1, UInt<1>("h1")) @[counter.scala 16:26]
    node _counter1_T_1 = tail(_counter1_T, 1) @[counter.scala 16:26]
    node _T_1 = eq(io_opcode, UInt<1>("h1")) @[counter.scala 17:24]
    node _counter1_T_2 = sub(counter1, UInt<1>("h1")) @[counter.scala 18:26]
    node _counter1_T_3 = tail(_counter1_T_2, 1) @[counter.scala 18:26]
    node _T_2 = eq(io_opcode, UInt<2>("h2")) @[counter.scala 19:24]
    node _GEN_0 = mux(_T_2, counter1, counter1) @[counter.scala 19:33 counter.scala 20:14 counter.scala 22:14]
    node _GEN_1 = mux(_T_1, _counter1_T_3, _GEN_0) @[counter.scala 17:33 counter.scala 18:14]
    node _GEN_2 = mux(_T, _counter1_T_1, _GEN_1) @[counter.scala 15:27 counter.scala 16:14]
    io_out <= counter1 @[counter.scala 25:10]
    counter1 <= mux(reset, UInt<1>("h0"), _GEN_2) @[counter.scala 13:25 counter.scala 13:25]

MLIR-based FIRRTL Compiler (MFC)

Chisel 需要一个 FIRRTL Complier 实现.fir 到.v 的格式转换,构建如此的领域专用编译器正是 MLIR 所擅长的,也正契合 CIRCT 项目所希望构建的硬件设计生态。正因如此,随着 CIRCT 生态的不断成熟,将 Chisel 的编译后端接入 CIRCT 是一个必然的发展路径。

1
2
3
4
5
6
7
8
9
// .fir file: FIRRTL IR
module Foo:  
  input clk: Clock  
  input bus: {valid: UInt<1>, data: UInt<32>}  
  
  reg dataReg: UInt, clk  
  
  when bus.valid:    
    dataReg <= bus.data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// .mlir file: High FIRRTL
firrtl.module @Foo(in %clk: !firrtl.clock, in %bus:
                   !firrtl.bundle<valid: uint<1>, data: uint<32>>) {  
  %dataReg = firrtl.reg %clk  : (!firrtl.clock) -> !firrtl.uint  

  %0 = firrtl.subfield %bus("valid") :       
       (!firrtl.bundle<valid: uint<1>, data: uint<32>>) -> !firrtl.uint<1>  

  firrtl.when %0  {    
    %1 = firrtl.subfield %bus("data") :         
         (!firrtl.bundle<valid: uint<1>, data: uint<32>>) -> !firrtl.uint<32>    

    firrtl.connect %dataReg, %1 : !firrtl.uint, !firrtl.uint<32> 
} }
1
2
3
4
5
6
7
8
9
10
// .mlir file: Low FIRRTL
firrtl.module @Foo(in %clk: !firrtl.clock, in %bus_valid: !firrtl.uint<1>,                   
                    in %bus_data: !firrtl.uint<32>) {  
  %dataReg = firrtl.reg %clk  : (!firrtl.clock) -> !firrtl.uint<32>  

  %0 = firrtl.mux(%bus_valid, %bus_data, %dataReg) :    
       (!firrtl.uint<1>, !firrtl.uint<32>, !firrtl.uint<32>) -> !firrtl.uint<32>  

  firrtl.connect %dataReg, %0 : !firrtl.uint<32>, !firrtl.uint<32> 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// .mlir file: HW+SV+Comb
hw.module @Foo(%clk: i1, %bus_valid: i1, %bus_data: i32) {
  %dataReg = sv.reg : !hw.inout<i32>  
  sv.ifdef "SYNTHESIS"  {
  } else  {    
    sv.initial  {      
      sv.verbatim "`INIT_RANDOM_PROLOG_" 
        sv.ifdef.procedural "RANDOMIZE_REG_INIT"  {  
          %RANDOM = sv.verbatim.expr "`RANDOM" : () -> i32 
          sv.bpassign %dataReg, %RANDOM : i32      
    }}}  
    %0 = sv.read_inout %dataReg : !hw.inout<i32>  
    %1 = comb.mux %bus_valid, %bus_data, %0 : i32
    sv.alwaysff(posedge %clk)  {    
      sv.passign %dataReg, %1 : i32}  
    hw.output
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// .sv file
module Foo( 
  input        clk, bus_valid, 
  input [31:0] bus_data); 
  reg [31:0] dataReg;   // Foo.mlir:32:16 
  `ifndef SYNTHESIS     // Foo.mlir:33:5   
    initial begin       // Foo.mlir:35:7     
      `INIT_RANDOM_PROLOG_      // Foo.mlir:36:9     
      `ifdef RANDOMIZE_REG_INIT // Foo.mlir:37:9      
        dataReg = `RANDOM;      // Foo.mlir:38:21, :39:11     
      `endif   
    end // initial 
  `endif 
  wire [31:0] _T = bus_valid ? bus_data : dataReg;     
  // Foo.mlir:43:10, :44:10 
  always_ff @(posedge clk)      // Foo.mlir:45:5   
    dataReg <= _T;      // Foo.mlir:46:7 
endmodule

构建高效统一的全链条开源 EDA 软件栈

EDA工具对Chisel的源码级支持

实际应用中,Chisel 生成的 Verilog 代码一直被工程师诟病可读性差、端口名不一致导致难以 Debug…… 但回到根本,Verilog 的诞生本就不是为了硬件设计,Chisel 到 Verilog 多级优化更重要的目的应该是提高最终生成的硬件性能。因此 Chisel 社区也并没有兴趣提高生成的 Verilog 代码的可读性,反而是更希望大家在设计阶段更专注于 Chisel 的实现逻辑。对于这一问题,CIRCT暂时作出的妥协是提供多种 firtool 优化模式:release、debug,或其他更细粒度的选项用于控制Verilog层级的优化程度。

其实 FIRRTL 可以直接作为仿真工具的输入文件,或者做为综合工具的输入文件。从 firtool生成的 HGLDD 文件与其为之开发的 VCS/Verdi 紧密结合。SiFive 的 Jack Koenig 在 2024 年 4 月LatchUp 会议上宣布 Chisel 将获得 VCS/Verdi 的支持,Surfer 也将提供的 Chisel 专用的数据类型支持。随着 EDA 工具对 Chisel 的逐步支持,开发者可以更好地应对调试和验证的挑战,而不需要关注 Verilog 层面的语法和结构。

加速硬件敏捷设计

CIRCT 的出现代表了硬件设计和仿真工具发展的一个重要趋势,即更加模块化、灵活和高效的设计框架。随着硬件架构的复杂性和异构性不断增加,CIRCT 通过其独特的多层次设计表示和优化方法,能够为设计者提供更大的灵活性和性能提升。CIRCT 也在尝试向硬件仿真、形式化验证、综合优化的方向推进。CIRCT 框架利用 MLIR 的中间表示能力,有望整合更多 EDA点工具,推动硬件设计工具链的统一化。

构建这样一个统一的开源 EDA 软件栈,将极大地促进硬件设计与软件开发的深度融合。这种融合不仅能提升设计的可复用性和扩展性,还能为新兴计算需求提供灵活的解决方案,推动硬件开发方法学的进步。通过这种创新的设计框架,设计团队将能够更快地响应市场变化,加速整个设计周期。