聊到土壤监测,很多人第一反应是“板子接个传感器,串口打印一下就完事”。但真把板子埋进土里,你会发现“读数”和“可信的读数”完全是两回事。最近我用STM32F103+CubeMX整理了一套土壤墒情监测的最小方案,从CubeMX图形化配置、传感器选型、ADC采集,一路做到标定和低功耗,感觉这套组合非常适合从零起步的嵌入式朋友参考。它能做的不只是测个湿乎乎的数字,而是把“土壤到底处于什么状态”这个含糊问题,变成可以长期观察、可以对比的量化数据。
1. 为什么是STM32F103:一颗“老”芯片凭什么还适合做土壤监测
1.1 先把需求拆开:你的“土壤”项目到底要测什么
很多人一上来就找芯片、挑传感器,其实土壤监测第一步是搞清楚你要回答什么问题。
如果你只是想判断“花盆该不该浇水”,那一个简单的湿度阈值就够了,用什么都行;如果你想做的是大棚、农田或者实验用的墒情记录,那需求就完全不一样了。这类项目通常要持续测量土壤的体积含水量,也就是单位体积土壤里水的体积占比,而不是只看表面干湿。此外你可能还需要土壤温度,因为温度会影响水分蒸发和根系活动,也能做湿度数据的补偿。再往上一点,还有人测土壤EC值(电导率),用来推断养分浓度,但那已经是另一个量级的事了。
以我做的这套最小方案为例,目标定得很清楚:低功耗、电池供电、每隔一段时间采集一次土壤湿度和温度,通过串口把数据传出来。后端可以是数据记录仪,也可以是无线模块。这个需求并不复杂,但对稳定性和可重复性有要求,因为土壤环境比空气恶劣得多——潮湿、盐分、温差、还有各种物理挤压,板子和传感器都要扛得住。
1.2 F103+CubeMX的组合优势:外设、生态与配置效率
确定需求之后,芯片选型就顺理成章了。STM32F103这个系列属于ARM Cortex-M3内核,主频最高72MHz,看起来参数不亮眼,但它是做这类采集任务的“万金油”。
土壤监测需要的东西它基本都齐了:12位ADC,分辨率4096级,足够覆盖湿度传感器从干到饱和的变化;多个USART,方便接串口屏、蓝牙模块或者LoRa透传;还带RTC和多种低功耗模式,Stop模式下电流可以压到几十微安级别,对电池供电很关键。更现实的一点是,这颗芯片的资料和社区案例极多,遇到问题基本都能查到解决思路,对新手非常友好。
CubeMX则是把“配置芯片外设”这件事从手写寄存器变成了图形化操作。你不需要记住每个寄存器的偏移地址和位定义,只要在界面上把时钟树、ADC、串口、GPIO勾选好,它就能自动生成初始化代码。这个价值在调试时特别明显:想改采样引脚、换波特率、调ADC采样周期,改完重新生成即可,不用打着手电翻参考手册。
当然,F103也不是没有短板。它没有硬件DMA和ADC的深度FIFO协同(虽然有DMA但用法偏传统),也没有USB原生支持,但土壤监测这种低频采集场景,这些短板基本不影响。相比之下,同价位有人会考虑ESP8266或ESP32,觉得自带Wi-Fi更方便。但如果你不是立刻要做联网,F103的模拟采集能力、低功耗表现和稳定性明显更适合先跑通链路,后面再通过串口外接无线模块也不迟。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 传感器选型是第一个大坑:电阻式、电容式还是数字式
2.1 电阻式传感器:便宜但容易在土里“自毁”
市面上最便宜的土壤湿度传感器,通常就是两块裸露的金属探针,利用土壤的电阻随含水量变化这个原理。土壤越湿,离子越多,导电性越好,探针之间的电阻就越小。MCU读到的就是分压后的电压值。
听起来很直接,但这类传感器有一个致命弱点:直流极化腐蚀。探针长期插在潮湿的土壤里,工作电流会在探针表面形成电解效应,金属离子慢慢析出,探针间距和接触特性都会改变。结果就是同一块土壤,同一个传感器,用了一个月之后的读数和刚买回来时明显不一样。加上土壤本身的盐分变化也会干扰电导率,电阻式方案的长期稳定性很难保证。
我不是说它完全不能用。如果你只是短期测试、或者只需要分辨“干/湿”两个状态,它的性价比确实高,一块钱级别就能买到。但做长期监测项目,我建议直接跳过,免得后期数据漂移到你怀疑人生。
2.2 电容式传感器:非接触测量,我最终的选择
我最后选用的是电容式土壤湿度传感器,常见形态是PCB板上的叉指铜箔,外面覆盖一层防焊层或树脂,不直接接触土壤。它的原理是利用土壤的介电常数随含水量变化这个特性—水的介电常数大约在80左右,干燥土壤颗粒的介电常数只有个位数。土壤中水分越多,等效介电常数越高,叉指电容的电容值就越大。
传感器内部通常用振荡电路或电容-频率转换电路,把电容变化变成频率或电压输出。常见模块输出模拟电压,在干燥土壤时约1.2V到1.5V,在饱和土壤时约2.5V到3.0V,具体范围因型号而异。因为检测部分不裸露,没有直流电化学腐蚀问题,埋在土里能工作更久,这是它最大的优势。
这类模块的工作电流一般在5mA以内,但上电瞬时冲击可能到几十毫安,因此我建议用GPIO加MOS管控制它的供电,采集完就断电,既能省电,也能进一步减少电极极化和发热漂移。
2.3 数字式与专业探头:什么样的情况才值得上
除了电容式模拟输出,市场上还有不少数字式土壤传感器,比如I2C接口的小型湿度温度一体芯片,以及专业级的RS485透传探头。数字式的好处是抗干扰强,数据直接是校正过的,省去标定环节。但价格从几十到几百上千不等,专业探头甚至需要配套的采集器。
我的看法是:如果你是给学校课程、个人DIY或者小规模农业项目做方案,电容式模拟输出传感器是性价比最高的起点。它便宜、可替换、原理透明,而且标定过程本身就是一次很好的学习。如果你是要做商用级产品、或者需要精确的土壤水分物理量(比如科研要求),那再考虑数字式或者专业探头。
| 类型 | 原理 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| 电阻式 | 土壤电阻随含水量变化 | 便宜、电路简单 | 直流极化、长期漂移大 | 短期干湿判断 |
| 电容式 | 土壤介电常数随含水量变化 | 非接触、寿命长、响应快 | 受土壤类型影响需标定 | DIY与长期监测首选 |
| I2C数字式 | 芯片内置介电常数测量 | 免标定、直接读数据 | 价格较高、土壤类型限制 | 小型原型、快速验证 |
| RS485专业探头 | 高频反射/频域测量 | 精度高、可深埋远传 | 成本高、需专业采集器 | 农业物联网、科研 |
3. CubeMX工程搭建:时钟、ADC、串口与传感器供电配置
3.1 时钟树与ADC时钟:这里偷懒,后面数据全是花的
CubeMX里新建工程、选定具体型号后,第一步不是急着配置外设,而是先把时钟树理清楚。土壤传感器采集对时间要求不高,但ADC的采样质量与采样时钟直接相关。
我通常用板载8MHz晶振做HSE,经过PLL倍频到72MHz系统时钟。关键是ADC时钟:STM32F103的ADC最大时钟约为14MHz,超过这个值转换精度会明显下降。而72MHz的系统时钟经过APB2预分频后,ADC外设时钟可以选2、4、6、8分频。我习惯把ADC时钟设为72/6=12MHz,既低于上限,又能保证合理的转换速度。
具体配置里,ADC采样周期我建议直接拉到最大,即239.5周期。这样单次转换总时间大约是(239.5+12.5)/12MHz≈21微秒,对土壤监测来说完全可接受。采样时间越长,ADC内部的采样保持电容充电越充分,读到的电压越接近真实值。这一点在后面讲阻抗匹配时会更清楚,但你先记下:采样周期拉满不会错。
3.2 ADC与串口配置:采集链路和调试链路一次接通
在CubeMX的左侧列表里勾选ADC1,使能一个通道,比如ADC1_IN1对应的PA1引脚。配置为单次转换模式、软件触发,关闭连续转换。至于扫描模式,如果只接一个传感器不用开;如果后面要接多路,可以后续再加。
串口部分我习惯用USART1,配置为异步模式,波特率115200,8位数据、无校验、1位停止位。串口是调试时的“眼睛”,没有它你会非常痛苦。打开NVIC里的串口中断,不需要在CubeMX里写接收逻辑,先用发送功能把调试数据打出来就行。
生成代码之后,在main函数里其实只需要写一个循环:先读取ADC值,再通过串口打印。我强烈建议你在正式接土壤传感器之前,先用一个电位器把电压调到1.0V到3.3V之间,看看串口打印的ADC码值是否跟万用表读数对应。这个“干跑”步骤可以帮你快速排除接线错误,而不是等到埋土里才发现数据全是乱码。
c复制uint32_t read_adc_value(void)
{
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 100);
uint32_t value = HAL_ADC_GetValue(&hadc1);
HAL_ADC_Stop(&hadc1);
return value;
}
3.3 GPIO控制传感器电源:省电且延长寿命的关键
土壤传感器模块不需要一直通电。一个很实用的小设计,是用一个GPIO控制MOS管或三极管,把传感器的VCC作为开关端。采集前拉高GPIO,延时几十毫秒等传感器稳定,然后读取ADC,读完立刻拉低GPIO切断电源。
为什么要这么做?一方面是为了省电,电容式传感器虽然静态电流不大,但一直通电也是浪费;另一方面是保护传感器,尤其是在潮湿环境里,减少通电时间能显著降低电极电解和电路板受潮引发的故障概率。
CubeMX里把PA0配置为GPIO_Output即可,输出低电平默认为关闭。MOS管的栅极串一个10kΩ电阻,源极接3.3V,漏极接传感器VCC。延时我用HAL_Delay,虽然它会占CPU,但采集频率低,无伤大雅。
c复制void read_soil_sensor(void)
{
HAL_GPIO_WritePin(SENSOR_PWR_GPIO_Port, SENSOR_PWR_Pin, GPIO_PIN_SET);
HAL_Delay(50);
uint32_t adc = read_adc_value();
float voltage = adc * 3.3f / 4095.0f;
HAL_GPIO_WritePin(SENSOR_PWR_GPIO_Port, SENSOR_PWR_Pin, GPIO_PIN_RESET);
char buf[64];
snprintf(buf, sizeof(buf), "adc:%lu volt:%.3f\r\n", adc, voltage);
HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 1000);
}
3.4 代码骨架与printf重定向
C语言里用printf打点东西会更顺手,但HAL库默认没有重定向。在工程里加入以下代码,把printf输出重定向到USART1即可:
c复制int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
return ch;
}
注意需要包含头文件stdio.h,同时在CubeMX生成代码的配置里确保Use MicroLIB是打开的,否则重定向容易踩坑。有了printf,后面的数据打印、标定记录都方便很多,你会真切感受到“能直接看到数值”对调试效率的提升。
4. ADC读数不准?先解决VREF、阻抗匹配和滤波
4.1 VREF与电源噪声:同一个3.3V供电搅在一起,读数自然会跳
第一次把传感器接上去,我遇到了一个非常典型的坑:串口打印的ADC数值一直在波动,明明传感器插在同一个花盆里没动,读数却在几百个码值之间跳。查了一圈,问题出在供电上。
STM32F103的ADC参考电压VREF+通常直接连VDDA,简单说,你的供电电压就是ADC的“尺子”。土壤传感器工作时电流会变化,如果传感器和MCU共用同一路3.3V,且电源走线又细又长,VCC上就会叠加纹波和跌落。传感器输出信号本身也会随之波动,这就导致“尺子”本身不准,测出来的结果自然不稳定。
解决办法有三个层级。第一,尽量把传感器供电和MCU供电分开,最理想的是传感器用独立的LDO供电。第二,如果不想增加器件,就把传感器电源控制GPIO和ADC采样之间的延时拉开,等供电稳定后再采集。第三,在传感器VCC引脚对地加一个100uF电解电容和0.1uF陶瓷电容,构成简单的LC滤波。这三种方案可以组合使用,实测下来读数稳定性提升相当明显。
4.2 传感器输出阻抗与采样保持电容:为什么数据忽大忽小
还有一类ADC读数不准,问题出在STM32F103内部的采样保持电容上。ADC在启动采样时,内部的采样电容需要从外部源快速充电,此时外部信号源的输出阻抗越低,充电越快,最终采到的电压越接近真实值。
土壤湿度传感器模块的输出端通常经过一个分压电阻网络,输出阻抗可能高达几十千欧甚至更高。当这个阻抗远大于ADC采样电容的充电路径阻抗时,采样电容在短暂采样时间内根本充不满,读到的电压会比真实值偏低,而且不稳定。这就像用一个小水管去灌一个大水池,时间不够,水位自然上不去。
解决思路有两种。第一种是把采样时间拉到最大,这也是我在CubeMX里选239.5周期的原因,能在一定程度上缓解充电不足的问题。第二种更彻底,加一级电压跟随运放,把传感器的高阻抗输出转换成低阻抗信号再接ADC。运放供电注意要选择轨到轨输入输出型,否则在接近电源轨的电压范围会失真。对于土壤湿度这种慢变化信号,普通的单运放就够用了。
4.3 软件滤波:先滑动平均,别急着上卡尔曼
硬件做好了,软件上再加一道保险。我建议从最简单的滑动平均和中值滤波开始,它们在绝大多数土壤监测场景下已经足够有效。
滑动平均就是把最近N次ADC采样值存进一个数组,每次求平均。比如N取10,那么最新输出是最近10次读数的均值。它的优点是能平滑随机噪声,缺点是对快速变化反应稍慢,但土壤湿度本来就是缓慢变化的,这个矛盾不存在。中值滤波则是取N次采样的中间值,能非常好地剔除偶发的毛刺。
千万不要一开始就上卡尔曼滤波。卡尔曼滤波适合有明确运动模型和过程噪声的场景,用来做土壤湿度有点杀鸡用牛刀,参数调不好反而会把数据弄得奇奇怪怪。先用简单方法把数据稳定下来,再评估有没有必要上复杂算法。
c复制#define FILTER_N 10
uint32_t filter_buffer[FILTER_N];
uint8_t filter_index = 0;
uint32_t moving_average(uint32_t new_value)
{
filter_buffer[filter_index] = new_value;
filter_index = (filter_index + 1) % FILTER_N;
uint32_t sum = 0;
for (uint8_t i = 0; i < FILTER_N; i++)
sum += filter_buffer[i];
return sum / FILTER_N;
}
5. 从ADC码值到含水量:标定才是“准”与“不准”的分界线
5.1 为什么不能直接套厂商曲线
传感器的数据手册上通常会画一条“输出电压-含水量曲线”,但如果你直接把这条曲线当成万能公式,现场数据大概率对不上。原因在于土壤介电响应受土壤类型影响很大:黏土的颗粒结构、沙土的孔隙大小、有机质含量、土壤压实程度,都会改变同样含水量下的介电常数。
举个简单例子,两块土壤,一块是沙土,一块是黏土,体积含水量都是30%,电容式传感器的输出电压很可能差0.3V以上。这就是为什么专业土壤传感器出厂时都带有大量土壤样本标定数据,而通用模块只能给一个参考曲线。
你需要的是一条自己土壤样本的专属曲线。说白了,就是用已知含水量的人工土壤样本,去记录传感器的实际ADC输出,然后拟合一个数学关系。这个工作不难,但需要耐心。
5.2 一套能落地的标定流程:烘干称重与多点采集
标定土壤湿度的传统基准是烘干称重法,也叫重量含水量法。具体操作是:取一定量风干后的土壤,称重记为干土质量m0;然后加入一定质量的水Δm,搅拌均匀,此时土壤质量含水量为Δm/m0×100%。把拌匀的土壤装进一个固定容器里,压实到和实际使用场景接近的程度,再把传感器插入并读取稳定后的ADC值。
我建议至少做五个标定点:干土、25%、50%、75%以及饱和状态。饱和状态可以通过持续加水直到土壤表面出现水膜来判断,此时土壤的含水量接近田间最大持水量。每做完一个点,记录一组“ADC码值-含水量”对应数据,全部做完后,在表格软件里用线性或二次多项式拟合出ADC到含水量的换算公式。
需要提醒的是,每次加水后一定要搅拌均匀,并且放置一段时间让水分在土壤中重新分布,否则传感器测到的只是局部的含水量,不是整杯土的平均值。另外,传感器插入的位置每次尽量一致,因为同一杯土里不同位置的紧实程度也会影响读数。
5.3 温度补偿的简单处理
土壤温度对介电常数有影响,同一块土壤在夏季和冬季的读数会略有不同。最省事的补偿方式,是在传感器旁边加一个NTC热敏电阻或者DS18B20温度传感器,测量土温,然后在公式计算的基础上做一阶修正。
一个比较实用的做法是:在标定时同时记录温度,然后拟合一个“每摄氏度导致的ADC偏移量”。以我用的电容式传感器为例,温度每升高1℃,ADC读数大约下降几个码值。我一般会在程序里加上一句线性补偿:修正后的ADC = 原始ADC + (参考温度 − 当前温度) × 温度系数。这个系数不需要特别精确,因为土壤湿度本身就是个粗糙物理量,能把趋势校准到一个合理的量级就够了。
c复制float calc_soil_moisture(uint32_t adc_value, float temperature)
{
// 以25℃为参考点做温度补偿
float compensated_adc = (float)adc_value + (25.0f - temperature) * 2.5f;
// 由实测标定得到的二次拟合系数(示例)
float vol = -0.0000018f * compensated_adc * compensated_adc
+ 0.0185f * compensated_adc
- 16.72f;
if (vol < 0.0f) vol = 0.0f;
if (vol > 100.0f) vol = 100.0f;
return vol;
}
上面代码里的拟合系数只是举例,你把标定获得的数据在表格里做趋势线,就能得到你自己的系数。把这个公式写进程序,串口输出的就不再是“电压多少伏”,而是“土壤含水量大约百分之多少”,这样后端就可以直接使用这个数据。
6. 实测数据与低功耗:让板子在现场长期干活
6.1 一组真实土壤状态的ADC读数
我用晾干的园艺土做了最简单的三状态测试:干燥土、湿润土(浇透后放置半天)、泥浆(表面可见积水)。传感器插入深度保持约5厘米,每次等读数稳定后记录100次采样的平均值,结果如下:
| 土壤状态 | 输出电压(V) | ADC码值 | 估算含水量(%) |
|---|---|---|---|
| 干燥土 | 1.28 | 1588 | 约8 |
| 湿润土 | 1.96 | 2432 | 约32 |
| 泥浆 | 2.61 | 3238 | 约55 |
这组数据说明,电容式传感器配合F103的12位ADC,区分干湿状态、甚至分辨湿润和泥浆状态是完全没有问题的。当然,实际土壤环境的数值会随土样差别而变化,但趋势是一致的。
值得强调的是,不要把ADC码值当成“水分百分比”直接对外输出。同样的ADC码值,放进沙土和黏土里代表的意义完全不同。这就是为什么必须有标定环节。
6.2 睡眠唤醒与间歇采集:电池供电的续航思路
土壤监测设备通常部署在户外,不能天天换电池。这时候STM32F103的低功耗模式就派上用场了。
我的做法是:系统平时进入Stop模式,用RTC定时唤醒,比如每10分钟唤醒一次。唤醒后完成土壤采集、串口发送、然后再次进入Stop。传感器电源平时是断开的,只在采集时上电,这样绝大多数时间系统电流非常低。
实测下来,整板在Stop模式下的电流大约为几十微安;传感器供电时间按每次100毫秒计算,10分钟一次,平均电流几乎可以忽略。用两节AA电池或者一块1000mAh锂电,跑几个月没有问题。如果你需要更低的功耗,可以考虑待机模式或者关掉不用的外设时钟,但工程复杂度会随之上升,建议先跑通Stop模式再逐步优化。
进入Stop模式的代码大致如下:
c复制void enter_stop_mode(void)
{
HAL_GPIO_WritePin(SENSOR_PWR_GPIO_Port, SENSOR_PWR_Pin, GPIO_PIN_RESET);
HAL_SuspendTick();
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
/* 唤醒后恢复时钟 */
SystemClock_Config();
HAL_ResumeTick();
}
这里有一个非常容易踩的坑:从Stop模式唤醒后,系统时钟会回到复位状态,必须重新调用SystemClock_Config恢复时钟配置,否则HAL_Delay和串口都会异常。我第一版代码忘了这一步,结果就是串口偶尔打不出东西,排查了半天才发现是时钟没恢复。
6.3 户外长期运行会遇到的“存活率”问题
土壤监测板子在户外长期运行,最大的敌人不是芯片,而是环境和连接。传感器线缆和板子的连接点如果暴露在潮湿环境中,很容易因为凝露或渗水导致短路,读数开始乱跳,甚至直接损坏MCU引脚。
我的建议是给板子加一个防水外壳,所有线缆进出口用防水接头或打热熔胶密封。传感器的叉指部分虽然本身耐腐蚀,但焊盘和引线根部最好也做一下防护处理,常见做法是刷一层三防漆或者用树脂灌封。如果是埋地安装,还要考虑土壤冻胀对探针和线缆的机械拉扯,线缆要留出足够的松弛余量。
另一个实际问题,是土壤环境不是一成不变的。植物根系会挤压传感器周围的土壤,导致局部容重变化;浇水后土壤沉降也会改变传感器与土壤的接触状态。因此我建议设备运行一段时间后,比如一个季度,重新做一次现场校准或者至少检查一下数据是否出现系统性的偏移。
这个项目做完之后,我最大的体会是:土壤监测的难点从来不在“把电压读出来”,而在“让读出来的电压长期可信”。CubeMX帮你省掉了繁琐的寄存器配置,F103给了你足够的外设和功耗空间,传感器选型和标定则是决定最终数据质量的分水岭。先花一个下午把标定曲线做出来,再谈什么智能化、联网,这是最实在的路线。
