ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于STM32F103ZET6的智能灌溉控制系统设计与联调避坑

基于STM32F103ZET6的智能灌溉控制系统设计与联调避坑

简介:《基于STM32设计的智能灌溉控制系统》是一份面向嵌入式初学者、课程设计与毕业设计学生及智慧农业物联网开发者的PDF项目文档,围绕以STM32为主控的灌溉控制器展开,解决传统灌溉水资源浪费、人工投入大与控制方式单一等问题。内容从系统总体方案、硬件模块到软件流程与核心代码均有涉及,涵盖电压、电流与累计用水量采集,自动、定时、周期和手动灌溉模式,继电器与PWM阀门控制,4G云平台通信、二维码或卡预付费、液晶中文菜单及后台大屏动态显示,并给出异常报警与参数配置思路。压缩包共1个文件,即1份PDF,大小约1.29MB,便于直接阅读和按章节查阅。已有721人学习,适合用于理解STM32在智慧农业中的完整落地路径,也可作为方案论证、模块选型、程序框架和答辩整理的参考。

1. 一块控制板把灌溉从「看天吃饭」变成参数配置:这套系统到底解决什么

去年帮一个灌区看现场,十几亩地浇一遍水,两个人守着一排阀门来回跑,浇完还得拿本子记用了多少电。这套基于 STM32F103ZET6 的智能灌溉控制系统,干的就是把这件事从人手上搬到参数表里:起始时间、喷灌时长、温湿度上下限、流量上限,全都写进 Flash,板子上电就按表执行。它自动采集电压、电流、累计用水量,支持自动、定时、周期、手动四种灌溉模式,还能用中控室、短信、遥控、现场按键四种方式接管。220VAC 供电,流量计量误差按 2 级做,二维码或 IC 卡预付费,4G 模块往云平台推数据。适合三类人:做农业物联网终端的产品开发、要做预付费水电表这类计费型设备的嵌入式工程师、想拿一个软硬件闭环完整练手的同学。整套东西的价值不在算法多深,而在「采集—判断—执行—上报—计费」这条链路是完整闭合的,链条上每一环的坑都得踩一遍。

2. 硬件架构拆解:从 220VAC 进线到 4G 上云的信号链

先把板子摊开看。这块板子不复杂,但要稳定跑在田间地头,硬件上得分清楚三个域:强电域、弱电域、通信域。强电域是 220VAC 进来的水泵和电磁阀,弱电域是 STM32 和它的信号调理电路,通信域是 4G 模组和云平台。分不清这三个域,后面调试会一直怀疑是软件问题。

2.1 主控与电源域:220VAC 进来先分几路

主控选 STM32F103ZET6,Cortex-M3 内核,72MHz 主频,512KB Flash、64KB SRAM,片上 3 个 ADC、多个通用定时器、5 个 USART。选它的理由很实际:外设够用,LQFP144 封装引脚多,带 FSMC 能直接驱 8080 并口的液晶屏,做中文菜单界面不用再挂一颗显示驱动芯片,BOM 成本能压下来。

电源按三级走:220VAC 先经隔离变压器或成品开关电源降到 12V,12V 给继电器线圈和电磁阀驱动留余量;再由 12V 转 5V 给 4G 模组(这玩意儿发射瞬间电流能冲到 2A,单独一路 LDO 或者同步降压,别和 MCU 共用一条线);最后 5V 转 3.3V 给 MCU 和信号调理。常见做法是在 3.3V 这级前面串一个肖特基二极管加 TVS,防止 4G 模组起振时把 MCU 拉复位。

提示:4G 模组的电源脚旁边必须并一颗 470uF 以上的电解电容加 0.1uF 陶瓷电容,位置尽量贴着模组电源引脚。这个电容不是可选项,是能不能联网的前提。

2.2 采集链路:电压、电流、流量三路信号怎么进 ADC

电压采样走 AC-AC 变压器降压,把 220V 降到 6V 左右,再用电阻分压网络调到 0~3.3V 的 ADC 输入窗口。这里有两个细节容易漏:一是交流信号会出负半周,得在分压后加直流偏置,把信号抬到 1.65V 中点上;二是 ADC 引脚前要加限幅保护,两个反向并联的肖特基二极管把输入钳在 -0.3V~3.6V 之间。

