做嵌入式这么多年,测电阻这件事我一直觉得没表面上那么简单。手里虽然有万用表,但每次测低阻值电阻或者在线测量时,接触电阻、导线压降带来的误差能让人抓狂。后来项目里需要做一个自动化的电阻测量模块,干脆自己用STM32搭了一个恒流源法电阻测量仪,顺带把显示也换成了0.96寸OLED,效果还不错。这篇就把整个设计与实现过程拆开讲清楚,从恒流源原理、硬件电路、软件代码到实际测试数据,全都有,适合正在做相关课设、毕业设计或者想自己做一个万用表模块的朋友参考。
1. 方案如何选型与实际硬件设计逻辑
很多人拿到“测量电阻”这个需求,第一反应是用电阻分压加ADC去读。这个方案本身没有错,但一旦涉及宽量程测量,问题就很明显。分压法需要根据待测电阻的预估范围反复调整分压电阻,否则小电阻上分到的电压太小,ADC的分辨率根本不够看;大电阻又可能超过量程。恒流源法就好比用一个已知大小的水流去冲一个未知宽度的河道,根据水位算河道截面,思路更直接,测量值跟待测电阻成线性关系,不用去解分压方程。
1.1 恒流源法测量的基本原理
恒流源法的核心公式只有一个:Rx = Ux / Iset。其中Iset是流过待测电阻的恒定电流,Ux是待测电阻两端的电压,通过STM32的ADC采集得到。只要恒流源足够稳定,ADC的采样足够准,整个系统的精度就完全取决于这两个环节。
这个方案最大的好处是线性度好,计算简单,而且电流恒定后,电源电压波动和导线电阻对测量结果的影响被大幅削弱。举个例子,如果我设定1mA的恒流电流,当接入一个10kΩ电阻时,电阻两端电压是10V,这个电压可以直接进ADC;接入100Ω电阻时,电压只有0.1V,虽然小,但因为ADC是12位的,也还能读到一定的分辨率。如果想覆盖更大的范围,就必须分档切换电流。
1.2 量程设计与电流档位划分
在设计量程时,我参考了数字万用表常见的量程划分思路。核心原则是保证每个档位下待测电阻两端的电压都落在ADC的合适采集区间内,既不能超过ADC参考电压,也不能小到接近噪声底。
以STM32F103的ADC为例,参考电压设定为3.3V,12位分辨率,理论最小分辨电压大约是0.8mV。为了留出余量,我把每个档位的满量程电压控制在2V左右。
- 低阻档:恒流10mA,适合测1Ω到200Ω,满量程约2V
- 中阻档:恒流100uA,适合测200Ω到20kΩ,满量程约2V
- 高阻档:恒流10uA,适合测2kΩ到200kΩ,满量程约2V
- 超高阻档:恒流1uA,适合测20kΩ到2MΩ,满量程约2V
这样分档的好处是待测电阻两端电压始终落在0.1V到2V这个区间内,ADC的有效分辨率利用得很充分,又不会过压。切换方式我用的是继电器或者模拟开关,实际制作中如果对切换速度要求不高,用继电器更稳妥,接触电阻小。
2. 硬件电路设计与关键器件选型
硬件电路是整个仪器的基础,恒流源建不出来,后面程序写得再好也没用。我在设计时花了不少时间在恒流源电路的选型上,试过LM334、试过三极管分立方案,最后发现运放加基准源的方案精度和温度稳定性综合表现最好。
2.1 恒流源电路结构与运放选型
电路上我选用了运算放大器深度负反馈结构,配合精密基准源产生恒定电流。核心思路是:用基准源产生一个稳定的参考电压,输入到运放的同相端,运放输出驱动调整管,在采样电阻上形成压降,运放的反馈端检测采样电阻电压,最终使得采样电阻上的电压等于参考电压,从而电流等于参考电压除以采样电阻。
这里运放的选型非常关键。普通LM358也能工作,但失调电压和温漂都不算小。我实际用下来,OP07或者OPA2277这类精密运放效果更好。具体电路参数上,如果基准源输出2.5V,采样电阻用250Ω,那么恒流就是10mA;采样电阻换成25kΩ,恒流就变成100uA。切换采样电阻就能切换档位,这也是我最终采用的档位切换方式。
补充一点,运放输出端到调整管基极之间要加限流电阻,防止上电瞬间电流过大。调整管我用的是一只小功率MOS管,压降小,带载能力强,选三极管的话最好用达林顿结构,不然小电流档位容易压不住。
2.2 ADC采样电路与输入保护
STM32内置的是逐次逼近型ADC,输入通道的等效输入阻抗不算高。恒流源输出端直接接ADC引脚会存在两个问题:一是恒流源输出电压被ADC内部采样电容分压,导致采样值偏低;二是如果待测电阻很大,输出阻抗高,信号源驱动ADC采样电容的能力不足,采样结果会明显跳动。
我的处理办法是加一级电压跟随器。用另一个运放做跟随,把恒流源输出的电压缓冲后再送入ADC。跟随器输入阻抗极高,基本不会从恒流源吸取电流,采样值也更稳定。
输入保护也很重要,不能忽略。万一待测电阻开路或者误接高压,恒流源输出端会瞬间被拉得很高,直接烧ADC引脚。我在ADC输入端对地并联了双向TVS管,同时串了一颗1kΩ电阻限流,实测下来误接过12V电压也没有损坏芯片。
2.3 OLED显示模块与整机结构
显示部分用的是最常见的0.96寸I2C接口OLED,SSD1306驱动,4个引脚:VCC、GND、SCL、SDA。这个模块批量使用时经常遇到点不亮的问题,但大部分情况不是模块坏了,而是地址不对或者上拉电阻缺失。我的板上SCL和SDA都加了4.7kΩ上拉电阻,OLED供电用3.3V,和STM32同电压,避免电平不匹配。
整机结构上,我做的是一个小型PCB,电源入口加了LDO稳压到3.3V,MCU用STM32F103C8T6最小系统,预留了SWD下载口和一个串口调试口。这样调试的时候方便打印内部数据,量产或长期运行时通过OLED看结果就行。
3. 软件程序设计与关键模块实现
硬件只算完成了一半,软件才是把这个仪器真正“盘活”的部分。我基于STM32的HAL库开发,代码结构分了三层:底层是OLED驱动和ADC读取,中间是滤波与电阻计算,顶层是量程切换和显示逻辑。工程在Keil5里编译,烧录用ST-Link,非常常规。
3.1 系统软件主流程
主程序的逻辑相对直白,主循环里跑一个状态机:
- 上电初始化:配置时钟、GPIO、I2C、ADC、UART
- OLED显示开机画面
- 进入自动测量循环:先默认中阻档,读取一次电阻值,根据结果自动切换到最优档位,再重新测量
- 数据处理:软件滤波、去零漂、线性修正
- OLED刷新:显示当前档位、采样电压、计算电阻值
我实际把一次完整的测量周期控制在300ms以内,其中ADC采样的时间占了大头。如果以后需要更快的刷新率,可以把采样次数减少或者提高主频。
3.2 ADC采样与软件滤波算法
ADC配置上,我开了DMA,扫描多个通道,一次触发连续采集32次,然后软件做中位值平均滤波。中位值平均滤波的原理是先把采集到的数据排序,去掉最大值和最小值,再对剩下的数据求平均。这个方法对付随机脉冲干扰和周期性噪声都有效果,比单纯滑动平均更抗尖峰。
下面是关键的ADC读取与滤波代码,实测跑起来效果很好:
#define ADC_SAMPLE_NUM 32 uint32_t adc_raw_buf[ADC_SAMPLE_NUM]; // 排序函数,用于中位值平均滤波 void sort_array(uint32_t *arr, uint32_t len) { for (uint32_t i = 0; i < len - 1; i++) { for (uint32_t j = 0; j < len - 1 - i; j++) { if (arr[j] > arr[j + 1]) { uint32_t temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } uint32_t get_adc_filtered(ADC_HandleTypeDef *hadc, uint32_t channel) { ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = channel; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(hadc, &sConfig); for (uint32_t i = 0; i < ADC_SAMPLE_NUM; i++) { HAL_ADC_Start(hadc); HAL_ADC_PollForConversion(hadc, 10); adc_raw_buf[i] = HAL_ADC_GetValue(hadc); HAL_ADC_Stop(hadc); } sort_array(adc_raw_buf, ADC_SAMPLE_NUM); uint64_t sum = 0; for (uint32_t i = 2; i < ADC_SAMPLE_NUM - 2; i++) { sum += adc_raw_buf[i]; } return (uint32_t)(sum / (ADC_SAMPLE_NUM - 4)); }这里采样时间我配置成了239.5个周期,也就是最长的采样时间。因为恒流源输出经过跟随器之后输出阻抗虽然低了,但依然存在高频噪声,延长采样时间相当于一个内置的硬件低通滤波,对读数稳定帮助很大。
3.3 OLED显示驱动与界面设计
OLED驱动直接基于HAL库的I2C接口写。初始化SSD1306的时候,注意要先发软件复位命令,再关闭显示、清屏、设置显存地址模式,最后开启显示。现在网上很多现成的HAL库驱动代码,但不少都缺少关键延时,导致复位不彻底,我建议写驱动的时候先对照SSD1306数据手册走一遍命令时序,别直接抄。
显示界面分了三个区域:顶部显示当前量程和电流档位,中间一行大字显示电阻值,底部显示电阻两端的实测电压。OLED本身刷新速度不快,所以动态动画别做太多,我做了个简单的开机动画就收手了。
字库方面,如果只是显示数字和字母,用ASCII字库就够了,取模软件生成数组就行。如果要显示汉字,就需要准备16x16的GB2312字库。我的界面上用了“测量”、“档位”等中文,所以提前在工程里塞了一份常用汉字字模。
3.4 自动量程切换与电阻计算
电阻计算不是直接用采样值除以电流那么简单。实际系统中存在零漂和增益误差,我采用了两点校准法,把计算流程拆成两步:先电压校准,再电阻计算。
电压校准是在没有接负载时记录ADC的零漂值,然后在已知电压下记录换算系数。这样最终计算电压时用(ADC值 - 零点偏移)乘以系数,把跟随器和ADC通道的零点误差一起消掉。电阻计算则直接除以当前恒流值,同时做了温度补偿修正。
自动量程切换我采用“先粗测,再精测”的策略。第一轮先固定用中阻档(100uA)测量,根据得到的大致数值判断目标量程,第二轮切换量程后再正式采样。这样避免低阻档直接测大电阻导致输出电压超过ADC参考电压,也让最终读数更稳。切换量程到重新采样之间加入100ms延时,留时间让恒流源稳定下来,这点尤其重要,不然刚切换完瞬间去读ADC,采到的还都是瞬态值。
4. 实测数据与误差分析
代码写完,硬件打样回来,最期待的就是实测。我准备了一组标称值的精密电阻,从10Ω到1MΩ,用这个仪器测了一遍,又跟四位半万用表对照了一下,结果还是很有参考价值的。
4.1 实测精度数据记录
以下是典型档位下的实测记录:
| 标称电阻 | 设定电流 | 实测电压 | 实测电阻 | 实际相对误差 |
|---|---|---|---|---|
| 10Ω | 10mA | 0.1012V | 10.12Ω | 1.20% |
| 100Ω | 10mA | 1.0045V | 100.45Ω | 0.45% |
| 1kΩ | 100uA | 0.9950V | 995.0Ω | 0.50% |
| 10kΩ | 100uA | 0.9942V | 994.2Ω | 0.58% |
| 100kΩ | 10uA | 0.9985V | 99.85kΩ | 0.15% |
| 1MΩ | 1uA | 0.9978V | 997.8kΩ | 0.22% |
低阻档10Ω测量误差偏大,主要原因是接触电阻和PCB走线电阻的叠加。10mA电流流过10Ω电阻时,如果有0.1Ω接触电阻,就会带来1%的误差,这在低阻测量中占比很可观。这也解释了为什么商品万用表测低阻时要用四线开尔文接法。
4.2 误差来源与标定方法分析
从数据看,中高阻档位的误差普遍在0.5%以内,这个精度对于DIY仪器来说已经算不错了。误差的主要来源有三个:恒流源的电流精度、ADC参考电压的精度、以及开关接触电阻。
恒流源电流精度取决于基准源电压精度和采样电阻精度。我用的采样电阻是0.1%精密电阻,基准源是TL431,温漂性能一般,室温下勉强够用;如果对精度要求更高,建议换REF3030这类低温漂基准源。ADC参考电压直接影响电压换算精度。STM32的参考电压其实是内部VDD经过LDO出来的,虽然稳定度还行,但绝对精度并不保证。我实测它的实际值在3.28V左右,和我们配置时假设的3.3V有偏差,所以两点校准时我以实际测量值为准,而不是用理论值。
标定方法上,我用的是一个简单线性模型:最终电压 = (ADC原始值 - 零点偏移) × 电压系数。零点偏移在开机时自动测量,电压系数在出厂时写入Flash。这样即使换了一片不同供电电压的STM32,也只需要重新标定一次系数,不需要改代码。
5. 常见问题与调试技巧汇总
这套仪器做完之后,我在反复调试中踩了不少坑,尤其是OLED显示和ADC采样这两个方面。这里挑几个典型案例分享一下,帮大家少走弯路。
5.1 OLED点不亮与花屏的排查实录
0.96寸OLED批量点不亮这个问题,网上讨论很多,我实际碰到的原因也五花八门。最常见的是SCL、SDA接反了,其次就是I2C地址不对。SSD1306默认地址一般是0x3C,但某些模块是0x3D,跑不通的时候先用I2C扫描程序扫一下地址,比肉眼翻手册快得多。
还有一次遇到的现象是OLED上电后满屏雪花点,看起来像花屏。后来排查发现是OLED的VCC接到了5V上,而I2C信号是3.3V,虽然模块大多自带稳压,但显存初始化要求先断电再上电,否则会进入不可控状态。解决方法是程序中上电后延时200ms再初始化OLED,并且把VCC改成3.3V供电。
另外一个值得注意的细节:OLED的I2C总线需要上拉电阻,很多模块板上没有。如果直接用杜邦线连接STM32和OLED,容易被干扰或者根本不工作,我建议在靠近STM32的一侧加装4.7kΩ到10kΩ的上拉电阻到3.3V。
5.2 ADC采样数值跳动与恒流源噪声处理
ADC读数来回跳是高频出现的问题。我最初以为是滤波不够,后来用示波器一测,发现恒流源输出端有一大堆几百kHz的振荡。问题出在运放的反馈补偿电容没加,电路自激了。在运放反馈电阻上并联一个几十pF的小电容,振荡立刻消失。
还有一个容易忽视的地方是地线处理。恒流源电流从采样电阻回来走的路径如果和ADC参考地靠得太近,地上压降就会被ADC当作信号采进去。我把电源地、模拟地、数字地做了单点汇接,模拟电路集中在板子的一个角落,信号线尽量短粗,采样出来的数据稳定了很多。
5.3 排查思路速查表
我把调试过程中常用的几个排查点整理成了速查表,方便上电异常时按顺序检查:
| 现象 | 优先检查项 | 备选方案 |
|---|---|---|
| OLED完全无显示 | I2C地址、接线顺序 | 上拉电阻、OLED供电电压 |
| OLED花屏或乱码 | 复位时序、延时不足 | 重新上电时序、检查显存访问方式 |
| ADC采样明显偏高/偏低 | 校准系数、参考电压实际值 | 重新做两点标定 |
| ADC数值乱跳 | 运放自激、采样时间 | 加补偿电容、延长ADC采样周期 |
| 高阻档读数偏低 | 输入阻抗不足、跟随器输入偏置电流 | 换JFET输入运放 |
| 量程切换后读数异常 | 切换后稳定时间不足 | 增加延时到100ms以上 |
还有一个隐藏很深的坑:如果程序里切换了ADC通道,但通道配置没有生效就立刻开始转换,会采到上一次通道的残留值。这里建议切换通道后丢弃前两次转换结果,再进入正式的采样循环。
6. 实际使用中的一些心得与细节建议
最后再补充几个我个人用了很久之后才总结出来的细节,适合后续想继续优化这版设计的朋友参考。
关于电流档位,我目前用的是四档,但对于小阻值测量精度要求更高的场景,可以考虑增加一档更大的电流,比如100mA,专门测1Ω以下的微小电阻。不过这时候必须用四线开尔文接法,不然线阻误差完全掩盖了真实电阻值。
关于ADC精度,如果觉得12位不够用,有两个低成本改进方向:一是用外部基准源芯片,比如REF3030,提供更精确的3.0V参考电压,稳定性比内部参考好很多;二是在软件里用过采样技术,对同一个电压采样多次累加取平均,等效提高分辨率。后者对恒流源的稳定性要求会更高,因为信号本身要足够稳,累加才有意义。我在这个项目里用了中位值平均滤波,效果介于两者之间,性价比比较高。
OLED显示部分,如果想做交互,可以加上两个按键用于手动切换档位,并支持长按进入校准菜单。我后来的版本就在代码里预留了这个入口,校准的时候不用改源码,直接通过OLED菜单操作,非常方便。这也是这个项目后续最值得扩展的地方,有了按键菜单后,整套设备就更接近一个独立仪表了。