news 2026/11/2 8:10:14

STM32F103ZET6智能灌溉系统:采集控制通信联调避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103ZET6智能灌溉系统:采集控制通信联调避坑

简介:《基于STM32设计的智能灌溉控制系统》是一份面向嵌入式开发、物联网及智慧农业方向学习者的项目设计文档,围绕以STM32为主控的灌溉控制系统展开,帮助读者理解从环境数据采集到执行控制的完整实现思路。全文以1个PDF文件交付,压缩包约1.29MB,篇幅精炼,便于在电脑端查阅和打印。内容覆盖系统总体方案、硬件模块与软件程序划分,具体涉及AD模块采集电压、电流与流量,继电器配合PWM调节阀门开度,4G模块接入云平台,二维码或卡预付费,以及液晶中文菜单和后台大屏动态展示;同时给出自动、定时、周期、手动等灌溉模式与异常报警设计。文中还附有PWM控制直流电机转速的核心代码示例,并说明采集、控制、前端、后台四类程序的职责,便于读者对照梳理项目结构与调试要点。已有721人学习,适合课程设计、毕业设计及嵌入式项目入门参考。

1. 某灌溉项目现场,泵一启动液晶屏就跳数:这套 STM32 方案到底治哪些病

某灌溉项目现场,泵一启动,控制柜的液晶屏就开始跳数:电压从 220V 掉到 200V 附近,电流指针抖得厉害,流量计累计值也跟着虚涨。这类毛病很少是接线松了,多半是采集、控制、通信三条链路各写各的,没人管时序边界。手头这份基于 STM32F103ZET6 的智能灌溉控制系统设计资料,把主控、AD 采集电压电流流量、继电器启停水泵、PWM 调阀门开度、自动/定时/周期/手动四种灌溉模式、4G 上云、二维码预付费,连同液晶中文菜单和后台大屏,完整拆了一遍。它适合正在做农业物联网课程设计的人,也适合要给园区搭小批量控制柜的一线工程师——最大价值是能把原理图、参数表、控制时序三样东西对齐,省掉从图纸到现场调试的那段弯路。

2. STM32F103ZET6 的资源账:五类外设塞进一颗芯片前先算清引脚

这套系统的所有坑,追到根上都是资源没算清就开始画板。采集要 ADC,控制要 PWM,液晶要并行总线,4G 要串口,预付费要读卡或扫码,五类外设互相抢引脚,抢不过去的地方就得靠软件绕,越绕越容易出玄学。

2.1 为什么是 F103ZET6,而不是手边更常见的 C8T6

很多人第一反应是拿 F103C8T6 起步,便宜、资料多、最小系统板到处有。但把需求摊开一看就明白为什么这份设计选了 ZET6。C8T6 是 48 脚封装,Flash 只有 64KB,SRAM 20KB,片内 ADC 两颗,通用定时器只有 TIM2/3/4 三个,最要命的是没有 FSMC,想驱动 8080 并口的液晶屏只能靠 GPIO 软件模拟,刷一个中文菜单页要几十毫秒,按键一快就明显拖影。

ZET6 是 144 脚,Flash 512KB、SRAM 64KB,ADC 有三颗,定时器从 TIM1 排到 TIM8(TIM1/TIM8 还是带死区的高级定时器),FSMC 直接挂在 AHB 上,并口屏的读写由硬件时序发生器完成,CPU 只管填数据。这套系统要在片内塞 GB2312 一级字库(3755 个 16×16 点阵,约 120KB)、要留环形缓冲存历史数据、还要跑菜单状态机和 4G 协议栈,64KB SRAM 和 512KB Flash 才算刚够喘气。

资源项系统实际需求F103C8T6F103ZET6
Flash代码 + 120KB 中文字库 + 参数备份64KB(不够)512KB(余量约 300KB)
SRAM显存缓冲 + 采样环缓 + 协议栈20KB(紧张)64KB(宽裕)
定时器PWM 1 路 + 输入捕获 1 路 + 系统节拍 1 路3 个通用(刚好卡死)8 个,含 2 个高级
ADC电压、电流两路轮询2 个 ADC,10 通道3 个 ADC,21 通道
液晶接口8080 并口 16 位仅 GPIO 模拟FSMC 硬件驱动
IO 总数屏 21 + 按键 4 + 继电器 4 + 通信 4 + 传感 337 脚(不够)112 可用(够)

提示:如果预算真的卡在 C8T6,可行的降级方案是把液晶换成 I2C 或 SPI 的 12864,字库改成按需取模从外部 SPI Flash 读,但菜单刷新率和中文显示效果会明显下降,现场阳光下基本看不清。这套系统的交互设计是围绕并口屏做的,硬降级要重写整个前端。

