☰
以太网温湿度采集节点:断线重连与断点续传的可靠传输方案
2026/9/29 23:56:42 网站建设 项目流程

1. 项目缘起与整体设计思路

温湿度采集这件事,单机跑通不难,难的是把它扔到现场机柜里、接上以太网、连续跑上几个月甚至一年。我自己做过好几个环境监控类的项目,从食品冷链仓库到数据中心机房,再到农业大棚的分布式监测,几乎每一个项目在验收之后都会遇到同一个问题:网络不是永远稳定的。交换机重启、网线被老鼠咬断、上位机软件崩溃、路由器DHCP租约到期,这些事在现场发生的频率远比实验室里高得多。

这个项目的核心目标很明确:用带以太网接口的温湿度采集节点,把数据稳定地送到上位机或云端,并且在网络中断时做到两件事——断线自动重连和断点续传。前者保证通信链路恢复后能自动接上,不需要人到现场按复位键;后者保证中断期间采集到的数据不会丢,网络恢复后能补传上去。

为什么要把这两个机制放在一起讲?因为它们解决的是同一个问题的两个阶段。断线重连解决的是“链路层和会话层”的恢复问题,断点续传解决的是“应用层数据完整性”的问题。只做重连不做续传,网络恢复后中间那段数据就是空洞;只做续传不做重连,链路断了根本传不出去,续传也无从谈起。两者必须配合设计,才能构成一个完整的可靠传输方案。

在方案选型上,我考虑过几种常见路线。一种是采集节点本地只做透传,所有缓存和重传逻辑放在上位机;另一种是节点本身带本地存储和协议栈,自己管理缓存队列。前者的好处是节点固件简单、成本低,缺点是上位机必须一直在线,而且一旦上位机重启,缓存状态就丢了。后者对节点资源要求高一些,但可靠性明显更好,因为数据在采集端就有落盘保障。这个项目我选择的是节点侧缓存+多协议上报的方案,节点用带以太网MAC和PHY的MCU,外挂一颗SPI Flash做本地数据缓存,同时支持MQTT和Modbus TCP两种上报协议,上位机可以根据场景选择。

多协议这个点也值得说一下。很多项目一开始只定一种协议,比如Modbus TCP,因为组态软件对接方便。但实际部署中你会发现,有些客户希望数据直接上云,用MQTT更合适;有些客户坚持用SCADA系统,Modbus TCP是刚需。如果节点固件只支持一种,后期改需求就要重新烧录甚至换硬件。所以我在设计时把协议层做成了可配置的,节点启动时读取配置区,决定用哪种协议上报,甚至支持双协议并行——本地SCADA走Modbus TCP,云端走MQTT,互不干扰。

整个系统的数据流大致是这样的:温湿度传感器(比如SHT30或AM2301)通过I2C或单总线接到MCU,MCU周期性采集,给每条数据打上时间戳和序列号,先写入本地环形缓存区,然后尝试通过以太网发送。发送成功则标记该条已上传,发送失败或超时则保留在缓存中,等待下次重连后按序列号顺序补传。这个“先缓存后发送”的顺序很关键,它保证了即使发送瞬间断网,数据也不会丢。

注意:本地缓存一定要用环形缓冲区加磨损均衡,SPI Flash的擦写次数有限,如果每次都从头写,几个月就能把前几个扇区写坏。我一般把缓存分成若干扇区轮转写,每个扇区写满后擦除最旧的扇区再复用。

2. 以太网通信底层与多协议适配细节

2.1 以太网硬件选型与电平匹配

以太网采集节点和普通串口节点最大的区别就在物理层。串口你只要接对TX、RX、GND就能通,以太网要考虑的东西多得多。首先是MAC和PHY的选择。我用的MCU自带以太网MAC控制器,外接一颗PHY芯片,比如LAN8720或DP83848。这两颗都是工业现场常用的,LAN8720便宜、外围简单,DP83848抗干扰更好一些,适合电磁环境复杂的机房。

