简介:这份PDF面向农业物联网方向的学习者与嵌入式开发者,围绕基于STM32微控制器的智能灌溉控制系统展开完整设计讲解,可用于课程设计、毕业设计选题参考或智慧农业项目的技术方案借鉴。资源包共1个文件,为约1.29MB的PDF文档,以图文与代码结合的形式呈现,篇幅紧凑但不失完整性。目前已有721人学习下载,具备一定的参考热度。内容从系统总体架构入手,逐层拆解硬件与软件实现:硬件部分以STM32为主控,涵盖AD采集电压、电流与流量、继电器启停控制、PWM调节阀门开度、4G上云及二维码预付费模块;软件部分拆分为采集、控制、前端与后台四类程序,并给出灌溉模式选择、参数设置、异常报警与液晶中文菜单显示等具体做法。文中还附有PWM调速等核心代码片段与STM32CubeMX配置思路,便于读者对照复现实验、理解数据采集到执行控制的完整链路,也可作为后续功能扩展与系统优化的参考。
1. 需求先立住:一套灌溉控制器要先答对的四个硬指标
做基于 STM32 的智能灌溉控制系统,最容易翻车的不是代码,而是需求一开始就没定死。我见过不少毕设和课设方案,开篇就往上堆传感器和云平台,做到中期发现计量精度、预付费结算、远程控制三件事互相打架,只能推倒重来。这套方案要解决的问题很具体:把灌溉区的水量、电量、电压电流和运行状态采上来,按自动、定时、周期、手动四种模式把水泵和阀门控制住,用 4G 把数据送到云端,再靠二维码或刷卡把水费算清楚。它适合两类人——一类是要交完整设计方案的学生,需要一套能画图、能写代码、能测数据的闭环;另一类是接了小面积灌溉改造的工程师,想找一版可直接改的参考实现。后面的章节按需求、硬件、软件、代码、避坑、验收的顺序推下去,每一段都给能照着做的参数和判据。
2. 硬件方案与模块划分:以 STM32F103ZET6 为中心的六个功能块
这套系统的硬件边界画得很清楚:主控、采集、执行、通信、计量付费、人机交互,六块。把它拆成这六块不是为了好看,是因为后面每一块都对应一组独立的调试手段——采集看标定、执行看波形、通信看日志、付费看对账。板上所有模块最终都挂到一颗 STM32F103ZET6 上,选它的理由和资源怎么分配,是这一章要先说清的事。
2.1 主控选型与资源预算:为什么是 F103ZET6
F103ZET6 是 Cortex-M3 内核,主频 72MHz,512KB Flash、64KB SRAM,片上 3 个 12 位 ADC、4 个通用定时器加 2 个高级定时器,还带 FSMC 总线。对这套系统来说,最关键的是三点:ADC 通道够多,能同时接电压、电流和备用通道;定时器够多,PWM、采样触发、脉冲计数可以各占一个互不打架;FSMC 能直接挂并口液晶,中文菜单刷新不掉帧。换成引脚更少的型号,光是把这些外设分配到不冲突的引脚上就够折腾半天。
资源分配我一般按下面这张表定死,画原理图和写初始化代码都以此为准,避免后期改一处牵动全身。
| 片上资源 | 用途 | 关键配置 |
|---|---|---|
| TIM2_CH1 | 阀门比例 PWM | 72MHz / 7200 / 10000,输出 1kHz |
| TIM3 | 采样定时触发 | 10kHz 更新事件,触发 ADC |
| TIM4 | 流量计脉冲计数 | 外部时钟模式 2,接流量计脉冲脚 |
| ADC1_IN0 | 电压采样 | 12 位,1.65V 偏置 |
| ADC1_IN1 | 电流采样 | 12 位,霍尔零点 2.5V |
| USART1 | 4G 模块 | 115200,AT + MQTT |
| USART2 | 调试串口 | 115200,日志与标定输出 |
| USART3 | 短信/遥控接收 | 9600 |
| FSMC | 并口液晶 | 16 位,8080 时序 |
| RTC | 定时/周期灌溉基准 | 外挂 32.768kHz 晶振 |
供电这块别图省事。整机输入 220VAC,先用隔离型开关电源降到 5V,再用 LDO 单独给 MCU 和 ADC 参考供电,继电器和 4G 模块走另一路 5V。模拟地和数字地单点连接,ADC 参考脚旁边并 100nF 加 10μF。现场电机启停时的干扰是玄学级的,电源不隔离,后面采样数据怎么滤波都白搭。
注意:4G 模块发射瞬间电流能冲到 2A 左右,电源余量按峰值留,别按平均值算,否则联网瞬间单片机必复位。
2.2 采集链路:电压、电流、流量三路信号怎么进 ADC
电压采样不能直接电阻分压取 220V,安全上过不去,必须先用 AC-AC 变压器降到 9V 左右再做分压。变压器副边输出是双极性交流,而 STM32 的 ADC 只能测 0~3.3V,所以要在分压网络后加一个 1.65V 的直流偏置,把交流信号整体抬到量程中点。这样 ADC 读到的 2048 左右对应 0V,高于 2048 是正半周,低于 2048 是负半周。
电流采样用霍尔电流传感器,输出零点在 2.5V,量程按 50A 选,对应 0~5V 输出,进 ADC 前用电阻分压到 0~3.3V 范围。霍尔的好处是天然隔离,一次侧和二次侧不共地,水泵这种感性负载的浪涌打不到主控上。
流量计量走脉冲流量计,输出的是与流量成正比的方波,接 TIM4 的外部计数脚。这类流量计出厂会给一个 K 系数,单位是脉冲数每升,常见在 400~500 之间。累计水量按脉冲总数除以 K 得到,瞬时流量则用固定窗口内的脉冲数换算。系统标称的流量计量误差 2 级(即 ±2%),最终能不能达标,取决于 K 系数标得准不准,以及低流量时有没有丢脉冲。
三路的换算关系先在纸上定清楚,写代码时直接照抄:
- 电压瞬时值:
v = (adc - 2048) × VOLT_K,VOLT_K由标准表两点标定得出,理论值约 0.152 V/码 - 电流瞬时值:
i = (adc - 2048) × CURR_K,50A 量程下理论值约 0.0244 A/码 - 有功功率:
P = (1/N) × Σ(v × i),必须用瞬时值逐点相乘再平均,不能拿有效值直接相乘 - 累计水量:
Q = pulse_total / K,单位升 - 累计电量:
E = Σ(P × Δt),Δt取采样窗口长度
第三条是很多人栽跟头的地方。水泵是感性负载,功率因数不到 1,用电压有效值乘电流有效值算出来的"功率"会明显偏大,电能累计跟着全错。逐点相乘再取平均得到的才是有功功率,这是计量准确的前提。
2.3 执行链路:继电器管启停,PWM 管开度
执行部分有两只手:继电器负责水泵的启停,PWM 负责阀门开度。两者分工不能混,继电器是开关量,只有通断;比例阀要的是连续调节,靠 PWM 占空比映射成 0~10V 或 4~20mA 的模拟量。
继电器这一级的典型接法是:MCU 引脚 → 光耦隔离 → 三极管驱动 → 继电器线圈 → 继电器触点带交流接触器。线圈两端必须反并一只续流二极管,触点上并 RC 吸收网络。水泵是典型感性负载,断电瞬间的反电动势能轻松把没有隔离的板子打到复位。
PWM 转模拟量用两级 RC 低通加一级运放跟随。PWM 频率取 1kHz,RC 截止频率设在 150Hz 左右,取 R=10kΩ、C=0.1μF,纹波能压到毫伏级。占空比不能从 0 直接用到 100%,比例阀机械上有死区和饱和区,实测下来 10%~90% 才是有效调节段,映射关系要在代码里做死区补偿,否则用户设 30% 开度和设 35% 开度出来的流量可能一模一样。
2.4 通信与预付费:4G 上云和刷卡扫码的边界
通信模块用 4G 透传,串口发 AT 指令,走 MQTT 协议连云平台。上行报数据,下行收控制命令和参数。这里有个设计取舍值得说清楚:不是所有数据都要实时上云,电压电流这类高频量在本地做 1 分钟聚合再上报,流量、余额、运行状态这类关键量变化即报。全量直传会把流量费烧穿,而且云端也存不住。
预付费是这套系统里最容易出纠纷的模块。二维码和刷卡两条路要分开处理:二维码由云平台生成,用户扫码支付后云端下发充值指令,本地收到后更新余额;实体卡走本地读卡器,离线也能判断余额够不够。核心原则是本地账本为准,云端账本做对账。断网时允许一定额度的离线使用,比如 20 元,联网后自动补扣;不允许完全依赖云端判断,否则网一断整套系统就瘫了。
提示:每条充值、扣费记录都带一个全局唯一流水号,云端重复下发同一条指令时本地按流水号去重,这是防重复扣费的唯一可靠办法。
3. 软件架构设计:四个程序怎么分工、数据怎么走
硬件定了之后,软件要是还写成一个大while(1)里从头串到尾,后面加一个模式就得改十处。这套系统的软件按采集、控制、前端、后台四块拆,底层用裸机加中断的前后台结构,主循环按固定节拍跑任务调度,中断只做最短的搬运。这样拆的好处是每块都能单独测:采集程序脱开控制能跑,控制程序喂假数据也能跑,前端和后台各自对着串口调。
3.1 任务调度:把主循环切成六个固定节拍
裸机上最容易犯的错是到处HAL_Delay,一个地方延时 1 秒,整个系统的响应就废了。正确做法是开一个 1ms 的 SysTick,主循环里维护一组软件计时器,每个任务按自己的周期判断该不该执行。
| 任务 | 周期 | 职责 |
|---|---|---|
| 采样任务 | 20ms | 采满一个工频周期的 200 点,算有效值和有功功率 |
| 控制任务 | 100ms | 推进灌溉状态机,处理阀门和继电器动作 |
| 计量任务 | 1s | 累计水量电量,扣减余额,判断余额阈值 |
| 上报任务 | 10s | 组包经 4G 上云,失败入缓存队列 |
| 显示任务 | 200ms | 刷新中文菜单和数据页 |
| 存储任务 | 参数变化时 | 参数写 Flash,带 CRC 和双备份 |
采样周期定 20ms 是有讲究的。工频 50Hz 一个周期正好 20ms,在这个窗口里均匀采 200 点,相当于 10kHz 采样率,每个周期采 10 个完整周波,算出来的有效值才稳。采样间隔必须由定时器触发 ADC,不能靠软件延时凑,否则间隔抖动直接体现在有效值误差上。
3.2 控制程序:四种灌溉模式其实是一套参数化调度
自动、定时、周期、手动,听起来是四套逻辑,实现上完全可以收敛成一个状态机加一张计划表。计划表里放四个字段:起始时刻、持续时长、目标水量、触发条件。四种模式只是这张表的填法不同。
- 自动模式:触发条件填温湿度阈值,比如土壤湿度低于 35% 且当前不在禁灌时段
- 定时模式:起始时刻填具体时间点,比如每天 06:30 开始,持续 20 分钟
- 周期模式:起始时刻加周期字段,比如每 48 小时一次,每次喷灌 15 分钟
- 手动模式:目标水量留空,由现场按键或远程指令直接启停
状态机在IDLE → PRECHECK → RUNNING → STOPPING → FAULT之间流转。PRECHECK这一步别省,要检查余额是否充足、上次是否正常停机、流量计有无脉冲、阀门是否在零位,任何一项不过就停在原地报错,不带着问题开水泵。STOPPING也有顺序:先关阀,延时 3 秒再停泵,避免管道里憋压造成水锤。
RUNNING状态的退出条件要做成"或"逻辑:到达持续时长、累计水量达到目标、收到停止指令、任一保护条件触发,四个条件满足一个就退出。这样无论哪种模式,收尾路径都是同一条,测试的时候只需要测一次。
3.3 前端程序:中文菜单的两级结构和参数落盘
现场液晶用两级中文菜单,一级是主菜单,二级是参数页。一级大概五项:实时数据、灌溉模式、参数设置、记录查询、系统信息。二级页最多显示四行,超出翻页。按键用五个:上、下、确认、返回、急停。急停键独立于菜单逻辑,任何时候按下都直接进STOPPING,这个优先级要写死在中断里。
参数页能改的东西包括温湿度上下限、灌溉起止时间、喷灌时长、流量计 K 系数、余额告警阈值。改完不立即生效,要按确认键并二次确认才写 Flash,防误触。写 Flash 之前先把参数结构体算一遍 CRC16,和参数一起存,上电读出来先校验,不对就回落到备份区。
3.4 报警与异常处理:延时确认比阈值本身更重要
电压、电流、流量任何一路异常都要报警,但阈值判断不能一拍就报。水泵启动瞬间电流是额定值的 5~7 倍,流量计在管道有空气时会乱跳,这些正常工况都会被朴素阈值误判成故障。
我的做法是每条报警配一个确认计数,连续 N 个采样周期都超限才真正触发,N 一般取 3~5。电压异常看有效值偏离额定值 ±15% 持续 3 秒;电流异常分两种,超过额定 1.5 倍持续 1 秒判过流,低于额定 20% 但阀门在开位持续 10 秒判空转;流量异常判"开了阀却没有脉冲",持续 30 秒判断水或堵塞。
报警分三级处理:提示级只记录和上报,不影响运行;告警级闪屏加蜂鸣,仍继续运行;保护级直接进FAULT,关阀停泵并锁死,必须人工确认才能复位。过流和断水属于保护级,余额不足属于告警级,温湿度越限属于提示级。
4. 关键实现:PWM 阀门控制与电压电流采集的代码落地
前面讲的是结构和分工,这一章落到能直接编译的代码。选这两块是因为它们最能体现这套系统的技术含量:PWM 决定灌溉量准不准,ADC 决定计量和报警靠不靠谱。代码基于 HAL 库,在 CubeMX 里配好时钟和引脚之后直接往工程里加文件即可。
4.1 阀门 PWM:占空比到机械开度的死区映射
先在 CubeMX 里把 TIM2 的 Channel1 配成 PWM Generation,预分频 7199、计数周期 9999,得到 72MHz / 7200 / 10000 = 1kHz 的输出。初始化代码用 CubeMX 生成的MX_TIM2_Init(),业务层只封装开度设置这一个入口。
/* valve.c —— 比例阀 PWM 驱动,占空比按机械死区做线性映射 */ #include "stm32f1xx_hal.h" #define VALVE_PWM_MIN 1000U /* 10% 占空比,阀门刚好脱离全关死区 */ #define VALVE_PWM_MAX 9000U /* 90% 占空比,阀门到达满开饱和点 */ #define VALVE_CLOSE_PULSE 0U /* 完全关闭时输出 0,配合常闭阀 */ static TIM_HandleTypeDef *s_htim; static uint32_t s_channel; /* 初始化:配置 PWM 通道并启动输出,默认全关 */ void Valve_Init(TIM_HandleTypeDef *htim, uint32_t channel) { TIM_OC_InitTypeDef oc = {0}; s_htim = htim; s_channel = channel; oc.OCMode = TIM_OCMODE_PWM1; oc.Pulse = VALVE_CLOSE_PULSE; oc.OCPolarity = TIM_OCPOLARITY_HIGH; oc.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim, &oc, channel); HAL_TIM_PWM_Start(htim, channel); } /* open_pct:0~100,直接对应阀门机械开度百分比 */ void Valve_SetOpen(uint8_t open_pct) { uint32_t pulse; if (open_pct == 0U) { /* 0 走全关分支,不进死区映射 */ __HAL_TIM_SET_COMPARE(s_htim, s_channel, VALVE_CLOSE_PULSE); return; } if (open_pct > 100U) open_pct = 100U; /* 线性映射到 10%~90% 有效调节段,跳过滤械死区 */ pulse = VALVE_PWM_MIN + (VALVE_PWM_MAX - VALVE_PWM_MIN) * (uint32_t)open_pct / 100U; __HAL_TIM_SET_COMPARE(s_htim, s_channel, pulse); }VALVE_PWM_MIN和VALVE_PWM_MAX这两个数不是拍脑袋定的,要拿实际比例阀做台架试验测出来:从 0 开始一点点加占空比,记录阀门开始有流量的点和流量不再增长的点,前者对应 1000,后者对应 9000。不同厂家的阀这两个值差别不小,换阀必须重测。
open_pct传 0 时特意走单独分支输出 0,是为了让常闭电磁阀彻底断电,避免死区映射把 0% 开度变成 10% 占空比、阀门一直微开漏水。这个细节不改,池塘边放一周水就下去了。
4.2 ADC 采样:整周期采集与有功功率计算
采样这部分,很多人栽在"采得太随意"上。用HAL_Delay控制采样间隔,误差能到几十微秒,算出来的有效值抖得没法看。正确做法是定时器触发、缓冲区整周期采满、再统一计算。
/* meter.c —— 电压电流整周期采样与有功功率、累计电量计算 */ #include "stm32f1xx_hal.h" #include <math.h> #define SAMPLE_N 200U /* 20ms / 100us,一个工频周期采 200 点 */ #define ADC_MID 2048.0f /* 12 位 ADC 中点,对应 1.65V 偏置 */ #define VOLT_K 0.1520f /* 电压标定系数 V/码,满量程约 220V */ #define CURR_K 0.0244f /* 电流标定系数 A/码,50A 量程 */ #define CURR_ZERO 2048.0f /* 霍尔零点,对应 2.5V */ static uint16_t s_vbuf[SAMPLE_N]; static uint16_t s_ibuf[SAMPLE_N]; static float s_energy_wh = 0.0f; /* 累计电量,单位 Wh */ /* 按 10kHz 采满一个工频周期,电压电流轮流采 */ void Meter_SampleOnce(void) { for (uint16_t i = 0; i < SAMPLE_N; i++) { s_vbuf[i] = ADC_ReadChannel(ADC_CHANNEL_0); /* 电压 */ s_ibuf[i] = ADC_ReadChannel(ADC_CHANNEL_1); /* 电流 */ Delay_us(100); /* 10kHz 采样间隔 */ } } /* 电压有效值,去偏置后平方求和再开方 */ float Meter_VoltageRMS(void) { float sum = 0.0f; for (uint16_t i = 0; i < SAMPLE_N; i++) { float v = ((float)s_vbuf[i] - ADC_MID) * VOLT_K; sum += v * v; } return sqrtf(sum / SAMPLE_N); } /* 电流有效值,零点按霍尔 2.5V 对应码值扣 */ float Meter_CurrentRMS(void) { float sum = 0.0f; for (uint16_t i = 0; i < SAMPLE_N; i++) { float c = ((float)s_ibuf[i] - CURR_ZERO) * CURR_K; sum += c * c; } return sqrtf(sum / SAMPLE_N); } /* 有功功率:瞬时电压乘瞬时电流逐点累加,感性负载必须这么算 */ float Meter_ActivePower(void) { float p = 0.0f; for (uint16_t i = 0; i < SAMPLE_N; i++) { float v = ((float)s_vbuf[i] - ADC_MID) * VOLT_K; float c = ((float)s_ibuf[i] - CURR_ZERO) * CURR_K; p += v * c; } return p / SAMPLE_N; } /* 每次采完调用一次,窗口 20ms,换算成 Wh 累加 */ void Meter_Accumulate(void) { float p = Meter_ActivePower(); /* 单位 W */ s_energy_wh += p * 0.020f / 3600.0f; /* 20ms 折算成小时 */ }几个参数必须说清楚。VOLT_K的理论值按满量程 220V 有效值、峰值 311V 对应 ADC 满码 4096 反推,得到约 0.152 V/码,实际值一定要用标准电压表做两点标定修正。CURR_K同理,50A 量程双向对应 100A 跨度、4096 码,得 0.0244 A/码。
Meter_ActivePower这个函数是整个计量链路的命门。它逐点做v × i再平均,得到的是有功功率;如果偷懒写成Meter_VoltageRMS() * Meter_CurrentRMS(),功率因数 0.85 的水泵会算出偏大 17% 的功率,累计电量和余额扣减全跟着错。这一个函数的写法差别,就是方案能不能过验收的差别。
调用顺序是:先Meter_SampleOnce()采满一窗,再依次调三个计算函数,最后调Meter_Accumulate()累加。三个计算函数共用同一组缓冲区,不要再开新的采样,否则电压电流不同步,功率算出来是错的。
4.3 参数存储:双备份加 CRC,别让一次掉电毁掉全部设置
参数存在内部 Flash 的最后一页,按"主区 + 备份区 + CRC"的结构来。写入顺序是先擦备份区、写备份、再擦主区、写主区,任何一步掉电,上电时总有一份完整数据可以回退。
/* param.c —— 灌溉参数掉电存储,双备份 + CRC16 校验 */ #include "stm32f1xx_hal.h" #include <string.h> #define PARAM_MAGIC 0x49524754U /* "IRGT" */ #define PARAM_ADDR_A 0x0807F000U /* 主区,最后一页起始 */ #define PARAM_ADDR_B 0x0807F800U /* 备份区 */ typedef struct { uint32_t magic; uint8_t mode; /* 0 自动 1 定时 2 周期 3 手动 */ uint8_t start_hour; uint8_t start_min; uint16_t spray_sec; /* 单次喷灌时长,秒 */ uint16_t cycle_hour; /* 周期模式间隔小时数 */ uint8_t hum_low; /* 土壤湿度下限,% */ uint8_t hum_high; /* 土壤湿度上限,% */ int8_t temp_low; /* 温度下限,℃ */ int8_t temp_high; /* 温度上限,℃ */ uint16_t flow_k; /* 流量计 K 系数,脉冲/升 */ uint32_t balance_cent; /* 余额,单位分 */ uint16_t crc16; } SysParam_t; /* 标准 CRC16-MODBUS,覆盖结构体除 crc16 外的所有字节 */ static uint16_t Param_CRC16(const uint8_t *buf, uint16_t len) { uint16_t crc = 0xFFFFU; for (uint16_t i = 0; i < len; i++) { crc ^= buf[i]; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x0001U) crc = (crc >> 1) ^ 0xA001U; else crc >>= 1; } } return crc; } /* 先写备份再写主区,两次写之间掉电也能恢复 */ uint8_t Param_Save(const SysParam_t *p) { SysParam_t tmp = *p; tmp.magic = PARAM_MAGIC; tmp.crc16 = Param_CRC16((uint8_t *)&tmp, sizeof(SysParam_t) - 2); if (Param_WritePage(PARAM_ADDR_B, &tmp) != HAL_OK) return 1; if (Param_WritePage(PARAM_ADDR_A, &tmp) != HAL_OK) return 2; return 0; } /* 上电读取:主区校验失败自动回落备份区 */ uint8_t Param_Load(SysParam_t *out) { SysParam_t tmp; if (Param_ReadPage(PARAM_ADDR_A, &tmp) == HAL_OK && tmp.magic == PARAM_MAGIC && tmp.crc16 == Param_CRC16((uint8_t *)&tmp, sizeof(SysParam_t) - 2)) { *out = tmp; return 0; } if (Param_ReadPage(PARAM_ADDR_B, &tmp) == HAL_OK && tmp.magic == PARAM_MAGIC && tmp.crc16 == Param_CRC16((uint8_t *)&tmp, sizeof(SysParam_t) - 2)) { *out = tmp; return 1; /* 主区损坏,已从备份恢复 */ } Param_SetDefault(out); return 2; /* 两区都坏,回落到出厂默认 */ } /* 余额只在变化量达到阈值时落盘,避免频繁擦写 */ void Param_UpdateBalance(SysParam_t *p, uint32_t new_cent) { p->balance_cent = new_cent; /* 内存里随时更新 */ if (p->balance_cent < 100U || (p->balance_cent % 100U == 0U)) { Param_Save(p); /* 每满 1 元或余额告急才写一次 */ } }结构体里balance_cent用整数存"分",不用浮点。浮点在 Flash 里逐字节比较不靠谱,掉电恢复后可能出现 12.999998 这种余额,用户看着难受,对账也对不上。水量同理,用毫升或升的整数存。
Param_UpdateBalance里的落盘策略是血泪经验。早期版本每扣一次水费就写一次 Flash,一天几百次,内部 Flash 十万次擦写寿命看着很长,几个月就磨到坏块,参数莫名其妙丢。改成每满 1 元写一次,写次数直接降两个数量级,掉电最多丢 1 元以内的记录,可以接受。
5. 避坑与常见问题:现场最容易翻车的五个点
实验室能跑和现场能跑,中间隔着一堆看不见的坑。下面五条是这套方案最容易出问题的地方,每条都按现象、原因、解决三步写,遇到时按顺序排,别乱猜。
第一条:ADC 采样值跳动大,有效值算出来忽高忽低。
现象是接上标准电压表对比,读数偏差一会儿 2V 一会儿 15V,根本没法标定。原因通常是采样间隔靠软件延时凑,抖动大到覆盖不住整周期,加上没做任何滤波。解决办法是改用定时器触发 ADC、固定窗口采满 200 点、再做一次中值加滑动平均;同时把模拟地和数字地分开走,ADC 参考脚单独加 LC 滤波,采样点尽量远离继电器和 4G 天线。
第二条:继电器一吸合,单片机就复位。
现象很典型,手动按启动键,屏幕一黑又重启,串口日志从头开始打。原因是水泵这种感性负载断电瞬间产生反电动势,沿着共地路径灌回 MCU,或者 4G 模块发射时的电流冲击把 5V 拉塌。解决办法是继电器前加光耦隔离,线圈反并续流二极管,触点上并 RC 吸收;MCU 和 4G 各走独立 LDO;软件上把动作顺序改成先关阀、延时 3 秒、再停泵,把冲击错开。
第三条:PWM 调阀门,用户设 30% 和 35% 流量一样。
现象是开度调节不线性,中间一段调了没反应,两头又特别敏感。原因是比例阀本身有机械死区,而且 PWM 经 RC 滤波后的纹波在低压段比例上被放大。解决办法是把 0~100% 映射到阀门的 10%~90% 有效段,RC 滤波的截止频率再压低一档(R 加到 20kΩ、C 保持 0.1μF),如果还不行就引入流量反馈做闭环修正,用实测流量反推开度,比开环映射稳得多。
第四条:运行几个月后参数丢失,余额归零。
现象是某天开机,所有设置回到出厂值,或者余额显示 0。原因是 Flash 擦写次数被耗光,或者写参数的过程中掉电,主区和备份区同时处于半写状态。解决办法是余额按阈值落盘而不是每次变都写,擦写前先写备份区,上电做 CRC 校验并支持双区回退,电源端加一颗掉电检测芯片,检测到电压跌落立刻中止写操作。
第五条:4G 断网后余额对不上,同一笔充值扣两次。
现象是网络恢复后,云端和本地余额差了一笔,或者一次扫码充值本地入账两次。原因是本地和云端双写没有唯一标识,重连补传时把旧消息又发了一遍。解决办法是每条充值、扣费记录带全局唯一流水号,本地维护一张最近 100 条流水号的环形表,收到重复流水号直接丢弃;断网期间的上报数据进环形缓存,重连后按时间顺序补传并标记补传位,云端按流水号幂等处理。
6. 验收与答辩:用一份测试记录把系统讲清楚
系统做完不等于方案成立,能不能拿出可复现的测试记录,是答辩和交付的分水岭。我一般按"指标先行、逐项实测、异常兜底"三步走,先把验收表列出来,再一项项填数据,最后补一组故障注入测试。
| 测试项 | 测试方法 | 合格判据 |
|---|---|---|
| 电压测量 | 标准电压表三点对比(180V/220V/250V) | 相对误差 ≤ 1% |
| 电流测量 | 钳形表对比额定负载 | 相对误差 ≤ 2% |
| 流量计量 | 标准表或称重法对比 100L 水 | 误差 ≤ 2%,达 2 级精度 |
| 阀门开度 | 0/25/50/75/100% 五点看实际流量 | 线性度偏差 ≤ 5% |
| 定时灌溉 | 设定 30 分钟,秒表计时 | 起停偏差 ≤ 10 秒 |
| 周期灌溉 | 设定 24 小时周期,跨天验证 | 周期偏差 ≤ 1 分钟 |
| 欠压报警 | 调压器降至 180V | 3 秒内告警并上报 |
| 断水保护 | 关闭进水阀模拟无水 | 30 秒内停机并锁死 |
| 预付费 | 余额耗尽后尝试启动 | 拒绝启动,提示充值 |
| 断网续传 | 拔天线 10 分钟后恢复 | 缓存数据完整补传,无重复无丢失 |
填表的时候有两个细节要注意。电压电流的标定必须先用标准表做两点修正,把VOLT_K和CURR_K改成实测值再测,拿理论值直接测大概率超差。流量计量按 100L 起测,水量太少时脉冲计数分辨率不够,误差会被放大,测出来的数没有参考价值。
答辩讲的时候,顺序比内容重要。先讲这套系统要满足哪几个指标(精度、模式、控制方式、付费),再讲硬件怎么划模块、软件怎么分层,然后直接放测试表,最后讲异常怎么处理。评委最容易被问住的是"你怎么知道它准"和"出问题怎么办",这两问的答案都在测试记录和故障注入里,提前准备好。
故障注入至少要演示三项:拔掉流量计信号线看是否在 30 秒内停机;把余额改成 0 看是否拒绝启动;断开 4G 天线 5 分钟再插回,看云端数据是否连续。这三项演示完,方案的完整性基本就立住了。代码层面留个后门,串口能直接改余额和模拟报警,演示时省很多事。
从那以后我每次带这类项目,都会在写第一行代码之前先把验收表定下来,后面每加一个模块就回来填一行。表填不满的地方就是方案还没想透的地方,早点暴露总比答辩现场被问住强。希望帮到你。
本文还有配套的精品资源,点击获取