2.2 ADC 通道、定时器通道、继电器 IO 的分配表

引脚分配不是随便找空地插,得按「模拟量聚堆、高速信号避让、复用功能优先」三条原则走。模拟输入集中到 ADC1 的 IN0/IN1,是为了后续开 DMA 扫描时一个触发源就能把两路都搬走;PWM 放在 TIM1_CH1(PA8),因为 TIM1 是高级定时器,后面如果要加死区控制或者互补输出,不用改板;流量计的脉冲输出走 TIM3_CH1(PA6)的输入捕获,而不是外部中断,原因是高频脉冲下中断风暴会把菜单刷新拖垮,硬件捕获只占一个寄存器。

功能外设资源引脚备注
电压采样ADC1_IN0PA0交流采样需 1.65V 偏置
电流采样ADC1_IN1PA1霍尔或互感器输出
流量脉冲TIM3_CH1 输入捕获PA6上升沿捕获,内部上拉
阀门 PWMTIM1_CH1PA8驱动直流减速电机
水泵继电器GPIO 推挽输出PB0经光耦驱动,低电平有效
4G 模块USART2 TX/RXPA2/PA3波特率 115200
液晶数据线FSMC_D0~D15PD14/PD15/PD0/PD1 等16 位并口
液晶控制线FSMC_NE1 / A16PD7 / PD11片选与命令数据切换
按键GPIO 输入上拉PC0~PC3四个独立键,软件消抖
蜂鸣器GPIO 推挽PB5报警用,三极管驱动

分配完之后要养成一个习惯:把这张表打印出来贴在调试台前。现场改线的时候,最怕的就是「我记得 PA8 是 PWM」这种记忆,改错一次烧掉一个阀门驱动器,代价比打印一张纸高得多。

2.3 中文菜单的字库放哪、显存放哪

液晶要显示中文,必须有字库。常见做法有三种:把 GB2312 点阵字库烧进片内 Flash 尾部、外挂一颗 SPI Flash 存字库、或者用带字库的液晶模组。这份设计走的是第一条,理由是 ZET6 的 512KB Flash 装完代码还剩 300KB 左右,120KB 字库存得下,而且片内读取不需要额外总线,取模速度快。

具体做法是在链接脚本里把 Flash 划一段出来做字库区,起始地址放在 0x08070000 之后,编译时用__attribute__((at(0x08070000)))或者分散加载文件定位。取模时按「区码 - 0xA1」算出偏移,一次读 32 字节(16×16 点阵每行 2 字节,共 16 行),拼成显存后一次性刷给 FSMC。

菜单本身用结构体数组 + 状态机实现,比一长串switch好维护得多:

/* menu.h —— 菜单项描述结构体 */ typedef struct { const char *text; /* 显示文本,UTF-8 或 GB2312 编码 */ uint8_t type; /* 0=只读显示 1=可编辑数值 2=子菜单 3=执行动作 */ void *value; /* 指向被编辑的变量,只读项填 NULL */ float min, max; /* 可编辑数值的上下限 */ void (*action)(void);/* type=3 时执行的函数指针 */ } MenuItem; /* 一级菜单示例:灌溉模式选择页 */ extern uint8_t g_mode; /* 0=手动 1=自动 2=定时 3=周期 */ static MenuItem s_mode_page[] = { {"灌溉模式", 1, &g_mode, 0, 3, 0}, {"起始时间", 1, &g_start, 0, 2359, 0}, {"喷灌时长", 1, &g_dur, 1, 240, 0}, {"立即启动", 3, 0, 0, 0, Act_StartNow}, };

这段结构体的关键在type和action两个字段。type决定按键按下时是翻页、加减数值还是触发动作,action让「立即启动」「参数清零」这类一次性操作不用在菜单引擎里写死分支。min/max是必须的——现场调试时把喷灌时长设成 0 分钟,泵会一直空转,限幅比事后改代码便宜。

2.4 220VAC 侧的隔离与地线,别等炸了才想起

泵和阀门走 220VAC,MCU 走 3.3V,中间只隔一个继电器和几根光耦,这条边界必须画得干净。常见做法是:继电器模块选带光耦隔离的成品,MCU 侧的地和 220V 侧的地物理分开,只在电源入口处单点连接;水泵这类感性负载在触点两端并 RC 吸收网络(典型 100Ω + 0.1μF/630V),否则断电瞬间的反电动势能把触点烧出麻点。