电流采样用霍尔电流传感器或者分流电阻加运放,输出 0~3.3V 的电压信号,灵敏度按传感器手册取值,常见的是 50mV/A 或 66mV/A。流量用脉冲输出的水表,脉冲进定时器的外部计数通道,每来一个脉冲中断一次,累计脉冲数乘上每脉冲对应的升数就是累计用水量。这个方案比 AD 采流量稳,不占 ADC 资源,也不用担心采样间隔漏脉冲。

三路信号进 ADC 的资源分配要提前规划:电压和电流轮流用 ADC1 的通道 0 和通道 1,采样间隔由定时器触发;流量走 TIM 的 ETR 或者 IC 通道。如果还想采温湿度,SHT 系列的 I2C 数字传感器更省事,别挤到 ADC 上。

2.3 执行与通信:继电器、PWM 阀门、4G 模组、预付费读头

执行侧分两类。水泵这种大功率负载走继电器或者接触器,MCU 的 GPIO 经光耦隔离后驱动三极管,三极管带继电器线圈,线圈两端反并一个续流二极管。电磁阀这种要求开度可调的走 PWM,用定时器输出占空比驱动阀门的驱动板,占空比 0 对应全关,100% 对应全开。

通信侧 4G 模组通过 UART 和 MCU 相连,跑 AT 指令或者直接透传模式。一个实用的做法是把模组设成透传,MCU 只管往串口吐 JSON,模组负责建链和重连,MCU 端再做一层发送队列和超时重发,掉线了不会把主循环卡死。

预付费读头分两种:二维码扫描模组走串口,扫到码之后把支付流水号发给 MCU;IC 卡读头走 SPI 或 I2C,读卡号和余额。两种都要在本地存一份用户余额和流水号,等 4G 在线时再和云平台对账,不能等云端确认了才放水——田里信号不好是常态。

3. 软件四层程序:采集、控制、前端、后台怎么分工协作

软件按原料里的划分是采集程序、控制程序、前端程序、后台程序四块。实际落地的时候,前两块跑在 MCU 上,后两块一个跑在液晶屏加按键上、一个跑在云平台侧。四块之间靠共享的数据结构通信,不能互相直接调函数,否则改一处崩一片。

3.1 采集程序:定时采样、滤波、打包上传

采集的节拍由定时器给,常见做法是 100ms 触发一次 ADC 转换,1s 做一次汇总上传。ADC 每次只采一路,采完电压切通道再采电流,用一个状态变量在两个通道之间轮转。

/* 采集任务:100ms 周期调用一次 */ typedef struct { float voltage; /* 电压 V */ float current; /* 电流 A */ float power; /* 功率 W */ float energy; /* 累计电能 kWh */ uint32_t flow_pulse;/* 流量脉冲累计 */ } SensorData_t; static SensorData_t g_sensor; void Sample_Task_100ms(void) { static uint8_t ch = 0; uint16_t ad; HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); ad = HAL_ADC_GetValue(&hadc1); if (ch == 0) { /* 电压通道:分压比 10,偏置扣除后取绝对值 */ g_sensor.voltage = fabsf((ad * 3.3f / 4096.0f) - 1.65f) * 10.0f * 2.0f; ch = 1; } else { /* 电流通道:灵敏度 50mV/A */ g_sensor.current = ad * 3.3f / 4096.0f / 0.05f; ch = 0; } g_sensor.power = g_sensor.voltage * g_sensor.current; }

ch变量负责在电压和电流两个 ADC 通道之间轮转,这是单 ADC 多通道的基本做法。fabsf取绝对值是因为交流信号被抬到了 1.65V 中点,减掉偏置之后还剩正负半周,取绝对值再乘系数才是有效值——严格说应该做平方和开方算真有效值,但灌溉这种场景误差允许,取绝对值加系数够用。/0.05是把伏特换算成安培,对应 50mV/A 的灵敏度,换了传感器这个数就得跟着改。

滤波不能省。田里的电磁环境比实验室差得多,水泵启停时 ADC 读数能跳一大截。常见做法是滑动平均,开一个 8 点的环形缓冲,每次采样入队、求和除 8,简单但管用。别用一阶低通滤波还带浮点运算,F103 没 FPU,浮点全靠软件模拟,跑一次滤波几十微秒,采样频率一高就顶不住了。

3.2 控制程序:四种灌溉模式的调度状态机

