简介:一套完整的电阻、电感与电容测量仪设计和制作工程资料,面向电子工程专业学生、嵌入式开发者和电子设计竞赛参与者。项目基于STM32F429微控制器与Keil环境,采用C和C++混合编程,覆盖电阻四端子法、电感自谐振频率测量、电容并联谐振测阻抗等原理,可学习仪表类项目从硬件搭建到软件调试的完整链路。资源共312个文件,以C源文件、头文件和Keil工程配置为主,附带编译生成的十六进制烧录文件、调试辅助文件及项目备份,压缩包约11.5MB,打开Keil工程即可直接编译运行和修改。目前已有1867人学习浏览;通过该工程可掌握ADC数据采集、测量误差校正、LCD显示界面设计,以及C与C++分层编程的组织方式,是一份步骤完整、适合复现和二次开发的高价值参考方案。资源包目录结构清晰,源码注释完整,便于按模块逐项学习。
1. 电阻电感电容测量仪:把测元件变成测复数阻抗
万用表量电阻很顺手,可量电容只能看容量是否在标称范围内,量电感就得靠电感表或临时搭一个LC振荡器。真正需要确认的是电解电容在100kHz下的ESR、功率电感的Q值、谐振电容在标称频率附近的实部虚部,普通万用表给不了。
这套电阻电感电容测量仪的思路,是把所有元件测量统一成“复数阻抗测量”:DDS产生正弦激励,运放构成自动平衡电桥,ADC同步采样两路电压,C/C++负责正交解调、量程切换、校准和结果显示。硬件成本几十元级别,频率范围覆盖1kHz到100kHz,适合仪表开发、嵌入式实践和实验室小批量验证。
2. 测量原理与硬件方案:自动平衡电桥和DDS激励
2.1 为什么选自动平衡电桥而不是谐振法
元件测量的本质是获取复数阻抗 Z = R + jX。对电阻、电感、电容分别搭不同电路,会带来三套标定参数和维护成本,所以终端产品通常收敛到“测复数阻抗”这个统一模型。常见的阻抗测量方案有谐振法、四臂电桥、I-V法和自动平衡电桥,取舍如下:
| 方案 | 实现难度 | 频率上限 | 量程宽度 | 自动化程度 |
|---|---|---|---|---|
| 谐振法 | 低 | 较高 | 窄 | 低 |
| 手动四臂电桥 | 中 | 中 | 宽 | 低 |
| I-V法 | 低 | 中 | 中 | 高 |
| 自动平衡电桥 | 中高 | 中高 | 宽 | 高 |
谐振法测单个L或C很快,但换频率必须换谐振电容,而且分布参数直接影响读数。手动电桥精度可以做得很好,却依赖人工调平衡,不适合做成一键测量的仪表。I-V法实现最简单:给DUT串一个取样电阻,测量两个电压矢量即可,但当DUT阻抗跨四个数量级时,取样电阻不是压垮激励就是让信号淹没在噪声里。
自动平衡电桥在1kHz到100kHz区间是商用LCR表的常见路线。结构上,激励电压加在DUT高端,DUT低端接到运放反相输入端,运放输出经过反馈电阻Rref回到反相输入端。闭环后反相端被强制到“虚拟地”,流过DUT的电流几乎全部流过Rref,于是运放输出电压成了DUT电流的镜像。只要比较DUT高端电压Vx和运放输出Vout两个复数之比,就能算出:
Zx = Rref × (Vx / Vout)
量程扩展只需切换Rref,不需要改动主放大链路,频率特性也容易保持一致。缺点是对运放的带宽和压摆率有要求,做100kHz以上需要更复杂的RF-IV结构,但做1kHz到100kHz的便携式RLC表是完全合算的选择。
2.2 DDS正弦激励:AD9833的寄存器配置
阻抗测量必须用正弦激励。方波的高次谐波会被DUT的频率特性扭曲,相位关系无法解释。正弦源可以用PLL加VCO,也可以用DDS;这里用AD9833是因为它便宜、SPI控制简单、频率分辨率28位,正好覆盖需要的1kHz到100kHz。
AD9833输出幅度约0.6V且带直流偏置,必须用隔直电容再进激励端口,后面还要接一个二阶有源低通滤除DDS镜像频率。配置频率的C语言代码如下:
#define AD9833_MCLK 25000000.0f #define FREQ0_REG 0x4000u // D15=0, D14=1 选择FREQ0 #define CTRL_RESET 0x0100u // D8=1 复位状态 #define CTRL_B28 0x2000u // D13=1 低14位+高14位分两次写 void spi_write(uint16_t word); void ad9833_set_frequency(float f_hz) { uint32_t freq_word; if (f_hz > 12.5e6f) { f_hz = 12.5e6f; // 上限 MCLK/2 } freq_word = (uint32_t)(f_hz * 268435456.0f / AD9833_MCLK); spi_write(CTRL_B28 | CTRL_RESET); // 进入复位,准备写FREQ0 spi_write(FREQ0_REG | (freq_word & 0x3FFFu)); // 低14位 spi_write(FREQ0_REG | ((freq_word >> 14) & 0x3FFFu)); // 高14位 spi_write(CTRL_B28); // 退出复位,开始输出 }代码里268435456等于2的28次方,这是AD9833相位累加器的满量程。25MHz是晶振频率,freq_word就是每次相位累加步长。B28位置1后,FREQ0寄存器要按低14位、高14位两次写入;D15和D14组合0x4000表示访问FREQ0寄存器。退出复位后,FREQ0上的频率立即生效,但DDS输出从复位状态到稳定需要一点时间,测量代码应在切换频率后等待至少200个输出周期再触发ADC。
常用的几个控制位如下表:
| 控制位 | 值 | 作用 |
|---|---|---|
| D13 B28 | 1 | 28位频率寄存器分成两个14位写入 |
| D8 RESET | 1/0 | 复位/退出复位,复位时输出为零 |
| D5 OPBITEN | 0 | 选择DAC正弦输出 |
| D3 MODE | 0 | 关闭三角波模式 |
| D12 SLEEP1 | 0 | 保持DAC活动,不关断 |
DDS输出后的低通滤波器截止频率建议取激励频率的3到5倍,太高滤不掉镜像,太低会引入额外移相并被校准吃掉。
2.3 采样通道与ADC选型
数字解调要求两路波形尽量在时间上对齐。10kHz以下用单ADC加模拟开关分时采样,也能靠软件修正相位;到100kHz时一个采样周期的相位差就是3.6°,不能再忽略。稳妥做法是使用同步采样ADC,两通道同时转换,转换结果天然对齐。
采样率至少取激励频率的16倍,这样每个周期有16个采样点,正交解调才够用。分辨率要求16位起,因为RLC表经常要处理“大激励信号、小响应信号”的比值,分辨率不够时ESR和Q值会抖动。
| 激励频率 | 建议采样率 | 每周期点数 | ADC方向 |
|---|---|---|---|
| 1kHz | 32kSPS | 32 | 24位过采样型,如ADS1262 |
| 10kHz | 320kSPS | 32 | 16位SAR,单通道也够 |
| 100kHz | 1.6MSPS | 16 | 同步采样型,如AD7606 |
两路信号中Vx可以直接从激励缓冲器输出端取,幅值大、共模电压干净;Vout是运放输出,幅值随DUT阻抗变化,需要配合Rref量程控制,避免进入ADC的满量程附近。ADC前端都要加同相缓冲器,用高输入阻抗运放隔离信号源与ADC输入电容,否则采样开关切换会造成电压跌落。
3. C/C++解调算法与RLC参数换算
3.1 正交解调为什么比FFT更适合单片机
从ADC拿到同步采样序列后,需要提取两个电压矢量的幅度和相位。做完整FFT也可以,但测量只需要一个频点,算全部频谱浪费大量CPU周期。正交解调等价于只算DFT在激励频率处的值:
I = (2/N) × Σ x[n] × cos(2π fout n / fs) Q = (2/N) × Σ x[n] × sin(2π fout n / fs)
当采样点数N是整数个信号周期时,直流偏置对I和Q没有贡献,与激励频率不同的噪声会被积分平均弱化,这比简单测峰值抗干扰强得多。C语言和C++都能跑,区别只在代码组织方式:C语言用结构体,C++可以封装成类。
3.2 用C++实现同步解调与查表
在MCU上逐点调用sinf、cosf不是不行,但100kHz信号每周期16个点、一次解调还要做多次测量,CPU会花大量时间在三角函数上。更常见的做法是预生成128点正余弦查找表,相位索引用按位与代替除法。
#include <cmath> struct demod_result { float i; float q; }; class SyncDemod { public: static constexpr uint32_t kTableSize = 128; SyncDemod(uint32_t n, float fs, float fout) : n_(n) { phase_step_ = 2.0f * 3.14159265358979f * fout / fs; for (uint32_t i = 0; i < kTableSize; ++i) { float ph = 2.0f * 3.14159265358979f * i / kTableSize; cos_table_[i] = cosf(ph); sin_table_[i] = sinf(ph); } } demod_result run(const uint16_t* samples) { float sum_i = 0.0f; float sum_q = 0.0f; for (uint32_t n = 0; n < n_; ++n) { float phase = phase_step_ * n; uint32_t idx = (uint32_t)(phase * kTableSize / (2.0f * 3.14159265358979f)); idx &= (kTableSize - 1); sum_i += samples[n] * cos_table_[idx]; sum_q += samples[n] * sin_table_[idx]; } demod_result r; r.i = 2.0f * sum_i / n_; r.q = 2.0f * sum_q / n_; return r; } private: uint32_t n_; float phase_step_; float cos_table_[kTableSize]; float sin_table_[kTableSize]; };这段代码中,kTableSize取128是因为2的幂可以用按位与算索引,省掉取模。phase_step_是每个采样点之间的相位增量;如果采集两个周期共32点,phase_step_×32就等于4π,积分结果正好对应信号幅度。乘2再除以N是为了把幅度从二分之一余弦的平均值恢复成峰值。
如果MCU没有硬件浮点单元,可以把采样值和查找表都转成int32_t定点数,但LCR测量对动态范围要求高,建议直接用带FPU的Cortex-M4/M7或者加了Cortex-M0+外置16位ADC的方案。C语言实现同样逻辑时,把类改成几个全局函数、把成员变量放进上下文结构体即可。
3.3 从电压矢量到R、L、C和Q/D值
两个通道解调后得到Vx的复数(ix, qx)和Vout的复数(iv, qv),阻抗公式为:
Zx = Rref × (Vx / Vout)
复数除法要先算分母模方,再分别乘实部虚部。下面代码把电阻、电感、电容和Q/D值一次算完:
typedef struct { float re; float im; } cplx_t; typedef enum { MODEL_SERIES, MODEL_PARALLEL } model_t; static cplx_t cplx_div(cplx_t a, cplx_t b) { float den = b.re * b.re + b.im * b.im + 1e-12f; cplx_t r; r.re = (a.re * b.re + a.im * b.im) / den; r.im = (a.im * b.re - a.re * b.im) / den; return r; } void rlc_calculate(cplx_t z, float freq_hz, model_t model, float* out_r, float* out_l, float* out_c, float* out_q, float* out_d) { float w = 2.0f * 3.14159265358979f * freq_hz; cplx_t y; if (model == MODEL_SERIES) { *out_r = z.re; if (z.im > 0.0f) { *out_l = z.im / w; *out_c = 0.0f; *out_q = z.im / (*out_r + 1e-12f); } else { *out_c = -1.0f / (w * z.im + 1e-12f); *out_l = 0.0f; *out_q = -z.im / (*out_r + 1e-12f); } *out_d = 1.0f / (*out_q + 1e-12f); } else { float den = z.re * z.re + z.im * z.im + 1e-12f; y.re = z.re / den; y.im = -z.im / den; *out_r = 1.0f / (y.re + 1e-12f); if (y.im > 0.0f) { *out_c = y.im / w; *out_l = 0.0f; *out_q = y.im / (y.re + 1e-12f); } else { *out_l = -1.0f / (w * y.im + 1e-12f); *out_c = 0.0f; *out_q = -y.im / (y.re + 1e-12f); } *out_d = 1.0f / (*out_q + 1e-12f); } }串联模型下,实部就是ESR;虚部正为感性,负为容性。并联模型先把阻抗转成导纳,再按电阻、电导关系换算。Q值定义为无功分量与有功分量之比,D值是Q的倒数,LCR表常同时显示两者。加1e-12是为了防止除零,因为测量开路时实部可能接近0。
选串联还是并联模型要看被测元件的阻值。大电容、小电感受引线电阻影响明显,用串联模型;小电容、大电感受绝缘电阻和介质损耗影响大,用并联模型更接近物理本质。自动模式下可按阻抗模做粗判断,但手动设置模型在调试时更好用。
典型元件在不同频率下的阻抗量级如下,选Rref和ADC增益时可参考:
| 被测元件 | 频率 | 阻抗模 | 需要注意 |
|---|---|---|---|
| 100nF电容 | 1kHz | 1.59kΩ | 介质损耗影响D值 |
| 100nF电容 | 100kHz | 15.9Ω | 引线电阻开始干扰 |
| 100μH电感 | 10kHz | 6.28Ω | 夹具接触电阻会进ESR |
| 10μH电感 | 100kHz | 6.28Ω | 与ESR同量级,相位误差敏感 |
4. 校准、自动量程与常见坑
4.1 开路、短路校准到底在消除什么
PCB走线、夹具和杜邦线给测量路径引入了串联阻抗和并联导纳。串联阻抗对低阻抗元件影响大,并联导纳对高阻抗元件影响大。开路校准时什么都不接,测到的导纳Yopen就是并联寄生;短路校准时把夹具直接短接,测到的阻抗Zshort就是串联寄生。
修正公式是:先减短路阻抗,再减开路导纳。C语言实现如下:
void calibration_apply(cplx_t z_meas, cplx_t z_short, cplx_t y_open, cplx_t* z_out) { float den, real, imag; z_meas.re -= z_short.re; z_meas.im -= z_short.im; den = z_meas.re * z_meas.re + z_meas.im * z_meas.im + 1e-12f; real = z_meas.re / den; imag = -z_meas.im / den; real -= y_open.re; imag -= y_open.im; den = real * real + imag * imag + 1e-12f; z_out->re = real / den; z_out->im = -imag / den; }校准数据必须按频率和量程分组保存。同一个夹具在1kHz和100kHz下测到的Yopen差别很大,继电器在不同档位下触点电阻也不一样。只做一组校准再切换频率,等于没校准。上电时可以自动短接和开路各测一次,也可以做成“按校准键后进入提示状态”的手动流程,后者更适合只用一个固定夹具的场合。
4.2 自动量程切换逻辑
自动量程本质是选合适的Rref,让运放输出电压既不饱和也不过小。常用档位如下:
| 档位 | Rref | 适合阻抗范围 |
|---|---|---|
| 0 | 100Ω | 约1Ω ~ 1kΩ |
| 1 | 1kΩ | 约100Ω ~ 10kΩ |
| 2 | 10kΩ | 约1kΩ ~ 100kΩ |
| 3 | 100kΩ | 约10kΩ ~ 1MΩ |
切换判断依据是Vout的幅度,代码可以写成:
void auto_range_update(float vout_peak, uint8_t* range_idx) { const float vhigh = 0.85f * ADC_FULL_SCALE; const float vlow = 0.10f * ADC_FULL_SCALE; if (vout_peak > vhigh && *range_idx < RANGE_MAX) { (*range_idx)++; // 响应过大,增大Rref } else if (vout_peak < vlow && *range_idx > 0) { (*range_idx)--; // 响应过小,减小Rref } }阈值设成0.85和0.10而不是0.95和0.05,是为避免在边界反复跳动。继电器切换后触点要几十毫秒才能稳定,代码必须等待稳定时间后再采样。每切一次档位,需要重新加载该档位的Zshort和Yopen校准值。ADC前端若加了程控增益放大器,增益切换也要纳入同样流程,否则换挡后第一次测量会带瞬态。
4.3 相位误差、浮点陷阱和开发环境
相位误差是自制RLC表最头疼的问题。运放带宽不足、低通滤波器阶数、DDS到ADC采样时钟不同步,都会让Vout相对Vx产生系统性相移。一个有效的对齐做法是:在夹具上接一颗0.1%精密电阻,测出当前频率下的复数偏差,存成phase_comp;后续测量把这一组复数值旋转回去。
C/C++代码里要注意浮点异常来源:靠近量程边界时,Zx的实部和虚部可能相差三个数量级,直接做减法会损失有效数字。解决方法是先判断阻抗模落在哪个区间,再选择串联或并联模型计算。比如测大电容时用并联模型,ESR作为导纳虚部的残留值来算,比直接在阻抗里减一个很小的实部更稳定。
开发环境直接用VSCode配置C/C++环境,配合CMake把算法文件单独编译成PC可执行程序,用生成的正弦波形喂进去,再对比计算结果。这样可以在没有硬件时先把算法调对。硬件代码里避免用C++的异常、RTTI和堆分配,这些特性在裸机环境会增加不确定性,C语言或受限C++才是仪表固件的常态。
5. 用C++类封装测量流程:从裸机代码到可验证的仪表
5.1 定义一个与硬件解耦的测量核
把解调、校准和RLC计算写成一个不依赖硬件的C++类,主程序只负责提供采样数据和回调输出,这样同一套算法既能编译进STM32,也能在PC上重放波形数据。
class RlcMeterCore { public: struct Settings { float freq_hz; uint8_t range_idx; model_t model; }; struct Result { cplx_t z; float r; float l; float c; float q; float d; }; Result measure(const float* vx, const float* vout, uint32_t n, const Settings& settings) { SyncDemod demod_vx(n, sample_rate_, settings.freq_hz); SyncDemod demod_vout(n, sample_rate_, settings.freq_hz); demod_result dx = demod_vx.run((const uint16_t*)vx); demod_result dv = demod_vout.run((const uint16_t*)vout); cplx_t vx_c = { dx.i, dx.q }; cplx_t vr_c = { dv.i, dv.q }; cplx_t z = impedance_from_voltages(vx_c, vr_c, ranges_[settings.range_idx]); z = apply_calibration(z, settings.freq_hz, settings.range_idx); Result out; out.z = z; rlc_calculate(z, settings.freq_hz, settings.model, &out.r, &out.l, &out.c, &out.q, &out.d); return out; } private: float sample_rate_; const float* ranges_; };这个类把三个核心职责分清楚:SyncDemod负责从原始采样值提取复数矢量,impedance_from_voltages负责按Rref计算阻抗,apply_calibration负责消除系统和夹具误差。硬件层只要保证vx和vout是同一时间点的采样序列,测量核根本不关心ADC型号和MCU是谁。
5.2 验证方法:三个自检步骤
第一步,用0.1%精密电阻做基准。分别测量100Ω、1kΩ、10kΩ、100kΩ,记录电阻读数和相位角偏差,电阻误差应主要在Rref精度和ADC噪声上。
第二步,用已知电容串一颗精密电阻组成RC网络,同时读数D值。比如10nF电容在1kHz下Xc约15.9kΩ,串100Ω电阻后D值应为100除以15900,约0.0063。如果D值偏差大,说明相位校准不对。
第三步,在不同频率下测同一个电感。100μH电感在10kHz读数和100kHz读数应当接近;如果读数随频率明显上升,通常不是电感变了,而是夹具存在并联寄生电容,高频下与电感发生并联谐振。
调试数据用串口输出为逗号分隔文本:频率、R、L、C、Q、D、原始I/Q值。这一行数据直接拖进电子表格就能画曲线。把这一行CSV和标准仪表的读数并排放在一起,整个设计是准还是不准,立刻能看出来。
本文还有配套的精品资源,点击获取