专注app软件定制开发【从嵌入式视角学习香山处理器】四、Chisel语言基础

文章目录

一、前言

专注app软件定制开发这是记录学习chisel专注app软件定制开发官方文档的笔记。原文档pdf专注app软件定制开发下载链接在这里:

专注app软件定制开发写这篇文章找到了另一专注app软件定制开发篇官方的中文简介文章,专注app软件定制开发可作为学习阶段反复阅专注app软件定制开发读复习用的材料。
(下载链接:)


二、Linux上对scala专注app软件定制开发工程的操作

1. helloworld执行命令:

sbt "runMain <对象名(专注app软件定制开发而不是文件名)>"

2. 专注app软件定制开发有多个工程目录时,专注app软件定制开发需要切换工程:

sbtprojectproject <工程名>ctrl+C
  • 1
  • 2
  • 3
  • 4

3. 编译报错:

[error] (run-main-0) java.lang.NoSuchMethodException: Hello.main([Ljava.lang.String;)
  • 1

main就是object;专注app软件定制开发代码中只有class是不够的;

[error] /home/cwq/6_chisel_book_and_example/1_cwq_example/2_Hello_hardware.scala:2:8: not found: object chisel3
  • 1

专注app软件定制开发缺少编译配置文件,默认是build.sbt;专注app软件定制开发需要从别的工程里复制出来用;

4. 给vscode的scala插件设置JAVA_HOME路径:

确认JAVA_HOME专注app软件定制开发路径的方法:


三、(ch4)专注app软件定制开发基本组成部分

ch4.1:专注app软件定制开发信号类型与常量:

  1. 信号类型:Bits、UInt、SInt
  2. 常量:.W(专注app软件定制开发表示信号类型或常量的宽度)、.U、.S
  3. 8.U(4.W)表示4bit专注app软件定制开发宽度的常量8
  4. “hff”.U、“o377”.U、“b1111_1111”.U专注app软件定制开发分别为十进制常量255.U专注app软件定制开发在其它进制下的表示
  5. bool类型:true.B、false.B

ch4.2:组合电路:

  1. 专注app软件定制开发算数操作符:(专注app软件定制开发和其它语言一样的)加减乘除、取余、取反
  2. 专注app软件定制开发逻辑操作符:与非或、异或、相等、不等
  3. 专注app软件定制开发操作符的优先级取决于电路的赋值顺序(不同于其它语言):所以有必要使用括号
  4. chisel提供的复用器:
val result = Mux(<条件>, <条件为真的输出选择>, <条件为假的输出选择>)
  • 1

ch4.3:状态寄存器:

  1. 寄存器定义:val reg = RegInit(0.U(8.W)),定义了一个八位寄存器,在复位初始化为0
  2. 寄存器用作计数器的示例:从0数到9,并重新返回0,以实现数10个数的目的
val cntReg = RegInit(0.U(8.W))cntReg := Mux(cntReg === 10.U, 0.U, cntReg+1.U)
  • 1
  • 2

ch4.4:使用Bundle和Vec来构建

  1. Bundle:组合不同类型的信号
  2. Vec:组合可索引的相同类型的信号
  3. Bundle和Vec可以相互嵌套
  4. 定义一个Bundle类型、有初始值的寄存器:先创建Bundle类型的Wire变量,再给这个变量赋值,再用这个变量去定义寄存器
val initVal = Wire(new Channel())initVal.data := 0.UinitVal.valid := false.Bval channelReg = RegInit(initVal)
  • 1
  • 2
  • 3
  • 4

四、(ch5)搭建过程和测试

ch5.1:使用sbt搭建你的项目

  1. 库文件通过build.sbt被引用
  2. 如果build.sbt设置latest.release则表示总是用最新的chisel版本,这意味着每次搭建都要联网查看maven仓库——实际上提倡无联网情况下的搭建
  3. “import <软件包名>._”表示包里的所有类都要被引用
  4. chisel工具流:参考文档中的fig5.2图,从.scala文件到生成.vcd波形文件和.v综合电路文件

ch5.2.1:PeekPokeTester

  1. chisel模块的单元测试:sbt "runMain xxx"

ch5.2.2:使用scalaTester

  1. scala模块的单元测试:sbt "testOnly xxx"

ch5.2.3:波形

  1. 在scalaTester下使用Driver.execute()代替Driver(),即可生成.vcd波形文件,用GTKWave(或ModelSim)可以打开

ch5.2.4:printf debugging

  1. printf是来源于C语言的另一种调试形式:在函数的任何地方都可以插入printf()函数
  2. printf支持C和scala两种风格
  3. 示例:略

五、(ch6)组成部分

ch6.1:chisel的组成部分是模块

  1. 模块的嵌套示例:fig6.1
    • (重要)“硬件组件”在chisel代码里称为module,所以它们都用extends Module继承的方式来定义。并且里面一定要用IO(new Bundle())定义它的全部IO——Input和Output都在里面一起定义。

ch6.2:一个运算逻辑单元

  1. 以一个简单的运算逻辑单元ALU为作为大Module的示例,讲解其内部fetch、decode、execute三个Module的互联关系
  2. 顺便引出:switch/is语句的使用,需要引入chisel3.util包

ch6.3:整体连接

  1. Bundle的整体双向互联,可用批量连接运算符"<>":Bundle中识别为同名的信号val,会互联到一起

ch6.4:使用函数的轻量级组成部分

  1. 函数(def):模块(class … extends Module)是构造硬件描述的通用方法。但是,也有一些“样板代码”可以在对模块进行声明、实例化、连接时使用(这就是函数)
  2. 示例1:用RegNext()函数构造延时一周期的新函数:
def delay(x:UInt) = RegNext(x)
  • 1
  1. 示例2:调用上述函数,来定义一个“对输入变量延时两个周期后输出的变量”
def delay(x:UInt) = RegNext(x)val delOut = delay(delay(defIn))
  • 1
  • 2

六、(ch7)组合搭建模块

ch7.1:组合电路

  1. 组合电路在chisel中的表示1:逻辑运算
    • 最简单的就是定义一个变量名,其内容为布尔表达式
    • val e = (a & b) | c
    • val f= ~e
  2. 组合电路在chisel中的表示2:复用器(输出信号要定义为Wire(UInt()))
    • 用chisel的when/.elsewhen/.otherwise表示二选一复用器的串联
    • 用switch/is表示多选一复用器
  3. 说明:scala中也有if/else语句,但它不产生硬件,只是纯软件语句

ch7.2:解码器

  1. 以2/4解码器为例,演示switch/is语句在实现解码器中的用法

ch7.3:编码器

  1. 以4/2编码器为例,演示switch/is语句在实现编码器中的用法

七、(ch8)时序建造模块

“因为我们感兴趣的是同步设计,所以当我们说时序电路时,就意味着是同步时序电路”

ch8.1:寄存器

  1. 寄存器的时钟输入信号不需要定义:chisel已自动隐含添加
  2. 用输入d和输出q来定义寄存器:val q = RegNext(d)
  3. 定义带reset信号的寄存器:val valReg = RegInit(0.U(4.W))
  4. 定义带enable信号的寄存器:
val enableReg = Reg(UInt(4.W))when(enable) { enableReg := inVal }
  • 1
  • 2
  1. 定义带reset和enable信号的寄存器:
val resetEnableReg = RegInit(0.U(4.W))when(enable) { resetEnableReg := inVal }
  • 1
  • 2

ch8.2:计数器

  1. 最简单形式的计数器就是将寄存器的输出连接到加法器,而加法器的输出连接到寄存器的输入(D触发器的输入D)

ch8.2.1:向上和向下计数

  1. 用when条件语句,实现向上或向下计数到特定值后回到0
  2. 用复用器硬件,实现向上或向下计数到特定值后回到0

ch8.2.2:使用计数器产生时序

  1. 一个常见的实践是,在我们的电路中以f_tick频率产生单周期的tick(时钟脉冲)

ch8.2.3:nerd计数器

  1. 向下计数到-1的计数器:检测最高bit为1就表示计数到了-1

ch8.2.4:一个计时器

  1. 计时器:只计数一次的计数器
    • 示例:fig8.9和listing8.1

ch8.2.5:脉冲宽度调制

  1. 示例:看不懂。略过

ch8.3:位移寄存器

  1. 示例:串转并输出、并转串输入的实现,都是用Cat()来实现(Cat=concatenate)

ch8.3.1:使用并行输出的移位寄存器

  1. 示例:fig8.12,serIn从高位开始移入outReg[3:0]
val outReg = RegInit(0.U(4.W))outReg := Cat(serIn, outReg(3, 1))val q = outReg
  • 1
  • 2
  • 3

ch8.3.2:并行读取的移位寄存器

  1. 示例:fig8.13,并行的loadReg[3:0]赋值给串行的寄存器serOut
	when(load) {        loadReg := d    } otherwise {        loadReg := Cat(0.U, loadReg(3, 1))    }    val serOut = loadReg(0)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

ch8.4:存储器

  1. 存储器可以通过一系列的寄存器搭建。但基于寄存器的存储器硬件上非常昂贵,所以更大的存储器是通过sram搭建的
  2. 同步存储器:在输入端(读/写地址、写数据、写使能)设计了寄存器。这意味着设置地址后一个周期,读的数据就可用了。
  3. 用chisel库函数SyncReadMem构建的存储器模块只是最基本的存储器:可以指定byte数,但输入、输出data的宽度固定为1byte,另外还有一个写使能。剩下的定义需要外部重新封装。
  4. 有一个有趣的问题:当在进行写操作的同一个时钟周期,对同一个地址进行读操作,会读到什么值、我们对存储器的read-during-write行为感兴趣。
    • 有三种可能:新值、旧值或未定义的值(新值和旧值不同bit的混合)。
    • 发生在fpga上的可能性取决于fpga的类型,有时还可以指定。
  5. 示例: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)        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  1. ch8.5:练习