四种灌溉模式——自动、定时、周期、手动——不要写成四个独立的 if 分支,那样加一条业务规则就得改四处。做成状态机,把模式抽象成「触发条件 + 执行动作 + 结束条件」三要素。

typedef enum { MODE_MANUAL = 0, MODE_AUTO, /* 按温湿度阈值触发 */ MODE_TIMER, /* 指定时刻开始,喷灌固定时长 */ MODE_CYCLE /* 每 N 小时喷一次,每次 M 分钟 */ } IrrigMode_t; typedef struct { IrrigMode_t mode; uint8_t enabled; uint32_t start_hour, start_min; /* 定时模式用 */ uint32_t duration_min; /* 单次喷灌时长 */ uint32_t cycle_hours; /* 周期模式间隔 */ float th_temp_high, th_hum_low; } IrrigParam_t;

参数结构体是整套系统的核心,它必须能被完整地序列化进 Flash、被前端菜单读写、被 4G 上报。所以我一般会把它的字段定义收敛到一处,前端和云端都按同一份字段表来,别各写各的。

控制主循环按秒跑一次状态机:先读传感器,再判安全条件(电压超限、电流超限、流量超限直接切报警态并关阀),通过了再按当前模式的触发条件决定开不开阀。开阀动作要给软启动——PWM 占空比从 0 往上爬,200ms 一个台阶爬到目标值,别一步跳到 100%,水锤能把管道打裂。

/* 阀门软启动:目标开度 0~100,每 200ms 递增 10 */ void Valve_RampTo(TIM_HandleTypeDef *htim, uint32_t ch, uint8_t target) { uint8_t cur = g_valve_open; while (cur != target) { cur = (cur < target) ? (cur + 10) : (cur - 10); if (cur > 100) cur = 100; /* Period=9999 对应 10kHz,占空比 = cur% */ __HAL_TIM_SET_COMPARE(htim, ch, (uint32_t)(9999ul * cur / 100ul)); HAL_Delay(200); g_valve_open = cur; } }

g_valve_open要声明成全局,因为报警处理里需要知道当前开度,直接调用Valve_RampTo(0)关阀。

3.3 前端与后台:中文菜单和云平台数据模型

前端是液晶屏加按键。菜单用树形结构组织:一级菜单是「运行状态 / 灌溉设置 / 参数校准 / 用户信息」,每级下面挂具体条目。中文显示要么用点阵字库烧进 Flash,要么用 GB2312 字库芯片。F103ZET6 的 512KB Flash 里划 256KB 放字库,剩下的放程序和参数区,够用。

界面刷新的坑在于不要在主循环里直接阻塞式刷屏,那样按键响应会迟。常见做法是主循环里跑一个 50ms 的界面状态机,只在数据变化超过阈值时才重绘对应区域,别整屏清屏重画,闪烁很明显。

后台跑在云平台上,接收 4G 上报的 JSON,存时序库,做大屏和报表。上报的数据模型建议固定成扁平结构,字段名用缩写但要有文档,别嵌套太深——农业现场的模组带宽和缓存都有限,一条报文超过 512 字节就可能被截断。

{ "dev": "IRR-0001", "ts": 1710000000, "v": 221.5, "i": 3.42, "p": 757.1, "e": 125.4, "flow": 128430, "mode": 2, "valve": 60, "alarm": 0 }

flow传的是脉冲累计值而不是换算后的升数,换算放到云端做,这样万一现场换了不同规格的水表,云端改个系数就行,不用重烧固件。alarm用位域表达,bit0 过压、bit1 过流、bit2 流量异常、bit3 余额不足,一位一个故障,扩展方便。

4. 核心代码落地:PWM 阀门控制与 ADC 电压电流采集两段可直接抄的实现

这一章把原料里那两段代码整理成能直接编进工程的样子。乱码字符已经清掉,函数签名和逻辑保持原样,只补了必要的边界处理。

4.1 PWM 初始化与开度设置:把定时器配置封装成可复用模块

原料里给的思路是两个函数加一层封装:PWM_Init负责通道配置,Set_Motor_Speed负责改占空比,Motor_Control把两者串起来。这个分法是对的,因为初始化只该跑一次,周期改占空比是高频操作,混在一起会反复调HAL_TIM_PWM_ConfigChannel,浪费时间还容易出状态错乱。

头文件先声明接口:

