)
簡介面向電子測量與電力監(jiān)控初學者的三相交流電壓表完整程序包涵蓋三相電壓數據采集、同步采樣、有效值/峰值計算、相位分析及過壓、欠壓、失衡等故障檢測的核心功能。程序基于Keil工程組織包含C語言源碼、頭文件、編譯生成的hex/bin/obj/lst文件以及仿真磁盤鏡像共15個文件整體僅46KB便于直接編譯燒錄或二次移植。已有713人學習下載。通過閱讀源碼與工程結構可掌握三相電壓的數字化測量流程、ADC同步采樣策略、真實有效值算法、相序判斷及報警閾值設定方法同時了解數據記錄與存儲、串口/網絡通信等擴展功能的實現思路為開發(fā)工業(yè)電力監(jiān)控、自動化儀表或智能電網終端提供一份可直接借鑒的完整樣板。1. 三相交流電壓表完整程序到底“完整”在哪些地方電工測量里要做三相交流電壓表看起來只是把三根火線接進來、屏幕上顯示三個電壓值但一臺表能不能穩(wěn)定運行、能不能過計量驗收軟件“完整”的程度才是分水嶺。所謂完整程序很少只是一個計算有效值的函數它要覆蓋從ADC采樣、真有效值計算、頻率跟蹤、校準參數存取到顯示刷新和通信協議的全套閉環(huán)。這類壓縮包解壓后通常是一整套按模塊分好的C源碼附帶接線圖和校準確認文件是電測儀表開發(fā)里非常典型、也特別適合改造復用的一份“全套作業(yè)”。如果你是做嵌入式儀表、工控二次開發(fā)或者正在搭畢業(yè)設計里的電測項目這份程序拆解可以幫你少走很多彎路。2. 采樣鏈路設計從380V到ADC引腳參數怎么定才不算錯2.1 為什么首選電壓互感器而不是電阻直接分壓不想把程序燒進板子后發(fā)現顯示值和萬用表差一大截就得先想清楚信號怎么進MCU。三相四線系統(tǒng)的線電壓是380V、相電壓是220V而MCU的ADC輸入范圍通常只有0~3.3V第一個問題是“怎么把220V降到ADC量程內”。常見做法有三種電阻直接分壓、電壓互感器取樣、霍爾電壓傳感器取樣。對電測儀表來說我不太建議電阻直接分壓有一條是硬性原因——安全絕緣。直接分壓意味著低壓側和高壓側共地一旦隔離失效儀表外殼會帶電同時開關柜里的共模干擾會順著地線進入ADC采樣值里混入大量毛刺軟件怎么濾波都干凈不了。常見做法是用1:1的精密微型電壓互感器把一次側電流限制在1~2mA二次側并聯采樣電阻取電壓再送到運放調理進ADC?;魻栯妷簜鞲衅骶群蛶挾疾诲e但成本高一個量級通常只在需要做諧波分析的儀表里用。三種方案對比下來電壓互感器在精度、成本、隔離安全性上最均衡方案隔離方式精度成本適用場景電阻直接分壓無隔離依賴電阻精度最低低壓實驗板不建議產品用1:1精密電壓互感器磁隔離0.2%可達中主流三相電壓表方案霍爾電壓傳感器磁隔離高帶寬大高諧波分析、寬頻測量2.2 一套常用參數表限流電阻、采樣電阻與放大倍數我一般按這個思路推參數輸入相電壓有效值220V互感器一次側電流取1mA那么限流電阻R限 220V / 1mA 220kΩ。實際選型時常用兩顆100kΩ、0.5W的金屬膜電阻串聯既把耐壓分攤開又留足功率余量。二次側并聯采樣電阻R2決定互感器輸出電壓例如取200Ω二次電流1mA產生0.2Vrms峰值約0.566V離ADC滿量程還很遠于是再接一級運放放大6倍左右使信號峰值接近3.3V把12位ADC的分辨率盡量用滿。參數推薦值作用限流電阻兩顆100kΩ串聯0.5W金屬膜限制互感器一次電流≈1mA分攤耐壓互感器1:1精密電壓互感器隔離高壓側與低壓側采樣電阻200Ω0.1%精度二次電流轉換為0.2Vrms電壓運放增益6倍左右反相放大加偏置把信號幅值抬到接近ADC滿量程ADC參考3.3V外部基準更穩(wěn)決定滿量程電壓和分辨率如果你想做0.5級以上的儀表外部基準基本是必選項。內部參考電壓溫漂大很容易造成讀數隨溫度慢慢漂移這在電測儀表里屬于硬傷后面排查章節(jié)會專門講。運放部分建議用低失調、單電源供電的型號輸出端串一顆200Ω電阻再并兩顆背靠背的鉗位二極管到電源軌防止誤接高壓時打壞ADC引腳。這個鉗位細節(jié)很多參考設計里沒有但現場操作一旦接錯線它能救回一整塊板子。2.3 偏置與中點電壓單電源運放處理交流信號的標配運放用3.3V單電源供電時交流負半周無法被ADC采集必須給信號加上直流偏置把零點抬到ADC量程的中點。常見做法是把“信號地”接到約1.65V用一個電壓跟隨器穩(wěn)定這個中點電壓互感器二次信號的零軸就等于1.65VADC碼的“零點”大約在2048附近。這塊電路決定了后面所有軟件算法的基準如果硬件上中點電壓不穩(wěn)軟件里再怎么校準都是治標不治本。ADC初始化對應的骨架代碼如下/* ADC初始化骨架三相電壓表三通道輪詢采樣 */ void adc_init(void) { // 1. 配置三個GPIO為模擬輸入分別接入三相調理電路輸出 gpio_analog_init(); // 2. ADC配置12位、右對齊采樣時間盡量放寬 adc_channel_setup(ADC_CH0, ADC_SAMPLE_TIME_MAX); adc_channel_setup(ADC_CH1, ADC_SAMPLE_TIME_MAX); adc_channel_setup(ADC_CH2, ADC_SAMPLE_TIME_MAX); // 3. 用定時器更新事件觸發(fā)ADC采樣率10kHz adc_set_trigger(ADC_TRIGGER_TIMER2_UPDATE); timer_set_prescaler(72); // 假設主頻72MHz分頻到1MHz timer_set_period(100); // 1MHz / 100 10kHz timer_enable(); }這里有兩個參數值得說。采樣時間開到最大是為了降低信號源輸出阻抗帶來的建立誤差調理電路輸出端有鉗位電阻和分布電容采樣瞬間會拉低一點電壓采樣時間越長誤差越小。采樣率10kHz對50Hz工頻意味著每周波200個點對純有效值測量非常寬裕RMS積分精度足夠如果以后要擴展諧波分析10k采樣率也勉強能覆蓋到50次諧波但那時就要換同步采樣方案了。2.4 三相共用一路ADC分時采樣夠不夠用很多“完整程序”不加同步采樣芯片原因是三相電壓表的計量對象是有效值。穩(wěn)態(tài)交流下每個通道分時采樣幾十毫秒對RMS計算精度幾乎沒有影響只要保證每個通道都采到整數個工頻周期就行。但如果工程以后要擴展成功率表、相位角測量就一定要上同步采樣因為三相同時刻的瞬時值才有意義。這個邊界想清楚硬件成本能省下一大塊而且不會影響當前產品的精度指標。3. 程序核心代碼測頻、整周期截取與有效值計算的落地寫法3.1 工程模塊怎么拆五個模塊的職責邊界拿到一套完整程序先別急著從頭讀按模塊拆開看效率最高。我習慣把三相電壓表工程分成五塊初始化、采樣、計算、校準存儲、顯示通信。每塊之間只留幾個接口函數主循環(huán)只做調度不摻和具體邏輯。模塊職責對外接口init時鐘、GPIO、ADC、定時器、串口sys_init()capture定時器中斷搬運ADC數據進緩沖區(qū)capture_start()calc過零測頻、整周期截取、RMS計算calc_three_voltage()calib校準參數存取、校驗cal_save(), cal_load()ui數碼管/LCD刷新、按鍵處理ui_update()主循環(huán)的骨架可以寫成這樣void main_loop(void) { while (1) { if (g_frame_ready) { // 采樣滿一輪由定時器中斷置1 calc_three_voltage(); // 計算三相有效值ADC碼 cal_apply_to_display(); // 套用校準系數得到顯示電壓 ui_update(); // 刷新顯示和通信寄存器 g_frame_ready 0; // 清除標志進入下一輪 } } }這段代碼的核心思想是主循環(huán)永遠只在“數據完整”的時候才處理中斷置標志主循環(huán)消費標志。這樣即使顯示或通信函數偶爾卡一下也不會破壞采樣的連續(xù)性。3.2 中斷只搬數據不在中斷里算電壓有一類典型的翻車程序把RMS計算寫進定時器中斷中斷服務函數執(zhí)行時間變長下次采樣間隔被拉大波形點數分布不均計算結果也跟著抖動。正確做法是中斷里只做“搬數據”這一件事浮點運算全部放到主循環(huán)。/* 定時器中斷服務只搬運ADC結果不計算電壓 */ void TIM2_IRQHandler(void) { adc_buf[0][write_idx[0]] adc_get(ADC_CH0); adc_buf[1][write_idx[1]] adc_get(ADC_CH1); adc_buf[2][write_idx[2]] adc_get(ADC_CH2); if (write_idx[0] BUF_LEN) { // 三個通道寫滿一輪 write_idx[0] 0; write_idx[1] 0; write_idx[2] 0; g_frame_ready 1; // 通知主循環(huán)可以計算了 } }每次中斷讀三個通道累計寫滿一輪就置標志。注意緩沖區(qū)長度BUF_LEN要能容納至少10個工頻周期也就是2000點以上這樣主循環(huán)做整周期截取時才有足夠的數據可選。3.3 過零測頻避免非整周期采樣誤差真有效值的離散計算公式是RMS sqrt(1/N * Σv2)。這個公式成立的前提是積分區(qū)間等于整數個信號周期。如果寫死每周期采200點但實際電網頻率是49.8Hz窗口誤差就有0.4%顯示值會周期性跳動尤其是喜歡用固定點數算RMS的程序幾乎都會在這里踩坑。處理方法先做過零檢測算出實際頻率再按整數個周期截取數據。/* 從緩沖區(qū)里找所有上升沿過零估算每周波平均點數 */ uint32_t period_points_estimate(const uint16_t *buf, uint32_t len, uint16_t mid) { uint32_t crossings 0; uint32_t sum 0; uint32_t last_zc 0; for (uint32_t i 1; i len; i) { if (buf[i - 1] mid buf[i] mid) { if (last_zc ! 0) { sum i - last_zc; // 相鄰過零間隔 半周期點數 crossings; } last_zc i; } } if (crossings 0) return 200; // 默認值按50Hz估算 return sum / crossings; // 半周期平均點數 }這段函數返回的是半周期點數真正的信號周期是它的2倍。做RMS時取2個完整周期即可也就是用4個半周期平均點數對應的采樣長度。為了防止噪聲在零點附近反復觸發(fā)過零檢測可以加一點滯回比如把閾值設成“大于mid4才算高、小于mid-4才算低”。3.4 有效值計算函數先去掉直流偏置再平方平均得到整周期窗口后有效值計算本身很直接但有一個細節(jié)必須處理信號疊加了1.65V直流偏置如果直接拿原始ADC碼做平方平均算出來的值會包含直流分量結果偏大。正確做法是先減去中點碼值mid再平方累加/* 計算有效值單位是ADC碼 */ float compute_rms(const uint16_t *buf, uint32_t start, uint32_t points, uint16_t mid) { float sum_sq 0.0f; for (uint32_t i 0; i points; i) { float v (float)(int32_t)(buf[start i] - mid); sum_sq v * v; } return sqrtf(sum_sq / points); }這里的mid是關鍵參數它對應硬件上1.65V偏置在ADC里的碼值理論上約等于2048但實際會受電阻精度影響建議在校準時實測并保存。返回單位是ADC碼真正常顯示的電壓還要套用下一節(jié)的校準函數換算。3.5 校準函數為什么單獨成模塊校準最常用的數學模型是一次線性顯示電壓 raw_adc × k b。三相各配一組參數因為每相的互感器、采樣電阻、運放增益不可能完全一致獨立的k和b能把三相誤差分別?;貋?* 校準參數結構體三相各一份 */ typedef struct { float k; // 增益系數默認1.0 float b; // 偏移量默認0.0 } CalParam; CalParam g_cal[3]; /* 把ADC碼轉換為顯示電壓 */ float cal_apply(float raw_adc, const CalParam *p) { return raw_adc * p-k p-b; }k和b的標定流程放在下一章這里要說的是存儲策略保存時一定要附帶一個標志字節(jié)和校驗值上電時校驗失敗就啟用默認參數并點亮報警標識避免現場出現“滿量程亂跳”又找不到原因的尷尬局面。3.6 顯示刷新與軟件濾波500ms刷新更穩(wěn)數碼管或LCD的刷新周期建議500ms不需要一毫秒一刷。刷太快反而會把數值低位抖動暴露出來現場觀感很差。更穩(wěn)的做法是對最近4次計算結果做平均再顯示相當于一個簡單的一階低通。如果你的產品對響應速度有要求比如做過壓告警那告警判斷用原始計算結果顯示用濾波結果兩者分開處理互不干擾。4. 校準與避坑把精度從“有顯示”做到“能交貨”的五個教訓4.1 校準必須先做零點再做增益校準前先準備一臺標準交流信號源等級至少0.2級。校準順序有講究先把輸入短接記錄此時ADC碼作為mid再輸入100.0V標定增益k。先做零點可以讓增益校準不受零點偏移污染否則k和b會互相牽制怎么調都差一點。步驟輸入條件記錄/計算1 零點校準三相輸入全部短接記錄每相mid碼b初始為02 增益校準標準源輸出100.0Vk 100.0 / (raw - mid)3 交叉驗證標準源輸出20V、200V檢查是否落在允許誤差帶內4.2 校準參數的保存與恢復別讓設備現場“變磚”保存參數時除了寫k和b還要寫一個約定的標志字和CRC校驗字節(jié)。上電時如果校驗失敗直接啟用默認參數并把“校準失效”的狀態(tài)寫到顯示區(qū)。這個習慣能避免一種很坑的情況用戶現場校準到一半斷電參數只寫了一半設備重啟后顯示滿量程還以為是硬件壞了。提示校準參數建議按通道獨立存儲不要三個通道共用一個k否則A相校好了B相還是偏。4.3 踩坑一三相電壓顯示全偏但用萬用表對照又正?,F象程序算出的電壓比實際輸入低3%左右三相都有偏差。 原因調試時發(fā)現采樣電阻選成了5%精度的普通碳膜電阻每相阻值批差很大校準系數只能蓋住固定偏差蓋不住隨溫濕度變化的阻值漂移。 解決換成0.1%或0.5%的金屬膜電阻校準前用萬用表實測每相采樣電阻記錄在調試筆記里。硬件底子不好軟件校準就是無底洞。4.4 踩坑二顯示數字每秒鐘跳幾個字現象所有通道在最后一位上不停跳動每秒鐘跳2到5個字。 原因頻率跟蹤沒生效程序用固定點數截取波形但實際工頻在49.9到50.1Hz之間浮動窗口誤差造成RMS結果周期性波動。 解決用本章3.3節(jié)的period_points_estimate做整周期截取再對RMS結果做一階低通濾波。這里要提醒的是光濾波不解決根因窗口誤差是系統(tǒng)性偏差濾波只是讓數字看起來平了精度問題還在。4.5 踩坑三校準時很準熱機半小時后變了現象冷機校準全部通過設備運行一段時間后顯示值偏高或偏低0.3%以上。 原因一次側限流電阻功耗較大自身溫度升高導致阻值變化進而改變互感器一次電流運放和采樣電阻也有溫漂多路誤差疊加。 解決一次側限流電阻選功率余量大的金屬膜電阻并做額定電壓老化測試有條件的話做高低溫箱驗證看溫漂方向后再決定要不要加溫度補償查表。這個坑最容易出現在“驗收時準、運行三個月后不準”的返工現場。4.6 踩坑四上電瞬間ADC讀滿量程顯示跳高現象上電后前幾百毫秒三相電壓顯示接近最大值隨后慢慢恢復正常。 原因調理電路的偏置電壓還沒建立穩(wěn)定運放輸出瞬間飽和到電源軌ADC先采到了錯誤的滿量程值。 解決采樣啟動延時300到500ms等偏置穩(wěn)定后再開中斷主循環(huán)里對初始幾個幀做數據有效性檢查如果連續(xù)兩幀變化超過閾值就繼續(xù)等待不做顯示更新。這個小邏輯還能順帶處理電源跌落恢復的場景。4.7 踩坑五現場接線后讀數是輸入的1.73倍現象表顯示電壓比實際值高約1.73倍用戶以為是儀表壞了。 原因相電壓和線電壓設置混淆。程序里默認顯示線電壓但現場實際接的是相電壓或者反過來。 解決在固件里提供一個“相電壓/線電壓”模式設置項通過按鍵或燒錄配置寫入不要寫死在代碼里?,F場安裝人員不一定看得懂源碼但一定會需要一個切換入口。5. 不接高壓先把程序調準信號源測試與串口診斷技巧5.1 用信號發(fā)生器模擬工頻輸入在接觸380V之前把全部邏輯在低壓下調通是必須養(yǎng)成的習慣。讓信號發(fā)生器輸出10V、50Hz正弦波經過一個隔離變壓器或直接串聯電阻接進調理板的“電壓輸入端”觀察ADC原始碼是否在預期區(qū)間內對稱變化。沒有信號發(fā)生器時用交流變壓器的低壓側也能粗測但波形失真大只能驗證鏈路通不通不能驗證精度。5.2 離線正弦表自檢算法先把算法從硬件里剝出來程序里保留一個自檢函數用固定正弦表喂給compute_rms對比理論值。這是把“軟件問題”和“硬件問題”分開的最快方法/* 自檢函數用固定正弦波驗證RMS算法 */ void self_test_rms(void) { static uint16_t tbl[200]; for (int i 0; i 200; i) { float v 2048 3000 * sinf(2.0f * 3.14159265f * i / 200); tbl[i] (uint16_t)v; } float rms compute_rms(tbl, 0, 200, 2048); /* 理論RMS 3000 / sqrt(2) ≈ 2121.3 */ }如果這里算出來的值和2121.3偏差超過0.1%問題就在算法層可能是數據類型溢出、窗口點數不對或mid取值錯誤如果離線算得準、接了調理板就不準問題就在采樣鏈路。這個定位方法能省下一大半查bug的時間。5.3 串口診斷把原始ADC碼打出來看調試時通過串口每秒打印一行三相原始碼均值、計算頻率、RMS碼、校準后電壓。如果頻率正常、原始波形區(qū)間正確但電壓不對問題在校準系數如果原始數據一側倒、波形削頂問題在硬件調理。我在所有儀表項目里都保留這個“調試模式”平時用編譯宏關掉出問題時打開比拿示波器到處點快得多。之前做模擬項目X調電壓表時就吃過一次愧校準用的標準源沒有預熱讀出的基準值整體偏低折騰了一下午最后才發(fā)現是標準源輸出還沒穩(wěn)定。后來每次校準都先等標準源穩(wěn)定半小時再動手這個習慣一直保留到現在。希望幫到你。本文還有配套的精品資源點擊獲取