數(shù)編程實(shí)戰(zhàn):從原理到嵌入式/DSP高效應(yīng)用)
1. 從浮點(diǎn)數(shù)到定點(diǎn)數(shù)為什么我們需要另一種數(shù)字表示在編程和硬件設(shè)計(jì)的日常工作中浮點(diǎn)數(shù)Float幾乎無(wú)處不在。無(wú)論是處理科學(xué)計(jì)算、圖形渲染還是簡(jiǎn)單的業(yè)務(wù)邏輯float和double類型都是我們最熟悉的數(shù)字伙伴。它們能優(yōu)雅地表示極大或極小的數(shù)值比如光速或電子的質(zhì)量這得益于其基于科學(xué)計(jì)數(shù)法的設(shè)計(jì)——一個(gè)尾數(shù)乘以2的指數(shù)次方。然而當(dāng)我們的戰(zhàn)場(chǎng)轉(zhuǎn)移到嵌入式系統(tǒng)、數(shù)字信號(hào)處理DSP、音頻編解碼或某些對(duì)性能和確定性要求極高的金融交易場(chǎng)景時(shí)浮點(diǎn)數(shù)這位“優(yōu)雅的貴族”就開始顯得有些笨拙和昂貴了。浮點(diǎn)數(shù)的運(yùn)算無(wú)論是加法還是乘法其硬件實(shí)現(xiàn)FPU都相對(duì)復(fù)雜消耗更多的晶體管、功耗和時(shí)鐘周期。在資源受限的微控制器MCU上可能根本沒(méi)有硬件FPU軟件模擬浮點(diǎn)運(yùn)算的效率又極其低下。更重要的是浮點(diǎn)運(yùn)算的結(jié)果有時(shí)是“非確定性的”——在不同的編譯器、不同的優(yōu)化等級(jí)、甚至不同的硬件平臺(tái)上對(duì)同一串浮點(diǎn)運(yùn)算可能產(chǎn)生極其微小的差異。對(duì)于要求結(jié)果絕對(duì)一致、可重現(xiàn)的系統(tǒng)如分布式仿真、區(qū)塊鏈智能合約中的金融計(jì)算這是不可接受的。這時(shí)定點(diǎn)數(shù)Fixed-Point Number就登場(chǎng)了。你可以把它理解為一種“簡(jiǎn)化版”或“特化版”的浮點(diǎn)數(shù)。它放棄了動(dòng)態(tài)變化的指數(shù)部分強(qiáng)制規(guī)定小數(shù)點(diǎn)隱含在一個(gè)固定不變的位置上。所有的數(shù)字無(wú)論大小都使用同一套整數(shù)運(yùn)算規(guī)則來(lái)處理。聽起來(lái)像是退步恰恰相反在特定領(lǐng)域這是巨大的進(jìn)步。它用可預(yù)測(cè)的、高效的整數(shù)運(yùn)算單元實(shí)現(xiàn)了對(duì)小數(shù)的高性能、確定性處理。理解定點(diǎn)數(shù)不僅是掌握一種數(shù)字表示法更是獲得了一把優(yōu)化關(guān)鍵性能路徑、深入理解計(jì)算機(jī)底層數(shù)值運(yùn)算的鑰匙。無(wú)論你是嵌入式開發(fā)者、算法工程師還是對(duì)性能有極致追求的后端工程師定點(diǎn)數(shù)都是你工具箱里不可或缺的利器。2. 定點(diǎn)數(shù)的核心原理把小數(shù)點(diǎn)“釘”在固定位置要理解定點(diǎn)數(shù)最直觀的方式是和浮點(diǎn)數(shù)做對(duì)比并從一個(gè)簡(jiǎn)單的整數(shù)開始。2.1 從整數(shù)到小數(shù)隱含的尺度假設(shè)我們有一個(gè)8位的無(wú)符號(hào)整數(shù)它的表示范圍是0到255。它只能表示整數(shù)。現(xiàn)在我們引入一個(gè)約定這個(gè)8位數(shù)字其最低的2位不再代表個(gè)位和十位而是代表“1/4位”和“1/2位”。更一般化地說(shuō)我們約定這個(gè)數(shù)字的小數(shù)點(diǎn)固定在從右向左數(shù)的第2位之后。這是什么意思呢我們定義一個(gè)格式UQ8.2。這里的“U”代表無(wú)符號(hào)Unsigned“Q”是Q格式標(biāo)記后面會(huì)詳述“8”是總位數(shù)“2”是小數(shù)部分的位數(shù)。那么這個(gè)UQ8.2格式的數(shù)字其表示的實(shí)際值 存儲(chǔ)的整數(shù)值 × 2^{-小數(shù)位數(shù)} 存儲(chǔ)的整數(shù)值 × 2^{-2} 存儲(chǔ)的整數(shù)值 × 0.25。舉個(gè)例子存儲(chǔ)的二進(jìn)制整數(shù)是00000100十進(jìn)制4。那么它表示的實(shí)際數(shù)值是 4 * 0.25 1.0。存儲(chǔ)的二進(jìn)制整數(shù)是00000101十進(jìn)制5。那么實(shí)際值是 5 * 0.25 1.25。存儲(chǔ)的二進(jìn)制整數(shù)是11111111十進(jìn)制255。那么實(shí)際值是 255 * 0.25 63.75。看到了嗎我們并沒(méi)有在內(nèi)存中存儲(chǔ)一個(gè)小數(shù)點(diǎn)。我們存儲(chǔ)的依然是一個(gè)純粹的整數(shù)這里是4、5、255。但是我們約定了這個(gè)整數(shù)背后有一個(gè)固定的縮放因子這里是0.25。當(dāng)我們需要這個(gè)數(shù)的“實(shí)際意義”時(shí)就乘以這個(gè)縮放因子。在進(jìn)行運(yùn)算時(shí)我們直接對(duì)存儲(chǔ)的整數(shù)進(jìn)行操作并在必要時(shí)考慮縮放因子帶來(lái)的影響。注意這種“約定”必須在整個(gè)系統(tǒng)內(nèi)保持一致。數(shù)據(jù)的生產(chǎn)者、處理者和消費(fèi)者都必須明確知道這個(gè)定點(diǎn)數(shù)格式是UQ8.2否則解讀出來(lái)的數(shù)值將完全錯(cuò)誤。這通常通過(guò)文檔、變量命名或強(qiáng)類型來(lái)保證。2.2 Q格式定點(diǎn)數(shù)的標(biāo)準(zhǔn)“方言”在工程實(shí)踐中人們常用“Q格式”來(lái)精確描述一個(gè)定點(diǎn)數(shù)。其通用形式是Qm.n或UQm.n/SQm.n。Q: 標(biāo)識(shí)此為定點(diǎn)數(shù)格式。m: 整數(shù)部分的位數(shù)包括符號(hào)位如果是有符號(hào)數(shù)。n: 小數(shù)部分的位數(shù)??偽粩?shù)m n。對(duì)于有符號(hào)數(shù)SQm.n通常用補(bǔ)碼表示一位符號(hào)位所以m包含了這1位。U/S: 可選前綴U代表無(wú)符號(hào)UnsignedS代表有符號(hào)Signed。有時(shí)會(huì)省略默認(rèn)可能為有符號(hào)。舉例說(shuō)明Q7.8: 這是一個(gè)有符號(hào)定點(diǎn)數(shù)總位寬為15位78。其中1位是符號(hào)位6位是整數(shù)位8位是小數(shù)位。其縮放因子為 2^{-8} 1/256。UQ10.6: 這是一個(gè)無(wú)符號(hào)定點(diǎn)數(shù)總位寬為16位。其中10位整數(shù)位6位小數(shù)位。縮放因子為 2^{-6} 1/64。Q1.14: 常見于音頻處理總位寬15位。1位符號(hào)位0位整數(shù)位即整數(shù)部分只能是-10或接近014位小數(shù)位??s放因子為 2^{-14} 1/16384。它能精細(xì)地表示-1.0到接近1.0之間的小數(shù)非常適合表示音頻采樣幅值??s放因子與精度 縮放因子S 2^{-n}它直接決定了該定點(diǎn)數(shù)格式的精度即能表示的最小非零小數(shù)。例如Q15.1632位定點(diǎn)數(shù)16位小數(shù)的精度是 2^{-16} ≈ 0.00001526這對(duì)于大多數(shù)控制算法和信號(hào)處理來(lái)說(shuō)已經(jīng)足夠精細(xì)。同時(shí)它也決定了表示范圍。例如UQ8.8的范圍是 0 到 (2^8 - 1) * 2^{-8} 0 到 255/256 ≈ 0 到 0.996。2.3 與浮點(diǎn)數(shù)的本質(zhì)區(qū)別現(xiàn)在我們可以清晰地對(duì)比兩者浮點(diǎn)數(shù) 由符號(hào)位S、指數(shù)位E和尾數(shù)位M組成。數(shù)值 (-1)^S * 1.M * 2^{(E-bias)}。小數(shù)點(diǎn)位置由指數(shù)E動(dòng)態(tài)決定因此可以在極大動(dòng)態(tài)范圍內(nèi)保持相對(duì)精度。定點(diǎn)數(shù) 就是一個(gè)整數(shù)或補(bǔ)碼整數(shù)乘以一個(gè)固定的縮放因子2^{-n}。小數(shù)點(diǎn)位置是靜態(tài)的、隱含的由格式Qm.n中的n固定死。這種靜態(tài)特性帶來(lái)了優(yōu)缺點(diǎn)優(yōu)點(diǎn)運(yùn)算等同于整數(shù)運(yùn)算速度快功耗低硬件實(shí)現(xiàn)簡(jiǎn)單結(jié)果確定。缺點(diǎn)動(dòng)態(tài)范圍有限。你必須事先預(yù)估好運(yùn)算過(guò)程中可能出現(xiàn)的最大值和最小值并選擇合適的m和n。如果選小了會(huì)溢出如果為了安全把m選得很大又會(huì)浪費(fèi)精度。3. 定點(diǎn)數(shù)的運(yùn)算規(guī)則與實(shí)操要點(diǎn)理解了表示法接下來(lái)就是如何在定點(diǎn)數(shù)之間進(jìn)行加、減、乘、除。這是定點(diǎn)數(shù)使用的核心也是最容易出錯(cuò)的地方。3.1 加法與減法對(duì)齊小數(shù)點(diǎn)定點(diǎn)數(shù)的加減法有一個(gè)黃金法則參與運(yùn)算的兩個(gè)操作數(shù)必須具有相同的小數(shù)點(diǎn)位置即相同的n。如果n不同直接對(duì)存儲(chǔ)的整數(shù)進(jìn)行加減結(jié)果將毫無(wú)意義。這就像你不能直接把“3.5米”和“350厘米”的數(shù)字3.5和350相加一樣必須先統(tǒng)一單位。操作假設(shè)有A Qa.ma, na和B Qb.mb, nb且na ! nb。確定目標(biāo)格式。通常選擇小數(shù)位數(shù)更多的那個(gè)n_target max(na, nb)以避免精度損失。對(duì)小數(shù)位數(shù)少的操作數(shù)進(jìn)行格式轉(zhuǎn)換。這本質(zhì)上是一個(gè)整數(shù)的移位操作。如果要將A (na)轉(zhuǎn)換為n_target且n_target na則需要將A存儲(chǔ)的整數(shù)值算術(shù)左移(n_target - na)位。這相當(dāng)于放大了數(shù)值。例如A是Q8.4值a_int要轉(zhuǎn)為Q12.8。8-44位需要將a_int 4。對(duì)轉(zhuǎn)換到同一格式后的兩個(gè)整數(shù)值進(jìn)行加減運(yùn)算。結(jié)果自然繼承了目標(biāo)格式Qm_target.n_target。實(shí)操心得在C語(yǔ)言中對(duì)于有符號(hào)定點(diǎn)數(shù)移位前最好先進(jìn)行類型提升到更高位寬的整數(shù)如int32_t以避免移位時(shí)符號(hào)位出現(xiàn)問(wèn)題或溢出。加減法可能溢出即使兩個(gè)操作數(shù)本身不溢出它們的和或差可能超出結(jié)果格式的表示范圍。在設(shè)計(jì)格式時(shí)m整數(shù)位數(shù)必須能容納運(yùn)算結(jié)果。// 示例Q8.4 與 Q8.6 相加 int16_t a_q8_4 160; // 實(shí)際值160 * 2^{-4} 10.0 int16_t b_q8_6 640; // 實(shí)際值640 * 2^{-6} 10.0 // 統(tǒng)一到Q10.6格式選擇n6 // 將a從Q8.4轉(zhuǎn)為Q10.6左移 (6-4)2位 int16_t a_converted (int16_t)((int32_t)a_q8_4 2); // 160 2 640 // 現(xiàn)在兩者都是Q10.6格式a_converted和b_q8_6 int16_t sum_q10_6 a_converted b_q8_6; // 640 640 1280 // 實(shí)際值1280 * 2^{-6} 1280 / 64 20.0正確。3.2 乘法小數(shù)位相加乘法相對(duì)直接但有一個(gè)重要特點(diǎn)兩個(gè)定點(diǎn)數(shù)相乘結(jié)果的小數(shù)位數(shù)是兩者小數(shù)位數(shù)之和。操作A (Qma.na)乘以B (Qmb.nb)。直接將存儲(chǔ)的整數(shù)值a_int和b_int相乘。注意兩個(gè)N位數(shù)相乘結(jié)果最多需要2N位來(lái)存儲(chǔ)因此通常需要使用雙倍位寬的中間變量如int32_t存放兩個(gè)int16_t的乘積。乘積結(jié)果是一個(gè)中間格式其小數(shù)位數(shù) na nb。通常我們需要將結(jié)果調(diào)整回某個(gè)標(biāo)準(zhǔn)的目標(biāo)格式例如存回一個(gè)16位變量。這就需要進(jìn)行重新定標(biāo)Rescaling即右移或左移并處理舍入。// 示例Q8.4 乘以 Q8.4結(jié)果希望存為 Q8.4 int16_t a_q8_4 160; // 10.0 int16_t b_q8_4 80; // 5.0 int32_t product_raw (int32_t)a_q8_4 * (int32_t)b_q8_4; // 160 * 80 12800 // 此時(shí) product_raw 的格式可以理解為 Q16.8因?yàn)?48位小數(shù) // 我們需要將其轉(zhuǎn)換回 Q8.4即保留4位小數(shù)。 // 從8位小數(shù)變?yōu)?位小數(shù)需要右移 (8-4)4位。 int16_t result_q8_4 (int16_t)(product_raw 4); // 12800 4 800 // 驗(yàn)證800 * 2^{-4} 800 / 16 50.0而 10.0 * 5.0 50.0正確。關(guān)鍵點(diǎn)中間精度與舍入上面的例子直接使用了截?cái)嘤乙苼G棄低位。這引入了截?cái)嗾`差。為了更精確通常采用舍入。最簡(jiǎn)單的舍入是“四舍五入”可以在右移前加上一個(gè)舍入因子。// 舍入到最近的數(shù)加上 1 (shift-1) int shift 4; int32_t rounded_product product_raw (1 (shift - 1)); // 加 2^(shift-1) result_q8_4 (int16_t)(rounded_product shift);乘法是溢出的高發(fā)區(qū)。即使單個(gè)操作數(shù)不溢出乘積很可能溢出中間變量的范圍。務(wù)必使用足夠大的數(shù)據(jù)類型如int32_t來(lái)存放兩個(gè)int16_t的乘積。3.3 除法最復(fù)雜的運(yùn)算定點(diǎn)數(shù)的除法是最棘手的因?yàn)檎麛?shù)除法本身就會(huì)丟失小數(shù)部分。我們需要通過(guò)技巧來(lái)保留精度。核心思想將被除數(shù)“放大”后再進(jìn)行整數(shù)除法。 假設(shè)計(jì)算A / B兩者格式均為Qm.n。為了在結(jié)果中保留k位小數(shù)我們可以先將被除數(shù)A算術(shù)左移k位。這相當(dāng)于將其乘以2^k。然后對(duì)這個(gè)放大后的被除數(shù)與除數(shù)B做整數(shù)除法。得到的結(jié)果其縮放因子可以理解為2^{-k}。更通用的方法是如果我們希望結(jié)果格式為Qm_res.n_res可以// A, B 都是 Q8.4 希望得到 Q8.4 的結(jié)果 int16_t a_q8_4 160; // 10.0 int16_t b_q8_4 80; // 5.0 期望結(jié)果 2.0 // 方法將被除數(shù)提升到更高精度再做除法 int32_t dividend (int32_t)a_q8_4 8; // 左移8位放大以便保留小數(shù)。160 8 40960 int32_t divisor b_q8_4; // 80 int32_t raw_result dividend / divisor; // 40960 / 80 512 // 現(xiàn)在 raw_result 的縮放因子是 2^{-(48)}? 需要分析。 // 原始 A: 值 a_int * 2^{-4} // 左移8位后: 值 (a_int * 2^8) * 2^{-4} a_int * 2^{4} // 除以 b_int 后: 值 (a_int / b_int) * 2^{4} // 而我們需要的是 (a_int * 2^{-4}) / (b_int * 2^{-4}) (a_int / b_int) * 2^{0} // 所以需要將 raw_result 右移4位以抵消之前多乘的 2^4。 int16_t result_q8_4 (int16_t)(raw_result 4); // 512 4 32 // 驗(yàn)證32 * 2^{-4} 32 / 16 2.0正確。除法避坑指南除零檢查必須進(jìn)行否則程序會(huì)崩潰。精度與移位量的權(quán)衡左移的位數(shù)k決定了除法運(yùn)算的精度但同時(shí)也可能引起被除數(shù)的溢出。需要根據(jù)數(shù)據(jù)范圍謹(jǐn)慎選擇。使用查找表或近似算法在性能關(guān)鍵的場(chǎng)合對(duì)于固定的除數(shù)可以考慮使用預(yù)先計(jì)算好的倒數(shù)乘法表將除法轉(zhuǎn)換為乘法速度極快。3.4 溢出保護(hù)與飽和處理溢出是定點(diǎn)數(shù)運(yùn)算的常態(tài)。處理溢出有兩種主要策略使用更大位寬在中間計(jì)算步驟中使用int32_t甚至int64_t來(lái)存放int16_t的運(yùn)算結(jié)果最后再縮放到目標(biāo)格式。這是最常用、最安全的方法。飽和運(yùn)算Saturation當(dāng)結(jié)果超出目標(biāo)格式能表示的范圍時(shí)不再簡(jiǎn)單地截?cái)喔呶贿@會(huì)導(dǎo)致正數(shù)變負(fù)數(shù)的環(huán)繞而是將其“鉗制”在該格式的最大值或最小值上。例如對(duì)于有符號(hào)Q7.8范圍約-128~128如果運(yùn)算結(jié)果是150則飽和處理會(huì)將其設(shè)置為最大值127.996。許多DSP指令集和硬件模塊直接提供了飽和加法/乘法指令。在軟件中實(shí)現(xiàn)需要額外的比較和賦值操作。4. 定點(diǎn)數(shù)的實(shí)戰(zhàn)應(yīng)用與格式設(shè)計(jì)理論懂了怎么用到實(shí)際項(xiàng)目中關(guān)鍵在于格式設(shè)計(jì)。4.1 設(shè)計(jì)流程以PID控制器為例假設(shè)我們要為一個(gè)直流電機(jī)速度環(huán)實(shí)現(xiàn)一個(gè)數(shù)字PID控制器。輸出是PWM占空比范圍0-100%。輸入是速度誤差單位轉(zhuǎn)/分鐘。分析物理量范圍速度誤差假設(shè)最大為 ±1000 RPM。積分項(xiàng)需要累積誤差假設(shè)最大累積值為 ±5000 RPM*秒這是一個(gè)估計(jì)值。輸出PWM0.0 到 1.0。比例系數(shù)Kp、積分時(shí)間Ti、微分時(shí)間Td通過(guò)調(diào)試確定假設(shè)Kp0.5 KiKp/Ti0.1 KdKp*Td0.02。選擇定點(diǎn)數(shù)格式核心原則在保證不溢出的前提下盡可能提高精度即給小數(shù)部分更多位數(shù)。我們選擇16位有符號(hào)整數(shù)int16_t作為存儲(chǔ)類型總位寬16。對(duì)于誤差e范圍±1000。我們需要整數(shù)部分能容納1000。2^101024所以至少需要10位整數(shù)位加上符號(hào)位1位共11位。剩下16-115位給小數(shù)。我們可以選擇Q11.5。縮放因子2^{-5}1/32。那么1000 RPM 對(duì)應(yīng)的整數(shù)值為 1000 * 32 32000在int16_t范圍-32768~32767內(nèi)安全。對(duì)于積分項(xiàng)integral范圍±5000。5000*32160000這已經(jīng)超出int16_t范圍。因此積分項(xiàng)必須用更寬的格式比如Q16.16int32_t?;蛘呶覀兛梢詫?duì)積分項(xiàng)使用不同的縮放因子例如Q19.13也是int32_t使其范圍更大。對(duì)于系數(shù)Kp, Ki, Kd它們都是小于1的小數(shù)精度很重要。我們可以用Q1.15格式即常見的Q15格式。這是16位有符號(hào)數(shù)1位符號(hào)0位整數(shù)15位小數(shù)。它能表示-1到接近1之間的高精度小數(shù)。0.5表示為 0.5 * 2^{15} 16384。對(duì)于輸出output范圍0.0~1.0。可以用UQ1.15無(wú)符號(hào)Q15或Q2.14。確定運(yùn)算順序與中間格式P項(xiàng) Kp * eQ1.15 * Q11.5。結(jié)果小數(shù)位15520位。需要用int32_t中間變量存放。然后根據(jù)輸出格式進(jìn)行定標(biāo)。I項(xiàng) Ki * e * dtQ1.15 * Q11.5 * Qx.y。dt是采樣時(shí)間也是一個(gè)定點(diǎn)數(shù)。需要仔細(xì)設(shè)計(jì)各環(huán)節(jié)格式避免溢出和精度損失。最終各項(xiàng)求和得到輸出并限制在0-1.0之間飽和處理。4.2 常見問(wèn)題與排查技巧實(shí)錄在實(shí)際編碼和調(diào)試定點(diǎn)數(shù)算法時(shí)以下是我踩過(guò)坑后總結(jié)出的經(jīng)驗(yàn)問(wèn)題1結(jié)果出現(xiàn)不正確的巨大數(shù)值或正負(fù)號(hào)翻轉(zhuǎn)。排查這是典型的溢出跡象。首先檢查所有乘法操作是否使用了足夠位寬的中間變量例如兩個(gè)int16_t相乘結(jié)果必須用int32_t接收。其次檢查加法鏈特別是積分項(xiàng)的累積是否可能超出變量范圍。技巧在關(guān)鍵運(yùn)算步驟后添加調(diào)試代碼打印出存儲(chǔ)的整數(shù)值和其對(duì)應(yīng)的物理值。對(duì)比理論物理值看是否匹配。問(wèn)題2系統(tǒng)控制精度不夠存在穩(wěn)態(tài)誤差或抖動(dòng)。排查可能是精度小數(shù)位數(shù)不足或舍入誤差累積導(dǎo)致。例如積分項(xiàng)Ki非常小用Q1.15表示時(shí)其整數(shù)值可能只有個(gè)位數(shù)乘以誤差后右移舍入時(shí)很容易被舍掉導(dǎo)致積分作用太弱。技巧提升關(guān)鍵路徑上數(shù)據(jù)的精度。例如將誤差e、積分項(xiàng)、系數(shù)全部提升到Q16.1632位進(jìn)行計(jì)算僅在最終輸出時(shí)降為16位?;蛘邔?duì)積分項(xiàng)使用“累加器”模式用高精度如32位變量累積只在用于計(jì)算輸出時(shí)才取部分高位。問(wèn)題3不同平臺(tái)或編譯器下運(yùn)算結(jié)果有微小差異。排查雖然定點(diǎn)數(shù)本身是確定的但移位運(yùn)算和除法運(yùn)算在不同編譯器下的行為可能有細(xì)微差別如對(duì)于有符號(hào)負(fù)數(shù)的右移是算術(shù)右移還是邏輯右移C語(yǔ)言標(biāo)準(zhǔn)未完全定義。除法的舍入方式也可能不同。技巧使用明確的、可移植的代碼。對(duì)于有符號(hào)數(shù)的右移如果希望是算術(shù)右移保留符號(hào)位使用運(yùn)算符在大多數(shù)平臺(tái)上是安全的但為了絕對(duì)明確可以使用條件判斷。或者使用編譯器內(nèi)置的、明確語(yǔ)義的函數(shù)或宏。對(duì)于除法舍入自己實(shí)現(xiàn)明確的舍入函數(shù)如四舍五入、向零取整等。問(wèn)題4調(diào)試時(shí)看著一堆十六進(jìn)制或十進(jìn)制整數(shù)無(wú)法直觀理解。技巧編寫簡(jiǎn)單的轉(zhuǎn)換函數(shù)或宏方便在整數(shù)值和物理值之間切換。// 定義格式 #define Q16_16_SCALE (1 16) // 2^16 typedef int32_t q16_16_t; // 從物理值轉(zhuǎn)換到定點(diǎn)數(shù) static inline q16_16_t float_to_q16_16(float f) { return (q16_16_t)(f * Q16_16_SCALE); } // 從定點(diǎn)數(shù)轉(zhuǎn)換到物理值 static inline float q16_16_to_float(q16_16_t q) { return (float)q / Q16_16_SCALE; } // 在調(diào)試打印時(shí)非常有用 printf(Error: phys%f, fixed0x%08lx\n, q16_16_to_float(error_fixed), error_fixed);5. 進(jìn)階話題定點(diǎn)數(shù)數(shù)學(xué)庫(kù)與優(yōu)化當(dāng)項(xiàng)目中的定點(diǎn)數(shù)運(yùn)算變得復(fù)雜時(shí)手動(dòng)管理格式和移位會(huì)非常繁瑣且易錯(cuò)。這時(shí)可以考慮以下方案5.1 使用定點(diǎn)數(shù)數(shù)學(xué)庫(kù)成熟的嵌入式或DSP社區(qū)通常有優(yōu)秀的定點(diǎn)數(shù)數(shù)學(xué)庫(kù)它們提供了經(jīng)過(guò)高度優(yōu)化的、類型安全的定點(diǎn)數(shù)運(yùn)算。例如ARM CMSIS-DSP提供了豐富的q7_t,q15_t,q31_t數(shù)據(jù)類型的函數(shù)對(duì)應(yīng)不同的Q格式如Q7, Q15, Q31并充分利用了ARM Cortex-M系列處理器的SIMD和飽和運(yùn)算指令。libfixmath: 一個(gè)開源的定點(diǎn)數(shù)數(shù)學(xué)庫(kù)實(shí)現(xiàn)了三角函數(shù)、指數(shù)、對(duì)數(shù)等復(fù)雜函數(shù)。自己封裝對(duì)于特定項(xiàng)目可以封裝一個(gè)簡(jiǎn)單的定點(diǎn)數(shù)類或結(jié)構(gòu)體重載運(yùn)算符讓代碼看起來(lái)像浮點(diǎn)數(shù)一樣簡(jiǎn)潔但底層是定點(diǎn)運(yùn)算。5.2 硬件加速與指令集現(xiàn)代許多微控制器和DSP都帶有助力定點(diǎn)數(shù)運(yùn)算的硬件特性飽和指令如ARM的QADD,QSUB。乘加指令MAC單周期內(nèi)完成乘法并累加是濾波器、矩陣運(yùn)算的核心。桶形移位器與ALU并行可以在數(shù)據(jù)進(jìn)入運(yùn)算單元前或后快速完成移位定標(biāo)。單指令多數(shù)據(jù)SIMD允許同時(shí)對(duì)多個(gè)定點(diǎn)數(shù)進(jìn)行相同操作大幅提升吞吐量。在編寫性能關(guān)鍵代碼時(shí)查閱芯片的數(shù)據(jù)手冊(cè)和編譯器 intrinsics內(nèi)聯(lián)函數(shù)直接調(diào)用這些硬件指令可以獲得數(shù)量級(jí)的性能提升。5.3 動(dòng)態(tài)定點(diǎn)數(shù)在某些場(chǎng)景下數(shù)據(jù)的動(dòng)態(tài)范圍可能變化很大靜態(tài)的定點(diǎn)數(shù)格式難以兼顧。這時(shí)可以考慮動(dòng)態(tài)定點(diǎn)數(shù)。其基本思想是存儲(chǔ)一個(gè)整數(shù)和一個(gè)獨(dú)立的、可變的指數(shù)縮放因子。運(yùn)算時(shí)需要?jiǎng)討B(tài)調(diào)整操作數(shù)的指數(shù)使其對(duì)齊。這有點(diǎn)像簡(jiǎn)化版的浮點(diǎn)數(shù)但通常指數(shù)變化范圍小且運(yùn)算規(guī)則可以自定義以優(yōu)化性能。它比純定點(diǎn)數(shù)復(fù)雜但比標(biāo)準(zhǔn)浮點(diǎn)數(shù)高效是兩者之間的一個(gè)折中方案。我個(gè)人在音頻處理項(xiàng)目中就曾大量使用定點(diǎn)數(shù)。將一個(gè)浮點(diǎn)版本的音頻濾波器如二階IIR移植到定點(diǎn)數(shù)實(shí)現(xiàn)最初被溢出和精度問(wèn)題折磨得不輕。后來(lái)嚴(yán)格遵循了“分析范圍-設(shè)計(jì)格式-寬中間變量-飽和處理”這個(gè)流程并用宏和調(diào)試函數(shù)武裝自己才讓代碼穩(wěn)定下來(lái)。實(shí)測(cè)在一顆沒(méi)有FPU的100MHz Cortex-M4內(nèi)核上定點(diǎn)數(shù)版本的濾波器比軟件浮點(diǎn)版本快了近20倍并且功耗顯著降低。這種將控制權(quán)從硬件交還給程序員的感覺雖然增加了前期設(shè)計(jì)負(fù)擔(dān)但帶來(lái)的性能收益和確定性是浮點(diǎn)數(shù)無(wú)法比擬的。當(dāng)你需要從系統(tǒng)中榨取最后一滴性能或者確保不同設(shè)備上算法行為完全一致時(shí)定點(diǎn)數(shù)是你必須掌握并信賴的工具。