Chisel教程——10.Chisel标准库中的中间件整理(接口和函数)

Chisel标准库中的中间件整理(接口和函数)

动机

Chisel很重要的特性就是复用,所以提供一个标准库很有意义。Chisel的标准库中有针对常用硬件模块的接口库(鼓励和RTL之间的互通性)和生成器库,非常好用。

Chisel Cheatsheet

Chisel教程——03.Chisel中的组合逻辑(结尾附上Chisel3 Cheat Sheet)_github-3rr0r的博客-CSDN博客_chisel教程一文中,我给出了Chisel的Cheatsheet,里面概括了全部主要的硬件构造API,包括了一些标准库实用工具(utility),本篇博客将会介绍它们。这里再一次放上Chisel的Cheatsheet:

在这里插入图片描述
在这里插入图片描述

重要Chisel标准库接口

Decoupled——标准的Ready-Valid接口

这个前面有介绍过。DecoupledIO是Chisel提供的最常用的接口之一,提供了ready-valid接口用于传输数据。其基本思想是,数据发送端在存在要传输的数据是,驱动要发送的数据信号bits和的valid信号,接收端驱动在准备好接受数据时驱动ready信号,当readyvalid信号在一个周期被声称(asserted,即被设置为有效)时,数据被认为成功传输了。

这为数据传输提供了双向流控制机制,包括背压机制(Backpressure Mechanism)。

这里解释一下这个背压机制,在前面将DecoupledIO其实遇到过这个词,但是当时没翻译出来,这里稍微解释一下。首先看看百度百科对“背压”的解释:

一、通常是指运动流体在密闭容器中沿其路径(譬如管路或风通路)流动时,由于受到障碍物或急转弯道的阻碍而被施加的与运动方向相反的压力。二、通常用于描述系统排出的流体在出口处或二次侧的压力。

显然,我们这里的背压指的是第二种,即数据传输在出口处遇到的压力,说人话就是接收端已经接受不了数据了,你发送端还在发,发不过来了啊。如果数据发送端发送数据速率太快,接收端来不及接受消化,导致发送端数据积压,这就是背压现象。而背压机制就是用来解决背压现象的——这里设置了ready信号,接收端没准备好,就别发数据了,也别再生成数据了,通过限制发送端的生产速率来解决背压现象。

回到正题,需要注意的是,ready信号和valid信号不可以耦合组合到一起,即两者不可以相互依赖,不然就会导致综合不了的组合循环。ready应该只依赖于接收端是否有能力接收数据,valid应该只依赖于发送端有没有数据要发送。只有在事务之后(下一个时钟周期),才会更新值。

任何的Chisel数据都可以用DecoupledIO来包装,然后成为一个bits字段,像这样:

val myChiselData = UInt(8.W)
// 其他数据类型也行,比如Bool()、SInt(...),甚至自定义的Bundle都行
val myDecoupled = Decoupled(myChiselData)

上面这两行代码将会创建一个新的DecoupledIOBundle,具有以下字段:

  1. validOutput(Bool)
  2. readyInput(Bool)
  3. bitsOutput(UInt(8.W))

讲完了DecoupledIO,剩下的部分将会针对每一个中间件给出一些代码实例和输出电路状态的测试案例,可以在学习代码的过程中,运行代码之前预测一下会输出什么内容。

Queue——队列

Queue可以创建一个FIFO(First-In First-Out,先进先出)队列,这个队列的两边都有Decoupled接口,允许背压。数据类型和元素数量都是可以配置的。

其用法为:

// enq为队列的源,entries为队列的元素个数
val queue = Queue(enq: DecoupledIO, entries: Int)

观察下面的代码,可以注意到前几次我们打印出peek的值时是在其上调用了litValue。它会将Chisel字面值转变回BigInt。后面我们没有调用litValue了,可以看到peek返回的值的额外信息,例如类型和宽度。

简单解释一下输出的三个信号:

  1. io.in.ready:队列已经准备好接受数据了;
  2. io.out.valid:队列已经准备好输出数据了;
  3. io.out.bits:队列准备好的数据;
