Skip to content

10 硬件生成器

Chisel 的强大之处在于允许我们编写所谓的 硬件生成器 (Hardware Generators)。在传统硬件描述语言(如 Verilog 和 VHDL)中,通常需要借助 Java、Python 等高级编程语言来生成硬件描述代码。而 Chisel 本身建立在 Scala 语言之上,因此我们可以直接使用 Scala 语言的强大功能进行硬件生成。

使用 Chisel 编写硬件生成器的优势包括:

  1. 灵活性:利用 Scala 的语法结构(循环、条件、函数等)动态生成电路模块。
  2. 复用性:通过函数和参数化设计实现硬件模块的高复用性。
  3. 高效性:使用函数和类简化复杂电路的设计,提高代码可维护性。

10.1 Scala 简介

Scala 变量类型

Scala 提供两种变量类型:

  • val:常量,一旦定义不可修改。
  • var:变量,可以重新赋值。

示例代码:

scala
// 定义一个常量
val zero = 0
// zero = 3 // 错误!val 不能被重新赋值

// 定义一个变量
var x = 2
x = 3 // 正确,var 可以被重新赋值

Scala 循环语句

Scala 支持经典的 for 循环,可用于遍历数据和生成硬件描述。

示例:打印循环索引值

scala
for (i <- 0 until 10) { 
  println(i) 
}
  • 0 until 10:生成从 0 到 9 的数字序列(不包含 10)。

示例:使用循环连接移位寄存器的位 在 Chisel 中,可以利用 for 循环动态生成移位寄存器:

scala
val regVec = Reg(Vec(8, UInt(1.W))) // 定义一个包含 8 个 1 位寄存器的向量

regVec(0) := io.din // 输入连接到第一个寄存器
for (i <- 1 until 8) {
  regVec(i) := regVec(i-1) // 将每个寄存器连接到前一个寄存器
}
  • 该示例通过 for 循环动态地将寄存器向量中的各个位连接起来,构成移位寄存器。
  • 优点:代码简洁,逻辑清晰,便于扩展。

条件判断 (if/else)

Scala 使用 ifelse 进行条件判断。条件判断通常在硬件生成阶段执行,不会生成硬件逻辑。

示例:判断奇偶数

scala
for (i <- 0 until 10) {
  if (i % 2 == 0) {
    println(i + " is even")
  } else {
    println(i + " is odd")
  }
}

元组 (Tuple)

元组用于存储多个不同类型的元素,可以将元组视为一组不可变的有序数据。

  • 元组常用于从函数中返回多个值。
  • 在硬件设计中,元组可以表示带有多个输出的轻量组件。

示例:定义城市的邮政编码和名称

scala
val city = (2090, "Frederiksberg")
val zipCode = city._1  // 访问第一个字段
val name = city._2     // 访问第二个字段

集合与序列 (Seq)

Scala 提供了强大的集合库,其中 Seq 是最常用的集合类型之一。

  • Seq 是不可变的有序集合,可以通过索引访问元素(从 0 开始)。
  • 在硬件生成器中,Seq 常用于生成具有固定大小的电路组件。

示例:定义一个整数序列并访问元素

scala
val numbers = Seq(1, 15, -2, 0)
val second = numbers(1) // 访问第二个元素,结果为 15

10.2 使用函数构建轻量级组件

在 Chisel 中,函数是构建可复用硬件模块的关键工具。

  • 函数定义:通过参数化和逻辑组合生成不同的电路。
  • 轻量级组件:使用函数代替完整模块,减少冗余代码。

示例:动态生成硬件逻辑

scala
def generateAdder(width: Int): UInt = {
  val a = Wire(UInt(width.W))
  val b = Wire(UInt(width.W))
  a + b
}
  • 参数化设计:通过 width 参数控制加法器的位宽。
  • 返回值:函数返回生成的硬件逻辑。

小结