ADC 前端同样要隔离。电压采样用 AC-AC 变压器降到 5V 左右再分压,电流采样用霍尔传感器,两者天然隔离,比电阻分流安全得多。强弱电走线在柜内要分开捆扎,交叉时走直角,别平行走超过 20cm——这不是讲究,是血泪经验,平行走线的工频干扰会让电流采样值凭空多出几十毫安的底噪。

注意:GPIO 上电复位后默认是浮空输入,继电器驱动脚如果直接接三极管基极,主控复位那几百毫秒里电平不定,泵可能抽搐一下。正确做法是基极下拉 10kΩ 电阻,保证复位期间可靠关断。

3. 把阀门和泵的控制写成模块:PWM 频率、占空比分辨率与联锁时序

控制代码写得好不好,不看能不能转,看的是「停的时候干不干净」。这套系统里泵是开关量、阀门是连续量,两者动作有先后顺序,顺序错了就是水锤或者憋泵。

3.1 10kHz 还是 1kHz:PWM 频率、预分频与分辨率的三角关系

STM32F103 的定时器时钟来自 APB2 时 72MHz,PWM 频率和占空比分辨率是一对跷跷板:

f_pwm = 72MHz / ((PSC + 1) × (ARR + 1)) 分辨率 = ARR + 1 级

要 10kHz 且 1000 级分辨率,就得 PSC=0、ARR=7199;要 20kHz 就只有 3600 级。反过来,如果只要 100 级分辨率,可以做到 100kHz 以上。选择依据是负载特性:直流减速电机在 10kHz 以上基本听不见啸叫,开关损耗也可接受;低频(几百 Hz)会有明显抖动和嗡嗡声,但转矩脉动小。阀门执行器如果走的是「PWM 转 0-10V」的模拟量模块,那频率反而要压到 1kHz 以下,否则 RC 滤波跟不上,输出电压会是锯齿。

场景PSCARR频率分辨率
直流减速电机直驱0719910 kHz7200 级
中速调节,兼顾噪声719991 kHz1000 级
PWM 转 0-10V 模拟量719991 kHz100 级
只需要粗略调速719910 kHz100 级

这份设计用的是第一行,10kHz、7200 级分词。之所以不用 100 级,是因为阀门开度要跟流量计的反馈做闭环,分辨率太低会导致开度在目标值附近来回跳,流量曲线一片毛刺。

3.2 把 PWM_Init / Set_Motor_Speed 拆成 .h 和 .c 两层

原始 PDF 里那段代码在转存时字符集坏了,注释成了乱码,而且Set_Motor_Speed函数只有限幅没有写入比较寄存器,逻辑是断的。按功能重写一版,头文件和实现分开,方便多个 .c 文件调用:

/* motor_control.h —— 阀门/电机 PWM 控制接口 */ #ifndef MOTOR_CONTROL_H #define MOTOR_CONTROL_H #include "stm32f1xx_hal.h" /* 初始化指定定时器的指定通道为 PWM 输出,默认占空比 0 */ void PWM_Init(TIM_HandleTypeDef *htim, uint32_t channel); /* 设置占空比,permille 为千分比 0~1000,内部自动限幅 */ void Set_Motor_Speed(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t permille); /* 一步到位:初始化 + 设置,供上电自检时调用 */ void Motor_Control(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t permille); #endif
/* motor_control.c —— 实现 */ #include "motor_control.h" void PWM_Init(TIM_HandleTypeDef *htim, uint32_t channel) { TIM_OC_InitTypeDef sConfigOC = {0}; sConfigOC.OCMode = TIM_OCMODE_PWM1; /* 计数值小于比较值时输出有效电平 */ sConfigOC.Pulse = 0; /* 上电默认 0%,阀门全关 */ sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; /* 关闭快速模式,避免毛刺 */ if (HAL_TIM_PWM_ConfigChannel(htim, &sConfigOC, channel) != HAL_OK) { Error_Handler(); /* 配置失败直接停机,不要带病运行 */ } HAL_TIM_PWM_Start(htim, channel); } void Set_Motor_Speed(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t permille) { uint32_t period = __HAL_TIM_GET_AUTORELOAD(htim); /* 读 ARR,不依赖 htim->Init */ uint32_t compare; if (permille > 1000) permille = 1000; /* 上限钳位 */ if (permille < 10) permille = 0; /* 低于 1% 直接关断,防堵转发热 */ /* 千分比换算成比较值:(ARR+1) * permille / 1000 */ compare = (uint32_t)(((uint64_t)(period + 1) * permille) / 1000U); if (compare > period) compare = period; __HAL_TIM_SET_COMPARE(htim, channel, compare); } void Motor_Control(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t permille) { PWM_Init(htim, channel); Set_Motor_Speed(htim, channel, permille); }