test(new Module {
  // 使用Queue的示例电路
  val io = IO(new Bundle {
    val in = Flipped(Decoupled(UInt(8.W)))
    val out = Decoupled(UInt(8.W))
  })
  val queue = Queue(io.in, 2)  // 2元素的队列
  io.out <> queue
}) { c =>
  c.io.out.ready.poke(false.B)	// 不能输出值
  c.io.in.valid.poke(true.B)  // 可以入列
  c.io.in.bits.poke(42.U)	// 42准备入列
  println(s"Starting:")
  println(s"\tio.in: ready=${c.io.in.ready.peek().litValue}")
  println(s"\tio.out: valid=${c.io.out.valid.peek().litValue}, bits=${c.io.out.bits.peek().litValue}")
  c.clock.step(1)	// 42入列

  c.io.in.valid.poke(true.B)  // 另一个元素可以入列
  c.io.in.bits.poke(43.U)	// 43准备入列
  // 分析看看io.out.valid和io.out.bits会输出什么
  println(s"After first enqueue:")
  println(s"\tio.in: ready=${c.io.in.ready.peek().litValue}")
  println(s"\tio.out: valid=${c.io.out.valid.peek().litValue}, bits=${c.io.out.bits.peek().litValue}")
  c.clock.step(1)	// 43入列

  c.io.in.valid.poke(true.B)  // 准备入列,但是队列是满的
  c.io.in.bits.poke(44.U)	// 给了信号但是入不了
  c.io.out.ready.poke(true.B)	// 可以出列了
  // What do you think io.in.ready will be, and will this enqueue succeed, and what will be read?
  println(s"On first read:")
  println(s"\tio.in: ready=${c.io.in.ready.peek()}")
  println(s"\tio.out: valid=${c.io.out.valid.peek()}, bits=${c.io.out.bits.peek()}")
  c.clock.step(1)	// 42出列,44入列失败

  c.io.in.valid.poke(false.B)  // 不读元素了
  c.io.out.ready.poke(true.B)	// 可以继续输出
  // What do you think will be read here?
  println(s"On second read:")
  println(s"\tio.in: ready=${c.io.in.ready.peek()}")
  println(s"\tio.out: valid=${c.io.out.valid.peek()}, bits=${c.io.out.bits.peek()}")
  c.clock.step(1)	// 43出列,队列空了

  // 这时候还会读到东西吗?
  println(s"On third read:")
  println(s"\tio.in: ready=${c.io.in.ready.peek()}")
  println(s"\tio.out: valid=${c.io.out.valid.peek()}, bits=${c.io.out.bits.peek()}")
  c.clock.step(1)
}

输出如下:

Starting:
        io.in: ready=1
        io.out: valid=0, bits=0
After first enqueue:
        io.in: ready=1
        io.out: valid=1, bits=42
On first read:
        io.in: ready=Bool(false)
        io.out: valid=Bool(true), bits=UInt<8>(42)
On second read:
        io.in: ready=Bool(true)
        io.out: valid=Bool(true), bits=UInt<8>(43)
On third read:
        io.in: ready=Bool(true)
        io.out: valid=Bool(false), bits=UInt<8>(42)

前几次都在意料之内,但是最后一次io.out.bits给出的信号值居然是已经出列了的42,这说明了两个问题:一是元素出列并不是真的出去了,而是输出了之后把那个元素的valid置为0了;二是队列的所有元素出列后,队列的“指针”会切换到下一个元素。

Arbiter——仲裁器

仲裁器根据给定的优先级,仲裁n个DecoupledIO发送端的数据到一个DecoupledIO接收端。Chisel中有两类仲裁器:

  1. Arbiter:低索引优先;
  2. RRArbiter:Round-Robin,上一次输出的输入口的下一个输入口优先级最高;

需要注意的是,仲裁器仲裁是通过组合逻辑实现的。

下面的例子演示了优先级仲裁器的使用:

test(new Module {
  // 使用优先级仲裁器的示例电路
  val io = IO(new Bundle {
    val in = Flipped(Vec(2, Decoupled(UInt(8.W))))
    val out = Decoupled(UInt(8.W))
  })
  // Arbiter没有方便的构造器,所以只能按照Module来构造
  val arbiter = Module(new Arbiter(UInt(8.W), 2))  // 2选1的优先级仲裁器
  arbiter.io.in <> io.in
  io.out <> arbiter.io.out
}) { c =>
  c.io.in(0).valid.poke(false.B)	// 两个输入都不输入数据
  c.io.in(1).valid.poke(false.B)
  c.io.out.ready.poke(false.B)	// 也不允许输出
  println(s"Start:")
  println(s"\tin(0).ready=${c.io.in(0).ready.peek().litValue}, in(1).ready=${c.io.in(1).ready.peek().litValue}")
  println(s"\tout.valid=${c.io.out.valid.peek().litValue}, out.bits=${c.io.out.bits.peek().litValue}")
  c.io.in(1).valid.poke(true.B)  // 输入1有效
  c.io.in(1).bits.poke(42.U)	// 输入1为42
  c.io.out.ready.poke(true.B)	// 可以输出了
  // 这里会输出什么
  println(s"valid input 1:")
  println(s"\tin(0).ready=${c.io.in(0).ready.peek().litValue}, in(1).ready=${c.io.in(1).ready.peek().litValue}")
  println(s"\tout.valid=${c.io.out.valid.peek().litValue}, out.bits=${c.io.out.bits.peek().litValue}")
  c.io.in(0).valid.poke(true.B)  // 输入0也有效了
  c.io.in(0).bits.poke(43.U)	// 输入0位43
  // 输出会是多少? 哪个输入的ready会被设置?
  println(s"valid inputs 0 and 1:")
  println(s"\tin(0).ready=${c.io.in(0).ready.peek().litValue}, in(1).ready=${c.io.in(1).ready.peek().litValue}")
  println(s"\tout.valid=${c.io.out.valid.peek().litValue}, out.bits=${c.io.out.bits.peek().litValue}")
  c.io.in(1).valid.poke(false.B)  // 仅输入0有效
  // 这里会输出什么
  println(s"valid input 0:")
  println(s"\tin(0).ready=${c.io.in(0).ready.peek().litValue}, in(1).ready=${c.io.in(1).ready.peek().litValue}")
  println(s"\tout.valid=${c.io.out.valid.peek().litValue}, out.bits=${c.io.out.bits.peek().litValue}")
}

输出如下:

Start:
	in(0).ready=0, in(1).ready=0
	out.valid=0, out.bits=0
valid input 1:
	in(0).ready=1, in(1).ready=1
	out.valid=1, out.bits=42
valid inputs 0 and 1:
	in(0).ready=1, in(1).ready=0
	out.valid=1, out.bits=43
valid input 0:
	in(0).ready=1, in(1).ready=0
	out.valid=1, out.bits=43

总结一下,对于低索引优先的仲裁器:

  1. 是组合逻辑!
  2. 某个索引有有效输入时,小于等于该索引的都会被置为ready;
  3. 对于多个ready的输入,其中有效输入中索引最低的会成为输出。

Chisel标准库方法

Chisel实用工具中有一些helper可以执行无状态方法,即函数本身没有状态信息,本节介绍的几个函数除了最后的Counter以外,其他的都是无状态方法。

按位的方法

PopCount

PopCount会返回输入中高电平(1)的比特位的数量,返回值类型为UInt,示例如下:

test(new Module {
  // 使用PopCount的示例电路
  val io = IO(new Bundle {
    val in = Input(UInt(8.W))
    val out = Output(UInt(8.W))
  })
  io.out := PopCount(io.in)
}) { c =>
  // Integer.parseInt用于从一个二进制描述创建一个Int类型的值
  c.io.in.poke(Integer.parseInt("00000000", 2).U)
  println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")

  c.io.in.poke(Integer.parseInt("00001111", 2).U)
  println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")

  c.io.in.poke(Integer.parseInt("11001010", 2).U)
  println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")

  c.io.in.poke(Integer.parseInt("11111111", 2).U)
  println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
}

输出如下:

in=0b0, out=0
in=0b1111, out=4
in=0b11001010, out=4
in=0b11111111, out=8