通过 Scala 和 Chisel 的结合,我们可以:

  1. 使用 变量循环条件判断 构建动态硬件生成器。
  2. 利用 元组集合 简化数据结构,方便返回多个输出。
  3. 使用 函数 定义轻量级组件,实现参数化设计和模块复用。

硬件生成器使 Chisel 不仅能描述硬件,还能动态生成复杂的硬件结构,极大提高了设计效率和灵活性。这种功能是传统硬件描述语言难以实现的,使 Chisel 成为现代硬件设计的强大工具。

10.2 使用函数构建轻量级组件

在硬件描述中,模块 (Module) 是结构化设计硬件的标准方式。然而,定义一个模块需要一些固定代码,包括模块声明、输入输出定义和实例化连接的代码。为简化代码结构,Chisel 提供了一种更轻量级的方法:使用函数来生成硬件组件

函数作为硬件生成器

Scala 函数可以接收 Chisel 的参数并返回生成的硬件逻辑。这种方法可以避免模块声明的固定开销,非常适合小型或重复性的硬件组件。

示例:加法器生成器

以下是一个简单的加法器生成函数:

scala
def adder(x: UInt, y: UInt) = {
  x + y
}

解释:

  • 这个函数接受两个 UInt 类型的输入 xy,并返回两者之和。
  • Scala 中,函数返回值默认是函数体中的最后一个表达式的结果。

实例化两个加法器:

scala
val x = adder(a, b) // 第一个加法器  
val y = adder(c, d) // 第二个加法器
  • 每次调用 adder 函数,都会生成一个新的硬件实例(加法器)。
  • 这里的 adder 是一个 硬件生成器,而不是在编译阶段执行的加法操作。

函数中包含状态

函数不仅可以生成组合逻辑,还可以包含状态(例如寄存器)。下面的示例展示了一个具有 单周期延迟 的函数:

scala
def delay(x: UInt) = RegNext(x)

解释:

  • RegNext(x) 返回一个单周期延迟的值。
  • 函数 delay 接收一个输入 x,并返回延迟一周期的输出。

多次调用实现多周期延迟:

scala
val delOut = delay(delay(delay(delIn)))
  • 通过多次嵌套调用 delay,可以实现多周期延迟。
  • 例如,这里输入 delIn 将被延迟三周期。

返回多个输出的函数

在 Scala 中,函数默认只能返回单一值。但可以通过 元组 (Tuple) 结构返回多个值。这在硬件设计中非常有用,适合生成带多个输出的组件。

示例:比较器生成器 以下函数实现两个输入的比较,并返回两个输出:

  1. equ:两个输入是否相等。
  2. gt:第一个输入是否大于第二个输入。
scala
def compare(a: UInt, b: UInt) = {
  val equ = a === b
  val gt = a > b
  (equ, gt) // 返回一个包含两个值的元组
}

调用比较器并获取结果:

scala
val cmp = compare(inA, inB)  
val equResult = cmp._1 // 访问元组的第一个值  
val gtResult  = cmp._2 // 访问元组的第二个值
  • .n 语法:用于访问元组中的第 n 个元素(从 1 开始)。

直接解构元组:

scala
val (equ, gt) = compare(inA, inB)
  • 直接将元组的两个值解构到 equgt 变量中,代码更加简洁。

函数与模块的区别

  1. 函数
    • 更轻量,不需要声明输入输出。
    • 适合生成简单逻辑或重复性的硬件组件。
    • 返回值代表生成的硬件逻辑。
  2. 模块 (Module)
    • 更适合复杂逻辑设计,支持更好的结构化组织。
    • 每个模块可以包含多个输入输出和状态逻辑。

最佳实践:

  • 对于简单逻辑,使用函数生成轻量级硬件组件。
  • 对于复杂逻辑,使用模块来组织和封装。
  • 若函数需要在不同模块中复用,可以将其放入 Scala 对象 (Object) 中,作为工具函数库使用。

10.3 参数化配置

Chisel 允许我们通过参数来配置硬件组件和函数。这些参数可以是简单的整数常量,也可以是更复杂的 Chisel 硬件类型。

