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