八、(ch9)输入处理

ch9.1:异步输入

  1. 异步输入因为没有时钟,所以直接输出到触发器,可能会违反触发器输入的建立和保持时间,导致触发器的多稳态,甚至震荡;
  2. 解决的方法是:使用“输入同步器”,即两个触发器串联(比如A和B),因为触发器是同步于时钟的,所以即使A输出可能是多稳态,但B输出可以是稳定的;
  3. 实现:
    val btnSync = RegNext(RegNext(btn))

ch9.2:防抖动

  1. 示例:在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        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

ch9.3:输入信号滤波

  1. 输入信号中有噪声,但不想用以上的两种方法来排除(输入同步器、防抖动滤波),所以这里提出第三种处理方法:使用投票电路;
  2. 实际这样的投票电路非常少用;
  3. 对信号进行相同周期间隔的三次采样,输出结果取两次相同的值(要用到计数器,产生采样周期);
  4. 示例:(略)

ch9.4:使用函数合并输入处理

  1. 第一次给出一个结合def定义函数、val定义变量的组合成的模块
  2. 这个示例实现的功能是:有滤波处理的计数器
    • 对输入信号(按键输入)进行3次投票实现滤波: def filter(v: Bool, t: Bool);
    • 投票的时间间隔(周期)由另一个函数实现: def tickGen(fac: Int);
    • 对滤波后的信号寻找上升沿,定义一个计数器对该上升沿进行+1,实现了一个对外部信号的计数器

