與無符號(hào)數(shù)轉(zhuǎn)換:從補(bǔ)碼原理到FPGA實(shí)戰(zhàn)避坑指南)
1. 項(xiàng)目概述從一次仿真Bug說起最近在調(diào)一個(gè)圖像處理相關(guān)的FPGA模塊里面涉及到一些濾波系數(shù)的運(yùn)算。仿真的時(shí)候數(shù)據(jù)流看起來一切正常但輸出圖像的邊緣部分總會(huì)出現(xiàn)一些奇怪的、非預(yù)期的亮斑或暗斑。排查了很久從算法到時(shí)序都查了一遍最后定位到問題出在一行看起來很簡(jiǎn)單的賦值語句上result coeff * pixel_data;。問題在于coeff被我定義成了signed有符號(hào)數(shù)而pixel_data是攝像頭輸入的unsigned無符號(hào)數(shù)。在Verilog中當(dāng)有符號(hào)數(shù)和無符號(hào)數(shù)進(jìn)行混合運(yùn)算時(shí)如果你沒有顯式地告訴工具你的意圖它可能會(huì)按照一套默認(rèn)的、有時(shí)反直覺的規(guī)則進(jìn)行處理導(dǎo)致計(jì)算結(jié)果與你的預(yù)期大相徑庭。這個(gè)“有符號(hào)數(shù)與無符號(hào)數(shù)的相互轉(zhuǎn)化”問題看似基礎(chǔ)卻是我踩過最深、也最隱蔽的坑之一。它不僅僅是語法問題更關(guān)乎你對(duì)數(shù)字電路底層數(shù)據(jù)表示的理解。無論是做信號(hào)處理、通信算法還是控制邏輯只要涉及數(shù)學(xué)運(yùn)算這都是一個(gè)繞不開的核心知識(shí)點(diǎn)。今天我就結(jié)合自己踩坑和填坑的經(jīng)驗(yàn)把Verilog里有符號(hào)數(shù)和無符號(hào)數(shù)轉(zhuǎn)換的那些門道徹底講清楚讓你在寫代碼時(shí)心里有底調(diào)試時(shí)有的放矢。2. 核心概念補(bǔ)碼、位寬與工具視角在深入轉(zhuǎn)換之前我們必須統(tǒng)一認(rèn)知在Verilog以及絕大多數(shù)數(shù)字系統(tǒng)中數(shù)據(jù)本身并沒有“符號(hào)”屬性它只是一串二進(jìn)制位bits。所謂的“有符號(hào)數(shù)”或“無符號(hào)數(shù)”是我們程序員和綜合/仿真工具附加在這串二進(jìn)制位上的“解讀方式”或“處理規(guī)則”。2.1 二進(jìn)制世界的兩種解讀假設(shè)我們有一個(gè)4位的寄存器reg [3:0] a 4‘b1111;。無符號(hào)數(shù)解讀我們把它當(dāng)作一個(gè)純二進(jìn)制數(shù)。4‘b1111就是 1518 14 12 11。有符號(hào)數(shù)解讀我們采用二進(jìn)制補(bǔ)碼規(guī)則。最高位第3位是符號(hào)位1表示負(fù)剩余位是數(shù)值位按補(bǔ)碼規(guī)則解讀。4‘b1111在補(bǔ)碼中表示 -1計(jì)算方式先取反得0000再加1得0001所以是 -1。關(guān)鍵在于這串1111的物理存在沒有變變的是我們和工具看待它的“眼鏡”。2.2 補(bǔ)碼硬件友好的表示法為什么用補(bǔ)碼因?yàn)樗尲臃ê蜏p法在硬件上統(tǒng)一了。對(duì)于減法A - B硬件可以將其計(jì)算為A (~B 1)即A加上B的補(bǔ)碼。這樣CPU或FPGA內(nèi)部只需要加法器就能完成加減運(yùn)算極大地簡(jiǎn)化了設(shè)計(jì)。理解補(bǔ)碼是理解有符號(hào)數(shù)運(yùn)算的基礎(chǔ)。2.3 變量的“符號(hào)屬性”聲明在Verilog中我們通過以下方式聲明變量的符號(hào)屬性reg [7:0] data_u;// 默認(rèn)或無關(guān)鍵字通常被工具視為無符號(hào)數(shù)。reg signed [7:0] data_s;// 使用signed關(guān)鍵字明確聲明為有符號(hào)數(shù)。這個(gè)聲明至關(guān)重要因?yàn)樗苯佑绊懛抡嫘袨榉抡嫫魅绾物@示這個(gè)變量的值是顯示十進(jìn)制正數(shù)還是可能顯示負(fù)數(shù)。運(yùn)算規(guī)則當(dāng)這個(gè)變量參與,-,*,,等運(yùn)算時(shí)工具采用哪套規(guī)則。綜合結(jié)果綜合工具可能會(huì)根據(jù)運(yùn)算類型生成不同的電路結(jié)構(gòu)例如選擇有符號(hào)數(shù)乘法器IP核還是無符號(hào)數(shù)乘法器IP核。2.4 工具如何對(duì)待未聲明的數(shù)這里有一個(gè)巨大的陷阱直接寫在代碼中的數(shù)字常量例如8-5 它的符號(hào)和位寬是由上下文決定的Verilog有一個(gè)復(fù)雜的“整數(shù)常量”規(guī)則。簡(jiǎn)單來說一個(gè)沒有指定位寬和進(jìn)制的十進(jìn)制數(shù)如-5會(huì)被當(dāng)作有符號(hào)整數(shù)來處理。但更常見也更安全的方法是使用明確的格式8‘d10 8位寬十進(jìn)制數(shù)值10。通常被視為無符號(hào)。8‘sd10 8位寬十進(jìn)制有符號(hào)數(shù)值10。-8‘sd5 8位寬十進(jìn)制有符號(hào)數(shù)值-5。8‘b1111_1010 就是一串二進(jìn)制位其符號(hào)屬性由賦值目標(biāo)或運(yùn)算上下文決定。我的踩坑心得養(yǎng)成對(duì)常量也指定符號(hào)和位寬的習(xí)慣尤其是在進(jìn)行混合類型運(yùn)算時(shí)。寫data_s 8‘sd1比寫data_s 1要清晰和安全得多能避免很多意想不到的符號(hào)擴(kuò)展問題。3. 轉(zhuǎn)換的本質(zhì)與核心方法轉(zhuǎn)換的核心目標(biāo)在保持二進(jìn)制位序列的物理意義即電路連接基本不變的前提下改變工具和我們對(duì)這串二進(jìn)制位的“解讀規(guī)則”。3.1 無符號(hào)數(shù)轉(zhuǎn)有符號(hào)數(shù)場(chǎng)景你從ADC模數(shù)轉(zhuǎn)換器讀到一個(gè)無符號(hào)數(shù)據(jù)adc_data[11:0]范圍0~4095?,F(xiàn)在你需要對(duì)其進(jìn)行數(shù)字濾波濾波系數(shù)是有負(fù)數(shù)的因此需要將ADC數(shù)據(jù)當(dāng)作有符號(hào)數(shù)來處理比如將零點(diǎn)設(shè)在2048范圍變?yōu)?2048~2047。方法1直接賦值最常用但需理解位寬匹配reg [11:0] adc_data_u; // 無符號(hào)0~4095 reg signed [12:0] adc_data_s; // 有符號(hào)我們想得到 -2048~2047 // 方法先進(jìn)行位寬擴(kuò)展再賦值 adc_data_s {1‘b0, adc_data_u}; // 錯(cuò)誤這只是簡(jiǎn)單的拼接adc_data_s[12]是0正數(shù)范圍是0~4095不是我們想要的-2048~2047。 adc_data_s $signed({1‘b0, adc_data_u}); // 仍然不對(duì)。$signed()只是改變“解讀方式”但{1‘b0, adc_data_u}本身是一個(gè)13位的無符號(hào)數(shù)最高位是0$signed()解讀后還是一個(gè)正數(shù)。正確的做法是進(jìn)行“符號(hào)化偏移”// 核心思想將無符號(hào)的原始值減去一個(gè)偏移量如2048將其轉(zhuǎn)化為以該偏移量為零點(diǎn)的有符號(hào)數(shù)。 localparam OFFSET 12‘d2048; // 偏移量 reg signed [12:0] adc_data_s; // 需要多1位因?yàn)闇p法可能產(chǎn)生負(fù)數(shù) always (posedge clk) begin adc_data_s $signed(adc_data_u) - $signed(OFFSET); end // 此時(shí)當(dāng) adc_data_u 0 時(shí)adc_data_s -2048當(dāng) adc_data_u 4095時(shí)adc_data_s 2047。方法2使用 $signed() 系統(tǒng)函數(shù)$signed()函數(shù)的作用是將一個(gè)表達(dá)式的結(jié)果當(dāng)作有符號(hào)數(shù)來解釋。它不改變底層的位值只改變“類型標(biāo)簽”。reg [7:0] unsigned_data 8‘hFF; // 二進(jìn)制 11111111無符號(hào)解讀為 255 reg signed [7:0] signed_data; signed_data $signed(unsigned_data); // 將 unsigned_data 的值255即8‘hFF當(dāng)作有符號(hào)數(shù)解讀。 // 仿真器會(huì)顯示 signed_data 的值為 -1。關(guān)鍵點(diǎn)$signed(unsigned_data)并不意味著把255“轉(zhuǎn)換”成了-1的位模式。它意味著工具在接下來的運(yùn)算中會(huì)把unsigned_data當(dāng)前的值8‘hFF當(dāng)成補(bǔ)碼-1來處理。如果unsigned_data的值是 8‘h7F127那么$signed()后就被當(dāng)成127。3.2 有符號(hào)數(shù)轉(zhuǎn)無符號(hào)數(shù)場(chǎng)景你完成了一個(gè)有符號(hào)濾波運(yùn)算得到了一個(gè)負(fù)的結(jié)果filter_out_s現(xiàn)在需要將其送到DAC數(shù)模轉(zhuǎn)換器輸出而DAC通常需要無符號(hào)的正整數(shù)輸入。方法1直接賦值同樣注意位寬與偏移reg signed [15:0] filter_out_s; // 濾波結(jié)果有正有負(fù) reg [15:0] dac_data_u; // 輸出給DAC需要是0~65535的正數(shù) // 如果DAC的零點(diǎn)是中間值32768 localparam DAC_OFFSET 16‘d32768; always (posedge clk) begin dac_data_u filter_out_s DAC_OFFSET; // 先加上偏移量使其變?yōu)檎龜?shù) // 注意filter_out_s DAC_OFFSET 這個(gè)表達(dá)式的結(jié)果由于包含有符號(hào)數(shù)默認(rèn)會(huì)被當(dāng)作有符號(hào)數(shù)運(yùn)算。 // 但賦值給無符號(hào)的 dac_data_u 時(shí)會(huì)發(fā)生隱式轉(zhuǎn)換。為了清晰可以 // dac_data_u $unsigned(filter_out_s DAC_OFFSET); end方法2使用 $unsigned() 系統(tǒng)函數(shù)$unsigned()與$signed()相反它強(qiáng)制將表達(dá)式結(jié)果當(dāng)作無符號(hào)數(shù)解釋。reg signed [7:0] signed_data -1; // 補(bǔ)碼表示為 8‘hFF reg [7:0] unsigned_data; unsigned_data $unsigned(signed_data); // 將 signed_data 的值-1當(dāng)作無符號(hào)數(shù)解讀。 // 仿真器會(huì)顯示 unsigned_data 的值為 255。重要警告直接將一個(gè)負(fù)的有符號(hào)數(shù)賦值給無符號(hào)變量或者用$unsigned()包裹一個(gè)負(fù)值不會(huì)產(chǎn)生任何數(shù)值上的轉(zhuǎn)換它只是粗暴地改變了“解讀方式”。-18‘hFF被解讀為無符號(hào)數(shù)就是255。這通常不是你想要的結(jié)果你真正需要的往往是一個(gè)“數(shù)值上的平移”如上文的加偏移量示例。3.3 轉(zhuǎn)換中的位寬擴(kuò)展問題這是轉(zhuǎn)換時(shí)最容易出錯(cuò)的地方。當(dāng)目標(biāo)位寬大于源位寬時(shí)需要進(jìn)行位擴(kuò)展。無符號(hào)數(shù)擴(kuò)展高位補(bǔ)0。這叫零擴(kuò)展。reg [3:0] small_u 4‘b1010; // 10 reg [7:0] large_u; large_u {4‘b0000, small_u}; // 8‘b00001010 仍然是10有符號(hào)數(shù)擴(kuò)展高位補(bǔ)符號(hào)位最高位的值。這叫符號(hào)擴(kuò)展。這是為了保持?jǐn)?shù)值不變例如4位有符號(hào)數(shù)-61010擴(kuò)展為8位應(yīng)該是-611111010而不是1000001010。reg signed [3:0] small_s 4‘b1010; // -6 (補(bǔ)碼) reg signed [7:0] large_s; large_s {{4{small_s[3]}}, small_s}; // 8‘b11111010 仍然是 -6 // {{4{small_s[3]}}} 是Verilog的復(fù)制運(yùn)算符將 small_s 的最高位復(fù)制4次。隱式擴(kuò)展的陷阱reg signed [7:0] a 8‘sh8F; // -113 reg [15:0] b; b a; // 這里會(huì)發(fā)生什么賦值時(shí)工具需要將8位的a擴(kuò)展到16位來匹配b。因?yàn)閎是無符號(hào)的你可能會(huì)期待零擴(kuò)展。但Verilog的規(guī)則是在賦值過程中如果源表達(dá)式是有符號(hào)的會(huì)先進(jìn)行符號(hào)擴(kuò)展然后再進(jìn)行賦值。所以a(-113) 先被符號(hào)擴(kuò)展為16位的16‘shFF8F仍然是-113然后這個(gè)位模式被賦值給無符號(hào)的b。b的值將是 65423即 16‘hFF8F這很可能不是你想要的結(jié)果。正確的做法是顯式控制擴(kuò)展b {8‘h00, a}; // 零擴(kuò)展b 143 不對(duì)a是8‘sh8F (143的位模式但作為有符號(hào)數(shù)是-113)。{8‘h00, 8‘sh8F} 得到 16‘h008F即 143。 // 看這又回到了“解讀”問題。如果你想要 a 的“數(shù)值”143你應(yīng)該用 b $unsigned(a); // 但 $unsigned(a) 得到的是 8‘h8F 143然后零擴(kuò)展為16位b143。 // 如果你想要 a 的“位模式”進(jìn)行零擴(kuò)展那就是上面 {8‘h00, a} 的做法。可見必須非常清楚你轉(zhuǎn)換的意圖是保持“數(shù)值”不變還是進(jìn)行某種“位模式”的映射4. 混合類型運(yùn)算的黃金法則當(dāng)表達(dá)式中同時(shí)出現(xiàn)有符號(hào)和無符號(hào)操作數(shù)時(shí)Verilog會(huì)根據(jù)一套優(yōu)先級(jí)規(guī)則將所有操作數(shù)轉(zhuǎn)換為同一類型后再計(jì)算。這套規(guī)則比較復(fù)雜但記住以下黃金法則可以避免99%的問題法則一顯式聲明優(yōu)于隱式轉(zhuǎn)換對(duì)于任何可能產(chǎn)生歧義的運(yùn)算使用$signed()或$unsigned()顯式地標(biāo)記每一個(gè)操作數(shù)的類型。這相當(dāng)于告訴工具“請(qǐng)按我指定的方式來解讀這個(gè)數(shù)據(jù)”。法則二警惕比較運(yùn)算符,,,這些關(guān)系運(yùn)算符在混合類型比較時(shí)極易出錯(cuò)。reg [7:0] u_data 8‘hFF; // 255 reg signed [7:0] s_data 8‘hFF; // -1 if (u_data s_data) // 危險(xiǎn)混合比較 $display(“u_data s_data”); else $display(“u_data s_data”);結(jié)果可能會(huì)輸出u_data s_data因?yàn)楣ぞ呖赡軙?huì)把u_data也當(dāng)作有符號(hào)數(shù)來比較255的補(bǔ)碼形式是-1不這里涉及整型提升規(guī)則很繞。絕對(duì)安全的做法是if ($unsigned(u_data) $unsigned(s_data)) // 都轉(zhuǎn)為無符號(hào)比 255 255? 不s_data轉(zhuǎn)無符號(hào)是255所以相等。 if ($signed(u_data) $signed(s_data)) // 都轉(zhuǎn)為有符號(hào)比 -1 -1? 相等。 // 更好的做法在比較前先將數(shù)據(jù)轉(zhuǎn)換到你期望的數(shù)值范圍再比。 if (u_data (s_data 128)) // 假設(shè)你想把s_data的零點(diǎn)偏移法則三賦值時(shí)的“類型覆蓋”賦值語句LHS RHS;最終結(jié)果的類型是由左值LHS的類型決定的。但RHS表達(dá)式計(jì)算過程中的類型轉(zhuǎn)換會(huì)影響中間結(jié)果。確保RHS的計(jì)算過程符合你的預(yù)期。法則四使用中間變量明確轉(zhuǎn)換步驟對(duì)于復(fù)雜的表達(dá)式不要追求一行寫完。拆分成多步用中間變量存儲(chǔ)轉(zhuǎn)換結(jié)果這樣代碼更清晰也更容易調(diào)試。// 模糊的寫法 output_data (coeff_s * adc_data_u) 8; // 清晰的寫法 reg signed [19:0] mult_result_s; // 假設(shè)位寬足夠 reg signed [11:0] adc_data_s_tmp; always (posedge clk) begin // 步驟1將無符號(hào)ADC數(shù)據(jù)轉(zhuǎn)換到有符號(hào)域減去偏移 adc_data_s_tmp $signed(adc_data_u) - OFFSET; // 步驟2進(jìn)行有符號(hào)乘法 mult_result_s coeff_s * adc_data_s_tmp; // 步驟3截位或縮放賦值給輸出輸出可能又是無符號(hào)的 output_data $unsigned((mult_result_s 8) OUTPUT_OFFSET); // 是算術(shù)右移會(huì)保持符號(hào) end5. 實(shí)戰(zhàn)案例一個(gè)簡(jiǎn)單的有符號(hào)FIR濾波器讓我們?cè)O(shè)計(jì)一個(gè)3抽頭的有符號(hào)FIR濾波器輸入是無符號(hào)的ADC數(shù)據(jù)系數(shù)是有符號(hào)的輸出最終要轉(zhuǎn)換為無符號(hào)送給DAC。module signed_fir_filter ( input wire clk, input wire rst_n, input wire [11:0] adc_data_u, // 無符號(hào)輸入0-4095 output reg [11:0] dac_data_u // 無符號(hào)輸出0-4095 ); // 參數(shù)定義 localparam signed [7:0] COEFF_0 8‘sd30; // 明確指定有符號(hào)系數(shù) localparam signed [7:0] COEFF_1 8‘sd100; localparam signed [7:0] COEFF_2 8‘sd30; localparam OFFSET 12‘d2048; // ADC零點(diǎn)偏移使輸入范圍變?yōu)?-2048 ~ 2047 localparam OUTPUT_GAIN 4; // 輸出增益放大倍數(shù) localparam OUTPUT_OFFSET 12‘d2048; // 輸出偏移使范圍回到 0~4095 // 輸入數(shù)據(jù)寄存與轉(zhuǎn)換 reg signed [12:0] adc_data_s; // 13位有符號(hào)容納 -2048~2047 reg signed [12:0] data_delay1, data_delay2; // 延遲線 always (posedge clk or negedge rst_n) begin if (!rst_n) begin adc_data_s 13‘sd0; data_delay1 13‘sd0; data_delay2 13‘sd0; end else begin // 關(guān)鍵轉(zhuǎn)換步驟1無符號(hào)輸入 - 有符號(hào)處理域 adc_data_s $signed(adc_data_u) - $signed(OFFSET); data_delay1 adc_data_s; data_delay2 data_delay1; end end // 有符號(hào)乘累加 reg signed [21:0] mult_acc_s; // 乘法累加結(jié)果需要足夠位寬防止溢出 // 位寬計(jì)算輸入13位系數(shù)8位乘法結(jié)果21位。3個(gè)乘積相加最多23位我們?nèi)?4位。 reg signed [23:0] sum_s; always (posedge clk or negedge rst_n) begin if (!rst_n) begin mult_acc_s 22‘sd0; sum_s 24‘sd0; end else begin // 有符號(hào)乘法使用 signed 聲明的變量和常量確保是有符號(hào)乘法 mult_acc_s (adc_data_s * COEFF_0) (data_delay1 * COEFF_1) (data_delay2 * COEFF_2); // 應(yīng)用增益并截位這里簡(jiǎn)單右移相當(dāng)于除以4 sum_s mult_acc_s * OUTPUT_GAIN; // 先增益 end end // 輸出轉(zhuǎn)換有符號(hào)域 - 無符號(hào)輸出 reg signed [23:0] sum_shifted; always (posedge clk or negedge rst_n) begin if (!rst_n) begin sum_shifted 24‘sd0; dac_data_u 12‘d0; end else begin // 算術(shù)右移保持符號(hào)進(jìn)行縮放 sum_shifted sum_s 2; // 除以4 // 關(guān)鍵轉(zhuǎn)換步驟2有符號(hào)結(jié)果 - 無符號(hào)輸出域 // 1. 加上輸出偏移量將零點(diǎn)移回 // 2. 使用 $unsigned 改變解讀方式 // 3. 同時(shí)進(jìn)行飽和處理防止超出0-4095范圍 if (($signed(sum_shifted) OUTPUT_OFFSET) 4095) dac_data_u 12‘d4095; else if (($signed(sum_shifted) OUTPUT_OFFSET) 0) dac_data_u 12‘d0; else dac_data_u $unsigned($signed(sum_shifted) OUTPUT_OFFSET); // 先做有符號(hào)加法再轉(zhuǎn)無符號(hào) end end endmodule在這個(gè)例子中轉(zhuǎn)換清晰地發(fā)生在兩個(gè)邊界輸入邊界adc_data_u - OFFSET并用$signed()包裹確保后續(xù)運(yùn)算在有符號(hào)域進(jìn)行。輸出邊界sum_shifted OUTPUT_OFFSET用$unsigned()包裹將最終數(shù)值轉(zhuǎn)換回DAC需要的無符號(hào)格式。飽和處理是防止轉(zhuǎn)換后溢出的重要步驟。6. 仿真、調(diào)試與常見問題排查6.1 仿真器顯示差異在Modelsim、VCS等仿真器中一個(gè)變量顯示的值取決于它的聲明。reg [7:0] a 8‘hFF;仿真器可能顯示255。reg signed [7:0] b 8‘hFF;仿真器可能顯示-1。調(diào)試技巧在查看波形時(shí)可以同時(shí)添加該信號(hào)的“無符號(hào)格式”和“有符號(hào)格式”。例如在Modelsim中你可以對(duì)同一個(gè)信號(hào)data添加data默認(rèn)顯示再添加一個(gè)虛擬信號(hào)$signed(data)和$unsigned(data)來觀察不同解讀下的值。這能幫你快速定位是否是符號(hào)解讀錯(cuò)誤導(dǎo)致的問題。6.2 綜合警告與錯(cuò)誤綜合工具如Vivado、Quartus可能會(huì)對(duì)混合類型運(yùn)算提出警告。警告示例[Synth 8-5536] Signed/unsigned mismatch in expression.不要忽略這個(gè)警告它提示你的代碼中存在潛在的邏輯錯(cuò)誤風(fēng)險(xiǎn)。仔細(xì)檢查警告所在行的表達(dá)式按照黃金法則顯式轉(zhuǎn)換操作數(shù)類型。位寬截?cái)嗑娈?dāng)你將一個(gè)較寬的有符號(hào)數(shù)賦值給較窄的無符號(hào)變量時(shí)綜合工具會(huì)警告可能的數(shù)據(jù)丟失。你需要確認(rèn)這種截?cái)嗍欠袷窃O(shè)計(jì)意圖例如丟棄高位是安全的或者你是否需要先進(jìn)行飽和處理。6.3 常見問題速查表現(xiàn)象可能原因排查與解決方法仿真結(jié)果與數(shù)學(xué)計(jì)算不符混合類型運(yùn)算未顯式轉(zhuǎn)換檢查所有運(yùn)算符兩邊的操作數(shù)類型使用$signed()/$unsigned()顯式標(biāo)記。輸出數(shù)據(jù)出現(xiàn)非預(yù)期的跳變?nèi)鐝恼龜?shù)突變?yōu)楹艽蟮臄?shù)有符號(hào)負(fù)數(shù)被誤當(dāng)作無符號(hào)數(shù)使用檢查從有符號(hào)到無符號(hào)的轉(zhuǎn)換點(diǎn)是否忘記了加偏移量進(jìn)行數(shù)值映射而只是簡(jiǎn)單改變了類型解讀。乘法或加法結(jié)果符號(hào)錯(cuò)誤常量未指定符號(hào)和位寬檢查代碼中的數(shù)字常量特別是負(fù)數(shù)。將-1改為8‘sd-1或-8‘sd1。比較邏輯始終為真或?yàn)榧訇P(guān)系運(yùn)算符兩邊的類型不一致在比較前將兩邊操作數(shù)統(tǒng)一轉(zhuǎn)換為同一種類型通常轉(zhuǎn)換為有符號(hào)數(shù)進(jìn)行比較更符合直覺。資源使用如DSP比預(yù)期多工具無法推斷出有符號(hào)乘法可能生成了更通用的乘法器確保乘法操作的兩個(gè)操作數(shù)都被明確聲明為signed或者使用$signed()包裹。這樣工具才能調(diào)用優(yōu)化的有符號(hào)乘法器IP。時(shí)序仿真與功能仿真結(jié)果不同在賦值時(shí)有符號(hào)數(shù)的符號(hào)擴(kuò)展行為與預(yù)期不符仔細(xì)檢查位寬擴(kuò)展的代碼。對(duì)于有符號(hào)數(shù)賦值給更寬的無符號(hào)數(shù)考慮是否先進(jìn)行了零擴(kuò)展{0, signed_data}而不是依賴默認(rèn)的符號(hào)擴(kuò)展。6.4 我的調(diào)試心得波形圖是你的朋友遇到運(yùn)算問題第一件事是把所有相關(guān)信號(hào)原始輸入、轉(zhuǎn)換后的中間值、運(yùn)算結(jié)果、最終輸出都拉到波形圖里。同時(shí)觀察它們的十六進(jìn)制形式和有/無符號(hào)十進(jìn)制形式。十六進(jìn)制形式反映了真實(shí)的位模式十進(jìn)制形式反映了當(dāng)前的解讀方式兩者對(duì)比往往能立刻發(fā)現(xiàn)問題。編寫小型測(cè)試平臺(tái)針對(duì)復(fù)雜的轉(zhuǎn)換邏輯不要在主測(cè)試平臺(tái)里調(diào)試。單獨(dú)寫一個(gè)簡(jiǎn)單的tb_conversion.v用幾個(gè)邊界值如最大值、最小值、0、-1進(jìn)行測(cè)試驗(yàn)證轉(zhuǎn)換函數(shù)是否正確。理解工具的“默認(rèn)行為”花點(diǎn)時(shí)間閱讀你所用的FPGA開發(fā)工具Vivado/Quartus和仿真器Modelsim/VCS關(guān)于有符號(hào)數(shù)運(yùn)算的文檔。了解它們?cè)谔囟ㄇ闆r下的默認(rèn)選擇才能寫出可預(yù)測(cè)、可移植的代碼。保持一致的數(shù)據(jù)流域在一個(gè)模塊內(nèi)部盡量讓數(shù)據(jù)在同一個(gè)“域”內(nèi)流動(dòng)要么全有符號(hào)要么全無符號(hào)。轉(zhuǎn)換只在模塊的輸入和輸出接口處進(jìn)行。這能極大簡(jiǎn)化內(nèi)部邏輯減少錯(cuò)誤。有符號(hào)數(shù)和無符號(hào)數(shù)的轉(zhuǎn)換本質(zhì)上是在數(shù)字的物理表示位模式和邏輯含義數(shù)值之間進(jìn)行映射。它考驗(yàn)的是你對(duì)二進(jìn)制系統(tǒng)、補(bǔ)碼表示以及Verilog語言規(guī)則的透徹理解。沒有捷徑多寫、多仿、多踩坑自然就能建立起準(zhǔn)確的直覺。當(dāng)你再看到$signed()和$unsigned()時(shí)你想到的不再是模糊的“類型轉(zhuǎn)換”而是清晰的“在此處請(qǐng)將這段比特流按補(bǔ)碼規(guī)則解讀”或“在此處請(qǐng)將其視為純二進(jìn)制正整數(shù)”的明確指令。