三处改动值得说明。第一,接口从「万分之一」改成「千分比」,因为现场调试时 0~1000 对应 0~100% 更符合直觉,参数改起来不用换算。第二,Set_Motor_Speed里用__HAL_TIM_GET_AUTORELOAD读 ARR,而不是原文的htim->Init.Period,后者在 CubeMX 重新生成代码或者运行中改过 ARR 后就不准了。第三,加了「低于 10‰ 直接关断」这条,阀门电机在 1% 占空比下根本转不动,只有电流没有转矩,长时间通电就是纯发热,这条限幅救了不止一个执行器。

提示:PWM_Init里Pulse = 0是刻意的。上电时阀门应该在关闭位,而不是继承上一次的占空比。如果系统需要断电保持开度,那要另配绝对位置编码器,别指望 PWM 占空比记位置。

3.3 泵与阀门的联锁:先开阀后启泵,这条必须写进代码

水锤的成因很简单:泵在满压运行、管道里全是水的时候,阀门如果瞬间关死,动能无处可去,只能撞在管壁和阀座上。轻则嗡嗡响,重则爆管。所以启停顺序有硬性规定:启动时先开阀到最小开度(一般 15%~20%),延时 2 秒让水流建立,再启泵,然后缓慢升到目标开度;停止时先把阀门关到最小开度,停泵,延时 2 秒,再全关。

这段逻辑用一个简单的状态机就够了:

/* valve_seq.c —— 阀门与泵的启停时序 */ typedef enum { SEQ_IDLE, SEQ_OPENING, SEQ_PUMPING, SEQ_CLOSING } SeqState; void Irrigation_Start(uint16_t target_permille) { Set_Motor_Speed(&htim1, TIM_CHANNEL_1, 150); /* 先到 15% 最小开度 */ HAL_Delay(2000); /* 等水流建立 */ Pump_On(); /* 继电器吸合,泵启动 */ HAL_Delay(500); Set_Motor_Speed(&htim1, TIM_CHANNEL_1, target_permille); /* 缓升到目标 */ } void Irrigation_Stop(void) { Set_Motor_Speed(&htim1, TIM_CHANNEL_1, 150); /* 回最小开度 */ HAL_Delay(1000); Pump_Off(); /* 先停泵,切断动力源 */ HAL_Delay(2000); /* 等管道泄压 */ Set_Motor_Speed(&htim1, TIM_CHANNEL_1, 0); /* 再全关阀门 */ }

HAL_Delay在正式产品里应该换成非阻塞的定时器状态机,否则这段时间菜单会卡住。但调试阶段用阻塞写法没问题,逻辑跑通了再改成状态机,比一上来就写复杂框架容易排查。

3.4 继电器侧的三个硬件细节

软件再稳,硬件翻车一样白搭。触点粘连是最常见的故障:泵启动电流是额定电流的 5~7 倍,触点每次吸合都在打火花,用久了就粘死,表现为「泵一直转,软件让停也停不下来」。选型上继电器触点额定电流要留 3 倍余量,比如 10A 的泵至少配 30A 触点;或者干脆换成固态继电器,没有机械触点,代价是发热和成本。

第二个细节是驱动脚的上电状态。前面提过 GPIO 复位后浮空,基极下拉电阻不能省。第三个是 RC 吸收网络,并在水泵触点两端,别并在继电器线圈两端——并错位置等于没并。

4. 电压、电流、流量的采集链路:从 ADC 读数到累计用水量

采集这块最容易出问题,因为它同时踩了三个雷:交流信号、小信号、多通道轮询。任何一个没处理好,后面的功率、电能、报警全是错的。

4.1 交流采样为什么必须抬 1.65V 偏置

F103 的 ADC 只能采 0~3.3V 的单极性信号。220V 交流经 AC-AC 变压器降到 5V 有效值后,正负半周都还在,直接送进 ADC 会把负半周削掉,采出来的是个半波整流波形,有效值算出来只有真实值的一半多一点。

标准做法是加直流偏置:用两个等值电阻(典型 10kΩ/10kΩ)从 3.3V 分压出 1.65V,经运放跟随器缓冲后叠加到信号上,让交流波形整体上移到 1.65V 附近摆动。这样正负半周都落在 0.1V~3.2V 区间内,ADC 才能完整采样。偏置电压的稳定性直接决定测量精度,分压电阻要用 1% 精度的金属膜电阻,别用碳膜。