ch9.5:练习: (略)


九、(ch10)有限状态机

ch10.1:基本有限状态机 (Moore FSM为例)

  1. FSM:Finite-States Machine, 有限状态机,在chisel中是作为module内部的一部分;
  2. 状态机的核心语句:
    • 状态定义1:用Enum枚举,状态名称自动被综合工具用二进制编码代替(当前chisel版本决定),如:val <状态1> :: <状态2> :: <状态3> :: Nil = Enum(<状态的个数,这里为3>)
    • 状态定义2:用Enum枚举,状态名称使用定义chisel常量(当前chisel版本需要显式使用才行);这不是常用编码,示例略;
    • 状态使用:“状态”定义为寄存器,如:val stateReg = RegInit(<状态1>)
    • 状态切换:用switch/is语句:实现硬件的多选一复用器;

ch10.2:使用Mealy FSM产生快速输出

  1. Moore FSM:输出由当前状态、当前输入决定,状态图的转换箭头用“<输入>”来标记;
  2. Mealy FSM:输出由当前输出、当前输入决定,状态图的转换箭头用“<输入>/<输出>”来标记;
  3. 示例:边沿检测电路
    • 不用状态机表示时,最简单的方法是一行chisel代码:val risingEdge = din & !RegNext(din)
    • 用Mealy状态机时,核心语句也是Enum、switch/is;
    • Mealy状态机代码:略;

