type
status
date
slug
summary
tags
category
icon
password
由于工作需要学习Chisel,准备对Chisel的官方说明文档进行翻译工作,以此来推进自己的学习。欢迎各位批评指正。

Chisel 数据类型(Chisel Data Types)

Chisel 的数据类型用来指代保持在状态元素中或在导线上流动的值。虽然硬件设计是在操作二进制数的向量,但更抽象的表示它们的值使得规范更清晰,帮助工具生成优化更好的电路。在 Chisel 中,位的原始集合用 bits 类型来表示。有符号和无符号整数被认为是浮点数的子集,分别使用 SIntUInt 来表示。有符号浮点数,包括整数用二进制补码形式表示。布尔值用 Bool 类型表示。请注意这些类型和 Scala 的内置格式,比如IntBoolean 是不同的。
另外,Chisel 定义了 Bundels 来表示有命名字段的值的集合(类似于其他语言中的 structs ),并使用 Vecs 来表示可索引的值的集合。
Bundels 和 Vecs 将在下一节介绍。
常量和字符值使用传递给以下构造函数的 Scala 的整数和字符串来表示:
为了可读性可以在长字符串中使用下划线作为分隔符,但在生成值时,下划线会被忽略,比如:
默认情况下,Chisel 的编译器为了将每个常量设置为需要保持的最小为鼠,包括符号位和有符号类型。位宽可以直接指定,如下所示。(.W 被用来将 Scala Int 转换为一个 Chisel 宽度)
对于 UInt 类型,所需位宽的值将会被 0 拓展;对于 UInt 类型,所需位宽的值将会被符号拓展。如果给出的位宽小于存储参数值的位宽,则会生成一个 Chisel error。

类型转换(Casting)

我们能够在Chisel中进行类型转换:
注意:显式指定了宽度的 asUInt/asSInt 不能被用于 Chisel 数据类型的转换。不接受任何位宽参数,因为在连接对象时,Chisel 会根据需要自行填充和截断。
我们也能对时钟进行类型转换,但你应当小心这一点,应为时钟需要特别注意(尤其是在 ASIC 中):

模拟/黑盒类型(Analog/BlackBox type)

(试验类型, Chisel 3.1+)
Chisel 支持能够用来支持Chisel任意网络的 Analog 类型(等同于 Verilog 中的 inout 类型)。这包括模拟导线,三态/双向导线和电源网络(包含合适的注释)
Analog 是一种无方向类型,所以可以用 attach 操作符将多个 Analog 网络连接在一起。也可以使用 <> 连接一次 Analog 网络,但连接多次是非法操作。

Bundles and Vecs

BundleVec 是允许用户使用其他类型的聚合来拓展 Chisel 数据类型的类
Bundles 将一些可能不同类型的命名字段集合进一个连贯的单元中,很像 C 语言的结构体。用户通过将他们的类定义为 Bundle 的子类来定义自己的 Boundles。
你可以使用试验中的 Bundle Literals 功能来创建 literal Bundle。
Scala 约定使用 UpperCamleCase 作为命名规范,我们建议你在 Chisel 代码中保持这一规范。
Vecs 创建元素的可索引的向量,结构如下所示
(注意我们指定了 Vec 元素的个数,类型,还指定了 SInt 的位宽)
这些基类(SInt, UInt, 和 Bool)还有聚合类(BundleVec)都继承自一个公共的超类,Data。在硬件设计中,所有最终从 Data 继承的对象都能表示为位向量。
Bundles 和 Vecs 可以任意连接来构成复杂的数据结构:
注意 Chisel 的基类和聚类在创建实例时不需要使用 new,但新的用户数据类型需要。可以定义 Scala apply 构造函数使用户数据类型也不需要使用 new,就像函数构造 - Function Constructor 中讲述的

翻转 Bundles - Flipping Bundles

Flipped() 函数递归的翻转 Bundle/Record 中的所有元素。这在创建一个互相连接的双向接口时非常有用(比如 Decoupled)。请看下面的示例:
这会生成下面的 Verilog:

MixedVec

(Chisel 3.2+)
Vec 的所有元素必须有相同的参数化,如果我们想要创建一个元素有相同类型但不同参数化的 Vec,我们可以使用 MixedVec
我们还可以通过编程来在 MixedVec 里创建类型:

组合电路(Combinational Circuits)

电路在 Chisel 中以节点图的形式保存。每个节点是一个 0 输入或多输入,驱动一个输出的硬件操作符。之前介绍的字符值是一种退化的节点,他没有输入并且驱动一个常量输出。创建节点并将其连接在一起的方法是使用文本表达式。比如,我们可以用下面这个例子表达一个简单的组合逻辑电路:
这个格式看起来很熟悉,用 &| 分别表示按位与和按位或,~ 表示按位取反。 ad 表示某种线宽(未指定)的命名导线。
任何简单表达式都可以转换成一个电路树(circuit tree),导线作为树叶,运算符作为内部节点。表达式的电路最终的输出从树根部的操作符得出,在这个例子中,是按位或(|)。
简单表达式可以建立一个树状电路,但构建任意一个有向无环图(directed acyclic graphs, DAGs)电路,我们需要描述扇出(fan-out)。在 Chisel 中,我们通过命名一个包含子表达式的导线来实现,之后我们可以多次调用这个子表达式。我们在 Chisel 中声明一个变量来命名一个导线。例如,对于选择表达式,在下面的多路选择器描述中,什么是被是用了两次的:
val 是 Scala 的关键词,用来声明值不能被改变的变量。在这里用它命名 Chisel 导线,sel,保持第一个按位或操作符的输出,这样这个输出就可以在第二个表达式中被多次使用。

导线(Wires)

Chisel 同样支持将导线作为硬件节点,给其赋值或连接其他节点 Chisel also supports wires as hardware nodes to which one can assign values or connect other nodes.
注意导线的最后一次连接才生效。例如,下面两个 Chisel 电路是等价的:

操作符(Opretors)

Chisel 定义了一个硬件操作符集合
操作符
说明
位操作符
有效类型: SInt, UInt, Bool
val hiBits = x & "h_ffff_0000".U
按位与
val invertedX = ~x
按位取反
val flagsOut = flagsIn \| overflow
按位或
val flagsOut = flagsIn ^ toggle
按位异或
位规约操作
有效类型: SInt, UInt 返回类型: Bool
val allSet = x.andR
x 的每一位相与
val anySet = x.orR
x 的每一位相或
val parity = x.xorR
x 的每一位相异或
相等判断
有效类型: SInt, UInt, Bool 返回类型: Bool
val equ = x === y
相等
val neq = x =/= y
不相等
移位
有效类型: SInt, UInt
val twoToTheX = 1.S << x
逻辑左移
val hiBits = x >> 16.U
右移(UInt 时逻辑移位,SInt 时算术移位)
位域操作
有效类型: SInt, UInt, Bool
val xLSB = x(0)
提取一位, 低位索引为 0
val xTopNibble = x(15, 12)
从结束位到开始位提取位字段
val usDebt = Fill(3, "hA".U)
多次复制一个位字符串
val float = Cat(sign, exponent, mantissa)
拼接位字段,第一个参数在左边
逻辑操作符
有效类型: Bool
val sleep = !busy
逻辑非
val hit = tagMatch && valid
逻辑与
val stall = src1busy || src2busy
逻辑或
val out = Mux(sel, inTrue, inFalse)
sel 为布尔变量的两输入选择器
算术操作符
有效类型: SInt and UInt,对数字有效
val sum = a + b or val sum = a +% b
加法(无宽度扩展)
val sum = a +& b
加法(有宽度扩展)
val diff = a - b or val diff = a -% b
减法(无宽度扩展)
val diff = a -& b
加法(有宽度扩展)
val prod = a * b
乘法
val div = a / b
除法
val mod = a % b
取模
算术比较
有效类型: SInt and UInt,对数字有效
val gt = a > b
大于
val gte = a >= b
大于等于
val lt = a < b
小于
val lte = a <= b
小于等于
Scala 语言约束了我们对于操作符名称的选择。我们不得不使用三等号 === 表示相等,用 =\= 表示不相等来保证原生的 Scala 相等操作符可以使用 {% endnote %}
Chisel 操作符的优先级没有定义为 Chisel 语言的一部分。实际上,他是由电路的评估等级决定的,天然遵循 Scala 操作符的优先级。如果对优先级存在怀疑,请使用括号。
{% note default %} Chisel/Scala 操作符的优先级与 Java 和 C 的优先级相似但不相同。Verilog 和 C 有相同的优先级,但 VHDL 没有。Verilog 的逻辑运算符有更高的优先级,但 VHDL 的那些操作符有相同的优先级,由左到右进行运算。 {% endnote %}

位宽推断(Width Inference)

Chisel 提供了位宽推断来减少设计工作量。鼓励用户手动指定端口和寄存器的位宽以避免任何意外情况。但在其他时候,FIRRTL 编译器会推断出为指定的位宽
对于未指定位宽的电路组件,FIRRTL 编译器会推断出能够保持其合法传入连接的最小宽度。这里的含义是,在 Chisel 的赋值语句中,推理是从右向左进行的,即从左手边到右手边。如果一个组件没有传入链接,又未指定宽度,会抛出一个错误指出无法推断连接。
对于模块的未指定位宽的输入端口,推断的位宽是保证模块所有实例的传入连接合法的最小宽度。地型多路转换器(ground-typed multiplexor)表达式的宽度是其两个相应输入宽度中的最大值。对于复用聚合类型表达式(multiplexing aggregate-typed expressions),每个叶元素的结果宽度是其对应的两个输入叶元素的子元素宽度的最大值。条件有效表达式的宽度是其输入表达式的宽度。
下表定义了硬件操作符的输出宽度:
| operation | bit width |
| :-: | :-: |
| z = x + y or z = x +% y | w(z) = max(w(x), w(y)) |
| z = x +& y | w(z) = max(w(x), w(y)) + 1 |
| z = x - y or z = x -% y | w(z) = max(w(x), w(y)) |
| z = x -& y | w(z) = max(w(x), w(y)) + 1 |
| z = x & y | w(z) = max(w(x), w(y)) |
| z = Mux(c, x, y) | w(z) = max(w(x), w(y)) |
| z = w * y | w(z) = w(x) + w(y) |
| z = x << n | w(z) = w(x) + maxNum(n) |
| z = x >> n | w(z) = w(x) - minNum(n) |
| z = Cat(x, y) | w(z) = w(x) + w(y) |
| z = Fill(n, x) | w(z) = w(x) * maxNum(n) |
w(z) 是导线 z 的线宽,& 规则适用于所有位逻辑操作符
给定一个以未指定宽度元素(最常见的是顶级输入)开头的连接路径,那么编译器将抛出一个异常,指示某个宽度不可更改。
一个常见的 “gotcha” 来自于用运算符 +- 截断加法和减法。希望结果保持加法或减法的完全扩展精度的用户应该使用扩展运算符 +&-&
默认的截断操作来源于 Chisel 作为一个微处理器设计语言的历史。

