3. 灌溉控制器定时启停逻辑实现
定时灌溉控制器的核心结构
定时灌溉控制器要解决的核心问题是:在什么时刻、把哪一路电磁阀打开多久,以及在什么情况下必须允许人手动接管。它不依赖土壤湿度反馈,而是以实时时钟(RTC)为时间基准,把“时段表”翻译成继电器动作。因此实现重点落在三件事上:时段数据的组织方式、RTC 时间与时段表的比较逻辑、以及手动覆盖与自动计划之间的优先级仲裁。
下面这张图给出控制器内部的数据与信号关系:RTC 提供当前时间,时段表提供计划,比较器决定是否进入灌溉,继电器驱动电磁阀,而手动按键通过覆盖仲裁层直接干预输出。
时段表的数据结构与比较逻辑
时段表用固定长度数组存储,每条记录包含起始时、起始分、持续分钟数、星期掩码和使能位。比较逻辑的关键是把“当前时刻”和“时段区间”都换算成当天的分钟数,这样跨零点的时段也能用同一套公式处理。
typedef struct {
uint8_t start_h; // 0-23
uint8_t start_m; // 0-59
uint16_t duration_min;// 1-240
uint8_t weekday_mask; // bit0=周一 ... bit6=周日
uint8_t enabled; // 0/1
} IrrigationSlot;
#define SLOT_COUNT 4
static IrrigationSlot slots[SLOT_COUNT] = {
{ 6, 30, 20, 0x7F, 1 }, // 每天 06:30 灌 20 分钟
{ 18, 0, 15, 0x1F, 1 }, // 周一至周五 18:00 灌 15 分钟
{ 12, 0, 10, 0x60, 0 }, // 周末 12:00,默认关闭
{ 22, 45, 30, 0x7F, 1 } // 每天 22:45 灌 30 分钟
};
判断某时段当前是否应处于灌溉状态,用分钟数比较即可。注意跨零点时段(如 23:50 开始、持续 30 分钟)需要把结束分钟加上 1440 再取模:
static uint16_t now_minutes(const RtcTime *t) {
return (uint16_t)t->hour * 60 + t->minute;
}
static uint8_t slot_active(const IrrigationSlot *s, const RtcTime *t) {
if (!s->enabled) return 0;
if (!(s->weekday_mask & (1u << t->weekday))) return 0;
uint16_t now = now_minutes(t);
uint16_t start = (uint16_t)s->start_h * 60 + s->start_m;
uint16_t end = start + s->duration_min;
if (end <= 1440) {
return (now >= start && now < end);
} else {
// 跨零点:区间为 [start,1440) ∪ [0,end-1440)
return (now >= start || now < (end - 1440));
}
}
主循环里遍历所有时段,只要任意一条处于激活状态,就产生自动灌溉请求。这里用“或”而不是“取第一条”,可以避免时段重叠时漏灌。
继电器驱动与手动覆盖
继电器由 GPIO 经三极管驱动,电磁阀线圈两端必须并联续流二极管,否则关断瞬间的反电动势会击穿三极管。驱动函数只负责把“最终决策”落到引脚上,不参与时间判断:
#define VALVE_PIN 5
static void valve_write(uint8_t on) {
digitalWrite(VALVE_PIN, on ? HIGH : LOW);
}
// 自动请求:任一时段激活即为 1
static uint8_t auto_request(const RtcTime *t) {
for (uint8_t i = 0; i < SLOT_COUNT; i++) {
if (slot_active(&slots[i], t)) return 1;
}
return 0;
}
手动覆盖采用“覆盖状态 + 超时释放”的方式:短按切换手动开/关,手动状态一旦生效就屏蔽自动请求;为避免忘记关闭导致长时间漫灌,手动开启后设定一个最长保持时间(例如 30 分钟),到时自动回到自动模式。长按则立即强制关闭并清除手动状态。
typedef enum { MODE_AUTO, MODE_MANUAL_ON, MODE_MANUAL_OFF } CtrlMode;
static CtrlMode mode = MODE_AUTO;
static uint32_t manual_start_ms = 0;
#define MANUAL_MAX_MS (30UL * 60UL * 1000UL)
static void on_button_short_press(void) {
if (mode == MODE_MANUAL_ON) {
mode = MODE_MANUAL_OFF;
} else {
mode = MODE_MANUAL_ON;
manual_start_ms = millis();
}
}
static void on_button_long_press(void) {
mode = MODE_MANUAL_OFF; // 强制关,且不自动恢复
}
static uint8_t final_valve_state(const RtcTime *t) {
if (mode == MODE_MANUAL_ON) {
if (millis() - manual_start_ms >= MANUAL_MAX_MS) {
mode = MODE_AUTO; // 超时释放
} else {
return 1;
}
}
if (mode == MODE_MANUAL_OFF) return 0;
return auto_request(t);
}
主循环只需读取 RTC、计算最终状态并写引脚。RTC 读取建议每秒一次,避免频繁 I²C 访问;继电器状态变化时才写 GPIO,减少无谓翻转。
void loop(void) {
static uint32_t last = 0;
if (millis() - last < 1000) return;
last = millis();
RtcTime now;
rtc_read(&now); // 填充 hour/minute/weekday
uint8_t state = final_valve_state(&now);
valve_write(state);
}
验证与判读
把 RTC 临时改为可手动设置的时间,逐项验证以下场景,观察继电器指示灯与电磁阀动作是否与预期一致:
| 测试场景 | 设置 | 预期现象 |
|---|---|---|
| 正常时段内 | 时间设为 06:35,时段 1 使能 | 继电器吸合,阀开 |
| 时段外 | 时间设为 06:55(已超 20 分钟) | 继电器释放,阀关 |
| 星期不匹配 | 周六,时段 2(周一至周五) | 不动作 |
| 跨零点时段 | 23:55,时段 4 持续 30 分钟 | 阀开,至 00:15 关闭 |
| 手动开覆盖 | 时段外短按 | 阀开,自动计划被屏蔽 |
| 手动超时 | 手动开后等待 30 分钟 | 自动回到 AUTO,按计划执行 |
| 长按强制关 | 时段内长按 | 阀立即关,且不再自动恢复 |