
1. 项目概述为什么用Arduino测电压是个好主意如果你玩过Arduino大概率用它点亮过LED、驱动过舵机或者读取过温湿度传感器。但你可能没意识到你手边那块小小的开发板其实还是一个相当灵活的数字万用表核心。用Arduino进行电压测量听起来像是电子爱好者的基础操作但这里面门道不少从简单的电池电量监测到复杂的电源系统分析它都能派上用场。我最初接触这个需求是因为想做一个太阳能充电宝的监控器需要实时知道电池电压判断充电状态。市面上的电压表模块要么精度不够要么功能单一于是干脆自己用Arduino Uno搭了一个。Arduino做电压测量的核心优势在于可编程和集成度。你不再需要一个独立的显示设备测量结果可以直接通过串口发送到电脑或者用一块LCD屏就地显示甚至可以通过Wi-Fi模块比如ESP32上传到云端。整个过程从信号采集、数据处理到结果输出全部可以在一个低成本、易上手的平台上完成。这对于学生做课题、创客做原型、甚至是一些轻量级的工业监测场景都非常有吸引力。当然它也有局限比如输入电压范围受硬件限制精度也取决于ADC模数转换器的性能和你的电路设计。但正是这些限制让掌握正确的测量方法变得尤为重要。接下来我会拆解整个流程从原理到实操再到避坑让你不仅能测出电压更能理解为什么这么测以及如何测得更准、更稳。2. 核心原理与硬件选型不只是接根线那么简单2.1 Arduino ADC的工作原理与局限Arduino Uno基于ATmega328P的核心测量部件是它的10位ADC。所谓10位意味着它能把0到参考电压默认是5V的这个区间分成2的10次方也就是1024个离散的等级。每个等级对应的电压值就是参考电压 / 1024。当参考电压为5V时理论分辨率大约是4.9毫伏5V / 1024 ≈ 0.0049V。这个数字看起来很美但实际精度远达不到这个水平。这里有个关键概念叫参考电压AREF。ADC需要知道“满量程”对应多少伏特才能进行换算。默认情况下Arduino Uno使用板载的5V稳压输出作为参考电压。但问题来了这个5V电压本身可能并不精确它会随着USB口供电质量、板载稳压芯片的负载和温度漂移而变化。如果你需要更高的测量精度就必须引入一个更稳定的外部参考电压源比如一个精密的3.3V或2.5V基准电压芯片如TL431、REF3030。通过analogReference(EXTERNAL)指令并把这个稳定电压接到AREF引脚ADC就会以这个电压为满量程基准测量精度和稳定性会大幅提升。另一个常被忽略的细节是ADC的输入阻抗。ATmega328P的ADC输入阻抗并不是无限大典型值在100兆欧姆左右。当你用一个大阻值的分压电路去测量高电压时如果分压电阻的阻值也很大比如都用1MΩ那么ADC的输入阻抗就会与分压电路并联形成一个意外的分压支路导致测量值严重偏低。因此设计分压电路时需要让分压电阻的等效输出阻抗远小于ADC的输入阻抗通常建议在10kΩ以下以确保ADC能“轻松”地读取到分压后的电压而不会因为“负载效应”导致信号被拉低。2.2 安全第一测量高于5V电压的分压电路设计直接测量高于5V的电压比如12V电池、24V电源会瞬间烧毁Arduino的ADC引脚甚至损坏整个MCU。我们必须使用电阻分压电路将高压“缩小”到0-5V的安全范围内。电路设计要点假设你要测量最高V_max的电压Arduino的ADC输入上限是V_ref比如5V。你需要两个电阻R1和R2构成一个分压器。R1接高压端和ADC引脚之间R2接ADC引脚和地GND之间。ADC引脚上的电压V_adc V_in * [R2 / (R1 R2)]。我们的目标是当V_in V_max时V_adc V_ref。由此可以推导出电阻比例关系R1 / R2 (V_max / V_ref) - 1。例如要测量最高24V的电压使用5V参考电压则R1 / R2 (24 / 5) - 1 3.8。你可以选择R2 10kΩ那么R1 38kΩ。标准电阻中没有38kΩ我们可以取一个接近的值比如39kΩ。这时重新计算当输入24V时ADC引脚电压为24V * (10k / (39k 10k)) ≈ 4.9V仍在安全范围内。电阻选型经验精度至少选择1%精度的金属膜电阻。5%的碳膜电阻误差太大会直接引入系统误差。阻值范围如前所述为了降低对ADC输入阻抗的负载分压电路的等效输出阻抗即R1与R2的并联值应尽可能小。但阻值太小又会消耗过多电流导致被测电路负载过重。一个折中的范围是让R1R2的总阻值在几十kΩ到一百多kΩ之间。例如R1100kΩ R220kΩ用于测0-30V总阻值120kΩ并联后输出阻抗约16.7kΩ对ADC的负载效应在可接受范围内静态电流也仅0.25mA在30V时。分压比校准由于电阻存在公差计算出的分压比并不绝对准确。更严谨的做法是用一块已知精确的万用表测量一个稳定的输入电压如12V同时读取Arduino的ADC原始值反推出实际的分压系数并在代码中应用这个校准后的系数。注意在分压电路和Arduino ADC引脚之间强烈建议串联一个100-500Ω的小电阻并在ADC引脚到地之间接一个0.1uF的陶瓷电容。这个小电阻和电容构成了一个简单的低通滤波器和限流保护可以吸收瞬间的电压尖峰和静电极大地保护ADC输入端口。这个细节很多教程里不提但能显著提高电路的可靠性。2.3 测量负电压与交流电压的策略Arduino的ADC只能测量0到参考电压之间的正电压。对于负电压或交流电压需要前端信号调理电路。测量负电压需要一个运放搭建的电平移位电路。例如用一个单电源运放如LM358构成一个加法器电路将负电压叠加一个正的偏置电压使输出整体抬升到0V以上。比如你要测量-5V到0V可以设计电路当输入-5V时输出0V输入0V时输出2.5V。这样ADC就能读取了代码里再反向换算回去。这需要一定的模拟电路知识。测量交流电压如市电极度危险非专业勿试如果必须测量低压交流如变压器次级通常步骤是先通过一个电压互感器或精密分压电阻将高压交流按比例降低然后通过一个精密整流电路用运放构成比二极管整流损耗小、线性度好将交流转为单向脉动直流最后通过一个低通滤波电路得到其平均值。对于正弦波平均值与有效值RMS有固定换算关系。绝对禁止将市电直接通过电阻分压接入Arduino这有致命危险且会损坏设备。3. 软件实现与精度提升技巧3.1 基础代码解读与滤波算法最基本的电压测量代码非常简单int analogPin A0; // 电压信号接入的模拟引脚 int rawValue 0; float voltage 0.0; float R1 100000.0; // 分压电阻R1阻值单位欧姆 float R2 20000.0; // 分压电阻R2阻值 void setup() { Serial.begin(9600); // analogReference(DEFAULT); // 默认使用5V参考可省略 // 如果使用外部基准则用analogReference(EXTERNAL); } void loop() { rawValue analogRead(analogPin); // 读取原始ADC值 (0-1023) voltage rawValue * (5.0 / 1023.0); // 计算ADC引脚上的电压 // 如果使用了分压电路需要换算回原电压 voltage voltage * ((R1 R2) / R2); Serial.print(Measured Voltage: ); Serial.print(voltage, 2); // 打印电压保留两位小数 Serial.println( V); delay(1000); }但这样直接读取的值噪声很大跳动可能达到几十毫伏。提升读数稳定性的核心是软件滤波。这里介绍三种最实用的方法多次采样取平均这是最简单有效的方法。连续读取N次然后求平均值。N越大滤波效果越好但响应速度越慢。通常取10-50次。int samples 10; long sum 0; for(int i0; isamples; i) { sum analogRead(analogPin); delay(1); // 稍作延时避免读取过快 } rawValue sum / samples;移动平均滤波维护一个固定长度的数组每次新读数替换最旧的读数然后计算数组中所有数的平均值。这种方法能兼顾实时性和平滑度。#define FILTER_LEN 10 int filterBuffer[FILTER_LEN]; int filterIndex 0; // 在loop中 filterBuffer[filterIndex] analogRead(analogPin); filterIndex (filterIndex 1) % FILTER_LEN; long sum 0; for(int i0; iFILTER_LEN; i) { sum filterBuffer[i]; } rawValue sum / FILTER_LEN;中值滤波特别适用于消除偶发的、幅度大的脉冲干扰尖峰噪声。取N次采样排序取中间值作为结果。结合平均值滤波效果更好如先去尖峰再平均。3.2 校准消除系统误差的关键步骤即使电路和代码都正确测量值也可能和万用表有几十毫伏甚至更大的偏差。这来自于ADC参考电压的实际值不是精确的5.000V。分压电阻的实际阻值与标称值有偏差。ADC本身的非线性误差和零点偏移。校准方法你需要一个已知精确的电压源或一块校准过的万用表作为基准。两点校准法推荐测量两个已知的、稳定的精确电压点通常一个在量程低端如1.000V一个在高端如4.000V。记录下这两个电压点对应的ADC原始读数rawLow和rawHigh。理论上电压V和读数raw的关系是V slope * raw offset。斜率slope (V_high - V_low) / (rawHigh - rawLow)偏移offset V_low - slope * rawLow以后所有测量值都通过这个公式计算voltage slope * rawValue offset单点校准参考电压测量如果你有一个精确的万用表可以先用它测量Arduino板子上5V引脚的实际电压V_5V_actual。然后在代码中将换算公式里的5.0替换为这个实际值voltage rawValue * (V_5V_actual / 1023.0)。这能消除参考电压不准带来的大部分误差。将校准得到的slope和offset作为常量写在代码开头你的测量系统就具备了“出厂校准”的精度。3.3 进阶过采样提升分辨率Arduino的ADC是10位但通过过采样和求平均的技术可以在牺牲一定速度的前提下获得更高的有效分辨率。原理是如果ADC噪声是随机的白噪声通过大量采样取平均可以将噪声平均掉从而分辨出比单个LSB最低有效位更细微的电压变化。例如要实现12位分辨率4096级需要对每个输出结果进行16次2^(12-10)*4 16采样。具体操作是连续读取16次ADC值将它们累加然后这个累加值本身就相当于一个12位的数因为10位数左移了相当于放大了。你可以直接使用这个累加值进行后续计算相当于获得了12位的动态范围。int oversampleBits 2; // 希望提升的位数例如2位 int oversampleTimes 1 (2 * oversampleBits); // 需要采样的次数提升2位需采样16次 long sum 0; for(int i0; ioversampleTimes; i) { sum analogRead(analogPin); } int highResValue sum oversampleBits; // 右移得到更高分辨率的数值 // 此时 highResValue 的范围是 0 ~ (1023 oversampleBits)例如提升2位后是0~4092过采样非常适用于测量变化缓慢的直流信号如电池电压、温度传感器输出可以显著提高测量精度和读数稳定性。4. 完整项目实战制作一个高精度电池电压监测器让我们综合以上所有知识做一个能测量0-15V电池电压、带LCD显示和电压过低报警的监测器。4.1 硬件清单与连接主控Arduino Uno显示16x2 I2C LCD屏简化接线分压电阻R1 100kΩ (1%), R2 20kΩ (1%)理论测量上限 5V * (120k/20k) 30V我们保守用于15V。保护与滤波在ADC引脚A0前串联一个200Ω电阻并在A0与GND之间接一个0.1uF陶瓷电容。报警一个蜂鸣器或无源喇叭接在数字引脚如D8上。供电被测电池或另一个独立电源。连接图电池正极 → R1 (100k) → Arduino A0引脚。在A0引脚处接200Ω电阻到分压点并接0.1uF电容到GND。电池负极 → Arduino GND。R1和R2的连接点即分压点 → 上述的200Ω电阻 → Arduino A0。R2 (20k)另一端 → Arduino GND。I2C LCD的SDA、SCL分别接Arduino的A4、A5VCC和GND接5V和GND。蜂鸣器正极接D8负极接GND如为有源蜂鸣器注意电压匹配。4.2 软件代码实现代码将包含过采样滤波、两点校准、电压计算、LCD显示和阈值报警。#include Wire.h #include LiquidCrystal_I2C.h // 引脚定义 const int voltagePin A0; const int buzzerPin 8; // 分压电阻参数 (实测值需校准) const float R1 100000.0; // 100k const float R2 20000.0; // 20k const float divRatio (R1 R2) / R2; // 分压比 // 两点校准参数 (示例值必须用标准电压源实测校准) // 假设输入1.000V时ADC读数为calRaw1 // 假设输入10.000V时ADC读数为calRaw2 const int calRaw1 205; // 对应1.000V时的原始读数示例 const int calRaw2 2050; // 对应10.000V时的原始读数示例 const float calVolt1 1.000; const float calVolt2 10.000; // 计算校准后的斜率和偏移 const float calSlope (calVolt2 - calVolt1) / (calRaw2 - calRaw1); const float calOffset calVolt1 - calSlope * calRaw1; // 报警阈值 (以12V铅酸电池为例) const float voltageLow 11.5; // 低压报警阈值 (V) const float voltageCrit 10.8; // 严重低压阈值 (V) // 初始化LCD (地址通常为0x27或0x3F) LiquidCrystal_I2C lcd(0x27, 16, 2); // 过采样设置 const int oversampleShift 2; // 提升2位分辨率 const int oversampleCount 1 (2 * oversampleShift); // 采样16次 void setup() { Serial.begin(115200); pinMode(buzzerPin, OUTPUT); digitalWrite(buzzerPin, LOW); lcd.init(); lcd.backlight(); lcd.setCursor(0, 0); lcd.print(Battery Monitor); delay(2000); lcd.clear(); // 如果需要使用外部基准电压取消下一行注释 // analogReference(EXTERNAL); } // 使用过采样读取ADC int readOversampledADC(int pin) { long sum 0; for (int i 0; i oversampleCount; i) { sum analogRead(pin); delayMicroseconds(100); // 微小延时让ADC充分稳定 } return (sum oversampleShift); // 右移得到高分辨率值 } void loop() { // 1. 读取并转换电压 int highResRaw readOversampledADC(voltagePin); // 2. 将高分辨率原始值映射回等效的10位ADC值范围以便使用10位校准参数 // 高分辨率值范围是 0 ~ (1023 oversampleShift)例如0~4092 // 将其除以 (1 oversampleShift) 得到近似的10位值 float rawVoltageAnalog highResRaw / float(1 oversampleShift); // 3. 应用两点校准公式 float measuredVoltage rawVoltageAnalog * calSlope calOffset; // 4. 换算回电池实际电压 (如果校准是在ADC引脚电压上做的则跳过此步) // 如果calRaw1/2是在电池端电压下测得则measuredVoltage已经是电池电压。 // 如果calRaw1/2是在ADC引脚电压下测得则需要乘以分压比 // measuredVoltage measuredVoltage * divRatio; // 5. 串口输出 (用于调试) Serial.print(HighRes Raw: ); Serial.print(highResRaw); Serial.print( | Volt: ); Serial.print(measuredVoltage, 3); Serial.println( V); // 6. LCD显示 lcd.setCursor(0, 0); lcd.print(Voltage: ); lcd.print(measuredVoltage, 2); lcd.print( V ); // 空格用于清除残留字符 // 7. 报警逻辑 lcd.setCursor(0, 1); if (measuredVoltage voltageLow) { lcd.print(Status: OK ); noTone(buzzerPin); } else if (measuredVoltage voltageCrit) { lcd.print(Status: LOW! ); // 间歇性蜂鸣 tone(buzzerPin, 1000, 200); // 响200ms } else { lcd.print(Status: CRITICAL!); // 急促蜂鸣 tone(buzzerPin, 2000, 100); } delay(500); // 主循环延时 }4.3 校准与调试流程硬件连接检查确保分压电路连接正确特别是接地共点要一致。获取校准点找一个可调稳压电源或已知电压的电池。将其调整到1.000V或一个你方便获得的精确低压连接到你的测量电路输入端电池正负极。在代码中暂时注释掉校准计算部分直接通过串口监视器打印highResRaw或rawVoltageAnalog的值。等待读数稳定后记录下这个值这就是calRaw1。将输入电压调整到10.000V应在量程内的高点同样记录稳定的ADC值作为calRaw2。更新代码将记录下的calRaw1,calRaw2以及对应的calVolt1,calVolt2更新到代码的常量中。验证重新给测量电路输入几个已知电压如3V 5V 12V查看串口和LCD显示的电压值与万用表测量值是否吻合。如果仍有小偏差可以微调calSlope和calOffset。5. 常见问题、故障排查与进阶思路5.1 典型问题速查表现象可能原因排查与解决读数跳动大不稳定1. 电源噪声大2. 未使用滤波算法3. 输入信号线过长引入干扰4. ADC参考电压不稳1. 为Arduino使用线性稳压电源或电池供电远离电机、继电器等噪声源。2. 实现软件滤波移动平均、过采样。3. 缩短信号线使用屏蔽线或双绞线。4. 在AREF引脚加一个10uF钽电容和0.1uF陶瓷电容并联到地或使用外部基准源。测量值始终为01. 分压电路断路或短路2. 模拟引脚配置错误3. 被测电压远低于测量下限1. 用万用表检查分压电路各点电压。2. 检查代码中引脚号是否正确。3. 确认被测电压是否高于ADC能检测到的最低电压约0.05V。测量值始终接近最大值10231. 分压电路计算错误输入电压超过ADC量程2. ADC引脚对地短路失效3. 外部基准电压未接或失效但代码设置了analogReference(EXTERNAL)1. 重新计算分压比确保最高输入电压时分压点电压5V。2. 检查ADC引脚连接的滤波电容是否短路。3. 检查外部基准电压源连接或改回analogReference(DEFAULT)。测量值线性度差误差随电压变化1. 电阻精度不够或温漂大2. ADC非线性误差显著3. 未进行系统校准1. 更换为1%精度金属膜电阻。2. 采用两点校准法可以很好地补偿非线性误差。3. 务必执行校准流程。测量负电压读数混乱电路不支持负电压测量增加运放电平移位电路将信号偏移至正电压范围。5.2 精度极限与进阶优化当你做完基础项目后如果对精度还有更高追求可以考虑以下方向使用外部ADC芯片Arduino内置的10位ADC是性能瓶颈。可以选用ADS111516位I2C接口或MCP342118位等专业ADC芯片。它们具有更高的分辨率、更低的噪声和内置可编程增益放大器PGA可以直接测量微伏级信号并通过I2C与Arduino通信轻松实现高精度测量。精密电压基准如前所述使用像REF30303.0V这样的精密基准电压源为ADC提供AREF能彻底解决参考电压不稳的问题。这是提升绝对精度的最有效手段之一。四线制开尔文测量对于测量电池内阻或微小电阻上的压降导线电阻会引入误差。采用四线制测量用一对线提供激励电流另一对高阻抗的线专门测量电压可以消除导线压降的影响。温度补偿高精度电阻的阻值会随温度变化。如果项目工作环境温度变化大需要考虑使用低温漂电阻如5ppm/°C的金属箔电阻或者在软件中集成温度传感器如DS18B20建立电阻值-温度查找表进行实时补偿。5.3 从测量到系统数据记录与远程监控单一的电压测量价值有限但将其作为数据节点融入系统价值就大了。本地数据记录在Arduino上插一张SD卡模块定期将时间戳和电压值写入CSV文件。这样就可以长时间记录电池的充放电曲线、太阳能板的输出波动等。无线传输换用ESP8266或ESP32这类带Wi-Fi的MCU将测量数据通过MQTT协议发送到本地服务器如Home Assistant或云平台如ThingsBoard、Blynk。你就能在手机App上实时查看电压并设置自动化报警。多通道与同步如果需要同时测量多路电压并关注它们之间的相位或时序关系如三相电就需要使用多通道ADC且支持同步采样的芯片或者用多个Arduino通过同步信号触发采样。用Arduino测电压从一根线的简单读取到构建一个稳定、精确、智能的监测系统中间每一步都充满了工程实践的细节。它考验的不仅仅是编程更是对模拟电路、信号处理、误差分析和系统设计的理解。我最开始做的那个太阳能监控器现在还在我家的露台上工作连续记录了三年多的数据期间根据遇到的问题反复优化了电路和代码。这个过程给我的最大体会是硬件项目里软件代码的稳定性往往建立在硬件电路的可靠性和对物理原理的深刻理解之上。别怕麻烦耐心校准认真处理每一个噪声源你的测量系统会回报给你值得信赖的数据。