【从嵌入式视角学习香山处理器】四、Chisel语言基础

文章目录

  • 一、前言
  • 二、Linux上对scala工程的操作
    • 1. helloworld执行命令:
    • 2. 有多个工程目录时,需要切换工程:
    • 3. 编译报错:
    • 4. 给vscode的scala插件设置JAVA_HOME路径:
  • 三、(ch4)基本组成部分
    • ch4.1:信号类型与常量:
    • ch4.2:组合电路:
    • ch4.3:状态寄存器:
    • ch4.4:使用Bundle和Vec来构建
  • 四、(ch5)搭建过程和测试
    • ch5.1:使用sbt搭建你的项目
      • ch5.2.1:PeekPokeTester
      • ch5.2.2:使用scalaTester
      • ch5.2.3:波形
      • ch5.2.4:printf debugging
  • 五、(ch6)组成部分
    • ch6.1:chisel的组成部分是模块
    • ch6.2:一个运算逻辑单元
    • ch6.3:整体连接
    • ch6.4:使用函数的轻量级组成部分
  • 六、(ch7)组合搭建模块
    • ch7.1:组合电路
    • ch7.2:解码器
    • ch7.3:编码器
  • 七、(ch8)时序建造模块
    • ch8.1:寄存器
    • ch8.2:计数器
      • ch8.2.1:向上和向下计数
      • ch8.2.2:使用计数器产生时序
      • ch8.2.3:nerd计数器
      • ch8.2.4:一个计时器
      • ch8.2.5:脉冲宽度调制
    • ch8.3:位移寄存器
      • ch8.3.1:使用并行输出的移位寄存器
      • ch8.3.2:并行读取的移位寄存器
    • ch8.4:存储器
  • 八、(ch9)输入处理
    • ch9.1:异步输入
      • ch9.2:防抖动
    • ch9.3:输入信号滤波
    • ch9.4:使用函数合并输入处理
    • ch9.5:练习: (略)
  • 九、(ch10)有限状态机
    • ch10.1:基本有限状态机 (Moore FSM为例)
    • ch10.2:使用Mealy FSM产生快速输出
    • ch10.3:Moore对比Mealy
    • ch10.4:练习: (略)
  • 十、(ch11)状态机通信
    • ch11.1:一个灯光闪烁器的例子
    • ch11.2:位1计数(器)的例子: (略)
    • ch11.3:ready-valid接口
  • 十一、(ch12)硬件生成器
    • ch12.1:一点scala的内容:
      • ch12.2.1:使用参数配置:
      • ch12.2.2:使用类型参数的函数:
      • ch12.2.3:具有类型参数的模块
      • ch12.2.4:参数化的捆束(Bundle)
    • ch12.3:生成组合逻辑
    • ch12.4:使用继承
    • ch12.5:使用函数式编程做硬件生成
  • 十二、(ch13)示例设计
    • ch13.1:fifo缓冲器
    • ch13.2:一个串口端口
    • ch13.3:设计fifo中的变量
      • ch13.3.1:参数化fifo:(略)
      • ch13.3.2:重新设计冒泡fifo
      • ch13.3.4:具有寄存存储器的FIFO
      • ch13.3.5:使用片上存储的FIFO
      • ch13.4.1:继续探索冒泡fifo
      • ch13.4.2:the UART
      • ch13.4.3:探索fifo
  • 十三、(ch14)设计一个处理器
    • ch14.1:从alu开始
    • ch14.2:译码指令(指令译码器)
    • ch14.3:汇编指令(指令汇编器)
    • ch14.4:练习:(略)
  • 十四、(ch15)贡献chisel
  • 十五、(ch16)总结

一、前言

这是记录学习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:信号类型与常量:

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

ch4.2:组合电路:

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

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)

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

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

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

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

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

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都在里面一起定义。
      【从嵌入式视角学习香山处理器】四、Chisel语言基础_第2张图片

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. 示例2:调用上述函数,来定义一个“对输入变量延时两个周期后输出的变量”
def delay(x:UInt) = RegNext(x)
val delOut = delay(delay(defIn))