10.3.1 简单参数化 (Simple Parameters)

最简单的参数化方法是将位宽或其他设计参数定义为可变参数。我们可以将这些参数作为 Chisel 模块的构造函数参数传入,从而实现灵活的硬件生成。

示例:参数化加法器

以下代码展示了如何创建一个 位宽可配置的加法器

  • 参数 n 代表加法器的位宽,类型为 Int
  • 参数通过构造函数传入,并在模块内被使用。

代码实现:

scala
class ParamAdder(n: Int) extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(n.W))  // 位宽为 n 的输入 a
    val b = Input(UInt(n.W))  // 位宽为 n 的输入 b
    val c = Output(UInt(n.W)) // 位宽为 n 的输出 c
  })

  io.c := io.a + io.b // 实现加法器逻辑
}

实例化不同位宽的加法器:

scala
val add8 = Module(new ParamAdder(8))   // 8 位加法器
val add16 = Module(new ParamAdder(16)) // 16 位加法器

解释:

  1. 参数 n 使得加法器的位宽可以灵活配置。
  2. 每次调用 new ParamAdder(n),都会生成一个对应位宽的加法器实例。
  3. 参数化设计使硬件模块更具通用性,可复用于不同位宽的需求。

10.3.2 使用 Case Class 组织参数

当设计需要多个参数时,逐个传递参数会显得繁琐,特别是当参数数量较多或参数类型较复杂时。

解决方法:使用 Case Class

Scala 提供了 case class,这是一种轻量级类定义方式,适用于将多个参数打包成一个整体。

  • 优势:易于创建、访问和传递参数。
  • 不变性case class 的字段是不可变的。

示例:使用 Case Class 组织参数

定义一个 Config 类,包含以下三个参数:

  • txDepth:传输缓冲区深度。
  • rxDepth:接收缓冲区深度。
  • width:位宽。

代码实现:

scala
case class Config(txDepth: Int, rxDepth: Int, width: Int)

创建配置对象:

scala
val param = Config(4, 2, 16) // 创建一个配置对象
println("The width is " + param.width) // 访问参数

输出结果:

The width is 16

验证参数合法性

我们可以在 case class 中添加约束,确保参数的有效性。例如,要求参数必须大于 0:

代码实现:

scala
case class SaveConf(txDepth: Int, rxDepth: Int, width: Int) {
  assert(txDepth > 0 && rxDepth > 0 && width > 0, 
    "parameters must be larger than 0")
}

解释:

  1. assert 语句:用于检查参数是否满足条件。
  2. 如果 txDepthrxDepthwidth 小于等于 0,程序会报错并给出提示信息。
  3. 这种约束可以在模块构建阶段确保输入参数的有效性,避免生成不合理的硬件配置。

10.3.3 带有类型参数的函数

在参数化设计中,仅使用位宽作为参数只是硬件生成器的起点。Chisel 支持更灵活的配置方式:类型参数。通过使用类型参数,我们可以设计一个接受任意类型的多路复用器 (Mux),适应不同的硬件需求。

类型参数与多路复用器 (myMux) 示例

下面的例子展示了如何使用 Chisel 的类型参数构建一个通用多路复用器,能够接受任意 Chisel 类型。

定义通用多路复用器

scala
def myMux[T <: Data](sel: Bool, tPath: T, fPath: T): T = {
  val ret = WireDefault(fPath) // 定义一个默认值为 fPath 的 Wire
  when(sel) {
    ret := tPath // 如果 sel 为 true,选择 tPath
  }
  ret // 返回结果
}

解释:

  1. T <: Data:类型参数 T 需要是 Data 类型或 Data 的子类,Data 是 Chisel 类型系统的根类。
  2. 参数:
    • sel:布尔类型的选择信号。
    • tPathfPath:两个路径的值,类型为 T
  3. 功能:
    • 如果 seltrueret 的值取自 tPath
    • 否则,ret 的值保持为 fPath
  4. 返回值:
    • 返回生成的硬件多路复用器的输出。

