4、ADC与传感器接口:土壤湿度传感器原理(电阻式 vs 电容式)
土壤湿度传感器,说白了就是帮我们判断「土里到底有没有水」的器件。我最早接触这个,是在一个温室大棚项目里。当时客户说「你给我测准点」,结果我选了电阻式传感器,没几天就锈断了探头……嗯,这里面的坑,咱们一个个说。
4.1 电阻式 vs 电容式:两种原理,两种命运
先讲电阻式。它的原理很简单:土壤含水越多,导电性越好,测出来的电阻就越小。你拿两根金属探针插土里,测它们之间的电阻值,就能反推湿度。
听起来很直接对吧?但我个人习惯,不太推荐在长期项目里用电阻式。为什么?
- 容易腐蚀:金属探针一直通电,在潮湿土壤里电解反应很快。我有个项目,用了不到一个月,探针就锈断了。
- 受离子干扰:土壤里的肥料、盐分会严重影响读数。同一块地,施肥前后测出来的值能差一倍。
- 精度有限:说白了就是个定性判断——干还是湿,想精确到百分比很难。
再讲电容式。它利用的是土壤介电常数变化。水分的介电常数(约80)远大于干土(约3-5),所以土壤含水量变了,电容值就会变。我们测这个电容值,就能算出湿度。
我后来所有项目都换成了电容式传感器。好处很明显:
- 不腐蚀:探头表面有绝缘涂层,不和土壤直接接触。
- 抗干扰:不受肥料、盐分影响。
- 寿命长:我有个项目用了两年多,读数依然稳定。
当然,电容式也有缺点——它比电阻式贵一点,而且对温度敏感。不过,加个温度补偿算法就能解决。
核心结论:做产品选电容式,做实验选电阻式。别问我怎么知道的——我当年就是图便宜选了电阻式,结果被客户骂了一周。
4.2 ADC采样:把模拟信号变成数字
传感器输出的是模拟电压信号,但单片机只认识0和1。所以我们需要ADC(模数转换器)。
大多数MCU都内置了ADC,比如STM32的12位ADC、ESP32的12位ADC。12位是什么意思?就是能把0-3.3V的电压范围,分成2^12=4096个台阶。每个台阶对应一个数字值。
举个例子:
- 如果传感器输出1.65V,ADC读到的值大约是2048(满量程的一半)。
- 如果输出3.3V,读到的值是4095。
- 如果输出0V,读到的值是0。
嗯,这里要注意:ADC的参考电压很重要。我建议用MCU内部的稳定参考电压,或者外接一个精密参考源。否则,电源波动会直接影响读数。
警告:ESP32的ADC线性度不太好,尤其是靠近0V和3.3V两端。我踩过这个坑——测出来的数据在两端明显失真。后来我加了一个软件校准,才勉强能用。如果你追求精度,建议用外部ADC芯片,比如ADS1115。
4.3 滤波算法:别让噪声骗了你
ADC读出来的原始数据,噪声很大。你想想看,土壤本身就不均匀,加上电磁干扰、电源纹波,读出来的值可能跳来跳去。直接拿这个值去控制灌溉,那阀门会频繁开关,水泵也会坏掉。
所以,我们需要滤波。我常用的几种方法:
| 算法 | 原理 | 适用场景 | 缺点 |
|---|---|---|---|
| 限幅滤波 | 如果当前值和上次值相差超过阈值,就丢弃 | 去除突发尖峰噪声 | 无法平滑缓慢变化 |
| 中值滤波 | 连续采N次,取中间值 | 去除随机噪声 | 需要缓存,响应慢 |
| 滑动平均 | 维护一个队列,取平均值 | 平滑缓慢变化的信号 | 有滞后 |
| 一阶低通滤波 | 新值 = α * 采样值 + (1-α) * 旧值 | 实时性要求高的场景 | α需要调参 |
我个人习惯用「限幅+滑动平均」的组合。先限幅去掉明显异常的值,再滑动平均平滑数据。这样既快又稳。
小技巧:滑动平均的窗口大小,我一般取8或16。为什么?因为2的幂次,可以用移位运算代替除法,速度快很多。在嵌入式里,能省一个时钟周期都是好的。
4.4 实战:读取土壤湿度并打印到串口
好了,理论说完了,咱们直接上代码。我用的是STM32的HAL库,但思路通用,换成ESP32或Arduino也一样。
// 土壤湿度传感器读取示例
// 使用ADC1,通道0(PA0)
// 电容式传感器,输出0-3.3V对应0-100%湿度
#include "main.h"
#define ADC_BUFFER_SIZE 16 // 滑动窗口大小
#define FILTER_ALPHA 0.3f // 一阶低通滤波系数
static uint16_t adc_buffer[ADC_BUFFER_SIZE];
static uint8_t buffer_index = 0;
static float filtered_value = 0.0f;
// 限幅滤波阈值
#define LIMIT_THRESHOLD 100 // ADC值变化超过100则丢弃
// 初始化ADC
void ADC_Init(void)
{
// 这里省略HAL库的初始化代码
// 配置ADC1,通道0,12位分辨率,连续转换模式
// 实际项目中请根据芯片型号配置
}
// 读取一次ADC原始值
uint16_t ADC_ReadRaw(void)
{
HAL_ADC_Start(&hadc1);
if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK)
{
return HAL_ADC_GetValue(&hadc1);
}
return 0;
}
// 限幅滤波
uint16_t LimitFilter(uint16_t new_value, uint16_t last_value)
{
if (abs((int)new_value - (int)last_value) > LIMIT_THRESHOLD)
{
return last_value; // 丢弃异常值
}
return new_value;
}
// 滑动平均滤波
uint16_t MovingAverageFilter(uint16_t new_value)
{
static uint16_t sum = 0;
static uint8_t count = 0;
// 移除旧值,加入新值
sum -= adc_buffer[buffer_index];
sum += new_value;
adc_buffer[buffer_index] = new_value;
buffer_index = (buffer_index + 1) % ADC_BUFFER_SIZE;
if (count < ADC_BUFFER_SIZE)
{
count++;
return sum / count;
}
return sum / ADC_BUFFER_SIZE;
}
// 一阶低通滤波
float LowPassFilter(float new_value)
{
filtered_value = FILTER_ALPHA * new_value + (1.0f - FILTER_ALPHA) * filtered_value;
return filtered_value;
}
// 将ADC值转换为湿度百分比(0.0 - 100.0)
float ADC_To_Humidity(uint16_t adc_value)
{
// 假设传感器输出:0V对应0%,3.3V对应100%
// 12位ADC,满量程4095
float voltage = (float)adc_value * 3.3f / 4095.0f;
float humidity = voltage / 3.3f * 100.0f;
// 限幅
if (humidity < 0.0f) humidity = 0.0f;
if (humidity > 100.0f) humidity = 100.0f;
return humidity;
}
// 主循环
int main(void)
{
HAL_Init();
SystemClock_Config();
ADC_Init();
UART_Init(); // 初始化串口,用于打印
uint16_t raw_value = 0;
uint16_t last_value = 0;
uint16_t filtered_adc = 0;
float humidity = 0.0f;
while (1)
{
// 1. 读取原始值
raw_value = ADC_ReadRaw();
// 2. 限幅滤波
raw_value = LimitFilter(raw_value, last_value);
last_value = raw_value;
// 3. 滑动平均
filtered_adc = MovingAverageFilter(raw_value);
// 4. 一阶低通(可选,如果滑动平均已经够平滑,可以省略)
// float smooth_value = LowPassFilter((float)filtered_adc);
// 5. 转换为湿度
humidity = ADC_To_Humidity(filtered_adc);
// 6. 打印到串口
printf("Raw: %4d, Filtered: %4d, Humidity: %.1f%%\r\n",
raw_value, filtered_adc, humidity);
HAL_Delay(1000); // 每秒打印一次
}
}
关键点:
- 先限幅,再滑动平均——顺序不能反。先去掉尖峰,再平滑,效果最好。
- ADC转湿度的公式,需要根据实际传感器标定。我这里的公式是假设线性关系,但实际传感器可能不是完全线性的。最好用两点校准:干土测一次,湿土测一次,然后线性插值。
- 串口打印的波特率建议用115200,别用9600——太慢了,调试时等得着急。
避坑指南:我曾经在一个项目里,发现湿度读数莫名其妙地跳变。查了两天,最后发现是ADC的采样时间设置太短,电容还没充放电完就开始转换了。解决办法:把ADC采样周期从3个时钟周期改成15个。嗯,从此以后,我所有ADC采样周期都至少设到15个以上。
4.5 小结
这一章我们聊了:
- 电阻式 vs 电容式:选电容式,别走弯路。
- ADC采样:12位够用,但要注意参考电压和线性度。
- 滤波算法:限幅+滑动平均,简单又可靠。
- 实战代码:从ADC读到串口打印,一条龙搞定。
下一章,我们会把土壤湿度数据存到EEPROM里,这样断电也不会丢。嗯,到时候再聊怎么选存储芯片、怎么防写坏。
公众号:蓝海资料掘金营,微信deep3321