ch10.3:Moore对比Mealy

  1. 还是以最简单的“上升沿检测电路”为例,对比两者的优缺点
  2. Moore FSM:
    • 优点:存在能切断组合路径的一个状态寄存器,所以不会发生FSM通信相关的两个问题(Mealy的缺点),这在稍微大一些的设计中尤为重要;
    • 缺点1:硬件实现所需要的逻辑比Mealy多一倍;
    • 缺点2:对输入信号的上升沿检测,最快也要同步到最近的一个时钟,不能同步于输入信号;
  3. Mealy FSM:
    • 优点1:硬件实现所需要的逻辑比Moore少;
    • 优点2:对输入信号的上升沿检测,能跟随输入信号,而不用等待、同步于时钟信号;
    • 缺点1:Mealy内部用于FSM通信的组合路径,实际的设计会比较长;
    • 缺点2:如果FSM通信构成一个圆圈,那么组合路径也会形成一个环回,这在同步设计中会是个错误;
  4. 总结1:Moore在FSM通信的组合中更好,因为它比Mealy更稳定;
  5. 总结2:除非关注在当前周期下FSM的反应,才会用Mealy(因为它的输出同步于输入信号、而不是时钟);
  6. 总结3:类似“上升沿检测电路”这种小电路,Mealy也很实用;

ch10.4:练习: (略)


十、(ch11)状态机通信

“通常问题会很复杂,以至于不能用单个fsm去描述。这种情况下,问题可以被分为两个或更多的更小、更简单的fsm。然后那些fsm使用信号去通信。一个fsm的输出是另一个fsm的输入,同时也观察其它fsm的输出。当我们分成一个大的fsm为许多简单fsm,这称为“分解fsm”。但是,fsm通信经常直接根据spec来设计,因为如果实现成单个fsm会是不可实现的大。”

ch11.1:一个灯光闪烁器的例子

  1. 示例的要求:
    • 状态机输入一个周期的start时,触发灯光闪烁器的序列,输出为light信号,有on/off两种状态
    • 一个序列闪烁三次
    • 每次闪烁表示为:light=on,6个周期;light=off,4个周期
    • 闪烁序列完成后,fsm变为light=off,等待下一次start触发开始
  2. 状态机1:
    • 实现为单个状态机
    • 计算一共会有27个状态;
  3. 状态机2:
    • 实现为分解的两个状态机:master和timer
    • master状态机:输出timerLoad信号,控制timer开始;输出timerSelect信号,选择计时时间为6或4;输入信号timerDone,表示timer状态机已完成计时
    • timer状态机:根据master输入的timerLoad、timerSelect开始计时,完成后输出timerDone
  4. 状态机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接口

  1. ready/valid接口是一个分别在发送端定义data/valid、接收端定义ready信号的简单控制流接口
  2. 为了让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)        }
  • 1
  • 2
  • 3
  • 4
  • 5
  1. ready/valid接口有一个问题:
    • 即:“ready和valid在全部有效以后是否可能自动清零?”
    • 这个问题可能发生在:发送端的valid或接受端的ready,在使能一段时间后就分别由于别的(意外)事件导致清零;然后数据无效,导致没有数据传输
    • 解决:上述两种行为(情况)是否被允许,并不属于ready/valid接口的内容;但是它需要在接口的具体使用上被定义
  2. 方案1:使用IrrevocableIO类
    • 使用DecoupledIO类的时候,chisel没有对ready/valid信号的交互行为做限制条件;
    • 但IrrevocableIO类会有限制条件(只是一个习惯、而不是强制规范?)——是对于接收端的:
    • “一个具体的ReadyValidIO的子类,当valid是高位,ready是低位,保证不会在bits数值改变的一个周期后改变;
      也就是说,一旦valid升高,它就不会变低,直到下一个ready也升高。”
  3. 方案2:以AXI接口为参考
    • 它对以下的4个总线操作使用了rady/valid接口:读地址、写地址、读数据、写数据;
    • AXI提出的限制是:一旦ready或valid为高,就直到发生了数据传输才能拉低