函数抽象(Functional Abstraction)

我们可以定义一个函数来表达重复的逻辑片段,这样我们可以在之后的设计中多次重用。例如,我们可以将我们之前用到的简单组合逻辑块包装为下面的格式
在这里 clb 是一个使用 a, b, c, d 作为参数并返回一个布尔电路的值的函数。关键字 def 是 Scala 的一部分,引入了一个函数定义,每个参数后紧跟一个冒号,然后是参数类型,函数的返回值类型在参数列表后的冒号后面。等号 = 分割了函数的参数列表和函数定义。
我们可以在之后的其他电路中这样使用这个函数:val out = clb(a, b, c, d)

端口(Ports)

端口是用来连接硬件组件的接口。端口只是指为其成员指定了方向的 Data 对象。
Chisel 提供了端口构建函数使得可以在构建对象时为其添加方向(输入或输出)。基类端口构造函数将端口的类型封装在 Input 或者 Output 中。
一个端口声明的示例如下所示:
在定义了 Decoupled 之后, 它变成了一个新的类型,可以根据需要作为模块的结构或者作为导线的集合。
通过将方向装入对象声明,Chisel 能够提供强大的布线结构,后面将会进行描述。

检查模块端口(Inspecting Module ports)

(Chisel 3.2+)
chisel 3.2 提出了 DataMirror.modulePorts,可以用来检查任何 Chisel 模块的 IO(这包括 import chisel3._import chisel._,以及每个模块中的黑盒)。这是使用这个 API 的示例:

模块(Modules)

Chisel 模块在定义生成电路的层级结构上和 Verilog 非常相似。
分层模块命名空间可在下游工具中访问,以帮助调试和物理布局。用户定义的模块定义为一个类,该类有以下特征: - 继承自 Module, - 包含至少一个封装在模块的的 IO() 方法中的接口(传统上存储在名为 io 的端口字段中),以及 - 在其构造函数中将子电路连接在一起 像这个示例一样定义你的 2 输入数据选择器模块:
模块的接口是一个 Bundle 形式的端口集合,通过一个 io 字段定义。对于 Mux2io 被定义为具有四个字段的集合,每个选择器端口一个字段。
定义体中用到的赋值操作符 := 是 Chisel 中的特殊操作符,可以将左边的输入与右边的输出相连接。

模块层次(Module Hierarchy)

我们现在可以创建电路的层级结构,即通过小的模块来构建大的模块。例如,我们可以将三个 2 输入数据选择器连接在一起,用 Mux2 模块构成 4 输入数据选择器:
我们又一次定义了像 io 这样的模块接口并连接了输入和输出。在这个例子中,我们创建了三个 Mux2 子模块,用 Module 的构造函数和 Scala 的关键字 new 创造了一个新的对象。之后我们将他们互相连接起来,并连接到 Mux4 接口的端口上。
注意:Chisel 模块有一个隐式的时钟(命名为 clock)和一个隐式的复位(命名为 reset)。Chisel 提供了 RawModule 来创建没有隐式时钟和复位的模块。

RawModule

RawModule 是一个不提供隐式时钟和复位的模块。这在希望将 Chisel 模块与指定命名的时钟和复位连接时可能会很有用。
我们可以用它来代替 Module 的使用:
在上面的例子中,RawModule被用来改变 SlaveSpi 复位信号的极性。事实上,Chisel 模块中默认的复位时高电位激活,用 withClockAndReset(clock, !rstn) 我们能够在整个设计中使用低电位触发的复位。
时钟按照原样接线,但如果需要,RawModule 可以和 BlackBox 混合使用,比如连接差分时钟输入。 The clock is just wired as is, but if needed, RawModule can be used in conjunction with BlackBox to connect a differential clock input for example.

时序电路(Sequential Circuits)

Chisel 支持的最简单形式的状态元素是一个上升沿触发的寄存器,可以被如下实例化
这个电路有的输出信号是延迟一个周期的输入信号 in 的复制。注意我们不需要指定这个寄存器的类型,它在这样实例化时,会通过他的输入自动推断出来。在当前版本的 Chisel 中,时钟和复位是被隐式包含在需要处的全局信号。
注意未指定初始值的寄存器在触发复位信号时不会发生变化。
通过寄存器,我们可以很快的定义许多有用的电路结构。例如,一个上升沿探测器,它可以结构一个布尔信号 in 并在当前值为真,之前值为假时输出 true,如下所示:
计数器时一种很重要的时序电路。构建一个向上计数器,计数值最大值 max,然后回到 0,我们写道:
计数器寄存器是在计数器函数中创建的,其重置值为 0(宽度足够大以保持最大值),当电路的全局重置被触发时,寄存器将被初始化。计数器中对 x 的 := 赋值连接了一个组合电路,该电路递增计数器值,当它达到最大值将返回 0。请注意,当 x 出现在赋值的右侧时,它的输出被引用,而当 x 显示在赋值的左侧时,其输入被引用。计数器可用于构建许多有用的时序电路。例如,我们可以通过在计数器达到 0 时输出true来构建脉冲发生器:
方波发生器可以通过脉冲串来实现,在每个脉冲翻转输出:

存储(Memories)

Chisel 为创建只读存储和读/写存储提供了支持。

ROM

用户可以通过 VecInit 构建一个 Vec 来定义一个只读存储。VecInit 能够接受 Data 字面值,也能接收初始化 ROM 的 Seq[Data]
例如,用户可以创建一个初始化为 1,2,3,4,8 的小 ROM 并使用计数器作为地址生成器来遍历所有值:
我们可以用如下初始化的 ROM 创建一个 n 值正弦查找表
在这里 amp 用来缩放存储在 ROM 中的定点值

读写存储(Read-Write Memories)

存储在 Chise 中被特殊处理因为存储的硬件实现差异很大。比如 FPGA 的存储和 ASIC 的存储完全不同。Chisel 定义了存储的抽象,可以映射到任何简单的 Verilog 行为描述,也可以映射到 IP 厂商或代工厂提供的外部存储生成器生成的存储模块。
### 同步读 RAM SyncReadMem:同步读取,同步写入
Chisel 对于同步读,同步写存储有一个叫 SyncReadMem 的结构。SyncReadMem 可能会被组成一个技术 SRAMs(而不是寄存器组)
如果在同一时钟沿上同时写入和顺序读取同一存储器地址,或者如果清除了顺序读取使能,则读取数据未定义。
读数据端口的值不能保证在下一个读取周期之前不改变。如果这是一个期望的行为,必须添加外部逻辑来保持上次读取的值。

读端口/写端口(Read port/write port)

SyncReadMems 是使用 UInt 索引创建的。有一个读端口和一个写端口的 1024 大小的 SRAM 可以被如下表达:
下面是带掩码的一个写端口/一个读端口 SyncReadMem 的波形示例。信号名称与为 SyncReadMem 生成的导线名称不同。通过掩码的操作,也有可能生成具有以下行为的多个RTL阵列。
notion image
read-write ports example waveform

单端口(Single-ported)

当读和写条件在同一时间链中互斥时,可以推断出单端口SRAM:
这里的 DontCare 是为了让 Chisel 的未连接线检测意识到边写边读是未定义的。)
以下是具有掩码的单个读/写端口波形的示例(同样,生成的信号名称和阵列数量可能不同):
notion image
read/write ports example waveform
单端口SRAM也可以通过使用readWrite调用显式生成,该调用产生一个单读/写访问器,如下所示:

Mem: 异步读取,同步写入

Chisel通过 Mem 结构支持随机存取存储器。对 Mem 的写入是组异步读取、同步写入。由于现代技术(FPGA、ASIC)中的大多数 SRAM 往往不再支持异步读取,因此这些 Mem 可能会被合成为寄存器组。
创建上面示例的异步读取版本只需将 SyncReadMem 替换为 Mem

掩码(Masks)

Chisel 存储器还支持子字的写掩码。如果存储的数据类型是向量,Chisel 将推断掩码。要推断掩码,需要指定创建写入端口的写入函数的掩码参数。如果设置了相应的掩码位,则写入给定的掩码长度。例如,在下面的例子中,如果掩码的第 0 位为真,它将把数据的较低字节写入相应的地址。
这是一个读写端口的掩码示例:

存储初始化(Memory Initialization)

Chisel存储器可以从外部二进制或十六进制文件初始化,该文件发出用于合成或模拟的正确Verilog。有多种初始化模式。
有关更多信息,请查看关于加载内存功能的实验文档

接口和连接(Interfaces & Connections)

对于更复杂的模块,在定义模块的IO时定义和实例化接口类很有用。首先,也是最重要的,接口类促进重用,允许用户以有用的形式捕获一次性的通用接口。
第二,接口允许用户通过支持生产者和消费者模块之间的批量连接显著的减少布线。最后用户可以在一处地方对大型接口进行更改,减少添加或删除接口部分时的更新数量。
注意 Chisel 有一些内置的标准接口,应该尽量使用这些接口实现相互操作(比如,Decoupled)

端口:子类和网络(Ports: Subclasses & Nesting)

像我们之前看到的,用户可以通过定义 Bundle 的子类定义他们自己的接口。例如,用户可以如下定义一个握手数据的简单连接:
我们可以扩展一个奇偶校验位。
通常,用户可以通过继承将他们的接口组织进层次结构中。
从那里,我们可以通过将两个 PLink 嵌套到一个新的 FilterIO 中来定义滤波器接口: From there we can define a filter interface by nesting two PLinks into a new FilterIO bundle:
在这里递归的翻转了接口的方向,将输入变为输出,输出变为输入。
我们现在可以使用 module 定义一个滤波器类。
在 io 域中包含 FilterIO。

Bundle Vectors

除了单个元素之外,元素的向量形成了更丰富的层次接口。例如,为了创建具有输入向量的交叉开关,产生输出向量,并由 UInt 输入选择,我们使用 Vec 构造函数:
其中 Vec 将大小作为第一个参数,将返回端口的块作为第二个参数。

批量连接(Bulk Connections)

一旦我们定义了接口,我们就能通过 MonoConnect 操作符(:=)或者 Biconnect 操作符(<>)连接到他。

MonoConnect Algorithm

MonoConnect.connect,或者 :=,按元素执行单向连接。
注意这不是可交换的。在调用之前已经确定了一个明确的源和汇。
这个连接操作将向下递归到左侧的 Data(带有右侧的 Data)。如果通过左侧的移动无法在右侧匹配,则会引发异常。右侧允许有额外的字段。Vec 的大小必须完全相同。
注意 LHS 元素必须是可写的,因此其中一个必须包含:
  • 是一个内部可写节点(Reg or Wire)
  • 是一个当前模块的输出
  • 是当前模块的子模块的输入