可以看到,这个函数很简单,输入有多少个1就返回多少。

Reverse

Reverse会返回按比特翻转的输入,示例如下:

test(new Module {
  // 使用Reverse的示例电路
  val io = IO(new Bundle {
    val in = Input(UInt(8.W))
    val out = Output(UInt(8.W))
  })
  io.out := Reverse(io.in)
}) { c =>
  // Integer.parseInt用于从一个二进制描述创建一个Int类型的值
  c.io.in.poke(Integer.parseInt("01010101", 2).U)
  println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")

  c.io.in.poke(Integer.parseInt("00001111", 2).U)
  println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")

  c.io.in.poke(Integer.parseInt("11110000", 2).U)
  println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")

  c.io.in.poke(Integer.parseInt("11001010", 2).U)
  println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
}

输出如下:

in=0b1010101, out=0b10101010
in=0b1111, out=0b11110000
in=0b11110000, out=0b1111
in=0b11001010, out=0b1010011

这个也是很好理解的,就是直接翻转过来。

独热编码(OneHot encoding)

独热编码是整数编码的一种方式,每个值都有一个wire,但其中有且只有一个是高电平的。

听起来很复杂,其实举个例子就明白了,现在对0到15这16个数进行独热编码,那么就需要用到16位二进制数,第 i i i位置高电平就表示整数 i i i,如0b0000100000000000,第11位是1(从0开始索引),因此这个独热编码就表示11。

这个功能允许一些方法的高效创建,比如说多路选择器(mux)。不过如果wire高电平这个条件不保持的话,那行为就可能是未定义的。

下面两个函数提供了二进制(UInt)和独热编码之间的转换:

  1. UInt到OneHot:UIntToOH
  2. OneHot到UIntOHToUInt

使用UIntToOH的示例如下:

test(new Module {
  // 使用UIntToOH的示例电路
  val io = IO(new Bundle {
    val in = Input(UInt(4.W))
    val out = Output(UInt(16.W))
  })
  io.out := UIntToOH(io.in)
}) { c =>
  c.io.in.poke(0.U)
  println(s"in=${c.io.in.peek().litValue}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")

  c.io.in.poke(1.U)
  println(s"in=${c.io.in.peek().litValue}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")

  c.io.in.poke(8.U)
  println(s"in=${c.io.in.peek().litValue}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")

  c.io.in.poke(15.U)
  println(s"in=${c.io.in.peek().litValue}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
}

输出如下:

in=0, out=0b1
in=1, out=0b10
in=8, out=0b100000000
in=15, out=0b1000000000000000

使用OHToUInt的示例如下:

test(new Module {
  // 使用`OHToUInt`的示例电路
  val io = IO(new Bundle {
    val in = Input(UInt(16.W))
    val out = Output(UInt(4.W))
  })
  io.out := OHToUInt(io.in)
}) { c =>
  // 这里的replace和Python里面的字符串的replace用法是一样的
  c.io.in.poke(Integer.parseInt("0000 0000 0000 0001".replace(" ", ""), 2).U)
  println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")

  c.io.in.poke(Integer.parseInt("0000 0000 1000 0000".replace(" ", ""), 2).U)
  println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")

  c.io.in.poke(Integer.parseInt("1000 0000 0000 0001".replace(" ", ""), 2).U)
  println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")

  // Some invalid inputs:
  // None high
  c.io.in.poke(Integer.parseInt("0000 0000 0000 0000".replace(" ", ""), 2).U)
  println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")

  // Multiple high
  c.io.in.poke(Integer.parseInt("0001 0100 0010 0000".replace(" ", ""), 2).U)
  println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
}

输出如下:

in=0, out=0b1
in=1, out=0b10
in=8, out=0b100000000
in=15, out=0b1000000000000000

多路选择器(Mux)

多路选择器会接受多个有选择信号的值,然后输出和最低索引选择信号相关联的值。

多路选择器可以是一组(select: Bool, value: Data)元组作为参数,也可以是一组选择信号和一组值作为参数。简单起见,下面的例子值演示第二种形式。

优先级多路选择器(Priority Mux)

PriorityMux会输出设置了的选择信号中最低索引的相关联的值,示例如下:

test(new Module {
  // 使用PriorityMux的示例电路
  val io = IO(new Bundle {
    val in_sels = Input(Vec(2, Bool()))
    val in_bits = Input(Vec(2, UInt(8.W)))
    val out = Output(UInt(8.W))
  })
  io.out := PriorityMux(io.in_sels, io.in_bits)
}) { c =>
  c.io.in_bits(0).poke(10.U)
  c.io.in_bits(1).poke(20.U)

  // 只设置高索引的选择信号
  c.io.in_sels(0).poke(false.B)
  c.io.in_sels(1).poke(true.B)
  println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")

  // 两个选择信号都设置,需要仲裁
  c.io.in_sels(0).poke(true.B)
  c.io.in_sels(1).poke(true.B)
  println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")

  // 只设置第索引的选择信号
  c.io.in_sels(0).poke(true.B)
  c.io.in_sels(1).poke(false.B)
  println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")
}

输出如下:

in_sels=0, out=20
in_sels=1, out=10
in_sels=1, out=10

很容易理解,和优先级仲裁器是类似的,只不过从validready信号换成了sel选择信号而已。

独热多路选择器

Mux1H提供了一种在能保证有且只有一个选择信号会被设置的时候的高效实现,如果这个前提不成立的话那行为就是未定义的。示例如下:

test(new Module {
  // 使用Mux1H的示例电路
  val io = IO(new Bundle {
    val in_sels = Input(Vec(2, Bool()))
    val in_bits = Input(Vec(2, UInt(8.W)))
    val out = Output(UInt(8.W))
  })
  io.out := Mux1H(io.in_sels, io.in_bits)
}) { c =>
  c.io.in_bits(0).poke(10.U)
  c.io.in_bits(1).poke(20.U)

  // 只设置高索引的选择信号
  c.io.in_sels(0).poke(false.B)
  c.io.in_sels(1).poke(true.B)
  println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")

  // 只设置低索引的选择信号
  c.io.in_sels(0).poke(true.B)
  c.io.in_sels(1).poke(false.B)
  println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")

  // 两个都不选择,未定义的行为
  c.io.in_sels(0).poke(false.B)
  c.io.in_sels(1).poke(false.B)
  println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")

  // 两个都选择,也是未定义的行为
  c.io.in_sels(0).poke(true.B)
  c.io.in_sels(1).poke(true.B)
  println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")
}

输出如下:

in_sels=0, out=20
in_sels=1, out=10
in_sels=0, out=0
in_sels=1, out=30

可以看到,对于未定义的行为,输出很诡异,所以不要让这种情况发生。

Counter(计数器)

Counter是在每一时钟周期都会自增的计数器,最高可以到某个指定的值,到达该点就会溢出。需要注意的是,Counter不是个Module,它的值是可以直接访问的。示例如下:

test(new Module {
  // 使用Counter的示例电路
  val io = IO(new Bundle {
    val count = Input(Bool())
    val out = Output(UInt(2.W))
  })
  val counter = Counter(3)  // 上线为3的计数器,即输出范围是[0, 1, 2]
  when(io.count) {
    counter.inc()
  }
  io.out := counter.value
}) { c =>
  c.io.count.poke(true.B)
  println(s"start: counter value=${c.io.out.peek().litValue}")

  c.clock.step(1)
  println(s"step 1: counter value=${c.io.out.peek().litValue}")

  c.clock.step(1)
  println(s"step 2: counter value=${c.io.out.peek().litValue}")

  // 设置为false的时候就不会增长了
  c.io.count.poke(false.B)
  c.clock.step(1)
  println(s"step without increment: counter value=${c.io.out.peek().litValue}")
  
  // 到达上限就会清零
  c.io.count.poke(true.B)
  c.clock.step(1)
  println(s"step again: counter value=${c.io.out.peek().litValue}")
}

输出如下:

start: counter value=0
step 1: counter value=1
step 2: counter value=2
step without increment: counter value=2
step again: counter value=0

小结

上面整理的这些功能都是很方便的,在后面的开发中使用可以节省不少时间。

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值