简介:《基于STM32设计的智能灌溉控制系统》PDF文档面向嵌入式开发、物联网与智慧农业方向的课程设计及项目实践者,围绕传统灌溉水资源浪费、人工投入大和精准控制不足等问题,给出以STM32为主控的完整系统方案。内容涵盖电压、电流与累计用水量采集,自动、定时、周期和手动灌溉模式,以及中控室、短信、遥控和现场手动控制,并涉及温湿度限值设置、液晶中文菜单、后台大屏动态显示、异常报警、4G云平台与二维码/卡预付费等功能。文档还梳理了AD采集、继电器与PWM阀门控制、Flash存储、采集/控制/前端/后台四类程序及PWM电机控制核心代码示例,便于读者理解软硬件协同设计与参数配置思路。资源包为1个PDF文件,约1.29MB,章节组织清晰,适合按模块查阅和对照实践。目前已有721人学习下载。
1. 从水泵控制柜说起:这套基于STM32的智能灌溉系统到底解决什么问题
大棚里最常见的一幕是:几个手动球阀,一台 220V 抽水泵,浇不浇水全看人蹲不蹲点。水浇多了作物烂根,浇少了减产,月底电费单一来才发现泵空转了半天。这套基于 STM32 的智能灌溉控制系统,干的就是把这段凭经验的操作变成可量化、可远程、可收费的闭环:采集电压、电流、累计用水量,按自动、定时、周期、手动四种模式驱动继电器和阀门,用 4G 把数据送到云平台,再叠加二维码或刷卡预付费。
它适合三类人:做农业物联网课题、需要一套能跑通采集-控制-上云的完整链路;接手灌溉控制柜维护、想把老柜子改成可远程抄表;手上已有水表和泵,只缺一层计量与收费能力。硬件门槛不高,STM32F103ZET6 加几路 ADC、一个 4G 模组就能起步,难点在于现场干扰和模式调度,后面几章会逐个拆开。
2. 硬件架构:以 STM32F103ZET6 为核心的板级拆解
整套板子可以按「一主四从」理解:STM32F103ZET6 是主控,往下挂数据采集、执行控制、无线通信、预付费四个从属模块。主控选 F103ZET6 而不是更小的 F103C8T6,主要是引脚和存储宽裕——液晶并口、按键、继电器、PWM、4G 串口、读卡接口同时挂上去,C8T6 的 48 脚很快就不够分,而且 512KB Flash 能给参数备份和历史数据留出空间。
2.1 主控与外设的引脚分配思路
我在画这类板子时习惯先把外设按「实时性」分三档,再决定挂到哪个引脚上。电压电流采样和流量脉冲属于毫秒级实时任务,走 ADC1 的 PA0/PA1 和外部中断;PWM 阀门控制走 TIM2 的通道输出,频率定在 10kHz 左右,避开人耳可听范围也避开继电器动作的抖动频段;4G 模组和读卡器是秒级任务,直接挂 USART2、USART3,用中断加环形缓冲收数据,绝不在主循环里死等 AT 返回。
液晶用 FSMC 并口驱动 3.2 寸屏,中文菜单的字库放在外部 SPI Flash 里按需取模,避免占满内部 Flash。按键做 4 个独立键加编码器,现场戴手套也能操作。这一层分配定下来之后,后面所有软件模块的耦合度都会低很多,改一个模块不至于牵动全局。
| 模块 | 器件/接口 | 关键参数 | 备注 |
|---|---|---|---|
| 主控 | STM32F103ZET6 | 72MHz,512KB Flash,64KB RAM | 液晶并口 + 多串口同时使用 |
| 电压采样 | AC-AC 变压器 + 电阻分压 | 分压比约 10:1 | 送 ADC1_IN0 |
| 电流采样 | 霍尔电流传感器 | 灵敏度约 50mV/A | 送 ADC1_IN1 |
| 阀门驱动 | TIM2 PWM + 驱动板 | 10kHz,占空比 0~1000 | 控制阀门开合度 |
| 泵启停 | 继电器 | 220VAC 触点 | 强弱电必须隔离 |
| 流量计 | 霍尔脉冲水表 | 2 级精度,约 450 脉冲/升 | 外部中断计数 |
| 通信 | 4G 模组 | AT 指令 / MQTT | 上报 + 远程控制 |
| 预付费 | 二维码 / IC 卡 | 本地余额校验 | 云端扣费 |
2.2 采集与控制回路的电路取舍
电压采样不能直接把 220V 往分压电阻上怼,常见做法是先过一只小型 AC-AC 变压器降到 9V 左右,再用电阻分压把信号压到 ADC 的 0~3.3V 量程内,中间串一级运放做跟随,输出阻抗才压得下来。电流侧用霍尔传感器,输出是围绕 1.65V 上下摆动的交流信号,加一个偏置电路把它抬到正电压区间,ADC 才采得到完整波形。这两路都建议在运放后面加 RC 低通,截止频率压在 1kHz 附近,把水泵启停时的尖峰滤掉。
执行侧要记住一条血泪经验:继电器和 PWM 驱动的地必须和 MCU 的地分开走,只在电源入口单点连接。水泵是感性负载,断电瞬间的反电动势能轻松把 MCU 拉复位,现场表现就是「一开泵屏幕就花」。驱动板上加续流二极管和光耦隔离是最低要求,预算够的话,泵和阀门的 220V 回路与弱电板之间留 8mm 以上的爬电距离。
2.3 通信、预付费与供电三个边界模块
4G 模组选的是常见的 AT 指令模组,上电后先做驻网检测,再走 MQTT 连接云平台,串口波特率一般设 115200。这里有个容易被忽略的点:模组在搜网和注册时的瞬时电流能到 2A,电源必须单独给它一路 DC-DC,别和 MCU 共用一颗 LDO,否则会出现「4G 一联网,ADC 采样值整体漂移」的玄学现象。
预付费模块分两种落地方式:扫二维码的走云端订单回调,刷卡的在本地读卡号后向云端查余额。无论哪种,本地只做「余额是否大于零」的门槛判断,扣费动作全部放云端,避免设备掉线时余额算错。
供电是 220VAC 进,内部转 12V 给继电器和阀门驱动,再转 5V 和 3.3V。整机功耗不大,但要求掉电时能把当前累计水量和参数安全写进 Flash,所以电源上并一组大电容做掉电保持,配合低压检测中断,给写 Flash 争取几十毫秒。
3. 采集程序:电压、电流与累计水量的完整换算链路
采集程序要解决三个问题:AD 值怎么稳定读进来、读进来怎么换算成物理量、换算完怎么存住不丢。原始资料里给出的示例代码方向是对的,但配置上有一个会让新手卡一整天的坑——两个通道读到的是同一个值,第 5 章会细讲,这里先把正确的配置写出来。
3.1 ADC 配置:扫描模式 + DMA 才是正解
STM32F103 的 ADC1 规则组支持多通道扫描,配合 DMA 可以一次性把电压、电流两路结果搬进内存,主循环只管取数,不用关心什么时候启动转换。CubeMX 里把 ScanConvMode 打开、ContinuousConvMode 打开、NbrOfConversion 设为 2,再把 DMA 配成循环模式即可。
/* ADC1 双通道扫描 + DMA,PA0 采电压,PA1 采电流 */ static void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE; /* 多通道必须开扫描 */ hadc1.Init.ContinuousConvMode = ENABLE; /* 连续转换,交给 DMA 搬 */ hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 2; /* 规则组里放两个通道 */ if (HAL_ADC_Init(&hadc1) != HAL_OK) Error_Handler(); sConfig.Channel = ADC_CHANNEL_0; /* 电压通道 */ sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; /* 交流取样,采样时间要足 */ if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) Error_Handler(); sConfig.Channel = ADC_CHANNEL_1; /* 电流通道,换 rank */ sConfig.Rank = ADC_REGULAR_RANK_2; if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) Error_Handler(); } /* 上电后调用一次,之后 ad_buf[0]/ad_buf[1] 由 DMA 自动刷新 */ uint16_t ad_buf[2]; void App_AdcStart(void) { HAL_ADC_Start_DMA(&hadc1, (uint32_t *)ad_buf, 2); }这里几个参数值得单独说:采样时间选 55.5 个周期,是因为 50Hz 交流信号经过变压器和运放后有相位偏移,采样窗口太短会把峰值削掉;DMA 缓冲区长度写 2,对应两个 rank,顺序和 Rank 配置必须一一对应,写反了电压电流就对调;ContinuousConvMode 打开之后就不需要反复调用 HAL_ADC_Start,DMA 循环搬运,CPU 占用几乎为零。原文示例里用轮询方式一个通道一个通道读,在连续转换模式下 PollForConversion 拿到的永远是第一个通道的值,这是最典型的翻车点。
3.2 AD 值到物理量的换算与标定
ADC 读到的只是 0~4095 的码值,要变成电压电流,中间要过两道系数。电压侧是变压器变比乘以分压比,再乘 1.414 做峰值到有效值的折算;电流侧是传感器灵敏度,50mV/A 意味着每安培对应 0.05V,换算成码值就是每安培约 62 个 LSB。
#define ADC_VREF 3.3f #define ADC_FULL_SCALE 4095.0f /* 标定系数,出厂时用标准表校一次写进 Flash,现场可微调 */ typedef struct { float k_volt; /* 变压器变比 × 分压比 × 峰值系数 */ float k_curr; /* 1 / 传感器灵敏度(A/V),50mV/A 时为 20 */ float k_flow; /* 每升脉冲数,按水表铭牌标定 */ float pf; /* 功率因数,水泵类负载一般 0.8~0.9 */ } Calib_t; static Calib_t g_cal = { 10.0f * 1.414f, 20.0f, 450.0f, 0.85f }; static float g_voltage, g_current, g_power, g_energy_wh; /* 100ms 周期调用一次,由定时器任务调度 */ void App_CalcElectrical(void) { float v_adc = ad_buf[0] * ADC_VREF / ADC_FULL_SCALE; /* 0~3.3V */ float i_adc = ad_buf[1] * ADC_VREF / ADC_FULL_SCALE; g_voltage = v_adc * g_cal.k_volt; /* 有效值 V */ g_current = i_adc * g_cal.k_curr; /* 有效值 A */ g_power = g_voltage * g_current * g_cal.pf; /* 有功功率 W */ /* 100ms 折算成 Wh,注意是除以 3600 而不是 3600000 */ g_energy_wh += g_power * 0.1f / 3600.0f; }标定这件事不要偷懒。同一批板子,分压电阻有 1% 的误差,霍尔传感器的一致性更差,直接把理论系数写死,十台设备能有三个不同读数。我一般会做两个标定点:空载时读一次零点,接标准负载再读一次满度,两点定一条直线,把斜率和截距写进参数区,之后每台板子单独校准。校准值跟着设备走,不要跟着固件走。
3.3 流量脉冲计数与参数掉电保存
流量计输出的是脉冲,水表铭牌上写着每升多少脉冲,常见 DN15 是 450 左右。计数放在外部中断里,回调里只做自增,不做任何浮点运算,否则水泵一转起来中断频率上来,主循环会被拖慢。
volatile uint32_t g_flow_pulse; /* 流量计脉冲中断:只自增,越轻越好 */ void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == FLOW_PULSE_Pin) { g_flow_pulse++; } } float Flow_GetLiter(void) { return (float)g_flow_pulse / g_cal.k_flow; } /* 结算或上报后清零,关中断取快照,避免清掉刚到的脉冲 */ uint32_t Flow_TakeSnapshot(void) { uint32_t snap; __disable_irq(); snap = g_flow_pulse; g_flow_pulse = 0; __enable_irq(); return snap; }累计水量和灌溉参数要能掉电保住,F103 没有真正的 EEPROM,只能用内部 Flash 划一页出来模拟。做法是定一个带头魔数和 CRC 的结构体,双页交替写,写完一页再擦另一页,掉电时至少有一份完整数据。
#define PARAM_MAGIC 0x49525247u /* 'IRRG' */ typedef struct { uint32_t magic; uint8_t mode; /* 0 手动 1 自动 2 定时 3 周期 */ uint16_t soil_lo, soil_hi; /* 湿度下限/上限,单位 0.1% */ uint16_t t_start, t_stop; /* 定时灌溉起止,单位分钟 */ uint16_t spray_min; /* 单次喷灌时长,分钟 */ uint16_t cycle_hour; /* 周期灌溉间隔,小时 */ float k_volt, k_curr, k_flow, pf; uint32_t total_liter; /* 累计水量,升 */ uint32_t crc32; } SysParam_t; /* 写入前先算 CRC,再擦除目标页,最后整块写回 */ bool Param_Save(const SysParam_t *p) { SysParam_t tmp = *p; tmp.magic = PARAM_MAGIC; tmp.crc32 = Crc32_Calc(&tmp, sizeof(tmp) - 4); return Flash_WritePage(PARAM_PAGE_A, &tmp, sizeof(tmp)); }这里的关键是 CRC 不能省。Flash 擦写过程中如果突然掉电,写进去的就是半截数据,靠 magic 判断不出来,只有 CRC 对不上才能识别并回退到备份页。另外,写 Flash 之前必须把中断关掉,擦一页在 F103 上要 20ms 左右,这期间进来的流量脉冲如果不关中断就会被中断服务程序打断,导致写时序错乱,现场表现是「偶尔参数变乱码」。
4. 控制程序:PWM 阀门调节与四种灌溉模式的状态机
控制这块是整套系统最容易写成一锅粥的地方。四种灌溉模式、四种控制方式、还有告警联锁,如果全用 if-else 堆在主循环里,改一个需求要动五处。我的做法是把所有触发条件收敛成一个状态机,模式只决定「谁能让状态机从 IDLE 进入 RUN」,执行动作和退出条件全部共用。
4.1 PWM 输出封装与阀门开合度映射
阀门开合度用 PWM 占空比表示,不直接对应电机转速,但控制逻辑一样:给定 0~1000 的千分比,换算成比较寄存器的值。TIM2 挂在 APB1 上,时钟 72MHz,预分频 71 得到 1MHz 计数频率,自动重装值 99 对应 10kHz,占空比分辨率正好 100 级,够用又不占 CPU。
/* 初始化指定通道的 PWM 输出,默认占空比 0 */ void PWM_Init(TIM_HandleTypeDef *htim, uint32_t channel) { TIM_OC_InitTypeDef sConfigOC = {0}; sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 0; sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim, &sConfigOC, channel); HAL_TIM_PWM_Start(htim, channel); } /* permille:0~1000,1000 表示阀门全开 */ void Valve_SetOpen(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t permille) { uint32_t arr = htim->Init.Period; /* 自动重装值,10kHz 时为 99 */ uint32_t compare; if (permille > 1000) permille = 1000; /* 参数兜底,防止越界 */ compare = (arr + 1) * permille / 1000; /* 千分比换算成比较值 */ __HAL_TIM_SET_COMPARE(htim, channel, compare); }参数说明里有两个细节:一是用(arr + 1)而不是arr,因为计数器从 0 数到 ARR 一共 ARR+1 个 tick,不补这个 1,占空比永远差一档,全开时到不了 100%;二是 permille 的边界判断必须放在函数内部,别指望所有调用方都传对,现场参数是从液晶菜单和云端两边都能改的,脏数据迟早会进来。
4.2 四种灌溉模式的状态机实现
状态机只保留五个有效状态:空闲、手动运行、自动运行、定时运行、周期运行,外加一个故障态。模式参数只影响 IDLE 的跳转条件,运行中的超时、告警、停止逻辑完全共用,代码量能砍掉一半。
typedef enum { IRR_IDLE = 0, IRR_RUN_MANUAL, IRR_RUN_AUTO, IRR_RUN_TIMER, IRR_RUN_CYCLE, IRR_FAULT } IrrState_t; static IrrState_t s_state = IRR_IDLE; static uint32_t s_run_100ms; /* 本次灌溉已运行时长,100ms 计一次 */ /* 100ms 周期调度,所有模式共用同一套进入/退出逻辑 */ void Irr_Task_100ms(void) { uint16_t now_min = Rtc_GetMinutes(); /* 0~1439,当天分钟数 */ switch (s_state) { case IRR_IDLE: if (g_cmd.manual_on) { s_state = IRR_RUN_MANUAL; } else if (g_param.mode == 1 && Soil_NeedWater()) { s_state = IRR_RUN_AUTO; /* 湿度低于下限自动开 */ } else if (g_param.mode == 2 && now_min == g_param.t_start) { s_state = IRR_RUN_TIMER; } else if (g_param.mode == 3 && now_min % (g_param.cycle_hour * 60) == 0) { s_state = IRR_RUN_CYCLE; } break; case IRR_RUN_MANUAL: case IRR_RUN_AUTO: case IRR_RUN_TIMER: case IRR_RUN_CYCLE: Pump_On(); Valve_SetOpen(&htim2, TIM_CHANNEL_1, g_cmd.valve_permille); s_run_100ms++; /* 三种退出条件:用户停、到时、触发保护 */ if (g_cmd.manual_off || s_run_100ms / 10 >= Irr_GetLimitSec()) { Irr_Stop(); } break; case IRR_FAULT: Pump_Off(); Valve_SetOpen(&htim2, TIM_CHANNEL_1, 0); break; } } void Irr_Stop(void) { Valve_SetOpen(&htim2, TIM_CHANNEL_1, 0); /* 先关阀,再停泵,减少水锤 */ Pump_Off(); s_state = IRR_IDLE; s_run_100ms = 0; }这段代码里有几个工程上的取舍值得说清。定时和周期模式判断用的是「等于当前分钟」,意味着 100ms 任务必须在一分钟内至少跑到一次,正常调度不会漏,但如果某次 Flash 写入阻塞超过 1 分钟,这一轮定时就错过了,所以更稳的做法是记「上次触发时间戳」再做差,而不是判等。自动模式的湿度判断要加回差,下限 30%、上限 45%,否则湿度在阈值附近抖动会让泵频繁启停,继电器寿命扛不住。
4.3 继电器互锁、超时保护与告警联动
泵和阀门之间要有硬互锁,软件里也要有软互锁:阀门没开到位不允许启泵,泵没停不允许关阀到零位。超时保护是最后一道防线,单次灌溉超过 4 小时强制停机,这个时长写在参数区里可以改,但改不到超过硬编码的上限。
#define IRR_HARD_LIMIT_SEC 14400 /* 硬上限 4 小时,参数改不过它 */ /* 安全巡检,200ms 一次,独立于状态机 */ void Safety_Check(void) { if (g_current > g_param.i_limit) { Irr_Abort(FAULT_OVERCURRENT); /* 卡阀、堵转、泵抱死 */ } else if (g_voltage < 180.0f || g_voltage > 250.0f) { Irr_Abort(FAULT_OVERVOLT); /* 电网异常 */ } else if (Pump_IsOn() && Flow_NoPulse(30)) { Irr_Abort(FAULT_NOFLOW); /* 泵在转但没水,30 秒无脉冲 */ } } void Irr_Abort(uint8_t fault_code) { Irr_Stop(); s_state = IRR_FAULT; Buzzer_On(3); /* 现场蜂鸣三声 */ Net_PushAlarm(fault_code); /* 同时上云 */ }三个告警判据对应三类真实现场故障:过流通常是阀门卡死或叶轮缠了杂物,欠压过压多半是农网末端电压不稳,无流量的情况最隐蔽——泵在转、电流也正常,但管路漏了或者阀门没真正打开。最后这条只能靠流量脉冲和运行状态做交叉判断,单看电流是查不出来的。
4.4 上云报文与预付费门槛校验
4G 上报走 MQTT,报文用最省的 JSON,字段名越短越好,流量是按条算钱的。上报周期正常 30 秒一次,告警立刻上报,远程控制下行走订阅主题。
static void Net_Report(void) { char json[192]; snprintf(json, sizeof(json), "{\"sn\":\"%s\",\"v\":%.1f,\"i\":%.2f,\"p\":%.0f,\"q\":%.2f,\"st\":%d}", DEV_SN, g_voltage, g_current, g_power, Flow_GetLiter(), (int)s_state); At_Send("AT+MQTTPUB=0,\"irr/data\",\"%s\",0,0\r\n", json); } /* 预付费只做门槛判断,扣费在云端,避免离线时把余额算错 */ bool Prepay_AllowStart(void) { if (g_card_status != CARD_OK) return false; /* 未刷卡/扫码 */ if (g_balance_yuan <= 0.0f) return false; /* 余额不足 */ return true; }Prepay_AllowStart这个函数要在状态机进入 RUN 之前调一次,而且不能在运行中反复调,否则网络抖动导致余额查询失败时会把正在进行的灌溉打断。合理做法是开泵前校验一次,运行中按 5 分钟周期向云端同步用量,余额扣到零后由云端下发停止指令,本地只做执行。
5. 避坑与排查:现场调试最容易翻车的五件事
这套系统在实验室里跑通和在现场跑通是两回事。下面五条是我在类似项目里反复遇到过的,每条按现象、原因、解决写,遇到问题可以先照着对一遍。
5.1 采样与驱动类问题
现象一:电压电流两个通道读出来一模一样,改一路另一路跟着变。
原因基本可以锁定在 ADC 规则组配置上。原文示例里两次调用HAL_ADC_ConfigChannel时,Rank都设成了ADC_REGULAR_RANK_1,两个通道挤在同一个 rank;同时ScanConvMode是 DISABLE,NbrOfConversion是 1,ADC 压根不知道还有第二个通道。再加上连续转换模式下用轮询读数,拿到的永远是规则组第一个通道的结果。解决办法就是 3.1 节那套:开扫描、NbrOfConversion = 2、两个通道分别配 RANK_1 和 RANK_2,用 DMA 搬运,别用轮询。
现象二:PWM 输出测不到波形,或者阀门一直在抖。
先量 GPIO 有没有切到复用推挽,F103 的 TIM 通道引脚默认是普通 IO,CubeMX 里不勾 Alternate Function 就永远输出不了波形。如果波形有但阀门抖,看自动重装值和预分频是不是配成了低频,几赫兹的 PWM 驱动电磁阀会产生明显的嗡嗡声和抖动,10kHz 附近才正常。还有一种情况是驱动板和 MCU 共地没做好,示波器上波形干净,一接负载就塌,这是地弹,必须在电源入口单点接地并加大电容。
现象三:累计水量比实际少一到两成。
先怀疑脉冲丢失。水表输出的脉冲宽度在微秒级,外部中断里如果做了浮点运算或者调了打印,中断服务时间拉长,后面来的脉冲就被吞掉。另一个常见原因是上拉电阻没加,霍尔输出是开漏,悬空时电平不确定。改进办法是把外部中断优先级提到比串口高,回调里只自增,再不行就改用定时器的编码器模式或者输入捕获加硬件计数,让 CPU 完全不参与。
5.2 存储、通信与环境类问题
现象四:设备运行几天后参数变成乱码,灌溉时间对不上。
Flash 模拟 EEPROM 的经典坑。一是写之前没关中断,擦除期间被中断打断,写时序错乱;二是没有 CRC,只靠 magic 判断,半截数据也能通过校验;三是擦除地址没按页对齐,F103 的页是 1KB 或 2KB,跨页写会静默失败。解决就是双页备份加 CRC32,写入前__disable_irq(),写完再开,上电读取时 CRC 不对就回退到另一页,两页都不对才恢复出厂参数。
现象五:4G 模组一联网,采样值就开始漂,屏幕偶尔卡住。
两个原因叠在一起。电源上,4G 模组搜网时瞬时电流能到 2A,和 MCU 共用 LDO 会把 3.3V 拉塌,ADC 参考电压跟着掉,所有读数整体偏低。软件上,AT 指令如果用阻塞方式等回复,一次等 5 秒,主循环直接停摆,看门狗都能给你复位了。做法是模组单独供电,AT 收发改成状态机加环形缓冲,用串口空闲中断收整帧,主循环只处理完整报文,绝不while等。
提示:现场调试时先把 4G 模组拔掉,只跑采集和控制,确认本地链路稳定后再插回去。很多「系统不稳定」的锅最后都能追到电源和串口阻塞上。
6. 二次开发:从液晶菜单到 Modbus 从站的改造路线
接手这套代码之后最值得做的一件事,是把私有协议换成 Modbus RTU 从站。现在的液晶菜单和 4G 报文都是自定义格式,换个屏、换个平台就得改一遍协议层;改成 Modbus 之后,寄存器表一贴出去,组态屏、PLC、上位机全都能直接接,二次开发的成本立刻降一个量级。
改造思路不复杂:把参数区结构体映射成保持寄存器,40001 开始放模式、湿度上下限、灌溉时长,40101 开始放实时电压电流功率和累计水量,线圈区放泵启停和阀门开度。串口用 USART2,波特率 9600、8N1,从站地址从拨码开关读。核心是把 3.3 节那个SysParam_t和寄存器表做双向映射,读写都走同一套 CRC 校验。
| 寄存器地址 | 内容 | 读写 | 单位 |
|---|---|---|---|
| 40001 | 灌溉模式 | R/W | 0~3 |
| 40002 | 湿度下限 | R/W | 0.1% |
| 40003 | 湿度上限 | R/W | 0.1% |
| 40004 | 单次喷灌时长 | R/W | 分钟 |
| 40101 | 当前电压 | R | 0.1V |
| 40102 | 当前电流 | R | 0.01A |
| 40103 | 累计水量 | R | 升 |
验证方法也简单:先用 Modbus Poll 读 40101 到 40103,对着万用表和流量计核一遍数,再写 40001 切模式看泵的动作。硬件在环测试时,我习惯用信号发生器模拟流量脉冲、用调压器模拟电压跌落,把 4.3 节那三个告警逐条触发一遍,确认蜂鸣和上报都正常。
另一条改造方向是灌溉策略。现在是湿度阈值加定时,属于规则控制,作物根系层的水分变化其实是非线性的,换成基于土壤含水率的 PID 或者简单的预测补偿,能再省一截水。不管走哪条路,改完都要保证安全巡检那几个判据还在,别为了策略灵活把过流和无流量保护绕过去。
从那以后我每次改这类控制程序,都强制先把寄存器表和状态机画出来再动手写代码,改完必定用信号源把每一个告警触发一遍——现场没人替你兜底,能提前发现问题的只有自己那套测试流程。希望帮到你。
本文还有配套的精品资源,点击获取