/* motor_control.h */ #ifndef MOTOR_CONTROL_H #define MOTOR_CONTROL_H #include "stm32f1xx_hal.h" void PWM_Init(TIM_HandleTypeDef *htim, uint32_t channel); void Set_Motor_Speed(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t speed); void Motor_Control(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t speed); #endif

实现文件:

/* motor_control.c */ #include "motor_control.h" /* 初始化指定定时器通道的 PWM 输出,默认占空比为 0 */ void PWM_Init(TIM_HandleTypeDef *htim, uint32_t channel) { TIM_OC_InitTypeDef sConfigOC = {0}; sConfigOC.OCMode = TIM_OCMODE_PWM1; /* 向上计数时 CNT<CCR 输出有效电平 */ sConfigOC.Pulse = 0; /* 初始占空比 0,阀门全关 */ sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim, &sConfigOC, channel); HAL_TIM_PWM_Start(htim, channel); } /* 设置占空比,speed 取值 0 ~ htim->Init.Period */ void Set_Motor_Speed(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t speed) { uint16_t max_speed = htim->Init.Period; if (speed > max_speed) { speed = max_speed; /* 超范围直接钳位,避免写进 CCR 溢出 */ } __HAL_TIM_SET_COMPARE(htim, channel, speed); } /* 一次完成初始化和设定,适合上电后立刻要动作的场景 */ void Motor_Control(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t speed) { PWM_Init(htim, channel); Set_Motor_Speed(htim, channel, speed); }

调用侧:

int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); /* CubeMX 生成,Prescaler=7199, Period=9999 */ /* 10kHz PWM,先给 50% 开度 */ Motor_Control(&htim2, TIM_CHANNEL_1, 5000); while (1) { /* 主循环 */ } }

Prescaler = 7199、Period = 9999这组参数的含义是:72MHz 时钟先除 7200 得到 10kHz 计数频率,再数 10000 个数溢出一次,所以 PWM 频率正好 10kHz。10kHz 对电磁阀驱动板来说偏高,常见的阀门驱动频率在 1~2kHz,用 CubeMX 把 Period 改成 35999 就降到 2kHz,代码不用动。

speed的取值范围是 0 到Period,也就是 0~9999。原料里注释写「单位为 1/10000 最大速度」,就是这个意思。想按百分比给参数,外面再包一层换算就行:

/* 传入 0~100 的开度百分比 */ void Valve_SetPercent(TIM_HandleTypeDef *htim, uint32_t ch, uint8_t percent) { if (percent > 100) percent = 100; Set_Motor_Speed(htim, ch, (uint16_t)(htim->Init.Period / 100u * percent)); }

这里有个顺序问题必须注意:HAL_TIM_PWM_ConfigChannel之前,定时器的时基必须已经初始化完(HAL_TIM_Base_Init+HAL_TIM_Base_Start)。CubeMX 生成的MX_TIM2_Init里两件事都做了,所以Motor_Control里再调一次PWM_Init是安全的。但如果是手写初始化、没调HAL_TIM_Base_Init就调PWM_Init,现象是引脚一直没波形,示波器量不到,查半天以为是 GPIO 复用配置错了。

4.2 ADC 轮询采样电压电流,换算成功率与电能

原料里的采样代码有两个问题:一是ScanConvMode = DISABLE却配了两个通道,这种情况下只有 Rank 1 的通道生效,电流那一路永远采不到;二是每轮循环都重新HAL_ADC_Start却没有HAL_ADC_Stop,连续转换模式下会一直采同一个通道。我按单通道轮转的写法重写一遍。

/* 电压电流采集,100ms 调用一次 */ #define V_REF 3.3f #define ADC_FULL 4096.0f #define V_DIVIDER 10.0f /* 分压比,按实际电阻网络改 */ #define I_SENS_V_A 0.05f /* 50mV/A */ #define SAMPLE_DT 0.1f /* 采样周期 100ms,单位秒 */ static uint16_t s_ad_v = 0, s_ad_i = 0; static float s_energy_ws = 0.0f; /* 累计电能,单位 W·s */ void Power_Sample_100ms(void) { /* 先采电压通道 */ HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { s_ad_v = HAL_ADC_GetValue(&hadc1); } /* 切到电流通道,重新配置后必须重新 Start */ ADC_ChannelConfTypeDef cfg = {0}; cfg.Channel = ADC_CHANNEL_1; cfg.Rank = ADC_REGULAR_RANK_1; cfg.SamplingTime = ADC_SAMPLETIME_13CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &cfg); HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { s_ad_i = HAL_ADC_GetValue(&hadc1); } /* 换算 */ float voltage = fabsf(s_ad_v * V_REF / ADC_FULL - 1.65f) * V_DIVIDER * 2.0f; float current = s_ad_i * V_REF / ADC_FULL / I_SENS_V_A; float power = voltage * current; s_energy_ws += power * SAMPLE_DT; }