注意 RHS 元素必须是可读的,其中一个必须包含:
  • 是一个内部可读节点(Reg, Wire, Op)
  • 是字面量
  • 是当前模块的端口或当前模块子模块的端口

BiConnect Algorithm

BiConnect.connect,或者 <>,按元素执行双向连接。请注意,自变量是左和右(而不是源和汇),因此目的是使运算具有交换性。连接操作将向下递归到左侧的Data(带有右侧的Data)。如果左侧的移动无法在右侧匹配,或者右侧有额外的字段,则会引发异常。
{% note default %} 注意:我们强烈建议使用 Connectable 操作符编写代码而不是使用 <>。 {% endnote %}
使用双向连接操作符 <>, 我们可以将两个滤波器组合成一个滤波器:
双向批量连接操作符 <> 将同名的叶端口相互连接。Bundle 的 Scala 类型不需要匹配。如果一个命名的信号在另一侧不存在,Chisel 会像下面的示例一样报错:
下面我们会看到这个示例的报错:
双向连接应仅与定向元素(如 IO)一起使用,例如不支持连接两条线,因为 Chisel 不一定能自动确定方向。例如,放置两条临时电线并在此处连接它们是行不通的,即使从端点可以知道方向:
下面我们会看到这个示例的报错:
有关详细信息和信息,请参阅深入了解连接操作符
注意:当使用 Chisel._(兼容模式)而不是 chisel3._ 时,:= 操作符以类似于 <> 的双向方式工作,但不完全相同。

The standard ready-valid interface (ReadyValidIO / Decoupled)

Chisel 为 ready-valid 接口提供了标准接口。就绪有效接口由就绪信号、有效信号和一些存储在位中的数据组成。就绪位表示消费者已准备好消费数据。有效位指示生产者在位上具有有效数据。当 readyvalid 都被断言时,就会发生从生产者到消费者的数据传输。提供了一个方便的方法 fire,如果 readyvalid 都被断言,它就会被断言。
通常,我们使用函数 Decoupled() 将任何类型转换为就绪有效接口,而不是直接使用 ReadyValidIO。
  • Decoupled(...) 创建一个生产者/输出就绪有效接口(即位是一个输出)。
  • Flipped(Decoupled(...)) 创建一个消费者/输入就绪有效接口(即位是输入)。
查看以下示例 Chisel 代码,以更好地理解生成的内容:
DecoupledIO 是一个就绪有效的接口,其约定不保证解除断言就绪或有效或位的稳定性。这意味着 readyvalid 也可以在没有数据传输的情况下取消断言。
IrrevocableIO 是一个 ready-valid 接口,其约定是在 valid 被断言和 ready 被取消断言时,位的值不会改变。此外,消费者应在 ready 为高且 valid 为低的周期后保持 ready 断言。请注意,不可撤销的约束只是一个约定,不能由接口强制执行。 Chisel 不会自动生成检查器或断言来强制执行不可撤销的约定。

BlackBoxes

Chisel 的 BlackBoxes 被用来实例化外部定义的模块。这个结构在使用不能被 Chisel 描述的硬件结构和连接到 FPGA 或其他没有使用 Chisel 定义的模块时很有用。
被定义为 BlackBox 的模块会在生成的 Verilog 中被实例化,但是不会生成定义模块行为的代码。
Module 不同,BlackBox 没有隐式时钟和复位。BlackBox 的时钟和复位端口要显示声明并连接到输入信号。在 IO Bundle 中声明的端口将会被生成为所需的名称(没有 io_ 前缀)。

参数化

Verilog 参数可以作为参数传递给 BlackBox 构造函数。
例如,考虑在 Chisel 设计中实例化 Xilinx 差分时钟缓冲器 (IBUFDS):
在 chisel 生成的 Verilog 代码中,IBUFDS 将会被实例化为:

提供实现的 BlackBoxes

Chisel 提供了以下方式来提供黑盒底层的代码。考虑以下将两个实数相加的黑盒。这些数字在 chisel3 中表示为 64 位无符号整数。
这个例子实现的例子如下面的 Verilog 代码描述:

Verilog 在资源文件中的 BlackBoxes

为了将上述代码传递到后端模拟器,chisel3 提供了基于 chisel/firrtl 注释系统的如下工具。在声明中添加 HasBlackBoxResource 特征,然后调用一个函数来告知系统在哪里可以找到 verilog。这个模块如下所示:
上述 verilog 代码放置在 real_math.v 文件中。什么是资源文件?它来自于将文件保存在项目中的 java 约定,这些文件会自动包含在库发行版中。在一个典型的 Chisel3 工程中(参考 chisel-template),这会是源文件层次结构中的一个目录:src/main/resource/real_math.v

内联 Verilog 的 BlackBoxes

我们也可以直接把 Verilog 代码放入 scala 源文件。使用 HasBlackBoxInline 替换掉 HasBlackBoxResource,然后用 setinline 替换掉 setResource。代码如下所示:
该技术会将内联 verilog 复制到目标目录的 BlackBoxRealAdd.v 文件中。

引擎之下

这种将 verilog 内容传递到测试后端的机制是通过 chisel/firrtl 注释实现的。 inline 和 resources 这两种方法是通过 setInlinesetResource 方法调用创建的两种注释。这些注释会传递给 chisel 测试人员,然后 chisel 测试人员会将它们传递给 firrtl。默认的 firrtl verilog 编译器有一个过程,可以检测注释并将文件或内联测试移动到构建目录中。对于添加的每个唯一文件,转换会向文件 black_box_verilog_files.f 添加一行,该文件将添加到为 verilator 或 vcs 构建的命令行中,以通知它们要查找的位置。dsptools 项目是使用此功能构建基于黑匣子的实数模拟测试器的一个很好的例子。

Chisel 枚举(ChiselEnum)

ChiselEnum 类型经常被用来减少编码选择器,操作码和功能单元操作的出错机会。与 Chisel.util.Enum 相比,ChiselEnumData 的子类,这意味着它可以用于定义 Bundle 中的字段,包括 IO 中的字段。

功能和示例

下面我们看到 ChiselEnum 被用作 RISC-V 内核的多路复用器选择信号。虽然不需要将对象包装在包中,但强烈建议这样做,因为它允许更轻松地在多个文件中使用该类型。
这里我们可以看到选择器使用 AluMux1Sel 来选择不同的输入。
ChiselEnum 也允许用户像如下所示的方式给 Value(...) 直接传递 UInt 来设定值。注意,每个 Value 的大小必须严格大于前一个。
用户可以“跳转”到某个值,并通过传递起点然后使用常规值定义来继续递增。

类型转换

你可以用 .asUInt 把枚举类型转换为 UInt
你也可以将 UInt 传递给 ChiselEnum 的 apply 方法来将其转换为枚举类型:
但是,如果在 UInt 可能遇到的枚举值中存在未定义状态时从 UInt 转换为枚举类型,您将看到如下警告:
(请注意,作为我们的文档生成流程的产物,枚举的名称很难看,在正常使用中它会更干净)。
您可以通过使用 .safe 工厂方法来避免此警告,该方法除了指示枚举是否处于有效状态的 Bool 之外,还返回强制转换枚举类型:
现在将不再有警告。
你也可以用 suppressEnumCastWarning 来抑制警告。这通常用于将 UInt 转换为包含枚举类型的 Bundle 类型,这里 UInt 是已知的合法的。

测试

枚举值的类型是 <ChiselEnum Object>.Type,它可用于将值作为参数传递给函数(或任何其他需要类型注释的时候)。对枚举值调用 .litValue 将返回该对象的 BigInt 整数值。
枚举类型也定义了一些简便的方法来使用 ChiselEnum 值。例如,仍然使用 RISC-V 的操作码示例,可以使用 .isOneOf 方法轻松的创建仅在 LOAD/STORE 操作(当枚举值等于 Opcode.loadOpcode.store 时)上断言的硬件信号。
Chisel.Enum 定义的一些有用的其他方法是: - .all:返回枚举中的枚举值 - .getWidth:返回一个硬件类型的宽度

解决问题

自 Chisel v3.4.3(2020 年 7 月 1 号)起,值的宽度都是被推断的,你可以添加一个额外的值来强制规定一个宽度,像下面例子这样,添加了 ukn 来强制使位宽为 3。
不支持带符号的值,因此如果您想要带符号的值,则必须使用 .asSInt 转换 UInt

其他资源

ChiselEnum 类型比上面所述的功能强大得多。它允许进行 Sequence、Vec 和 Bundle 分配,以及允许逐步执行顺序状态的 .next 操作和用于检查硬件值是否为有效值的 .isValid。 ChiselEnum 的源代码可以在 EnumFactory 类中找到。 ChiselEnum 操作的示例可以在此处找到。

DataView

Chisel 3.5 中的新功能

介绍

DataView 是一种将 Scala 对象 “viewing” 作为 chisel3.Data 子类型的机制。通常,这对于将 chisel3.Data 的一个子类型查看为另一种子类型很有用。人们可以将 DataView 视为从 Target 类型 TView 类型 V 的映射。这类似于强制转换(例如 .asTypeOf),但有一些差异: 1. View 是可连接的 —— 到 view 的连接发生目标上 2. 强制转换是结构性的(对底层位的重新解释),而 DataView 是可自定义的映射 3. View 可以只有一部分 —— 不是目标所有的字段都要包含在映射中

一个使用示例(AXI4)

AXI4 是一个数字设计中常用的接口。使用 AXI4 的典型 Verilog 外设将写通道定义成下面这样:
这将对应于以下 Chisel 代码:
在 Chisel 设计中用 BlackBoxes 实例化 Verilog 模块时,表达标准的 Verilog 接口是很重要的。但是一般来说,Chisel 开发者更喜欢通过 Decoupled 这样的程序来使用组合,而不是像上面那样单独处理 validready。一个更 “Chisel-y” 的实现方式如下所示:
当然,这会生成非常不同的 Verilog 代码:
那么我们该如何使用更结构化的类型,同时又保留预期的 Verilog 接口呢?看看 DataView:
这个 DataView 是 Verilog 风格的 AXI Bundle 和更具组合性的 Chisel 风格的 AXI Bundle 之间的映射。它允许我们定义我们的端口来匹配 Verilog 的接口,同时像更结构化的类型一样操作他:
这会生成匹配标准命名约定的 Verilog 代码:
注意,目标类型View 类型都是 Data 类型的子类型,DataView可逆的。这意味着我们可以轻松的从已有的 DataView[VerilogAXIBundle, AXIBundle] 中创建一个 DataView[AXIBundle, VerilogAXIBundle],我们需要做的是提供一个函数从 AXIBundle 的实例创建一个 VerilogAXIBundle
下面的示例展示了这个并且说明了 DataView 的另一个使用情况 —— 连接不相关的类型:
这个结果会使 Verilog 中的字段相连接:

其他示例

