天天看点

chisel生成Verilog与基本测试(更新)

主体内容摘自:https://blog.csdn.net/qq_34291505/article/details/87880730

一、生成Verilog

前面介绍Scala的内容里说过,Scala程序的入口是主函数。所以,生成Verilog的程序自然是在主函数里例化待编译的模块,然后运行这个主函数。

例化待编译模块需要特殊的方法调用。chisel3包里有一个单例对象Driver,它包含一个方法execute,该方法接收两个参数:

  • 第一个参数是命令行传入的实参即字符串数组args,
  • 第二个是返回待编译模块的对象的无参函数。

运行这个execute方法,就能得到Verilog代码。

假设在

src/main/scala

文件夹下有一个全加器的Chisel设计代码,如下所示:

// fulladder.scala
package test
 
import chisel3._
 
class FullAdder extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(1.W))
    val b = Input(UInt(1.W))
    val cin = Input(UInt(1.W))
    val s = Output(UInt(1.W))
    val cout = Output(UInt(1.W))  
  })
 
  io.s := io.a ^ io.b ^ io.cin
  io.cout := (io.a & io.b) | ((io.a | io.b) & io.cin)
}
           

接着,读者需要在

src/test/scala

文件夹下编写对应的主函数文件,如下所示:

// fullAdderGen.scala
package test
 
object FullAdderGen extends App {
  chisel3.Driver.execute(args, () => new FullAdder)
}
           

注1:

chisel3.Driver.execute()

函数的定义如下:

chisel生成Verilog与基本测试(更新)
  • 第一个参数是args,它是用来传参的,可以在命令行传,也可以直接提供一个数组,具体下面再说;
  • 第二个参数dut用来传入一个用户编写的模块的,它是无参、返回类型是RawModule的函数字面量。因为Chisel的模块本质上还是Scala的class,所以只需用new构造一个对象作为返回结果即可。
  • 主函数里可以包括多个execute函数,也可以包含其它代码。

注2:

chisel3.stage.ChiselStage.execute()

函数说明:

上述函数已经不推荐使用了,若是想使用execute函数,现在推荐使用

chisel3.stage.ChiselStage

的execute,其实你在

class ChiselStage

里面找不到该函数的定义,那是因为这个函数继承自

firrtl.Stage

,定义和使用方法如下:
chisel生成Verilog与基本测试(更新)
import chisel3.stage.{ChiselStage, ChiselGeneratorAnnotation}

(new chisel3.stage.ChiselStage).execute(args, Seq(ChiselGeneratorAnnotation(() => new FullAdder)))
           

注3:

chisel3.stage.ChiselStage.emitVerilog()

函数说明:

其实还有一个函数更被推荐使用,就是

chisel3.stage.ChiselStage.emitVerilog()

,该函数也可以用来生成verilog代码,定义如下,可以看出其实内部也是调用了

chisel3.stage.ChiselStage.execute()

这个函数,只不过所需传入的参数更加简单,第一参数不再需要传入一个返回RawModule的无参函数,而是直接将RawModule类型的对象传入即可,这用到了scala的传名参数的语法!!!
chisel生成Verilog与基本测试(更新)

注4: 关于如何运行主函数的说明

建议把设计文件和主函数放在一个包里,比如这里的“

package test

”,这样省去了编写路径的麻烦。

这里需要注意的是,放在同一个包里并不是指的同一路径下,这和sbt的编译机制有关,为了简单起见,我们只需要按照sbt的要求存放文件即可,这样就可以省去编写路径的麻烦!!!

要运行这个主函数,需要在

build.sbt

文件所在的路径下打开终端,然后执行命令:

注意,sbt后面有空格,再后面的内容都是被单引号对或双引号对包起来。其中,

test:runMain

是让sbt执行

src/test/scala

路径下的主函数的命令,而

test.FullAdderGen

就是要执行的那个主函数。其中

test

对应的就是

package test

FullAdderGen