fabsf(...) - 1.65f这一步是扣掉中点偏置。如果硬件上没有加偏置电路,而是用了真有效值转换芯片,这一步改成直接乘系数就行。* 2.0f是交流信号全波整流的补偿系数,只对取绝对值的近似算法有效。

累计电能单位是 W·s,上报前除以 3600000 换成 kWh:

float energy_kwh = s_energy_ws / 3600000.0f;

energy这类累计量有个坑:不能存在 MCU 的 RAM 里,掉电就清零,计量就废了。必须定期写进 Flash 或 EEPROM。F103ZET6 内部有 512KB Flash,划出一块 1KB 的页做参数区,每 5 分钟或者累计量变化超过阈值写一次。写入前先擦除整页,所以参数区里要放一个完整的参数结构体,别只存电量一个数,否则每次擦写都把那页的其他参数一起抹了。

typedef struct { uint32_t magic; /* 0x5A5A1234,用来判断是否首次上电 */ IrrigParam_t param; float energy_ws; uint32_t flow_total; uint32_t balance_cent; /* 余额,单位分 */ } ParamBlock_t;

上电时读第一块,magic不对就用默认值初始化并立刻回写;正常就加载进 RAM。这里最好做双备份,两页交替写,一页坏了另一页还能兜底。血泪经验是:单页方案在擦写过程中掉电,整页数据就没了,现场表现为「用着用着突然不计量了」,返修时查不出来。

4.3 把两段代码接进主循环

主循环不写业务逻辑,只做调度。用HAL_GetTick()做软定时,几个任务按不同周期跑:

int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ADC1_Init(); MX_TIM2_Init(); Param_Load(); uint32_t t_sample = 0, t_ctrl = 0, t_ui = 0, t_up = 0; while (1) { uint32_t now = HAL_GetTick(); if (now - t_sample >= 100) { t_sample = now; Power_Sample_100ms(); } if (now - t_ctrl >= 1000) { t_ctrl = now; Irrig_StateMachine_1s(); } if (now - t_ui >= 50) { t_ui = now; UI_Task_50ms(); } if (now - t_up >= 1000) { t_up = now; Upload_Task(); } /* 报警处理:任何一路超限立刻关阀 */ Alarm_Check(); } }

now - t_xxx >= 周期这种写法能扛住HAL_GetTick()溢出回绕,是嵌入式的标准套路。别写成if (HAL_GetTick() % 100 == 0),那种写法在循环耗时不定的时候会漏触发。

5. 联调避坑:灌溉控制器最容易翻车的五个地方

5.1 电流读出来永远是电压的一半

现象:串口打印出来的电流值和电压值成固定比例,换个负载比例不变。

原因:ScanConvMode = DISABLE时 ADC 只认 Rank 1 的通道,两个通道都配了但第二个根本没进转换序列。HAL_ADC_ConfigChannel返回 HAL_OK,看起来成功了,实际只是写进了一个不会被用到的配置。

解决:单 ADC 多通道要么开扫描模式加 DMA,要么老老实实每次切通道后重新HAL_ADC_Start。原料里那种写法属于后者的思路但漏了重新配置通道,按第 4.2 节的代码补上就行。

5.2 PWM 给 100 阀门全关,给 0 反而全开

现象:调试时给占空比 0 阀门不动,给满占空比反而关了。

原因:驱动板的光耦是反相接入的,或者OCPolarity配成了TIM_OCPOLARITY_LOW,高电平有效变成了低电平有效。

解决:先拿示波器量 MCU 引脚的实际波形,确认占空比和设定值一致,再查驱动板。极性不对就改OCPolarity,或者用一个inverted标志在Valve_SetPercent里取反,别去改硬件。

5.3 水泵启动瞬间 MCU 复位