PHY和MCU之间的接口是RMII,需要一根50MHz的参考时钟。这个时钟可以由MCU提供,也可以由PHY自己产生,具体看选型。我踩过的一个坑是:有些PHY芯片默认输出时钟,有些默认输入,如果配置反了,RMII通信直接不通,现象是PHY能识别到链路但MCU收不到包。解决办法是查PHY的数据手册,确认时钟方向,然后在硬件上把对应的电阻跳线改掉。

网络变压器和RJ45接口也不能省。有些低成本方案直接用PHY差分线接RJ45,省掉变压器,实验室能通,但到了现场,雷击浪涌、共模干扰一来,PHY很容易挂。我现在的做法是变压器和共模电感都加上,RJ45用带屏蔽的,外壳接地。虽然成本多了几块钱,但现场返修率明显下降。

电平标准方面,RMII是3.3V LVCMOS,MDIO和MDC也是3.3V。如果MCU是1.8V核心电压,IO口要确认是否兼容3.3V,不兼容就要加电平转换。这个细节在原理图评审时一定要确认,否则板子回来发现MDIO通信不上,查半天以为是软件问题,其实是电平不匹配。

2.2 多协议栈的裁剪与共存

节点上跑多协议,资源分配是个现实问题。MQTT协议栈如果用完整的Paho,代码体积和RAM占用都不小;Modbus TCP相对轻量,但也要维护TCP连接和寄存器映射。我的做法是:底层共用LwIP协议栈,TCP/IP部分只维护一套,上层MQTT和Modbus TCP各自独立,通过配置决定启用哪个。

LwIP的配置有几个关键参数。MEM_SIZE要根据并发连接数调整,如果同时开MQTT和Modbus TCP,再加上一个用于配置的HTTP服务,至少留8KB以上的堆。TCP_SND_BUF和TCP_WND影响吞吐,温湿度数据量小,默认值就够,但如果要传历史缓存的大批量数据,建议把TCP_SND_BUF调到4KB以上,否则补传时会很慢。

MQTT的保活机制和断线重连是绑在一起的。我一般把keepalive设成30秒,节点每15秒发一次PINGREQ。如果连续两次没收到PINGRESP,就判定连接断开,主动关闭socket并触发重连。这里有个细节:MQTT的clean session标志要设成0,这样broker会保留订阅关系和未确认消息,节点重连后能继续收到QoS 1的消息。如果设成1,每次重连都是全新会话,断点续传的上下文就丢了。

Modbus TCP这边,节点作为服务器,上位机作为客户端。断线重连主要靠上位机主动重连,节点侧要做的是:当TCP连接断开时,不销毁寄存器映射,保持数据缓存继续采集,等新连接建立后,上位机可以按寄存器地址读取缓存区。我通常会在寄存器映射里留一段“缓存索引区”,上位机通过读这个区域知道当前缓存里有多少条未上传数据,然后逐条读取。

提示:Modbus TCP的端口默认是502,但有些现场交换机会封这个端口,部署前最好确认一下。如果502被封,可以改成其他端口,比如1502,上位机同步改配置即可。

2.3 协议切换的配置管理

多协议节点最怕的是配置丢失。如果配置存在RAM里,断电就没了,现场维护很麻烦。我的做法是在SPI Flash里划一个独立的配置扇区,存协议类型、服务器地址、端口、采集周期、上报周期这些参数。节点启动时先读配置,再初始化对应协议栈。

配置的写入要加校验,我一般用CRC32加一个魔数头。如果校验失败,就加载默认配置,同时通过串口打印一条告警。这样即使配置区被意外擦除,节点也能以默认参数启动,不会变砖。

协议切换可以通过两种方式触发:一种是本地按键,长按进入配置模式;另一种是通过网络下发配置命令。网络下发更方便,但前提是当前协议能通。如果当前协议配置错了导致连不上,就只能靠本地按键恢复。所以本地按键这个“后门”一定要留,我见过太多项目为了省一个按键,结果现场配置错了只能拆机重新烧录。

3. 断线重连机制的核心实现

3.1 链路检测与重连状态机

断线重连不是简单地“断了就重连”,那样在网络抖动时会疯狂重连,反而加重网络负担。我设计的是一个带退避的状态机,状态包括:LINK_DOWN、LINK_UP、CONNECTING、CONNECTED、BACKOFF。