使用示例:简单类型

调用 myMux 并传入简单的 UInt 类型作为参数:

scala
val resA = myMux(selA, 5.U, 10.U) // 选择 5 或 10

注意:

  • tPathfPath 必须具有相同的类型,否则会引发错误。
scala
val resErr = myMux(selA, 5.U, 10.S) // 错误!类型不匹配
  • 5.UUInt 类型,而 10.SSInt 类型,类型不一致导致错误。

使用复杂类型

为了展示 myMux 的强大,我们定义一个新的复杂类型 ComplexIO,它是一个包含两个字段的 Bundle

定义 ComplexIO 类型:

scala
class ComplexIO extends Bundle {
  val d = UInt(10.W) // 10 位无符号整数
  val b = Bool()     // 布尔类型
}

设置 ComplexIO 的值:

scala
// 创建 tPath 和 fPath 的 Bundle 实例
val tVal = Wire(new ComplexIO)
tVal.b := true.B
tVal.d := 42.U

val fVal = Wire(new ComplexIO)
fVal.b := false.B
fVal.d := 13.U

调用 myMux 并传入复杂类型:

scala
val resB = myMux(selB, tVal, fVal)

解释:

  • selB 控制多路复用器的选择。
  • selBtrue 时,选择 tVal;否则选择 fVal
  • tValfValComplexIO 类型,包含两个字段 bd

使用 cloneType 处理复杂类型

在上面的例子中,WireDefault 使用了 fPath 的默认值来创建输出 ret。如果不想使用默认值,可以通过 cloneType 复制 Chisel 类型。

改进的 myMux 实现:

scala
def myMuxAlt[T <: Data](sel: Bool, tPath: T, fPath: T): T = {
  val ret = Wire(fPath.cloneType) // 使用 cloneType 复制类型
  ret := fPath
  when(sel) {
    ret := tPath
  }
  ret
}

解释:

  • cloneType:复制 fPath 的类型,而不设置默认值。
  • 使用 cloneType 更适合处理复杂类型,确保类型的一致性和灵活性。

10.3.4 带有类型参数的模块

除了函数可以使用类型参数,Chisel 模块也可以进行类型参数化。类型参数化使模块更加灵活,可以处理不同的数据类型,例如设计一个网络路由器(Network-on-Chip, NoC)来在多个处理核心之间传输数据时,我们希望数据的格式能够被参数化,而不是写死在模块内部。

模块的类型参数化

在模块的构造函数中添加一个类型参数 T,允许模块能够接受任意 Data 类型或其子类。例如,下面的代码定义了一个 NoC 路由器 模块:

scala
class NocRouter[T <: Data](dt: T, n: Int) extends Module {
  val io = IO(new Bundle {
    val inPort   = Input(Vec(n, dt))            // 输入端口:n 个
    val address  = Input(Vec(n, UInt(8.W)))     // 地址端口:n 个
    val outPort  = Output(Vec(n, dt))           // 输出端口:n 个
  })
  
  // Route the payload according to the address
  // ...
}

解释:

  1. T <: Data:类型参数 T 必须是 Data 类型或 Data 的子类。
  2. dt:构造函数接收一个具体的类型实例,例如 UInt 或一个 Bundle
  3. n:配置路由器的端口数量。
  4. 输入/输出端口:
    • inPortn 个输入端口,类型为 T
    • addressn 个地址端口,8 位 UInt 类型。
    • outPortn 个输出端口,类型为 T

定义具体的数据类型

在使用 NocRouter 之前,我们需要定义一个具体的数据类型。这里我们通过 Bundle 定义一个 Payload 数据结构:

scala
class Payload extends Bundle {
  val data = UInt(16.W) // 数据位宽为 16 位
  val flag = Bool()     // 标志位
}

实例化路由器模块:

scala
val router = Module(new NocRouter(new Payload, 2))

