1. 项目背景与硬件选型
第一次接触土壤湿度监测是在去年帮朋友改造智能花盆的时候。当时市面上成品监测模块动辄几百元,而用STM32+传感器方案成本不到50元。这种DIY方案不仅便宜,还能灵活适配各种场景,比如家庭绿植、阳台菜园或是小型农业实验。
核心硬件只需要三样:STM32开发板、土壤湿度传感器和几根杜邦线。我用的是一款常见的STM32F103C8T6最小系统板,价格20元左右,性能足够处理传感器数据。传感器方面推荐YL-69或FC-28,这两个都是经典款,带模拟量(AO)和数字量(DO)双输出,防水探头设计,某宝单价不到10块钱。
这里有个选购避坑经验:一定要确认传感器输出类型。早期我买过一款只有DO输出的传感器,结果只能判断"干/湿"二值状态,无法获取具体湿度百分比。后来换的YL-69模块就实用多了——AO输出0-3.3V模拟电压对应湿度变化,DO输出则可通过旋钮调节触发阈值,两种模式配合使用既灵活又可靠。
2. 硬件连接与电路设计
实际接线比想象中简单得多,但新手常犯两个错误:一是电源接反烧毁传感器,二是模拟信号线没接对导致读数异常。正确的连接方式应该是:
- 传感器VCC接开发板5V引脚(注意不是3.3V!)
- GND对GND
- AO接STM32的PA5(或其他ADC通道引脚)
- DO可接任意GPIO,我习惯用PA0
遇到过最头疼的问题是电源干扰。有次测试时读数总是跳变,后来发现是开发板USB供电不稳。解决方法有两个:要么给传感器单独供电,要么在VCC和GND之间加个100μF的滤波电容。实测下来,第二种方案成本最低效果也好,电容价格不到1毛钱。
3. ADC采集与校准实战
STM32的ADC模块用起来简单,但要获得稳定读数需要点技巧。先看初始化代码关键点:
void ADC1_Init(void) { // 时钟使能部分不能少! RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 必须设为模拟输入 GPIO_Init(GPIOA, &GPIO_InitStructure); ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换模式 ADC_InitStructure.ADC_NbrOfChannel = 1; // 单通道 ADC_Init(ADC1, &ADC_InitStructure); // 校准步骤千万不能省! ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); }实际采集时建议用多次采样取平均的方法。我封装了个实用函数:
float Get_SoilHumidity(uint8_t times) { uint32_t sum = 0; for(uint8_t i=0; i<times; i++) { sum += Get_Adc(ADC_Channel_5); // PA5对应Channel5 Delay_ms(5); // 适当延时 } float voltage = (sum/times) * (3.3f/4096); // 转电压值 return (100 - (voltage/3.3)*100); // 转百分比 }注意两个细节:一是STM32F103的ADC是12位精度(0-4095),二是土壤越湿输出电压越低,所以要做个100减的操作。
4. 数字信号处理技巧
DO口的处理看似简单,但直接读取会有抖动问题。我的解决方案是加状态检测和软件防抖:
#define DRY_THRESHOLD 800 // 自定义干燥阈值 uint8_t Check_SoilStatus(void) { static uint32_t last_change = 0; static uint8_t last_state = 0; // 硬件防抖:连续5次检测相同才认为有效 uint8_t stable_cnt = 0; for(uint8_t i=0; i<5; i++) { if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)) stable_cnt++; Delay_ms(10); } uint8_t current = (stable_cnt >= 3) ? 1 : 0; if(current != last_state) { if(HAL_GetTick() - last_change > 200) { // 200ms内状态不变才更新 last_state = current; last_change = HAL_GetTick(); } } return last_state; }这个方案比简单延时防抖更可靠,我在多个项目中验证过稳定性。阈值DRY_THRESHOLD需要根据实际土壤类型调整,建议先用ADC采集不同状态下的数值,记录几个关键点:
- 完全干燥时的读数(比如我的盆栽土干燥时ADC值约950)
- 浇水后的读数(同一盆土浇水后约200)
- 理想湿度时的读数(多数植物适宜在400-600之间)
5. 数据优化与滤波算法
原始ADC数据会有波动,分享几种实测有效的滤波方法:
移动平均法最简单实用:
#define FILTER_LEN 10 float filter_buf[FILTER_LEN]; float Moving_Average(float new_val) { static uint8_t index = 0; filter_buf[index++] = new_val; if(index >= FILTER_LEN) index = 0; float sum = 0; for(uint8_t i=0; i<FILTER_LEN; i++) { sum += filter_buf[i]; } return sum/FILTER_LEN; }中值滤波抗干扰更强:
float Median_Filter(float new_val) { static float buffer[5] = {0}; static uint8_t count = 0; buffer[count++] = new_val; if(count >= 5) count = 0; float temp[5]; memcpy(temp, buffer, sizeof(temp)); // 冒泡排序 for(uint8_t i=0; i<4; i++) { for(uint8_t j=i+1; j<5; j++) { if(temp[i] > temp[j]) { float swap = temp[i]; temp[i] = temp[j]; temp[j] = swap; } } } return temp[2]; // 取中值 }对于需要快速响应的场景,推荐一阶滞后滤波:
float FirstOrder_Filter(float new_val) { static float last = 0; last = 0.2*new_val + 0.8*last; // 系数可调 return last; }6. 实用功能扩展
基础功能实现后,可以增加这些实用特性:
阈值报警功能:
void Check_Humidity_Alert(float humidity) { if(humidity < 30.0f) { Buzzer_On(); // 触发蜂鸣器 LED_Blink(200); // LED快闪 } else if(humidity > 80.0f) { LED_Blink(1000); // LED慢闪 } else { Buzzer_Off(); LED_On(); // 正常状态常亮 } }自动浇水控制(需接继电器):
void Auto_Watering(float humidity) { static uint32_t last_water = 0; if(humidity < 40.0f && (HAL_GetTick()-last_water)>3600000) { Relay_On(); // 开启水泵 Delay_ms(5000); // 浇水5秒 Relay_Off(); last_water = HAL_GetTick(); } }数据记录功能(配合EEPROM):
typedef struct { float humidity[24]; // 24小时数据 uint8_t index; } Log_TypeDef; void Save_Humidity_Log(float val) { Log_TypeDef log; EE_ReadBytes(0, (uint8_t*)&log, sizeof(log)); log.humidity[log.index++] = val; if(log.index >= 24) log.index = 0; EE_WriteBytes(0, (uint8_t*)&log, sizeof(log)); }7. 常见问题排查
遇到过最诡异的问题是传感器读数始终为0,排查过程很有代表性:
- 首先检查硬件连接,发现VCC和GND接反了(教训:彩色杜邦线不一定可靠)
- 修正后读数固定在1023,测量AO引脚发现电压始终3.3V
- 更换传感器后正常,确认是传感器内部电路损坏
- 后来发现是焊接时没断电,静电击穿了传感器
其他典型问题及解决方案:
- 读数跳动大:尝试加大滤波系数,或在传感器电源端并联0.1μF电容
- 响应延迟:检查是否在循环中加了不必要延时,建议用定时器中断采样
- 数值不准:用万用表测量AO输出电压,对比ADC读数,校准分压电阻
- DO不变化:调节传感器上的蓝色电位器,用螺丝刀旋转直到指示灯状态变化
8. 低功耗优化方案
对于电池供电的场景,这几个技巧能大幅延长续航:
- 将STM32设为睡眠模式,用定时器唤醒(配置RTC唤醒间隔)
void Enter_StopMode(uint32_t sec) { RTC_SetAlarm(sec); // 设置唤醒时间 PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); SystemInit(); // 唤醒后需重新初始化时钟 }- 传感器供电改用GPIO控制,采样时才上电
#define SENSOR_PWR_PIN GPIO_Pin_1 void Sensor_Power(uint8_t state) { GPIO_WriteBit(GPIOA, SENSOR_PWR_PIN, (BitAction)state); if(state) Delay_ms(100); // 等待电源稳定 }- 降低ADC采样频率(调整ADC_SampleTime参数)
- 关闭调试接口和不用的外设时钟
实测下来,1分钟采集1次的情况下,800mAh的锂电池可以连续工作3个月以上。