STM32 RC充放电电容测量:pF级比较器捕获与自动量程
简介:面向嵌入式电子仪器开发者的STM32电容测量完整工程包,围绕STM32F103RC主控实现0pF~50uF、6~7位分辨率的便携式电容测量仪,涵盖RC充放电测时原理、自动调零校准、LMV393比较器信号检测以及液晶显示与电池供电设计。压缩包共417个文件,约29.19MB,以99个C源文件、84个头文件及70个编译中间文件构成固件主体,另有34个xcl、32个pbi、12个icf等工程配置,6个pcbdoc、5个schdoc及4个pdf对应硬件原理图、PCB与器件资料,并保留bat、jlink等烧录调试文件,便于按固件、硬件、文档分层查阅。已有2100人学习下载。读者可从中获得定时器捕获、ADC采样、LCD驱动、比较器控制与Keil/IAR工程配置等可复用代码,也能参考电路保护与量程匹配思路,适合具备一定STM32基础、需要做课程设计或便携仪器原型的开发者。
1. 从一颗测不出的 100pF 电容说起:这个 stm32 工程到底能测什么
手上一批 0805 贴片电容丝印早磨没了,万用表插上去只跳一个 0.00nF——普通表的下限基本在 1nF 以上,测小电容等于抓瞎。这份 stm32 电容测量工程干的就是这件事:借助 STM32 的比较器和定时器,通过 RC 充放电把 pF 到 µF 量级的电容值测出来,串口和 OLED 上直接读数,配套四档充电电阻做自动量程切换。它适合刚摸熟 GPIO 和定时器、想找个有真实物理量输入输出的练手项目的人,也适合要做电容分选、液位检测、触摸感应原型的开发者拿它当底座。整套代码是裸机 HAL,不挂 RTOS、不依赖中间件,改几个宏就能换量程。
2. 方案选型与电路框架:RC 充放电法凭什么能测到 pF 级
拆开压缩包之前先想清楚一件事:测电容有不止一条路,选错了后面全是返工。这一章把三条主流路线摆在一起比,再落到具体电路和引脚分配,看完你就知道为什么这套工程敢用一颗电阻加一个比较器去碰 pF 级。
2.1 三条技术路线的取舍
恒流源积分法是最“教科书”的做法:用运放搭一个恒流源给电容充电,电压随时间线性上升,dv/dt = I/C,测斜率反推容量。精度确实好,但代价是要一颗低失调精密运放、一个低温漂基准电阻,还得处理运放本身的输入电容,BOM 成本直接翻几倍。对只想测个大概值的场景,这套方案属于杀鸡用牛刀。
交流阻抗法走的是另一条路:给电容加已知频率的正弦信号,测流过它的电流,用容抗公式反算。好处是能测到很小的电容,也不受介质吸收影响,但需要 DDS 信号源加同步检波,代码量和模拟前端复杂度都不是一个量级,通常出现在专业 LCR 表里。
RC 充放电时间法处在中间位置:一个已知电阻 R 串一个 GPIO,给电容充电,测电压爬到某个阈值用了多长时间,t = R·C·ln(VCC/(VCC-Vth))。硬件只要一个电阻加一个比较器,剩下的全靠定时器数数。精度比不上前两种,但胜在便宜、好调、量程切换简单。
| 方案 | 最小可测 | 硬件成本 | 代码复杂度 | 适合场景 |
|---|---|---|---|---|
| 恒流源积分法 | 约 1pF | 高(精密运放+基准) | 中 | 仪器仪表、批量分选 |
| 交流阻抗法 | 约 0.1pF | 很高(DDS+检波) | 高 | 专业 LCR 测量 |
| RC 充放电时间法 | 约 10pF | 低(1 电阻+比较器) | 低 | 嵌入式入门、原型验证 |
我一般给刚上手的人推第三条,原因很实在:出问题时你能用示波器一眼看出波形对不对,而不是对着运放电路猜是失调还是寄生。这份工程用的就是 RC 充放电,后面所有代码都围绕它展开。
2.2 电路最小构成:一个电阻、一个比较器、两条 GPIO
核心支路只有三段。充电段是 PA0 经充电电阻 R 接到被测电容的上端,电容下端接地;放电段是 PB0 驱动一个 NMOS 的栅极,漏极并在电容上端,源极接地;比较段是被测电容上端接比较器同相输入,反相输入接内部 VREF 分压出来的 1/2 VCC。
测量动作拆成四步:先把放电管打开把电容放干净,再关断放电管,然后 PA0 推挽输出高电平开始经 R 充电,等比较器输出由低翻高那一刻记下时间。整个过程里电压按 V(t) = VCC·(1 − e^(−t/RC)) 上升,到 1/2 VCC 时正好是 t = RC·ln2 ≈ 0.693RC,所以容量直接等于时间除以 0.693R。
量程切换靠四路模拟开关选不同的 R:1k、10k、100k、1M。小电容配大电阻,把时间常数拉进定时器能分辨的窗口;大电容配小电阻,免得等到超时。这四个档位大致覆盖 10pF 到 10µF,再往上就得换恒流源方案了。
提示:被测电容上端到比较器输入的那根线尽量短,超过 3cm 就要考虑加保护环或者干脆把比较器挪近一点,否则走线电容会直接叠到读数上。
2.3 资源分配:定时器、比较器、引脚的落位
引脚分配看着琐碎,其实是这套代码能不能跑稳的地基。表里是这份工程默认的落位,换板子时按这个顺序改就行。
| 引脚 | 功能 | 配置要点 |
|---|---|---|
| PA0 | 充电驱动 | 推挽输出,速度设 High |
| PB0 | 放电控制 | 推挽输出,默认低(NMOS 关断) |
| PA1 | COMP1_INP | 模拟输入,关内部上下拉 |
| PA2 / PA3 | 模拟开关 A0/A1 | 推挽输出,选电阻档 |
| PA9 / PA10 | USART1 TX/RX | 复用推挽,115200 |
| PB6 / PB7 | I2C1 SCL/SDA | 开漏 + 外部 4.7k 上拉 |
| TIM2_CH1 | 比较器输出捕获 | 输入捕获,滤波器 0x3 |
| TIM3 | 微秒级延时 | 1MHz 计数,只做 delay |
定时器这一块要说清楚。TIM2 挂在 APB1 上,系统时钟 72MHz 时 APB1 分频后定时器时钟还是 72MHz,预分频设 7 得到 9MHz?不行,得整除,所以设 PSC = 6 得到 72/7 ≈ 10.28MHz,或者干脆用 PSC = 71 得到 1MHz。这份工程为了小电容分辨率,取 PSC = 6,实际计数频率约 10.28MHz,一个 tick 约 0.097µs,标定时把这个真实值算进去就行。
比较器比 ADC 快得多,翻转延迟在百纳秒级,这是能把 pF 级电容测出来的关键。如果用 ADC 轮询找阈值,一次采样加转换动辄几个微秒,小电容早就充完了,只能测 10nF 以上,这是后文兜底方案的天花板。
3. 工程骨架拆解:解压之后先看哪几个文件,初始化链怎么走
压缩包解开是个标准的 CubeMX 生成结构,Drivers 目录原样保留别动,真正要读的只有 Core 下面那七八个文件。这一章把目录职责划清楚,再顺着 main 的调用链走一遍,让你知道上电之后每一毫秒发生了什么。
3.1 目录树与文件职责
先看整体布局,心里有个地图再动手改。
cap_measure_stm32/ ├─ Core/ │ ├─ Inc/ │ │ ├─ main.h // 时钟宏、全局 include │ │ ├─ cap_measure.h // 测量 API、量程枚举、错误码 │ │ ├─ cap_calib.h // 标定数据结构与拟合接口 │ │ └─ bsp_board.h // 全部引脚宏集中在这里 │ └─ Src/ │ ├─ main.c // 初始化 + 主循环 │ ├─ cap_measure.c // 充放电时序、量程自动切换 │ ├─ cap_calib.c // 最小二乘拟合、标定表读写 │ ├─ bsp_gpio.c // 充电/放电/模拟开关引脚 │ ├─ bsp_tim.c // TIM2 捕获、TIM3 微秒延时 │ ├─ bsp_comp.c // 比较器 + 内部 VREF 分压 │ └─ bsp_oled.c // 0.96 寸 I2C OLED 驱动 ├─ Drivers/ // HAL 库与 CMSIS,原样保留 ├─ MDK-ARM/ // 或 STM32CubeIDE 工程目录 │ └─ cap_measure.uvprojx └─ Doc/ ├─ 接线图.png └─ 标定记录.xlsx分层逻辑很清楚:bsp_ 开头的是硬件抽象,改板子只动这一层;cap_ 开头的是业务逻辑,跟具体引脚无关;main.c 只负责串流程。这种切法在只有几千行的裸机项目里不算过度设计,但确实省事——换一块板子,理论上只要重写 bsp_gpio.c 和 bsp_board.h。
3.2 从上电到第一次读数:初始化调用链
main 函数短得有点意外,所有脏活都在各个 Init 里。
int main(void) { HAL_Init(); SystemClock_Config(); /* HSE 8M -> PLL -> 72MHz */ MX_GPIO_Init(); /* 充电脚、放电脚、模拟开关脚 */ MX_TIM2_Init(); /* 捕获用,PSC=6,ARR=0xFFFFFFFF */ MX_TIM3_Init(); /* 1MHz 自由跑,只给 delay 用 */ MX_COMP_Init(); /* COMP1,反相端接 1/2 VREF */ MX_USART1_UART_Init(); OLED_Init(); CAP_CalibLoad(); /* 从 Flash 末页读标定系数,读不到就用默认表 */ CAP_SelfTest(); /* 空载自检:应该读到接近 0 的容量 */ while (1) { float pf = 0.0f; if (CAP_AutoRangeMeasure(&pf) == CAP_OK) { printf("C = %.2f pF\r\n", pf); OLED_ShowFloat(0, 2, pf, 2); } else { printf("OUT OF RANGE\r\n"); OLED_ShowString(0, 2, "OUT OF RANGE"); } HAL_Delay(200); } }调用顺序有讲究。TIM2 必须在 COMP 之前初始化,因为比较器输出要接到 TIM2_CH1 的复用功能上,顺序反了捕获通道会配不上。TIM3 单独拿出来做延时,是因为 HAL_Delay 的精度只有 1ms,而放电和开关切换需要几百微秒级的等待,用 TIM3 轮询计数器最省事,也不占中断。
CAP_SelfTest 这一步建议保留。空载时(不接被测电容,只算 PCB 走线电容)读数应该在几个 pF 以内,如果读到几十上百 pF,八成是充电脚和电容焊盘之间有漏电或者助焊剂没洗干净。
3.3 关键宏集中管理:改一行换一个量程
所有能调的参数都塞进了 cap_measure.h 顶部,这是这份代码里最值得抄的习惯——现场调试时不用在几百行里翻魔法数字。
/* cap_measure.h —— 量程与阈值集中在这一段 */ #define CAP_TIM_CLK_HZ 10280000u /* TIM2 实际计数频率,PSC=6 时约 10.28MHz */ #define CAP_DISCHARGE_US 500u /* 放电时间,1uF 以上要加到 2000 */ #define CAP_MIN_TICK 200u /* 低于 200 tick 认为分辨率不够,换下一档 */ #define CAP_OVERRANGE_TICK 40000000u /* 单个量程内的计数上限,超了就报超量程 */ #define CAP_WAIT_MS 2000u /* 单次测量最长等待,防程序死等 */ #define CAP_ADC_HALF 2047u /* ADC 兜底方案里的半压阈值 */CAP_MIN_TICK 这个值不是拍脑袋定的。假设某档电阻下读数只有 50 个 tick,量化误差 ±1 tick 就意味着 2% 的抖动,再叠加比较器延迟和噪声,最终误差能到 5% 以上。把下限卡在 200 tick,量化误差压到 0.5% 以内,剩下的误差交给标定去补。
CAP_DISCHARGE_US 是最容易被忽略的一个。500µs 对几百 pF 的电容绰绰有余,但测 10µF 时放电回路的 RC 时间常数本身就很大,放电不彻底会导致下一次读数偏小。经验值是按 5 倍时间常数给:放电回路电阻约 100Ω,10µF 对应 τ = 1ms,那就要放 5ms。测大电容时把这个宏改到 5000。
4. 核心测量链路:从放电、捕获到 pF 换算的完整代码
前面都是铺垫,这一章是真正要抄的部分。一条完整的测量链路拆成四段:单次充放电的时序控制、硬件捕获和软件兜底两条实现路径、标定拟合、滤波输出。每段都能单独拿出去用。
4.1 单次测量函数:从放电、计时到换算
先看最核心的那个函数,它负责把一个电容从放电到读数完整走一遍。
/* cap_measure.c —— 单次充放电测量,返回定时器计数值 */ uint32_t CAP_MeasureRaw(void) { uint32_t t_start, t_end; /* 1) 拉低充电脚,打开放电管,把电容彻底放干净 */ HAL_GPIO_WritePin(CAP_CHG_PORT, CAP_CHG_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(CAP_DIS_PORT, CAP_DIS_PIN, GPIO_PIN_SET); CAP_DelayUs(CAP_DISCHARGE_US); /* 2) 关放电管,计数器清零,准备计时 */ HAL_GPIO_WritePin(CAP_DIS_PORT, CAP_DIS_PIN, GPIO_PIN_RESET); __HAL_TIM_SET_COUNTER(&htim2, 0); t_start = __HAL_TIM_GET_COUNTER(&htim2); /* 3) 开始充电:推挽输出高,经 R 给电容充电 */ HAL_GPIO_WritePin(CAP_CHG_PORT, CAP_CHG_PIN, GPIO_PIN_SET); /* 4) 等比较器翻转,带超时保护 */ while (CAP_COMP_READ() == 0) { if ((__HAL_TIM_GET_COUNTER(&htim2) - t_start) > CAP_OVERRANGE_TICK) { HAL_GPIO_WritePin(CAP_CHG_PORT, CAP_CHG_PIN, GPIO_PIN_RESET); return CAP_ERR_TIMEOUT; } } t_end = __HAL_TIM_GET_COUNTER(&htim2); HAL_GPIO_WritePin(CAP_CHG_PORT, CAP_CHG_PIN, GPIO_PIN_RESET); return (t_end - t_start); }第 1 步的放电时间由宏控制,这一步没做干净后面全白搭。第 2 步把计数器清零再读一次 t_start,是为了抵消清零指令本身的执行延迟——虽然只有几十纳秒,但测小电容时这点误差占比不小。
第 4 步的 while 循环必须带超时。曾经有个版本忘了写超时判断,接上一个 1000µF 的电解电容之后程序直接卡死在那里,看门狗都没开,只能拔电。现在的写法用计数器差值判断,不依赖 HAL_GetTick,即使主循环被打断也不会误判。
返回的是原始计数,不是容量。换算放到标定那一步做,这样同一份时序代码能服务所有量程。
4.2 硬件捕获路径:COMP 加 TIM 从模式,把误差压到纳秒级
轮询版本能用,但精度受限于 while 循环的响应时间,一次 GPIO 读加一次比较,几十纳秒就过去了。要更准就得让硬件自己干活:把比较器输出接到 TIM2_CH1,配置成从模式复位加输入捕获,边沿一到硬件自动把计数值锁进 CCR1。
/* bsp_tim.c —— COMP 输出接 TIM2_CH1,上升沿复位并捕获 */ static void CAP_TimerCaptureInit(void) { TIM_IC_InitTypeDef ic = {0}; TIM_SlaveConfigTypeDef slave = {0}; HAL_TIM_IC_Start(&htim2, TIM_CHANNEL_1); ic.ICPolarity = TIM_ICPOLARITY_RISING; /* 比较器由低翻高那一瞬 */ ic.ICSelection = TIM_ICSELECTION_DIRECTTI; ic.ICPrescaler = TIM_ICPSC_DIV1; ic.ICFilter = 0x3; /* 滤掉比较器输出的毛刺 */ HAL_TIM_IC_ConfigChannel(&htim2, &ic, TIM_CHANNEL_1); slave.SlaveMode = TIM_SLAVEMODE_RESET; /* 上升沿到来时 CNT 归零 */ slave.InputTrigger = TIM_TS_TI1FP1; slave.TriggerPolarity = TIM_TRIGGERPOLARITY_RISING; slave.TriggerPrescaler = TIM_TRIGGERPRESCALER_DIV1; slave.TriggerFilter = 0x0; HAL_TIM_SlaveConfigSynchro(&htim2, &slave); }ICFilter 设成 0x3 是实测调出来的。比较器输出在阈值附近会有几十纳秒的抖动,滤波太弱会捕获到假边沿,读数突然蹦出一个极小值;滤波太强(0xF)又会把真实的上升沿延迟几百纳秒,小电容直接测不准。0x3 对应约 4 个采样周期的滤波窗口,在这个场景下刚好。
从模式复位的好处是清零动作也由硬件完成,软件只需要在启动充电之后读一次 CCR1。
/* 用硬件捕获替代轮询,返回值依然是计数值 */ uint32_t CAP_MeasureRawHw(void) { CAP_Discharge(); /* 放电 + 关断放电管 */ __HAL_TIM_SET_COUNTER(&htim2, 0); __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_CC1); CAP_ChargeOn(); /* 开充电,硬件开始数数 */ uint32_t t0 = HAL_GetTick(); while (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_CC1) == RESET) { if (HAL_GetTick() - t0 > CAP_WAIT_MS) { CAP_ChargeOff(); return CAP_ERR_TIMEOUT; } } CAP_ChargeOff(); return HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1); }注意这里的 flag 检查和超时判断是两套机制:flag 由硬件置位,精度高;HAL_GetTick 只做兜底,精度差但不会死循环。两者配合,既保证测量精度又保证程序不卡死。
读出来的 CCR1 就是充电到 1/2 VCC 所需的 tick 数。因为从模式在边沿到来时已经把 CNT 清零,所以这个值就是全量时间,不用做减法。
4.3 标定与多点拟合:把计数值变成 pF
理论上 C = t / (0.693·R),但实际读数总有偏移:引脚和 PCB 的寄生电容、比较器的传输延迟、模拟开关的导通电阻,全叠在一起形成一个固定偏移量。直接套公式测 100pF 能差出 20%。解决办法是做一次线性拟合,用已知电容反推斜率和截距。
/* cap_calib.c —— 最小二乘拟合,把 tick 映射成 pF */ typedef struct { float k; /* pF / tick,斜率 */ float b; /* pF,截距,抵消寄生与延迟 */ } CAP_Calib_t; /* c_pf: 已知电容值;tick: 对应实测计数值;n 至少 3 点 */ void CAP_CalibLeastSquare(const float *c_pf, const uint32_t *tick, uint8_t n) { float sx = 0, sy = 0, sxx = 0, sxy = 0; for (uint8_t i = 0; i < n; i++) { float x = (float)tick[i]; float y = c_pf[i]; sx += x; sy += y; sxx += x * x; sxy += x * y; } float denom = (float)n * sxx - sx * sx; if (fabsf(denom) < 1e-6f) { /* 三点共线失败,放弃 */ g_calib.k = 0.0f; g_calib.b = 0.0f; return; } g_calib.k = ((float)n * sxy - sx * sy) / denom; g_calib.b = (sy - g_calib.k * sx) / (float)n; }每个量程必须单独标一套系数,因为斜率 k 和充电电阻成正比。工程里用数组存四组,切换量程时索引对应项。
| 量程 | 充电电阻 | 理论 k 值 | 实测 k 值 | 截距 b |
|---|---|---|---|---|
| RANGE_1K | 1kΩ | 144.3 pF/tick | 142.6 | −3.1 |
| RANGE_10K | 10kΩ | 14.43 pF/tick | 14.31 | −2.8 |
| RANGE_100K | 100kΩ | 1.443 pF/tick | 1.437 | −2.65 |
| RANGE_1M | 1MΩ | 0.1443 pF/tick | 0.1443 | −2.40 |
理论值按 k = 1e5 / (R × 0.693) 估算,前提是定时器 tick 取 0.0973µs。实测值和理论值差不到 1%,说明寄生参数主要影响的是截距而不是斜率,这也印证了做线性拟合而不是直接套公式的必要性。
标定操作有个小技巧:低量程用开路(约几个 pF)、100pF、470pF、1nF 四个点;高量程用 1nF、10nF、100nF、1µF。已知电容别用普通贴片,误差 5% 起,有条件借一台 LCR 表先标一遍手里的“标准件”,误差才能压到 1% 以内。
4.4 滤波与输出:中值加滑动平均,抗住单次毛刺
原始读数即使标定过也会偶尔蹦一个离谱的值,多半是比较器被电源噪声干扰了。单用滑动平均不行,一个野值能污染后面好几轮的输出;单用中值又太浪费,信息量丢得厉害。工程里用的是排序去极值再平均。
/* cap_measure.c —— 窗口 9,去掉最大最小各 2 个后求均值 */ #define CAP_FILTER_N 9 static uint32_t cap_buf[CAP_FILTER_N]; uint32_t CAP_FilterInsert(uint32_t raw) { static uint8_t idx = 0; cap_buf[idx] = raw; idx = (idx + 1) % CAP_FILTER_N; uint32_t tmp[CAP_FILTER_N]; memcpy(tmp, cap_buf, sizeof(tmp)); for (uint8_t i = 1; i < CAP_FILTER_N; i++) { /* 插入排序,9 个数足够快 */ uint32_t key = tmp[i]; int8_t j = i - 1; while (j >= 0 && tmp[j] > key) { tmp[j + 1] = tmp[j]; j--; } tmp[j + 1] = key; } uint32_t sum = 0; for (uint8_t i = 2; i < CAP_FILTER_N - 2; i++) sum += tmp[i]; return sum / (CAP_FILTER_N - 4); }窗口取 9 是个折中:太小抗不住野值,太大会让读数响应变慢,换一个电容要等好几秒才能稳定。9 个数插入排序的开销在 72MHz 上可以忽略,比 qsort 的函数调用开销还小。
输出走两路,串口给上位机记录,OLED 给人眼观察。串口格式建议固定成C,%.2f,pF这种能被解析的形式,后面接 Python 脚本画曲线或者做批量分选都方便。
5. 排查与避坑:测值跳变、归零失败、换档失准的 5 个现场记录
这套东西原理不复杂,但坑全在细节里。下面五条都是实际调过的,现象、原因、解决按顺序写,遇到问题可以对号入座。
5.1 接线与硬件层:读数恒为 0 或一直溢出
现象:串口一直打印 0.00 pF,示波器看充电脚却明明有电平跳变。
原因:充电脚配成了开漏输出,外部又没有上拉,实际上拉不上去;或者放电管的栅极和漏极焊反,NMOS 一直处于导通状态,电容刚充上就被放掉。
解决:用万用表直流档量充电脚对地电压,正常应该是 0 到 3.3V 之间跳变。确认 GPIO 初始化里是GPIO_MODE_OUTPUT_PP而不是GPIO_MODE_OUTPUT_OD,再核对 MOS 管封装引脚定义——SOT-23 封装的 1 脚是栅极,2 脚源极,3 脚漏极,不同厂家的丝印方向容易看错。
现象:读数永远是最大值,或者直接报超量程。
原因:放电管没打开,电容上残留的电荷让电压一开始就在阈值之上,比较器上电即翻转。
解决:示波器抓放电管栅极波形,确认测量前有 500µs 的高电平脉冲。如果用的是三极管而不是 MOS,注意基极电阻别选太大,10k 以上会导致深度饱和后关断变慢。
5.2 时序与量程层:数值乱跳、换档后整体偏
现象:同一个电容连续测十次,读数在 ±30% 之间跳。
原因:比较器输出没开滤波,阈值附近的噪声被反复捕获;或者测量期间串口在打印,中断打断了时序。
解决:把 TIM 的 ICFilter 从 0 提到 0x3,同时在比较器输入端并一个 100pF 到地做硬件低通。串口打印改成测完再发,别在测量过程中用阻塞式发送。
现象:换了量程之后读数不是偏一半,而是整体乘以某个奇怪系数。
原因:标定表没按量程索引,四个档位共用一套 k 和 b。斜率跟充电电阻成反比,混用必然错。
解决:把 g_calib 从单个结构体改成g_calib_tbl[RANGE_CNT]数组,CAP_TickToPf里按当前量程取系数。切换模拟开关后加 2ms 延时,等开关导通电阻稳定再开始测。
现象:接上 10µF 电容后程序卡住,看门狗复位。
原因:等待比较器翻转的循环没有超时退出,大电容的充电时间远超预期。
解决:在循环里同时判断计数器上限和 HAL_GetTick 差值,两套超时机制并行。CAP_WAIT_MS 设 2000 够用,超过这个时间肯定不正常。
5.3 标定与数值层:小电容测不出来
现象:100pF 以下读数基本不变,换个电容也是同一个数。
原因:引脚电容加 PCB 走线电容约 5~20pF,加上比较器几十纳秒的传输延迟,全部体现为一个固定偏移。当被测电容和这个偏移量在同一量级时,变化就被淹没了。
解决:标定必须带截距 b,而且要用低量程(1M 电阻档)单独标。另外可以做开路归零:上电时先测一次空载读数存起来,后续每次测量都减掉这个基准值。这一招在批量生产时特别管用,能抵消不同板子之间的寄生差异。
注意:开路归零要在整机装配完成后做,板子装进外壳之后寄生电容会变,工厂标定和实验室标定出来的值往往差几个 pF。
6. 二次开发:把它改成 8 通道电容扫描仪的改造路径
单个通道测着玩可以,真要拿去测液位或者做触摸面板,第一件事就是扩通道。最省事的做法是加一片 8 选 1 模拟开关,把八路被测电容依次接到充电节点上,主循环里轮询切换,采完一轮再统一滤波。
改造点集中在三个地方。第一,bsp_gpio.c 里新增三根通道选择脚,封装成CAP_SelectChannel(uint8_t ch),切换后延时 1ms 等开关稳定再测。第二,cap_measure.c 的测量函数加一个通道参数,内部先切通道再走原来的时序,返回值和错误码保持不变。第三,滤波器的缓冲区从一维改成二维,每个通道独立维护自己的滑动窗口,否则八个通道的数据会串在一起,读数乱成一锅粥。
/* 多通道扫描的主循环骨架 */ #define CAP_CH_NUM 8 static uint32_t cap_win[CAP_CH_NUM][CAP_FILTER_N]; void CAP_ScanTask(void) { for (uint8_t ch = 0; ch < CAP_CH_NUM; ch++) { CAP_SelectChannel(ch); CAP_DelayUs(1000); /* 等模拟开关稳定 */ uint32_t raw = CAP_MeasureRawHw(); if (raw == CAP_ERR_TIMEOUT) continue; float pf = CAP_TickToPf(raw, CAP_GetCurrentRange()); cap_result[ch] = CAP_FilterChannel(ch, pf); } }第二个方向是做差分。单端测量受环境温湿度影响明显,介质吸水之后介电常数变化能让读数漂 5% 以上。加一路固定参考电容,每次测量同时采参考通道,用比值代替绝对值,温漂能压掉一大半。这在液位检测里是标配做法,代价是多一路硬件和一次额外测量时间。
第三个方向是标定参数的持久化。现在的系数写死在代码里,现场换探头就得重新编译烧录,很别扭。改用 Flash 末页存一个结构体,上电校验魔数,不
本文还有配套的精品资源,点击获取