电流侧同理。霍尔传感器的输出通常是 2.5V 零点、±0.625V 摆幅(对应 ±5A),已经带偏置了,直接用就行;如果是电流互感器,输出是交流小信号,同样要抬偏置,而且互感器二次侧必须并一个采样电阻(典型 100Ω),否则输出是电流不是电压。

4.2 ADC + DMA 多通道扫描:别再手动切通道了

原始 PDF 里的采集代码有几个明显问题:ScanConvMode设成 DISABLE 却配置了两个通道、ContinuousConvMode设成 ENABLE 又手动 Start/Stop、采完电压没切换通道就直接采电流。这种写法在单通道下能用,双通道会拿到同一路的重复值。

正确做法是开扫描模式 + DMA 循环搬运,一个定时器触发一次,DMA 把两个通道的结果依次写进数组,CPU 完全不参与:

/* adc_sample.c —— 双通道 ADC + DMA 循环采样 */ #define ADC_BUF_LEN 2 uint16_t g_adc_buf[ADC_BUF_LEN]; /* [0]=电压通道 [1]=电流通道 */ void ADC_Multi_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE; /* 多通道必须开扫描 */ hadc1.Init.ContinuousConvMode = DISABLE; /* 由定时器触发,不连续转换 */ hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 调试期软件触发,量产改 TIM */ hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 2; /* 两个通道参与扫描 */ HAL_ADC_Init(&hadc1); sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; /* 交流信号建议 55.5 周期 */ sConfig.Channel = ADC_CHANNEL_0; /* PA0 电压 */ sConfig.Rank = ADC_REGULAR_RANK_1; HAL_ADC_ConfigChannel(&hadc1, &sConfig); sConfig.Channel = ADC_CHANNEL_1; /* PA1 电流 */ sConfig.Rank = ADC_REGULAR_RANK_2; HAL_ADC_ConfigChannel(&hadc1, &sConfig); /* DMA 循环模式,采完自动回到起点,永远拿最新值 */ HAL_ADC_Start_DMA(&hadc1, (uint32_t *)g_adc_buf, ADC_BUF_LEN); }

SamplingTime设成 55.5 个 ADC 周期是有讲究的。F103 的 ADC 时钟最高 14MHz,55.5 周期对应约 4μs,加上转换时间总共 5μs 左右。50Hz 交流一个周期 20ms,按每周期采 200 点算,采样间隔 100μs,完全够用。如果采样时间设成 1.5 周期,采到的是信号瞬间值,但前级运放的建立时间不够,读数会偏低且随频率漂移——这是很多人调了半天以为代码有问题、其实是模拟前端的锅。

4.3 有效值、功率因数和电能:别拿 Urms 乘 Irms 当有功

原始 PDF 里的公式是功率 = 电压 × 电流,在纯阻性负载(比如电热丝)下成立,但水泵是感性负载,功率因数通常在 0.7~0.85 之间,直接相乘会高估 20%~40%,累计电能就全错了。

正确做法是采瞬时值做点积再累加:

/* 有效值与有功功率计算,单周期 200 点 */ float Calc_Rms(const uint16_t *buf, uint16_t n, float scale, float offset) { float sum = 0.0f; for (uint16_t i = 0; i < n; i++) { float v = ((float)buf[i] * 3.3f / 4096.0f - offset) * scale; sum += v * v; } return sqrtf(sum / n); } /* 有功功率:同一时刻的 u*i 求平均,天然包含了功率因数 */ float Calc_Power(const uint16_t *ubuf, const uint16_t *ibuf, uint16_t n, float us, float uo, float is, float io) { float sum = 0.0f; for (uint16_t i = 0; i < n; i++) { float u = ((float)ubuf[i] * 3.3f / 4096.0f - uo) * us; float i2 = ((float)ibuf[i] * 3.3f / 4096.0f - io) * is; sum += u * i2; } return sum / n; }

两个函数的参数含义:scale是传感器变比(电压通道 = 变压器变比 × 分压比,电流通道 = 1/灵敏度),offset是零点对应的 ADC 归一化值(1.65V 偏置就是 0.5)。n取一个完整工频周期,50Hz 下是 200 点,这样正负半周自动抵消,不需要额外的滤波。

电能的累计用「功率 × 采样间隔」逐次累加,间隔由定时器保证,别用HAL_Delay的时间当间隔——HAL_Delay受中断影响,误差能到几个百分点,累计一天下来差好几度电。

4.4 流量脉冲的 K 系数与 2 级精度怎么落地

