引言
在了解了当前开源 EDA 工具的碎片化现状以及 CIRCT 的创立动机和发展历程后,读者应对 CIRCT 项目为定制化硬件设计带来的优势有了基本的认识。接下来,本文将重点介绍 CIRCT 项目中的技术细节。然而,在深入探讨这些技术细节之前,我们首先为读者普及一些必要的概念——方言(dialect)。本文将重点介绍 SystemVerilog 和 Chisel 这两条技术路线所涉及的所有方言(按照层次顺序),未提及的其他方言将在后续文章中单独介绍。
方言概述
方言是与 MLIR 生态系统交互和扩展的机制。它们允许定义新的 Operations、Type System、Attributes。可用于建模各种不同的抽象,从传统的算术运算到模式重写,方言是 MLIR 中最基本的组成部分之一。
- Type System:用于映射某种语言中定义的合法类型,为 Operations 提供支持。它确保在执行 Operations 时,所有数据类型都是有效且一致的。
- Attributes:用于表示常量数据,如数字、字符以及自定义信息等。与类型不同,在通过.td 文件生成的.inc 文件中,type 会被当做 operand,而 attribute 是 properties。
- Operations:用于映射某种语言中的特性,包括变量声明、运算符、条件语句和函数声明等。它们是方言的核心,定义了程序如何在特定上下文中执行。
方言这一抽象概念是 MLIR 编译器框架的核心,每个方言都代表着不同的功能模块,例如:
- Affine:用于表示与仿射相关的操作,如 for 循环定义了循环迭代的空间;
- Arith:专注于处理整型和浮点类型的数学运算,提供丰富的算术功能;
- CF:用于表示非结构化的控制流,适用于更复杂的执行路径;
- SCF:用于表示结构化的控制流,帮助简化和优化程序逻辑;
这一设计理念使得 MLIR 具备了模块化和可扩展的特征。通过这种方式,开发者可以根据特定需求,灵活地定义新的方言或扩展现有方言,使 MLIR 能够支持多种编程模型和硬件架构。 在对方言这一抽象概念有了初步了解后,我们可以进一步探讨 CIRCT 中各个方言所代表的功能。CIRCT 项目应用了 MLIR 的方言机制,定义了多个专门用于硬件设计和编译的方言。具体如下:
- FIRRTL(Flexible Internal Representation for RTL):用于映射 Chisel 代码中的特性,并且提供了对 SFC(Scala FIRRTL Compiler)注释的强大支持,这些注释可用于编码任意元数据;
- Moore:致力于映射 SystemVerilog IEEE Std 1800-2017 中的所有特性;
- FSM:对有限状态机进行建模;
- HW:CIRCT 项目中的核心方言,专用于表示硬件设计中的公共特性。该方言定义的 Operations 不涉及组合逻辑、时序逻辑和线网之间的连接逻辑,因此具有高度的通用性,并能够与其他方言灵活结合使用。这使得 HW 方言不依赖于特定的硬件语言。例如,它可以有效地表示硬件中的 module、instance 和 port;
- Comb:CIRCT 项目中的核心方言,与 HW 方言类似,提供一种不依赖于特定硬件语言的通用表示方式。专注于表示硬件电路中的组合电路,其设计理念强调简洁性,力求用更少的 Operations 表达更多的功能。例如,通过将 x 与全 1 进行异或运算,可以有效地表示一元位运算符(~x)。
- Seq:CIRCT 项目中的核心方言,用于表示带复位信号和使能信号的寄存器逻辑。例如
seq.comreg.ce
用于描述具有使能信号和复位信号的同步时序逻辑,而seq.firreg
则表示带有复位信号的同步或异步时序逻辑。 - SV:专为生成 SystemVerilog 代码而设计的方言,能够与 HW 方言和 Comb 方言灵活结合使用。它以类似 AST 的格式表示 SystemVerilog 中的语法特性和行为级特性,为开发者提供一种简单且可预测的访问方式,以便高效利用这些强大的功能;
- SystemC:用 C++编写的库,允许对系统进行功能建模,同样也是一种标准(IEEE Std 1666-2011)。在 CIRCT 项目中是专为生成 C++代码而设计的方言;
- Arc:用于表示硬件电路中信号的变化状态、时钟域、时钟树等;
- LLHD:目前仅用于 SV 技术路线,专门表示那些无法从 Moore 方言降级到 Core 方言的与时序逻辑相关的 Operations。例如,时延控制、无法区分复位信号和使能信号的时序逻辑。但由于语言本身的问题,如不能像 Chisel 那样区分使能信号与复位信号,因此目前该方言承担所有的时序逻辑转换。
SystemVerilog 细节举例
组合逻辑-全加器
SV 源码:
module half_add(
input A, B, // 输入端口
output Sum, Cout // 和输出端口以及进位输出端口
);
// 计算和和进位
assign Sum = A ^ B; // 和
assign Cout = A & B; // 进位
endmodule
module full_add(
input A,B,Cin,
output Sum,Cout
);
wire w1,w2,w3;
// 实例化两个半加器
half_add add1(A, B, w1, w2);
half_add add2(.A(Cin), .B(w1), .Sum(Sum), .Cout(w3));
assign Cout = w2 | w3; // 计算最终进位
endmodule
Moore IR:
module {
moore.module private @half_add(in %A : !moore.l1, in %B : !moore.l1, out Sum : !moore.l1, out Cout : !moore.l1) {
%0 = moore.xor %A, %B : l1
%1 = moore.and %A, %B : l1
moore.output %0, %1 : !moore.l1, !moore.l1 // %0 对应out Sum, %1 对应out Cout
}
moore.module @full_add(in %A : !moore.l1, in %B : !moore.l1, in %Cin : !moore.l1, out Sum : !moore.l1, out Cout : !moore.l1) {
%w1 = moore.assigned_variable %add1.Sum : l1 // 表示 w1 这根线与 %add1.Sum 相连
%w2 = moore.assigned_variable %add1.Cout : l1 // 表示 w2 这根线与 %add1.Cout 相连
%w3 = moore.assigned_variable %add2.Cout : l1 // 表示 w3 这根线与 %add2.Cout 相连
%add1.Sum, %add1.Cout = moore.instance "add1" @half_add(A: %A: !moore.l1, B: %B: !moore.l1) -> (Sum: !moore.l1, Cout: !moore.l1) // 表示将half_add中的Sum端口的值传给 %add1.Sum,Cout端口的值传给 %add1.Count
%add2.Sum, %add2.Cout = moore.instance "add2" @half_add(A: %Cin: !moore.l1, B: %w1: !moore.l1) -> (Sum: !moore.l1, Cout: !moore.l1) // 同上
%0 = moore.or %w2, %w3 : l1
moore.output %add2.Sum, %0 : !moore.l1, !moore.l1 // %add2.Sum 对应out Sum, %0 对应out Cout
}
}
Core IR:
module {
hw.module private @half_add(in %A : i1, in %B : i1, out Sum : i1, out Cout : i1) {
%0 = comb.xor %A, %B : i1
%1 = comb.and %A, %B : i1
hw.output %0, %1 : i1, i1
}
hw.module @full_add(in %A : i1, in %B : i1, in %Cin : i1, out Sum : i1, out Cout : i1) {
// 优化掉了 w1, w2, w3 临时变量,用 %add1.Sum, %add1.Cout,%add2.Cout 分别替代
%add1.Sum, %add1.Cout = hw.instance "add1" @half_add(A: %A: i1, B: %B: i1) -> (Sum: i1, Cout: i1) {sv.namehint = "w2"}
%add2.Sum, %add2.Cout = hw.instance "add2" @half_add(A: %Cin: i1, B: %add1.Sum: i1) -> (Sum: i1, Cout: i1) {sv.namehint = "w3"}
%0 = comb.or %add1.Cout, %add2.Cout : i1
hw.output %add2.Sum, %0 : i1, i1
}
}
时序逻辑-Johnson 计数器
SV 源码:
module JS_Counter(
input clk, rst_n, // 时钟信号和复位信号
output reg [3:0] q // 输出计数值
);
always @(posedge clk or negedge rst_n) begin // 异步时序逻辑,下降沿时触发复位信号
if (!rst_n) begin
q <= 4'b0000; // 触发复位信号置为0
end else begin
q <= {~q[0],q[3:1]}; // 将最后一位取反并移位
end
end
endmodule
Moore IR:
module {
moore.module @JS_Counter(in %clk : !moore.l1, in %rst_n : !moore.l1, out q : !moore.l4) {
%0 = moore.constant 0 : l4
// 端口默认类型为logic,并且会生成 moore.net表示线网
// 如果显示标明类型,如 out reg q,生成moore.variable以表示变量
// 下列声明的 %clk_0、%rst_n_1、%q表示在module内部进行运算的线网和变量
%clk_0 = moore.net name "clk" wire : <l1>
%rst_n_1 = moore.net name "rst_n" wire : <l1>
%q = moore.variable : <l4>
moore.procedure always {
// 检测敏感列表中的信号变化
moore.wait_event {
%10 = moore.read %clk_0 : <l1>
moore.detect_event posedge %10 : l1
%11 = moore.read %rst_n_1 : <l1>
moore.detect_event negedge %11 : l1
}
%2 = moore.read %rst_n_1 : <l1>
%3 = moore.not %2 : l1
%4 = moore.conversion %3 : !moore.l1 -> i1
cf.cond_br %4, ^bb1, ^bb2 // %4为true表示触发复位逻辑,否则进入取反移位逻辑
^bb1: // pred: ^bb0
moore.nonblocking_assign %q, %0 : l4 // 非阻塞赋值 <=
cf.br ^bb3
^bb2: // pred: ^bb0
%5 = moore.read %q : <l4>
%6 = moore.extract %5 from 0 : l4 -> l1 // 表示对变量q取其第0位
%7 = moore.not %6 : l1
%8 = moore.extract %5 from 1 : l4 -> l3 // 表示对变量q取其第1位到第三位
%9 = moore.concat %7, %8 : (!moore.l1, !moore.l3) -> l4 // 连接{}运算符
moore.nonblocking_assign %q, %9 : l4
cf.br ^bb3
^bb3: // 2 preds: ^bb1, ^bb2
moore.return
}
moore.assign %clk_0, %clk : l1
moore.assign %rst_n_1, %rst_n : l1
// 读取%q的值并返回给out reg q
// % 开头的才是表示MLIR中的SSA值,out reg q中的q仅是字符串
%1 = moore.read %q : <l4>
moore.output %1 : !moore.l4
}
}
Core IR + LLHD IR:
module {
hw.module @JS_Counter(in %clk : i1, in %rst_n : i1, out q : i4) {
%0 = llhd.constant_time <0ns, 0d, 1e>
%1 = llhd.constant_time <0ns, 1d, 0e>
%true = hw.constant true
%c0_i4 = hw.constant 0 : i4
%false = hw.constant false
%clk_0 = llhd.sig name "clk" %false : i1 // 声明%clk_0变量
%2 = llhd.prb %clk_0 : !hw.inout<i1> // 读取%clk_0的值
%rst_n_1 = llhd.sig name "rst_n" %false : i1
%3 = llhd.prb %rst_n_1 : !hw.inout<i1>
%q = llhd.sig %c0_i4 : i4
llhd.process {
cf.br ^bb1
^bb1: // 4 preds: ^bb0, ^bb2, ^bb4, ^bb5
%5 = llhd.prb %clk_0 : !hw.inout<i1>
%6 = llhd.prb %rst_n_1 : !hw.inout<i1>
llhd.wait (%2, %3 : i1, i1), ^bb2 // 等待时钟信号和复位信号的变化
// 开始检测时钟信号和复位信号是否符合在时钟沿才进行跳变
^bb2: // pred: ^bb1
%7 = llhd.prb %clk_0 : !hw.inout<i1>
%8 = comb.xor bin %5, %true : i1
%9 = comb.and bin %8, %7 : i1
%10 = llhd.prb %rst_n_1 : !hw.inout<i1>
%11 = comb.xor bin %10, %true : i1
%12 = comb.and bin %6, %11 : i1
%13 = comb.or bin %9, %12 : i1
cf.cond_br %13, ^bb3, ^bb1 // 不符合预期返回^bb1重新检测,否则执行后续逻辑
// 当符合时钟信号跳变时,检测是否触发复位信号
^bb3: // pred: ^bb2
%14 = llhd.prb %rst_n_1 : !hw.inout<i1>
%15 = comb.xor %14, %true : i1
cf.cond_br %15, ^bb4, ^bb5
// 执行复位信号逻辑
^bb4: // pred: ^bb3
llhd.drv %q, %c0_i4 after %1 : !hw.inout<i4>
cf.br ^bb1
// 执行取反移位逻辑
^bb5: // pred: ^bb3
%16 = llhd.prb %q : !hw.inout<i4>
%17 = comb.extract %16 from 0 : (i4) -> i1
%18 = comb.xor %17, %true : i1
%19 = comb.extract %16 from 1 : (i4) -> i3
%20 = comb.concat %18, %19 : i1, i3
llhd.drv %q, %20 after %1 : !hw.inout<i4>
cf.br ^bb1
}
llhd.drv %clk_0, %clk after %0 : !hw.inout<i1>
llhd.drv %rst_n_1, %rst_n after %0 : !hw.inout<i1>
%4 = llhd.prb %q : !hw.inout<i4>
hw.output %4 : i4
}
}
Chisel 细节举例:
组合逻辑-全加器
scala 源码:
import chisel3._
class HalfAdder extends Module {
val io = IO(new Bundle {
val a = Input(UInt(1.W)) // 输入a
val b = Input(UInt(1.W)) // 输入b
val sum = Output(UInt(1.W)) // 和输出
val carry = Output(UInt(1.W)) // 进位输出
})
// 计算和和进位
io.sum := io.a ^ io.b // 和
io.carry := io.a & io.b // 进位
}
class FullAdder extends Module {
val io = IO(new Bundle {
val a = Input(UInt(1.W)) // 第一个输入
val b = Input(UInt(1.W)) // 第二个输入
val cin = Input(UInt(1.W)) // 进位输入
val sum = Output(UInt(1.W)) // 和输出
val cout = Output(UInt(1.W)) // 进位输出
})
// 实例化两个半加器
val ha0 = Module(new HalfAdder())
val ha1 = Module(new HalfAdder())
// 将输入连接到第一个半加器
ha0.io.a := io.a
ha0.io.b := io.b
// 将第一个半加器的输出连接到第二个半加器
ha1.io.a := ha0.io.sum
ha1.io.b := io.cin
// 输出连接
io.sum := ha1.io.sum
io.cout := ha0.io.carry | ha1.io.carry // 使用OR门计算最终进位
}
// 用于测试的对象
object FullAdder extends App {
chisel3.Driver.execute(args, () => new FullAdder)
}
.fir:
FIRRTL version 2.0.0
circuit FullAdder :
// 实例化后得到的第一个半加器模块
module HalfAdder :
input clock : Clock
input reset : Reset
output io : { flip a : UInt<1>, flip b : UInt<1>, sum : UInt<1>, carry : UInt<1>}
// 半加器和与进位逻辑
node _T = xor(io.a, io.b) @[FullAdder.scala 12:18]
io.sum <= _T @[FullAdder.scala 12:10]
node _T_1 = and(io.a, io.b) @[FullAdder.scala 13:20]
io.carry <= _T_1 @[FullAdder.scala 13:12]
// 实例化后得到的第二个半加器模块
module HalfAdder_1 :
input clock : Clock
input reset : Reset
output io : { flip a : UInt<1>, flip b : UInt<1>, sum : UInt<1>, carry : UInt<1>}
// 半加器和与进位逻辑
node _T = xor(io.a, io.b) @[FullAdder.scala 12:18]
io.sum <= _T @[FullAdder.scala 12:10]
node _T_1 = and(io.a, io.b) @[FullAdder.scala 13:20]
io.carry <= _T_1 @[FullAdder.scala 13:12]
module FullAdder :
input clock : Clock
input reset : UInt<1>
output io : { flip a : UInt<1>, flip b : UInt<1>, flip cin : UInt<1>, sum : UInt<1>, cout : UInt<1>}
inst ha0 of HalfAdder @[FullAdder.scala 26:19] // 实例化第一个半加器
ha0.clock <= clock // 将FullAdder中的时钟信号传输给halfAdder
ha0.reset <= reset // 将FullAdder中的复位信号传输给halfAdder
inst ha1 of HalfAdder_1 @[FullAdder.scala 27:19] // 实例化第二个半加器
ha1.clock <= clock
ha1.reset <= reset
// 匹配实例化中的输入输出端口
ha0.io.a <= io.a @[FullAdder.scala 30:12]
ha0.io.b <= io.b @[FullAdder.scala 31:12]
ha1.io.a <= ha0.io.sum @[FullAdder.scala 34:12]
ha1.io.b <= io.cin @[FullAdder.scala 35:12]
io.sum <= ha1.io.sum @[FullAdder.scala 38:10]
// 计算最终进位值并输出
node _T = or(ha0.io.carry, ha1.io.carry) @[FullAdder.scala 39:27]
io.cout <= _T @[FullAdder.scala 39:11]
FIRRTL IR:
module {
firrtl.circuit "FullAdder" {
firrtl.module private @HalfAdder(in %io_a: !firrtl.uint<1>, in %io_b: !firrtl.uint<1>, out %io_sum: !firrtl.uint<1>, out %io_carry: !firrtl.uint<1>) {
// 声明信号
%io_sum_0 = firrtl.wire {name = "io_sum"} : !firrtl.uint<1>
%io_carry_1 = firrtl.wire {name = "io_carry"} : !firrtl.uint<1>
// firrtl.matchingconnect表示将两个信号相连
firrtl.matchingconnect %io_sum, %io_sum_0 : !firrtl.uint<1>
firrtl.matchingconnect %io_carry, %io_carry_1 : !firrtl.uint<1>
%0 = firrtl.xor %io_a, %io_b : (!firrtl.uint<1>, !firrtl.uint<1>) -> !firrtl.uint<1>
firrtl.matchingconnect %io_sum_0, %0 : !firrtl.uint<1>
%1 = firrtl.and %io_a, %io_b : (!firrtl.uint<1>, !firrtl.uint<1>) -> !firrtl.uint<1>
firrtl.matchingconnect %io_carry_1, %1 : !firrtl.uint<1>
}
firrtl.module @FullAdder(in %clock: !firrtl.clock, in %reset: !firrtl.uint<1>, in %io_a: !firrtl.uint<1>, in %io_b: !firrtl.uint<1>, in %io_cin: !firrtl.uint<1>, out %io_sum: !firrtl.uint<1>, out %io_cout: !firrtl.uint<1>) attributes {convention = #firrtl<convention scalarized>} {
%io_sum_0 = firrtl.wire {name = "io_sum"} : !firrtl.uint<1>
%io_cout_1 = firrtl.wire {name = "io_cout"} : !firrtl.uint<1>
firrtl.matchingconnect %io_sum, %io_sum_0 : !firrtl.uint<1>
firrtl.matchingconnect %io_cout, %io_cout_1 : !firrtl.uint<1>
// 实例化半加器
%ha0_io_a, %ha0_io_b, %ha0_io_sum, %ha0_io_carry = firrtl.instance ha0 @HalfAdder(in io_a: !firrtl.uint<1>, in io_b: !firrtl.uint<1>, out io_sum: !firrtl.uint<1>, out io_carry: !firrtl.uint<1>)
// 为端口声明临时变量
%ha0.io_sum = firrtl.wire : !firrtl.uint<1>
%ha0.io_carry = firrtl.wire : !firrtl.uint<1>
// 实例化中端口连接逻辑
firrtl.matchingconnect %ha0_io_a, %io_a : !firrtl.uint<1>
firrtl.matchingconnect %ha0_io_b, %io_b : !firrtl.uint<1>
firrtl.matchingconnect %ha0.io_sum, %ha0_io_sum : !firrtl.uint<1>
firrtl.matchingconnect %ha0.io_carry, %ha0_io_carry : !firrtl.uint<1>
// 实例化半加器
%ha1_io_a, %ha1_io_b, %ha1_io_sum, %ha1_io_carry = firrtl.instance ha1 @HalfAdder(in io_a: !firrtl.uint<1>, in io_b: !firrtl.uint<1>, out io_sum: !firrtl.uint<1>, out io_carry: !firrtl.uint<1>)
// 为端口声明临时变量
%ha1.io_a = firrtl.wire : !firrtl.uint<1>
%ha1.io_sum = firrtl.wire : !firrtl.uint<1>
%ha1.io_carry = firrtl.wire : !firrtl.uint<1>
// 实例化中端口连接逻辑
firrtl.matchingconnect %ha1_io_a, %ha1.io_a : !firrtl.uint<1>
firrtl.matchingconnect %ha1_io_b, %io_cin : !firrtl.uint<1>
firrtl.matchingconnect %ha1.io_sum, %ha1_io_sum : !firrtl.uint<1>
firrtl.matchingconnect %ha1.io_carry, %ha1_io_carry : !firrtl.uint<1>
firrtl.matchingconnect %ha1.io_a, %ha0.io_sum : !firrtl.uint<1>
firrtl.matchingconnect %io_sum_0, %ha1.io_sum : !firrtl.uint<1>
// 计算最终进位值
%0 = firrtl.or %ha0.io_carry, %ha1.io_carry : (!firrtl.uint<1>, !firrtl.uint<1>) -> !firrtl.uint<1>
firrtl.matchingconnect %io_cout_1, %0 : !firrtl.uint<1>
}
}
}
Core IR:
module {
hw.module private @HalfAdder(in %io_a : i1, in %io_b : i1, out io_sum : i1, out io_carry : i1) {
%0 = comb.xor bin %io_a, %io_b {sv.namehint = "io_sum"} : i1
%1 = comb.and bin %io_a, %io_b {sv.namehint = "io_carry"} : i1
hw.output %0, %1 : i1, i1
}
hw.module @FullAdder(in %clock : !seq.clock, in %reset : i1, in %io_a : i1, in %io_b : i1, in %io_cin : i1, out io_sum : i1, out io_cout : i1) {
// 与SV中细节举例类似,优化掉了临时变量声明逻辑
%ha0.io_sum, %ha0.io_carry = hw.instance "ha0" @HalfAdder(io_a: %io_a: i1, io_b: %io_b: i1) -> (io_sum: i1, io_carry: i1) {sv.namehint = "ha1.io_a"}
%ha1.io_sum, %ha1.io_carry = hw.instance "ha1" @HalfAdder(io_a: %ha0.io_sum: i1, io_b: %io_cin: i1) -> (io_sum: i1, io_carry: i1) {sv.namehint = "ha1.io_sum"}
%0 = comb.or bin %ha0.io_carry, %ha1.io_carry {sv.namehint = "io_cout"} : i1
hw.output %ha1.io_sum, %0 : i1, i1
}
om.class @FullAdder_Class(%basepath: !om.basepath) {
om.class.fields
}
}
时序逻辑-JohnSon 计数器
scala 源码:
import chisel3._
import chisel3.util._
class JohnsonCounter(n: Int) extends Module {
val io = IO(new Bundle {
val clk = Input(Clock()) // 时钟输入
val reset = Input(Bool()) // 复位信号
val count = Output(UInt(n.W)) // 输出计数值
})
// 创建一个寄存器数组,用于存储计数器状态
val reg = RegInit(0.U(n.W))
// Johnson计数器的逻辑
when(io.reset) {
reg := 0.U // 在复位时清零
}.otherwise {
reg := Cat(~reg(n-1), reg(n-1, 1)) // 将最后一位取反并移位
}
io.count := reg // 输出当前计数值
}
// 用于测试的对象
object JohnsonCounter extends App {
chisel3.Driver.execute(args, () => new JohnsonCounter(4)) // 创建一个4位的Johnson计数器
}
.fir:
FIRRTL version 2.0.0
circuit JohnsonCounter :
module JohnsonCounter :
input clock : Clock
input reset : UInt<1>
output io : { flip clk : Clock, flip reset : UInt<1>, count : UInt<4>}
// 创建一个寄存器数组,用于存储计数器状态
reg reg : UInt<4>, clock with :
reset => (reset, UInt<4>("h0")) @[JohnsonCounter.scala 12:20]
// 当触发复位信号时,将寄存器数组置为0
when io.reset : @[JohnsonCounter.scala 15:18]
reg <= UInt<1>("h0") @[JohnsonCounter.scala 16:9]
else :
// 否则执行取反移位逻辑
node _T = bits(reg, 3, 3) @[JohnsonCounter.scala 18:20]
node _T_1 = not(_T) @[JohnsonCounter.scala 18:16]
node _T_2 = bits(reg, 3, 1) @[JohnsonCounter.scala 18:30]
node _T_3 = cat(_T_1, _T_2) @[Cat.scala 30:58]
reg <= _T_3 @[JohnsonCounter.scala 18:9]
io.count <= reg @[JohnsonCounter.scala 21:12]
FIRRTL IR:
module {
firrtl.circuit "JohnsonCounter" {
firrtl.module @JohnsonCounter(in %clock: !firrtl.clock, in %reset: !firrtl.uint<1>, in %io_clk: !firrtl.clock, in %io_reset: !firrtl.uint<1>, out %io_count: !firrtl.uint<4>) attributes {convention = #firrtl<convention scalarized>} {
%c0_ui4 = firrtl.constant 0 : !firrtl.uint<4>
%io_count_0 = firrtl.wire {name = "io_count"} : !firrtl.uint<4>
firrtl.matchingconnect %io_count, %io_count_0 : !firrtl.uint<4>
// %clock 是时钟信号
// %reset 是复位信号
// %c0_ui4 是复位值
%reg = firrtl.regreset %clock, %reset, %c0_ui4 {firrtl.random_init_start = 0 : ui64} : !firrtl.clock, !firrtl.uint<1>, !firrtl.uint<4>, !firrtl.uint<4>
// 取%reg的第3位
%0 = firrtl.bits %reg 3 to 3 : (!firrtl.uint<4>) -> !firrtl.uint<1>
%1 = firrtl.not %0 : (!firrtl.uint<1>) -> !firrtl.uint<1>
// 取%reg的第1~3位
%2 = firrtl.bits %reg 3 to 1 : (!firrtl.uint<4>) -> !firrtl.uint<3>
// 连接运算
%3 = firrtl.cat %1, %2 : (!firrtl.uint<1>, !firrtl.uint<3>) -> !firrtl.uint<4>
// 根据信号变化为寄存器赋值
%4 = firrtl.mux(%io_reset, %c0_ui4, %3) : (!firrtl.uint<1>, !firrtl.uint<4>, !firrtl.uint<4>) -> !firrtl.uint<4>
firrtl.matchingconnect %reg, %4 : !firrtl.uint<4>
firrtl.matchingconnect %io_count_0, %reg : !firrtl.uint<4>
}
}
}
Core IR:
module {
hw.module @JohnsonCounter(in %clock : !seq.clock, in %reset : i1, in %io_clk : !seq.clock, in %io_reset : i1, out io_count : i4) {
%true = hw.constant true
%c0_i4 = hw.constant 0 : i4
// %4 代表触发时钟信号,但不触发复位信号时的值
// %clock 代表时钟信号
// sync 代表同步逻辑
// %reset 代表复位信号
// %c0_i4 代表触发复位信号时的复位值
%reg = seq.firreg %4 clock %clock reset sync %reset, %c0_i4 {firrtl.random_init_start = 0 : ui64, sv.namehint = "reg"} : i4
%0 = comb.extract %reg from 3 : (i4) -> i1 // 取 %reg的第3位
%1 = comb.xor bin %0, %true : i1
%2 = comb.extract %reg from 1 : (i4) -> i3 // 取 %reg的1~3位
%3 = comb.concat %1, %2 : i1, i3
%4 = comb.mux bin %io_reset, %c0_i4, %3 : i4
hw.output %reg : i4
}
om.class @JohnsonCounter_Class(%basepath: !om.basepath) {
om.class.fields
}
}
上述方言均可在下图(方言转换图)中找到其在 CIRCT 项目中对应的位置,但与验证、仿真、调试相关的方言并没有展现在下图中,如:
- Debug:提供独特的 Operations 来表示调试信息,主要用于追踪硬件电路中的信号变化;
- Verif:用于表示硬件语言中如断言等验证相关的特性;
- Sim:用于表示和仿真器进行互动的特性,如 SystemVerilog 中的
$value$plusargs
; - LTL:用于表示 SystemVerilog 中的线性时序逻辑,如 sequence、property 等。
总的来说,方言是由 Operations、Types、Attributes 组成的集合。不同的方言代表着各自独特的功能,同时为开发者提供了在方言层进行特定优化的灵活性。通过这种方式,方言能够有效地支持多样化的应用需求和优化策略。
定义方言
在前文中,我们简要介绍了一些方言及其用途,并提到方言这一概念源于 MLIR。接下来,我们将深入探讨如何在 CIRCT 中使用 TableGen 定义一个新的方言。这种方法的主要优势在于,开发者可以专注于方言的逻辑和功能,而无需过多关注底层实现细节。这不仅能够显著减少代码量,还能提高开发效率,确保生成的代码结构清晰且易于维护,从而加速新方言的开发和集成过程。
TableGen:一种通用语言,带有维护特定领域信息记录的工具;它通过自动生成所有必要的 c++样板代码来简化定义过程,在更改方言定义的各个方面时显著减少了维护负担,并且还提供了额外的工具,如文档生成。
假设我们要定义一个名为 Foo 方言,其中还定义了 IntType、FVIntegerAttr、ConstantOp,以下是需要新增的文件夹以及实现细节举例:
circt/include/circt/Dialect/Foo
:定义自己的方言名、Types、Attributes 以及 Operations;
# FooDialect.td //此文件用于定义Foo方言的名字、描述等基本信息
include "mlir/IR/DialectBase.td" // 定义方言是必须引入的头文件,其中定义了基类class Dialect
def FooDialect : Dialect {
let name = "foo";
let cppNamespace = "::circt::foo";
let summary = "一句话说明Foo方言的用途";
let description = [{概述Foo方言的设计理念或者展开说明Foo方言的用途}];
let extraClassDeclaration = [{
void registerTypes();
void registerAttributes();
}];
... // 其余未提到的字段请查看引入的头文件
}
# FooTypes.td // 自定义所需Types
include "circt/include/circt/Dialect/Foo/FooDialect.td"
include "mlir/IR/AttrTypeBase.td" // 此文件定义了有关Types的基类class TypeRef
class FooTypeDef<string name> : TypeDef<FooDialect, name> {}
def IntType : FooTypeDef<"Int"> {
let mnemonic = "int"; // 将以 !foo.int 的形式打印在终端
let summary = "同上";
let description = [{同上}];
let parameters = (ins); // 如果是定义数组等复杂类型,需要用到此字段
let assemblyFormat = [{}]; // 类型的输出格式将按照assemblyFormat中的形式打印在终端
}
# FooAttributes.td // 自定义所需Attributes
include "circt/include/circt/Dialect/Foo/FooDialect.td"
include "mlir/IR/AttrTypeBase.td" // 此文件定义了有关Attributes的基类class AttrDef
def FVIntegerAttr : AttrDef<FooDialect, "FVInteger"> { // FV = four-valued : 0, 1, x, z
let mnemonic = "fvint"; // 生成的IR并不会显示的打印此信息
let summary = "同上";
let description = [{同上}];
let parameters = (ins "FVInt":$value); // FVInt是CIRCT中通用的类型,用于表示硬件电路中的四值态
let hasCustomAssemblyFormat = 1; // 开启此字段代表着需要在circt/lib/Dialect/Foo/FooAttributes.cpp中实现其定义
}
# FooOps.td // 自定义所需Operations
include "circt/lib/circt/Dialect/Foo/FooAttributes.td"
include "circt/lib/circt/Dialect/Foo/FooDialect.td"
include "circt/lib/circt/Dialect/Foo/FooTypes.td"
include "mlir/IR/OpBase.td" // 其中定义了有关Operations的特征,如ConstantLike, Commutative, SameOperandsAndResultType
include "mlir/Interfaces/SideEffectInterfaces.td" // 标识某个Op是否会对内存进行操作,如MemRead, RecursiveMemoryEffects, Pure(无副作用)
def ConstantOp : FooOp<"constant", [Pure, ConstantLike]> {
let summary = "同上";
let arguments = (ins I32Attr:$value); // 表示需要传入一个32位的Attribute,否则会报错
let results = (outs IntType:$result); // 返回值类型为自定义的IntType
let assemblyFormat = [{ $value attr-dict `:` type($result)}]; // 这里不展开讨论细节
let hasFolder = 1; // 需要在circt/lib/Dialect/Foo/FooOps.cpp中实现其定义
}
circt/lib/Dialect/Foo
:初始化方言,或者在定义 Operations 时,当使用let hasCustomAssemblyFormat = 1
时,需要在这个文件夹中实现相应代码等;
# FooDialect.cpp // 初始化方言
#include "circt/include/circt/Dialect/Foo/FooOps.h"
void FooDialect::initialize() {
// Register types and attributes.
registerTypes();
registerAttributes();
// Register operations.
addOperations<
#define GET_OP_LIST
#include "circt/build/include/circt/Dialect/Foo/Foo.cpp.inc"
>();
}
circt/lib/Dialect/Foo/Transforms
:在方言内部进行优化时,将对应的优化 Pass 实现写在此文件夹中;circt/test/Dialect/Foo
:测试定义的 Type、Attributes 是否符合预期;circt/lib/Conversion/FooToXX
:此文件夹用于实现从 Foo 方言到其他方言的转换。
上述代码是核心实现部分,相关的头文件以及 CMakeLists 并未一一列举,这些头文件主要用于引入必要的依赖项,CMakeLists 主要用于辅助生成 C++代码、文档以及添加依赖等。此外,在实际开发中,可能还需要定义 CAPI 接口等,这要求在 circt/include/circt-c/Dialect
目录中新增 Foo.h
文件。关于这一点,我们在此不作过多详述。
应用场景
在 CIRCT 系列教程的另一篇文章CIRCT - 基于 MLIR 的电路编译器和工具链中,我们简要介绍了 CIRCT 的发展历程和 MLIR 起源。CIRCT 项目仅是 MLIR 框架下众多实际应用场景之一,具体应用包括:
- 高性能计算:如 PolyBlocks,这是一个基于 MLIR 的高性能端到端编译器,适用于深度学习和非深度学习计算,支持 JIT 和 AOT 编译;如 Polygeist,可以自动将以一种编程模型 (CUDA) 编写的程序转换为基于 Polygeist/MLIR 的另一种编程模型(CPU 线程);
- 机器学习 & 深度学习:如 IREE、OpenXLA 和 Torch-MLIR,能够将 PyTorch、JAX 和 TensorFlow 等主流机器学习框架映射到 MLIR,并进一步降低到目标硬件;
- 量子计算:如 Catalyst,该项目是一个用于 PennyLane 的 AOT/JIT 编译器,支持混合量子程序加速,并具备完整的自动微分支持、动态量子编程模型;
- 代码生成:如 MLIR-EmitC,提供将机器学习模型转换为 C++ 代码的方法;
- 硬件验证:如 BTOR2MLIR,该项目支持使用软件验证方法解决以 Bᴛᴏʀ2 格式表示的硬件验证问题,并促进形式化验证领域的研究,结果显示其性能与现有方法具有竞争力;
为工业界带来的变化
MLIR 的方言机制为工业界带来了显著的变化,主要体现在以下几个方面:
- 灵活性与扩展性:允许开发者根据特定需求定义新的 Operations、Types、Attributes。这种灵活性使得 MLIR 能够适应多种不同的计算模型和应用场景,从而满足各种领域的需求。
- 多层级中间表示:可以在不同的抽象层次上进行操作和优化。通过这种结构,开发者可以逐步将高层次的描述降低到特定硬件架构上,提升了代码的可重用性和可维护性。
- 简化的转换与优化:提供了便捷的机制来支持同一方言内部以及不同方言之间的转换。这使得在编译过程中可以轻松实现各种优化策略,而无需重写大量代码。
- 统一的命名空间:方言在同一命名空间中定义,确保了不同方言之间的互操作性。这种设计避免了命名冲突,并简化了跨方言操作的实现。
- 丰富的工具支持:MLIR 生态系统提供了多种内置工具和库,支持方言的创建、分析和优化。这些工具极大地提高了开发效率,使得开发者能够专注于功能实现而非底层细节。
- 兼容 LLVM:MLIR 包括 LLVM IR 方言,允许开发者将自定义方言转换为 LLVM IR,从而利用现有的 LLVM 工具链进行后端代码生成。这种兼容性为开发者提供了更广泛的选择和灵活性。
总结
本文首先阐述了方言的本质,即由 Operations、Types 和 Attributes 构成的集合,不同的方言代表着各自独特的功能。通过具体实例,详细介绍了 CIRCT 中部分方言的用途,以加深读者对 CIRCT 的理解。接着,文章提供了如何定义自定义方言的示例,帮助读者掌握这一过程。最后,我们探讨了 MLIR 框架的应用场景,强调了 MLIR 的方言机制不仅提升了编译器开发的效率,还促进了软硬件协同设计,为工业界提供了更高效、更灵活的编译解决方案。
参考资料:
[1] CIRCT官方文档
[2] Exciting times at the intersection of Compilers and Applied Cryptography: Cairo and MLIR
[3] IREE源码仓库
[4] MLIR官方文档
[5] OpenXLA源码仓库
[6] Torch-MLIR源码仓库