天天看點

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裡保留下來的。将來也許會有更便捷的方式,讀者可以留意。