链路检测分两层。第一层是PHY层,通过读取PHY的状态寄存器判断网线是否插入、链路是否建立。这个检测很快,PHY芯片一般有中断引脚,链路变化时直接触发MCU中断。第二层是应用层,通过TCP keepalive或MQTT心跳判断对端是否可达。两层都正常才认为连接健康。

重连退避策略我用的是指数退避加随机抖动。第一次断线后等1秒重连,失败等2秒,再失败等4秒,最大到60秒。每次等待时间加上0到1秒的随机抖动,避免多个节点同时重连造成网络拥塞。这个策略在几十个节点的现场验证过,网络恢复后节点会陆续连上,不会一拥而上。

状态机的代码结构我一般用一个switch-case放在主循环里,每个状态做一件事,不阻塞。比如CONNECTING状态下调用非阻塞的connect(),然后检查返回值,成功就切到CONNECTED,失败就切到BACKOFF并设置下次重连时间。这样整个重连过程不影响采集任务,采集一直在后台跑。

3.2 TCP连接保活与异常断开处理

TCP连接有个特点:网线拔掉后,如果没有任何数据交互,协议栈可能很久才发现连接断了。默认的TCP keepalive时间通常是2小时,这对现场来说太长了。我的做法是在应用层自己做保活,MQTT用PINGREQ,Modbus TCP用定期读寄存器的方式。

对于Modbus TCP,如果节点是服务器,上位机是客户端,节点侧其实不太好主动发数据。这种情况下我让节点在TCP层开启keepalive,把TCP_KEEPIDLE设成30秒,TCP_KEEPINTVL设成10秒,TCP_KEEPCNT设成3次。这样30秒没数据就开始探测,连续3次没响应就关闭连接。LwIP里这几个参数可以通过socket选项设置,具体是SO_KEEPALIVE配合TCP_KEEPALIVE相关宏。

异常断开的处理要区分几种情况。一种是网线拔掉,PHY中断会触发,状态机直接切到LINK_DOWN,关闭所有socket,等链路恢复后重新连接。另一种是对端主动关闭,recv()返回0,这时候要关闭本地socket并触发重连。还有一种是发送超时,send()返回错误,这种情况我一般先重试一次,再失败就关闭连接重连。

注意:关闭socket后一定要把相关的资源释放干净,包括MQTT的会话状态、Modbus的连接句柄。我见过一个bug,重连时没有释放旧的socket,导致文件描述符耗尽,最后节点再也连不上,只能重启。后来我在重连前加了一个强制清理函数,确保旧连接彻底关闭。

3.3 重连后的会话恢复

重连成功只是第一步,会话恢复才是关键。MQTT这边,如果clean session是0,broker会保留会话,节点重连后不需要重新订阅,直接继续收发即可。但要注意,QoS 1的消息在重连后可能会重复推送,节点侧要做去重,我一般用消息ID加时间戳来判断。

Modbus TCP这边没有会话概念,每次连接都是独立的。但节点侧要维护一个“当前上传位置”的指针,记录上次传到哪条数据了。这个指针存在RAM里,重连后继续从指针位置往后传。如果节点重启了,指针丢失,就从缓存区最旧的一条开始传,上位机根据序列号去重。

这里有个设计取舍:指针要不要持久化到Flash?持久化的话,每次上传都要写Flash,寿命受不了;不持久化的话,节点重启后会重复上传。我的选择是不持久化,因为温湿度数据重复上传的代价很小,上位机去重即可,而Flash寿命是实打实的成本。如果数据量特别大,可以考虑每隔N条持久化一次指针,减少写入次数。

4. 断点续传的缓存管理与补传策略

4.1 本地环形缓存区的设计

断点续传的基础是本地缓存。我在SPI Flash上划出一块区域做环形缓冲区,每条记录包含:序列号(4字节)、时间戳(4字节)、温度(2字节)、湿度(2字节)、CRC校验(2字节),一共14字节。为了对齐方便,我实际用16字节一条。