十一、(ch12)硬件生成器

ch12.1:一点scala的内容:

  1. val变量:定义一个(硬件组件)表达式,但不能被赋值;(尝试重新赋值会在编译时报错)
  2. var变量:定义一个(硬件生成器?)表达式,且能被赋值;
  3. val和var变量的类型:隐式类型,由scala编译时自动推断;显式类型,可以类似这样定义:val number:Int=42
  4. “:=”:这种赋值是chisel的操作符,而不是scala的操作符;
  5. if/else语句:在进行电路生成的scala进行时执行,并不生成硬件复用器(复用器的生成方法是when/.elsewhen/.otherwise和switch/is语句);

ch12.2.1:使用参数配置:

  1. 示例:参数化位宽的加法器
        val add8 = Module(new ParamAdder(8))        val add16 = Module(new ParamAdder(16))
  • 1
  • 2

ch12.2.2:使用类型参数的函数:

  1. 示例1:二进一出、io类型支持自定义的复用器
        def myMux[T <: Data](sel: Bool, tPath: T, fPath: T): T = {            ...        }
  • 1
  • 2
  • 3
  1. 上面的def函数表示:
    • 整个函数头中T表示chisel类型系统的根类型Data
    • 第二个参数tPath和第三个参数fPath都使用T类型
    • 函数的返回值也使用T类型
  2. 示例2:二进一出、io类型支持自定义的复用器
        def myMux[T <: Data](sel: Bool, tPath: T, fPath: T): T = {            val ret = Wire(fPath.cloneType)            ...            ret        }
  • 1
  • 2
  • 3
  • 4
  • 5
  1. 上面的def函数新增了:
    • 用chisel内置的.cloneType来获取参数的类型,来作为返回值的类型(实际上这个用法很少用;Nutshell代码里就没有)

ch12.2.3:具有类型参数的模块

  1. 模块和函数的区别(?):
    • 模块定义:class xx(xx) extends Module {...}
    • 函数定义:def xx(xx) = {...}
  2. 示例: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))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  1. 上面的示例表示:
    • 定义一个noc芯片,数据输入、输出端口(线束bundle)的类型是参数化、可自定义的(甚至连bundle的组数也是参数化的)
    • noc芯片的输入、输出端口每一组bundle的类型,是通过先定义Bundle类,再把该类作为参数传给模块的(上例即class Payload)

ch12.2.4:参数化的捆束(Bundle)

  1. 当在Vec内部使用bundle时,需要对参数声明为私有的参数化类型?否则会一直使用到最上层调用时传参传来的类型
  2. 示例:
        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?        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

ch12.3:生成组合逻辑

  1. 从外部读取文本文件来生成逻辑表?
  2. 示例:(略)

ch12.4:使用继承

  1. 示例:对基本计数器定义一个必有的输出信号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            }        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  1. 顺便给出单元测试示例: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)            }        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

执行命令以开始单元测试:sbt "testOnly TickerSpec"