虽然像 AXI 实例中那样在 Bundle 类型之间映射的能力十分引人注目。但 DataView 还有很多其他应用。最重要的,因为 DataView目标类型不一定是 Data,所以他提供了一种使用需要 Data 的 API 获取 non-Data 对象的方法。

元组(Tuple)

也许最有效的为 non-Data 使用 DataView 的方式就是将 Scala 的元组视为 Bundle。举个例子,在包含 DataView 之前的 Chisel,有人可能会尝试 Mux 元组然后收到下面的报错:
这个问题是因为像 Mux:= 这样的 Chisel 原语只操作 Data 和元组的子类型(作为 Scala 标准库的成员),而不是 Data 的子类。DataView 提供了一种查看 Tuple 的机制,就好像他是 Data 一样:
现在,我们可以使用 .viewAs 像他们是 Data 的子类型一样来查看元组:
这比直接使用元组的原始想法要冗长得多。我们可以通过提供将 Tuple 视为 HWTuple2 的隐式转换来改善这一点:
现在,原始的代码就可以运行了!
注意这个示例忽略了另一个必须的部分,DataProduct(查看下面的相关文档)
所有的这些都可以通过一次导入启用:

完全的和部分的 DataView

当目标类型和 View 类型的所有字段都包含在映射中时,DataView 是完整的。如果 DataView 中有的字段被遗忘了 Chisel 就会报错。如下所示:
就像报错中建议的,如果我们希望 view 是不完全的,我们可以使用 PartialDataView
虽然 PartialDataView 不需要是目标类型的全部,但是View 类型PartialDataViewDataView 必须是全部。这导致 PartialDataView 不能DataView 那样可逆。
例如:
就像提到的那样,映射必须是 View 的全部。

进阶细节

DataView 利用了很多 Chisel 用户可能不熟悉的 scala 功能,特别是 Type 类。

Type 类

Type 类是用于编写多态代码的强大语言功能。它们是 Scala、Swift(参见 Protocols)和 Rust(参见 Traits)等“现代编程语言”的常见功能。Type 类可能看起来类似于面向对象编程中的继承,但有一些重要的区别:
  1. 你可以为不属于你的类型提供 Type 类(例如,在第三方库、Scala 标准库或 Chisel 本身中定义的类型)
  1. 你可以为许多没有子类型关系的类型编写单个 Type 类
  1. 你可以为同一类型提供多个不同的 Type 类
  1. 对于 DataView,1 至关重要,因为我们希望能够实现内置 Scala 类型(如元组和 Seq)的 DataView。此外,DataView 有两个类型参数(TargetView 类型),因此继承实际上没有意义 —— 哪种类型可以扩展 DataView?
在 Scala2 中,Type 类不是一个内置的语言特性,而是使用 implicit 实现。若有兴趣可以阅读: - 基础教程 - 在 StackOverflow 上很有意义的解释
请注意,Scala 3 添加了不适用于 Chisel 3 的 Type 类内置语法,Chisel 3 目前仅支持 Scala 2。

implicit 解析

鉴于 DataView 是使用 implicit 实现的,理解 implicit 就很重要。当编译器需要隐式参数时,它首先在当前作用域寻找,然后才在隐式作用域寻找。
  1. 当前作用域
      • 当前作用域定义的值
      • 显式导入
      • 通配符导入
  1. 隐式作用域
      • 类型的伴生对象
      • 参数类型的隐式范围
      • 类型参数的隐式范围
如果在任一阶段发现多个 implicit,则使用静态重载规则来解决它。简而言之,如果一种 implicit 用于另一种更具体的类型,则将选择更具体的类型。如果在给定阶段内应用多个 implicit,则编译器会抛出不明确的 implicit 解析错误。
本节大量借鉴了 12。尤其是查看 [1] 的示例

implicit 解析示例

为了更加清楚的理解,我们考虑一下 implicit 解析是如何为 DataView 工作的。看 viewAs 的定义:
有了上一节的知识,我们知道每当我们调用 .viewAs 时,Scala 编译器将首先在当前作用域(定义或导入)中查找 DataView[T, V],然后查找 DataViewTV 的伴生对象。这实现了一种相当强大的模式,即 DataView 的默认或典型实现应该在两种类型之一的伴生对象中定义。我们可以将这种方式定义的 DataView 视为“低优先级默认值”。如果给定用户想要不同的行为,那么它们可以被特定的导入否决。例如:
给定以下类型:
这在隐式作用域中提供了 DataView 的实现,作为 Foo 和 Bar 之间的“默认”映射(甚至不需要导入!):
然而,一些 FooBar 的用户可能希望有不同的行为,也许相对于直接映射他们更喜欢 “Swizzling” 的行为:

DataProduct

DataProductDataView 用于验证用户提供的映射的正确性的 Type 类。为了使类型可查看(即 DataViewTarget 类型),它必须具有 DataProduct 的实现。
例如,假设我们有一些 non-Bundle 类型:
假设我们要将 MyCounter 视为 Valid[UInt]
如你所见,Scala 会编译失败,我们需要提供 DataProductp[Mycounter] 的实现,它为 Chisel 提供了一种访问 MyCounterData 类型的对象的方法:
为什么这有用?这就是 Chisel 能够检查 DataView 完整性的方式。除了检查用户是否在映射之外遗漏了字段之外,它还允许 Chisel 检查用户是否在映射中包含了实际上不属于目标view 类型部分的 Data

功能模块创建(Function Module Creation)

Scala 中的对象有一个预先存在的创建函数(方法),叫 apply。当一个对象在表达式中被用作值时(这一般意味着调用了构造函数),这个方法确认了返回值。当处理硬件模块时,人们会希望模块的输出能够代表模块的功能。因此,我们有时会希望对象在表达式中被用作值时的返回值是模块的输出。因为硬件模块被表达为 Scala 对象,所以我们可以定义对象的 apply 方法使其返回模块的输出。这可以被称作为模块的构造创建函数接口。如果我们将其用于标准的 mux2 示例,我们要在表达式中使用 mux2 时返回 mux2 的输出。实现这一功能需要建立使用多路选择器输入作为参数,返回多路选择器输出的构造函数:
就如我们在代码示例中看到的一样,我们定义了 apply 方法来将 Mux2 的输入作为参数,并将 Mux2 的输出作为幻术的返回值。通过这样定义模块,使得之后实现这一常用模块的更大更复杂的版本变得简单。例如,我们之前是这样实现 Mux4 的:
然而,使用我们为 Mux2 重定义的创建函数,现在在编写 Mux4 输出表达的时候就可以将 Mux2 的输出当作模块自身的值使用了:
这使得我们能够编写更直观可读的硬件连接描述,这类似于软件表达式。

多路复用器和输入选择(Muxes and Input Selection)

在硬件描述中选择输入非常重要,因此 Chisel 提供了一些内置的通用输入选择的实现。

Mux

第一个是 Mux。这是一个 2 输入选择器。与之前展示的 Mux2 示例不同,内置的 Mux 允许输入(in0in1)是任何数据类型,只要他们都是 Data 的子类。
通过使用上一节提到的函数模型创建的特性,我们可以简单的创建一个多输入选择器:

MuxCase

嵌套的 Mux 不是必须的,因为 Chisel 提供了内置的 MuxCase,它实现了这个功能。MuxCase 是一个 n 路 Mux,使用示例如下:
其中每一个选择依赖项都表示为 Scala 元组 [condition -> selected_input_port]

MuxLookup

Chisel 也提供了 MuxLookup,它是一个 n 路可索引多路选择器:
这和条件是基于索引的 MuxCase 作用相同:
注意条件/情况/选择信号(即 c1, c2)都要在括号里。

Mux1H

另一种 Mux 是独热多路复用器,Mux1H。它使用一系列选择信号和值,并返回与置位的选择信号关联的值。没有置位的选择信号和多个置位的选择信号是未定义的行为。举个例子:
Mux1H 只要有可能就会生成很容易优化为低深度与/或树的 Firrtl。这一优化在值的类型为 FixedPoint 或一个包含 FixedPoint 的聚合类型时不可用,并且结果会使用一个简单的 Mux 树替代。这个行为不一定是最佳的。由于 FixedPoint 仍处于实验阶段,这一行为未来可能会有变化。

多时钟域(Multiple Clock Domains)

Chisel 3 支持多时钟域。
注意,为了安全的跨时钟域,你需要适当的同步逻辑(比如异步 FIFO)。你可以使用 AsyncQueue 库轻松的做到这一点。
你也可以在其他时钟域中实例化模块:
如果你只希望将你的时钟连接到新的时钟域,使用常用的隐式复位信号,你可以使用 withClock(clock) 替换 withClockAndReset

复位(Reset)

从 chisel 3.2.0 开始,Chisel 3 支持同步和异步复位,这意味着它可以原生支持同步复位寄存器和异步复位寄存器。
寄存器的类型基于关联至其的复位信号的类型。
有三种类型的复位实现了公共特征 Reset: - Bool —— 用 Bool() 构造,也称“同步复位”。 - AsyncReset —— 用 AsyncReset() 构造,也称“异步复位”。 - Reset —— 用 Reset() 构造,也称为“抽象复位”。
出于实现原因,具体的 Scala 类型是 ResetType。从风格上讲,我们避免使用 ResetType,而是使用公共特征 Reset
使用 Bool() 类型的复位信号的寄存器会作为同步复位触发器,使用 AsyncReset 类型复位信号的寄存器作为异步复位寄存器,使用 Reset 类型复位信号的寄存器将在 FIRRTL 编译期间推断它的类型。

复位推断

FIRRTL 将为有抽象 Reset 类型的信号推断一个具体类型,规则如下: 1. 在其扇入扇出中只有 AsyncReset 类型,抽象 Reset 类型和 Dontcare 类型,推断为 AsyncReset。 2. 在其扇入扇出中有 Bool 类型和 AsyncReset 类型是错误的。 3. 其他情况下将被推断为 Bool 类型。
你可以将第 3 条视为第 1 条用 Bool 替换 AsyncReset 的版本,并附加一条,即扇入扇出中既没有 AsyncReset 也没有 Bool 类型的抽象复位将被默认为 Bool 类型。这种“默认”情况并不常见,因为它意味着复位信号由 Dontcare 驱动。

隐式复位

一个 Modulereset 是一个抽象 Reset。为了向后兼容,如果顶层模块有隐式复位,将默认为 Bool 类型。

设置隐式复位类型

Chisel 3.3.0 的新特性
如果你想在模块(包括顶级模块)内设置重置类型,而不是依赖重置推理,可以加入以下特征之一:
  • RequireSyncReset —— 将 reset 的类型设置为 Bool
  • RequireAsyncReset —— 将 reset 的类型设置为 AsyncReset
比如:
注意:这会设置具体类型,但 Scala 类型将保持 Reset,因此类型转换仍然是有必要的。这一问题通常出现于在逻辑中使用 Bool 类型复位的时候。

