部參考電壓校準:實現(xiàn)高精度電池電壓測量的原理與實踐)
1. 項目緣起為什么需要測量電池電壓在嵌入式開發(fā)尤其是基于電池供電的便攜設備或物聯(lián)網(wǎng)節(jié)點項目中準確監(jiān)測電池電壓是一個基礎(chǔ)且關(guān)鍵的需求。無論是為了在電量過低時及時告警、實現(xiàn)低功耗休眠還是為了進行電量百分比估算第一步都是要獲取一個可靠的電壓讀數(shù)。STM32作為業(yè)界廣泛使用的微控制器其內(nèi)置的模數(shù)轉(zhuǎn)換器ADC為我們提供了將模擬電壓轉(zhuǎn)換為數(shù)字值的直接手段。然而一個初學者常踩的坑是直接讀取ADC轉(zhuǎn)換值然后簡單地乘以一個理論上的參考電壓比如3.3V就認為是輸入引腳上的電壓。這種方法在實驗室用穩(wěn)壓電源供電時可能問題不大但一旦設備由電池供電情況就變了。電池電壓會隨著放電而下降而為STM32芯片供電的電壓VDD正是這個變化的電池電壓或經(jīng)過LDO穩(wěn)壓后的電壓。ADC的參考電壓通常與VDD相連這意味著參考電壓本身是浮動的。你用著一個變化的“尺子”去測量外部電壓得到的數(shù)字自然不準。這就是為什么我們需要一個穩(wěn)定的、已知的“基準尺”——參考電壓。STM32很多型號都內(nèi)置了一個高精度的內(nèi)部參考電壓源比如1.2V或1.22V它不隨VDD變化正是解決這個問題的鑰匙。最近在幾個低功耗傳感器節(jié)點的項目里我都用到了這個方案。從鋰亞電池供電的遠程儀表到紐扣電池供電的藍牙標簽精準的電壓監(jiān)測是延長續(xù)航、保證數(shù)據(jù)可靠回傳的前提。網(wǎng)上很多教程只講了“怎么讀ADC值”卻很少深入說清楚“為什么讀不準”以及“如何校準得準”這正是本文想掰開揉碎講明白的。2. 核心原理ADC、參考電壓與測量鏈路剖析要理解整個方案我們需要把ADC測量鏈路上的幾個關(guān)鍵角色和它們之間的關(guān)系理清楚。這就像一套測量系統(tǒng)每個環(huán)節(jié)的誤差都會影響最終結(jié)果。2.1 ADC轉(zhuǎn)換的基本公式STM32的ADC是一個12位精度的轉(zhuǎn)換器部分型號有16位模式它將輸入引腳ADCx_INy上的模擬電壓V_in與參考電壓V_ref進行比較輸出一個數(shù)字值A(chǔ)DC_Data。其理想轉(zhuǎn)換公式為ADC_Data (V_in / V_ref) * (2^n - 1)其中n是ADC的分辨率對于12位ADC2^n - 1 4095。所以公式簡化為V_in (ADC_Data / 4095) * V_ref這里就是第一個關(guān)鍵點公式里的V_ref是什么在很多單片機中V_ref就是供電電壓VDD。在STM32中V_ref通常對應VDDA引腳上的電壓模擬供電。在默認情況下VDDA和VDD是連在一起的。所以當電池電壓從4.2V滿電降到3.0V欠壓時V_ref(VDDA) 也從4.2V降到了3.0V。如果你在代碼里硬編碼V_ref 3.3V那么計算出的V_in將產(chǎn)生巨大誤差。2.2 內(nèi)部參考電壓 (VREFINT) 的角色STM32芯片內(nèi)部集成了一個帶隙基準電壓源其輸出電壓被標記為VREFINT。這個電壓值非常穩(wěn)定典型值為1.20V或1.22V具體值在芯片數(shù)據(jù)手冊的電氣特性章節(jié)給出并且每個芯片在出廠時都在特定條件下如VDDA3.3V, 溫度25°C測量并校準校準值存放在芯片的系統(tǒng)存儲區(qū)System Memory中通常是一個12位的數(shù)值對應VDDA3.3V時測量VREFINT通道得到的ADC原始值。它的核心作用不是直接作為測量外部信號的參考電壓而是作為一個已知的、穩(wěn)定的“標尺”來反向推算出當前實際的VDDA電壓。我們可以通過ADC去測量這個內(nèi)部的VREFINT電壓STM32有專門的內(nèi)部通道連接它例如在STM32F1系列中是通道17得到一個原始值A(chǔ)DC_Data_RefInt。由于VREFINT的電壓是已知的通過出廠校準值推算得出根據(jù)ADC公式我們就可以反推出當前的VDDA實際_VDDA (VREFINT_理論值 * 4095) / ADC_Data_RefInt這里就是第二個關(guān)鍵點VREFINT_理論值如何獲取它不是簡單的1.20V。更準確的做法是使用芯片的出廠校準值。這個校準值記為VREFINT_CAL是在VDDA3.3V且溫度25°C時測量VREFINT通道得到的ADC原始值。所以VREFINT通道此時的電壓實際值為VREFINT_實際 3.3V * (VREFINT_CAL / 4095)這個VREFINT_實際才是你這個芯片在理想條件下的真實內(nèi)部參考電壓值。我們后續(xù)計算就基于它。2.3 完整的測量鏈路與計算步驟理解了以上兩點我們就可以梳理出測量外部電池電壓假設通過分壓電阻連接到ADC引腳的完整鏈路測量內(nèi)部參考電壓通道使能ADC讀取連接VREFINT的內(nèi)部通道得到原始值A(chǔ)DC_RefInt。計算當前實際VDDA從芯片特定地址例如STM32F103是0x1FFFF7BA讀取出廠校準值VREFINT_CAL。計算VREFINT的實際電壓值VREFINT 3.3 * (VREFINT_CAL / 4095)。計算當前VDDAVDDA_實際 VREFINT * 4095 / ADC_RefInt。測量外部電池電壓通道讀取連接電池分壓電路的ADC引腳得到原始值A(chǔ)DC_Batt。計算外部輸入引腳電壓V_in_pin (ADC_Batt / 4095) * VDDA_實際。計算真實電池電壓如果使用了分壓電阻比如R1上拉R2下拉則電池電壓V_batt V_in_pin * ( (R1 R2) / R2 )。通過這個流程我們巧妙地將一個變化的VDDA參考電壓通過一個穩(wěn)定的內(nèi)部基準VREFINT給“標定”了出來從而消除了供電電壓波動對ADC讀數(shù)的影響。注意VREFINT本身也有微小的溫漂但對于大多數(shù)電池監(jiān)測場景溫度變化不劇烈精度要求通常在±0.1V以內(nèi)來說其影響可以接受。如果對精度要求極高則需要查閱數(shù)據(jù)手冊中的溫漂曲線并進行軟件補償。3. 硬件設計分壓電路與抗干擾要點原理清楚了硬件是實現(xiàn)準確測量的基礎(chǔ)。這里主要涉及分壓電阻的計算和PCB布局的注意事項。3.1 分壓電阻的選型與計算電池電壓如鋰電池滿電4.2V通常高于ADC引腳允許的最大輸入電壓通常為VDDA多為3.3V或3.6V。因此必須使用電阻分壓。這不是簡單地把電壓降下來就行需要考慮多個因素。計算分壓比假設VDDA 3.3V我們希望電池電壓在最高時4.2V分壓后的電壓略小于3.3V留有一定余量??梢栽O定目標分壓后電壓為3.0V。則分壓比K V_out / V_batt_max 3.0 / 4.2 ≈ 0.714。根據(jù)公式V_out V_batt * (R2 / (R1 R2))可得R2 / (R1 R2) 0.714。選取R2為10kΩ則可計算出R1約為4kΩ。為了使用標準阻值可以選擇R13.9kΩ R210kΩ此時分壓比約為0.719滿電時分壓電壓約為3.02V是安全的。電阻精度與功耗權(quán)衡精度應選擇1%精度的薄膜電阻以保證分壓比準確。5%精度的碳膜電阻會引入不可忽視的系統(tǒng)誤差。阻值大小阻值不宜過小否則分壓電路從電池消耗的靜態(tài)電流過大。例如若R1R23.9k10k13.9k接在4.2V電池上靜態(tài)電流I 4.2V / 13.9kΩ ≈ 0.3mA。對于常年待機的設備這個電流可能偏大??梢钥紤]將總阻值提高到100kΩ以上如R168k, R2100k此時靜態(tài)電流約24μA顯著降低。但阻值過大又會帶來新的問題。高阻值帶來的挑戰(zhàn)ADC采樣時間ADC輸入端可以等效為一個采樣電容C_S需要通過分壓電阻對其充電。根據(jù)RC充電公式時間常數(shù)τ R_source * C_S。這里的R_source就是分壓網(wǎng)絡的輸出阻抗等于R1與R2的并聯(lián)值R1//R2。阻值越大充電到穩(wěn)定所需的時間越長。如果ADC的采樣時間設置過短采樣電容未充滿電就會導致讀數(shù)偏低且不穩(wěn)定。噪聲敏感性高阻抗節(jié)點更容易拾取板上的數(shù)字噪聲導致ADC讀數(shù)跳動。解決方案增加濾波電容在分壓點ADC輸入引腳到地之間并聯(lián)一個100nF~1μF的陶瓷電容C1。這個電容與分壓電阻的并聯(lián)值構(gòu)成低通濾波器可以濾除高頻噪聲更重要的是它為ADC采樣瞬間提供了電荷池減小了對前端電阻驅(qū)動能力的要求。調(diào)整ADC采樣時間在STM32的ADC配置中增加對應通道的采樣周期SAMPLETIME。對于高阻抗源可能需要設置到最大的幾百個ADC時鐘周期確保采樣充分。一個推薦的電路參數(shù)對于大多數(shù)3.3V系統(tǒng)測量單節(jié)鋰電池3.0V-4.2V我常用的組合是R1100kΩ,R2220kΩ分壓比約0.6875滿電輸入ADC的電壓約為2.89V。在分壓點接一個100nF的電容到地。ADC采樣時間設置為239.5個周期STM32CubeMX中可配置。這個組合在靜態(tài)電流約10μA、精度和穩(wěn)定性之間取得了很好的平衡。3.2 PCB布局與接地考量模擬測量對噪聲敏感PCB布局不當會毀掉所有軟件努力。模擬與數(shù)字分離確保為模擬部分VDDA, VSSA, ADC輸入走線提供干凈的電源和地。如果板上有開關(guān)電源DCDC其噪聲很大務必用電感或磁珠將模擬電源隔離出來。分壓電阻靠近MCU分壓電阻和濾波電容應盡可能靠近STM32的ADC輸入引腳放置引線要短減少天線效應。正確處理VDDA/VSSA即使不使用外部精密基準VDDA和VSSA也必須正確連接。通常VDDA應連接到與VDD相同的電源但通過一個磁珠或小電阻如0Ω進行隔離濾波。VSSA必須直接連接到干凈的模擬地平面并且是VSS的單一連接點。避免數(shù)字信號線穿越不要讓高頻數(shù)字信號線如時鐘、SPI、PWM從分壓電路或ADC引腳下方或附近穿過。4. 軟件實現(xiàn)從寄存器配置到校準計算硬件準備妥當后我們進入軟件環(huán)節(jié)。這里以STM32的HAL庫為例展示關(guān)鍵步驟和代碼。雖然不同系列STM32的ADC外設和庫函數(shù)略有差異但核心流程和思想是通用的。4.1 ADC初始化與通道配置首先我們需要初始化ADC并配置兩個通道一個是測量電池電壓的外部通道假設是ADC1_IN1另一個是測量內(nèi)部參考電壓的通道ADC1_IN17或?qū)ǖ馈?/ 1. ADC句柄與初始化結(jié)構(gòu)體 ADC_HandleTypeDef hadc1; ADC_ChannelConfTypeDef sConfig {0}; // 2. 基礎(chǔ)ADC初始化 hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_DISABLE; // 非掃描模式單通道 hadc1.Init.ContinuousConvMode DISABLE; // 單次轉(zhuǎn)換 hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; // 軟件觸發(fā) hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; // 數(shù)據(jù)右對齊 hadc1.Init.NbrOfConversion 1; // 轉(zhuǎn)換序列數(shù)為1 if (HAL_ADC_Init(hadc1) ! HAL_OK) { Error_Handler(); } // 3. 配置內(nèi)部參考電壓通道 (VREFINT) sConfig.Channel ADC_CHANNEL_VREFINT; // 內(nèi)部通道 sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; // 使用長采樣時間確保穩(wěn)定 if (HAL_ADC_ConfigChannel(hadc1, sConfig) ! HAL_OK) { Error_Handler(); }這里將采樣時間設置為最大值因為內(nèi)部參考電壓通道的驅(qū)動能力不強需要足夠的時間采樣。4.2 讀取內(nèi)部參考電壓并計算實際VDDA這是最核心的校準步驟。我們需要先讀取VREFINT的出廠校準值然后測量當前的VREFINTADC值。// 定義相關(guān)變量 #define VREFINT_CAL_ADDR ((uint16_t*) (0x1FFFF7BA)) // F103的校準地址其他型號需查手冊 #define VDDA_APPLI (3.3f) // 出廠校準時的VDDA電壓通常是3.3V float read_actual_vdda(void) { uint32_t vrefint_cal_value *VREFINT_CAL_ADDR; // 讀取出廠校準值 uint32_t adc_raw_vrefint 0; float vdda_actual 0.0f; // 確保ADC當前配置為VREFINT通道 // ... (可以重新配置通道或使用多通道掃描。這里為清晰假設已配置) HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) { adc_raw_vrefint HAL_ADC_GetValue(hadc1); } HAL_ADC_Stop(hadc1); // 核心計算公式 // 1. 計算出廠校準時的VREFINT實際電壓 float vrefint_voltage VDDA_APPLI * ((float)vrefint_cal_value / 4095.0f); // 2. 用當前測得的ADC值反推當前VDDA vdda_actual vrefint_voltage * 4095.0f / (float)adc_raw_vrefint; return vdda_actual; // 返回計算得到的實際VDDA電壓 }注意VREFINT_CAL_ADDR這個地址因STM32系列和具體型號而異。例如STM32F4系列可能在0x1FFF7A2A。務必在對應芯片的參考手冊Reference Manual或數(shù)據(jù)手冊Datasheet中查找“ADC calibration values”或“System memory”章節(jié)確認。4.3 測量外部電池電壓并換算計算出VDDA_實際后我們就可以用它作為參考電壓去測量外部通道了。float read_battery_voltage(float vdda_actual) { uint32_t adc_raw_batt 0; float vin_pin 0.0f; float vbatt 0.0f; const float R1 100000.0f; // 分壓電阻上臂100kΩ const float R2 220000.0f; // 分壓電阻下臂220kΩ const float DIVIDER_RATIO (R1 R2) / R2; // 分壓系數(shù) // 重新配置ADC通道為外部電池輸入通道 (例如 ADC_CHANNEL_1) ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel ADC_CHANNEL_1; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; // 根據(jù)分壓電阻阻抗設置 HAL_ADC_ConfigChannel(hadc1, sConfig); HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) { adc_raw_batt HAL_ADC_GetValue(hadc1); } HAL_ADC_Stop(hadc1); // 計算ADC輸入引腳電壓 vin_pin ((float)adc_raw_batt / 4095.0f) * vdda_actual; // 計算真實電池電壓 vbatt vin_pin * DIVIDER_RATIO; return vbatt; }在實際應用中為了減少偶然誤差通常會對VREFINT通道和電池電壓通道進行多次采樣取平均。例如連續(xù)采樣16次或32次去掉最大最小值后求平均。4.4 軟件濾波與數(shù)據(jù)處理ADC讀數(shù)難免會有噪聲尤其是環(huán)境復雜的應用中。簡單的多次平均濾波均值濾波可以有效平滑數(shù)據(jù)#define SAMPLE_TIMES 32 uint32_t adc_average_sample(ADC_HandleTypeDef* hadc) { uint32_t sum 0; for (int i 0; i SAMPLE_TIMES; i) { HAL_ADC_Start(hadc); HAL_ADC_PollForConversion(hadc, 10); sum HAL_ADC_GetValue(hadc); HAL_ADC_Stop(hadc); // 可以添加少量延時避免ADC過熱或干擾 HAL_Delay(1); } return (sum SAMPLE_TIMES/2) / SAMPLE_TIMES; // 四舍五入 }對于電池電壓這種變化緩慢的信號也可以使用一階低通濾波指數(shù)加權(quán)平均代碼更簡單占用內(nèi)存少且能反映變化趨勢float battery_voltage_filtered 0.0f; // 濾波后的值 const float ALPHA 0.1f; // 濾波系數(shù) (0~1)越小越平滑響應越慢 void update_battery_voltage(float new_sample) { battery_voltage_filtered ALPHA * new_sample (1 - ALPHA) * battery_voltage_filtered; }5. 實測調(diào)試與精度提升技巧代碼寫完燒錄進板子得到的電壓值可能和萬用表測量值還有幾十毫伏的差距。別急這是正?,F(xiàn)象我們可以通過系統(tǒng)性的調(diào)試和校準來提升精度。5.1 基礎(chǔ)驗證與常見問題排查驗證VDDA計算是否正常首先用read_actual_vdda()函數(shù)讀出計算出的VDDA_實際。用萬用表實際測量你板子上STM32的VDD/VDDA引腳電壓。兩者應該非常接近比如相差在0.1V以內(nèi)。如果差距巨大比如計算出來是2.5V實際測量是3.3V問題可能出在VREFINT校準值地址錯誤這是最常見的原因。仔細核對芯片型號和參考手冊。VREFINT通道采樣時間不足嘗試將采樣時間設置為最大值。電源噪聲過大用示波器觀察VDDA引腳看是否有大的紋波。確保濾波電容已焊接且容值正確。驗證分壓電路斷開與MCU的連接直接用萬用表測量分壓點的電壓并計算電池電壓看是否與理論值相符。檢查電阻阻值是否與設計一致特別是用了0603、0402等小封裝容易焊接錯誤。檢查ADC讀數(shù)是否穩(wěn)定連續(xù)打印ADC原始值A(chǔ)DC_Batt觀察其跳動范圍。對于12位ADC在穩(wěn)定的電源和信號下跳動應該在±2LSB以內(nèi)。如果跳動很大幾十個LSB檢查PCB布局ADC輸入線是否靠近噪聲源。濾波電容分壓點對地的濾波電容是否焊接容值是否合適建議用X7R/X5R材質(zhì)的陶瓷電容軟件觸發(fā)時機避免在數(shù)字電路如GPIO翻轉(zhuǎn)、串口發(fā)送頻繁動作時進行ADC采樣可以適當延時或選擇系統(tǒng)相對空閑時采樣。5.2 兩點校準法提升絕對精度即使使用了內(nèi)部參考電壓校準由于分壓電阻的精度1%仍有誤差、PCB漏電、ADC本身的增益誤差和偏移誤差等因素最終換算出的電池電壓可能與萬用表測量值存在一個固定的比例偏差或固定偏移。我們可以通過“兩點校準法”來軟件消除這個系統(tǒng)誤差。校準步驟準備一個可調(diào)精密電源代替電池連接到你的設備分壓電路輸入端。第一點低點校準調(diào)節(jié)電源輸出一個電壓V_actual_low例如3.5V在設備端通過上述代碼讀取并記錄計算出的電壓值V_read_low。第二點高點校準調(diào)節(jié)電源輸出一個電壓V_actual_high例如4.1V記錄設備讀出的V_read_high。在代碼中使用這兩個點來計算一個校準斜率和偏移量// 校準參數(shù)通過實測得到 float calib_scale (V_actual_high - V_actual_low) / (V_read_high - V_read_low); float calib_offset V_actual_low - (V_read_low * calib_scale); // 校準函數(shù) float calibrate_battery_voltage(float raw_voltage) { return (raw_voltage * calib_scale) calib_offset; }將read_battery_voltage()函數(shù)返回的值再送入calibrate_battery_voltage()函數(shù)進行修正。經(jīng)過兩點校準后通??梢詫⒔^對精度提升到±0.5%以內(nèi)完全滿足電池電量監(jiān)測的需求。這兩個校準參數(shù)可以存儲在STM32的Flash中設備上電時讀取。5.3 低功耗場景下的優(yōu)化對于電池供電的設備ADC測量本身也會耗電。需要優(yōu)化間歇性測量不需要實時監(jiān)測電壓時如每小時監(jiān)測一次在測量前才使能ADCHAL_ADC_MspInit中開啟ADC時鐘和GPIO時鐘測量完成后立即關(guān)閉ADC時鐘和對應GPIO時鐘并將ADC輸入引腳設置為模擬輸入模式低功耗??s短采樣時間在滿足精度的前提下盡量使用較短的ADC采樣時間可以減少ADC模塊的工作時間。使用DMA定時器觸發(fā)如果需要周期性高頻率采樣可以配置ADC由定時器觸發(fā)并使用DMA傳輸數(shù)據(jù)。CPU可以在采樣期間進入睡眠模式由DMA完成數(shù)據(jù)搬運采樣完成后再喚醒CPU處理最大化節(jié)省功耗。6. 進階話題從電壓到電量估算準確測量電壓是第一步更高級的需求是估算電池剩余電量State of Charge, SoC。這是一個更復雜的課題因為電池電壓和剩余電量的關(guān)系并非線性且受負載電流、溫度、電池老化等因素影響。對于鋰電池一個簡單實用的方法是查表法在不同剩余電量下100% 90% ... 10% 0%測量電池在典型工作電流下的電壓。這需要電池規(guī)格書或自己實驗獲得。在代碼中建立一個電壓-電量對應表查找表。根據(jù)實測的、帶負載的電池電壓在表中查找最近的兩個點進行線性插值估算當前電量。一個極其重要的提示電池的空載電壓無負載時測得的電壓和帶載電壓設備工作時測得的電壓差異很大。電量估算必須基于帶載電壓。因此你的ADC測量電路應該直接連接在電池兩端經(jīng)過分壓而不是在LDO之后。并且最好在設備處于典型工作狀態(tài)例如無線模塊正在發(fā)射MCU正常運行時進行電壓采樣這時的電壓讀數(shù)對于電量估算才有意義。例如一個簡單的查找表可能如下數(shù)值僅為示例請根據(jù)實際電池測試typedef struct { float voltage; // 帶載電壓 (V) float soc; // 剩余電量百分比 (0.0 ~ 1.0) } VoltageSocPair; VoltageSocPair soc_lookup_table[] { {4.15f, 1.00f}, {3.90f, 0.80f}, {3.75f, 0.50f}, {3.60f, 0.20f}, {3.45f, 0.05f}, {3.30f, 0.00f} // 截止電壓 }; float estimate_soc_from_voltage(float v_batt) { int table_size sizeof(soc_lookup_table) / sizeof(soc_lookup_table[0]); if (v_batt soc_lookup_table[0].voltage) return 1.0f; if (v_batt soc_lookup_table[table_size - 1].voltage) return 0.0f; for (int i 0; i table_size - 1; i) { if (v_batt soc_lookup_table[i].voltage v_batt soc_lookup_table[i 1].voltage) { // 線性插值 float vol_range soc_lookup_table[i].voltage - soc_lookup_table[i 1].voltage; float soc_range soc_lookup_table[i].soc - soc_lookup_table[i 1].soc; float ratio (soc_lookup_table[i].voltage - v_batt) / vol_range; return soc_lookup_table[i].soc - (ratio * soc_range); } } return 0.0f; // 不應執(zhí)行到這里 }實現(xiàn)這個功能后你的設備就具備了基礎(chǔ)的“電量顯示”能力。當然工業(yè)級的電量計Gas Gauge會使用庫侖計電流積分等更復雜的方法精度更高但對于很多成本敏感、功耗要求極高的物聯(lián)網(wǎng)設備基于內(nèi)部參考電壓校準的ADC電壓測量配合一個精心測試的電壓-電量查找表已經(jīng)是一個非??煽壳腋咝У慕鉀Q方案了。