就是单例对象的名字。
其实主函数也可以直接和设计代码写在一起,也即写在

fulladder.scala

文件中,注意这个文件在

src/main/scala

路径下,此时如果要运行该主函数,命令需要稍作改变:

sbt 'runMain test.FullAdderGen'

。可以看到唯一的区别就是少了

test:

,此时的含义是去执行

src/main/scala

路径下的主函数。

如果设计文件没有错误,那么最后就会看到“

[success] Total time: 6 s, completed Feb 22, 2019 4:45:31 PM

”这样的信息。此时,终端的路径下就会生成三个文件:

FullAdder.anno.json、FullAdder.fir和FullAdder.v

第一个文件用于记录传递给Firrtl编译器的Scala注解,读者可以不用关心。第二个后缀为“.fir”的文件就是对应的Firrtl代码,第三个自然是对应的Verilog文件。

首先查看最关心的Verilog文件,内容如下:

// FullAdder.v
module FullAdder(
  input   clock,
  input   reset,
  input   io_a,
  input   io_b,
  input   io_cin,
  output  io_s,
  output  io_cout
);
  wire  _T; // @[fulladder.scala 14:16]
  wire  _T_2; // @[fulladder.scala 15:20]
  wire  _T_3; // @[fulladder.scala 15:37]
  wire  _T_4; // @[fulladder.scala 15:45]
  assign _T = io_a ^ io_b; // @[fulladder.scala 14:16]
  assign _T_2 = io_a & io_b; // @[fulladder.scala 15:20]
  assign _T_3 = io_a | io_b; // @[fulladder.scala 15:37]
  assign _T_4 = _T_3 & io_cin; // @[fulladder.scala 15:45]
  assign io_s = _T ^ io_cin; // @[fulladder.scala 14:8]
  assign io_cout = _T_2 | _T_4; // @[fulladder.scala 15:11]
endmodule
           

可以看到,代码逻辑与想要表达的意思完全一致,而且对应的代码都用注释标明了来自于Chisel源文件的哪里。但由于这是通过语法分析的脚本代码得到的,所以看上去显得很笨拙、僵硬,生成了大量无用的中间变量声明。对于下游的综合器而言是一个负担,可能会影响综合器的优化。而且在进行仿真时,要理解这些中间变量也很麻烦。对后端人员来说,这也是让人头疼的问题。

接着再看一看Firrtl代码,内容如下:

// FullAdder.fir
;buildInfoPackage: chisel3, version: 3.2-SNAPSHOT, scalaVersion: 2.12.6, sbtVersion: 1.1.1
circuit FullAdder : 
  module FullAdder : 
    input clock : Clock
    input reset : UInt<1>
    output io : {flip a : UInt<1>, flip b : UInt<1>, flip cin : UInt<1>, s : UInt<1>, cout : UInt<1>}
    
    node _T = xor(io.a, io.b) @[fulladder.scala 14:16]
    node _T_1 = xor(_T, io.cin) @[fulladder.scala 14:23]
    io.s <= _T_1 @[fulladder.scala 14:8]
    node _T_2 = and(io.a, io.b) @[fulladder.scala 15:20]
    node _T_3 = or(io.a, io.b) @[fulladder.scala 15:37]
    node _T_4 = and(_T_3, io.cin) @[fulladder.scala 15:45]
    node _T_5 = or(_T_2, _T_4) @[fulladder.scala 15:28]
    io.cout <= _T_5 @[fulladder.scala 15:11]  
           

可以看到,Firrtl代码与它生成的Verilog代码非常接近。这种代码风格虽然不方便人工阅读,但是适合语法分析脚本使用。

二、在命令里增加参数

Ⅰ、给Firrtl传递参数

在运行主函数时,可以在刚才的命令后面继续增加可选的参数。例如,增加参数“–help”查看帮助菜单,运行命令:

可以得到如下帮助信息:

common options
  -tn, --top-name <top-level-circuit-name>
                           This options defines the top level circuit, defaults to dut when possible
  -td, --target-dir <target-directory>
                           This options defines a work directory for intermediate files, default is .
  -ll, --log-level <Error|Warn|Info|Debug|Trace>
                           This options defines a work directory for intermediate files, default is .
  -cll, --class-log-level <FullClassName:[Error|Warn|Info|Debug|Trace]>[,...]
                           This options defines a work directory for intermediate files, default is .
  -ltf, --log-to-file      default logs to stdout, this flags writes to topName.log or firrtl.log if no topName
  -lcn, --log-class-names  shows class names and log level in logging output, useful for target --class-log-level
  --help                   prints this usage text
  <arg>...                 optional unbounded args
chisel3 options
  -chnrf, --no-run-firrtl  Stop after chisel emits chirrtl file
firrtl options
  -i, --input-file <firrtl-source>
                           use this to override the default input file name , default is empty
  -o, --output-file <output>
                           use this to override the default output file name, default is empty
  -faf, --annotation-file <input-anno-file>
                           Used to specify annotation files (can appear multiple times)
  -foaf, --output-annotation-file <output-anno-file>
                           use this to set the annotation output file
  -X, --compiler <high|middle|low|verilog|sverilog>
                           compiler to use, default is verilog
 --info-mode <ignore|use|gen|append>
                           specifies the source info handling, default is append
  -fct, --custom-transforms <package>.<class>
                           runs these custom transforms during compilation.
  -fil, --inline <circuit>[.<module>[.<instance>]][,..],
                           Inline one or more module (comma separated, no spaces) module looks like "MyModule" or "MyModule.myinstance
  -firw, --infer-rw        Enable readwrite port inference for the target circuit
  -frsq, --repl-seq-mem -c:<circuit>:-i:<filename>:-o:<filename>
                           Replace sequential memories with blackboxes + configuration file
  -clks, --list-clocks -c:<circuit>:-m:<module>:-o:<filename>
                           List which signal drives each clock of every descendent of specified module
  -fsm, --split-modules    Emit each module to its own file in the target directory.
  --no-check-comb-loops    Do NOT check for combinational loops (not recommended)
  --no-dce                 Do NOT run dead code elimination 
           

例如,最常用的是参数“-td”,可以在后面指定一个文件夹,这样之前生成的三个文件就在该文件夹里,而不是在当前路径下。其格式如下:

Ⅱ、给主函数传递参数

Scala的类可以接收参数,自然Chisel的模块也可以接收参数。假设要构建一个n位的加法器,具体位宽不确定,根据需要而定。那么,就可以把端口位宽参数化,例化时传入想要的参数即可。例如:

// adder.scala
package test
 
import chisel3._
 
class Adder(n: Int) extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(n.W))
    val b = Input(UInt(n.W))
    val s = Output(UInt(n.W))
    val cout = Output(UInt(1.W))  
  })
 
  io.s := (io.a +& io.b)(n-1, 0)
  io.cout := (io.a +& io.b)(n)
}
 
// adderGen.scala
package test
 
object AdderGen extends App {
  chisel3.Driver.execute(args, () => new Adder(args(0).toInt))
}
           

在这里,模块Adder的主构造方法接收一个Int类型的参数n,然后用n去定义端口位宽。主函数在例化这个模块时,就要给出相应的参数。前面的帮助菜单里显示,在运行sbt命令时,可以传入若干个独立的参数。

和运行Scala的主函数一样,这些命令行的参数也可以由字符串数组args通过下标来索引。从要运行的主函数后面开始,后面的内容都是按空格划分、从下标0开始的args的元素。比如例子中的主函数期望第一个参数即args(0)是一个数字字符串,这样就能通过方法toInt转换成Adder所需的参数。

执行如下命令:

可以在相应的文件夹下得到如下Verilog代码,其中位宽的确是8位的:

// Adder.v
module Adder(
  input        clock,
  input        reset,
  input  [7:0] io_a,
  input  [7:0] io_b,
  output [7:0] io_s,
  output       io_cout
);
  wire [8:0] _T; 
  assign _T = io_a + io_b; 
  assign io_s = _T[7:0]; 
  assign io_cout = _T[8];