与复位无关的代码(Reset-Agnostic Code)

抽象复位的目的是使设计与所使用的复位规则无关的硬件成为可能。这使得重置规则与模块的功能无关的实用程序和设计可以重用代码。
考虑下面的两个示例模块,它们与其中使用的重置类型无关:
这些模块可用于同步和异步复位域。它们的重置类型将根据使用它们的上下文来推断。

强制复位类型(Forcing Reset Type)

你可以如上所述设置模块隐式复位的类型。
你也可以强制执行具体类型的复位。
  • .asBool 会将 Reset 重新解释为 Bool
  • .asAsyncReset 将重置重新解释为 AsyncReset
然后,您可以使用 withReset 将强制转换复位用作隐式重置。有关 withReset 的更多信息,请参阅多时钟域。
以下将使 myReg 以及两个 ResetAgnosticRegs 同步复位:
以下将使 myReg 以及 ResetAgnosticRegs 异步重置:
注意:FIRRTL 不会检查此类转换(asBoolasAsyncReset)。在进行这样的强制转换时,作为设计者,实际上是在告诉编译器你知道自己在做什么,并强制类型进行强制转换。

最后连接语义(Last-Connect Semantics)

使用最后连接语义覆盖重置类型是不合法的,除非您要覆盖 DontCare

多态与参数化(Polymorphism and Parameterization)

这一节是进阶部分,第一次阅读可以掠过。
Scala 是一种强类型语言,使用参数化类型来指定函数和类。在本节中,我们将展示 Chisel 用户如何使用参数化类定义自己的可重用函数和类。

参数化函数

之前我们在 Bool 上定义了 Mux2,但现在我们展示如何定义通用多路复用器函数。我们将此函数定义为采用布尔条件以及 T 类型的 con 和 alt 参数(对应于 then 和 else 表达式):
其中 T 必须是 Bits 的子类。 Scala 确保在 Mux 的每次使用中,它都能找到实际 con 和 alt 参数类型的公共超类,否则会导致 Scala 编译类型错误。例如,
这会产生一个 UInt 线,因为 con 和 alt 参数都是 UInt 类型。

参数化类

与参数化函数一样,我们也可以参数化类以使其更具可重用性。例如,我们可以概括 Filter 类以使用任何类型的链接。我们通过参数化 FilterIO 类并定义构造函数以采用 T 类型的单个参数 gen 来实现此目的,如下所示。
现在,我们可以通过定义一个模块类来定义 Filter,该模块类也采用链接类型构造函数参数并将其传递给 FilterIO 接口构造函数:
我们现在可以如下定义一个基于 PLinkFilter
一个通用 FIFO 可以被定义为:
具有 8 个 DataBundle 类型元素的 Fifo 可以实例化为:
还可以定义一个通用的去耦合接口(ready/valid):
然后,该模板可用于将握手协议添加到任何信号集:
FIFO 接口可以简化如下:

基于模块的参数化

你也可以使用其他模块而不只是类型来参数化模块。下面的例子是使用另一个模块来参数化模块的例子:

在 Chisel 中打印(Printing in Chisel)

Chisel 为了调试需求提供了 print_f。它有两种样式:
  • Scala 风格
  • C 风格

Scala 风格

Chisel 支持 printf,其风格类似于 Scala 的 String Interpolation。Chisel 提供了一个自定义字符串插值器 cf,它遵循 C 样式格式说明符(请参阅下面的 C 风格一节)。
注意,在 Chisel 构造中不支持 Scalas-interpolator,它会抛出一个错误:
用 Chisel 中的 cf interpolator 替换它:
请注意,连接 cf“…” 字符串时,需要以 cf“…” 字符串开头:

简单的格式化

其他格式如下所示:

聚合数据类型

Chisel 为 Vec 和 Bundle 类型提供默认的 “pretty-printing”。Vec 的默认打印类似于在 Scala 中打印 Seq 或 List,而打印 Bundle 类似于打印 Scala Map。

自定义打印

Chisel 也为用户定义的 Bundle 提供自定义打印的功能。
这会得到下面的输出:
注意在 cf 插入字符串之间使用的 +cf 插入的结果可以通过 + 操作符来连接。

C 风格

Chisel 提供的 printf 风格与其同名的 C 语言风格相似。它接受一个双引号格式字符串和一个可变数量的参数,然后这些参数将打印在时钟上升沿上。Chisel 支持以下格式说明符:
格式说明符
含义
%d
十进制数
%x
十六进制数
%b
二进制数
%c
8 位 ASCII 字符
%%
百分号字符
它还支持一小组转义字符: | 转义符 | 含义 | | :-: | :-: | | \n | 新行 | | \t | tab | | \" | 双引号字符| | \' | 单引号字符 | | \\ | 反斜杠字符 |
注意,单引号不需要转义,但转义是合法的。
因此 printf 的使用方式与 C 中的使用方式非常相似:

命名(Naming)

从历史上看,Chisel 一直难以可靠的捕捉信号的名称。造成这种情况的原因是,(1)主要依靠映射来查找名称,(2)使用有不可靠行为的 @chiselName 宏。
Chisel 3.4 引入了一个自定义的 Scala 编译器插件,当信号名称被声明时,该插件可以确认信号名称的可靠性和自动捕获。此外,此版本还大量使用了一个新的前缀 API,可以对通过函数调用以编程方式生成的信号进行更可靠的命名。
本节解释了如何在 Chisel 中对信号和模块名称进行命名。有关如何解决系统名称稳定性问题的示例,请参阅 cookbook

编译插件

使用 Chisel 3.5 的用户需要在 build.sbt 设置中加入一行:
这个插件将在 Scala 编译器的 ‘typer’ 阶段之后运行。它查找形式为 val x=y 的任何用户代码,其中 x 的类型为 “chisel3.Data”, “chisel3.MemBase” 或 “chisel3.experial.BaseModule”。对于符合此标准的每一行,它都会重写该行。在以下示例中,注释行是上面的行被重写的行。
如果该行在 bundle 声明中或是模块实例化,则会重写该行,将右侧替换为对 autoNameRecursively 的调用,该调用命名信号/模块。
否则,它将被重写,以将该名称作为前缀包含在执行 val 声明的右侧时生成的任何信号中:
前缀也可以源自一个连接左边的信号名称。虽然这不是依靠编译器插件实现的,但他的行为类似于:
注意,如果硬件类型嵌套在 OptionIterable 的子类型中,则命名也有效:
还有一个轻微的变体(autoNameRecursivevelyProduct),用于使用 unapply 提供的名称命名硬件:
注意,编译器插件在这些情况下不会插入前缀,因为前缀应该是什么并不明确。鼓励想要前缀的用户提供如下所述的前缀。

前缀

如上所示,编译器插件会自动为一些信号添加前缀。但是,用户也可以添加自己的前缀。这对于 ECO 类型的修复特别有用,在这种修复中,需要向模块添加一些逻辑,但又不想影响模块中的其他名称。
在以下示例中,我们在附加逻辑前面加上 “ECO”,其中 Example4 是加入前缀前的 ECO,Example5 是加入前缀后的 ECO:
还要注意,前缀相互附加(包括编译器插件生成的前缀)
有时你可能希望关闭前缀。如果您正在编写库函数,并且不希望使用前缀,在这种情况下,可以使用 noPrefix 对象:

Suggest a Signal’s Name (or the instance name of a Module)

如果要指定信号的名称,可以使用 .sugestName API。注意,建议的名称仍将作为前缀(包括插件)。您可以一直使用 noPrefix 对象来剥离。
注意,使用 .suggestName 不会影响从 val 名称派生的前缀;但是,它可能会影响从连接派生的前缀(如 :=):
如本例所示,这种行为有点不一致,因此在未来的Chisel版本中可能会发生变化。

“Unnamed signals” 的行为(又名 “Temporaries”)

如果你想表示信号的名称无关紧要,你可以在 val 的名称前面加上 _。Chisel将保留前导的惯例,表示前缀之间的未命名信号。例如:
如果一个未命名的信号本身被用来生成前缀,那么前导的_将被忽略,以避免在其他嵌套信号的名称中出现双 _。

设置一个模块名称

如果要指定模块的名称(而不是模块的实例名称),则可以始终覆盖 desiredName 值。注意,可以通过模块的参数来参数化名称。这是一种使模块名称更加稳定的好方法,强烈建议您这样做。

映射命名

无论编译器插件是否启用,Chisel 构造模块后,都会尝试命名模块的所有成员。这将命名作为模块类字段的所有 val,但不会命名嵌套函数或作用域中的任何 val。
如果插件成功地命名了一个信号,那么映射命名将不会起任何作用。我们计划在未来的 Chisel 版本中取消所有映射命名,但允许插件命名是可选的(但推荐)。
例如,以下模块中的信号处于嵌套范围内;插件成功地命名了它们,但映射命名不能:

@chiselName

不再推荐使用此宏,因为它的功能已被编译器插件完全取代。请随意从你的Chisel设计中删除!

未连接的线(Unconnected Wires)

Invalidate API 添加了支持使 Chisel 可以将未连接的导线报告为错误。
Prior to this pull request, Chisel automatically generated a firrtl is invalid for Module IO(), and each Wire() definition. This made it difficult to detect cases where output signals were never driven. Chisel now supports a DontCare element, which may be connected to an output signal, indicating that that signal is intentionally not driven. Unless a signal is driven by hardware or connected to a DontCare, Firrtl will complain with a “not fully initialized” error. 在此 pull 请求之前,Chisel 自动生成一个 firrtl,该 firrtl 对于 Module IO()和每个 Wire() 定义无效。这使得难以检测从未驱动输出信号的情况。Chisel 现在支持 DontCare 元件,该元件可以连接到输出信号,表明该信号是故意不被驱动的。除非信号由硬件驱动或连接到 DontCare,否则 Firrtl 将报告 “not fully initialized” 错误。

API

输出信号可能被连接到 DontCare,当发出 firrtl 的时候生成 a invalid
这表示故意不驱动信号 io.out.debugOption,firrtl 不应对此信号发出 “not fully initialized” 错误。
这可以应用于集合以及单个信号:

确定未连接的元素

我有一个 42 条线的接口。他们之中谁是未连接的?
firrtl 的错误信息应该包含一些东西:
第一行是初始化错误报告。接下来一行,缩进并以源文件的行数开头表示有问题的信号。不幸的是,如果他们在包含选择器的 when 语句中,可能会很难读懂。最后一行中,缩进并以冒号开头表示未初始化的组件。这个示例(来源于 Router tutorial)输出队列未初始化时产生的。以前的代码为:
它初始化了队列的有效位,但没有初始化实际的输出值。修改后的代码是:

Chisel 类型 vs Scala 类型

Scala 编译器无法区分 Chisel 对硬件的表示,如 false.B, Reg(Bool()) 和纯 Chisel 类型(如 Bool() )。当需要硬件时,可以通过 Chisel 类型获得运行时错误,反之亦然。