解释:

  1. new Payload:传递 Payload 类型作为路由器的数据类型。
  2. 2:配置路由器的端口数量为 2。

10.3.5 参数化的 Bundle

在上述例子中,NocRouter 模块使用了两个不同的向量来表示输入:一个是数据向量,另一个是地址向量。我们可以进一步优化,使 Bundle 自身也支持类型参数化。

参数化的 Bundle 定义:

scala
class Port[T <: Data](dt: T) extends Bundle {
  val address = UInt(8.W)
  val data    = dt.cloneType
}

解释:

  1. T <: Data:类型参数 TData 类型或其子类。

  2. dt.cloneType:复制类型 T,确保类型信息的完整性。

  3. address

    data

    • address:固定为 8 位 UInt 类型。
    • data:由类型参数 T 决定的字段,使用 cloneType 确保类型一致。

解决 Bundle 的字段暴露问题

dt 作为构造函数的参数时,它会自动成为 Bundle 的公共字段(public field)。在某些情况下(例如克隆类型时),这个字段可能会导致冲突。因此,可以通过 private 修饰符将其设为私有:

改进后的参数化 Bundle:

scala
class Port[T <: Data](private val dt: T) extends Bundle {
  val address = UInt(8.W)
  val data    = dt.cloneType
}

重新定义 NocRouter 并使用参数化的 Port

在改进后的版本中,NocRouter 使用 Port 代替输入输出的 Vec

scala
class NocRouter2[T <: Data](dt: T, n: Int) extends Module {
  val io = IO(new Bundle {
    val inPort  = Input(Vec(n, dt))    // 输入端口
    val outPort = Output(Vec(n, dt))   // 输出端口
  })
  
  // Route the payload according to the address
  // ...
}

实例化改进后的 NocRouter2

scala
val router = Module(new NocRouter2(new Port(new Payload), 2))

10.4 生成组合逻辑

组合逻辑(也称为逻辑表或真值表)是一种常见的数字电路设计。它也可以被视为只读存储器(ROM),其中输入被用作表的地址,并输出对应的数据。在 Chisel 中,我们可以通过 VecInit 轻松生成这样的逻辑表。

使用 VecInit 生成逻辑表

示例:计算数字的平方 下面的代码片段演示了如何创建一个逻辑表来计算某个数字 nn 的平方:

scala
val squareROM = VecInit(0.U, 1.U, 4.U, 9.U, 16.U, 25.U)
val square = squareROM(n)

说明:

  1. VecInit:用于初始化一个 Vec 类型(向量),其中包含固定的元素。
  2. squareROM:定义了一个逻辑表(ROM),存储输入 nn 的平方结果。
  3. squareROM(n):将输入 nn 作为表的索引,返回平方结果。

生成更复杂的逻辑表

Chisel 和 Scala 的结合使我们可以充分利用 Scala 的编程能力生成复杂的逻辑表。例如,计算三角函数的固定点常数、数字滤波器系数,甚至编写一个汇编程序生成逻辑表代码。

示例:二进制转 BCD 转换表

下面是一个二进制数转换为 BCD(Binary-Coded Decimal)编码的示例。在 BCD 中,每个十进制数字用 4 位二进制表示。 例如:

  • 二进制1101(13)
  • BCD 表示0001 0011(1 和 3)。

代码实现:

scala
import chisel3._

class BcdTable extends Module {
  val io = IO(new Bundle {
    val address = Input(UInt(8.W))
    val data    = Output(UInt(8.W))
  })

  val table = Wire(Vec(100, UInt(8.W)))

  // Convert binary to BCD
  for (i <- 0 until 100) {
    table(i) := (((i / 10) << 4) + (i % 10)).U
  }

  io.data := table(io.address)
}

代码解析:

  1. table:一个 Vec 向量,用于存储 0 到 99 的 BCD 转换结果。

  2. for 循环 :遍历 0 到 99,计算每个数字的 BCD:

    • i / 10:计算十位。
    • i % 10:计算个位。
    • << 4:将十位移到高 4 位(乘以 16)。
  3. io.data := table(io.address):将输入的 address 作为索引,输出对应的 BCD 数据。