缓存区大小取决于Flash容量和断网时长。假设采集周期是1分钟,断网最长按7天算,那就是10080条,乘以16字节约161KB。我一般留256KB给数据缓存,足够覆盖大多数场景。如果采集周期更短,比如10秒一次,那就要相应扩大缓存区,或者缩短断网容忍时间。

环形缓冲区的管理用读指针和写指针。写指针在采集时推进,读指针在上传成功时推进。当写指针追上读指针时,说明缓存满了,这时候有两种策略:覆盖最旧数据,或者停止采集。我选择覆盖最旧数据,因为温湿度是连续监测,最新的数据通常比最旧的数据更有价值。覆盖时读指针也跟着推进,保证不会读到已被覆盖的数据。

Flash的磨损均衡通过扇区轮转实现。我把缓存区分成16个扇区,每个扇区4KB,写满一个扇区后擦除下一个要用的扇区,而不是每次都擦同一个。这样每个扇区的擦写次数均匀分布,寿命能延长十几倍。

4.2 补传的触发条件与顺序控制

补传不是一重连就疯狂发,那样可能把当前实时数据都堵住了。我的策略是:重连成功后,先恢复实时上报,然后以较低的优先级在后台补传历史数据。具体做法是在主循环里,每次实时上报完成后,如果缓存里还有未上传数据,就补传一条。这样实时数据的延迟不受影响,历史数据慢慢补。

补传的顺序必须是从旧到新,按序列号递增。因为上位机可能依赖时间顺序做趋势分析,如果乱序到达,曲线会跳来跳去。我在缓存记录里存了序列号,补传时从读指针开始,逐条读取并发送,收到确认后再推进读指针。

如果补传过程中再次断网,读指针保持不动,等下次重连后从同一位置继续。这样就实现了真正的“断点续传”——不管断多少次,每次都是从上次中断的地方继续,不会重复也不会遗漏。

补传的速度控制也很重要。如果补传太快,可能占满带宽,影响实时数据。我一般限制补传的速率为实时上报速率的2到3倍,比如实时1分钟一条,补传就控制在20到30秒一条。这样既不会太慢,也不会把网络打满。

4.3 数据去重与完整性校验

断点续传有一个绕不开的问题:重复。因为确认机制可能丢失,节点可能认为某条没发成功而重发,上位机就收到了两条一样的。解决办法是在数据里带序列号,上位机维护一个已接收序列号的窗口,收到重复的就丢弃。

序列号我用的是32位循环计数,从0开始,到0xFFFFFFFF后回绕。上位机判断重复时不能简单比较大小,要用滑动窗口。比如维护一个最近1000条的位图,收到序列号后查位图,如果在窗口内且已置位,就判定重复;如果序列号比窗口最旧的还旧,也判定重复;否则接受并更新位图。

完整性校验用CRC16,覆盖序列号、时间戳、温湿度数据。上位机收到后重新计算CRC,不一致就丢弃并请求重传。重传请求可以通过MQTT的QoS 1机制自动完成,Modbus TCP这边则需要上位机主动读缓存索引,发现缺号后重新读取对应寄存器。

提示:时间戳建议用Unix时间戳,节点启动时如果还没同步RTC,可以先用一个递增的计数器代替,等NTP同步后再切换。上位机对没有真实时间戳的数据要标记出来,避免趋势分析时误判。

5. 现场部署中的常见问题与排查实录

5.1 网络不通的排查思路

现场最常遇到的问题就是“连不上”。我的排查顺序是:先看PHY链路状态,再看IP层,再看TCP层,最后看应用层。

PHY链路状态可以通过读PHY寄存器或者看板子上的Link/Act指示灯。如果灯不亮,说明物理层没通,检查网线、交换机端口、RJ45焊接。如果灯亮但MCU收不到包,检查RMII时钟和MDIO配置。

IP层用ping测试。如果ping不通,检查IP地址、子网掩码、网关是否和上位机在同一网段。我遇到过好几次是IP冲突,节点和现场另一台设备用了同一个IP,ping时通时不通。解决办法是部署前用ARP扫描确认IP未被占用。