现象:手动模式点「开泵」,屏幕闪一下,程序从头开始跑,参数恢复默认。

原因:继电器吸合加泵启动,瞬间电流冲击把 5V 或 3.3V 拉下来,低于 MCU 的 BOR 阈值触发复位。

解决:电源上加大电容缓冲,继电器线圈的反向二极管必须有,MCU 和继电器驱动之间加光耦隔离,启动时序上让继电器先吸合再开 PWM。另外检查Param_Load是不是在复位后把参数冲掉了——如果参数区没写 magic 就直接回默认值,那每次瞬时复位都等于恢复出厂,这个坑更隐蔽。

5.4 参数改一次 Flash 就磨损一次

现象:设备用了两三个月,参数区读出来全是 0xFF,设置全丢。

原因:每改一个参数就擦写一次 Flash 页,F103 的 Flash 擦写寿命典型值 1 万次,UI 上按一下键就写一次,很快就到寿命。

解决:参数改动只在 RAM 里生效,加一个「脏标记」,定时(比如 5 分钟)或者按键退出菜单时才落盘。落盘做双页交替,写之前校验。再进一步可以把参数区挪到外部 EEPROM,擦写寿命上百万次,成本多几毛钱。

5.5 4G 断网后预付费对不上账

现象:现场信号差,用户扫码充值了,本地余额没加,云平台显示已充值,两边打架。

原因:余额校验逻辑依赖云平台返回,MCU 拿到扫码结果后同步等云平台回包,等不到就丢弃,用户白付钱。

解决:改成异步对账。扫码结果带流水号,先写进本地待确认队列,本地按「预授信额度」临时放行(比如允许欠费 5 元),4G 恢复后按流水号批量上报、拉取对账结果,多退少补。流水号要落 Flash,断电不丢。

6. 二次开发改造:把写死的中文菜单改成可配置的参数体系

这套代码拿过来最想改的地方,通常是前端菜单和参数耦合得太死。IrrigParam_t里加一个字段,菜单代码、Flash 存储、4G 报文三处都得跟着改,改一处漏一处,现场就是参数显示对不上。我一般上来先做三件事。

第一件,把参数表做成数组驱动。每个参数一条记录,包含偏移、类型、上下限、显示名,菜单只负责遍历和渲染,加字段只改表:

typedef struct { uint16_t offset; uint8_t type; /* 0=u8 1=u16 2=u32 3=float */ float min, max; const char *name; } ParamItem_t; static const ParamItem_t g_param_tab[] = { { offsetof(IrrigParam_t, duration_min), 2, 1, 600, "喷灌时长(分)" }, { offsetof(IrrigParam_t, cycle_hours), 2, 1, 168, "周期间隔(时)" }, { offsetof(IrrigParam_t, th_temp_high), 3, 0, 60, "温度上限" }, };

配合offsetof和一个通用的读写函数,菜单、Flash、JSON 序列化全部复用同一张表。加字段只改结构体和表,其他代码不动。

第二件,把 4G 上报从裸 AT 指令改成 MQTT 客户端。原料的方案是往串口吐 JSON,模组透传。换成 MQTT 之后,主题按设备 ID 分,服务端下发指令和上报走同一条链路,云端能主动下发参数,不用等设备轮询。改的时候注意发布主题和订阅主题分开,QoS 用 1,本地保留一份未确认消息队列,掉线重连后补发。

第三件,加自校准流程。电压电流采样的系数(V_DIVIDER、I_SENS_V_A)不该写死在代码里,放到参数区,出厂时用标准表比对一次,把校准系数写进去。校准命令通过菜单触发,界面上引导「接入标准负载,按确认键采样 10 次取平均」,写完自动落盘。这一套做完,同一版固件能给不同量程的板子用。

验证方法我习惯分三步:先在实验台上用可调电源和电子负载跑满量程,看采样误差是不是在 2% 以内;再断网 24 小时,看数据队列会不会把 RAM 撑爆;最后拿一台设备反复上电断电 200 次,检查参数区和电量累计有没有丢。第三步最枯燥,但也最容易抓出 Flash 写入时序的问题。

从那以后我每次接这类计费型终端,都强制先画一张「数据在哪儿」的表——每个变量存在哪、掉电会不会丢、谁负责写、谁负责读——再动手写代码。这张表想清楚了,后面少一半的现场返修。希望帮到你。

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

返回列表