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.3VGND对GNDAO接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 i0; itimes; 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 i0; i5; 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 i0; iFILTER_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 i0; i4; i) { for(uint8_t ji1; j5; 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(); } }数据记录功能配合EEPROMtypedef 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个月以上。