TCP层用telnet或nc测试端口。如果端口不通,检查防火墙、交换机ACL、节点上的socket是否正常监听。应用层的问题一般看日志,节点侧通过串口打印连接状态和错误码,上位机侧看MQTT broker或Modbus客户端的日志。

5.2 断线重连失效的典型原因

重连失效我遇到过几种情况。一种是退避时间设得太长,比如最大退避到10分钟,现场等不及以为坏了。后来我把最大退避改成60秒,兼顾了网络保护和恢复速度。

另一种是socket资源泄漏。前面提过,重连时没关旧socket,几次之后文件描述符耗尽。解决办法是在重连函数入口先调用清理函数,强制关闭所有相关socket。

还有一种是PHY中断丢失。有些PHY芯片的中断是电平触发,如果MCU没有及时清除中断标志,后续中断就丢了。我改成轮询加中断结合的方式,中断用于快速响应,轮询作为兜底,每秒钟读一次PHY状态寄存器。

5.3 断点续传数据丢失的排查

数据丢失一般查三个地方:缓存写入、缓存读取、上传确认。

缓存写入丢失通常是Flash写失败。SPI Flash写之前要先擦除,如果擦除没完成就写,数据会错。我在写之前加了一个等待擦除完成的循环,并且写完后回读校验,不一致就重写。

缓存读取丢失可能是读指针和写指针的并发问题。如果采集任务和上传任务在不同线程,指针操作要加锁。我一般用临界区保护,或者把采集和上传放在同一个任务里,用状态机调度,避免并发。

上传确认丢失是协议层的问题。MQTT QoS 0没有确认,发了就不管,断网就丢。所以断点续传场景必须用QoS 1。Modbus TCP这边,上位机读完寄存器后要显式写一个确认寄存器,节点收到确认才推进读指针。如果上位机没写确认,节点就认为没传成功,下次继续传。

5.4 常见问题速查表

现象可能原因排查方法解决措施
Link灯不亮网线坏、交换机端口坏、RJ45虚焊换网线、换端口、万用表测通断修复硬件
Ping不通IP冲突、网段不对、防火墙ARP扫描、检查配置改IP、关防火墙
TCP连不上端口被封、socket未监听telnet测试、看节点日志换端口、修复监听
频繁重连网络抖动、退避太短抓包看重连间隔加长退避、加抖动
数据重复确认丢失、序列号回绕查上位机去重逻辑加滑动窗口去重
数据丢失Flash写失败、指针并发回读校验、加锁重写、临界区保护
补传太慢补传速率限制太低看补传间隔调整速率
补传影响实时补传优先级太高看实时数据延迟降低补传优先级

6. 实操心得与后续扩展方向

这个项目从第一版到现在,前后改了七八次固件,现场跑了两年多,积累了一些文档里不会写的经验。

第一个心得是:缓存区大小要按最坏情况设计,但补传速率要按最好情况控制。缓存区宁可大一点,Flash成本不高,但数据丢了就是事故。补传速率则要保守,现场网络带宽往往比实验室紧张,补传太猛会把实时数据挤掉。

第二个心得是:状态机的每个状态都要有超时。比如CONNECTING状态如果卡住,要有超时机制强制切到BACKOFF。我早期版本没加超时,结果一次TCP连接卡在半开状态,节点就一直等,再也不重连了。后来每个状态都加了超时定时器,问题再没出现过。

第三个心得是:日志要分级,现场能关掉debug日志。调试时打印越多越好,但现场跑起来,串口打印会拖慢主循环。我一般设成运行时可通过配置切换日志级别,默认只打错误和警告,需要排查时再开debug。

后续扩展的话,有几个方向可以考虑。一是加NTP时间同步,让时间戳更准确;二是加TLS加密,虽然温湿度数据敏感度不高,但有些客户有合规要求;三是加边缘计算,节点本地做简单的阈值判断和告警,减少上传数据量。这些都可以在现有框架上叠加,不需要大改。

最后分享一个小技巧:节点出厂前做一次断网压力测试。拔掉网线跑24小时,再插上,看数据是否完整补传。这个测试能提前发现大部分缓存和续传的bug,比在现场出问题再排查成本低得多。我现在的产测流程里,这一项是必做的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询