chisel快速入门(二)
????????上一篇見此:
chisel快速入門(一)_滄海一升的博客-CSDN博客簡單介紹了chisel,使硬件開發者能快速上手chisel。https://blog.csdn.net/qq_21842097/article/details/121415341
十、運行和測試
????????現在我們已經定義了模塊,我們將討論如何實際運行并測試電路。Chisel代碼可以轉換為C++或Verilog。 為了編譯電路,我們需要調用chiselMain:
object tutorial {def main(args: Array[String]) = {chiselMain(args, () => Module(new Mux2())) } }????????測試是電路設計的關鍵部分,因此在 Chisel 中,我們通過使用 Tester 類的子類在 Scala 中提供測試向量來提供一種測試電路的機制:
class Tester[T <: Module] (val c: T, val isTrace: Boolean = true) {var t: Intval rnd: Randomdef int(x: Boolean): BigInt def int(x: Int): BigIntdef int(x: Bits): BigInt def reset(n: Int = 1)def step(n: Int): Intdef pokeAt(data: Mem[T], index: Int, x: BigInt) def poke(data: Bits, x: BigInt)def poke(data: Aggregate, x: Array[BigInt])def peekAt(data: Mem[T], index: Int)def peek(data: Bits): BigIntdef peek(data: Aggregate): Array[BigInt]def expect (good: Boolean, msg: String): Boolean def expect (data: Bits, target: BigInt): Boolean }????????它將tester綁定到模塊,并允許用戶使用給定的調試協議編寫測試。用戶會用到一下這些:
- poke: 設置輸入端口以及狀態值
- step: 以一個時間單元執行電路
- peek: 讀取端口和狀態值
- expect: 比較peek獲得的值和期望的值
????????用戶使用如下的方式連接tester和模塊:
object chiselMainTest { def apply[T <: Module](args: Array[String], comp: () => T)( tester: T => Tester[T]): T }????????當- -test作為參數傳遞給chiselMainTest時,tester實例在獨立的進程中運行被測器件(DUT),并連接stdin和stdout,這樣調試命令可以發送到DUT,響應也可以從DUT接收,如圖所示。
????????舉例說明:
class Mux2Tests(c: Mux2) extends Tester(c) { val n = pow(2, 3).toIntfor (s <- 0 until 2) {for (i0 <- 0 until 2) { for (i1 <- 0 until 2) {poke(c.io.sel, s)poke(c.io.in1, i1)poke(c.io.in0, i0)step(1)expect(c.io.out, (if (s == 1) i1 else i0))}}} }?????????使用poke將Mux2的每個輸入的分別設置為合適的值。對于這個例子,我們通過硬編碼輸入到一些已知的值并檢查輸出是否對應于已知的值來測試Mux2。為此,在每次迭代中,我們生成模塊輸入,讓模擬將這些值分配給我們正在測試的器件c的輸入,單步運行電路并對比期望值。最后,簡單說明一下如何調用測試器:
chiselMainTest(args + "--test", () => Module(new Mux2())){ c => new Mux2Tests(c) }還有其他的一些命令參數:
- –targetDir 目標路徑名前綴
- –genHarness 生成C++文件
- –backend v 生成verilog
- –backend c 生成C++(默認)
- –vcd 開啟vcd打印
- –debug 把所有的wire放入class文件
十一、狀態元素
????????Chisel支持的狀態元素的最簡單形式是上升沿觸發寄存器,可以實例化為:
val reg = Reg(next = in)????????該電路具有輸出,該輸出是前一個時鐘周期的輸入信號產生的值。注意,我們不必指定Reg的類型,因為它會在實例化時從輸入開始自動推斷。在當前版本的Chisel中,時鐘和復位是全局信號,在需要時可以隱式包含。
????????使用寄存器,我們可以快速定義一些有用的電路結構。 例如,當當前值為true且之前的值為false時,上升沿檢測器能夠獲取到布爾信號并輸出true,如下所示:
def risingedge(x: Bool) = x && !Reg(next = x)????????計數器是一個重要的時序電路。 如果想構建一個向上計數器,計數到最大值max后回到零:
def counter(max: UInt) = {val x = Reg(init = UInt(0, max.getWidth))x := Mux(x === max, UInt(0), x + UInt(1))x }????????計數器復位值為0(寬度大到足以容納max),當電路的全局復位置位時,寄存器將初始化為該值。
????????計數器可用于構建很多有用的時序電路。例如,我們可以通過在計數器達到零時輸出true來構建脈沖發生器:
def pulse(n: UInt) = counter(n - UInt(1)) === UInt(0)????????然后可以通過切換方波發生器脈沖序列,在每個脈沖上的true和false之間切換:?
// Flip internal state when input true. def toggle(p: Bool) = {val x = Reg(init = Bool(false)) x := Mux(p, !x, x)x } // Square wave of a given period. def squareWave(period: UInt) = toggle(pulse(period/2))1、轉發聲明
????????純組合電路在節點之間不存在周期,如果檢測到這樣的周期,則Chisel將報告錯誤。因為它們不具有周期,所以可以總是以前饋方式構建組合電路,通過添加一些輸入從已經定義的節點導出的新節點。
????????時序電路在節點之間具有反饋,因此有時需要在生成節點被定義之前輸出。因為Scala順序執行程序語句,所以我們允許數據節點作為wire來提供節點聲明,這樣可以立即被使用,但其輸入將稍后設置。
????????如下例所示,在簡單的CPU中,我們需要定義pcPlus4和brTarget的線,以便在定義之前引用它們:
val pcPlus4 = UInt() val brTarget = UInt() val pcNext = Mux(io.ctrl.pcSel, brTarget, pcPlus4) val pcReg = Reg(next = pcNext, init = UInt(0, 32)) pcPlus4 := pcReg + UInt(4) ... brTarget := addOut????????接線操作符:=用于在pcReg和addOut定義后連接。
2、條件更新
????????在前面使用到寄存器的示例中,我們簡單地將組合邏輯塊連接到寄存器的輸入。當描述狀態元素的操作時,指定何時將發生寄存器更新并且用幾個單獨的語句指明這些更新。
????????Chisel以when的形式提供條件更新規則,以支持這種順序邏輯描述的風格。例如,
val r = Reg(init = UInt(0, 16)) when (cond) {r := r + UInt(1) }????????其中只有在cond為真時,才在當前時鐘周期的結尾更新寄存器r。when的參數是返回Bool值。后面的更新塊只能包含使用賦值運算符:=,簡單表達式和用val定義的命名引線的更新語句。
????????在條件更新序列中,條件為真的最近條件更新優先。 例如:
when (c1) { r := UInt(1) } when (c2) { r := UInt(2) }????????上述表達式會根據以下真值表更新r:
????????條件更新結構可以嵌套,任何給定塊在所有外嵌套條件的聯合下才能執行。
????????條件可以使用when,.elsewhen,.otherwise來鏈式表達,對應于Scala中的if, else if, else。例如:
when (c1) { u1 } .elsewhen (c2) { u2 } .otherwise { ud } // the same as when (c1) { u1 } when (!c1 && c2) { u2 } when (!(c1 || c2)) { ud }????????Chisel還允許Wire,即一些組合邏輯的輸出,成為條件性更新語句的目標,以允許逐步構建復雜的組合邏輯表達式。Chisel不允許不指定組合輸出,并且如果組合輸出未遇到無條件更新,則報告錯誤。
3、有限狀態機
????????在數字設計中有限狀態機(FSM)是時序電路常用的類型。簡單FSM的例子就是奇偶校驗生成器:
class Parity extends Module { val io = new Bundle {val in = Bool(dir = INPUT)val out = Bool(dir = OUTPUT) }val s_even :: s_odd :: Nil = Enum(UInt(), 2) val state = Reg(init = s_even)when (io.in) {when (state === s_even) { state := s_odd }when (state === s_odd) { state := s_even } }io.out := (state === s_odd) }????????其中Enum(Uint(), 2)生成兩個UInt數。當io.in為true時更新狀態。需要注意的是,FSM的所有機制都建立在寄存器,線和條件更新的基礎上。
????????下面是一個復雜的FSM例子,這是一個自動售貨機接收貨幣的電路:
class VendingMachine extends Module {val io = new Bundle {val nickel = Bool(dir = INPUT)val dime = Bool(dir = INPUT)val valid = Bool(dir = OUTPUT)}val s_idle :: s_5 :: s_10 :: s_15 :: s_ok :: Nil = Enum(UInt(), 5)val state = Reg(init = s_idle) when (state === s_idle) {when (io.nickel) { state := s_5 }when (io.dime) { state := s_10 } }when (state === s_5) {when (io.nickel) { state := s_10 } when (io.dime) { state := s_15 }}when (state === s_10) {when (io.nickel) { state := s_15 }when (io.dime) { state := s_ok } }when (state === s_15) {when (io.nickel) { state := s_ok } when (io.dime) { state := s_ok }}when (state === s_ok) {state := s_idle}io.valid := (state === s_ok) }? ? ? ? 采用switch風格代碼如下:
class VendingMachine extends Module {val io = new Bundle {val nickle = Bool(dir = INPUT)val dime = Bool(dir = INPUT)val valid = Bool(dir = OUTPUT)}val s_idle :: s_5 :: s_10 :: s_15 :: s_ok :: Nil = Enum(UInt(), 5)val state = Reg(init = s_idle) switch (state) { is (s_idle) {when (io.nickel) { state := s_5 }when (io.dime) { state := s_10 } } is (s_5) {when (io.nickel) { state := s_10 } when (io.dime) { state := s_15 }}is (s_10) {when (io.nickel) { state := s_15 }when (io.dime) { state := s_ok } }is (s_ok) {state := s_idle}}io.valid := (state === s_ok) }十二、內存
????????Chisel提供了創建只讀和讀/寫存儲器的功能。
1、ROM
????????用戶可以使用Vec定義ROM:
Vec(inits: Seq[T]) Vec(elt0: T, elts: T*)????????其中inits是初始化ROM的初始Data序列。例如,用戶可以創建一個初始化為1,2,4,8的小型ROM,并使用計數器作為地址生成器循環訪問所有值,如下所示:
val m = Vec(Array(UInt(1), UInt(2), UInt(4), UInt(8))) val r = m(counter(UInt(m.length)))????????我們可以使用如下初始化的ROM創建n值正弦查找表:
def sinTable (amp: Double, n: Int) = { val times = Range(0, n, 1).map(i => (i*2*Pi)/(n.toDouble-1) - Pi) val inits = times.map(t => SInt(round(amp * sin(t)), width = 32)) Vec(inits) } def sinWave (amp: Double, n: Int) =sinTable(amp, n)(counter(UInt(n))????????其中amp用于縮放存儲在ROM中的固定點值。
2、Mem
????????存儲器在Chisel中被給予特殊處理,因為存儲器的硬件實現具有許多變化,例如,FPGA存儲器與ASIC存儲實例化的結果完全不同。Chisel定義了一個內存抽象,可以映射到簡單的Verilog行為描述,也可以映射到從代工廠或IP廠商提供的外部內存生成器獲得的內存模塊實例。
????????Chisel通過Mem結構可以支持隨機存取存儲器。寫入Mems是正邊沿觸發,讀取是組合或正邊沿觸發。
object Mem {def apply[T <: Data](type: T, depth: Int,seqRead: Boolean = false): Mem } class Mem[T <: Data](type: T, depth: Int, seqRead: Boolean = false)extends Updateable { def apply(idx: UInt): T}????????通過使用UInt索引創建到Mems的端口。具有一個寫入端口和兩個組合讀取端口的32-entry的寄存器堆可以如下表示:
val rf = Mem(UInt(width = 64), 32) when (wen) { rf(waddr) := wdata } val dout1 = rf(waddr1) val dout2 = rf(waddr2)????????如果設置了可選參數seqRead,當讀地址為Reg時,Chisel將嘗試推斷順序讀端口。
????????單讀端口,單寫端口SRAM可以描述如下:
val ram1r1w = Mem(UInt(width = 32), 1024, seqRead = true) val reg_raddr = Reg(UInt()) when (wen) { ram1r1w(waddr) := wdata } when (ren) { reg_raddr := raddr } val rdata = ram1r1w(reg_raddr)????????單端口SRAM可以在讀和寫條件在鏈中相同時相互排斥時推斷:
val ram1p = Mem(UInt(width = 32), 1024, seqRead = true) val reg_raddr = Reg(UInt()) when (wen) { ram1p(waddr) := wdata } .elsewhen (ren) { reg_raddr := raddr } val rdata = ram1p(reg_raddr)????????如果相同的Mem地址在相同的時鐘沿上被寫入和順序讀取,或者如果順序讀取使能被清除,則讀取數據為未定義。
十三、接口和批量連接
????????對于更復雜的模塊,在定義模塊的 IO 時定義和實例化接口類通常很有用。
????????首先,接口類促進重用,允許用戶以有用的形式一次性捕獲所有通用接口。 其次,接口允許用戶通過模塊之間的批量連接來顯著減少布線。 最后,用戶可以在一個地方對大型接口進行更改,從而減少添加或刪除接口部分時所需的更新次數。
1、端口類、子類和嵌套
????????正如我們之前看到的,用戶可以通過定義一個繼承 Bundle 的類來定義他們自己的接口。
???????? 例如,可以為握手數據定義一個簡單的鏈接,如下所示:
class SimpleLink extends Bundle { val data = UInt(16, OUTPUT) val valid = Bool(OUTPUT) }????????然后我們可以通過使用包繼承添加奇偶校驗位來擴展 SimpleLink:
class PLink extends SimpleLink { val parity = UInt(5, OUTPUT) }????????通常,用戶可以使用繼承將他們的接口組織成層次結構。 從那里我們可以通過將兩個 PLink 嵌套到一個新的 FilterIO 包中來定義過濾器接口:
class FilterIO extends Bundle { val x = new PLink().flip val y = new PLink() }????????其中flip遞歸地改變Bundle的“相性”,將輸入改變為輸出和將輸出改變為輸入。
????????我們現在可以通過定義一個過濾器類繼承模塊來定義一個過濾器:
class Filter extends Module { val io = new FilterIO() ... }????????其中io包含了FilterIO。
2、Bundle Vector
????????除了單個元素之外,元素Vector可以組成更豐富的分層接口。 例如,為了創建一個帶有輸入向量的交叉開關,產生一個輸出向量,并由 UInt 輸入選擇,我們使用 Vec 構造函數。
class CrossbarIo(n: Int) extends Bundle {val in = Vec.fill(n){ new PLink().flip() } val sel = UInt(INPUT, sizeof(n))val out = Vec.fill(n){ new PLink() } }????????其中Vec用第一個參獲取大小,區塊返回一個端口作為第二個參數。
3、批量連接
????????我們現在可以將兩個過濾器組成一個過濾器塊,如下所示:
class Block extends Module { val io = new FilterIO()val f1 = Module(new Filter()) val f2 = Module(new Filter())f1.io.x <> io.x f1.io.y <> f2.io.x f2.io.y <> io.y }????????其中<>批量連接同級模塊之間的相反接口或父/子模塊之間的相同接口。批量連接將相同名稱的端口彼此連接。在所有連接完成后,Chisel警告用戶端口是否只有一個到它們的連接。
4、接口視圖
????????考慮一個由控制路徑和數據路徑子模塊以及主機和內存接口組成的簡單 CPU,如圖。
????????在這個 CPU 中,我們可以看到控制路徑和數據路徑各自只連接到一部分指令和數據內存接口。Chisel 允許用戶通過部分實現接口來做到這一點。 用戶首先定義完整的 ROM 和 Mem 接口如下:
class RomIo extends Bundle { val isVal = Bool(INPUT)val raddr = UInt(INPUT, 32) val rdata = UInt(OUTPUT, 32) } class RamIo extends RomIo { val isWr = Bool(INPUT)val wdata = UInt(INPUT, 32) }????????現在控制邏輯可以根據這些接口構建接口:
class CpathIo extends Bundle { val imem = RomIo().flip() val dmem = RamIo().flip() }????????而且控制和數據通路模塊可以通過部分地分配來給這個接口來構建,如下所示:
class Cpath extends Module { val io = new CpathIo();...io.imem.isVal := ...;io.dmem.isVal := ...; io.dmem.isWr := ...; ... } class Dpath extends Module { val io = new DpathIo(); ...io.imem.raddr := ...; io.dmem.raddr := ...; io.dmem.wdata := ...;... }????????我們現在可以使用批量連接來連接CPU,就像使用其他bundle一樣:
class Cpu extends Module {val io = new CpuIo()val c = Module(new CtlPath()) val d = Module(new DatPath()) c.io.ctl <> d.io.ctlc.io.dat <> d.io.dat c.io.imem <> io.imemd.io.imem <> io.imemc.io.dmem <> io.dmemd.io.dmem <> io.dmemd.io.host <> io.host }總結
以上是生活随笔為你收集整理的chisel快速入门(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: chisel快速入门(一)
- 下一篇: gcc 4.9编译