STM32状态机编程实战——智能灌溉系统(上)

张开发
2026/4/13 7:01:53 15 分钟阅读

分享文章

STM32状态机编程实战——智能灌溉系统(上)
1. 智能灌溉系统与状态机编程的完美结合第一次接触智能灌溉系统是在三年前的一个农业科技展上当时看到一套进口设备要价好几万我就琢磨着能不能用STM32自己搞一套。经过几个月的折腾终于用状态机编程实现了基础功能成本不到五百块。状态机这种编程思想特别适合处理这种有明确状态转换的系统比如我们今天的智能灌溉场景。智能灌溉系统的核心是要根据环境条件自动控制水阀开关。想象一下你家的花园需要浇水但不同季节、不同天气情况下浇水的频率和量都不一样。传统定时器灌溉就像是个固执的老头不管下雨还是暴晒都按固定时间浇水而智能灌溉系统则像是个细心的园丁会根据土壤实际湿度来决定是否需要浇水。用STM32实现这样的系统状态机编程绝对是首选方案。它能把复杂的逻辑拆分成一个个明确的状态比如检测湿度、等待指令、开启灌溉、停止灌溉等。每个状态都有明确的进入条件、执行动作和退出条件这样写出来的代码既好理解又容易维护。2. 系统功能分析与硬件选型2.1 功能需求拆解我设计的这套智能灌溉系统主要实现这几个功能自动模式根据土壤湿度控制灌溉手动模式可以随时开关水泵支持定时灌溉任务还能根据环境温度调整灌溉策略。比如夏天高温时适当增加灌溉量雨季则减少灌溉频率。硬件方面需要这些组件STM32F103C8T6最小系统板性价比之王土壤湿度传感器电容式比电阻式更耐用DHT11温湿度传感器测量环境温湿度继电器模块控制水泵开关OLED显示屏显示系统状态按键模块模式切换和手动控制特别说下土壤湿度传感器的选型心得。最早我用的是电阻式传感器便宜是便宜但电极容易腐蚀用不了几个月就失灵了。后来换成了电容式的虽然贵点但使用寿命长很多测量也更稳定。2.2 硬件连接示意图主要硬件连接方式如下土壤湿度传感器 → STM32的ADC接口DHT11 → 普通GPIO口继电器 → GPIO口控制OLED → I2C接口按键 → GPIO输入这里有个坑要提醒大家继电器控制水泵时一定要加续流二极管否则容易烧单片机。我第一次做的时候就因为这个烧了两块板子都是血泪教训啊。3. 状态图设计与系统状态划分3.1 绘制状态转换图先给大家看看我画的状态转换图。整个系统有6个主要状态初始化状态SYSTEM_INIT待机状态STANDBY湿度检测状态CHECK_MOISTURE灌溉状态IRRIGATING暂停状态PAUSED故障状态ERROR状态之间的转换条件也很明确。比如从待机状态到湿度检测状态的转换条件是自动模式且到达检测间隔时间从灌溉状态到待机状态的转换条件是土壤湿度达到设定值或灌溉时间超时。3.2 状态枚举定义在代码中我是这样定义状态的typedef enum { SYSTEM_INIT, // 系统初始化 STANDBY, // 待机状态 CHECK_MOISTURE, // 检测湿度 IRRIGATING, // 灌溉中 PAUSED, // 暂停状态 ERROR // 故障状态 } SystemState;每个状态都有对应的处理函数返回下一个要进入的状态。这种结构让程序逻辑特别清晰后期加新功能也很方便。4. 核心状态机实现4.1 主状态机框架主状态机的骨架代码长这样SystemState currentState SYSTEM_INIT; void system_run_loop() { switch(currentState) { case SYSTEM_INIT: currentState handle_init_state(); break; case STANDBY: currentState handle_standby_state(); break; case CHECK_MOISTURE: currentState handle_check_moisture_state(); break; case IRRIGATING: currentState handle_irrigating_state(); break; case PAUSED: currentState handle_paused_state(); break; case ERROR: currentState handle_error_state(); break; } }这个循环放在主函数的while(1)里每隔100ms执行一次。为什么不直接用定时器中断因为状态机处理不需要那么高的实时性放在主循环里更简单。4.2 关键状态处理函数重点说说几个关键状态的处理。首先是湿度检测状态SystemState handle_check_moisture_state() { int moisture read_soil_moisture(); int temperature read_temperature(); if (moisture get_threshold(temperature)) { start_irrigation(); return IRRIGATING; } return STANDBY; }这里有个小技巧灌溉阈值不是固定值而是根据环境温度动态调整的。温度越高阈值设得越低这样天热时会更早开始灌溉。灌溉状态的处理也很关键SystemState handle_irrigating_state() { static uint32_t startTime 0; // 第一次进入灌溉状态 if (get_last_state() ! IRRIGATING) { startTime get_system_tick(); turn_on_pump(); } // 检查停止条件 if (check_stop_conditions()) { turn_off_pump(); return STANDBY; } // 检查暂停按键 if (is_pause_pressed()) { turn_off_pump(); return PAUSED; } return IRRIGATING; }这里用静态变量记录灌溉开始时间避免使用全局变量。停止条件包括湿度达标、超时或者手动停止。5. 多任务处理与事件驱动5.1 定时检测与看门狗智能灌溉不需要实时响应所以我用定时器设置了几种不同周期的事件每5分钟检测一次土壤湿度自动模式每1分钟检查一次温度用于调整阈值每10秒刷新一次OLED显示每500ms检查一次按键状态看门狗定时器也一定要用上防止程序跑飞。我有次没开看门狗系统死机后水泵一直开着差点把花园给淹了。5.2 事件队列实现为了处理各种异步事件按键、定时器、传感器数据我实现了个简单的事件队列typedef struct { EventType type; uint32_t timestamp; uint16_t data; } Event; #define MAX_EVENTS 10 Event eventQueue[MAX_EVENTS]; uint8_t eventHead 0; uint8_t eventTail 0; void push_event(EventType type, uint16_t data) { if ((eventHead 1) % MAX_EVENTS ! eventTail) { eventQueue[eventHead].type type; eventQueue[eventHead].data data; eventQueue[eventHead].timestamp get_system_tick(); eventHead (eventHead 1) % MAX_EVENTS; } } bool pop_event(Event *event) { if (eventHead eventTail) return false; *event eventQueue[eventTail]; eventTail (eventTail 1) % MAX_EVENTS; return true; }状态机在处理每个状态时都会先检查有没有新事件需要处理。这种方式比全局变量轮询要优雅得多。6. 传感器数据处理技巧6.1 土壤湿度传感器校准土壤湿度传感器的读数容易受土壤类型、温度等因素影响必须做好校准。我的做法是先把传感器完全干燥时和泡水时的ADC值记录下来在实际土壤中测试记录感觉干燥和感觉湿润时的值用这些关键点建立线性映射关系校准函数大概长这样int get_calibrated_moisture(int raw) { // 干燥时的ADC值 const int DRY_VALUE 2800; // 湿润时的ADC值 const int WET_VALUE 1200; // 映射到0-100%范围 int percent 100 - (raw - WET_VALUE) * 100 / (DRY_VALUE - WET_VALUE); // 限制在0-100范围内 return (percent 0) ? 0 : ((percent 100) ? 100 : percent); }6.2 数据滤波处理传感器数据常有噪声我用了移动平均滤波#define MOISTURE_READINGS 5 int moistureReadings[MOISTURE_READINGS]; uint8_t currentReading 0; int get_filtered_moisture() { // 添加新读数 moistureReadings[currentReading] read_soil_sensor(); currentReading (currentReading 1) % MOISTURE_READINGS; // 计算平均值 int sum 0; for (int i 0; i MOISTURE_READINGS; i) { sum moistureReadings[i]; } return sum / MOISTURE_READINGS; }这种滤波简单有效能消除大部分随机噪声。如果想更高级点可以试试卡尔曼滤波不过对STM32来说可能有点重了。7. 模式切换与用户交互7.1 自动/手动模式实现系统支持两种工作模式自动模式根据土壤湿度自动控制手动模式通过按键直接控制水泵模式切换通过长按按键实现状态机处理如下SystemState handle_standby_state() { if (is_mode_switch_pressed()) { toggle_auto_mode(); display_mode(); return STANDBY; } if (is_manual_control_pressed() !is_auto_mode()) { start_irrigation(); return IRRIGATING; } if (is_auto_mode() check_timer_expired()) { return CHECK_MOISTURE; } return STANDBY; }7.2 OLED状态显示OLED屏显示这些信息当前模式自动/手动土壤湿度百分比环境温度系统运行状态最后灌溉时间显示更新放在一个独立的状态中不影响主状态机运行SystemState handle_display_state() { refresh_display(); return get_previous_state(); }8. 电源管理与低功耗优化8.1 睡眠模式实现为了省电系统在没有操作时会进入低功耗模式。STM32提供了几种睡眠模式我用的STOP模式功耗可以降到几十微安。关键代码void enter_sleep_mode() { // 配置唤醒源比如外部中断 EXTI_InitTypeDef EXTI_InitStructure; EXTI_InitStructure.EXTI_Line WAKEUP_PIN; EXTI_InitStructure.EXTI_Mode EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Rising; EXTI_InitStructure.EXTI_LineCmd ENABLE; EXTI_Init(EXTI_InitStructure); // 进入STOP模式 PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); // 唤醒后重新初始化时钟 SystemInit(); }8.2 定时唤醒策略系统大部分时间都在睡觉通过RTC定时唤醒比如每小时唤醒一次检查湿度。在干旱季节可以增加唤醒频率雨季则可以减少。

更多文章