endmodule
           

注5: 关于使用命令行给

Firrtl

和主函数传递参数的说明

上面提到的三种生成verilog的函数的使用方法是一样的,都是直接使用args数组,它会在命令行接收输入:
sbt 'test:runMain test.FullAdderGen -td ./generated/fulladder' 

sbt 'test:runMain test.AdderGen 8 -td ./generated/adder'
           

注6: 关于不用命令行,直接传入Array数组给

Firrtl

传递参数的说明

其实从args的类型我们可以看出,它就是一个

Array[String]

,因此我们可以直接传入一个包含要传入的参数数组,如下面的几种写法:
chisel3.Driver.execute(Array("--target-dir", "generated"), () => new FullAdder)

(new chisel3.stage.ChiselStage).emitVerilog(new FullAdder, Array("--target-dir", "generated"))

(new chisel3.stage.ChiselStage).execute(Array("--target-dir", "generated"), Seq(ChiselGeneratorAnnotation(() => new FullAdder)))
           

注7: 关于不用命令行,直接传入Array数组给主函数传递参数的说明

和注6不同的是,我们需要先定义一个Array数组,(注意变量名不能为args,因为这是一个特殊的变量,它是App特质中的一个属性);然后将引用这个数组的变量传入,否则会有问题,比如以下两种情况:
  • 正确写法
val my_arg = Array("8","--target-dir", "generated")

(new chisel3.stage.ChiselStage).emitVerilog(new Adder(my_arg(0).toInt),my_arg)

(new chisel3.stage.ChiselStage).execute(my_arg, Seq(ChiselGeneratorAnnotation(() => new Adder(my_arg(0).toInt))))
           
  • 错误写法
(new chisel3.stage.ChiselStage).emitVerilog(new Adder(Array(0).toInt),Array("8","--target-dir", "generated"))

(new chisel3.stage.ChiselStage).execute(Array("--target-dir", "generated"), Seq(ChiselGeneratorAnnotation(() => new Adder(Array(0).toInt))))
           

三、编写简单的测试

Chisel的测试有两种:

  • 第一种是利用Scala的测试来验证Chisel级别的代码逻辑有没有错误。因为这部分内容比较复杂,而且笔者目前也没有深入学习有关Scala测试的内容,所以这部分内容可有读者自行选择研究。
  • 第二种是利用Chisel库里的

    peek和poke函数

    ,给模块的端口加激励、查看信号值,并交由下游的Verilator来仿真、产生波形。这种方式比较简单,类似于Verilog的testbench,适合小型电路的验证。对于超大型的系统级电路,最好还是生成Verilog,交由成熟的EDA工具,用UVM进行验证。

注8: 关于测试包的说明

现在已经推了chiseltest包,可以用其进行测试,写peekpoke更简洁而且数据类型也更严谨了。之后,会再研究一下!!!下面先简单看下使用chiseltest和使用iotesters在代码风格上的不同:
chisel生成Verilog与基本测试(更新)

可以看出,这些方法已经变成了每个IO的属性!!!

1、使用

chisel3.iotesters

要编写一个简单的testbench,首先也是定义一个类,这个类的主构造方法接收一个参数,参数类型就是待测模块的类名。因为模块也是一个类,从Scala的角度来看,一个类就是定义了一种类型。

其次,这个类继承自

chisel3.iotesters.PeekPokeTester

类,并且把接收的待测模块也传递给此超类。最后,测试类内部有四种方法可用:

  • ①“poke(端口,激励值)”方法给相应的端口添加想要的激励值,激励值是

    Int

    类型的;
  • ②“peek(端口)”方法返回相应的端口的当前值;
  • ③“expect(端口,期望值)”方法会对第一个参数(端口)使用peek方法,然后与Int类型的期望值进行对比,如果两者不相等则出错;
  • ④“step(n)”方法则让仿真前进n个时钟周期。

