主體内容摘自: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()
函數的定義如下:
- 第一個參數是args,它是用來傳參的,可以在指令行傳,也可以直接提供一個數組,具體下面再說;
- 第二個參數dut用來傳入一個使用者編寫的子產品的,它是無參、傳回類型是RawModule的函數字面量。因為Chisel的子產品本質上還是Scala的class,是以隻需用new構造一個對象作為傳回結果即可。
- 主函數裡可以包括多個execute函數,也可以包含其它代碼。
注2:
chisel3.stage.ChiselStage.execute()
函數說明:
上述函數已經不推薦使用了,若是想使用execute函數,現在推薦使用的execute,其實你在
chisel3.stage.ChiselStage
裡面找不到該函數的定義,那是因為這個函數繼承自
class ChiselStage
,定義和使用方法如下:
firrtl.Stage
import chisel3.stage.{ChiselStage, ChiselGeneratorAnnotation}
(new chisel3.stage.ChiselStage).execute(args, Seq(ChiselGeneratorAnnotation(() => new FullAdder)))
注3:
chisel3.stage.ChiselStage.emitVerilog()
函數說明:
其實還有一個函數更被推薦使用,就是,該函數也可以用來生成verilog代碼,定義如下,可以看出其實内部也是調用了
chisel3.stage.ChiselStage.emitVerilog()
這個函數,隻不過所需傳入的參數更加簡單,第一參數不再需要傳入一個傳回RawModule的無參函數,而是直接将RawModule類型的對象傳入即可,這用到了scala的傳名參數的文法!!!
chisel3.stage.ChiselStage.execute()
注4: 關于如何運作主函數的說明
建議把設計檔案和主函數放在一個包裡,比如這裡的“
package test
”,這樣省去了編寫路徑的麻煩。
這裡需要注意的是,放在同一個包裡并不是指的同一路徑下,這和sbt的編譯機制有關,為了簡單起見,我們隻需要按照sbt的要求存放檔案即可,這樣就可以省去編寫路徑的麻煩!!!
要運作這個主函數,需要在
build.sbt
檔案所在的路徑下打開終端,然後執行指令:
注意,sbt後面有空格,再後面的内容都是被單引号對或雙引号對包起來。其中,是讓sbt執行
test:runMain
路徑下的主函數的指令,而
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庫裡的
,給子產品的端口加激勵、檢視信号值,并交由下遊的Verilator來仿真、産生波形。這種方式比較簡單,類似于Verilog的testbench,适合小型電路的驗證。對于超大型的系統級電路,最好還是生成Verilog,交由成熟的EDA工具,用UVM進行驗證。peek和poke函數
注8: 關于測試包的說明
現在已經推了chiseltest包,可以用其進行測試,寫peekpoke更簡潔而且資料類型也更嚴謹了。之後,會再研究一下!!!下面先簡單看下使用chiseltest和使用iotesters在代碼風格上的不同:
可以看出,這些方法已經變成了每個IO的屬性!!!
1、使用 chisel3.iotesters
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
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
要運作測試,自然也是通過主函數,但是這次是使用
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
在使用
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裡保留下來的。将來也許會有更便捷的方式,讀者可以留意。