简介:一套基于STM32F103RC的便携式电容测量仪完整工程资料,面向嵌入式开发者和电子爱好者,解决0pF至50uF电容高精度测量问题。资源共417个文件,以C/H源码为核心,附带PCB原理图、PDF文档、工程配置、编译中间文件等,整包29.19MB,分类清晰便于按模块查阅。项目基于RC充放电原理,利用定时器计时换算容值,通过自动调零消除系统误差,配合LMV393电压比较器判断充放电临界点,并由液晶屏直观显示结果。工程源码覆盖STM32定时器配置、ADC采样、液晶驱动、比较器控制等关键部分,电路资料包含原理图与PCB文件,支持电池供电与低功耗设计,可直接导入Keil等IDE进行二次开发。目前已有2094人学习下载,适合希望深入学习STM32测量应用与仪器设计的开发者参考。
1. 电容测量3_stm32.rar:一个 STM32 电容测量工程到底值不值得打开
拿到「电容测量3_stm32.rar」这个压缩包命名时,我第一反应是:又是一个基于 STM32 的电容测量工程,八成是 RC 充放电法或者 PWM 占空比法做的触摸按键或者液位检测。真正打开项目、梳理代码之后,发现这类工程的坑远比想象中多——环境寄生电容动不动就能把测量结果拉偏几十 pF,温漂能让阈值判断彻底失效。这篇文章就以这类工程为载体,把 STM32 电容测量的原理选型、电路搭建、代码实现、参数调优和排障路径一次性讲透。不管你是刚接触电容传感的新手,还是想把这套方案用到液位、接近感应、触摸按键上的熟手,都可以照着文中的步骤落地,并用最小代价验证方案能不能用、准不准、稳不稳。
2. 先搞清楚测量对象:STM32 电容测量到底在测什么
2.1 自电容与互电容:两种对象的本质区别
STM32 做电容测量,本质上测的是电极对地或者电极之间的等效电容变化。自电容方案是电极对地的电容,人手靠近或者液体覆盖电极时,等效介电常数变化导致电容变化,触摸按键、液位检测基本都用自电容。互电容则是两个电极之间的耦合电容,手指或物体靠近会改变耦合量,滑条、多点触控这类场景偏互电容。
在「电容测量3_stm32.rar」这类工程里,最常见的是自电容方案,因为硬件简单——一个铜箔电极加上一个 GPIO 口和一个电阻就能开始测。但理解对象很重要:如果你拿测自电容的电路去测互电容,电极间距、屏蔽层处理全都要变,否则读出来的数据跳动会大得让你怀疑硬件坏了。
需要注意的是,STM32 内部没有专用的电容数字转换器,所以所有测量都是间接的。要么用定时器捕获 RC 充放电时间,要么用 ADC 采样充电电压斜率,要么用比较器加定时器构成振荡器测频率。每种方法对应不同的精度和成本,下面逐个展开。
2.2 环境寄生电容:所有误差的根源
任何电容测量方案,第一步要认清一个事实:你能测到的永远是「传感器电容 + 环境寄生电容」的组合。走线、焊盘、芯片引脚、PCB 覆铜,甚至人手靠近电路板背面,都会给测量结果叠加一个不确定的偏置。
所以工程里几乎都会做一个「校准基线」的操作:上电后先测一次空载值当作 baseline,后续的测量值跟 baseline 做差。差值才是有效信号。如果你拿到一个工程,发现它没有做基线校准,那基本可以判断这版代码只适合实验室稳定环境,不太可能直接上生产。
寄生电容的另一个麻烦是温漂。PCB 板材介电常数随温度变化,湿度也会影响表面阻抗,这些叠加起来可能让基线慢慢漂移。后面讲算法时会提到用滑动平均和周期性基线刷新来对抗漂移。
2.3 这个测量量级:pF 级变化需要什么样的分辨率要求
一个典型的触摸按键在手指按下时电容变化约 1~5 pF,一个液位电极从空到满可能变化几 pF 到几十 pF。STM32 的 GPIO 输入捕获分辨率和 ADC 分辨率决定了你能分辨多小的变化。
假设用 RC 充放电法,充电时间 t = RC × ln(1 - Vt/Vcc) 的倒数来算,C 变化 1 pF 导致的充放电周期变化可能只有几百纳秒。定时器时钟如果是 72 MHz,周期分辨率约 13.9 ns,这意味着你有大约 10~30 个计数值的变化空间,配合多次采样平均,基本能稳定分辨 1 pF。如果你用 32.768 kHz 的 LSI 做定时器时钟,那 1 pF 的变化可能只有零点几个计数值,根本分不出来。
所以选定时器时钟、确定采样次数之前,先算清楚分辨率预算。这是整个工程设计的第一步,也是很多人一上来就翻车的点。
3. 测量电路与选型:RC 充放电、比较器振荡、电容数字芯片怎么选
3.1 RC 充放电法:STM32 上最通用的入门方案
RC 充放电法的原理很直接:通过一个已知电阻给未知电容充电,用 GPIO 检测电容电压到达阈值的时间,再换算电容值。具体实现有两种常见的接法。
第一种是普通 GPIO 直接驱动:PA0 接电阻 R 到被测电容 C,电容另一端接地。PA0 先输出低电平放电,然后切换为输入模式并启动定时器,电容通过 R 充电到 GPIO 的 TTL 阈值(STM32 一般约 0.4~0.6 Vcc),GPIO 检测到上升沿就停止定时器。充放电时间 T ≈ R × C × ln(Vcc/Vt)。
第二种是配一个比较器,把阈值固定为一个精确电压,消除 GPIO 阈值随温度、电压漂移的影响。比较器输出接定时器输入捕获通道,充电到参考电压时比较器翻转,捕获定时器计数值。精度比第一种高一些,但多一个芯片。
在「电容测量3_stm32.rar」这类工程里,第一种方式是绝对的多数,因为一个电阻加一个 GPIO 就能跑,BOM 成本趋近于零。缺点是精度受 GPIO 阈值影响,线性度没那么理想,需要做分段校准。
3.2 比较器振荡法:用频率换精度
比较器振荡法是把被测电容接到一个张弛振荡器里,电容的充放电周期决定振荡频率,STM32 用定时器输入捕获测频率,再反推电容值。电路上需要一个比较器或者可以用 STM32 内部比较器(有些系列有),加两个电阻构成滞回。
频率 f ≈ 1 / (2 × R × C × ln(2)),C 变化会导致频率变化。用定时器测频率时,闸门时间越长分辨率越高。假设测频闸门 10 ms,主频 72 MHz,计数值可达 720,000,用这个计数值去反推电容,分辨率可以做到 0.1 pF 甚至更高。
这个方案的优点是把电容变化转换成了频率,频率测量是数字电路最擅长的,抗噪声能力强。缺点是硬件多一个比较器,而且对运放的输入偏置电流、失调电压敏感,选型不注意的话低端比较器带来的误差可能比 RC 法还大。
3.3 电容数字转换芯片:什么时候应该放弃 STM32 内置资源
如果你要做的产品是液位精度 ±0.5 mm,或者需要长期 24 小时连续运行且不允许漂移,那么用 STM32 裸做电容测量大概率是浪费时间的。市面上有专门的电容数字转换芯片(常见如 FDC 系列、 capacitance-to-digital converter 类),内置屏蔽驱动、温度补偿、EMI 滤波,测量范围从 fF 到 uF 都有,输出直接走 I2C 或 SPI。
这时候 STM32 的角色变成主机:初始化芯片、读寄存器、跑判断逻辑。很多人会纠结「用 STM32 内置资源才是真技术」,但实际产品选型只看指标和成本。芯片方案一颗料可能几块钱,但省下的调试时间、良率损失和售后维护成本远不止这些。
我的习惯是:如果项目目标是快速验证原理,用 STM32 内置资源搭一个最小系统;如果要出产品、过认证、保证一致性,直接上专用芯片。这里的边界不是技术能力,而是工程效率。
4. 打开工程之后:从最小系统到可复现的电容测量代码
4.1 用定时器输入捕获实现 RC 充电时间测量
拿到「电容测量3_stm32.rar」这类工程,第一步先看它的定时器配置。以常见的 STM32F103 系列为例,用 TIM2 的通道 1 做输入捕获,PA0 作为捕获引脚。代码逻辑如下:
// 初始化PA0为输入捕获模式,TIM2_CH1 void Cap_Init(void) { GPIO_InitTypeDef gpio; TIM_ICInitTypeDef ic; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA0配置为浮空输入,捕获外部电平变化 gpio.GPIO_Pin = GPIO_Pin_0; gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &gpio); // TIM2_CH1输入捕获,上升沿触发 ic.TIM_Channel = TIM_Channel_1; ic.TIM_ICPolarity = TIM_ICPolarity_Rising; ic.TIM_ICSelection = TIM_ICSelection_DirectTI; ic.TIM_ICPrescaler = TIM_ICPSC_DIV1; ic.TIM_ICFilter = 0x00; TIM_ICInit(TIM2, &ic); // 预分频71,计数时钟1MHz,最大计数65535,溢出后重新装载 TIM_PrescalerConfig(TIM2, 71, TIM_PSCReloadMode_Immediate); TIM_Cmd(TIM2, ENABLE); }这段代码把定时器配置成每微秒计数一次,计数值直接对应充电时间微秒数。要注意的是 TIM2 是 16 位定时器,最大计数 65535,如果电容比较大(比如 10 uF 级别的电解电容),充电时间可能超过 65 ms,这时候需要用定时器溢出中断扩展计数值,或者改用 32 位定时器(F1 系列没有,F3/F4 系列有)。
参数说明:预分频 71 是因为 APB1 时钟通常为 72 MHz,72 MHz / (71+1) = 1 MHz,也就是计数周期 1 us。如果你改了系统时钟,这个值要跟着变。TIM_ICFilter 设置为 0 意味着不做输入滤波,如果现场有较强噪声,可以逐步调到 0x0F,但要注意滤波会增加信号延迟,影响精度。
4.2 完整测量流程:放电、充电、捕获、换算
充放电测量的完整流程要严格按顺序:先放电确保电容从已知状态开始充电,然后启动定时器,等待捕获中断。这里有一个很容易犯的错误——放电时间不够,电容没放完就开始充电,导致测量值偏小且不稳定。
uint32_t Get_Cap_ChargeTime(void) { uint32_t time = 0; // 第一步:PA0配置为推挽输出低电平,确保电容完全放电 GPIO_InitTypeDef gpio; gpio.GPIO_Pin = GPIO_Pin_0; gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); GPIO_ResetBits(GPIOA, GPIO_Pin_0); Delay_us(100); // 放电时间至少3倍RC时间常数 // 第二步:切换为输入捕获模式,清空计数器并启动 TIM_SetCounter(TIM2, 0); TIM_ClearFlag(TIM2, TIM_FLAG_CC1); gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &gpio); // 第三步:等待捕获事件发生,超时保护 while(TIM_GetFlagStatus(TIM2, TIM_FLAG_CC1) == RESET) { if(TIM_GetCounter(TIM2) > 60000) { time = 60000; // 超时返回,防止死循环 break; } } time = TIM_GetCapture1(TIM2); return time; }这段代码有几点值得注意。第一,放电时间用了固定 100 us,实际应该根据被测电容大小调整:C 越大放电时间要越长,否则电荷残留会导致充电时间偏短。第二,切换 GPIO 模式的瞬间会有引脚电平跳变,可能产生一个伪的捕获触发,所以切换后要清一次捕获标志。第三,超时保护必须加,否则如果电容短路或电极断开(开路相当于电容极小,充电极快),程序会卡死在 while 循环里。
4.3 用 ADC 采样充电曲线的替代方案
如果你的 MCU 定时器资源紧张,也可以用 ADC 采样充电曲线的斜率来推算电容。原理是:对 RC 充电曲线上的几个点采样,拟合时间常数 τ = RC,再从 τ 算出 C。这个方案的好处是不需要输入捕获引脚,普通 ADC 引脚即可,但代价是 ADC 采样本身有转换时间,而且采样时刻和充电起始时刻的同步需要额外处理。
// ADC采样充电电压,取两个电压点计算时间常数 float Cap_Calc_By_ADC(void) { uint16_t v1, v2; uint32_t t1_us, t2_us; // 放电 GPIO_ResetBits(GPIO_CAP_PORT, GPIO_CAP_PIN); Delay_ms(10); // 切换为ADC模式开始充电,同时启动一个1MHz的定时器 TIM_SetCounter(TIM3, 0); // 等待电压到达预设点1(约0.3Vcc),记录时间 while(Get_ADC_Value() < 1000) { } t1_us = TIM_GetCounter(TIM3); v1 = Get_ADC_Value(); // 等待电压到达预设点2(约0.6Vcc),记录时间 while(Get_ADC_Value() < 2000) { } t2_us = TIM_GetCounter(TIM3); v2 = Get_ADC_Value(); // τ = Δt / ln((Vcc-V1)/(Vcc-V2)) float tau = (float)(t2_us - t1_us) / logf((4095.0f - v1) / (4095.0f - v2)); // C = τ / R float cap_pF = tau * 1000000.0f / 10000.0f; // 假设R=10kΩ return cap_pF; }这个方案的坑在于 ADC 采样本身有建立时间,连续采样之间有时间差,导致两个电压点记录的时间戳不够精确。我的经验是,如果系统时钟余量足够,优先用输入捕获方案;只有 ADC 外设闲着而定时器被其他功能占用时,才考虑这个方案。
5. 参数与标定:让测量数据从「能动」到「可信」
5.1 采样次数与滤波参数的确定
单次测量值几乎不可能直接用,因为噪声、电源波动、电磁干扰都会叠加在结果上。常见的做法是连续采样 16~64 次,去掉最大最小值后取平均,再送进滑动平均滤波器。
// 软件滤波:去除极值后平均 + 滑动平均 #define SAMPLE_CNT 32 #define MAX_CNT 5 volatile uint32_t sample_buf[MAX_CNT]; volatile uint8_t buf_index = 0; volatile uint32_t filtered_value = 0; uint32_t Cap_Get_Filtered(void) { uint32_t raw[SAMPLE_CNT]; uint32_t sum = 0; uint32_t min, max; uint8_t i; // 采集SAMPLE_CNT次原始值 for(i = 0; i < SAMPLE_CNT; i++) { raw[i] = Get_Cap_ChargeTime(); } // 找极值 min = raw[0]; max = raw[0]; for(i = 1; i < SAMPLE_CNT; i++) { if(raw[i] < min) min = raw[i]; if(raw[i] > max) max = raw[i]; } // 去掉极值求和 sum = 0; for(i = 0; i < SAMPLE_CNT; i++) { if(raw[i] != min && raw[i] != max) { sum += raw[i]; } } uint32_t avg = sum / (SAMPLE_CNT - 2); // 5点滑动平均 sample_buf[buf_index++] = avg; if(buf_index >= MAX_CNT) { buf_index = 0; } uint32_t acc = 0; for(i = 0; i < MAX_CNT; i++) { acc += sample_buf[i]; } filtered_value = acc / MAX_CNT; return filtered_value; }这里有两个参数值得细说。SAMPLE_CNT 取 32 是因为单次测量耗时约 100~200 us(取决于电容大小),32 次总耗时约 3~6 ms,人手的触摸动作持续时间几百毫秒,这个采样周期足够跟上实时性。MAX_CNT 取 5 是因为滑动窗口越小响应越快,但滤波效果越差;窗口越大越平滑但滞后明显,触摸场景超过 10 就会感觉「肉」,按键按下后要顿一下才触发。
5.2 基线校准与阈值设置,兼顾灵敏度与稳定性
基线校准是电容测量工程的灵魂。上电后先连续测 50 次取均值作为 baseline,后续每次测量值与 baseline 的差值 delta 才是有效信号。阈值设置要看具体应用:触摸按键一般 delta 超过 20 到 40 个计数值判定按下,液位检测则是连续多级阈值分段。
这里给一组我调试时常用的参考参数:环境稳定时,基线噪声峰峰值约 ±5 个计数值,触摸引起的 delta 约 80~150 计数值,阈值取 30~40 比较安全;如果电极面积较小或隔着 5 mm 亚克力板,delta 可能只有 30~50,阈值就要降到 15~20,但噪声误触概率会上升。
实际调试时我一般先抓 10 秒空载数据看噪声峰峰值,再触摸电极看 delta 值,取两者中间偏上一点做阈值。还有一种做法是自适应阈值:实时维护噪声统计值,阈值 = 3 倍标准差 + 固定偏置。但自适应的参数需要调得更小心,否则很容易在噪声突变时误触发。
5.3 环境变化怎么办:自动基线漂移补偿
基线漂移是电容测量从 demo 走向产品最难过的一关。温度变化、湿度变化、电极表面氧化,都会让基线慢慢滑动。如果 baseline 是固定的,几个小时后可能误触发或者测不到。
常见做法是慢速基线跟踪:每次测量时,如果 delta 小于阈值的三分之一,就把 baseline 朝当前测量值方向缓慢拉动,拉动力度是一个很小的系数,比如 1/1000。这样环境缓慢变化时基线跟着走,而快速的人手触摸或液位变化则不会被吸收掉。
// 基线慢跟踪算法 void Baseline_Track(uint32_t current_value) { int32_t delta = (int32_t)current_value - (int32_t)baseline; // 只有delta很小时才更新基线,避免把手势变化吸收掉 if(delta > -10 && delta < 10) { // 每次只移动1/256,时间常数约256个采样周期 baseline += (int32_t)(delta / 256); } }这个跟踪逻辑要求 delta 很小时才更新基线。如果阈值会随环境变化,一般会配合另一个策略:周期性的强制校准,比如每 5 分钟在没有触摸(通过另一路传感器确认)时刷新一次 baseline。但强制校准有风险——液位应用里液面恰好在这段时间变化了,会把新液位当成基线。
6. 避坑实战:5 个最常见的踩坑记录
6.1 现象:触摸按键偶发自触发,没人碰也乱跳
原因分析:电源纹波叠加在充电曲线上。STM32 的 GPIO 阈值比较受 Vcc 波动影响,Vcc 上叠加 100 mV 的纹波相当于阈值电压变化了几十 mV,充电时间随之抖动。如果是开关电源供电且纹波较大,这个问题尤其明显。
解决办法:先量 Vcc 纹波,如果超过 50 mV 就需要改进电源。测量供电加一个 LC 滤波器,Vcap 引脚附近加 100 nF 陶瓷电容。软件上最直接的缓解措施是提高采样次数,让噪声平均掉。如果纹波来自外部高频干扰,还可以打开定时器的输入滤波功能,TIM_ICFilter 从 0 改为 0x0F,能有效滤掉 ns 级毛刺。但注意滤波会引入相位延迟,充电时间测量值会轻微偏大,需要重新标定。
6.2 现象:手指按上去没反应,但用示波器探头碰就有反应
原因分析:人体通过空气耦合到电极的电容很小,如果用示波器探头(本身带 10x 档位约 10 MΩ 阻抗和 10 pF 电容)碰电极,探头的寄生电容远大于手指的电容变化,自然会触发。这说明要么电极面积太小,要么覆盖层太厚,手指的电容变化被衰减到阈值以下。更关键的是,很多人忽略了一个点:触摸按键的参考地跟人体之间需要构成回路,如果设备是电池供电的浮地系统,触摸效果会大打折扣。
解决办法:加大电极面积(触摸按键一般不小于 8 mm × 8 mm);降低覆盖层厚度,亚克力不要超过 3 mm;如果是浮地系统,考虑把测量地通过一个大电阻(1 MΩ)接到外壳金属部分,给人体耦合的电荷提供泄放路径。还有一个工程技巧:电极周围加一圈地环(guard ring),把电场约束在电极正面,减少边缘效应和对周围走线的串扰。
6.3 现象:校准好好的,过一夜第二天测量值全偏了
原因分析:温漂。夜间环境温度变化,PCB 板材介电常数和电阻值都受影响。电阻的温漂系数如果没选低温漂型号,100 ppm/°C 的电阻在 10°C 温差下就有 0.1% 的变化,换算到电容测量值可能是几 pF。加上电极表面可能凝露,湿度变化对测量影响更大。
解决办法:电阻选 25 ppm/°C 或更好的金属膜电阻;代码里启用基线跟踪,把慢漂移吸收掉;如果是液位检测这种连续测量场景,额外加一个温度传感器,必要时做温度补偿查表。我在一个实际项目中遇到基线每 10 分钟漂 2 个计数值,开启基线跟踪后连续运行两天误差控制在 ±2 个计数值以内,完全够用。
6.4 现象:多路通道一起工作时互相干扰,单路都正常
原因分析:多路电极共用一套测量电路,通道切换时没有充分放电和稳定。上一路电极残留电荷会影响下一路的初始状态;或者多路同时测量时,电极之间的耦合电容互相影响(互电容效应)。
解决办法:通道切换后先做一次 dummy 测量丢弃,再做正式测量;电极间距保持至少 5 mm,中间加地线隔离;如果测量电路分时复用,确保放电完成后延时稳定再开始充电。软件上还可以错开各路测量时刻,避免同步采样互相注入噪声。
6.5 现象:测量值跳变剧烈,滤波后依然不稳
原因分析:静电放电或者接触不良。电极到测量引脚的连接如果用了杜邦线或者接插件,接触电阻变化直接影响充电回路;静电积累在电极上,电荷泄放路径不畅,会造成剧烈跳变。还有一个容易被忽视的点:测量引脚的 GPIO 配置如果是浮空输入,在放电阶段切换到输出低电平时,引脚本身的对地电容会跟电极电容并联,切换瞬间产生尖峰。
解决办法:焊接连接替代接插件;电极周围做好静电泄放(接一个 1 MΩ 电阻到地);软件上把首次测量值丢弃。如果环境里确实存在强静电,加 TVS 管保护引脚,否则 STM32 的 GPIO 可能直接被静电打坏。
7. 进阶验证:一块已知电容板让方案从「能跑」变成「可信」
到这里,围绕「电容测量3_stm32.rar」的原理、实现、参数和坑都讲透了。最后一章聊一个具体的进阶习惯:拿一块标准的电容排阻板或者自制已知容量的电容板,给整个测量链路做端到端验证。
我一般会准备一组已知容量的电容,比如 1 pF、4.7 pF、10 pF、22 pF、47 pF、100 pF,分别接到测量电极上,记录每个标准电容对应的计数值。然后用最小二乘法拟合一阶线性关系,得到计数值到电容的换算曲线。这样能一次性验证三个关键指标:线性度(不同容量点是否均匀分布)、灵敏度(每个 pF 对应多少计数值)、重复性(多测几次是否一致)。
这个验证板的制作很简单。找一块覆铜板,刻出几个独立的焊盘,每个焊盘焊一个标准的直插电容,留出测试夹的接口。测试时把夹子夹到测量电极上,切换不同电容值,记录数据。整个过程半小时以内完成,但能省下后面整周的调试时间。这是我长期的习惯:先验证测量链路可信,再优化软件逻辑,最后调应用参数。顺序反了会陷入「数据不准但不知道是硬件问题还是算法问题」的泥潭。
最后说一个这个方向比较容易忽略的收获:以「电容测量3_stm32.rar」为代表的工程实践做完之后,你会发现这套测量思路能复用的地方非常多——接近感应、湿度检测、液位监测、按键检测,本质都是同一个测量核心加上不同的电极形态和判断逻辑。算是投入产出比很高的一项基本功。希望这篇文章能帮到你。
本文还有配套的精品资源,点击获取