懂得“?數(shù)據(jù)結(jié)構(gòu)與算法?” 寫出?高效的代碼?,懂得“?設(shè)計模式?”寫出?高質(zhì)量的代碼?。
何為高質(zhì)量的代碼?
下面這些詞匯是我們常用的形容好代碼的詞匯:
靈活性(flexibility)、可擴(kuò)展性(extensibility)、可維護(hù)性(maintainability)、可讀性(readability)、可理解性(understandability)、易修改性(changeability)、可復(fù)用(reusability)、可測試性(testability)、模塊化(modularity)、高內(nèi)聚低耦合(high cohesion loose coupling)、高效(high effciency)、高性能(high performance)、安全性(security)、兼容性(compatibility)、易用性(usability)、整潔(clean)、清晰(clarity)、簡單(simple)、直接(straightforward)、少即是多(less code is more)、文檔詳盡(well-documented)、分層清晰(well-layered)、正確性(correctness、bug free)、健壯性(robustness)、魯棒性(robustness)、可用性(reliability)、可伸縮性(scalability)、穩(wěn)定性(stability)、優(yōu)雅(elegant)、好(good)、
如何寫出高質(zhì)量代碼?
面向?qū)ο?a target="_blank">編程因?yàn)槠渚哂胸S富的特性(封裝、抽象、繼承、多態(tài)),可以實(shí)現(xiàn)很多復(fù)雜的設(shè)計思路,是很多設(shè)計原則、設(shè)計模式等編碼實(shí)現(xiàn)的基礎(chǔ)。
設(shè)計原則是指導(dǎo)我們代碼設(shè)計的一些經(jīng)驗(yàn)總結(jié),對于某些場景下,是否應(yīng)該應(yīng)用某種設(shè)計模式,具有指導(dǎo)意義。
設(shè)計模式是針對軟件開發(fā)中經(jīng)常遇到的一些設(shè)計問題,總結(jié)出來的一套解決方案或者設(shè)計思路。應(yīng)用設(shè)計模式的主要目的是提高代碼的可擴(kuò)展性。
編程規(guī)范主要解決的是代碼的可讀性問題。
重構(gòu)作為保持代碼質(zhì)量不下降的有效手段。
面向?qū)ο?/strong>
含義
面向?qū)ο缶幊痰挠⑽目s寫是 OOP,全稱是 Object Oriented Programming。對應(yīng)地,面向?qū)ο缶幊陶Z言的英文縮寫是 OOPL,全稱是 Object Oriented Programming Language。
面向?qū)ο缶幊讨杏袃蓚€非常重要、非?;A(chǔ)的概念,那就是類(class)和對象(object)。這兩個概念最早出現(xiàn)在 1960 年,在 Simula 這種編程語言中第一次使用。而面向?qū)ο缶幊踢@個概念第一次被使用是在 Smalltalk 這種編程語言中。Smalltalk 被認(rèn)為是第一個真正意義上的面向?qū)ο缶幊陶Z言。
Systemverilog作為面向?qū)ο蟮恼Z言,相比C++, 更"像“ Java. Java語言并不直接運(yùn)行在真實(shí)機(jī)器上,而是有一個虛擬機(jī)(即?Java Virtual Machine ,JVM?)來承載其運(yùn)行,JVM使用C++編寫的,而C++是C的超集。
UML
UML(Unified Model Language),統(tǒng)一建模語言。用畫圖表達(dá)面向?qū)ο蠡蛟O(shè)計模式的設(shè)計思路。對于UML的使用,純軟件人員之間仍存在一些爭議。
示例:
封裝(Encapsulation)
將屬性和方法封裝到到類中,但類中的屬性并不需要全部暴露出去,可以通過加上訪問權(quán)限控制這一語法機(jī)制,限制對類屬性的訪問,修改。
Java中的權(quán)限修飾符:
private?修飾的函數(shù)或者成員變量,只能在類內(nèi)部使用。
protected?修飾的函數(shù)或者成員變量,可以在類及其子類內(nèi)使用。
public?修飾的函數(shù)或者成員變量,可以被任意訪問。
SV中的訪問權(quán)限控制qualifiers限定符:
local?:表示的成員或方法只對該類的對象可見,子類以及類外不可見。
protected?:表示的成員或方法對該類以及子類可見,對類外不可見。
除此之外,我們還常見?const?,?static修飾變量。
const:分為兩種:全局性、instance性的 (const 在run-time階段,而 localparam需要在elaboration-time
被賦值)
全局性const:在聲明時即賦值,之后不可修改;
instace性const:只使用const進(jìn)行聲明,賦值發(fā)生在new()中
const修飾的變量,不允許被修改,否則編譯器報錯。
為什么const修飾的變量不可以被修改呢??其實(shí)無論SV,C++還是C語言,各種語言的語法不同,但是最終都是通過編譯器編譯后,程序運(yùn)行在系統(tǒng)內(nèi)存里,如果const修飾的變量被編譯器分配到了一個.rodata只讀的內(nèi)存段,那么就可以很好的解釋為什么不可以被修改了。同理,static對應(yīng)靜態(tài)分配的地址(?存儲在全局?jǐn)?shù)據(jù)區(qū)?),該段地址相對automatic屬性的地址段,不會被釋放內(nèi)存,自然可以在整個仿真過程一直存在。
為了地址對齊,SV仿真器會把byte放在32bit的地址空間。
對于task/function調(diào)用,則對應(yīng)?棧空間?,如果使用的是input,output類型的參數(shù),開始調(diào)用時“input” 變量 copy到棧中,結(jié)束調(diào)用時“ouput” 變量再pop出棧。所以在task/function中修改變量,修改的結(jié)果對其他調(diào)用函數(shù)不可見。如果使用SV中的?ref?,一方面對于數(shù)據(jù)量較大的數(shù)組,不用copy到??臻g,可以獲得更佳的性能,同時修改變量的結(jié)果對外可見。
對于上述諸多的變量修飾符,從編譯存儲的角度分析,可以加深理解。C語言相對其他語言O(shè)OP語言,更接近硬件,可以通過objdump –dS a.out 反匯編查看各個變量的
main 函數(shù)位于.text段,GLOBAL修飾屬于External Linkage
‘A’ 位于 .rodata段
‘Hello World” 也位于.rodata段 hexdump –C a.out可以查看。
程序加載運(yùn)行時, .rodata段和.text段通常合并到一個Segment中,操作系統(tǒng)將這個Segment的頁面只讀保護(hù)起來,防止意外的改寫。
.data段中有 ‘a(chǎn)’, ‘b’, ‘d’ , 其中a是GLOBAL全局變量,b被static修飾,為LOCAL,不會被鏈接器處理。d被static修飾,并位于main函數(shù)中,靜態(tài)分配。
.bss段緊挨著.data段,被0填充,不占內(nèi)存。所以c位于.bss段,未賦值初始化為0. .data 和.bss在加載時合并到一個Segment中,這個Segment是可讀可寫的。
‘e’ 位于函數(shù)內(nèi)部,放在棧上存儲, 省略auto修飾
參考:Linux C編程一站式學(xué)習(xí) 宋勁杉 *19.3 *變量的存儲布局
抽象(Abstraction)
OOP中抽象這一特性本身就很“抽象”,如果單單從語法上看,SV在《IEEE Standard for SystemVerilog 1800-2012》才加入了像Java語言那樣支持抽象(面向接口編程)的語法。關(guān)鍵詞是 interface class, implements。
當(dāng)一個class implements 一個 interface class時,必須override interface class 中的純虛(pure virtual)方法,這也很符合 implements這個單詞本身的含義。
下面看一個列子(?from?IEEE Standard for SystemVerilog 1800-2012 8.26?):
兩個interface class,PutImp, GetImp 分別包含純虛方法put, get的原型。class Fifo 和 class Stack 使用關(guān)鍵詞 implementes來實(shí)現(xiàn)這兩個interface class中的純虛方法。
class Fifo and class Stack share common behaviors without sharing a common implementation.
classs Fifo 和 class Stack 都有 put, get的操作,但是實(shí)現(xiàn)的具體方式不同(?FIFO:先進(jìn)后出,Stack:先進(jìn)先出?)。這就體現(xiàn)了“抽象”的含義,interface僅僅暴露出的是common behavirs,調(diào)用人員不需要關(guān)心具體的實(shí)現(xiàn)。
實(shí)際上,如果上升一個思考層面的話,抽象及其前面講到的封裝都是人類處理復(fù)雜性的有效手段。在面對復(fù)雜系統(tǒng)的時候,人腦能承受的信息復(fù)雜程度是有限的,所以我們必須忽略掉一些非關(guān)鍵性的實(shí)現(xiàn)細(xì)節(jié)。而抽象作為一種只關(guān)注功能點(diǎn)不關(guān)注實(shí)現(xiàn)的設(shè)計思路,正好幫我們的大腦過濾掉許多非必要的信息。
可能因?yàn)閕ntreface class這一語法加入SV較晚,并且EDA工具支持有一定延遲, 在UVM源碼中,并沒有使用 interface class這一語法。但抽象僅僅是一個非常通用的設(shè)計思想, 比如一個上報錯誤的function, 命名為report_error()就比命名為report_size_mismatch_error()抽象,具體的錯誤類型,不必體現(xiàn)在函數(shù)命名上。(?2016 DVCon US : SystemVerilog Interface Classes - More Useful Than You Thought?涉及 interface classes 在實(shí)際項(xiàng)目中的使用)
在SV沒有加入接口類(intreface class)之前,也有抽象類(virtual class)可以代替抽象的特性。
抽象類不能直接例化,一個由抽象類擴(kuò)展而來的類只有在所有虛方法都有實(shí)體的時候才能被例化。抽象類中可以定義非純虛方法,但是接口類不行。
接口類的一些特性,抽象類并不具備。比如一個類可以實(shí)現(xiàn)多個接口類,并同時繼承某一個類。比如下面這個用例。
extends 和 implements還是有本質(zhì)區(qū)別的,extends繼承,是?is-a的關(guān)系,而implements更像是has-a的關(guān)系。所以SV中加入interface class,使其更接近高級語言所具備的特性。
抽象類和接口類如何選擇呢?抽象類是is-a的關(guān)系,解決代碼復(fù)用問題,接口類是has-a的關(guān)系,更側(cè)重于解耦,隔離接口和具體的實(shí)現(xiàn),提高代碼的擴(kuò)展性。
基于接口而非實(shí)現(xiàn)編程(Program to an interface, not an implementation),將接口(interface)和實(shí)現(xiàn)(implements)相分離,封裝不穩(wěn)定的實(shí)現(xiàn),暴露穩(wěn)定的接口。
上游系統(tǒng)面向接口而非實(shí)現(xiàn)編程,不依賴不穩(wěn)定的實(shí)現(xiàn)細(xì)節(jié),這樣當(dāng)實(shí)現(xiàn)發(fā)生變化的時候,上游系統(tǒng)的代碼基本上不需要做改動,以此來降低耦合性,提高擴(kuò)展性。
UVM驗(yàn)證平臺,已規(guī)定好了hierarchy結(jié)構(gòu)和各component功能,驗(yàn)證工程師只需根據(jù)實(shí)際業(yè)務(wù)“填充”具體內(nèi)容,屬于硬件?驗(yàn)證?,而純軟件要實(shí)現(xiàn)多交互的復(fù)雜業(yè)務(wù)側(cè)重?設(shè)計?,所以一般工作中沒有需求用到抽象類和接口類。對于沒有使用UVM方法學(xué),自己寫Systemverilog搭建的驗(yàn)證平臺, 接口類,抽象類,純虛方法可以建立具有統(tǒng)一觀感的測試平臺,這就使任何一個工程師都可以讀懂你的代碼并且快速理解其結(jié)構(gòu)。
繼承(Inheritance)
繼承是用來表示類之間的 is-a 關(guān)系,比如狗是一種哺乳動物??梢酝ㄟ^extends 關(guān)鍵字來實(shí)現(xiàn)繼承(可以通過繼承+參數(shù)化的類來實(shí)現(xiàn)多繼承的效果,有點(diǎn)非常規(guī)操作,參考SystemVerilog: Reusable Class Features and Safe Initialization of Static Variables。另外interface class也可以實(shí)現(xiàn)多繼承),C++和Python既支持單重繼承,也支持多重繼承。
在構(gòu)造用例時,一般會創(chuàng)建一個base_class作為父類,子類extends繼承父類的特性,使用super關(guān)鍵字指示編譯器來顯式的引用父類中定義的數(shù)據(jù)成員和方法。
SV語法規(guī)定父類的new()函數(shù)(?構(gòu)造函數(shù)?),子類必須顯示調(diào)用,寫出super.new()。如果父類new()函數(shù)有參數(shù),子類也需要傳入?yún)?shù)。不管子類是否重載new()函數(shù),都要顯式調(diào)用父類的構(gòu)造函數(shù)。
在實(shí)際驗(yàn)證工作中,一般不會出現(xiàn)下述問題,基本繼承2次就足以覆蓋大部分需求了,但是純軟件編程可能會因?yàn)闃I(yè)務(wù)復(fù)雜,導(dǎo)致繼承過度,采用 *“多用組合少用繼承” *是一個規(guī)避辦法。
繼承的概念很好理解,也很容易使用。不過,過度使用繼承,繼承層次過深過復(fù)雜,就會導(dǎo)致代碼可讀性、可維護(hù)性變差。為了了解一個類的功能,我們不僅需要查看這個類的代碼,還需要按照繼承關(guān)系一層一層地往上查看“父類、父類的父類……”的代碼。還有,子類和父類高度耦合,修改父類的代碼,會直接影響到子類。
在SV使用中,我們也會遇到合成和繼承的選擇問題,合成使用了“有”(has-a)的關(guān)系,繼承使用了“是”(is-a)的關(guān)系。
SV構(gòu)建測試平臺并非標(biāo)準(zhǔn)的軟件開發(fā)項(xiàng)目?,除了繼承與合成之外,根據(jù)現(xiàn)實(shí)的場景使用,把所用變量集成在一個類中,通過條件約束達(dá)到目的。Constraint-driven?的策略更有利于我們的驗(yàn)證工作。
如下示例:(?Systemverilog驗(yàn)證 測試平臺編寫指南 8.4?)
多態(tài)(Polymorphism)
多態(tài)是指,子類可以替換父類,父類句柄可以指向子類的實(shí)例?。(?子類句柄不可以指向父類的實(shí)例,因?yàn)樽宇愓{(diào)用的方法,父類實(shí)例中或許并不存在?)
多態(tài)的例子這里就不再列舉了,建議學(xué)習(xí)《The UVM Primer》,這是一本很好學(xué)習(xí)OOP的書籍,足以應(yīng)對工作中的絕大部分內(nèi)容。
當(dāng)父類句柄指向子類的實(shí)例時,通過父類句柄調(diào)用方法,如果方法使用virtual修飾,則會動態(tài)的調(diào)用子類的方法(?雖然是父類句柄,但是實(shí)例是子類,實(shí)際調(diào)用子類?override?(重寫or覆蓋)的方法?)。如果方法沒有使用virtual修飾,則是靜態(tài)的根據(jù)句柄調(diào)用方法(?動態(tài):實(shí)例 靜態(tài):句柄?)。
父類的task/function已經(jīng)用virtual修飾,子類沒有必要在加上virtual了。
所以多態(tài)的實(shí)現(xiàn)要依賴虛函數(shù)virtual,總結(jié)就是“繼承加方法重寫?”。
SV語法目前還不支持overload(重載),override指的是重寫,也可以理解成覆蓋,一般不做詳細(xì)區(qū)分。
對于多態(tài)的底層實(shí)現(xiàn)及virtual, function override,$cast()轉(zhuǎn)化的底層原理,需要深入研究編程語言的編譯原理。檢索并沒有介紹Systemverilog的相關(guān)文章,可以通過學(xué)習(xí)C++或者Jave擴(kuò)充學(xué)習(xí),檢索內(nèi)存模型或者對象模型獲取相關(guān)知識。
除了上述“繼承加方法重寫”實(shí)現(xiàn)多態(tài)的方法,Systemverilog也可以采用之前介紹的 interface class實(shí)現(xiàn)多態(tài)。還有一種是利用 duck-typing 語法,SV并不支持,動態(tài)語言Python才支持。
實(shí)例如下:
?
class Logger: def record(self): print(“I write a log into file.”) class DB: def record(self): print(“I insert data into db. ”) def test(recorder): recorder.record() def demo(): logger = Logger() db = DB() test(logger) test(db)
?
設(shè)計模式之美 從這段代碼中,我們發(fā)現(xiàn),duck-typing 實(shí)現(xiàn)多態(tài)的方式非常靈活。Logger 和 DB 兩個類沒有任何關(guān)系,既不是繼承關(guān)系,也不是接口和實(shí)現(xiàn)的關(guān)系,但是只要它們都有定義了 record() 方法,就可以被傳遞到 test() 方法中,在實(shí)際運(yùn)行的時候,執(zhí)行對應(yīng)的 record() 方法。
設(shè)計原則
純軟件設(shè)計中的設(shè)計原則,對于IC的驗(yàn)證和設(shè)計工作也有指導(dǎo)意義,我們?nèi)粘9ぷ髦械囊恍傲?xí)慣”,可能就是在踐行某一個設(shè)計原則。依次列舉如下:
單一職責(zé)原則
一個類只負(fù)責(zé)一個功能,避免設(shè)計大而全的類,避免不相關(guān)的功能耦合,提高內(nèi)聚性。也可以延申到驗(yàn)證的測試用例,每個用例應(yīng)該對應(yīng)一個場景或者功能。
開閉原則
對擴(kuò)展開放,對修改關(guān)閉。對于新加的功能,應(yīng)在已有代碼基礎(chǔ)上擴(kuò)展,而非修改已有代碼。所以在最初代碼編寫時,就應(yīng)該充分考慮可擴(kuò)展性,當(dāng)然也不是完全杜絕修改,要把握“粗細(xì)粒度”。對于已經(jīng)充分驗(yàn)證的rtl模塊,側(cè)重在原來基礎(chǔ)上新加功能,而不是“大修”原來的模塊,容易引入bug, 相應(yīng)的測試用例也可以做到最小修改。
里式替換原則
子類對象可以替換程序中出現(xiàn)的父類對象,并保證原來程序的邏輯行為的正確性。這一原則跟多態(tài)比較像,側(cè)重于繼承關(guān)系中子類該如何設(shè)計。
接口隔離原則
接口的調(diào)用者不應(yīng)該強(qiáng)迫依賴ta不需要的接口。如果B模塊內(nèi)包含B-1,B-2兩個模塊,A模塊的正常工作依賴于B-1模塊的初始配置,C模塊的正常工作依賴于B-2模塊的初始配置。B模塊的驗(yàn)證人員可以將B模塊的初始配置流程寫到一個函數(shù)中,這個函數(shù)供A,C模塊的驗(yàn)證人員調(diào)用,這個函數(shù)就像API接口一樣,調(diào)用者只負(fù)責(zé)調(diào)用,不用關(guān)心具體實(shí)現(xiàn)。如果B模塊的函數(shù)同時包含B-1,B-2的初始配置,A模塊的驗(yàn)證人員調(diào)用,雖然不會影響功能驗(yàn)證,但是B-2模塊與A模塊并無聯(lián)系,恰當(dāng)?shù)淖龇☉?yīng)該是將B-1,B-2模塊的初始配置隔離開來,供使用者按需調(diào)用。
依賴倒置原則
程序要依賴于抽象接口,不要依賴于具體實(shí)現(xiàn)。簡單的說就是要求對抽象進(jìn)行編程,不要對實(shí)現(xiàn)進(jìn)行編程,這樣就降低了客戶與實(shí)現(xiàn)模塊間的耦合。高層次的模塊不應(yīng)該依賴于低層次的模塊,他們都應(yīng)該依賴于抽象。和依賴接口編程的含義相近。
KISS、YANGI ,DRY原則
KISS: Keep It Stupid Simple 不要使用同事不懂的技術(shù);不要重復(fù)造輪子,使用現(xiàn)有的方法;不要過度優(yōu)化;
YANGI: You Ain't Gonna Need It 不要過度設(shè)計
DRT: Don't repeat yourself 減少重復(fù)的代碼。對于重復(fù)的代碼,思考是否可以通過封裝到函數(shù)中,通過傳參的方式實(shí)現(xiàn)。
迪米特法則
Talk only to your immediate friends and not to strangers,只與你的直接朋友交談,不跟“陌生人”說話。
如果兩個模塊實(shí)體無須直接通信,那么就不應(yīng)當(dāng)發(fā)生直接的相互調(diào)用,可以通過第三方轉(zhuǎn)發(fā)該調(diào)用。其目的是降低類之間的耦合度,提高模塊的相對獨(dú)立性?!案邇?nèi)聚,松耦合”
規(guī)范與重構(gòu)
代碼風(fēng)格與規(guī)范?:Easier UVM Coding Guidelines
代碼測試?:SV單元測試方法SVUnit SVUnit Download SVUnit blog
SVUnit采用了一種特別的方式來生成task。一般task負(fù)責(zé)時序相關(guān)的驅(qū)動和采樣,開發(fā)者根據(jù)設(shè)計文檔中的時序圖編寫task代碼,但是代碼的準(zhǔn)確性有待驗(yàn)證。SVUnit從另一個思路出發(fā),直接通過時序圖,來生成對應(yīng)task。這樣便保證了task中時序的準(zhǔn)確性,畢竟時序圖要是都錯了,那只能通過review發(fā)現(xiàn)了。
SVUnit將時序圖轉(zhuǎn)化成task的方法,是通過編寫wavdrom可識別的?json格式?(?有固定格式,但是很容易上手,支持網(wǎng)頁,linux, window平臺。UserGuide?). 然后調(diào)用SVUnit中的腳本wavedromSVUnit.py解析json文件,生成時序圖對應(yīng)的代碼。SVUnit對json文件做了額外描述,可以參照 test/wavedrom_0/1下面的json文件深入理解。
示例:
json描述:
?
{ "name": "read", "signal": [ {"name": "clk", "wave": "p|...|." , "node": ".ab...d"}, {"name": "psel", "wave": "0.1...0" }, {"name": "penable", "wave": "0..1..0" }, {"name": "paddr", "wave": "x.=...x" , "data": ["addr"] }, {"name": "pready", "wave": "0....10" , "input": "True", "node": "......c"}, {"name": "prdata", "wave": "x....=x" , "output": "True", "data": ["data"] } ], "input": [ {"name": "addr", "type": "logic [7:0]"} ], "output": [ {"name": "data", "type": "logic [31:0]"} ], "edge": ["a~>b 8,12", "c->d pready==1"], config: { hscale: 3 } }
?
waverom生成的時序圖:
自動生成的task:
這種方式的限制就是僅適用于直接測試用例。
絕大部分驗(yàn)證人員開發(fā)UVC,都是一遍debug DUT, 一遍調(diào)試驗(yàn)證平臺,并不會專門使用SVUnit對UVC進(jìn)行驗(yàn)證。但是對于sv庫的開發(fā),使用SVUnit是一個很好的選擇。
不過仍建議在monitor, driver開發(fā)初期,同時RTL還沒有ready的情況下,使用SVUnit將波形轉(zhuǎn)化成直接的時序激勵,做一些直接用例的測試,及早發(fā)現(xiàn)問題。如果設(shè)計文檔中的波形也是使用wavedrom繪制的,那么對于驗(yàn)證人員的工作又省了一步,可以直接拿設(shè)計人員波形的json文件生成用例。
重構(gòu)?:隨著項(xiàng)目的推進(jìn),迭代,原來的代碼也會慢慢變“差”,重構(gòu)可能是一條"挽回“路徑。在項(xiàng)目初期,盡可能地劃分好驗(yàn)證平臺的組件,目錄,文件調(diào)用,宏定義,腳本等,重構(gòu)的同時也在引入不確定性。
審核編輯:湯梓紅
評論