Scala 类型 vs Chisel 类型 vs 硬件类型

Data 的 Scala 类型由 Scala 编译器识别,例如中的 Bool, Decoupled[UInt]MyBundle
Chisel 类型的 Data 是一个 Scala 对象。它通过名称及其类型(包括宽度)捕获实际存在的所有字段。例如,MyBundle(3) 创建了一个 Chisel 类型,其中包含字段 foo:UInt(3.W), bar:UInt(3.W)
硬件类型是指与可合成硬件绑定的 Data。例如 false.BReg(Bool())。绑定决定了每个字段的实际方向性,它不是 Chisel 类型的属性。
字符是一种 Data,它可以作为文字值重新表示,而无需包装在 Wire、Reg 或 IO 中。

Chisel 类型 vs 硬件类型 vs 字符

下面的代码演示了具有相同 Scala 类型(MyBundle)的对象如何具有不同的属性。

Chisel Type vs Hardware – Specific Functions and Errors

.asTypeOf 对硬件类型和 Chisel 类型都有效:
只能 := 到硬件:
只能从硬件 := 连接:
必须将硬件传递给 chiselTypeOf
必须把硬件类型传递给 *Init
不能把硬件传递给 Wire, Reg, IO:
.Lit 只能被 Chisel 类型调用:
在 Bundle 定义中只能使用 Chisel 类型:
只能在硬件上调用 directionOf
可以在硬件类型和 Chisel 类型上调用 specifiedDirectionOf
.asInstanceOf vs .asTypeOf vs chiselTypeOf .asInstanceOf 是一个 Scala 运行时强制转换, 通常用来告诉编译器你有比他更多的信息来转换 Scala 类型:
如果我们确实拥有比编译器更多的信息,那么这是有效的:
但如果我们错了,会得到 Scala 的运行时异常:
.asTypeOf 是从一个 Data 子类到另一个子类的转换。它通常用于将数据分配给全零,如这一节 cookbook 中所述,但也可以用于将一种 Chisel 类型转换为另一种:
asInstanceOfasTypeOf 不同,chiselTypeOf 不是强制转换操作。它返回一个 Scala 对象,如上面的例子所示,该对象可以用来创建更多的 Chisel 类型和与现有硬件具有相同 Chisel 类型的硬件。

连接操作符(Connectable Operators)

术语

  • “Chisel type” - 一种没有绑定在硬件上的 Data,即不是一个组件。
    • 例如,UInt(3.W), new Bundle(...), Vec(3, SInt(2.W)) 都是 Chisel type
  • Aggregate - 一种包含其它 Chisel type 或者组件的 Chisel type 或者组件。(即 Vec, Record, 或者 Bundle
  • Element - 一种不包含其它 Chisel type 或者 compnoent 的 Chisel type 或者组件。(即 UInt, SInt, Clock, Bool 等)
  • “组件” - 一种绑定在硬件上的 DataIO, Reg, Wire 等)
    • 例如,Wire(UInt(3.W)) 是一个组件,它的 Chisel type 是 UInt(3.W)
  • “成员” - 一种 Chisel type 或者组件,或者他们的任何子集(可以是 AggregateElement
    • 例如 Vec(3, UInt(2.W))(0) 是父项 Chisel type 的一个成员
    • 例如 Wire(Vec(3, UInt(2.W)))(0) 是父项 Wire 组件的一个成员
    • 例如 IO(Decoupled(Bool)).ready 是父项 IO 组件的一个成员
  • “relative alignment” - 同一个组件或者 Chisel type 的两个成员是否对齐/翻转
    • 在下面的章节中详细定义
  • “structural type check” - 如果 AB 有匹配的绑定名称和类型(RecordVectorElement),向量大小,Element 类型(UInt/SInt/Bool/Clock 等)。Chisel type A 和 Chisel type B 在结构上等价。
    • 忽略 relative alignment
  • “alignment type check” - 如果 AA 的 relative alignment 的每一个成员都与 B 的 relative alignment 的结构对应的成员相同,则 Chisel type A 和另一个 Chisel type B 匹配对齐。

概述

连接操作符是连接 Chisel 硬件组件到另一个 Chisel 硬件组件的标准方法。
所有连接操作符都需要两个硬件组件(生产者和消费者)结构类型等价。
结构类型等效规则的一个例外是使用 connectable 机制,在本文档末尾的本节中对此进行了详细说明。
AggregateRecord, Vec, Bundle)Chisel 类型可以包括相对于彼此翻转的数据成员。因此,在两个 Chisel 组件之间存在许多所需的连接行为。以下是 Chisel 连接操作符:
  • c := p (单向): 将 p 的所有成员连接到 c; 需要 c 和 p 都没有翻转的成员
  • c :#= p (强制 单向): 将 p 的所有成员连接到 c; 忽略对齐
  • c :<= p (对齐): 将 p 的所有对齐的成员(非翻转的)连接到 c
  • c :>= p (翻转方向): 将 p 的所有非翻转的成员连接到 c
  • c :<>= p (双向操作): 从 p 连接 c 的所有对齐的成员; 从 c 连接所有 p 的翻转的成员
这些操作符可能看起来是符号的随机集合;然而,操作符之间的字符是一致的,每个操作符的语义描述如下:
  • : 表示消费者,或操作符的左侧。
  • = 表示生产者,或操作符的右侧。
    • 因此, c := p 连接消费者(c)和生产者(p)
  • < 表示某些成员将驱动生产者到消费者,即从右到左。
    • 因此, c :<= p 驱动生产者(p)的成员到消费者(c)的成员。
  • > 表示一些信号将驱动消费者到生产者,即从左到右。
    • 因此, c :>= p 驱动消费者(c)的成员到生产者(p)的成员。
    • 因此, c :<>= p p 到 c 的成员和 c 到 p 的成员都驱动
  • # 表示忽略成员对齐并驱动生产者到消费者。
    • 因此, c :#= p 将成员从 p 驱动到 c,忽略方向
{% note default %} 注意:此外,以 = 结尾的操作符具有赋值优先级,这意味着 x :<>= y + z 将转换为 x :<>= (y + z),而不是 (x :<>= y ) + z。 <> 操作符并非如此,这对用户来说是一个小痛点。 {% endnote %}

对齐方式:翻转与对齐

成员的对齐是一个相对属性:一个成员相对于同一组件或 Chisel 类型的另一个成员对齐/翻转。 因此,必须始终说明某个成员是否相对于该类型的另一个成员(父级、兄弟级、子级等)翻转/对齐。
我们使用以下非嵌套 bundle Parent 的示例来说明 p 成员之间的所有对齐关系。
首先,所有成员是与与他们自己对齐的:
  • p 和 w.r.t p 对齐
  • p.alignedChild is aligned w.r.t p.alignedChild
  • p.flippedChild is aligned w.r.t p.flippedChild
接下来,我们列出所有的父/子关系。因为 flippedChild 是翻转的,它改变了它相对于 Parent 的对齐关系。
  • p is aligned w.r.t p.alignedChild
  • p is flipped w.r.t p.flippedChild
最后,我们可以列出所有的同级关系:
  • p.alignedChild is flipped w.r.t p.flippedChild
下一个例子有一个嵌套的 GrandParent bundle,它实例化了一个对齐的 Parent 和一个翻转的 Parent
考虑下面的祖父与孙辈的对齐关系。奇数次翻转表示翻转的关系;偶数次翻转表示对齐的关系。
  • g 与 w.r.t g.flippedParent.flippedChild 对齐
  • g 与 w.r.t g.alignedParent.alignedChild 对齐
  • g 与 w.r.t g.flippedParent.alignedChild 翻转
  • g 与 w.r.t g.alignedParent.flippedChild 翻转
考虑下面从 g.alignedParentg.flippedParent 开始的对齐关系。注意无论 g.alignedParent 相对于 g 是对齐/翻转的,对于 g.alignedParentg.alignedParent.alignedChild 之间的对齐关系没有影响,因为这个问题中对齐是只相对于两个成员的!:
  • g.alignedParent is aligned w.r.t. g.alignedParent.alignedChild
  • g.flippedParent is aligned w.r.t. g.flippedParent.alignedChild
  • g.alignedParent is flipped w.r.t. g.alignedParent.flippedChild
  • g.flippedParent is flipped w.r.t. g.flippedParent.flippedChild
总之,一个构件相对于硬件组件的另一个构件对齐或翻转。这意味着消费者/生产者的类型是确定任何操作符的行为所需的唯一信息。消费者/生产者是否是一个更大的 bundle 的成员是无关紧要的;你只需要知道消费者/生产者的类型。

输入/输出