六、(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. 定义带reset和enable信号的寄存器:
val resetEnableReg = RegInit(0.U(4.W))
when(enable) { resetEnableReg := inVal }

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
      【从嵌入式视角学习香山处理器】四、Chisel语言基础_第3张图片
      【从嵌入式视角学习香山处理器】四、Chisel语言基础_第4张图片

ch8.2.5:脉冲宽度调制

  1. 示例:看不懂。略过

ch8.3:位移寄存器

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

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

  1. 示例:fig8.12,serIn从高位开始移入outReg[3:0]
    【从嵌入式视角学习香山处理器】四、Chisel语言基础_第5张图片
val outReg = RegInit(0.U(4.W))
outReg := Cat(serIn, outReg(3, 1))
val q = outReg

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

  1. 示例:fig8.13,并行的loadReg[3:0]赋值给串行的寄存器serOut
    【从嵌入式视角学习香山处理器】四、Chisel语言基础_第6张图片
	when(load) {
        loadReg := d
    } otherwise {
        loadReg := Cat(0.U, loadReg(3, 1))
    }
    val serOut = loadReg(0)

ch8.4:存储器

  1. 存储器可以通过一系列的寄存器搭建。但基于寄存器的存储器硬件上非常昂贵,所以更大的存储器是通过sram搭建的
  2. 同步存储器:在输入端(读/写地址、写数据、写使能)设计了寄存器。这意味着设置地址后一个周期,读的数据就可用了。
  3. 用chisel库函数SyncReadMem构建的存储器模块只是最基本的存储器:可以指定byte数,但输入、输出data的宽度固定为1byte,另外还有一个写使能。剩下的定义需要外部重新封装。
  4. 有一个有趣的问题:当在进行写操作的同一个时钟周期,对同一个地址进行读操作,会读到什么值、我们对存储器的read-during-write行为感兴趣。
    • 有三种可能:新值、旧值或未定义的值(新值和旧值不同bit的混合)。
    • 发生在fpga上的可能性取决于fpga的类型,有时还可以指定。
  5. 示例:fig8.15,使用添加前递电路来使得read-during-write输出新值
    【从嵌入式视角学习香山处理器】四、Chisel语言基础_第7张图片
        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. 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
        }

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. 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))

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

  1. 示例1:二进一出、io类型支持自定义的复用器
        def myMux[T <: Data](sel: Bool, tPath: T, fPath: T): T = {
            ...
        }
  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. 上面的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. 上面的示例表示:
    • 定义一个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?
        }

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. 顺便给出单元测试示例: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:使用函数式编程做硬件生成

  1. 将实现了硬件生成的基本函数a作为一个参数,传给另一个函数作为参数b,以被调用来生成多个、或组合的新硬件模块
  2. 示例1:将基本的二进一出加法器作为向量操作函数vec的参数,来实现多进一出的加法链(向量加法器)
        def add(a:UInt, b:UInt) = a + b
        val sum = vec.reduce(add)
  1. 示例2:(优化)把示例1直接写成一行语句(利用scala通配符"_")
        val sum = vec.reduce(_ + _)
  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:冒泡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. 示例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
            }
        }
  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
      
    • 转换为对应的机器码:
          val prog = Array[Int] (
              0x0903, //addi 0x3
              0x09ff, //addi -1
              0x0d02, //subi 2
              0x21ab, //ldi 0xab
              0x230f, //and 0x0f
              0x25c3, //or 0xc3
              0x0000
          )
      
  3. 示例:从字符串到机器码的转换
    • 从外部读取文件,导入保存为数组;里面按行放置汇编指令
    • 汇编器要实现的功能1:识别指令字符串,比如:add、sub、or
    • 汇编器要实现的功能2:能区分汇编语句的参数是寄存器还是立即数
    • 汇编器要实现的功能3:能解析数字(立即数)为统一的无符号整形,包括:十六进制数、有/无符号的十进制数(实际要调用scala的库函数来实现,比如:Integer.parseInt()、String.substring())
    • 按行解析完成汇编指令的指令、参数部分后,拼接为十六进制的机器码,比如:"addi 0x3"的输出结果为0x0903

ch14.4:练习:(略)


十四、(ch15)贡献chisel

  1. (略)

十五、(ch16)总结

  1. (略)

你可能感兴趣的:(香山处理器,嵌入式,scala,risc-v,嵌入式)