从文件读取数据生成逻辑表

有时,我们可能需要将外部文件中的数据导入并生成逻辑表。例如,从一个文本文件 data.txt 读取整数常量,然后生成逻辑表。可以使用 Scala 的 Source 类实现这一目标。

Scala 代码示例:

scala
import scala.io.Source

val array = Source.fromFile("data.txt").getLines.map(_.toInt).toSeq
val table = VecInit(array.map(_.U(8.W)))

代码解释:

  1. Source.fromFile:从文件 data.txt 中读取数据。
  2. map(_.toInt):将每一行转换为整数。
  3. VecInit(array.map(_.U(8.W))):将整数序列转换为 UInt 类型的 Chisel 向量。

VecInit 和 Scala Array 的关系

VecInit 可以从 Scala 的 ArraySeq 序列中创建一个 Chisel 向量。

  • map 函数:用于将 Scala Array 中的元素逐一转换为 Chisel 类型。
  • _.U:将 Scala Int 类型的值转换为 Chisel UInt 类型。

示例:Scala 字符串到 Chisel Vec 以下代码将一个字符串转换为 Chisel 的 Vec

scala
val msg = "Hello World!"
val text = VecInit(msg.map(_.U))
val len = msg.length.U

解释:

  1. msg.map(_.U):将字符串中的每个字符映射为 Chisel UInt 类型。
  2. VecInit:将映射后的结果转换为一个 Chisel Vec
  3. len:计算字符串的长度。

10.5 使用继承

Chisel 是基于 Scala 的硬件描述语言,而 Scala 是一种面向对象的编程语言。因此,Chisel 模块也可以作为类进行继承,这使得硬件设计中复用代码扩展功能更加高效。我们可以通过继承的方式实现通用模块,并在此基础上扩展或重写功能。

基础类与继承的实现

我们以生成定时器(ticker)的模块为例,演示如何使用继承来定义不同版本的定时器。

基类定义:Ticker

首先,我们定义一个通用的基类 Ticker。该类提供了通用的定时器逻辑,其他定时器模块可以通过继承它来扩展功能。

scala
abstract class Ticker(n: Int) extends Module {
  val io = IO(new Bundle {
    val tick = Output(Bool())
  })
}

代码解释:

  1. abstract 关键字Ticker 是一个抽象类,用于定义公共接口,不能直接实例化。
  2. 参数化设计: 构造函数 Ticker(n: Int) 允许子类实现时传入参数 n,可以根据参数自定义具体的硬件行为。
  3. IO 定义: 抽象类中定义了一个输出 tick 信号,供具体实现类进行输出逻辑的定义。

作用:

  • 为不同的 Ticker 实现(如上升计数器或下降计数器)提供了一个通用的接口与设计框架。
  • 子类可以扩展此基类并实现不同的 tick 生成逻辑。

派生类:不同版本的 Ticker

基于 Ticker 类,我们可以定义多个派生类,每个派生类实现不同的计数逻辑,例如向上计数向下计数,或从指定值开始计数。

1. 向上计数的 Ticker

scala
class UpTicker(n: Int) extends Ticker(n) {
  val s = (n - 1).U
  val cntReg = RegInit(0.U(8.W))

  cntReg := cntReg + 1.U
  when(cntReg === s) {
    cntReg := 0.U
  }

  io.tick := cntReg === s
}

说明:

  • UpTicker:派生于 Ticker,实现向上计数逻辑。
  • cntReg:计数寄存器,每个时钟周期加 1。
  • cntReg 达到最大值 n-1 时,重置为 0,并输出一个 tick 信号。

2. 向下计数的 Ticker

scala
class DownTicker(n: Int) extends Ticker(n) {
  val s = (n - 1).U
  val cntReg = RegInit(s)

  cntReg := cntReg - 1.U
  when(cntReg === 0.U) {
    cntReg := s
  }