因为测试模块只用于仿真,无需转成Verilog,所以类似for、do…while、to、until、map等Scala高级语法都可以使用,帮助测试代码更加简洁有效。

如下所示是一个对前一例中的8位加法器的testbench:

// addertest.scala
package test
 
import scala.util._
import chisel3.iotesters._
 
class AdderTest(c: Adder) extends PeekPokeTester(c) {
  val randNum = new Random
  for(i <- 0 until 10) {
    val a = randNum.nextInt(256)
    val b = randNum.nextInt(256)
    poke(c.io.a, a)
    poke(c.io.b, b)
    step(1)
    expect(c.io.s, (a + b) & 0xff)
    expect(c.io.cout, ((a + b) & 0x100) >> 8)
  }
}
           

其中,第一个包scala.util里包含了Scala生成伪随机数的类Random,第二个包chisel3.iotesters包含了测试类PeekPokeTester。

2、使用

chiseltest

// addertest.scala
package test
//特质ChiselScalatestTester在该包下,并且定义了需要使用的test方法
import chiseltest._
//FlatSpec在该包下
import org.scalatest._

class AdderTest extends FlatSpec with ChiselScalatestTester{
  behavior of "MyModule"
  // test class body here
  it should "do something" in {
    //test case body here
    test(new Adder(8)) { c =>
    //test body here
    val randNum = new Random
  	for(i <- 0 until 10) {
    val a = randNum.nextInt(256)
    val b = randNum.nextInt(256)
    c.io.a.poke(a.U)
    c.io.b.poke(b.U)
    c.clock.step(1)
    c.io.s.expect(((a + b) & 0xff).U)
    c.io.cout.expect((((a + b) & 0x100) >> 8).U)
  }
  }
 }
} 
           

四、运行测试

1、运行

chisel3.iotesters

写的测试

要运行测试,自然也是通过主函数,但是这次是使用

chisel3.iotesters

包里的execute方法。该方法与前面生成Verilog的方法类似,仅仅是多了一个参数列表,多出的第二个参数列表接收一个返回测试类的对象的函数:

// addertest.scala
object AdderTestGen extends App {
  chisel3.iotesters.Driver.execute(args, () => new Adder(8))(c => new AdderTest(c))
}
           

运行如下命令(需要安装verilator):

执行成功后,就能在相应文件夹里看到一个新生成的文件夹,里面是仿真生成的文件。其中“Adder.vcd”文件就是波形文件,使用GTKWave软件打开就能查看,将相应的端口拖拽到右侧就能显示波形。

如果只想在终端查看仿真运行的信息,则执行命令:

[info] [0.002] SEED 1550906002475

[info] [0.005] POKE io_a <- 184

[info] [0.006] POKE io_b <- 142

[info] [0.006] STEP 0 -> 1

[info] [0.007] EXPECT AT 1 io_s got 70 expected 70 PASS

[info] [0.008] EXPECT AT 1 io_cout got 1 expected 1 PASS

[info] [0.008] POKE io_a <- 114

[info] [0.009] POKE io_b <- 231

[info] [0.009] STEP 1 -> 2

[info] [0.009] EXPECT AT 2 io_s got 89 expected 89 PASS

[info] [0.009] EXPECT AT 2 io_cout got 1 expected 1 PASS

[info] [0.010] POKE io_a <- 183

[info] [0.010] POKE io_b <- 168

[info] [0.010] STEP 2 -> 3

[info] [0.011] EXPECT AT 3 io_s got 95 expected 95 PASS

[info] [0.011] EXPECT AT 3 io_cout got 1 expected 1 PASS

[info] [0.012] POKE io_a <- 223

[info] [0.012] POKE io_b <- 106

[info] [0.012] STEP 3 -> 4

[info] [0.012] EXPECT AT 4 io_s got 73 expected 73 PASS

[info] [0.013] EXPECT AT 4 io_cout got 1 expected 1 PASS

