FPGA程序的調(diào)試,尤其是大型程序,一直都是耗時(shí)耗力的工作。首先是因?yàn)镠DL語言沿空間并發(fā)擴(kuò)展的特性不同于一般基于按時(shí)間線性敘事的計(jì)算機(jī)語言,各個(gè)元素之間的邏輯關(guān)系更加緊密,不易理解和思考,debug手段也不多。同時(shí),F(xiàn)PGA程序的綜合布線過程都比較緩慢,經(jīng)常一個(gè)中等大小的程序會(huì)需要等待數(shù)十分鐘才能得到輸出文件,大型設(shè)計(jì)run隔夜是常見的事情,隨便一個(gè)筆誤都會(huì)帶來很高的沉沒成本。每次說到這里,都不由得回憶起多年以前某次出差做實(shí)驗(yàn),邂逅過一位研究所大哥玩笑說最愛領(lǐng)導(dǎo)指派FPGA任務(wù),因?yàn)橹灰聪戮C合按鈕,便能安心歇息半個(gè)午后。那時(shí)候,天地初開,摸魚這個(gè)詞還沒有發(fā)明,也沒有想到即便是當(dāng)下最強(qiáng)最fancy的CPU也沒能拯救這種等待。
言歸正傳。在邏輯構(gòu)思基本正確,程序編寫大致規(guī)范的前提下,大多數(shù)的問題,都只是寫代碼時(shí)的一些小疏忽。通過實(shí)施功能仿真,其實(shí)可以把多數(shù)邏輯問題都找出來。但是,實(shí)際工作中,經(jīng)常沒有時(shí)間或耐心做仿真,而是直接編碼后下硬件調(diào)試了。此時(shí),通過仔細(xì)閱讀綜合器給出的警告信息,就可以在最耗時(shí)的布局布線之前把各種小問題找出來并修正,從而有效地提高開發(fā)效率。在調(diào)試階段,如果遇到了難以理解的現(xiàn)象,回過頭去分析警告信息也是一種有效的辦法。在本文中,我們以vivado自帶綜合器為例、以verilog為編程語言,看看如何理解和利用警告信息排除代碼中的小bug。
(以上圖片來自網(wǎng)絡(luò):?My?digital?designing?diary?by?Mandapati) 為了方便敘述,我們先建一個(gè)樣例工程,包含模塊top和adder。
為了避免像很多經(jīng)典教科書中的?“a=(b++)+(++c)”?那樣被指為“例子代碼不注重軟件工程”,特意把這個(gè)程序?qū)懙谋M量貼近工程實(shí)際一些(除了沒有注釋)。首先它具有特定的功能,對兩路并發(fā)輸入的數(shù)據(jù)流先相加再累加。其次,數(shù)據(jù)端口定義采用時(shí)下常用的AXI-Stream風(fēng)格。
模塊top是頂層模塊,其源碼Top.v如下圖。該模塊具備時(shí)鐘信號clk和異步復(fù)位信號rst。輸入數(shù)據(jù)端口din_tdata[31:0],配套流控握手信號din_tvalid和din_tready。在模塊內(nèi)部,輸入數(shù)據(jù)首先被劈成2個(gè)16bit數(shù)據(jù),代表要相加的兩路數(shù)據(jù)流,并注入加法器模塊adder。加法器的輸出數(shù)據(jù)是adder_out[15:0]。第31行開始的always語句則完成對加法結(jié)果的累加操作。累加結(jié)果從端口acc_tdata輸出,配套數(shù)據(jù)有效標(biāo)志acc_tvalid。
再看看加法器的源碼Adder.v,如下圖。兩路數(shù)據(jù)din1和din2,共享同一組流控握手信號din_tvalid和din_tready。加法結(jié)果從端口dout輸出。在模塊內(nèi)部,第14行的always語句負(fù)責(zé)完成所有邏輯處理。核心語句在第21行:當(dāng)輸入數(shù)據(jù)有效而且后級設(shè)備準(zhǔn)備好時(shí),進(jìn)行一次加法操作。寫到這里作者也是十分感慨,三十多行代碼就是為了伺候第23行的這個(gè)“+”號。
至此,例子程序搭建好了。程序比較小,按下Run Synthesis按鈕等待半分鐘左右就能看到綜合結(jié)果。下面我們一起一邊修改代碼一邊看看常見的警告信息都有哪些。 1.?常數(shù)驅(qū)動(dòng) 警告之所以是警告,是因?yàn)榫C合器分不清它是否真的有問題。很多警告是可以忽略的。例如上述例程,看上去很完美,但綜合完成后仍然會(huì)得到如下的警告信息:
這里,綜合器提醒我們,top模塊的端口din_tready被驅(qū)動(dòng)為常數(shù)1,可能是一個(gè)潛在的問題。在top的邏輯中,din_tready的功能是提醒模塊外部的前級數(shù)據(jù)源“是否準(zhǔn)備好接收數(shù)據(jù)”。被驅(qū)動(dòng)為1則表示“永遠(yuǎn)都準(zhǔn)備好接收數(shù)據(jù)”。用順藤摸瓜的方法分析代碼,可以看到din_tready是被加法器實(shí)例adder1驅(qū)動(dòng)的,而在加法器內(nèi)部(adder.v的第31行),該信號來自于加法器輸出端的tready。
再看回top.v的第17行,果然加法器的tready被置為常數(shù)1,根源在此。具體到這個(gè)例子,此邏輯本身沒有大問題,因?yàn)閠op模塊的輸入端口只有數(shù)據(jù)有效信號acc_tvalid,并沒有配套的tready,說明該端口是強(qiáng)行輸出的,并不考慮后級沒有準(zhǔn)備好的情況。 所以,在此例中,這個(gè)常數(shù)驅(qū)動(dòng)警告“基本上”可以被忽略。但是,問題禁不住細(xì)琢磨,比如此例程并沒有考慮在復(fù)位信號rst有效期間din_tready應(yīng)當(dāng)拉低來禁止數(shù)據(jù)輸入,不太周全。
進(jìn)一步的,我們還能聯(lián)想到,在真實(shí)的系統(tǒng)中,這個(gè)接口定義是否存在隱患? 即,后級模塊是否真的可以無條件接收數(shù)據(jù)? 這些都是警告信息帶來的福利。 2.?無用信號 下面開始折騰代碼。首先把top.v line27處的端口連接去掉,只留下空括號。這樣,adder1實(shí)例的dout_tvalid輸出就懸空了。
綜合之后,得到如下圖的警告。綜合器告知adder1中的dout_tvalid所對應(yīng)的寄存器資源被移除。
顯然,這是由于在top中斷開了信號連接,于是dout_tvalid信號在adder內(nèi)部雖然被賦值,但是在整個(gè)邏輯中沒有被任何其它地方使用,也沒有輸出,于是綜合器在給出警告后就將其刪除了。從這個(gè)例子可以看到,如果一個(gè)信號被自動(dòng)移除了,應(yīng)當(dāng)首先應(yīng)當(dāng)考慮它是否沒有在別處被用到。不過,在下一個(gè)例子里馬上可以看到這并不是信號被優(yōu)化掉的唯一的原因。 ? 3.?無源信號 首先,先把源碼復(fù)原,然后試著把Top.v第17行注釋掉: ?
綜合之后,得到如下警告信息:
第一條信息直奔主題:adder_tready信號沒有被驅(qū)動(dòng)。這顯然是前述修改帶來的,源程序里缺乏對adder_tready的賦值操作。第二條以及隨后更多的信息則會(huì)讓人困惑:adder1/dout[15:0]被從邏輯中移除了。這些信號明明都有被后續(xù)的累加操作用到,為什么還會(huì)被優(yōu)化掉? 通過分析adder中的邏輯關(guān)系可以知道,這仍然是因?yàn)閍dder_tready沒有被驅(qū)動(dòng),于是綜合器認(rèn)為凡是依賴于adder_tready的后續(xù)信號都已經(jīng)沒有存在的意義,于是一股腦全拿掉了。這就提示我們,如果發(fā)現(xiàn)有大片的邏輯消失了,不但要往后尋找看是否缺乏最終的輸出,而且要往前尋找看是否存在不確定或者無驅(qū)動(dòng)的輸入。
當(dāng)然,對于各種異常情況,不同的綜合器以及同一個(gè)綜合器的不同的參數(shù),會(huì)表現(xiàn)很大的差異。比如作者也見過有的綜合器會(huì)直接給無驅(qū)動(dòng)信號賦值為0,這種好心好意的掩飾反而導(dǎo)致有時(shí)候問題很難查找。 4.?多重驅(qū)動(dòng) 在top.v的第16行,把原先的adder_d2改成adder_d1,形成一個(gè)典型的筆誤。本來是要分別給信號addr_d1和addr_d2賦值,一不小心變成了給信號adder_d1賦值兩次。
? 對于上述情況,綜合器明確指出了有信號被multi-driven了,如下圖。
但是,它指出的對象卻并不是addr_d2,而是我數(shù)據(jù)源din_tdata。這是因?yàn)椋诰C合器看來,din_tdata[15:0]和din_tdata[31:16]都連接到了addr_d2[15:0],其實(shí)就是din_tdata[15:0]與din_tdata[31:16]直接點(diǎn)對點(diǎn)短接了,所以它們本身就面臨多驅(qū)動(dòng)問題,addr_d2此時(shí)只是一個(gè)“別名”而已。好比你出門忘記戴帽子,而綜合器告訴你:請注意冷風(fēng)已經(jīng)接觸頭皮。這種機(jī)器式的敘述風(fēng)格,有時(shí)候著實(shí)會(huì)帶來一些小麻煩,不過習(xí)慣了就好了。 5.?復(fù)位缺失 top.v第31行的always語句采用了異步復(fù)位。復(fù)位信號rst與時(shí)鐘clk一起作為語句觸發(fā)條件,在語句內(nèi)部先按判斷rst是否為真來選擇執(zhí)行復(fù)位操作。這是verilog典型的異步復(fù)位語句寫法。這里,嘗試把第36行注釋掉,如下圖。
? 綜合器會(huì)給出如下的警告。
字面背后的意思可以理解為:語句中存在復(fù)位語段,但是并沒有對acc_tvalid信號做復(fù)位操作,導(dǎo)致邏輯缺失,或者說綜合器分不清應(yīng)該set還是reset,于是擔(dān)心綜合結(jié)果會(huì)與仿真結(jié)果不符。 這類警告可以幫助我們找出因?yàn)橥藢憦?fù)位而初始值不確定的寄存器,這往往是很多重大bug的來源。如果存在某些寄存器的確不需要復(fù)位操作,則應(yīng)當(dāng)單獨(dú)寫一個(gè)只有clk做觸發(fā)的always句段,就能避免上述警告。 那么,此時(shí)綜合結(jié)果有沒有生成期望的邏輯呢? 打開綜合輸出的邏輯圖(如下),可以看到acc_tvalid由一個(gè)沒有復(fù)位和置位的D觸發(fā)器驅(qū)動(dòng),符合修改后的語句原意。然而,我們?nèi)匀粦?yīng)該設(shè)法避免這類不太規(guī)范的寫法。尤其是對于新手,務(wù)必要了解語言與真實(shí)邏輯的映射關(guān)系,謹(jǐn)記verilog就那么幾種常見的語句套路。新奇的寫法,可能導(dǎo)致完全不可預(yù)期的綜合結(jié)果。
6.?位寬失配
修改adder模塊的端口聲明,如下圖,把din1和din2的位寬從16bit分別改為17和15。
如下圖,綜合器會(huì)明確指出在top.v中實(shí)現(xiàn)adder模塊時(shí)遇到了端口寬度不匹配的問題。
需要指出的是,至少對于vivado + verilog,位寬失配警告只對模塊端口連接有用。如果是兩個(gè)位寬不同的信號賦值,綜合器將會(huì)直接做高位截?cái)嗷蛘吒呶谎a(bǔ)零,而不給任何警告,除非截?cái)嗖僮饔|發(fā)了無用信號警告。所以,不論是wire類型還是reg類型,賦值時(shí)的位寬對齊問題,完全需要編程者自行關(guān)注。例如下面的語段,16bit的src被賦值給16bit的dst1和15bit的dst2,顯然賦值給dst2時(shí)最高位會(huì)丟失,但是此時(shí)綜合器不會(huì)給出警告,這是verilog語言本身的特點(diǎn),改不了。而且,因?yàn)閐st1用到了src的所有bit位,所以在綜合器看來src里也不存在無用的bit位,也不會(huì)觸發(fā)無用信號警告。最終結(jié)果就是,可能你就是筆誤給dst2少寫了1位,但這個(gè)錯(cuò)誤要到后期調(diào)試時(shí)通過各種故障才被發(fā)現(xiàn)。這里并不是綜合器犯懶,而是verilog語言本身就是這樣設(shè)計(jì)的,相比之下VHDL就要嚴(yán)格的多,不同位寬信號互相賦值不給警告,而是直接報(bào)錯(cuò)。
7.?不應(yīng)有的鎖存器 把top.v中對adder_d1和adder_d2的直接賦值語句改為always句段,如下圖:
上述修改將產(chǎn)生如下圖的警告信息:adder_d1和adder_d2變量引入了鎖存器(latch)。
分析上述語句,可以看到din_tvalid的確相當(dāng)于鎖存使能信號,當(dāng)它為1時(shí)din_tdata可穿透到adder_d1和adder_d2。如果打開schematic觀察綜合結(jié)果,會(huì)發(fā)現(xiàn)此處使用了一個(gè)名為LDCE的鎖存器元件。 我們知道,F(xiàn)PGA公認(rèn)的基礎(chǔ)邏輯資源是查找表和D觸發(fā)器,是否具備鎖存器要看具體的FPGA型號和綜合器的算法,所以在HDL語言中書寫鎖存器風(fēng)格的語句并不是好辦法,也是這條警告存在的意義。 至此,我們簡單介紹了在綜合階段常見的一些警告問題。當(dāng)然,在后續(xù)的implementation操作中,還會(huì)有很多更難理解的提示和警告出現(xiàn),它們更加地與具體器件的內(nèi)部結(jié)構(gòu)和元素有關(guān)。到了這些階段,更加需要去關(guān)注XDC文件、關(guān)注物理和時(shí)序約束,而不是HDL語言本身。
審核編輯:黃飛
評論