  io.tick := cntReg === 0.U
}

说明:

  • DownTicker:派生于 Ticker,实现向下计数逻辑。
  • cntReg:计数寄存器,每个时钟周期减 1。
  • cntReg 到达 0 时,重置为最大值 n-1,并输出一个 tick 信号。

3. 特殊 NerdTicker

scala
class NerdTicker(n: Int) extends Ticker(n) {
  val MAX = (n - 2).S(8.W)
  val cntReg = RegInit(MAX)
  io.tick := false.B

  cntReg := cntReg - 1.S
  when(cntReg(7)) {
    cntReg := MAX
    io.tick := true.B
  }
}

说明:

  • NerdTicker:实现一种特殊的定时器,它以 -1 为特殊比较条件。
  • 当寄存器的最高位为 1 时(即负数),表示定时器已完成计数,输出 tick 信号,并重置寄存器。

测试类:验证 Ticker 功能

为了验证不同派生类的功能,我们编写了一个测试类 TickerTester。该类可以测试不同版本的 Ticker 逻辑。

scala
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec

class TickerTest extends AnyFlatSpec with ChiselScalatestTester {
  behavior of "Ticker"

  it should "pass for UpTicker" in {
    test(new UpTicker(5)) { dut =>
      dut.io.tick.expect(false.B)
    }
  }

  it should "pass for DownTicker" in {
    test(new DownTicker(5)) { dut =>
      dut.io.tick.expect(false.B)
    }
  }

  it should "pass for NerdTicker" in {
    test(new NerdTicker(5)) { dut =>
      dut.io.tick.expect(false.B)
    }
  }
}

说明:

  • 使用 ChiselScalatestTester 框架进行测试。

  • 测试逻辑 :

    • 创建不同版本的 Ticker(UpTickerDownTickerNerdTicker)。
    • 验证它们的 tick 信号输出是否符合预期。

总结

  1. 基类与继承
    • 定义一个通用的 Ticker 类,提供基础的定时器逻辑。
    • 使用继承扩展功能,实现不同的计数逻辑。
  2. 派生类的实现
    • UpTicker:向上计数。
    • DownTicker:向下计数。
    • NerdTicker:实现特殊的计数逻辑(-1 比较)。
  3. 测试类:通过 ChiselScalatestTester 验证不同版本的 Ticker 功能。

通过继承和多态,我们可以高效复用代码,设计灵活可扩展的硬件模块。

10.6 用函数式编程进行硬件生成

Chisel 支持函数式编程,因此可以使用函数来表示硬件,并通过函数式编程的组合特性生成硬件组件。这种方式允许我们通过高阶函数生成更复杂的硬件结构。

向量求和的简单示例

我们从一个简单的例子开始,定义向量求和的硬件:

scala
def add(a: UInt, b: UInt) = a + b
val sum = vec.reduce(add)

代码解释:

  1. add 函数:定义了两个 UInt 类型输入的求和功能。
  2. reduce 方法:将向量 vec 中的所有元素通过 add 函数进行归约操作(reduce),从第一个元素开始,将其与下一个元素进行求和,直到所有元素累加完成。

最终,reduce 方法生成一个由加法器链组成的硬件结构。

使用匿名函数优化

我们可以通过匿名函数简化代码,而不必定义一个独立的函数:

scala
val sum = vec.reduce(_ + _)

解释:

  • _ + _ 表示一个匿名函数,自动绑定两个输入参数并执行加法操作。
  • 这种写法更为简洁,但生成的硬件结构与前面的代码相同。

10.6.1 最小搜索示例

目标:

构建一个硬件电路,从向量 Vec 中找出最小值,并返回最小值及其索引

步骤 1:实现基本的最小值搜索

首先,我们通过 reduceTree 方法和多路选择器Mux)找到向量中的最小值:

scala
val min = vec.reduceTree((x, y) => Mux(x < y, x, y))