[info] [0.013] POKE io_a <- 12

[info] [0.013] POKE io_b <- 182

[info] [0.013] STEP 4 -> 5

[info] [0.014] EXPECT AT 5 io_s got 194 expected 194 PASS

[info] [0.014] EXPECT AT 5 io_cout got 0 expected 0 PASS

[info] [0.014] POKE io_a <- 52

[info] [0.014] POKE io_b <- 41

[info] [0.015] STEP 5 -> 6

[info] [0.015] EXPECT AT 6 io_s got 93 expected 93 PASS

[info] [0.016] EXPECT AT 6 io_cout got 0 expected 0 PASS

[info] [0.016] POKE io_a <- 187

[info] [0.017] POKE io_b <- 60

[info] [0.017] STEP 6 -> 7

[info] [0.017] EXPECT AT 7 io_s got 247 expected 247 PASS

[info] [0.018] EXPECT AT 7 io_cout got 0 expected 0 PASS

[info] [0.018] POKE io_a <- 218

[info] [0.019] POKE io_b <- 203

[info] [0.019] STEP 7 -> 8

[info] [0.019] EXPECT AT 8 io_s got 165 expected 165 PASS

[info] [0.020] EXPECT AT 8 io_cout got 1 expected 1 PASS

[info] [0.020] POKE io_a <- 123

[info] [0.021] POKE io_b <- 115

[info] [0.021] STEP 8 -> 9

[info] [0.021] EXPECT AT 9 io_s got 238 expected 238 PASS

[info] [0.022] EXPECT AT 9 io_cout got 0 expected 0 PASS

[info] [0.022] POKE io_a <- 17

[info] [0.022] POKE io_b <- 197

[info] [0.023] STEP 9 -> 10

[info] [0.023] EXPECT AT 10 io_s got 214 expected 214 PASS

[info] [0.024] EXPECT AT 10 io_cout got 0 expected 0 PASS

test Adder Success: 20 tests passed in 15 cycles in 0.047415 seconds 316.36 Hz

[info] [0.025] RAN 10 CYCLES PASSED

[success] Total time: 7 s, completed Feb 23, 2019 3:13:26 PM

2、运行

chiseltest

写的测试

在使用

chiseltest

的代码中,如果

AluTester

继承了

FlatSpec

,那么即使定义成了

class

(正常情况下,在

scala

中主函数必须定义在

object

中),该段代码还是可以运行,只不过运行方式和之前不一样了,写法如下:

如果想要生成vcd文件,写法如下:

注意此时不能使用

test:runMain

来运行了,因为只有定义成

object

才可以这样被运行,

class

不行。但是如果还想这样运行,那就必须改成如下形式:

package test
//特质ChiselScalatestTester在该包下,并且定义了需要使用的test方法
//peek poke等方法也定义在了该包下,并且定义了隐式转换,所以我们可以直接.xxxx调用
import chiseltest._
import chiseltest.RawTester.test

object AdderTest extends App{

    //test case body here
    test(new Adder(8)) { c =>
    //test body here
    ......
  }
}
           

然后在命令行执行以下命令即可:

注意,不能简单的只将

class

修改为

object

,然后再混入

App

。因为这样虽然不报错,但是好像不工作。如下所示:

package test
//特质ChiselScalatestTester在该包下,并且定义了需要使用的test方法
//peek poke等方法也定义在了该包下,并且定义了隐式转换,所以我们可以直接.xxxx调用
import chiseltest._
//FlatSpec在该包下
import org.scalatest._

object AdderTest extends FlatSpec with ChiselScalatestTester with App{
  behavior of "MyModule"
  // test class body here
  it should "do something" in {
    //test case body here
    test(new Adder(8)) { c =>
    //test body here
    ......
  }
 }
}
           

五、总结

本章介绍了从Chisel转换成Verilog、测试设计的基本方法。因为Chisel还在更新中,这些方法也是从Chisel2里保留下来的。将来也许会有更便捷的方式,读者可以留意。