流量计选的是脉冲输出型,每流过 1 升水输出若干个脉冲,这个比例就是 K 系数(单位:脉冲/升)。2 级精度意味着误差不超过 ±2%,听着宽松,实际要做到得注意三件事。

第一,K 系数不能信铭牌。出厂标定值是在标准实验台上、特定流速区间测的,现场管道有弯头、有变径、有气泡,实际 K 值会偏。落地做法是拿一个已知容积的桶接水,走 100 升,读脉冲数,反算 K = 脉冲数 / 100,重复三次取平均。

第二,脉冲捕获要用硬件。用HAL_GPIO_EXTI做外部中断,每来一个脉冲进一次中断,流量大时(比如 30L/min,K=450)中断频率能到 225Hz,看着不高,但中断里如果还有别的活,就会丢脉冲。用 TIM3 的输入捕获 + 从模式复位,硬件计数,CPU 定期读计数值清零,一个脉冲都不会丢。

第三,累计用水量要存 Flash。预付费系统里水量是钱,掉电丢了就是纠纷。每累计 1 升写一次 Flash 太奢侈(F103 的 Flash 擦写寿命约 1 万次),常规做法是每 10 升存一次,掉电时最多丢 10 升,配合超级电容或者掉电检测中断,可以在断电瞬间补存一次。

4.5 报警阈值与滑动滤波

电压、电流、流量三路都要报警。阈值不是拍脑袋定的:电压低于 187V(220V 的 -15%)持续 5 秒报欠压,高于 253V 报过压;电流超过额定值 1.2 倍持续 3 秒报过流,这个延时是躲开泵启动时的冲击电流;流量在泵运行状态下持续 30 秒为零,报管路堵塞或者无水。

滤波用中值 + 一阶低通组合。先取 5 点中值滤掉脉冲干扰(比如继电器吸合瞬间的尖峰),再走一阶低通平滑:

/* 一阶低通,alpha 越小越平滑但响应越慢,0.1 对应约 10 个采样周期的时间常数 */ float LowPass(float new_val, float old_val, float alpha) { return old_val + alpha * (new_val - old_val); }

alpha的取值要看采样率。采样周期 100ms、alpha=0.1时时间常数约 1 秒,报警响应不会太迟钝,显示值也不会跳。如果用来做显示,alpha可以压到 0.05;如果用来做保护判断,反而要绕过滤波,直接用原始值加延时确认,免得滤波把真实的过流峰值削平了。

5. 避坑与排查:这套系统在现场最容易翻车的五处

下面五条都是踩过的,按「现象 → 原因 → 解决」写,遇到同类问题可以直接对照。

5.1 泵一启动,电流采样值就飘

现象:泵停机时电流读数稳定在 0.02A 左右,泵一启动,读数从 0.8A 跳到 1.5A 又落回 0.9A,来回晃,功率算出来完全不可信。

原因有两个叠加。一是采样和泵的启停没有同步,泵启动瞬间的冲击电流持续时间只有几十毫秒,刚好落在某个采样窗口里,就采到一个尖峰。二是电流通道的模拟地和 220V 侧的地没隔离干净,继电器吸合时的共模干扰窜进了 ADC。

解决:软件上把功率计算改成「连续 10 个周期取中值」,把单次尖峰剔掉;硬件上在霍尔传感器的输出和 ADC 之间串一个 100Ω 电阻加 0.1μF 电容到地,做成简单的 RC 低通,截止频率约 16kHz,滤掉高频共模。另外把继电器的控制线和 ADC 的信号线在柜内分开走,别捆在一束。

5.2 阀门关不死,流量计一直有微小读数

现象:手动模式点「全关」,阀门动作到位、限位开关也触发了,但流量计还在缓慢累加,一小时能走两三升。

原因:阀门是直流减速电机驱动的,靠限位开关断电停止。限位开关的动作点有回差,电机惯性会多转一点或者少转一点,阀芯没有完全贴合阀座。加上管道里有压力,微小的缝隙就会渗水。

解决:软件上在限位触发后,反向给一个 5% 占空比、持续 300ms 的「压紧」动作,把阀芯顶到位。同时把流量小于某个阈值(比如 0.5L/min)的读数在计算累计用水量时按 0 处理,避免长年累月积出个假数字。这个阈值要根据管道口径定,不能照抄。

5.3 Flash 参数掉电丢失,重启后灌溉时长变回默认值

现象:现场设好了定时灌溉参数,运行一周都正常,某次停电后再上电,参数全回到出厂值。

原因:参数写入没有做完整性校验。F103 的 Flash 在写过程中如果掉电,那一页会处于半擦除状态,读出来全是 0xFF 或者乱码。程序启动时读到非法值,就走了「恢复默认」的分支。

