1. 为什么我要用STM32做一台蓄电池内阻测试仪
铅酸蓄电池这东西,玩过电动车、UPS、太阳能储能的朋友都不陌生。它便宜、皮实、大电流放电能力强,但有个特别烦人的特点——坏得悄无声息。你拿万用表量它的开路电压,12V还是12V,看起来一切正常,可一接上负载,电压瞬间掉到9V,设备直接重启。这种情况十有八九是电池内阻变大了,极板硫化或者失水导致活性物质失效。
问题在于,普通万用表根本测不出内阻。你想知道电池健康状态,要么买一台专业的内阻测试仪(便宜的也要大几百上千),要么用交流注入法自己搭电路。我手头正好有几块退役的电动车电池,扔了可惜,留着又不知道还能不能用,于是决定用STM32做一个铅酸蓄电池内阻测试仪。这个项目的核心思路是:用STM32产生交流激励信号,注入电池,测量电池两端的交流响应,通过欧姆定律算出内阻。整个方案成本控制在五十块以内,精度足够判断电池好坏,代码全部开源。
这篇文章适合谁看?如果你有基本的STM32开发经验,会用Keil或者STM32CubeIDE,懂一点模拟电路基础,那就可以直接跟着做。如果你是完全的新手,也没关系,我会把每个环节的原理和操作都讲清楚,你照着抄作业也能跑起来。关键词就三个:STM32、铅酸蓄电池、内阻测试仪。下面我从方案选型开始,一步步拆解整个实现过程。
2. 内阻测试的方案选型与核心原理拆解
2.1 为什么选交流注入法而不是直流放电法
测电池内阻,市面上主要有两种方法:直流放电法和交流注入法。
直流放电法的思路很直接:给电池接一个已知阻值的负载,测量负载前后的电压差,用ΔV/ΔI算内阻。这个方法原理简单,但有几个致命问题。第一,放电电流必须足够大才能测出准确的内阻,通常需要几安培到几十安培,对铅酸电池来说,每次测量都是一次深度放电,测几次电池就废了。第二,极化效应会干扰测量结果,电流刚加上去的瞬间和稳定后的电压不一样,你取哪个值?第三,大电流放电需要大功率电阻和散热措施,体积和成本都上去了。
交流注入法就不一样了。它给电池注入一个高频交流信号(通常1kHz左右),电流很小(几十毫安),不会对电池造成任何损伤。电池的内阻在交流信号下表现为纯电阻特性,测量的是欧姆内阻,不受极化影响。而且交流信号频率高,可以避开电池内部的电化学极化,测出来的值更接近真实内阻。
我选的是交流注入法,具体实现方式是:用STM32的DAC或者PWM产生1kHz正弦波,经过恒流源电路转换成交流恒流信号,注入电池。然后用差分放大电路采集电池两端的交流电压,经过整流、滤波、ADC采样,算出交流电压幅值。已知注入电流幅值,用欧姆定律R=V/I就算出内阻了。
注意:交流注入法的关键是恒流源精度和电压检测精度。恒流源不稳定,注入电流就会波动,测量结果直接飘。电压检测的噪声抑制也很重要,因为电池两端的交流信号只有几毫伏到几十毫伏,很容易被干扰淹没。
2.2 STM32的选型与资源分配
STM32系列型号很多,从F0到H7,选哪个合适?我的原则是:够用就行,不浪费性能,也不为了省几块钱给自己找麻烦。
这个项目需要的资源有:一个定时器产生PWM或者触发DAC,一个ADC采集电压,一个串口输出数据,几个GPIO控制模拟开关和LED指示。运算量不大,F1系列完全够用。我手头正好有一块STM32F103C8T6最小系统板,也就是常说的“蓝板”,72MHz主频,64KB Flash,20KB 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最小系统板 | F103C8T6 | 1 | 12 |
| 运放 | LM358 | 2 | 0.5 |
| MOS管 | IRF540 | 1 | 1.5 |
| 精密电阻 | 10Ω 1% | 2 | 0.3 |
| 金属膜电阻 | 1kΩ 1% | 10 | 0.1 |
| 电解电容 | 10uF/25V | 4 | 0.2 |
| 瓷片电容 | 100nF | 10 | 0.1 |
| 二极管 | 1N4148 | 4 | 0.1 |
| 接线端子 | 2P | 2 | 0.5 |
| 洞洞板 | 5x7cm | 1 | 2 |
| 合计 | 约20元 |
加上STM32最小系统板和电源模块,总成本控制在50元以内。相比动辄几百块的专业内阻测试仪,这个成本几乎可以忽略不计。
3.2 恒流源参数计算与调试
恒流源的输出电流由参考电压和采样电阻决定:I = Vref / Rsense。我选的参考电压是1V,采样电阻是10Ω,所以注入电流是100mA。等等,前面不是说10mA吗?这里我改主意了。10mA的注入电流在电池两端产生的交流电压太小,只有几百微伏,放大100倍后也才几十毫伏,ADC采集精度不够。100mA的注入电流在100mΩ内阻的电池上产生10mV的交流电压,放大100倍后是1V,ADC采集刚刚好。
但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到1V,ADC的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); }这段代码把系统时钟配置为72MHz,APB1为36MHz,APB2为72MHz。APB1上挂的是TIM2,APB2上挂的是ADC和USART。时钟配置错了,定时器频率和串口波特率都会不对,所以这一步一定要仔细核对。
4.2 PWM产生1kHz激励信号
我用TIM2的通道1产生PWM信号,频率1kHz,占空比50%。TIM2挂在APB1上,时钟频率是36MHz。定时器频率计算公式是:Fpwm = Ftim / ((ARR+1)(PSC+1))。我要1kHz的PWM,所以(ARR+1)(PSC+1) = 36000。取PSC=35,ARR=999,这样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.5 | 12.85 | 健康 |
| 两年电池 | 28.3 | 12.72 | 容量下降 |
| 鼓包电池 | 85.6 | 12.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注入电流产生的电压是1mV,ADC能分辨出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接串口模块的RX,RX接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和RX |
7. 项目扩展与个人实操体会
这个项目做完之后,我发现它还有很多可以扩展的地方。比如加一个OLED屏幕,把测量结果直接显示出来,不用连电脑。或者加一个SD卡模块,把测量数据记录下来,方便做趋势分析。再或者加一个蓝牙模块,把数据传到手机上看。
我实际用下来,最实用的扩展是加一个蜂鸣器。测量完成时响一声,不用盯着串口助手看。还有就是加一个按键,按一下测一次,不用一直循环测量。
关于精度,我的体会是:对于判断电池好坏来说,这个精度完全够用了。铅酸电池的内阻从12mΩ变到28mΩ,变化超过一倍,测量误差才6%,完全不会误判。但如果你想做更精确的容量估算,那就需要更高精度的ADC和更复杂的算法了。
最后分享一个小技巧:测量之前,先给电池加一个短暂的放电脉冲(比如1A放电100ms),然后再测量。这样能激活电池表面的活性物质,测量结果更接近真实内阻。我试过,加了放电脉冲之后,测量结果的重复性提高了不少。
这个项目我从开始做到调试完成,大概花了两个周末的时间。中间踩了不少坑,比如运放自激振荡、ADC参考电压不稳、串口波特率不对等等。但正是这些坑,让我对模拟电路和STM32的理解更深了一层。如果你也在做类似的项目,希望这篇文章能帮你少走一些弯路。