文章目录
一、前言
这是记录学习chisel官方文档的笔记。原文档pdf下载链接在这里:
http://www.imm.dtu.dk/~masca/chisel-book-chinese.pdf
写这篇文章找到了另一篇官方的中文简介文章,可作为学习阶段反复阅读复习用的材料。
(下载链接:http://www.aiotek.pro/aiotek/doclib/chisel/chisel-getting-started-chinese.pdf)
二、Linux上对scala工程的操作
1. helloworld执行命令:
sbt "runMain <对象名(而不是文件名)>"
2. 有多个工程目录时,需要切换工程:
sbt
project
project <工程名>
ctrl+C
3. 编译报错:
[error] (run-main-0) java.lang.NoSuchMethodException: Hello.main([Ljava.lang.String;)
main就是object;代码中只有class是不够的;
[error] /home/cwq/6_chisel_book_and_example/1_cwq_example/2_Hello_hardware.scala:2:8: not found: object chisel3
缺少编译配置文件,默认是build.sbt;需要从别的工程里复制出来用;
4. 给vscode的scala插件设置JAVA_HOME路径:
确认JAVA_HOME路径的方法:https://www.cnblogs.com/huaisn/articles/14499330.html
三、(ch4)基本组成部分
ch4.1:信号类型与常量:
- 信号类型:Bits、UInt、SInt
- 常量:.W(表示信号类型或常量的宽度)、.U、.S
- 8.U(4.W)表示4bit宽度的常量8
- “hff”.U、“o377”.U、“b1111_1111”.U分别为十进制常量255.U在其它进制下的表示
- bool类型:true.B、false.B
ch4.2:组合电路:
- 算数操作符:(和其它语言一样的)加减乘除、取余、取反
- 逻辑操作符:与非或、异或、相等、不等
- 操作符的优先级取决于电路的赋值顺序(不同于其它语言):所以有必要使用括号
- chisel提供的复用器:
val result = Mux(<条件>, <条件为真的输出选择>, <条件为假的输出选择>)
ch4.3:状态寄存器:
- 寄存器定义:val reg = RegInit(0.U(8.W)),定义了一个八位寄存器,在复位初始化为0
- 寄存器用作计数器的示例:从0数到9,并重新返回0,以实现数10个数的目的
val cntReg = RegInit(0.U(8.W))
cntReg := Mux(cntReg === 10.U, 0.U, cntReg+1.U)
ch4.4:使用Bundle和Vec来构建
- Bundle:组合不同类型的信号
- Vec:组合可索引的相同类型的信号
- Bundle和Vec可以相互嵌套
- 定义一个Bundle类型、有初始值的寄存器:先创建Bundle类型的Wire变量,再给这个变量赋值,再用这个变量去定义寄存器
val initVal = Wire(new Channel())
initVal.data := 0.U
initVal.valid := false.B
val channelReg = RegInit(initVal)
四、(ch5)搭建过程和测试
ch5.1:使用sbt搭建你的项目
- 库文件通过build.sbt被引用
- 如果build.sbt设置latest.release则表示总是用最新的chisel版本,这意味着每次搭建都要联网查看maven仓库——实际上提倡无联网情况下的搭建
- “import <软件包名>._”表示包里的所有类都要被引用
- chisel工具流:参考文档中的fig5.2图,从.scala文件到生成.vcd波形文件和.v综合电路文件
ch5.2.1:PeekPokeTester
- chisel模块的单元测试:
sbt "runMain xxx"
ch5.2.2:使用scalaTester
- scala模块的单元测试:
sbt "testOnly xxx"
ch5.2.3:波形
- 在scalaTester下使用Driver.execute()代替Driver(),即可生成.vcd波形文件,用GTKWave(或ModelSim)可以打开
ch5.2.4:printf debugging
- printf是来源于C语言的另一种调试形式:在函数的任何地方都可以插入printf()函数
- printf支持C和scala两种风格
- 示例:略
五、(ch6)组成部分
ch6.1:chisel的组成部分是模块
- 模块的嵌套示例:fig6.1
- (重要)“硬件组件”在chisel代码里称为module,所以它们都用extends Module继承的方式来定义。并且里面一定要用IO(new Bundle())定义它的全部IO——Input和Output都在里面一起定义。
- (重要)“硬件组件”在chisel代码里称为module,所以它们都用extends Module继承的方式来定义。并且里面一定要用IO(new Bundle())定义它的全部IO——Input和Output都在里面一起定义。
ch6.2:一个运算逻辑单元
- 以一个简单的运算逻辑单元ALU为作为大Module的示例,讲解其内部fetch、decode、execute三个Module的互联关系
- 顺便引出:switch/is语句的使用,需要引入chisel3.util包
ch6.3:整体连接
- Bundle的整体双向互联,可用批量连接运算符"<>":Bundle中识别为同名的信号val,会互联到一起
ch6.4:使用函数的轻量级组成部分
- 函数(def):模块(class … extends Module)是构造硬件描述的通用方法。但是,也有一些“样板代码”可以在对模块进行声明、实例化、连接时使用(这就是函数)
- 示例1:用RegNext()函数构造延时一周期的新函数:
def delay(x:UInt) = RegNext(x)
- 示例2:调用上述函数,来定义一个“对输入变量延时两个周期后输出的变量”
def delay(x:UInt) = RegNext(x)
val delOut = delay(delay(defIn))
六、(ch7)组合搭建模块
ch7.1:组合电路
- 组合电路在chisel中的表示1:逻辑运算
- 最简单的就是定义一个变量名,其内容为布尔表达式
- val e = (a & b) | c
- val f= ~e
- 组合电路在chisel中的表示2:复用器(输出信号要定义为Wire(UInt()))
- 用chisel的when/.elsewhen/.otherwise表示二选一复用器的串联
- 用switch/is表示多选一复用器
- 说明:scala中也有if/else语句,但它不产生硬件,只是纯软件语句
ch7.2:解码器
- 以2/4解码器为例,演示switch/is语句在实现解码器中的用法
ch7.3:编码器
- 以4/2编码器为例,演示switch/is语句在实现编码器中的用法
七、(ch8)时序建造模块
“因为我们感兴趣的是同步设计,所以当我们说时序电路时,就意味着是同步时序电路”
ch8.1:寄存器
- 寄存器的时钟输入信号不需要定义:chisel已自动隐含添加
- 用输入d和输出q来定义寄存器:
val q = RegNext(d)
- 定义带reset信号的寄存器:
val valReg = RegInit(0.U(4.W))
- 定义带enable信号的寄存器:
val enableReg = Reg(UInt(4.W))
when(enable) { enableReg := inVal }
- 定义带reset和enable信号的寄存器:
val resetEnableReg = RegInit(0.U(4.W))
when(enable) { resetEnableReg := inVal }
ch8.2:计数器
- 最简单形式的计数器就是将寄存器的输出连接到加法器,而加法器的输出连接到寄存器的输入(D触发器的输入D)
ch8.2.1:向上和向下计数
- 用when条件语句,实现向上或向下计数到特定值后回到0
- 用复用器硬件,实现向上或向下计数到特定值后回到0
ch8.2.2:使用计数器产生时序
- 一个常见的实践是,在我们的电路中以f_tick频率产生单周期的tick(时钟脉冲)
ch8.2.3:nerd计数器
- 向下计数到-1的计数器:检测最高bit为1就表示计数到了-1
ch8.2.4:一个计时器
- 计时器:只计数一次的计数器
- 示例:fig8.9和listing8.1
- 示例:fig8.9和listing8.1
ch8.2.5:脉冲宽度调制
- 示例:看不懂。略过
ch8.3:位移寄存器
- 示例:串转并输出、并转串输入的实现,都是用Cat()来实现(Cat=concatenate)
ch8.3.1:使用并行输出的移位寄存器
- 示例:fig8.12,serIn从高位开始移入outReg[3:0]
val outReg = RegInit(0.U(4.W))
outReg := Cat(serIn, outReg(3, 1))
val q = outReg
ch8.3.2:并行读取的移位寄存器
- 示例:fig8.13,并行的loadReg[3:0]赋值给串行的寄存器serOut
when(load) {
loadReg := d
} otherwise {
loadReg := Cat(0.U, loadReg(3, 1))
}
val serOut = loadReg(0)
ch8.4:存储器
- 存储器可以通过一系列的寄存器搭建。但基于寄存器的存储器硬件上非常昂贵,所以更大的存储器是通过sram搭建的
- 同步存储器:在输入端(读/写地址、写数据、写使能)设计了寄存器。这意味着设置地址后一个周期,读的数据就可用了。
- 用chisel库函数SyncReadMem构建的存储器模块只是最基本的存储器:可以指定byte数,但输入、输出data的宽度固定为1byte,另外还有一个写使能。剩下的定义需要外部重新封装。
- 有一个有趣的问题:当在进行写操作的同一个时钟周期,对同一个地址进行读操作,会读到什么值、我们对存储器的read-during-write行为感兴趣。
- 有三种可能:新值、旧值或未定义的值(新值和旧值不同bit的混合)。
- 发生在fpga上的可能性取决于fpga的类型,有时还可以指定。
- 示例:fig8.15,使用添加前递电路来使得read-during-write输出新值
class ForwardingMemory() extends Module {
val io = IO(new Bundle {
val rdAddr = Input(UInt(10.W))
val rdData = Output(UInt(8.W))
val wrEna = Input(Bool())
val wrData = Input(UInt(8.W))
val wrAddr = Input(UInt(10.W))
})
val mem = SyncReadMem(1024, UInt(8.W))
val wrDataReg = RegNext(io.wrData )
val doForwardReg = RegNext(io.wrAddr === io.rdAddr && io.wrEna)
val memData = mem.read(io.rdAddr)
when(io.wrEna) {
mem.write(io.wrAddr, io.wrData)
}
io.rdData := Mux(doForwardReg, wrDataReg, memData)
}
- ch8.5:练习
- 略
八、(ch9)输入处理
ch9.1:异步输入
- 异步输入因为没有时钟,所以直接输出到触发器,可能会违反触发器输入的建立和保持时间,导致触发器的多稳态,甚至震荡;
- 解决的方法是:使用“输入同步器”,即两个触发器串联(比如A和B),因为触发器是同步于时钟的,所以即使A输出可能是多稳态,但B输出可以是稳定的;
- 实现:
val btnSync = RegNext(RegNext(btn))
ch9.2:防抖动
- 示例:在100MHz下,每隔10ms采样一次,以确认电平的变化,实现防抖动(要用到计数器,产生防抖动周期)
val FAC = 100000000/100
val btnDebReg = Reg(Bool())
val cntReg = RegInit(0.U(32.W))
val tick = cntReg === (FAC-1).U
//相当于bool变量的定义:tick为cntReg寄存器和(FAC-1).U常量的比较结果(硬件);虽然后面没有显式地更新tick,但它在硬件运行过程中不断自动变化。
cntReg := cntReg + 1 .U
when (tick) {
cntReg := 0.U
btnDebReg := btnSync
}
ch9.3:输入信号滤波
- 输入信号中有噪声,但不想用以上的两种方法来排除(输入同步器、防抖动滤波),所以这里提出第三种处理方法:使用投票电路;
- 实际这样的投票电路非常少用;
- 对信号进行相同周期间隔的三次采样,输出结果取两次相同的值(要用到计数器,产生采样周期);
- 示例:(略)
ch9.4:使用函数合并输入处理
- 第一次给出一个结合def定义函数、val定义变量的组合成的模块
- 这个示例实现的功能是:有滤波处理的计数器
- 对输入信号(按键输入)进行3次投票实现滤波:
def filter(v: Bool, t: Bool);
- 投票的时间间隔(周期)由另一个函数实现:
def tickGen(fac: Int);
- 对滤波后的信号寻找上升沿,定义一个计数器对该上升沿进行+1,实现了一个对外部信号的计数器
- 对输入信号(按键输入)进行3次投票实现滤波:
ch9.5:练习: (略)
九、(ch10)有限状态机
ch10.1:基本有限状态机 (Moore FSM为例)
- FSM:Finite-States Machine, 有限状态机,在chisel中是作为module内部的一部分;
- 状态机的核心语句:
- 状态定义1:用Enum枚举,状态名称自动被综合工具用二进制编码代替(当前chisel版本决定),如:val <状态1> :: <状态2> :: <状态3> :: Nil = Enum(<状态的个数,这里为3>)
- 状态定义2:用Enum枚举,状态名称使用定义chisel常量(当前chisel版本需要显式使用才行);这不是常用编码,示例略;
- 状态使用:“状态”定义为寄存器,如:
val stateReg = RegInit(<状态1>)
- 状态切换:用switch/is语句:实现硬件的多选一复用器;
ch10.2:使用Mealy FSM产生快速输出
- Moore FSM:输出由当前状态、当前输入决定,状态图的转换箭头用“<输入>”来标记;
- Mealy FSM:输出由当前输出、当前输入决定,状态图的转换箭头用“<输入>/<输出>”来标记;
- 示例:边沿检测电路
- 不用状态机表示时,最简单的方法是一行chisel代码:val risingEdge = din & !RegNext(din)
- 用Mealy状态机时,核心语句也是Enum、switch/is;
- Mealy状态机代码:略;
ch10.3:Moore对比Mealy
- 还是以最简单的“上升沿检测电路”为例,对比两者的优缺点
- Moore FSM:
- 优点:存在能切断组合路径的一个状态寄存器,所以不会发生FSM通信相关的两个问题(Mealy的缺点),这在稍微大一些的设计中尤为重要;
- 缺点1:硬件实现所需要的逻辑比Mealy多一倍;
- 缺点2:对输入信号的上升沿检测,最快也要同步到最近的一个时钟,不能同步于输入信号;
- Mealy FSM:
- 优点1:硬件实现所需要的逻辑比Moore少;
- 优点2:对输入信号的上升沿检测,能跟随输入信号,而不用等待、同步于时钟信号;
- 缺点1:Mealy内部用于FSM通信的组合路径,实际的设计会比较长;
- 缺点2:如果FSM通信构成一个圆圈,那么组合路径也会形成一个环回,这在同步设计中会是个错误;
- 总结1:Moore在FSM通信的组合中更好,因为它比Mealy更稳定;
- 总结2:除非关注在当前周期下FSM的反应,才会用Mealy(因为它的输出同步于输入信号、而不是时钟);
- 总结3:类似“上升沿检测电路”这种小电路,Mealy也很实用;
ch10.4:练习: (略)
十、(ch11)状态机通信
“通常问题会很复杂,以至于不能用单个fsm去描述。这种情况下,问题可以被分为两个或更多的更小、更简单的fsm。然后那些fsm使用信号去通信。一个fsm的输出是另一个fsm的输入,同时也观察其它fsm的输出。当我们分成一个大的fsm为许多简单fsm,这称为“分解fsm”。但是,fsm通信经常直接根据spec来设计,因为如果实现成单个fsm会是不可实现的大。”
ch11.1:一个灯光闪烁器的例子
- 示例的要求:
- 状态机输入一个周期的start时,触发灯光闪烁器的序列,输出为light信号,有on/off两种状态
- 一个序列闪烁三次
- 每次闪烁表示为:light=on,6个周期;light=off,4个周期
- 闪烁序列完成后,fsm变为light=off,等待下一次start触发开始
- 状态机1:
- 实现为单个状态机
- 计算一共会有27个状态;
- 状态机2:
- 实现为分解的两个状态机:master和timer
- master状态机:输出timerLoad信号,控制timer开始;输出timerSelect信号,选择计时时间为6或4;输入信号timerDone,表示timer状态机已完成计时
- timer状态机:根据master输入的timerLoad、timerSelect开始计时,完成后输出timerDone
- 状态机3:
- 优化状态机2,分解为三个状态机:master、timer、counter
- master状态机:(同上,)另外还有3个信号:输出cntLoad,表示闪烁剩余次数从2开始;输出cntDecr信号,表示timer状态机(经过master状态机)单次闪烁完成,次数可减1;输出cntDone信号,表示闪烁剩余次数归0
- timer状态机:(同上)
- counter状态机:根据master输入的cntLoad、cntDecr开始倒计数,闪烁次数归0后后输出timerDone
ch11.2:位1计数(器)的例子: (略)
ch11.3:ready-valid接口
- ready/valid接口是一个分别在发送端定义data/valid、接收端定义ready信号的简单控制流接口
- 为了让ready/valid接口可以集成到其它模块,ready和valid都不允许组合性依赖。因为这个接口比较常用,所以chisel定义了DecoupledIO线束,定义类似如下:
class DecoupledIO [T <: Data] (gen: T) extends Bundle {
val ready = Input(Bool())
val valid = Output(Bool())
val bits = Output(gen)
}
- ready/valid接口有一个问题:
- 即:“ready和valid在全部有效以后是否可能自动清零?”
- 这个问题可能发生在:发送端的valid或接受端的ready,在使能一段时间后就分别由于别的(意外)事件导致清零;然后数据无效,导致没有数据传输
- 解决:上述两种行为(情况)是否被允许,并不属于ready/valid接口的内容;但是它需要在接口的具体使用上被定义
- 方案1:使用IrrevocableIO类
- 使用DecoupledIO类的时候,chisel没有对ready/valid信号的交互行为做限制条件;
- 但IrrevocableIO类会有限制条件(只是一个习惯、而不是强制规范?)——是对于接收端的:
- “一个具体的ReadyValidIO的子类,当valid是高位,ready是低位,保证不会在bits数值改变的一个周期后改变;
也就是说,一旦valid升高,它就不会变低,直到下一个ready也升高。”
- 方案2:以AXI接口为参考
- 它对以下的4个总线操作使用了rady/valid接口:读地址、写地址、读数据、写数据;
- AXI提出的限制是:一旦ready或valid为高,就直到发生了数据传输才能拉低
十一、(ch12)硬件生成器
ch12.1:一点scala的内容:
- val变量:定义一个(硬件组件)表达式,但不能被赋值;(尝试重新赋值会在编译时报错)
- var变量:定义一个(硬件生成器?)表达式,且能被赋值;
- val和var变量的类型:隐式类型,由scala编译时自动推断;显式类型,可以类似这样定义:val number:Int=42
- “:=”:这种赋值是chisel的操作符,而不是scala的操作符;
- if/else语句:在进行电路生成的scala进行时执行,并不生成硬件复用器(复用器的生成方法是when/.elsewhen/.otherwise和switch/is语句);
ch12.2.1:使用参数配置:
- 示例:参数化位宽的加法器
val add8 = Module(new ParamAdder(8))
val add16 = Module(new ParamAdder(16))
ch12.2.2:使用类型参数的函数:
- 示例1:二进一出、io类型支持自定义的复用器
def myMux[T <: Data](sel: Bool, tPath: T, fPath: T): T = {
...
}
- 上面的def函数表示:
- 整个函数头中T表示chisel类型系统的根类型Data
- 第二个参数tPath和第三个参数fPath都使用T类型
- 函数的返回值也使用T类型
- 示例2:二进一出、io类型支持自定义的复用器
def myMux[T <: Data](sel: Bool, tPath: T, fPath: T): T = {
val ret = Wire(fPath.cloneType)
...
ret
}
- 上面的def函数新增了:
- 用chisel内置的.cloneType来获取参数的类型,来作为返回值的类型(实际上这个用法很少用;Nutshell代码里就没有)
ch12.2.3:具有类型参数的模块
- 模块和函数的区别(?):
- 模块定义:
class xx(xx) extends Module {...}
- 函数定义:
def xx(xx) = {...}
- 模块定义:
- 示例:noc芯片(network-on-chip,核间的片上网络路由)
class NocRouter[T <: Data](data: T, n: Int) extends Module {
val io = IO(new Bundle {
val inPort = Input(Vec(n, data))
val address = Input(Vec(n, UInt(8.W)))
val outPort = Output(Vec(n, data))
})
}
class Payload extends Bundle {
val data = UInt(16.W)
val flag = Bool()
}
val router = Module(new NocRouter(new Payload, 2))
- 上面的示例表示:
- 定义一个noc芯片,数据输入、输出端口(线束bundle)的类型是参数化、可自定义的(甚至连bundle的组数也是参数化的)
- noc芯片的输入、输出端口每一组bundle的类型,是通过先定义Bundle类,再把该类作为参数传给模块的(上例即class Payload)
ch12.2.4:参数化的捆束(Bundle)
- 当在Vec内部使用bundle时,需要对参数声明为私有的参数化类型?否则会一直使用到最上层调用时传参传来的类型
- 示例:
val router = Module(new NocRouter2(new Port(new Payload), 2))
class NocRouter2[T :< Data](dt: T, n: Int) extends Module {
val io = IO(new Bundle) {
...
val inPort = Input(Vec(n, dt))
}
}
class Port [T <: Data](private val dt: T) extends Bundle {
...
val address = dt.cloneType //保证这里cloneType的结果就是Port()定义时选用的参数类型T?
}
ch12.3:生成组合逻辑
- 从外部读取文本文件来生成逻辑表?
- 示例:(略)
ch12.4:使用继承
- 示例:对基本计数器定义一个必有的输出信号tick,然后基于对这个基本计数器的继承,来实现定义多种定时器
abstract class Ticker (n:Int) extends Module {
val io = IO(new Bundle {
val tick = Output(Bool())
})
}
class UpTicker(n:Int) extends Ticker(n) {
...
io.tick := cntReg === N
}
class DownTicker(n:Int) extends Ticker(n) {
...
io.tick := cntReg === N
}
class NerdTicker(n:Int) extends Ticker(n) {
...
io.tick := false.B
when(...) {
io.tick := true.B
}
}
- 顺便给出单元测试示例:PeekPokeTester(实际Nutshell和香山都没有用这个来进行单元测试)
import chisel3.iotesters.PeekPokeTester
import org.scalatest._
class TickerTester[T <: Ticker](dut: T, n: Int) extends PeekPokeTester(dut: T) {
...
step(1)
}
class TickerSpec extends FlatSpec with Matchers {
"UpTicker 5" should "pass" in {
chisel3.iotesters.Driver(() => new UpTicker(5)) { c =>
new TickerTester(c, 5)
} should be (true)
}
"DownTicker 7" should "pass" in{
chisel3.iotesters.Driver(() => new DownTicker(7)) { c =>
new TickerTester(c, 7)
} should be (true)
}
"NerdTicker 11" should "pass" in{
chisel3.iotesters.Driver(() => new NerdTicker(11)) { c =>
new TickerTester(c, 11)
} should be (true)
}
}
执行命令以开始单元测试:sbt "testOnly TickerSpec"
ch12.5:使用函数式编程做硬件生成
- 将实现了硬件生成的基本函数a作为一个参数,传给另一个函数作为参数b,以被调用来生成多个、或组合的新硬件模块
- 示例1:将基本的二进一出加法器作为向量操作函数vec的参数,来实现多进一出的加法链(向量加法器)
def add(a:UInt, b:UInt) = a + b
val sum = vec.reduce(add)
- 示例2:(优化)把示例1直接写成一行语句(利用scala通配符"_")
val sum = vec.reduce(_ + _)
- 示例3:(优化)把示例2的组合性延迟降低
- 上述语句实现的一串加法链会产生多个时钟延迟;
- 如果我们不信任综合工具会正确重新排列这个加法链,我们可以用chisel的reduceTree方法去生成一个加法器的树
val sum = vec.reduceTree(_ + _)
十二、(ch13)示例设计
ch13.1:fifo缓冲器
- 示例1:单级fifo(寄存器)
- 单级fifo就是单个支持读写异步操作的数据寄存器
- 写入侧(enqueueing)的信号包括:输入写控制write、输出满标志full、输入数据din
- 读出侧(dequeueing)的信号包括:输入读控制read、输出空标志empty、输出数据dout
class WriterIO(size: Int) extends Bundle {
val write = Input(Bool())
val full = Output(Bool())
val din = Input(UInt(size.W))
}
class ReaderIO(size: Int) extends Bundle {
val read = Input(Bool())
val empty = Output(Bool())
val dout = Output(UInt(size.W))
}
class FifoRegister(size: Int) extends Module {
val io = IO(newBundle{
val enq = new WriterIO(size)
val deq = new ReaderIO(size)
val empty::full::Nil = Enum(2) //即使是单级fifo,也是一个小状态机
val stateReg = RegInit(empty)
val dataReg = RegInit(0.U(size.W))
... //状态机实现
})
- 示例2:冒泡fifo(单级fifo的数组的串联)
- 用scala的Array.Fill(){}来定义单级fifo串联的冒泡fifo
- 每个相邻单级fifo的输入、输出信号分别相连,以实现自动的数据搬移控制
class BubbleFifo(size: Int, depth: Int) extends Module {
val io = IO(new Bundle {
val enq = new WriterIO(size)
val deq = new ReaderIO(size)
})
val buffers = Array.fill(depth) {Module(new FifoRegister(size))}
for(i <- 0 until depth - 1) {
buffers(i+1).io.enq.din := buffers(i).io.deq.dout
buffers(i+1).io.enq.write := ~buffers(i).io.deq.empty
buffers(i).io.deq.read := ~buffers(i+1).io.enq.full
}
io.enq <> buffers(0).io.enq //Bundle的整体双向互联,可用批量连接运算符"<>":Bundle中识别为同名的信号val,会互联到一起
io.deq <> buffers(depth-1).io.deq
}
ch13.2:一个串口端口
- 示例1:不带fifo的串口发送端tx
- 包括:11bit宽的移位寄存器、时钟到波特率的分频值寄存器、移位剩余bit数寄存器
class Tx(frequency: Int, baudRate: Int) extends Module {
val io = IO(newBundle{
val txd = Output(Bits(1.W))
val channel = newChannel()
})
val BIT_CNT = ((frequency+baudRate/2)/baudRate - 1).asUInt()
val shiftReg = RegInit(0x7ff.U) //移位寄存器:bit0输出到输出引脚tdx,即右移,低bit先发
val cntReg = RegInit(0.U(20.W)) //分频系数寄存器:从时钟频率到串口波特率的分频
val bitsReg = RegInit(0.U(4.W)) //移位bit数计数寄存器:从11个bit倒计数到0
io.channel.ready := (cntReg === 0.U) && (bitsReg === 0.U)
io.txd := shiftReg(0)
when(cntReg === 0.U){
cntReg := BIT_CNT
when(bitsReg =/= 0.U) { //chisel中“不等于”的运算符是这样表示的:"=/="
val shift = shiftReg>>1
shiftReg := Cat(1.U,shift(9,0)) //寄存器的移位操作:总是用Cat(新bit值, 其余bit值)来实现的
bitsReg := bitsReg1.U
} .otherwise {
when(io.channel.valid){
//two stop bits, data, one start bit
//移位寄存器shiftReg的11bit定义(从右向左看,和波形时序相反): 1bit start的0、8bit的data、2bit stop的11
shiftReg := Cat(Cat(3.U,io.channel.data),0.U)
bitsReg := 11.U
} .otherwise {
shiftReg := 0x7ff.U
}
}
} .otherwise {
cntReg := cntReg - 1.U
}
}
- 示例2:带单级的字节fifo的串口发送端tx
- (略)
- 示例3:带单级的word宽fifo的串口发送端tx
- 略,手册也没给出
- 示例4:带单级的字节fifo的串口接收端rx
- (略)
ch13.3:设计fifo中的变量
- 使用继承来实现不同的fifo队列
ch13.3.1:参数化fifo:(略)
ch13.3.2:重新设计冒泡fifo
- 示例:使用标准的ready/valid接口来重新定义冒泡fifo,并可以通过chisel数据类型参数化
- (略)
- ch13.3.3:double buffer fifo
- ready/valid接口在ready和valid信号都有效时,会不满足协议的要求,导致fifo不能写入新的数据(?)
- 通过引入shadow寄存器(影子寄存器)来解决:即使ready信号有效,fifo依然可以被写入,只不过是写到影子寄存器
- 等ready信号无效后,影子寄存器的数据会被自动写入到fifo
- 示例:(略)
ch13.3.4:具有寄存存储器的FIFO
ch13.3.5:使用片上存储的FIFO
ch13.4.1:继续探索冒泡fifo
- 尝试执行demo中的冒泡fifo示例:(略)
ch13.4.2:the UART
- 尝试执行demo中的uart示例:(略)
ch13.4.3:探索fifo
- 尝试执行demo中的4深度、word位宽的fifo示例:(略)
十三、(ch14)设计一个处理器
ch14.1:从alu开始
- 实现一个简单的累加器,文档有一个对应的示例叫做leros,代码开源在https://github.com/leros-dev/leros
- 示例:简单的累加器alu
- alu是个状态机,所有指令中的基础指令组成它的枚举类型定义,这里有8个:nop/add/sub/and/or/xor/ld/shr
- alu有两个数据输入a/b、一个操作码选择输入op、一个结果输出y
- 用switch/is结合枚举类型来定义它的基本操作
- 为了测试这个chisel实现的alu,需要用scala另外实现一个alu,以进行处理结果的对比
- scala实现的alu,需要被peekpoke调用来运行测试
- leros项目中运行测试的命令: sbt “test:runMain leros.AluTester”
ch14.2:译码指令(指令译码器)
- 首先,在指令译码器的scala类和shared包里定义机器码常量;因为想要在leros硬件实现、leros的汇编器、leros的指令集模拟器之间共享这些编码常量
- 示例:从机器码到alu操作码的转换
- 定义decode用于输出到alu的bundle,信号包括:使能信号ena、操作码选择func、退出信号exit
- 定义decode用于输入的信号,只有一个:指令常量UInt(8.W)
ch14.3:汇编指令(指令汇编器)
- 为leros编写程序时我们需要一个汇编器。但在最开始的时候,我们先hard code一些指令,把它们放到一个可以用来初始化指令存储器的scala数组里
- 汇编器要实现的效果:
- 将以下字符串:
addi 0x3 addi -1 subi 2 ldi 0xab and 0x0f or 0xc3
- 转换为对应的机器码:
val prog = Array[Int] ( 0x0903, //addi 0x3 0x09ff, //addi -1 0x0d02, //subi 2 0x21ab, //ldi 0xab 0x230f, //and 0x0f 0x25c3, //or 0xc3 0x0000 )
- 将以下字符串:
- 示例:从字符串到机器码的转换
- 从外部读取文件,导入保存为数组;里面按行放置汇编指令
- 汇编器要实现的功能1:识别指令字符串,比如:add、sub、or
- 汇编器要实现的功能2:能区分汇编语句的参数是寄存器还是立即数
- 汇编器要实现的功能3:能解析数字(立即数)为统一的无符号整形,包括:十六进制数、有/无符号的十进制数(实际要调用scala的库函数来实现,比如:Integer.parseInt()、String.substring())
- 按行解析完成汇编指令的指令、参数部分后,拼接为十六进制的机器码,比如:"addi 0x3"的输出结果为0x0903
ch14.4:练习:(略)
十四、(ch15)贡献chisel
- (略)
十五、(ch16)总结
- (略)