解决:参数区做双备份 + CRC 校验。A 区写完后计算 CRC16 一起存,读的时候校验不过就切到 B 区,B 区也不对才恢复默认。写入顺序是「先擦 B 区、写 B 区、校验、再把 A 区标记为无效」,保证任何时刻至少有一份完整数据。另外 Flash 写操作要放在泵停止、系统空闲的时候做,别在灌溉过程中写,避免电压波动叠加。

5.4 4G 掉线后,定时灌溉不执行

现象:现场信号不好的那几天,后台看不到数据,定时灌溉也没启动,作物差点旱死。

原因:定时逻辑写在了「收到云端下发的参数」之后才启动,本质上把云平台当成了必须依赖。4G 模块一旦掉线重连,参数同步流程卡住,本地定时器就没起来。

解决:把灌溉决策完全下沉到本地。云端只负责下发参数和收数据,参数存 Flash,本地 RTC 独立跑定时任务,4G 在线与否不影响执行。掉线时数据先存本地环形缓冲(按 1 分钟一条、存 3 天算,需要约 20KB,64KB SRAM 放得下),恢复后补传。这条改动是整套系统可靠性的分水岭。

5.5 断网期间刷了卡,恢复后余额被重复扣

现象:预付费用户在网络中断时刷卡启动灌溉,本地扣了费也正常浇水,网络恢复后后台又扣了一次,用户投诉。

原因:本地扣费和云端扣费是两条独立路径,没有做幂等。本地记了一笔「已扣 3.5 元」,但这个记录没上传成功,云端看到的是「上次同步余额 20 元」,按 20 元重新算了一遍。

解决:每笔消费生成一个全局唯一的流水号(设备 ID + 时间戳 + 自增序号),本地先落盘再上报,云端按流水号去重。同步时本地把未上报的流水批量推上去,云端返回「已处理」或「重复」,本地据此清理队列。余额显示以「云端余额 - 本地未同步消费」为准,这样断网期间也能正确判断够不够钱。

5.6 一份现场排查顺序表

出问题时最忌讳东摸一把西摸一把,按下面的顺序走,八成问题前三步就能定位。

步骤检查项判断依据
13.3V 与 5V 电源纹波示波器看峰峰值,超过 100mV 先修电源
2晶振起振与复位电平万用表测 NRST 应为 3.3V,晶振两脚约 1.6V
34G 模块心跳串口抓 AT 返回,看是否注册上网络
4ADC 零点与满量程短接输入测零点码值,加已知电压测增益
5继电器动作与反馈强制置位 GPIO,听吸合声,量触点通断
6Flash 参数读回串口打印 CRC 与各参数值,对比设定值
7流量计脉冲手动放水,示波器看 PA6 有无脉冲

6. 联调验收:把四个灌溉模式跑成一张可复现的测试清单

台架联调有个原则:先不接水,先接灯。把水泵和阀门的线换成 220V 指示灯(或者干脆用万用表看触点),所有时序、联锁、报警逻辑先在无水状态下跑通,确认没有误动作再通水。通水之后一旦出问题,损失的是管路和作物,而不是一个灯泡。

6.1 分阶段联调顺序

第一阶段只上主控和液晶,验证菜单能进能出、参数能存能读、断电重启后参数还在。第二阶段接 ADC 前端,用信号发生器给 50Hz、1V 有效值的正弦波,看采样算出的有效值是否在 0.98~1.02V 之间,超出就回头查偏置和标定系数。第三阶段接继电器和指示灯,把四种模式的启停时序各跑三遍,用秒表对时间。第四阶段才接水泵、阀门、流量计,做整机联动。

每个阶段都要留一份记录:日期、环境温度、关键读数、异常现象。现场出问题时,这份记录比任何日志都管用,因为你能看出是「一直就这样」还是「最近才变」。

6.2 四种模式的验收用例

模式测试用例预期结果
手动按下启动键,设定开度 60%阀先到 15%,2 秒后泵启动,再升到 60%
定时设 08:00 启动、喷灌 15 分钟08:00 整启动,08:15 停止,误差小于 10 秒
周期设每 6 小时一次、每次 10 分钟一天触发 4 次,间隔误差小于 1 分钟
自动湿度低于下限启动,高于上限停止湿度回落过程中不反复启停(需设回差)
报警拔掉流量计信号线,泵运行 30 秒蜂鸣器响,液晶显示「流量异常」,泵自动停
预付费余额 0.5 元,启动灌溉余额扣至 0 时自动停止并提示充值