ch12.5:使用函数式编程做硬件生成

  1. 将实现了硬件生成的基本函数a作为一个参数,传给另一个函数作为参数b,以被调用来生成多个、或组合的新硬件模块
  2. 示例1:将基本的二进一出加法器作为向量操作函数vec的参数,来实现多进一出的加法链(向量加法器)
        def add(a:UInt, b:UInt) = a + b        val sum = vec.reduce(add)
  • 1
  • 2
  1. 示例2:(优化)把示例1直接写成一行语句(利用scala通配符"_")
        val sum = vec.reduce(_ + _)
  • 1
  1. 示例3:(优化)把示例2的组合性延迟降低
    • 上述语句实现的一串加法链会产生多个时钟延迟;
    • 如果我们不信任综合工具会正确重新排列这个加法链,我们可以用chisel的reduceTree方法去生成一个加法器的树
      val sum = vec.reduceTree(_ + _)

十二、(ch13)示例设计

ch13.1:fifo缓冲器

  1. 示例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))            ... //状态机实现        })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  1. 示例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        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

ch13.2:一个串口端口

  1. 示例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 := bitsReg􀀀1.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            }        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  1. 示例2:带单级的字节fifo的串口发送端tx
    • (略)
  2. 示例3:带单级的word宽fifo的串口发送端tx
    • 略,手册也没给出
  3. 示例4:带单级的字节fifo的串口接收端rx
    • (略)

ch13.3:设计fifo中的变量

  1. 使用继承来实现不同的fifo队列

ch13.3.1:参数化fifo:(略)

ch13.3.2:重新设计冒泡fifo

  1. 示例:使用标准的ready/valid接口来重新定义冒泡fifo,并可以通过chisel数据类型参数化
    • (略)
  2. 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

  1. 尝试执行demo中的冒泡fifo示例:(略)

ch13.4.2:the UART

  1. 尝试执行demo中的uart示例:(略)

ch13.4.3:探索fifo

  1. 尝试执行demo中的4深度、word位宽的fifo示例:(略)

十三、(ch14)设计一个处理器

ch14.1:从alu开始

  1. 实现一个简单的累加器,文档有一个对应的示例叫做leros,代码开源在https://github.com/leros-dev/leros
  2. 示例:简单的累加器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:译码指令(指令译码器)

  1. 首先,在指令译码器的scala类和shared包里定义机器码常量;因为想要在leros硬件实现、leros的汇编器、leros的指令集模拟器之间共享这些编码常量
  2. 示例:从机器码到alu操作码的转换
    • 定义decode用于输出到alu的bundle,信号包括:使能信号ena、操作码选择func、退出信号exit
    • 定义decode用于输入的信号,只有一个:指令常量UInt(8.W)

ch14.3:汇编指令(指令汇编器)

  1. 为leros编写程序时我们需要一个汇编器。但在最开始的时候,我们先hard code一些指令,把它们放到一个可以用来初始化指令存储器的scala数组里
  2. 汇编器要实现的效果:
    • 将以下字符串:
          addi 0x3    addi -1    subi 2    ldi 0xab    and 0x0f    or 0xc3
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
    • 转换为对应的机器码:
          val prog = Array[Int] (        0x0903, //addi 0x3        0x09ff, //addi -1        0x0d02, //subi 2        0x21ab, //ldi 0xab        0x230f, //and 0x0f        0x25c3, //or 0xc3        0x0000    )
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
  3. 示例:从字符串到机器码的转换
    • 从外部读取文件,导入保存为数组;里面按行放置汇编指令
    • 汇编器要实现的功能1:识别指令字符串,比如:add、sub、or
    • 汇编器要实现的功能2:能区分汇编语句的参数是寄存器还是立即数
    • 汇编器要实现的功能3:能解析数字(立即数)为统一的无符号整形,包括:十六进制数、有/无符号的十进制数(实际要调用scala的库函数来实现,比如:Integer.parseInt()、String.substring())
    • 按行解析完成汇编指令的指令、参数部分后,拼接为十六进制的机器码,比如:"addi 0x3"的输出结果为0x0903

ch14.4:练习:(略)


十四、(ch15)贡献chisel

  1. (略)

十五、(ch16)总结

  1. (略)

网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发