Input(gen)/Output(gen) 是强制操作符。他们执行两个函数:(1)创建一个从所有子类型递归删除所有翻转的新的 Chisel type(仍然结构等价但对齐类型不再等价)。(2)如果是 Input 就使用 Flipped, 如果 Output,则保持对齐(不执行任何操作)。例如,如果我们想象一个名为 cloneHiselTypeButStripAllFlips 的函数,那么 Input(gen)在结构上和对齐类型等效于 Flipped(cloneHisel TypeButStrip AllFlips(gen))
注意如果 gen 是 non-aggregate 的,则 Input(nonAggregateGen)Flipped(nonAggregateGen)是等效的。
{% note default %} Future work will refactor how these primitives are exposed to the user to make Chisel’s type system more intuitive. See [https://github.com/chipsalliance/chisel3/issues/2643]. {% endnote %}
了解了这些内容后,我们可以考虑下面的示例和成员的详细相对对齐:
首先,我们可以使用一个和 Parent 相似的例子但是用 Input/Output 代替 Flipped。因为 alignedChildflippedChild 是 non-aggregates 的,Input 基本上只是一个 Flipped,因此对齐和之前的 Parent 示例相比没有变化。
它的对齐关系和之前的 Parent 示例相同:
  • p 和 w.r.t p 是对齐的
  • p.alignedCoerced 和 w.r.t p.alignedCoerced 是对齐的
  • p.flippedCoerced 和 w.r.t p.flippedCoerced 是对齐的
  • p 和 w.r.t p.alignedCoerced 是对齐的
  • p 和 w.r.t p.flippedCoerced 是翻转的
  • p.alignedCoerced 和 w.r.t p.flippedCoerced 是翻转的
下一个示例有嵌套的 GrandParent bundle,它实例化了一个 Output ParentWithOutputInput 和一个 Input ParentWithOutputInput
记住 Output(gen)/Input(gen) 递归的删除了每一个子项的 Flipped。这使得 gen 的每一个成员和其他成员保持一致。
考虑以下祖父和孙辈之间的关系。因为 alignedCoercedflippedCorced 与它们的所有递归成员对齐,所以它们是完全对齐的。因此,只有它们与 g 的对齐会影响孙辈的对齐:
gg.alignedCoerced.alignedChild 是对齐的 g 和 w.r.t g.alignedCoerced.flippedChild 是对齐的 g 和 w.r.t g.flippedCoerced.alignedChild 是对齐的 g 和 w.r.t g.flippedCoerced.flippedChild 是对齐的
考虑下面从 g.alignedCoercedg.flippedCoerced 开始的对齐关系。注意无论 g.alignedCoerced 相对于 g 是对齐/翻转的,对于 g.alignedCoercedg.alignedCoerced.alignedChildg.alignedCoerced.flippedChild 之间的对齐关系没有影响,因为这个问题中对齐是只相对于两个成员的!然而,因为对齐是强制的,g.alignedCoerced/g.flippedAligned 和他们的子项的一切都是对其的:
g.alignedCoerced 和 w.r.t. g.alignedCoerced.alignedChild 是对齐的。 g.alignedCoerced 和 w.r.t. g.alignedCoerced.flippedChild 是对齐的。 g.flippedCoerced 和 w.r.t. g.flippedCoerced.alignedChild 是对齐的。 g.flippedCoerced 和 w.r.t. g.flippedCoerced.flippedChild 是对齐的。
总的来说,Input(gen)Output(gen) 递归的强制子类对齐, 并规定 gen 与其父项的 bundle 对齐(如果存在的话)。

使用完全对齐的成员连接组件

单向连接操作符(:=)

对于所有成员都对齐的简单链接,用 :=
这会生成以下 Verilog,其中 incoming 的每个成员都会驱动 outgoing 的每个成员:

使用混合对齐的成员连接组件

聚合 Chisel type 可以包括相对于彼此翻转的数据成员;在下面的示例中,alignedChildflippedChild 相对于 MixedAssignmentBundle 对齐/翻转。
因此,在两个 Chisel 组件之间存在许多所需的连接行为。首先,我们将介绍最常见的 Chisel 连接操作符 :<>=,用于连接具有混合排列成员的组件,然后花点时间研究端口方向和连接方向之间混淆的常见来源。然后,我们将探讨其余的 Chisel 连接操作符。

双向连接操作符(:<>=)

对于需要“批量连接式语义”的连接,其中对齐的成员由生产者驱动到消费者,翻转的成员由消费者驱动到生产者,使用 :<>=
他会生成如下的 Verilog,对其的成员将 incoming 驱动到 outcoming,翻转的成员将 outcoming 驱动到 incoming

端口方向计算与连接方向计算

一个常见的问题是,如果使用混合对齐连接(例如:<>=)连接父组件的子成员,则子成员与其父组件的对齐会影响什么吗?答案是否定的,因为对齐总是相对于连接到的内容来计算的,并且成员总是与自己对齐。
在以下从 incoming.alignedChild 连接到 outgoing.aligndChild 的示例中,incoming.lignedChild 是否与incoming 对齐是无关紧要的,因为:<>= 仅计算相对于连接对象的对齐,而 incoming.AlignedCildincomingaligndCild 对齐。
虽然 incoming.flicpedChildincoming 的对齐不会影响我们的操作符,但它确实会影响 incoming.Flicpedchild 是我的模块的输出端口还是输入端口。混淆的一个常见来源是将确定 incoming.flicpedChild 是否将解析为 verilog 输出/输入(端口方向计算)的过程与确定如何 :<>= 用什么驱动什么(连接方向计算)。虽然这两个过程都考虑了相对一致性,但它们是不同的。
端口方向计算始终计算相对于标有IO的组件的对齐。IO(Flipped(gen)) 是一个传入端口,与 gen 对齐/翻转的 gen 的任何成员都是传入/传出端口。IO(gen)是传出端口,与 gen 对齐/翻转后的 gen 的任何成员都是传出/传入端口。
连接方向计算始终基于连接所引用的明确消费者/生产者来计算对齐。如果连接传入 :<>= 传出,则根据传入和传出计算对齐。如果连接 incoming.alignedChild:<>=outgoing.alignedChild,则根据 incoming.liginedChild和outgoing.alignedChild 计算对齐(incomingincoming-alignedChild 的对齐无关)。
这意味着用户可以尝试连接到其模块的输入端口!如果我写 x:<>=y,并且 x 是当前模块的输入,那么这就是连接试图做的。然而,由于输入端口无法从当前模块中驱动,Chisel 将抛出错误。这与用户使用单向操作符时会遇到的错误相同:如果 x 是当前模块的输入,则 x:=y 将抛出相同的错误。组件是否可驱动与任何试图驱动它的连接操作符的语义无关。
总之,端口方向计算是相对于根标记 IO 的,但连接方向计算是关于连接正在进行的消费者/生产者的。这具有一个积极的特性,即连接语义仅基于 Chisel 结构类型及其消费者/生产者的相对对齐(没有更多,也没有更少)。

对齐连接操作符 (:<=)

对于需要“类批量连接语义”的对齐一半的连接,其中对齐的成员由生产者驱动到消费者,翻转的成员被忽略,请使用:<=(对齐的连接)。
这会生成以下Verilog,其中对齐的成员将驱动 incoming 成员到 outcoming 成员,翻转的成员被忽略:

翻转连接操作符(:>=)

对于需要“类批量连接语义”的翻转一半的连接,其中对齐的成员被忽略,翻转的成员被连接到消费者到生产者,请使用 :>=(“翻转连接”或“背压连接”)。
这会产生以下 verilog,在该 verilog 中,对齐成员被忽略,而翻转的成员被驱动到 incoming
注意:精明的观察者将意识到语义上的 c :<>= p 完全等于 c :<= p,然后是 c :>= p

强制单向连接操作符(:#=)

对于你想要每个生产者成员始终驱动每个消费者成员的连接,无论对齐方式如何,请使用 :#=(“强制连接”)。该操作员可用于初始化包含混合对齐方式的类型的 wire
这会产生以下 verilog,其中所有成员都从字面上驱动到 w,而不管有什么对齐方式:
注意:精明的观察者将意识到语义上的 c :#= P 完全等于 c :<= p,然后是p :>= c(注意第二连接中的 pc 切换位置)。
另一个用例 :#= 是将混合方向 Bundle 连接到完全对齐的 monitor。
这会产生以下 verilog,其中所有成员都从字面上驱动到 w,湖库额对齐方式:

Connectable

用户要连接不等效类型的 Chisel 组件并不少见。例如,用户可能想连接可能与字段相交的匿名 Record 组件,但不能连接,因为它们在结构上不是等效的。另外,一个人可能想要连接两种具有不同宽度的类型。
在这些情况下,connectable 是专业连接操作符行为的机制。对于未存在连接到的其他组件中或不匹配宽度的其他成员,或者对于始终将成员排除在连接的情况下,可以从可连接的对象中明确调用它们,而不是触发错误。
此外,还有其他技术可用于解决类似用例,包括 .viewassupertype,静态转换到超类(例如 (x: T))或创建自定义数据范围。有关何时使用每种技术的讨论,会在之后讨论。
本节演示了如何在多种方案中专门使用 connectable

连接 Records

一个不常见的用途是尝试连接两个 Records。对于匹配的成员,应该连接它们,但是对于无与伦比的成员,由于无与伦比而引起的错误应被忽略。要实现这一目标,请使用其他操作员初始化所有记录成员,然后使用带有 waiveAll:<>= 仅连接匹配成员。
{% note default %} 注意,.ViewAssuperType,静态转换中没有任何一个自定义数据范围,因为 Scala 类型仍然是 Record。 {% endnote %}
这会产生以下 verilog,其中 p.bc.b 驱动的:

waived 连接的默认

另一个不常见的用途是尝试连接两个 Records。对于匹配的成员,应该连接它们,但是对于不匹配的的成员,应将它们连接为默认值。要实现这一目标,请使用其他操作符初始化所有 Records 成员,然后使用带 waiveAll:<>= 仅连接匹配成员。
这会生成以下 Verilog,其中 p.b 是从 c.b 驱动的,并且 p.a, c.bc.c 被初始化为默认值:

与可选成员连接类型

在下面的示例中,我们可以使用 :<>=waive 来连接两个 MyDecoupledOpts,其中只有一个具有 bits 成员。
这会生成以下 verilog,即 readyvalid 被连接起来,并且忽略了 bits

总是忽略由额外成员(部分连接操作符)造成的错误

最不安全的连接是仅连接消费者和生产者中存在的成员,并忽略所有其他成员。这是不安全的,因为此连接永远不会在任何 Chisel 类型上出错。
做到这一点,你可以使用 .waiveAll 和静态转换到 Data
这会生成如下 verilog,没有任何东西被连接起来:

连接不同位宽的组件

如果宽度较大的组件连接到宽度较小的组件,则不可连接的操作符会隐式截断。可连接的操作符不允许这种隐式截断行为,并要求被驱动的组件与源组件的宽度相等或更大。
如果希望隐式截断的行为发生,connectable 操作符提供了一种 squeeze 机制,该机制将使连接能够继续并隐式截断。
这会生成以下 verilog,其中 p 在驱动 c 之前被隐式截断:

在可连接的任何操作符中排除任何成员

如果用户想始终将字段从连接中排除,请使用永远不会连接字段的 exclude 机制(好像它不存在连接)。
请注意,如果一个字段匹配生产者和消费者,但仅排除了一个字段,则另一个非排除的字段仍然会触发错误。要解决此问题,请使用 waiveexclude
这会生成以下verilog,其中 special 字段未连接:

连接结构上不相等的 Chisel 类型的技术

DataviewViewAssuperType 创建具有不同 Chisel 类型的组件的 view。这意味着用户可以首先创建消费者或生产者(或两者)的 Dataview,以便 Chisel 类型在结构上等效。当消费者和生产商之间的差异不是超级嵌套时,并且如果他们具有编码其结构的丰富类型时,这很有用。通常,Dataview 是使用的首选机制(如果可以的话),因为它在 Scala 类型中保留了最多的 Chisel 信息,但是在许多情况下,它不起作用,因此必须退回 Connectable
Connectable 不会更改 Chisel 类型,而是将悬挂或无连接的成员的操作符语义更改来避免错误。这对于当消费者和生产者之间的差异不显示在 Scala 类型系统中(例如类型 Option[Data] 的当前/缺失字段或匿名 Record)或深深地嵌套在一个特别繁重的捆绑包中时,创建 Dataview
静态转换(例如(x: T))允许连接具有不同类型的组件,但使 Chisel 类型保持不变。即使 Scala 类型不同,也要使用它来强迫连接发生。
{% note default%} 人们可能想知道,如果操作符可以很容易被绕过,为什么它们首先需要相同的 Scala 类型。原因是鼓励用户使用 Scala 类型系统来编码Chisel 信息,因为它可以使他们的代码更加健壮;然而,我们不想对此采取严厉措施,因为有时我们想让用户“just connect the darn thing”。 {% endnote %}
当所有其他方法都失败时,人们总是可以手动扩展连接以逐个成员执行他们想要发生的事情。这种方法的缺点是其冗长,并且向组件添加新成员将需要更新手动连接。
关于 ConnectableviewAsSupertype/DataView 与静态转换(例如 (x: T))需要记住的事项:
  • DataViewviewAsSupertype 将抢先删除具有不同 Chisel 类型的新视图中不存在的成员,因此 DataView 确实会影响连接的内容
  • Connectable 可用于免除最终悬空或未连接的成员的错误。 重要的是,Connectable waives 不会影响已连接的内容
  • 静态转换不会删除额外的成员,因此静态转换不会影响连接的内容

使用冲突的名称连接同一超类的不同子类

在这些示例中,我们将 MyDecoupledMyDecoupledOtherBits 连接。两者都是 MyReadyValid 的子类型,并且都有 UInt(32.W) 的位字段。
第一个示例将使用 .viewAsSupertype 将它们连接为 MyReadyValid。 由于它更改了 Chisel 类型以省略两个 bits 字段,因此 bits 字段未连接。
注意 bits 字段是未连接的。
第二个示例将使用静态转换和 .waive(_.bits) 将它们连接为 MyReadyValid。 请注意,由于静态转换不会更改 Chisel 类型,因此连接会发现消费者和生产者都有一个 bits 字段。 这意味着,由于它们在结构上是等效的,因此它们匹配并连接。 waive(_.bits) 不执行任何操作,因为这些位不是悬空的,也不是未连接的。
请注意,bits 字段是连接的,即使它们被放弃,因为放弃只是改变如果它们丢失则是否应该抛出错误,而不是如果它们在结构上等效则不连接它们。 要始终忽略连接,请在一侧使用 exclude,并在另一侧使用 excludewaive

通过忽略多余的成员将子类与超类连接

{% note default %} 注意这个例子,使用 .viewAsSupertype 会更好。 {% endnote %}
在接下来的示例中,我们可以使用 :<>= 通过放弃 bits 成员把 MyReadyValid 连接到 MyDecoupled
这个例子生成了下面的 verilog, readyvalid 是连接的, 然后 bits 被忽略了:

连接不同的子类

{% note default %} 注意这个例子,使用 .viewAsSupertype 会更好。 {% endnote %}
请注意,连接操作符要求消费者和生产商是相同的 Scala 类型,以鼓励静态捕获更多信息,但是在连接之前,它们始终可以将其转换到 Data 或其他常见的超类。
在下面的示例中,我们可以使用 :<>=waiveAs 来连接 MyReadyValid 的两个不同的子类。
这个例子生成了下面的 verilog, readyvalid 是连接的, 然后 bitsecho 被忽略了:

FAQ

我如何尽可能灵活地连接两个项目(尽你所能,但永远不要出错)
.unsafe
如何连接两个项,但不关心 scala 类型是否等效?
.as

译码器(Decoders)

在复杂的设计中,通常会从来自数据总线的大 UInt 中识别某些模式,并根据这种观察将操作分派到下一个管道阶段。 执行此操作的电路可称为“译码器”,例如总线交叉开关中的地址解码器或 CPU 前端中的指令解码器。 Chisel 提供了一些实用程序类来在 util.exprimental.decode 包中生成它们。

基本译码器

decoder 提供的最简单的 API 本质上只是一个编码了你期望的输入和输出的真值表。

DecoderTable

当译码结果包含多个字段,每个字段有自己的含义时,真值表很快就变得难以维护起来。DecoderTable1 API 被设计出来用结构化的定义来生成译码表。
DecodePattern trait 是结构化信息和它的编码之间的桥梁。成员 bitPat 定义了译码真值表的输入 BitPat,其他的成员可以被定义来包含一些结构信息。
为了生成输出一侧的译码真值表,要使用 DecodeField trait。给定一个 DecodePattern 对象的实现,genTable 方法会返回需要的输出。
然后,所有的 DecodePattern 案例都可以生成或从外部源读取。使用所有的 DecodeField 对象,可以很容易地生成解码器,并通过相应的 DecodeFields 读取输出。

Intrinsics

Chisel Intrinsics 用于实例化实现定义功能。Intrinsics 为特定编译器提供了一种以库代码无法实现的方式扩展语言功能的方法。
定义为 IntrinicModule 的模块将被实例化为正常模块,但是 Intrinsics 字段将通信与编译器通信用于实现模块的哪种功能。实现可能不是实际模块,内在的模块性质仅用于实例化目的。
Intrinsics 的实现将被打字检查。实现记录了 Intrinsics 什么可用。

参数化

参数可以作为参数传递给 IntModule 构造函数。

Example

以下为“Other Intrinsic”创建了一个 Intrinsic 模块。它采用一个名为“STRING”的参数,并具有多个端口。

Annotations

Annotation 是与 FIRRTL 电路中零个或多个“事物”相关联的元数据容器。 通常,Annotation 用于将信息从 Chisel 传递到特定的已知 FIRRTL transform。 通过这种方式,注释可以被视为特定 Transform 消耗的“参数”。
Annotation 旨在成为 Chisel 的实现细节,而不是由用户手动构建或直接交互。 相反,它们旨在通过现有或新的 Chisel API 来使用。 例如,dontTouch API 为用户提供了一种指示不应优化线路或端口的方法。 该 API 由 DontTouchAnnotation 支持,但这对 Chisel 用户是隐藏的。
所有支持的 Annotation 的列表作为 circt.llvm.org 上 FIRRTL 文档的一部分进行维护。

源定位器(Source Locators)

详细阐述 Chisel 设计并发出 FIRRTL 文件或 Verilog 文件时,Chisel 将自动添加源定位器,这些定位器将其引用到包含相应的 Chisel 代码的 Scala 文件。
在 FIRRTL 文件中,看上去像是:
在 Verilog 文件中,看上去像是:
默认情况下,包含调用 JVM 的文件的相对路径。 要更改相对路径的计算位置,请设置 Java 系统属性 -Dchisel.project.root=/absolute/path/to/root。 该选项可以直接传递给 sbt (sbt -Dchisel.project.root=/absolute/path/to/root)。 在 build.sbt 文件中设置值将不起作用,因为它需要传递给调用 sbt 的 JVM(而不是相反)。 我们预计这仅与可能需要更多定制的发布版本相关。

深入了解连接操作符

Chisel 包含两个连接运算符 :=<>。本文档对两者的差异以及何时使用其中一个或另一个进行了更深入的解释。使用 DecoupledIO 的 Scastie 实例的实验证明了这些差异。

实验设置

实验的图表可以在这里查看。
notion image
下面我们可以看到这个例子的 Verilog 结果:

概念 1:<> 是可交换的

本实验是利用上述实验来测试 <> 的功能。
实现这一点需要翻转 <> 运算符的 RHS 和 LHS,并查看 <> 将如何反应。(实验的 Scastie 链接:https://scastie.scala-lang.org/Shorla/LVhlbkFQQnq7X3trHfgZZQ)
下面我们可以看到这个例子的 Verilog 结果:

结论:

Verilog 保持不变,没有产生错误,表明 <> 运算符是可交换的。

概念 2::= 表示分配来自 RHS 的所有 LHS 信号,无论 LHS 上的方向如何。

使用与上面相同的实验代码,我们设置为测试 := 的函数。在上面的示例代码中,我们将 <> 的所有实例替换为 :=。(实验的 Scastie 链接:https://scastie.scala-lang.org/Shorla/o1ShdaY3RWKf0IIFwwQ1UQ/1)
下面我们可以看到这个例子的 Verilog 结果:

结论:

:= 运算符在 LHS 上逐个字段,并试图将其连接到来自 RHS 的相同命名信号。如果 LHS 上的某个信号实际上是输入,或者 RHS 上的相应信号是输出,则会出现如上所示的错误。

概念 3:总是使用 := 来把 DontCare 赋值给 Wires

DontCare 分配给未定向的对象时,是否应使用 :=<> ?我们将使用以下示例代码找到答案:(实验的 Scastie 链接:https://scastie.scala-lang.org/Shorla/ZIGsWcylRqKJhZCkKWlSIA/1)
下面我们可以看到这个例子的 Verilog 结果:

结论:

如果使用 <> 将未处理的连线 tmp 分配给 DontCare,我们将得到一个错误。但是在上面的例子中,我们使用了 :=,并且没有出现错误。但是,当使用 := 将导线分配给 DontCare 时,不会发生错误。
因此,当把 DontCare 指定给导线时,请始终使用 :=

概念4:您可以使用 <>:=DontCare 分配给有向的事物(IOs)

DontCare 分配给定向的对象时,应该使用 := 还是 <> ?我们将使用以下示例代码找到答案:(实验的 Scastie 链接:https://scastie.scala-lang.org/Shorla/ZIGsWcylRqKJhZCkKWlSIA/1)
下面我们可以在此示例中看到生成的 Verilog:

结论:

<>:= 均可用于分别为 io.inp.io.a 中所示的有向的(IOs)赋值给 DontCare。这基本上是等效的,因为在这种情况下,<>:= 将确定 LHS 的方向。

概念5:<> 在至少一个已知流向(IO 或子模块的 IO)的事物之间起作用。

如果至少有一个已知的流向,<> 会怎么做?这将使用下面的实验代码显示:(实验的 Scastie 链接:https://scastie.scala-lang.org/Shorla/gKx9ReLVTTqDTk9vmw5ozg)
下面我们可以在此示例中看到生成的 Verilog:

结论:

上面的连接顺利进行,没有错误,只要至少有一个方向的东西(IO 或子模块的 IO)来“解决”方向,这将显示 <>

概念6:<>:= 按字段名称连接信号。

该实验创建了一个 MockDecoupledIO,其名称与 DeCopledIO 具有相同的字段。Chisel 可以将其连接并产生相同的 Verilog,即使 MockDecoupledIO 和 DeCopledIO 是不同的类型。(实验的 Scastie 链接:https://scastie.scala-lang.org/uf4tqquvqyigzaw705nfiq)
下面我们可以在此示例中看到生成的 Verilog:
这是另一个实验,在其中我们删除了一个 MockDecoupledIO的领域:(实验的 scastie 链接:https://scastie.scala-lang.org/chtkhkcps9cvjkjjqpdeia)
下面我们可以在此示例中看到生成的 Verilog:
这个失败是因为缺少一个字段 bits

结论:

对于 :=, Scala 类型不需要匹配,但是 LHS 上的所有信号都必须由 RHS 提供,否则您将获得 Chisel 错误。RHS 上可能还有其他信号,这些信号将被忽略. 对于 <>, Scala 类型不需要匹配,但是所有信号必须在 LHS 和 RHS 之间完全匹配。在这两种情况下,字段的顺序都不重要。
Chisel 工程模板(mill + verilator)OpenOCD 源码分析
Loading...