自动模式那条特别容易忽略回差。湿度限值如果只设一个点,湿度在阈值附近抖动时泵会疯狂启停,一天能启停上百次,继电器撑不过一个月。常见做法是设「启动下限 40%、停止上限 55%」,中间 15% 的死区。

6.3 一个技巧:把现场数据回放到 Flash 做离线复算

系统跑起来之后,最有价值的不是实时曲线,是历史数据。我的习惯是在 Flash 里划一块 8KB 的环形区,每 10 秒存一条记录(时间戳 4 字节 + 电压 2 + 电流 2 + 流量 2 + 状态 1,共 11 字节),能存约 700 条,够两个小时的密集记录。出问题时把这块数据通过串口导出来,用 Python 画图,能清楚看到故障前那几分钟电压是怎么掉的、电流是什么时候冲上去的。

# replay.py —— 把串口导出的十六进制记录还原成曲线 import struct import matplotlib.pyplot as plt raw = open("dump.bin", "rb").read() n = len(raw) // 11 t, u, i, f = [], [], [], [] for k in range(n): ts, uv, iv, fv, st = struct.unpack("<IHHHB", raw[k*11:(k+1)*11]) t.append(ts / 10.0) u.append(uv / 10.0) # 存的是 0.1V 为单位 i.append(iv / 100.0) # 存的是 0.01A 为单位 f.append(fv / 10.0) plt.plot(t, u, label="voltage(V)") plt.plot(t, i, label="current(A)") plt.legend() plt.xlabel("time(s)") plt.show()

关键是打包格式要和固件里写 Flash 的结构体严格对应,struct.unpack的格式串一个字符写错,解出来全是天文数字。我一般在固件里加一个#pragma pack(1)保证结构体不填充,Python 这边照着字段顺序写,两边对一遍再烧。

这套系统真正考验人的地方不在代码写得多漂亮,而在有没有把「泵停了阀门还没关」「网断了定时还得跑」「掉电了钱不能丢」这些边界想在前面。从那以后我每次做带执行器的项目,都会在画原理图之前先把引脚分配表和联锁时序表写出来,两张表定不下来就不动烙铁——这个习惯替我挡掉了至少三次返工。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 8:09:56

Java核心知识体系:JVM内存、垃圾回收与集合框架底层原理

简介&#xff1a;《Java核心知识整理&#xff08;常考八股文&#xff09;》PDF面向Java后端开发者与准备技术面试的求职者&#xff0c;围绕高频考点做系统梳理&#xff0c;适合面试前查漏补缺和日常巩固基础。资源包共1个PDF文件&#xff0c;约10.71MB&#xff0c;从JVM架构、类…

作者头像 李华
网站建设 2026/11/2 8:09:09

FineReport数据显示方式全解析:格式化、条件属性、图表与交互

报表工具用久了会发现一个有意思的现象&#xff1a;同一份数据&#xff0c;换个呈现方式&#xff0c;业务方的反馈能差出十万八千里。FineReport 这类工具最容易被低估的地方&#xff0c;恰恰不是取数逻辑&#xff0c;而是"数据显示方式"这一层——它决定了数据最终以…

作者头像 李华
网站建设 2026/11/2 8:06:36

如何用AI打造智能数据质量异常检测系统?

1. 项目概述与整体设计思路1.1 为什么"数据坏了"比"模型不准"更让人头疼数据质量异常检测这件事&#xff0c;听起来没有模型调优那么光鲜&#xff0c;但真正在业务线上跑过的人都知道&#xff1a;数据一旦坏了&#xff0c;后面所有分析、训练和决策全是空中…

作者头像 李华
网站建设 2026/11/2 8:06:35

uni-app H5 hash路由配置与导航守卫实战

上周有个朋友拿着他们组的 H5 项目来找我&#xff0c;说“守卫写了但拦不住”。我打开代码翻了十分钟就发现问题不在守卫&#xff1a;他们为了绕开某些分享链接被截断的情况&#xff0c;把 manifest.json 里的路由模式从 history 改回了 hash&#xff0c;改完之后没重新编译&am…

作者头像 李华
网站建设 2026/11/2 8:05:16

MUI Chip组件实践:交互事件、受控数据流与可访问性

1. Chip 在界面里到底解决什么问题&#xff0c;什么时候不该用它先说一个我反复遇到的场景&#xff1a;后台列表页里&#xff0c;一条记录同时挂着状态、渠道、负责人、几个业务标签&#xff0c;用<span>拼字符串能跑&#xff0c;但过两周产品要求"状态可以点开筛选…

作者头像 李华