ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STM32 ADC内部参考电压校准:实现高精度电池电压测量的原理与实践

STM32 ADC内部参考电压校准:实现高精度电池电压测量的原理与实践 1. 项目缘起为什么需要测量电池电压在嵌入式开发尤其是基于电池供电的便携设备或物联网节点项目中准确监测电池电压是一个基础且关键的需求。无论是为了在电量过低时及时告警、实现低功耗休眠还是为了进行电量百分比估算第一步都是要获取一个可靠的电压读数。STM32作为业界广泛使用的微控制器其内置的模数转换器ADC为我们提供了将模拟电压转换为数字值的直接手段。然而一个初学者常踩的坑是直接读取ADC转换值然后简单地乘以一个理论上的参考电压比如3.3V就认为是输入引脚上的电压。这种方法在实验室用稳压电源供电时可能问题不大但一旦设备由电池供电情况就变了。电池电压会随着放电而下降而为STM32芯片供电的电压VDD正是这个变化的电池电压或经过LDO稳压后的电压。ADC的参考电压通常与VDD相连这意味着参考电压本身是浮动的。你用着一个变化的“尺子”去测量外部电压得到的数字自然不准。这就是为什么我们需要一个稳定的、已知的“基准尺”——参考电压。STM32很多型号都内置了一个高精度的内部参考电压源比如1.2V或1.22V它不随VDD变化正是解决这个问题的钥匙。最近在几个低功耗传感器节点的项目里我都用到了这个方案。从锂亚电池供电的远程仪表到纽扣电池供电的蓝牙标签精准的电压监测是延长续航、保证数据可靠回传的前提。网上很多教程只讲了“怎么读ADC值”却很少深入说清楚“为什么读不准”以及“如何校准得准”这正是本文想掰开揉碎讲明白的。2. 核心原理ADC、参考电压与测量链路剖析要理解整个方案我们需要把ADC测量链路上的几个关键角色和它们之间的关系理清楚。这就像一套测量系统每个环节的误差都会影响最终结果。2.1 ADC转换的基本公式STM32的ADC是一个12位精度的转换器部分型号有16位模式它将输入引脚ADCx_INy上的模拟电压V_in与参考电压V_ref进行比较输出一个数字值ADC_Data。其理想转换公式为ADC_Data (V_in / V_ref) * (2^n - 1)其中n是ADC的分辨率对于12位ADC2^n - 1 4095。所以公式简化为V_in (ADC_Data / 4095) * V_ref这里就是第一个关键点公式里的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将产生巨大误差。2.2 内部参考电压 (VREFINT) 的角色STM32芯片内部集成了一个带隙基准电压源其输出电压被标记为VREFINT。这个电压值非常稳定典型值为1.20V或1.22V具体值在芯片数据手册的电气特性章节给出并且每个芯片在出厂时都在特定条件下如VDDA3.3V, 温度25°C测量并校准校准值存放在芯片的系统存储区System Memory中通常是一个12位的数值对应VDDA3.3V时测量VREFINT通道得到的ADC原始值。它的核心作用不是直接作为测量外部信号的参考电压而是作为一个已知的、稳定的“标尺”来反向推算出当前实际的VDDA电压。我们可以通过ADC去测量这个内部的VREFINT电压STM32有专门的内部通道连接它例如在STM32F1系列中是通道17得到一个原始值ADC_Data_RefInt。由于VREFINT的电压是已知的通过出厂校准值推算得出根据ADC公式我们就可以反推出当前的VDDA实际_VDDA (VREFINT_理论值 * 4095) / ADC_Data_RefInt这里就是第二个关键点VREFINT_理论值如何获取它不是简单的1.20V。更准确的做法是使用芯片的出厂校准值。这个校准值记为VREFINT_CAL是在VDDA3.3V且温度25°C时测量VREFINT通道得到的ADC原始值。所以VREFINT通道此时的电压实际值为VREFINT_实际 3.3V * (VREFINT_CAL / 4095)这个VREFINT_实际才是你这个芯片在理想条件下的真实内部参考电压值。我们后续计算就基于它。2.3 完整的测量链路与计算步骤理解了以上两点我们就可以梳理出测量外部电池电压假设通过分压电阻连接到ADC引脚的完整链路测量内部参考电压通道使能ADC读取连接VREFINT的内部通道得到原始值ADC_RefInt。计算当前实际VDDA从芯片特定地址例如STM32F103是0x1FFFF7BA读取出厂校准值VREFINT_CAL。计算VREFINT的实际电压值VREFINT 3.3 * (VREFINT_CAL / 4095)。计算当前VDDAVDDA_实际 VREFINT * 4095 / ADC_RefInt。测量外部电池电压通道读取连接电池分压电路的ADC引脚得到原始值ADC_Batt。计算外部输入引脚电压V_in_pin (ADC_Batt / 4095) * VDDA_实际。计算真实电池电压如果使用了分压电阻比如R1上拉R2下拉则电池电压V_batt V_in_pin * ( (R1 R2) / R2 )。通过这个流程我们巧妙地将一个变化的VDDA参考电压通过一个稳定的内部基准VREFINT给“标定”了出来从而消除了供电电压波动对ADC读数的影响。注意VREFINT本身也有微小的温漂但对于大多数电池监测场景温度变化不剧烈精度要求通常在±0.1V以内来说其影响可以接受。如果对精度要求极高则需要查阅数据手册中的温漂曲线并进行软件补偿。3. 硬件设计分压电路与抗干扰要点原理清楚了硬件是实现准确测量的基础。这里主要涉及分压电阻的计算和PCB布局的注意事项。3.1 分压电阻的选型与计算电池电压如锂电池满电4.2V通常高于ADC引脚允许的最大输入电压通常为VDDA多为3.3V或3.6V。因此必须使用电阻分压。这不是简单地把电压降下来就行需要考虑多个因素。计算分压比假设VDDA 3.3V我们希望电池电压在最高时4.2V分压后的电压略小于3.3V留有一定余量。可以设定目标分压后电压为3.0V。则分压比K V_out / V_batt_max 3.0 / 4.2 ≈ 0.714。根据公式V_out V_batt * (R2 / (R1 R2))可得R2 / (R1 R2) 0.714。选取R2为10kΩ则可计算出R1约为4kΩ。为了使用标准阻值可以选择R13.9kΩ R210kΩ此时分压比约为0.719满电时分压电压约为3.02V是安全的。电阻精度与功耗权衡精度应选择1%精度的薄膜电阻以保证分压比准确。5%精度的碳膜电阻会引入不可忽视的系统误差。阻值大小阻值不宜过小否则分压电路从电池消耗的静态电流过大。例如若R1R23.9k10k13.9k接在4.2V电池上静态电流I 4.2V / 13.9kΩ ≈ 0.3mA。对于常年待机的设备这个电流可能偏大。可以考虑将总阻值提高到100kΩ以上如R168k, R2100k此时静态电流约24μA显著降低。但阻值过大又会带来新的问题。高阻值带来的挑战ADC采样时间ADC输入端可以等效为一个采样电容C_S需要通过分压电阻对其充电。根据RC充电公式时间常数τ R_source * C_S。这里的R_source就是分压网络的输出阻抗等于R1与R2的并联值R1//R2。阻值越大充电到稳定所需的时间越长。如果ADC的采样时间设置过短采样电容未充满电就会导致读数偏低且不稳定。噪声敏感性高阻抗节点更容易拾取板上的数字噪声导致ADC读数跳动。解决方案增加滤波电容在分压点ADC输入引脚到地之间并联一个100nF~1μF的陶瓷电容C1。这个电容与分压电阻的并联值构成低通滤波器可以滤除高频噪声更重要的是它为ADC采样瞬间提供了电荷池减小了对前端电阻驱动能力的要求。调整ADC采样时间在STM32的ADC配置中增加对应通道的采样周期SAMPLETIME。对于高阻抗源可能需要设置到最大的几百个ADC时钟周期确保采样充分。一个推荐的电路参数对于大多数3.3V系统测量单节锂电池3.0V-4.2V我常用的组合是R1100kΩ,R2220kΩ分压比约0.6875满电输入ADC的电压约为2.89V。在分压点接一个100nF的电容到地。ADC采样时间设置为239.5个周期STM32CubeMX中可配置。这个组合在静态电流约10μA、精度和稳定性之间取得了很好的平衡。3.2 PCB布局与接地考量模拟测量对噪声敏感PCB布局不当会毁掉所有软件努力。模拟与数字分离确保为模拟部分VDDA, VSSA, ADC输入走线提供干净的电源和地。如果板上有开关电源DCDC其噪声很大务必用电感或磁珠将模拟电源隔离出来。分压电阻靠近MCU分压电阻和滤波电容应尽可能靠近STM32的ADC输入引脚放置引线要短减少天线效应。正确处理VDDA/VSSA即使不使用外部精密基准VDDA和VSSA也必须正确连接。通常VDDA应连接到与VDD相同的电源但通过一个磁珠或小电阻如0Ω进行隔离滤波。VSSA必须直接连接到干净的模拟地平面并且是VSS的单一连接点。避免数字信号线穿越不要让高频数字信号线如时钟、SPI、PWM从分压电路或ADC引脚下方或附近穿过。4. 软件实现从寄存器配置到校准计算硬件准备妥当后我们进入软件环节。这里以STM32的HAL库为例展示关键步骤和代码。虽然不同系列STM32的ADC外设和库函数略有差异但核心流程和思想是通用的。4.1 ADC初始化与通道配置首先我们需要初始化ADC并配置两个通道一个是测量电池电压的外部通道假设是ADC1_IN1另一个是测量内部参考电压的通道ADC1_IN17或对应通道。// 1. ADC句柄与初始化结构体 ADC_HandleTypeDef hadc1; ADC_ChannelConfTypeDef sConfig {0}; // 2. 基础ADC初始化 hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_DISABLE; // 非扫描模式单通道 hadc1.Init.ContinuousConvMode DISABLE; // 单次转换 hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; // 软件触发 hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; // 数据右对齐 hadc1.Init.NbrOfConversion 1; // 转换序列数为1 if (HAL_ADC_Init(hadc1) ! HAL_OK) { Error_Handler(); } // 3. 配置内部参考电压通道 (VREFINT) sConfig.Channel ADC_CHANNEL_VREFINT; // 内部通道 sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; // 使用长采样时间确保稳定 if (HAL_ADC_ConfigChannel(hadc1, sConfig) ! HAL_OK) { Error_Handler(); }这里将采样时间设置为最大值因为内部参考电压通道的驱动能力不强需要足够的时间采样。4.2 读取内部参考电压并计算实际VDDA这是最核心的校准步骤。我们需要先读取VREFINT的出厂校准值然后测量当前的VREFINTADC值。// 定义相关变量 #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或数据手册Datasheet中查找“ADC calibration values”或“System memory”章节确认。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; // 分压系数 // 重新配置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; // 根据分压电阻阻抗设置 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通道和电池电压通道进行多次采样取平均。例如连续采样16次或32次去掉最大最小值后求平均。4.4 软件滤波与数据处理ADC读数难免会有噪声尤其是环境复杂的应用中。简单的多次平均滤波均值滤波可以有效平滑数据#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; // 四舍五入 }对于电池电压这种变化缓慢的信号也可以使用一阶低通滤波指数加权平均代码更简单占用内存少且能反映变化趋势float battery_voltage_filtered 0.0f; // 滤波后的值 const float ALPHA 0.1f; // 滤波系数 (0~1)越小越平滑响应越慢 void update_battery_voltage(float new_sample) { battery_voltage_filtered ALPHA * new_sample (1 - ALPHA) * battery_voltage_filtered; }5. 实测调试与精度提升技巧代码写完烧录进板子得到的电压值可能和万用表测量值还有几十毫伏的差距。别急这是正常现象我们可以通过系统性的调试和校准来提升精度。5.1 基础验证与常见问题排查验证VDDA计算是否正常首先用read_actual_vdda()函数读出计算出的VDDA_实际。用万用表实际测量你板子上STM32的VDD/VDDA引脚电压。两者应该非常接近比如相差在0.1V以内。如果差距巨大比如计算出来是2.5V实际测量是3.3V问题可能出在VREFINT校准值地址错误这是最常见的原因。仔细核对芯片型号和参考手册。VREFINT通道采样时间不足尝试将采样时间设置为最大值。电源噪声过大用示波器观察VDDA引脚看是否有大的纹波。确保滤波电容已焊接且容值正确。验证分压电路断开与MCU的连接直接用万用表测量分压点的电压并计算电池电压看是否与理论值相符。检查电阻阻值是否与设计一致特别是用了0603、0402等小封装容易焊接错误。检查ADC读数是否稳定连续打印ADC原始值ADC_Batt观察其跳动范围。对于12位ADC在稳定的电源和信号下跳动应该在±2LSB以内。如果跳动很大几十个LSB检查PCB布局ADC输入线是否靠近噪声源。滤波电容分压点对地的滤波电容是否焊接容值是否合适建议用X7R/X5R材质的陶瓷电容软件触发时机避免在数字电路如GPIO翻转、串口发送频繁动作时进行ADC采样可以适当延时或选择系统相对空闲时采样。5.2 两点校准法提升绝对精度即使使用了内部参考电压校准由于分压电阻的精度1%仍有误差、PCB漏电、ADC本身的增益误差和偏移误差等因素最终换算出的电池电压可能与万用表测量值存在一个固定的比例偏差或固定偏移。我们可以通过“两点校准法”来软件消除这个系统误差。校准步骤准备一个可调精密电源代替电池连接到你的设备分压电路输入端。第一点低点校准调节电源输出一个电压V_actual_low例如3.5V在设备端通过上述代码读取并记录计算出的电压值V_read_low。第二点高点校准调节电源输出一个电压V_actual_high例如4.1V记录设备读出的V_read_high。在代码中使用这两个点来计算一个校准斜率和偏移量// 校准参数通过实测得到 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); // 校准函数 float calibrate_battery_voltage(float raw_voltage) { return (raw_voltage * calib_scale) calib_offset; }将read_battery_voltage()函数返回的值再送入calibrate_battery_voltage()函数进行修正。经过两点校准后通常可以将绝对精度提升到±0.5%以内完全满足电池电量监测的需求。这两个校准参数可以存储在STM32的Flash中设备上电时读取。5.3 低功耗场景下的优化对于电池供电的设备ADC测量本身也会耗电。需要优化间歇性测量不需要实时监测电压时如每小时监测一次在测量前才使能ADCHAL_ADC_MspInit中开启ADC时钟和GPIO时钟测量完成后立即关闭ADC时钟和对应GPIO时钟并将ADC输入引脚设置为模拟输入模式低功耗。缩短采样时间在满足精度的前提下尽量使用较短的ADC采样时间可以减少ADC模块的工作时间。使用DMA定时器触发如果需要周期性高频率采样可以配置ADC由定时器触发并使用DMA传输数据。CPU可以在采样期间进入睡眠模式由DMA完成数据搬运采样完成后再唤醒CPU处理最大化节省功耗。6. 进阶话题从电压到电量估算准确测量电压是第一步更高级的需求是估算电池剩余电量State of Charge, SoC。这是一个更复杂的课题因为电池电压和剩余电量的关系并非线性且受负载电流、温度、电池老化等因素影响。对于锂电池一个简单实用的方法是查表法在不同剩余电量下100% 90% ... 10% 0%测量电池在典型工作电流下的电压。这需要电池规格书或自己实验获得。在代码中建立一个电压-电量对应表查找表。根据实测的、带负载的电池电压在表中查找最近的两个点进行线性插值估算当前电量。一个极其重要的提示电池的空载电压无负载时测得的电压和带载电压设备工作时测得的电压差异很大。电量估算必须基于带载电压。因此你的ADC测量电路应该直接连接在电池两端经过分压而不是在LDO之后。并且最好在设备处于典型工作状态例如无线模块正在发射MCU正常运行时进行电压采样这时的电压读数对于电量估算才有意义。例如一个简单的查找表可能如下数值仅为示例请根据实际电池测试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; // 不应执行到这里 }实现这个功能后你的设备就具备了基础的“电量显示”能力。当然工业级的电量计Gas Gauge会使用库仑计电流积分等更复杂的方法精度更高但对于很多成本敏感、功耗要求极高的物联网设备基于内部参考电压校准的ADC电压测量配合一个精心测试的电压-电量查找表已经是一个非常可靠且高效的解决方案了。
返回列表