代码解释:

  • reduceTree:类似 reduce,但会构建一个树形的硬件结构。
  • Mux:多路选择器,根据条件 x < y 判断,返回 xy

步骤 2:返回最小值及其索引

为了同时返回最小值及其索引,我们需要定义一个 Bundle,用于存储这两个值。

scala
class Two extends Bundle {
  val v   = UInt(8.W)  // 最小值
  val idx = UInt(8.W)  // 索引
}

val vecTwo = Wire(Vec(n, new Two()))
for (i <- 0 until n) {
  vecTwo(i).v   := vec(i)
  vecTwo(i).idx := i.U
}

val res = vecTwo.reduceTree((x, y) => Mux(x.v < y.v, x, y))

代码解释:

  1. class Two:定义了一个 Bundle,包含最小值 v 和索引 idx
  2. vecTwo:创建一个包含 Two 类型的向量,每个元素包含向量的值和索引。
  3. reduceTree:在向量 vecTwo 上执行最小值搜索,通过比较 v 字段(值)找到最小值,同时保留索引。

最终,res 包含了最小值和对应的索引。

步骤 3:更进一步的 Scala 和 Chisel 结合

我们可以使用Scala 库的功能进一步优化搜索过程。例如,使用 zipWithIndex 将向量值与索引绑定,然后将其映射到 Chisel 类型:

scala
val resFun = vec.zipWithIndex
  .map(x => MixedVecInit(x._1, x._2.U(8.W)))
  .reduceTree((x, y) => Mux(x(0) < y(0), x, y))

val minVal = resFun(0)  // 最小值
val minIdx = resFun(1)  // 最小值的索引

代码解释:

  1. zipWithIndex:将向量 vec 的值与其索引绑定,结果是一个 Scala 序列。
  2. MixedVecInit:将值和索引转化为 Chisel 类型(MixedVec),方便在 Chisel 中使用。
  3. reduceTree:通过比较两个元素的第 0 位(值)找到最小值,最终得到最小值及其索引。

总结

  1. 函数式编程与硬件生成
    • 使用 reducereduceTree 方法,可以构建加法器链树形硬件结构
    • 使用 Mux 实现多路选择,组合出复杂的比较逻辑。
  2. 最小搜索的实现
    • 定义一个 Bundle 存储值和索引,使用 reduceTree 搜索最小值及其索引。
    • 使用 zipWithIndexMixedVec 简化搜索过程,将 Scala 功能与 Chisel 结合使用。
  3. 优雅的函数式编程
    • Chisel 的函数式编程特性,使得代码更为简洁可复用,同时生成高效的硬件结构。

感谢您的指正,您提到的代码属于不同章节且顺序有所混淆。这里是清晰归类后的代码解释与章节对应:

10.4 Generate Combinational Logic

文本文件生成逻辑表

代码示例 Listing 10.2 说明了如何读取一个文本文件并将其内容用于生成一个逻辑表(ROM):

scala
import chisel3._
import scala.io.Source

class FileReader extends Module {
  val io = IO(new Bundle {
    val address = Input(UInt(8.W))
    val data = Output(UInt(8.W))
  })

  val array = new Array 
  var idx = 0

  // 读取文本文件中的数据到 Scala 数组
  val source = Source.fromFile("data.txt")
  for (line <- source.getLines()) {
    array(idx) = line.toInt
    idx += 1
  }

  // 将 Scala 整数数组转换为 Seq 并转化为 Chisel Vec
  val table = VecInit(array.toIndexedSeq.map(_.U(8.W)))

  // 使用 table 进行地址查询
  io.data := table(io.address)
}

代码解释:

  1. 文本读取: 使用 Scala 的 Source.fromFiledata.txt 逐行读取数据并存储到 array 数组中。
  2. 转换为 Vec: 将 array 转换为 Chisel 的 Vec 类型,使用 toIndexedSeqmap 方法将每个整数转换为 UInt
  3. 生成逻辑表: 最终的 table 是一个只读存储器(ROM),可以通过输入地址 io.address 查询相应数据 io.data