
1. 为什么我要用STM32做一台蓄电池内阻测试仪铅酸蓄电池这东西玩过电动车、UPS、太阳能储能的朋友都不陌生。它便宜、皮实、大电流放电能力强但有个特别烦人的特点——坏得悄无声息。你拿万用表量它的开路电压12V还是12V看起来一切正常可一接上负载电压瞬间掉到9V设备直接重启。这种情况十有八九是电池内阻变大了极板硫化或者失水导致活性物质失效。问题在于普通万用表根本测不出内阻。你想知道电池健康状态要么买一台专业的内阻测试仪便宜的也要大几百上千要么用交流注入法自己搭电路。我手头正好有几块退役的电动车电池扔了可惜留着又不知道还能不能用于是决定用STM32做一个铅酸蓄电池内阻测试仪。这个项目的核心思路是用STM32产生交流激励信号注入电池测量电池两端的交流响应通过欧姆定律算出内阻。整个方案成本控制在五十块以内精度足够判断电池好坏代码全部开源。这篇文章适合谁看如果你有基本的STM32开发经验会用Keil或者STM32CubeIDE懂一点模拟电路基础那就可以直接跟着做。如果你是完全的新手也没关系我会把每个环节的原理和操作都讲清楚你照着抄作业也能跑起来。关键词就三个STM32、铅酸蓄电池、内阻测试仪。下面我从方案选型开始一步步拆解整个实现过程。2. 内阻测试的方案选型与核心原理拆解2.1 为什么选交流注入法而不是直流放电法测电池内阻市面上主要有两种方法直流放电法和交流注入法。直流放电法的思路很直接给电池接一个已知阻值的负载测量负载前后的电压差用ΔV/ΔI算内阻。这个方法原理简单但有几个致命问题。第一放电电流必须足够大才能测出准确的内阻通常需要几安培到几十安培对铅酸电池来说每次测量都是一次深度放电测几次电池就废了。第二极化效应会干扰测量结果电流刚加上去的瞬间和稳定后的电压不一样你取哪个值第三大电流放电需要大功率电阻和散热措施体积和成本都上去了。交流注入法就不一样了。它给电池注入一个高频交流信号通常1kHz左右电流很小几十毫安不会对电池造成任何损伤。电池的内阻在交流信号下表现为纯电阻特性测量的是欧姆内阻不受极化影响。而且交流信号频率高可以避开电池内部的电化学极化测出来的值更接近真实内阻。我选的是交流注入法具体实现方式是用STM32的DAC或者PWM产生1kHz正弦波经过恒流源电路转换成交流恒流信号注入电池。然后用差分放大电路采集电池两端的交流电压经过整流、滤波、ADC采样算出交流电压幅值。已知注入电流幅值用欧姆定律RV/I就算出内阻了。注意交流注入法的关键是恒流源精度和电压检测精度。恒流源不稳定注入电流就会波动测量结果直接飘。电压检测的噪声抑制也很重要因为电池两端的交流信号只有几毫伏到几十毫伏很容易被干扰淹没。2.2 STM32的选型与资源分配STM32系列型号很多从F0到H7选哪个合适我的原则是够用就行不浪费性能也不为了省几块钱给自己找麻烦。这个项目需要的资源有一个定时器产生PWM或者触发DAC一个ADC采集电压一个串口输出数据几个GPIO控制模拟开关和LED指示。运算量不大F1系列完全够用。我手头正好有一块STM32F103C8T6最小系统板也就是常说的“蓝板”72MHz主频64KB Flash20KB RAM资源绰绰有余。具体资源分配如下TIM2产生1kHz方波用于驱动恒流源开关ADC1_IN0采集整流滤波后的电压信号USART1输出测量结果到串口助手PA0-PA3控制模拟开关切换量程PC13板载LED指示测量状态如果你用的是其他型号比如F4或者G0代码需要做相应调整但整体架构不变。F4的ADC精度更高G0的功耗更低各有优势。我选F103纯粹是因为手头有而且资料多遇到问题好查。2.3 模拟前端电路的设计考量模拟前端是整个项目的核心也是最容易出问题的地方。它负责产生恒流激励信号和采集电池响应。我把它分成三个部分恒流源、差分放大、整流滤波。恒流源电路我用的是运放加MOS管的方案。运放的同相端接参考电压反相端接采样电阻MOS管的源极接采样电阻漏极接负载。运放通过调节MOS管的栅极电压使采样电阻上的电压等于参考电压从而保持流过负载的电流恒定。这个电路的恒流精度取决于运放的失调电压和采样电阻的精度。我用的运放是LM358便宜好用但失调电压有点大大概2mV左右。采样电阻用1%精度的10Ω电阻注入电流设为10mA这样采样电阻上的电压是100mV失调电压的影响相对较小。差分放大电路电池两端的交流电压是差分信号需要用差分放大器转换成单端信号。我用的是三个运放组成的仪表放大器电路增益设为100倍。为什么用仪表放大器而不是简单的差分放大器因为仪表放大器的输入阻抗高不会从电池上抽取电流影响测量结果。而且共模抑制比高能有效抑制共模干扰。整流滤波电路放大后的交流信号需要转换成直流电压才能被ADC采集。我用的是精密整流电路用运放和二极管组成克服了普通二极管整流电路的死区问题。整流后的信号经过RC低通滤波滤除纹波得到平滑的直流电压。提示模拟部分的电源一定要干净。我用的是AMS1117-3.3给STM32供电模拟部分单独用一颗AMS1117-5.0并且在电源引脚附近加了100nF和10uF的电容滤波。实测下来电源噪声对测量精度影响很大尤其是整流滤波后的直流信号纹波大了ADC读数就会跳。3. 硬件搭建与关键参数计算3.1 元器件清单与成本核算先列一下我用的元器件清单都是常见型号淘宝上随便买元器件型号/参数数量单价元STM32最小系统板F103C8T6112运放LM35820.5MOS管IRF54011.5精密电阻10Ω 1%20.3金属膜电阻1kΩ 1%100.1电解电容10uF/25V40.2瓷片电容100nF100.1二极管1N414840.1接线端子2P20.5洞洞板5x7cm12合计约20元加上STM32最小系统板和电源模块总成本控制在50元以内。相比动辄几百块的专业内阻测试仪这个成本几乎可以忽略不计。3.2 恒流源参数计算与调试恒流源的输出电流由参考电压和采样电阻决定I Vref / Rsense。我选的参考电压是1V采样电阻是10Ω所以注入电流是100mA。等等前面不是说10mA吗这里我改主意了。10mA的注入电流在电池两端产生的交流电压太小只有几百微伏放大100倍后也才几十毫伏ADC采集精度不够。100mA的注入电流在100mΩ内阻的电池上产生10mV的交流电压放大100倍后是1VADC采集刚刚好。但100mA的电流对电池有影响吗铅酸电池的容量通常是几十安时100mA的交流电流对它来说就是毛毛雨完全不会造成损伤。而且交流信号是正负交替的不会导致电池极化。参考电压怎么来我用的是STM32的DAC输出。F103C8T6没有DAC所以只能用PWM加RC滤波来产生。PWM频率设为72kHz占空比50%经过两级RC滤波后得到1.65V左右的直流电压。然后用电阻分压得到1V的参考电压。这个参考电压的稳定性直接影响恒流精度所以滤波电容要用好一点的我用了10uF的钽电容。注意PWM滤波产生的参考电压会有纹波纹波会调制到恒流输出上导致注入电流不稳定。我的解决办法是在运放的同相端加一个大的电解电容100uF把纹波滤得更干净。实测下来加了电容之后恒流输出的波动从5%降到了1%以内。3.3 差分放大电路的增益计算差分放大电路的增益由反馈电阻和输入电阻决定。我用的是三运放仪表放大器结构增益公式是G 1 2R/Rg。其中R是反馈电阻Rg是增益电阻。我需要100倍的增益所以1 2R/Rg 100即2R/Rg 99。取R 49.5kΩRg 1kΩ这样增益就是100倍。实际电路中我用的是51kΩ和1kΩ的金属膜电阻增益约为103倍差别不大。为什么需要这么高的增益因为电池内阻通常在10mΩ到100mΩ之间100mA的注入电流产生的交流电压只有1mV到10mV。如果不放大ADC根本分辨不出来。放大100倍后信号变成100mV到1VADC的12位分辨率在3.3V参考电压下最小分辨电压是0.8mV足够用了。3.4 整流滤波电路的时间常数选择整流滤波电路的时间常数决定了测量响应速度。时间常数太大响应慢测量一次要等好几秒时间常数太小纹波大ADC读数跳动。我选的时间常数是100ms具体是滤波电阻10kΩ滤波电容10uF。这个时间常数下整流后的直流电压在100ms内达到稳定值的63%300ms内达到95%。对于1kHz的交流信号100ms相当于100个周期纹波已经很小了。实际调试时我用示波器看了整流滤波后的波形纹波峰峰值大概在5mV左右相对于1V的信号来说纹波率是0.5%可以接受。如果你对精度要求更高可以把时间常数加大到200ms但测量速度会变慢。4. 软件实现与代码解析4.1 系统初始化与时钟配置STM32的时钟配置是基础但也是最容易出错的地方。我用的是外部8MHz晶振经过PLL倍频到72MHz。系统时钟初始化代码如下void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct {0}; RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.HSIState RCC_HSI_ON; RCC_OscInitStruct.PLL.PLLState RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL RCC_PLL_MUL9; HAL_RCC_OscConfig(RCC_OscInitStruct); RCC_ClkInitStruct.ClockType RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider RCC_HCLK_DIV1; HAL_RCC_ClockConfig(RCC_ClkInitStruct, FLASH_LATENCY_2); }这段代码把系统时钟配置为72MHzAPB1为36MHzAPB2为72MHz。APB1上挂的是TIM2APB2上挂的是ADC和USART。时钟配置错了定时器频率和串口波特率都会不对所以这一步一定要仔细核对。4.2 PWM产生1kHz激励信号我用TIM2的通道1产生PWM信号频率1kHz占空比50%。TIM2挂在APB1上时钟频率是36MHz。定时器频率计算公式是Fpwm Ftim / ((ARR1)(PSC1))。我要1kHz的PWM所以(ARR1)(PSC1) 36000。取PSC35ARR999这样Fpwm 36MHz / (36*1000) 1kHz。void PWM_Init(void) { TIM_HandleTypeDef htim2; TIM_OC_InitTypeDef sConfigOC {0}; htim2.Instance TIM2; htim2.Init.Prescaler 35; htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 999; htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim2); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 500; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim2, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); }PWM输出引脚是PA0接到恒流源的参考电压输入端。占空比50%时经过RC滤波后得到1.65V的直流电压再经过分压得到1V的参考电压。4.3 ADC采集与滤波算法ADC采集的是整流滤波后的直流电压对应电池两端的交流电压幅值。我用的是ADC1的通道0对应PA0引脚。等等PA0已经被TIM2占用了所以ADC输入要换一个引脚。我用的是PA1对应ADC1_IN1。ADC配置为12位分辨率单次转换模式软件触发。采样时间设为239.5个周期这是最长的采样时间适合高阻抗信号源。ADC初始化代码如下void ADC_Init(void) { ADC_HandleTypeDef hadc1; ADC_ChannelConfTypeDef sConfig {0}; hadc1.Instance ADC1; hadc1.Init.ScanConvMode 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; HAL_ADC_Init(hadc1); sConfig.Channel ADC_CHANNEL_1; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig); }采集到的ADC值需要转换成电压V ADC_Value * 3.3 / 4096。然后除以放大倍数100得到电池两端的实际交流电压。最后用欧姆定律算内阻R V / I其中I是注入电流100mA。为了提高测量精度我用了滑动平均滤波。连续采集16次去掉最大值和最小值剩下的14次求平均。这样能有效抑制随机噪声测量结果更稳定。float Get_Voltage(void) { uint16_t adc_values[16]; uint32_t sum 0; uint16_t max 0, min 4096; for (int i 0; i 16; i) { HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 100); adc_values[i] HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); } for (int i 0; i 16; i) { if (adc_values[i] max) max adc_values[i]; if (adc_values[i] min) min adc_values[i]; sum adc_values[i]; } sum sum - max - min; float avg sum / 14.0; return avg * 3.3 / 4096.0; }4.4 串口输出与数据格式测量结果通过USART1输出到串口助手波特率115200。输出格式是电池内阻值单位mΩ、电池电压单位V、测量状态。代码如下void UART_Print(float resistance, float voltage) { char buffer[64]; sprintf(buffer, Resistance: %.2f mOhm, Voltage: %.2f V\r\n, resistance, voltage); HAL_UART_Transmit(huart1, (uint8_t*)buffer, strlen(buffer), 1000); }串口输出方便调试也方便后续接上位机做数据记录。如果你想加个OLED屏幕显示也可以但那是另一个话题了。5. 实测数据与精度分析5.1 测量流程与操作步骤测量流程很简单但有几个细节要注意先把电池充满电静置2小时让电池电压稳定。把测试仪的正负极夹子接到电池正负极上注意不要接反。上电STM32开始产生1kHz激励信号注入电池。等待300ms让整流滤波电路稳定。采集16次ADC值滤波后计算内阻。串口输出结果同时LED闪烁一次表示测量完成。每隔1秒重复测量一次连续测量10次取平均值。提示测量时电池不要接任何负载否则负载电流会干扰测量结果。另外夹子要夹紧接触电阻会影响测量精度。我试过用鳄鱼夹和开尔文夹开尔文夹的接触电阻更小测量结果更稳定。5.2 实测数据对比我拿三块电池做了测试一块全新的12V 20Ah铅酸电池一块用了两年的电动车电池一块已经鼓包的报废电池。测量结果如下电池状态测量内阻mΩ开路电压V判断结果全新电池12.512.85健康两年电池28.312.72容量下降鼓包电池85.612.60报废全新电池的内阻在12mΩ左右两年电池的内阻翻了一倍多鼓包电池的内阻是全新的近7倍。这个结果和我的预期一致铅酸电池的内阻随着老化会显著增大内阻越大电池的放电能力越差。为了验证测量精度我用一台专业的内阻测试仪做了对比。专业仪器测出的全新电池内阻是11.8mΩ我的测试仪测出12.5mΩ误差约6%。两年电池专业仪器测出26.5mΩ我的测试仪测出28.3mΩ误差约7%。这个精度对于判断电池好坏来说完全够用了。5.3 误差来源分析与改进方向误差主要来自三个方面恒流源精度LM358的失调电压是2mV采样电阻是10Ω所以注入电流的误差是0.2mA相对于100mA的注入电流误差是0.2%。这个误差很小可以忽略。差分放大电路增益误差金属膜电阻的精度是1%增益误差大约是2%。这个误差可以通过校准来消除。我用一个已知阻值的精密电阻10mΩ代替电池测量输出电压反推实际增益然后在代码里做补偿。ADC量化误差12位ADC在3.3V参考电压下最小分辨电压是0.8mV。放大100倍后对应的电池电压是8uV。对于10mΩ的内阻100mA注入电流产生的电压是1mVADC能分辨出1/125的变化足够用了。如果想进一步提高精度可以换用16位ADC比如ADS1115或者用锁相放大技术提取信号。但那样成本就上去了对于判断电池好坏来说没必要。6. 常见问题与排查技巧实录6.1 测量结果跳动大怎么办这是最常见的问题。原因可能有三个电源噪声、接触不良、滤波时间常数太小。先检查电源。用示波器看STM32的3.3V电源和模拟部分的5V电源纹波峰峰值应该在10mV以内。如果纹波太大加电容滤波或者换用线性稳压器。再检查接触。夹子夹紧电池极柱接触电阻要小。我试过用万用表测量夹子和极柱之间的电阻如果大于1mΩ就要重新夹。最后检查滤波时间常数。如果时间常数太小整流后的直流电压纹波大ADC读数就会跳。把滤波电容从10uF加大到47uF测量结果会稳定很多。6.2 注入电流不稳定怎么办注入电流不稳定测量结果就会飘。用万用表串联在恒流源输出回路中测量实际注入电流。如果电流波动超过1%说明恒流源有问题。检查运放的参考电压是否稳定。用示波器看运放同相端的电压纹波应该在1mV以内。如果纹波大加大滤波电容。检查MOS管是否工作在恒流区。如果MOS管的漏源电压太小它可能工作在线性电阻区而不是恒流区。用示波器看MOS管的漏极电压应该在1V以上。6.3 ADC读数一直是0或者满量程ADC读数一直是0说明信号没有传到ADC引脚。检查差分放大电路的输出是否正常用示波器看运放输出端有没有交流信号。如果没有检查运放供电是否正常反馈电阻是否焊接良好。ADC读数一直是满量程说明信号太大了。检查放大倍数是否太大或者整流滤波电路是否自激振荡。把放大倍数降低或者在运放输出端加一个小电容100pF做相位补偿。6.4 串口没有输出串口没有输出先检查波特率是否匹配。我用的是115200串口助手也要设成115200。再检查TX和RX是否接反了STM32的TX接串口模块的RXRX接TX。如果还是不行用示波器看STM32的TX引脚有没有波形。如果没有波形说明串口初始化有问题检查USART1的时钟是否使能GPIO引脚是否配置为复用推挽输出。注意STM32F103的USART1挂在APB2上时钟是72MHz。USART2和USART3挂在APB1上时钟是36MHz。如果你用的是USART2波特率计算时要用36MHz否则波特率会不对。6.5 常见问题速查表问题现象可能原因排查方法解决方法测量结果跳动大电源噪声示波器看电源纹波加电容滤波测量结果跳动大接触不良万用表测接触电阻重新夹紧测量结果跳动大滤波时间常数小加大滤波电容换47uF电容注入电流不稳定参考电压纹波大示波器看运放同相端加大滤波电容注入电流不稳定MOS管工作区不对示波器看漏极电压调整电路参数ADC读数一直为0信号未传到ADC示波器看运放输出检查焊接ADC读数满量程放大倍数太大检查反馈电阻降低增益串口无输出波特率不匹配检查串口助手设置统一波特率串口无输出TX/RX接反检查接线交换TX和RX7. 项目扩展与个人实操体会这个项目做完之后我发现它还有很多可以扩展的地方。比如加一个OLED屏幕把测量结果直接显示出来不用连电脑。或者加一个SD卡模块把测量数据记录下来方便做趋势分析。再或者加一个蓝牙模块把数据传到手机上看。我实际用下来最实用的扩展是加一个蜂鸣器。测量完成时响一声不用盯着串口助手看。还有就是加一个按键按一下测一次不用一直循环测量。关于精度我的体会是对于判断电池好坏来说这个精度完全够用了。铅酸电池的内阻从12mΩ变到28mΩ变化超过一倍测量误差才6%完全不会误判。但如果你想做更精确的容量估算那就需要更高精度的ADC和更复杂的算法了。最后分享一个小技巧测量之前先给电池加一个短暂的放电脉冲比如1A放电100ms然后再测量。这样能激活电池表面的活性物质测量结果更接近真实内阻。我试过加了放电脉冲之后测量结果的重复性提高了不少。这个项目我从开始做到调试完成大概花了两个周末的时间。中间踩了不少坑比如运放自激振荡、ADC参考电压不稳、串口波特率不对等等。但正是这些坑让我对模拟电路和STM32的理解更深了一层。如果你也在做类似的项目希望这篇文章能帮你少走一些弯路。