到工程實(shí)踐徹底講清)
寫(xiě)了這么多年RTL帶過(guò)不少剛?cè)腴T(mén)的同學(xué)我發(fā)現(xiàn)關(guān)于 wire 和 reg 這個(gè)問(wèn)題的出現(xiàn)頻率高得有點(diǎn)離譜。幾乎每隔一陣子就有人在群里問(wèn)一遍“這個(gè)信號(hào)到底該定義成 reg 還是 wire”面試的時(shí)候十個(gè)人里也有七八個(gè)會(huì)在這上面被繞進(jìn)去。今天干脆把這件事徹底講透從硬件本質(zhì)、仿真模型、綜合行為到真實(shí)工程里的選擇習(xí)慣一次性掰開(kāi)揉碎。先給一句話(huà)結(jié)論wire 描述的是“物理連線(xiàn)”或“組合邏輯的連續(xù)驅(qū)動(dòng)”reg 描述的是“always 塊內(nèi)部的過(guò)程賦值目標(biāo)”兩者在仿真模型里的角色完全不同但綜合出來(lái)的硬件未必是你想的那樣。至于“什么時(shí)候用哪個(gè)”說(shuō)到底就一條最實(shí)用的判斷準(zhǔn)則你先想清楚要用 assign 連續(xù)賦值還是要用 always 過(guò)程賦值前者聲明成 wire后者聲明成 reg。1. 先搞懂本質(zhì)wire 是“線(xiàn)”reg 是“變量”很多初學(xué)者在學(xué) Verilog 的時(shí)候腦子里的第一反應(yīng)是“wire 就是導(dǎo)線(xiàn)reg 就是寄存器?!边@個(gè)說(shuō)法在八成的情況下能對(duì)付作業(yè)但一旦接觸到真實(shí)工程會(huì)翻車(chē)翻得很難看。原因在于這個(gè)說(shuō)法混淆了仿真層面的數(shù)據(jù)類(lèi)型和綜合后的硬件邏輯這兩件事不是一回事。1.1 從硬件視角看 wire它就是一根物理連線(xiàn)wire 這個(gè)詞的本意就是“導(dǎo)線(xiàn)”在語(yǔ)言標(biāo)準(zhǔn)里屬于net type網(wǎng)絡(luò)類(lèi)型。網(wǎng)絡(luò)類(lèi)型描述的是一個(gè)信號(hào)值由某個(gè)驅(qū)動(dòng)源持續(xù)驅(qū)動(dòng)的連接關(guān)系它本身不存儲(chǔ)任何狀態(tài)。你把一個(gè) wire 想成一塊電路板上的一截銅線(xiàn)就行了當(dāng)前時(shí)刻這根線(xiàn)上是什么電平完全取決于現(xiàn)在誰(shuí)在驅(qū)動(dòng)它。在 RTL 設(shè)計(jì)里wire 最典型的出現(xiàn)位置有幾個(gè)模塊與模塊之間的端口連接。assign 連續(xù)賦值的左側(cè)。頂層文件里把子模塊輸出接到另一個(gè)子模塊輸入的中間信號(hào)。你寫(xiě)assign led cnt[3];的時(shí)候左側(cè)的 led 就必須是 wire。這句話(huà)翻譯成硬件行為是把計(jì)數(shù)器第 3 位這根線(xiàn)的電平直接連到 led 引腳上。它不是某一時(shí)刻賦值一次而是每一時(shí)刻都被持續(xù)驅(qū)動(dòng)這也是連續(xù)賦值這四個(gè)字的含義。還有個(gè)容易忽略的小細(xì)節(jié)wire 這類(lèi)網(wǎng)絡(luò)類(lèi)型的默認(rèn)值是高阻態(tài) z。在仿真里如果一個(gè) wire 沒(méi)有被任何驅(qū)動(dòng)源驅(qū)動(dòng)它讀出來(lái)是 z 而不是 0。這個(gè)特性在某些總線(xiàn)模擬場(chǎng)景下很關(guān)鍵。1.2 從硬件視角看 reg它是一種“會(huì)記住”的存儲(chǔ)行為reg 在語(yǔ)言標(biāo)準(zhǔn)里屬于variable type變量類(lèi)型。它描述的是一個(gè)過(guò)程賦值procedural assignment的目標(biāo)也就是說(shuō)它的值是在 always、initial、task、function 這些過(guò)程塊內(nèi)部通過(guò)賦值語(yǔ)句在某一時(shí)刻被更新然后保持這個(gè)值直到下一次被更新。你寫(xiě)always (posedge clk) begin cnt cnt 1b1; end這里的 cnt 必須聲明成 reg。如果把電路行為翻譯成自然語(yǔ)言時(shí)鐘上升沿來(lái)的時(shí)候把 cnt 更新成新值其他時(shí)間它保持不動(dòng)。這種保持的行為天然適合用變量類(lèi)型去建模。所以從仿真模型上看reg 更像程序語(yǔ)言里的變量它是內(nèi)存里的一個(gè)值可以被賦值可以保持可以在被賦值之前讀取雖然這樣會(huì)讀到未知態(tài) x。1.3 最容易踩的坑reg 并不等于硬件寄存器這是整個(gè)問(wèn)題里最經(jīng)典的認(rèn)知偏差。很多人以為 reg 類(lèi)型綜合出來(lái)就一定是觸發(fā)器wire 綜合出來(lái)就一定是連線(xiàn)這個(gè)想法錯(cuò)得非常徹底。reg 只是一個(gè)仿真語(yǔ)義層面的變量類(lèi)型。一個(gè) reg 信號(hào)綜合出來(lái)到底變成什么硬件完全取決于你對(duì)它做了什么。always (posedge clk)塊里賦值的 reg綜合成 D 觸發(fā)器。always (*)塊里賦值的 reg如果賦值完整綜合成純組合邏輯。always (*)塊里賦值不完整綜合成鎖存器latch。在initial塊里使用的 reg只在仿真里存在綜合時(shí)往往被直接優(yōu)化掉。我用一個(gè)更直白的例子幫你建立感覺(jué)。下面這段代碼reg result; always (*) begin if (sel) result a b; else result a - b; endresult 聲明的是 reg但它綜合出來(lái)是純組合邏輯實(shí)現(xiàn)的是一個(gè)二選一的加減法器沒(méi)有時(shí)鐘、沒(méi)有觸發(fā)器。因?yàn)?always 塊是 level sensitive電平敏感的且在 if 和 else 兩個(gè)分支里都賦值了信號(hào)每個(gè)時(shí)刻都是被驅(qū)動(dòng)的所以不需要存儲(chǔ)。反過(guò)來(lái)看如果你寫(xiě)wire out; assign out din_reg;out 是 wire但它接的實(shí)際連過(guò)去的是寄存器輸出。綜合后硬件上out 可能就是某個(gè)觸發(fā)器輸出引腳連出來(lái)的一小段金屬線(xiàn)。所以別再背reg 就是寄存器這種簡(jiǎn)化口訣了。reg 描述的是賦值方式不是硬件結(jié)構(gòu)。2. 什么時(shí)候用 wire核心場(chǎng)景拆解講完本質(zhì)現(xiàn)在落實(shí)到具體工程里到底怎么選。一個(gè)信號(hào)聲明成 wire就意味著這個(gè)信號(hào)會(huì)出現(xiàn)在 assign 語(yǔ)句左側(cè)、模塊實(shí)例化的輸出端口、或者連續(xù)賦值產(chǎn)生的表達(dá)式中。2.1 結(jié)構(gòu)建模與端口連接wire 是模塊間通信的管線(xiàn)數(shù)字系統(tǒng)設(shè)計(jì)到一定規(guī)模幾乎逃不開(kāi)自頂向下、模塊化這套方法。當(dāng)你例化一個(gè)子模塊的時(shí)候子模塊的輸出端口在父模塊這一側(cè)連接它的一定是 wire。來(lái)看一個(gè)典型的例子// 子模塊并轉(zhuǎn)串發(fā)送器 module uart_tx( input wire clk, input wire rst_n, input wire tx_start, input wire [7:0] tx_data, output reg txd ); // ... endmodule // 頂層模塊例化 uart_tx module top( input wire clk, input wire rst_n, input wire send_en, input wire [7:0] data_in, output wire uart_txd ); wire tx_start_w; assign tx_start_w send_en; uart_tx u_uart_tx ( .clk (clk), .rst_n (rst_n), .tx_start (tx_start_w), .tx_data (data_in), .txd (uart_txd) ); endmodule父模塊聲明的連接線(xiàn) tx_start_w 和輸出端口 uart_txd 都是 wire。原因很簡(jiǎn)單這些信號(hào)是“別人驅(qū)動(dòng)的”當(dāng)前模塊只需要讀取它的值或者把它繼續(xù)連出去不需要在某個(gè)時(shí)刻捕獲并保持它。從硬件意義上說(shuō)它們就是一根根真實(shí)存在的連線(xiàn)。2.2 組合邏輯的數(shù)據(jù)流建模assign 連續(xù)賦值的黃金搭配assign 語(yǔ)句是 Verilog 里描述組合邏輯最基本的手段而任何 assign 語(yǔ)句的左側(cè)目標(biāo)信號(hào)都必須是 wire 類(lèi)型。這算是語(yǔ)法上的硬性規(guī)定。組合邏輯什么時(shí)候優(yōu)先用 assign而不是用 always我的習(xí)慣是當(dāng)邏輯可以用一條或幾條布爾表達(dá)式直接描述清楚時(shí)用 assign 更簡(jiǎn)潔、更不容易出錯(cuò)。比如wire flag_valid (byte_cnt 4d8) (state BUSY); wire [7:0] next_data (fifo_empty) ? 8h00 : fifo_dout;這兩個(gè)信號(hào)的硬件含義就是一堆與門(mén)、或門(mén)、比較器和多路選擇器的組合沒(méi)有任何“時(shí)序狀態(tài)”在里面。我在做滑動(dòng)窗口濾波或者累加求和這類(lèi)純粹的組合運(yùn)算時(shí)也偏好用 assign 把中間結(jié)果鋪開(kāi)。好處是一眼能看出數(shù)據(jù)的流向不用去翻 always 塊的端口列表仿真排錯(cuò)的時(shí)候非常直觀。2.3 多驅(qū)動(dòng)與總線(xiàn)場(chǎng)景wire 才能掛多個(gè)驅(qū)動(dòng)源wire 類(lèi)型還有個(gè) reg 不具備的特性它允許多個(gè)驅(qū)動(dòng)源共同驅(qū)動(dòng)雖然這在行為級(jí)建模時(shí)要非常小心。真實(shí)工程里典型的場(chǎng)景是三態(tài)門(mén)和雙向 IO。// 雙向數(shù)據(jù)線(xiàn)由讀寫(xiě)控制信號(hào)決定驅(qū)動(dòng)方向 inout wire sda; assign sda (wr_en) ? tx_data_reg : 1bz; assign sda (rd_en) ? 1bz : tx_data_reg;這種用高阻態(tài) z 釋放總線(xiàn)的寫(xiě)法本質(zhì)是在說(shuō)“我不驅(qū)動(dòng)這根線(xiàn)的時(shí)候把它讓給別人?!边@種能力只有網(wǎng)絡(luò)類(lèi)型wire具備。如果你把一個(gè) inout 引腳內(nèi)部信號(hào)聲明成 reg綜合工具會(huì)直接報(bào)錯(cuò)因?yàn)?reg 是有狀態(tài)保持的變量無(wú)法實(shí)現(xiàn)掛起釋放。需要再補(bǔ)一句多個(gè) assign 同時(shí)驅(qū)動(dòng)同一個(gè) wire 時(shí)如果兩個(gè)驅(qū)動(dòng)源同時(shí)有效并且電平相反仿真結(jié)果取決于驅(qū)動(dòng)強(qiáng)度硬件上則可能燒毀。所以設(shè)計(jì)內(nèi)最好避免邏輯上的多驅(qū)動(dòng)沖突總線(xiàn)場(chǎng)景請(qǐng)用三態(tài)控制或仲裁邏輯。2.4 一個(gè)小結(jié)看到這些信號(hào)就用 wire綜合多年工程經(jīng)驗(yàn)下面這些信號(hào)我基本上是閉眼聲明成 wire模塊端口里聲明的 input 信號(hào)input 天然只能是 wire。模塊例化時(shí)連接輸出的外部連線(xiàn)。assign 語(yǔ)句左側(cè)的組合邏輯目標(biāo)。三態(tài)總線(xiàn)、雙向 IO 的內(nèi)部信號(hào)。由子模塊輸出經(jīng)過(guò)簡(jiǎn)單邏輯立即作為另一個(gè)子模塊輸入的中間信號(hào)。判斷的底層邏輯是**如果一個(gè)信號(hào)不需要在 always 塊里被賦值那么它就是 wire。**反過(guò)來(lái)只要它出現(xiàn)在任何 always 塊左側(cè)就必須是 reg。3. 什么時(shí)候用 reg核心場(chǎng)景拆解3.1 always 塊中的過(guò)程賦值必須用 reg 的硬性規(guī)則Verilog 語(yǔ)法規(guī)定過(guò)程賦值語(yǔ)句在 always、initial、task、function 內(nèi)賦值的目標(biāo)變量必須聲明為變量類(lèi)型也就是 reg、integer 等。所以判斷一個(gè)信號(hào)該不該用 reg最快的方法是看它有沒(méi)有出現(xiàn)在 always 塊里。這里我多說(shuō)一點(diǎn)“為什么”。因?yàn)?reg 變量類(lèi)型的仿真語(yǔ)義是“存儲(chǔ)并保持”在某一個(gè)仿真時(shí)間點(diǎn)被賦值后直到下一次被賦值之前它的值都保持不變。而 always 塊天然就是事件觸發(fā)、執(zhí)行一次、然后等待下一個(gè)事件這種執(zhí)行模型兩者正好天然匹配。wire 則是持續(xù)驅(qū)動(dòng)的把它放進(jìn) always 里賦值語(yǔ)義上就矛盾了。3.2 時(shí)序邏輯的建模計(jì)數(shù)器、分頻器與狀態(tài)機(jī)時(shí)序邏輯是 reg 最重要的主戰(zhàn)場(chǎng)。凡是要在每個(gè)時(shí)鐘沿更新值的信號(hào)必須聲明為 reg。計(jì)數(shù)器是最直觀的例子reg [7:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) cnt 8d0; else if (cnt_en) cnt cnt 1b1; else cnt cnt; endcnt 是 reg。它在時(shí)鐘上升沿更新時(shí)鐘沒(méi)來(lái)的時(shí)候就保持舊值綜合后就是 8 個(gè) D 觸發(fā)器級(jí)聯(lián)成的計(jì)數(shù)器。類(lèi)似地做 UART 多字節(jié)收發(fā)、SPI 時(shí)序控制、出租車(chē)計(jì)價(jià)器的金額累加這些從熱詞看都是大家經(jīng)常練手的工程案例核心都繞不開(kāi)狀態(tài)機(jī)和計(jì)數(shù)器而這些狀態(tài)寄存器、計(jì)數(shù)值寄存器統(tǒng)統(tǒng)都是 reg??梢赃@么說(shuō)只要設(shè)計(jì)里有時(shí)鐘就一定有 reg。再提一句三段式狀態(tài)機(jī)。三段式寫(xiě)法里第一段是狀態(tài)轉(zhuǎn)移用 always 時(shí)序邏輯狀態(tài)寄存器 current_state 聲明成 reg次態(tài)判斷 next_state 可以在第二段組合邏輯里聲明成 reg第三段輸出邏輯用 assign 接 wire 或直接在 always 里輸出 reg 都可以。整體上只要邏輯在 always 塊內(nèi)就繞不開(kāi) reg。3.3 組合邏輯的 always 建模reg 處理組合邏輯的典型場(chǎng)景這是很多人的第二個(gè)知識(shí)盲區(qū)。我面試的時(shí)候特別喜歡問(wèn)一個(gè)問(wèn)題always (*) 塊里賦值的信號(hào)聲明成 reg綜合出來(lái)是什么答案經(jīng)常讓人意外純組合邏輯。當(dāng)一個(gè) always 塊是電平敏感(*) 或 (a or b)、而且所有分支都對(duì)目標(biāo)變量完成了賦值時(shí)綜合工具會(huì)把它優(yōu)化成組合邏輯。沒(méi)有任何存儲(chǔ)的必要的條件下reg 這個(gè)名字就只是個(gè)仿真變量名而已。舉個(gè)例子一個(gè)很常見(jiàn)的組合邏輯建模方式reg [7:0] alu_out; always (*) begin case (op_code) 2b00: alu_out a b; 2b01: alu_out a - b; 2b10: alu_out a b; default: alu_out a ^ b; endcase endalu_out 聲明成 reg但它綜合出來(lái)是一個(gè) 8 位多路選擇器加運(yùn)算組合邏輯沒(méi)有時(shí)鐘也沒(méi)有寄存器。這里用 always 而不用 assign 的原因是 if/else 和 case 分支邏輯寫(xiě)起來(lái)更靠近 C 語(yǔ)言思維多人協(xié)作時(shí)更好讀。不過(guò)我要提醒一句組合邏輯用 always 寫(xiě)最大的風(fēng)險(xiǎn)是分支覆蓋不完全導(dǎo)致推斷出鎖存器。這個(gè)坑后面專(zhuān)門(mén)講。3.4 又一個(gè)陷阱always 塊里別亂用 wire見(jiàn)過(guò)不少新手把模塊輸出端口明明聲明成 output reg然后在頂層連接時(shí)卻發(fā)現(xiàn)綜合報(bào)錯(cuò)。還有更常見(jiàn)的毛病// 錯(cuò)誤示范 reg [7:0] data_bus; wire foo; always (*) begin foo data_bus[0]; // 編譯報(bào)錯(cuò)foo 是 wire不能在過(guò)程賦值里賦值 end這個(gè)錯(cuò)誤信息大概是說(shuō) foo is not a reg or integer解決方式就是要么把 foo 改成 reg要么把賦值語(yǔ)句改成 assign。語(yǔ)法層面的原因在上面已經(jīng)說(shuō)過(guò)回歸到思維層面就是第一反應(yīng)要先分清**我是要寫(xiě) always 還是 assign。**定了語(yǔ)句形式數(shù)據(jù)類(lèi)型就跟著定了。4. 仿真與綜合wire 和 reg 在“兩個(gè)世界”的行為差異如果你只想寫(xiě)作業(yè)到第 3 節(jié)就夠了。但如果你想做真實(shí)的 FPGA 開(kāi)發(fā)、ASIC 設(shè)計(jì)或者準(zhǔn)備秋招面試這里才是真正拉開(kāi)差距的部分仿真模型的規(guī)則和綜合工具的行為在很多細(xì)節(jié)上并不一致。4.1 仿真模型wire 是事件驅(qū)動(dòng)的信號(hào)reg 是程序變量在仿真器內(nèi)部wire 和 reg 的數(shù)據(jù)更新機(jī)制是不同的。wire 的更新遵循 net 類(lèi)型的事件驅(qū)動(dòng)規(guī)則當(dāng)它的任何一個(gè)驅(qū)動(dòng)源assign 右側(cè)表達(dá)式、門(mén)原語(yǔ)輸出、模塊輸出端口發(fā)生變化時(shí)仿真器會(huì)重新計(jì)算 wire 的值事件傳遞到下一級(jí)。這種模型對(duì)應(yīng)的是硬件里電平信號(hào)的連續(xù)傳播。reg 的更新則像普通編程語(yǔ)言的變量賦值在 always 塊被觸發(fā)時(shí)執(zhí)行塊內(nèi)語(yǔ)句把計(jì)算結(jié)果賦給 reg。在非阻塞賦值時(shí)這種更新還要分成“計(jì)算 RHS”和“更新 LHS”兩個(gè)階段這也是仿真器能模擬出真實(shí)觸發(fā)器采樣行為的底層機(jī)制。理解這個(gè)差異對(duì)你寫(xiě) testbench 很有幫助。比如在 initial 塊里你是可以給 reg 賦值的reg clk_tb; initial begin clk_tb 0; forever #5 clk_tb ~clk_tb; end但你不能在 initial 里給 wire 賦值除非你把它聲明成 wire 并用 force / assign 之類(lèi)的方式來(lái)驅(qū)動(dòng)。很多人在 testbench 里把輸入信號(hào)聲明成 wire 然后直接賦值一編譯就報(bào)錯(cuò)核心原因就是沒(méi)分清楚這種仿真層面的差異。4.2 綜合映射硬件上兩者到底變成了什么綜合工具做的是把 RTL 語(yǔ)言描述的行為變成結(jié)構(gòu)。它關(guān)注的是你的賦值邏輯、分支結(jié)構(gòu)、時(shí)序約束而不是信號(hào)名字里帶的是 wire 還是 reg。所以綜合之后的 netlist 里基本上全是 wire——因?yàn)榫W(wǎng)表本身就是用連線(xiàn)連接各個(gè)單元的描述。原來(lái)的 reg 信號(hào)如果最后映射成觸發(fā)器網(wǎng)表里是一個(gè) DFF 單元的 Q 輸出如果映射成組合邏輯那就是一堆邏輯門(mén)的輸出如果被優(yōu)化掉那網(wǎng)表里甚至找不到這個(gè)名字??偨Y(jié)一張表幫你把 仿真語(yǔ)言類(lèi)型 和 綜合硬件結(jié)果 這兩套思維徹底分開(kāi)聲明類(lèi)型賦值方式綜合結(jié)果通常情況wireassign 連續(xù)賦值組合邏輯連線(xiàn)與門(mén)、或門(mén)、多路器等regalways 時(shí)序塊賦值D 觸發(fā)器、寄存器堆、計(jì)數(shù)器regalways 組合塊完整賦值純組合邏輯無(wú)存儲(chǔ)單元regalways 組合塊不完整賦值鎖存器latch需盡量避免reginitial 塊賦值不可綜合僅仿真使用這張表建議直接背下來(lái)。面試?yán)镆悄馨堰@個(gè)完整講清楚基本上就把一半的人甩在后面了。4.3 一道經(jīng)典面試題為什么同樣的 always有時(shí)是觸發(fā)器有時(shí)是鎖存器說(shuō)到鎖存器這是組合邏輯 always 建模最經(jīng)典的坑也是面試官最?lèi)?ài)用來(lái)篩選候選人的點(diǎn)。reg q; always (*) begin if (enable) q d; end這個(gè)代碼里q 聲明成 regalways 是電平敏感的。但問(wèn)題是當(dāng) enable 為 0 的時(shí)候q 沒(méi)有被賦值。從仿真語(yǔ)義說(shuō)reg 會(huì)保持原有值從硬件映射說(shuō)要在 enable 為 0 時(shí)把值記住就必須引入一個(gè)電平敏感的存儲(chǔ)單元——這就是鎖存器。很多數(shù)字設(shè)計(jì)規(guī)范尤其是 ASIC 風(fēng)格是強(qiáng)烈不建議在組合邏輯中不必要地產(chǎn)生鎖存器的因?yàn)殒i存器對(duì)時(shí)序收斂和 DFT可測(cè)性設(shè)計(jì)都不友好。正確做法是給齊所有分支reg q; always (*) begin if (enable) q d; else q 0; // 或者 q q; 但要明白區(qū)別 end加上 else 分支后q 在每個(gè)輸入組合下都有確定的賦值路徑綜合工具就能放心地把它變成多路選擇器了。同理case 語(yǔ)句里的 default 分支也是絕對(duì)不能少的。5. 實(shí)戰(zhàn)案例在一個(gè) UART 發(fā)送模塊里wire 和 reg 怎么各司其職前面講完理論現(xiàn)在用一個(gè)完整的工程實(shí)例串一遍。就拿大家非常熟悉的 UART 發(fā)送模塊來(lái)當(dāng)例子這種模塊在握手、協(xié)議解析、多字節(jié)收發(fā)項(xiàng)目里幾乎都會(huì)用到。5.1 模塊端口劃分輸入輸出到底用 wire 還是 reg寫(xiě)任何模塊第一步就是定義端口。這里有個(gè)實(shí)用經(jīng)驗(yàn)**input 端口一律是 wireoutput 端口默認(rèn)也是 wire只有當(dāng)你確定要在 always 塊里給它賦值時(shí)才把它額外聲明成 output reg。**在 Verilog-2001 時(shí)端口類(lèi)型和數(shù)據(jù)類(lèi)型是可以寫(xiě)在一行的。module uart_tx ( input wire clk, // 系統(tǒng)時(shí)鐘 input wire rst_n, // 低電平復(fù)位 input wire tx_start, // 發(fā)送使能 input wire [7:0] tx_data, // 待發(fā)送數(shù)據(jù) output reg txd, // 串行輸出需要在 always 中賦值 output wire tx_busy // 忙標(biāo)志用 assign 給出 );txd 是串行輸出信號(hào)根據(jù)發(fā)送狀態(tài)機(jī)在每個(gè)時(shí)鐘沿移位更新所以必須是 output reg。tx_busy 是忙標(biāo)志它只是把內(nèi)部一個(gè)狀態(tài)寄存器的值或組合邏輯結(jié)果簡(jiǎn)單輸出用 assign 就能搞定因此是 output wire。其實(shí)這里也可以讓 tx_busy 直接等于內(nèi)部狀態(tài)assign tx_busy (state ! IDLE);這句話(huà)的含義是只要當(dāng)前不在 IDLE 狀態(tài)就對(duì)外拉高忙信號(hào)。它是典型的數(shù)據(jù)流組合邏輯用 wire 非常合適。5.2 內(nèi)部信號(hào)規(guī)劃哪些該 wire、哪些該 reg進(jìn)入模塊內(nèi)部規(guī)劃起來(lái)就更有章法了。我的做法是先畫(huà)信號(hào)列表分成“狀態(tài)類(lèi)信號(hào)”和“連接類(lèi)信號(hào)”兩大陣營(yíng)狀態(tài)類(lèi)信號(hào)必須用 reg因?yàn)樗鼈冃枰跁r(shí)鐘沿被一次次更新state當(dāng)前發(fā)送狀態(tài)三段式狀態(tài)機(jī)的第一段必用非阻塞賦值。clk_div_cnt波特率分頻計(jì)數(shù)器。bit_cnt當(dāng)前發(fā)送的第幾個(gè) bit。txd_reg實(shí)際串行發(fā)送的移位寄存器。連接類(lèi)信號(hào)用 wire它們是組合邏輯的中間產(chǎn)物tx_start_pulsetx_start 的上升沿脈沖。tick波特率時(shí)鐘脈沖。byte_done一字節(jié)發(fā)送完成的標(biāo)志。舉個(gè)例子波特率時(shí)鐘脈沖 tick 是這樣產(chǎn)生的wire tick (clk_div_cnt DIV_CNT_MAX);tick 是指定分頻計(jì)數(shù)達(dá)到最大值時(shí)的譯碼結(jié)果每一時(shí)刻都被連續(xù)驅(qū)動(dòng)決定當(dāng)然是 wire。接著狀態(tài)機(jī)可以用 tick 來(lái)驅(qū)動(dòng)數(shù)據(jù)移位這就在多個(gè) always 塊之間建立了清晰的信號(hào)通路。5.3 用 wire 把子模塊拼起來(lái)FIFO 與狀態(tài)機(jī)的連接如果這個(gè) UART 發(fā)送模塊再往上做成一個(gè)多字節(jié)收發(fā)系統(tǒng)往往前面還要接一個(gè)發(fā)送 FIFO。FIFO 的讀數(shù)據(jù)端口、空標(biāo)志、讀請(qǐng)求信號(hào)在頂層就是用 wire 互連的。wire [7:0] fifo_dout; wire fifo_empty; wire fifo_rd_en; uart_fifo u_fifo ( .clk (clk), .rst_n (rst_n), .wr_en (byte_recv_done), .din (rx_byte), .rd_en (fifo_rd_en), .dout (fifo_dout), .empty (fifo_empty) ); uart_tx u_uart_tx ( .clk (clk), .rst_n (rst_n), .tx_start (~fifo_empty), .tx_data (fifo_dout), .txd (txd) );這里幾乎全是 wire。原因很直接在頂層這個(gè)抽象層次你看到的不是“某段邏輯的賦值行為”而是“各個(gè)黑盒模塊之間的信號(hào)連線(xiàn)”。頂層的職責(zé)是連好線(xiàn)而不是描述內(nèi)部邏輯所以 wire 就是頂層的主旋律。5.4 綜合后的實(shí)際硬件結(jié)果把上述模塊做完綜合你大概率會(huì)看到txd 對(duì)應(yīng)的區(qū)域推斷出一組 D 觸發(fā)器移位寄存器和狀態(tài)寄存器。tx_busy、tick、byte_done 這些 wire 在網(wǎng)表里對(duì)應(yīng)的是組合邏輯門(mén)比較器、與門(mén)。FIFO 內(nèi)部的 RAM 和讀寫(xiě)地址計(jì)數(shù)器分別映射成存儲(chǔ)陣列和寄存器堆。硬件不會(huì)管你代碼里寫(xiě)的是 wire 還是 reg它只關(guān)心“你有沒(méi)有在時(shí)鐘沿保存狀態(tài)”和“你的組合邏輯是否完備”。所以我的建議很明確**寫(xiě)代碼的時(shí)候把心思放在賦值場(chǎng)景上而不是數(shù)據(jù)類(lèi)型上。**assign 就用 wirealways 就用 reg這樣寫(xiě)出來(lái)的 RTL綜合工具最省心后端也不會(huì)半夜給你打電話(huà)。6. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄最后分享一些我在實(shí)際工程中反復(fù)遇到過(guò)的報(bào)錯(cuò)和排查經(jīng)驗(yàn)。這些問(wèn)題基本是初學(xué)者的高頻雷區(qū)一個(gè)個(gè)過(guò)一遍。6.1 典型報(bào)錯(cuò)reg 被連續(xù)賦值了錯(cuò)誤信息類(lèi)似Error (10170): Verilog HDL syntax error ... object xx is not a wire或者更常見(jiàn)的是reg cannot be driven by continuous assignment這個(gè)場(chǎng)景一般是你聲明的信號(hào)是 reg但又不小心寫(xiě)了assign xx ...。修復(fù)方案很簡(jiǎn)單把自己?jiǎn)栆槐檫@個(gè)信號(hào)是組合邏輯的多路選擇結(jié)果還是時(shí)序狀態(tài)如果是前者看看能不能用 wire 表達(dá)如果實(shí)在要用 always 寫(xiě)就把賦值語(yǔ)句刪掉、搬到 always 塊里。6.2 典型報(bào)錯(cuò)wire 被過(guò)程賦值了錯(cuò)誤信息類(lèi)似Error (10028): Cant resolve xx to a reg or integer這個(gè)場(chǎng)景最常見(jiàn)的發(fā)生在寫(xiě) testbench 時(shí)wire clk_tb; initial clk_tb 0; // 報(bào)錯(cuò)因?yàn)?clk_tb 是 wire不能在 initial 里被賦值。改成 reg clk_tb; 就對(duì)了?;蛘吣阏娴南胗?wire就得用 assign 或 force 來(lái)驅(qū)動(dòng)。測(cè)試激勵(lì)這塊的解決思路是激勵(lì)信號(hào)用 reg因?yàn)樗窃谀硞€(gè)時(shí)間點(diǎn)改變的變量被測(cè)模塊輸出的檢測(cè)信號(hào)用 wire因?yàn)槟阒恍枰O(jiān)控它的變化。6.3 組合邏輯的鎖存器警告怎么處理綜合工具Quartus、Vivado、Design Compiler一般都會(huì)報(bào) latch 警告比如 Vivado 里是[Synth 8-327] inferring latch for variable xx處理思路按優(yōu)先級(jí)排序檢查是否每個(gè)分支都給目標(biāo)變量賦值了。if 要有 elsecase 要有 default。檢查變量是不是在多個(gè) always 塊里被賦值。同一個(gè) reg 不能被兩個(gè) always 驅(qū)動(dòng)這是語(yǔ)法錯(cuò)誤。檢查是不是把組合邏輯塊誤寫(xiě)成了電平敏感且分支不全的結(jié)構(gòu)。如果確實(shí)需要鎖存器比如總線(xiàn)保持之類(lèi)也要寫(xiě)成顯式的 latch 風(fēng)格并在注釋里說(shuō)明理由別讓工具“偷偷”推斷。再補(bǔ)充個(gè)我自己常用的檢查技巧寫(xiě)組合邏輯 always 時(shí)上電仿真把所有使能信號(hào)分別跑一遍 0 和 1觀察輸出是否有異常保持。如果某個(gè)輸入組合下輸出一直不變化十有八九就是分支缺了。6.4 wire 和 reg 實(shí)戰(zhàn)避坑清單這幾條是我自己在多輪項(xiàng)目里總結(jié)出來(lái)的“肌肉記憶”每一條背后都有真實(shí)踩坑經(jīng)歷分享給大家頂層連接信號(hào)全部用 wire。頂層不是寫(xiě)邏輯的地方它是畫(huà)圖紙的地方。讓子模塊自己處理 reg 的內(nèi)部邏輯頂層只負(fù)責(zé)把線(xiàn)連對(duì)。output 默認(rèn)為 wire要往 always 里塞才聲明成 output reg。反過(guò)來(lái)input 永遠(yuǎn)不可能聲明成 reg否則編譯直接罷工。testbench 里給 DUT 輸入的激勵(lì)信號(hào)用 reg檢測(cè) DUT 輸出的信號(hào)用 wire。這個(gè)規(guī)則百試不爽。不要出現(xiàn) “assign always 同時(shí)驅(qū)動(dòng)一個(gè)信號(hào)” 的代碼。無(wú)論是仿真還是綜合多驅(qū)動(dòng)都是災(zāi)難。組合邏輯 always 里一定要寫(xiě) else 和 default。哪怕你覺(jué)得條件全覆蓋了也要寫(xiě)。一方面防工具推斷 latch另一方面也是給別人讀代碼時(shí)留個(gè)明確的邏輯閉環(huán)。如果發(fā)現(xiàn)仿真波形里某個(gè) reg 輸出是 x優(yōu)先查復(fù)位邏輯和初始化賦值。reg 在仿真里默認(rèn)值是 x這跟 wire 的 z 不一樣很多新手第一次跑仿真看到滿(mǎn)屏 x 會(huì)慌其實(shí)這只是仿真模型的初始值。不要把 reg 和寄存器劃等號(hào)。我在帶人評(píng)審代碼的時(shí)候最怕聽(tīng)到有人介紹說(shuō)“這個(gè)信號(hào)是 reg所以它是寄存器”。正確說(shuō)法應(yīng)該是“這個(gè)信號(hào)在 always 時(shí)序塊里賦值所以綜合后是寄存器”。再補(bǔ)一句關(guān)于綜合和仿真不一致的問(wèn)題。有些新手會(huì)問(wèn)為什么我仿真的時(shí)候 reg 輸出能保持下載到板子上就不對(duì)了這通常不是 reg 本身的問(wèn)題而是代碼里復(fù)用組合邏輯 / 時(shí)序邏輯時(shí)不小心把應(yīng)該搭的時(shí)序關(guān)系寫(xiě)錯(cuò)了或者復(fù)位方式、時(shí)鐘域跨域出了問(wèn)題。建議排查的時(shí)候先用在線(xiàn)邏輯分析儀抓內(nèi)部寄存器的信號(hào)看狀態(tài)機(jī)是否按要求走再看對(duì)應(yīng) wire 的組合邏輯輸出是否符合預(yù)期。把兩者分開(kāi)看問(wèn)題就縮小一半了。聊到這里關(guān)于 wire 和 reg 的內(nèi)容基本就完整了。說(shuō)句心里話(huà)這個(gè)知識(shí)點(diǎn)本身并不難難的是很多人一開(kāi)始就陷入“wire 就是線(xiàn)、reg 就是寄存器”這種簡(jiǎn)化模型里后面越學(xué)越別扭。我個(gè)人的建議是**每次寫(xiě)代碼前先默念一遍assign 對(duì)應(yīng) wirealways 對(duì)應(yīng) reg然后放手去寫(xiě)。**寫(xiě)多了形成肌肉記憶你自然就理解為什么這種語(yǔ)法規(guī)則是合理的了。