1. 为什么要维护这个ESP透传固件
JFirmwareESP这个名字,老玩家可能还记得。当年那批玩ESP8266做串口透传的人,很多都用过它。简单说,这个固件干的事情就是:设备通过串口发数据,固件负责连上WiFi把数据转发到指定服务器;反过来服务器下发的数据,再从网络侧转到串口。全程透明,用户感知不到中间还隔着一层网络协议。
这五年里物联网生态变化很大,新芯片、新SDK、新的开发方式层出不穷。但说实话,我一直没怎么动这个项目。一个重要的原因是:旧版本太稳了。不少做设备集成的朋友,把老固件刷进去后连续跑几年都没重启过一次。这类固件讲究的是"能不动就不动",因为业务方往往已经把它当成一个黑盒串口通道来用,上游下游都依赖它,随便改动带来的风险比收益大得多。
那为什么现在又决定更新?原因并不复杂:一是老固件对新一代ESP芯片支持不好,二是有些老问题在新硬件上会放大,三是一些使用者反馈的需求确实值得做进去。作为一个从零开始写这个固件的人,我觉得是时候把欠的技术债还一还了。这篇文章就聊聊这次更新背后的思路、改动细节,以及我在这个过程中踩到的一堆坑。
2. 老版本的核心架构回顾
2.1 当年的实现方式
五年前的方案其实非常朴素。基于早期的非OS SDK,代码用裸循环方式处理。主循环里做三件事:查串口有没有新数据、查网络有没有新数据、查WiFi连接状态。串口数据通过循环里的查询函数读取,收满一定字节数或者超过一个很短的间隔,就拼成一个网络包发出去。反过来,网络收到数据就直接往串口写。
这套逻辑今天看起来确实简陋,但它有几个特点在工业场景里非常受欢迎。首先是极低的内存占用,整个固件跑下来RAM占用很小一部分,对ESP8266这种资源紧张的芯片来说很友好。其次是没有复杂的调度机制,代码路径非常短,出问题容易定位。最后是延迟很低,只要串口有数据,几百微秒内就能转为网络包发出。
2.2 老用户为什么不愿意换
维护老固件期间收到最多的私信内容几乎都是同一类:你们这个固件稳定,别改,千万别改。做嵌入式的人应该都能理解这种心态。现场几十台设备都靠这个透传通道干活,一台设备要升级固件,就得有人去现场把设备拆开、连烧录器、刷完再验证,这个时间成本远高于固件本身的价值。
这也是为什么这次更新我给自己定了一个硬性要求:老版本能用的场景,新版本必须继续能用;老版本的配置方式,尽量向下兼容;老版本的串口行为,不能出现语义变化。对透传固件来说,串口行为就是生命线。比如你原来发0xAA会得到0xAA的应答,升级完了要是变成0xAA后面多了几个字节,下游设备可能直接判断通信异常。
2.3 旧固件到底积累了哪些问题
不用不代表没有问题,只是问题都被稳定运行的表象掩盖了。整理下来主要有四类:
第一类是WiFi断线的重连策略太粗暴。老版本检测到断线后只会无限重连,不区分是路由器重启还是AP信号消失,导致在信号弱的场景下反复作无用功。第二类是串口数据分包的策略太简单。固定间隔打包的方式在面对高速连续大数据流时效率不错,但在低速交互场景下经常把一帧完整数据切成两半,下游协议栈解析起来很痛苦。第三类是协议栈太老,对新的加密方式和网络环境兼容性差。第四类是配置项越来越多,但都用宏定义的方式写在代码里,普通用户改起来需要重新编译,门槛太高。
核心问题说白了就是,当年能用,但维护性和体验已经跟不上现在的场景了。
3. 这次大版本更新的设计思路
3.1 硬件平台从单一转向多平台
老版本最初只针对ESP8266,后来虽然适配了老款ESP32,但代码结构没有真正把平台差异剥离开。这次更新我花了很大精力做平台抽象层。所有与芯片寄存器、外设相关的代码都收进底层驱动文件,上层业务逻辑只关心串口收发、网络收发、配置读写这三个抽象接口。
这样做的直接收益是,当前主流的新款ESP32芯片可以原生适配,未来即使再有新芯片发布,迁移成本也会小很多。实际测试下来,同一套业务代码在新款芯片上编译几乎不用做修改,只调整底层驱动对应的管脚和时钟配置就能跑起来。
当然,这种做法也有代价。因为要兼顾不同芯片的硬件特性,代码体积比老版本大了一些。好在现在的ESP32系列芯片Flash普遍比五年前大不少,多出来的这点空间完全可以接受。
3.2 串口收发从查询模式改为中断模式
这是本次更新里让我最犹豫但又最坚定的一次改动。旧代码在主循环里查询串口缓冲,逻辑简单、时序可预测,但在高波特率下会频繁触发循环查询,导致CPU占用偏高。新版改成串口中断接收,数据到硬件层后立刻触发回调,不需要轮询,CPU占用显著降低。
中断模式最大的好处是响应快。115200波特率下,一个字节大约需要87微秒,之前查询模式下极端情况下会产生几毫秒的延迟,外部设备就会判断超时。现在中断回调在字节落地的瞬间就能执行,延迟被压缩到微秒级。
但中断模式也有经典的坑,就是中断回调里绝不能做耗时操作。我见过很多人在回调里直接处理协议解析、分配内存、甚至打印日志,这种写法在低负载时没事,高负载下直接导致中断溢出。我最初一版也有这个问题,后来把回调里全部改成只是塞入环形缓冲区,由主循环统一取出处理,才彻底解决。
3.3 网络传输加入协议栈和超时策略
老版本的网络传输只支持TCP客户端模式,连接断了就重连。这次更新加入了UDP模式和TCP服务端模式。UDP模式适合对实时性要求高但可以容忍个别丢包的场景,比如传感器数据上报;TCP服务端模式适合让上位机主动连接设备,方便局域网内调试。
超时策略也做了细化。断线重连从单一固定间隔改成了自适应重连策略:如果短时间内连续失败,重连间隔会逐步拉长,避免在网络异常时疯狂发包导致模块发热和设备端服务器被频繁打扰;一旦重连成功,间隔立刻复位到初始值。
另外还增加了网线级的保活机制,TCP连接空闲超过设定时间会自动发送心跳包,防止路由器或对端服务器因为长时间无数据而回收连接。
3.4 配置方式改为网页化
这是这次改动中带来好评最多的一项。以前改参数要么重新编译,要么通过串口敲AT指令。编译方式对普通用户太不友好,AT指令又要记命令格式,还容易敲错。
新版内置了一个轻量级配置网页。设备上电后长按某个按键进入配置模式,自身发出一个热点,电脑或手机连上后浏览器打开固定IP就能进入配置页。WiFi账号密码、服务器地址、端口、串口波特率、打包间隔这些参数全部可视化填写,点保存就写入Flash,重启即生效。
为了让网站风格尽可能轻量,我手写了整个前端页面,没有引入任何重量级框架。整包资源加起来也就几十KB,放到Flash里几乎不占空间。
4. 更新过程中踩过的坑与解决方案
4.1 SDK大版本迁移的连锁反应
五年前的旧固件用的是早期SDK,如今新的官方SDK在底层架构上有了很大调整。刚开始我天真地以为把API调用的名字改一改就能编译通过,现实给了我狠狠一击。
首先是定时器API从传统的回调注册方式改成了更复杂的软硬件定时器抽象层,接口参数变了不少。其次是WiFi事件处理机制变化很大,原来是轮询系统事件状态,现在改成了事件驱动的注册回调模式,很多关于连接、断线、获取IP的状态判断逻辑都要重写。
最让我头疼的是内存管理的变化。老SDK允许你自由地申请和释放内存,新SDK在内存泄漏检查和堆碎片整理上要求更严格,而且不同芯片的堆空间差异很大。原先在ESP8266上跑得很好的内存策略,放到另一块芯片上就会出现莫名其妙的复位。
我的解决思路是尽可能减少动态内存分配。所有网络缓冲区在初始化时就按最大报文长度一次性分配好,运行过程中只复用不新增。串口缓冲区也一样,环形缓冲提前申请固定大小,不随着数据量变化而扩容。
4.2 新硬件与老设计的兼容问题
新芯片和老固件之间最明显的差异是管脚映射。老ESP8266的串口引脚是固定的,但新芯片支持任意GPIO做串口,这本来是个好事,但也带来了问题:默认配置里我沿用老引脚的编号,结果用户刷机后串口完全没有输出。
排查下来发现是新芯片默认启动时某些引脚电平状态和老芯片不一样,甚至有的引脚在有外部下拉电阻的情况下被内部逻辑强制拉高。解决办法是在配置中增加管脚映射选项,同时把默认值设为一组经过实测的兼容组合。
还有一个坑是Flash布局。老固件用固定分区表,新芯片的分区策略和容量都不同,如果直接写入容易覆盖配置区。我在分区表里专门割出一块独立的配置存储区,保证固件升级不会误擦配置,这也解决了过去偶尔出现的"升级后设置全丢"的问题。
4.3 固件现场升级的容错设计
透传固件有个特殊场景:设备往往部署在不好去的位置。老版本升级需要拆设备接线,这次更新我加入了OTA升级通道。固件通过网页配置页上传,或者通过网络端口直接推送,设备收到完整固件后先做校验再写入A分区,写入失败还能从B分区回滚。
不过OTA设计上有个很隐蔽的坑。因为透传设备本质上没有标准的应用协议,服务器下发什么数据都会被当成业务数据转发到串口。如果我直接复用这个透传通道传固件,很可能出现固件字节流和业务数据混淆的问题。解决方式是OTA升级启用一个独立的端口和专用的协议格式,并且由上位机先发送特定密钥码来握手,避免误触发。
4.4 老的硬件版本还需要照顾
更新期间也收到了另一个方向的反馈,有一些老硬件用户希望固件也能适配旧设备模块,因为他们手里库存还有很大一批这样的芯片没用完。
不能不管这批老用户。最终方案是保持双版本发布策略:经典版本继续基于老SDK维护,修复严重bug但不增加大功能;新版基于新SDK开发,支持新硬件,也支持老硬件上能跑的几种型号。编译时通过配置宏切换平台,复用上层业务代码,底层驱动各自独立。
这种方式工作量会翻倍,但对这个项目来说值得。因为很多老设备还在稳定运行,用得好好的场景没必要强迁到新固件,老版本作为一个长期维护分支存在,反而是对整个生态最负责任的选择。
5. 实际使用效果与配置推荐
5.1 实际测试环境数据
我搭建了一套对照测试环境来验证这次更新的实际效果。测试设备是几款主流的ESP系列开发板,对端服务器跑在内网的一台虚拟机上。串口侧用USB转串口工具连着一台模拟数据发生器,持续以不同速率发送数据包,同时记录网络侧到达服务器的字节数和时间戳。
在115200波特率、20字节一包、每包间隔50毫秒的场景下,老版本的平均每包延迟大约在4到6毫秒,新版本可以稳定在2到3毫秒。在连续1KB大块数据、最高速率发送的场景下,新旧版本的吞吐量差距更明显,新版本因为中断机制减少了CPU轮询开销,吞吐量大约提升了35%左右。
网络异常场景下的表现也做了一次对比。模拟路由器重启,老版本大概需要7秒左右恢复连接并重新建立数据通道,新版本在快策略下大约2秒就能恢复,在慢策略下也不会超过5秒。对注重稳定连接的工业场景来说,这个提升感知是很明显的。
5.2 推荐配置与适用场景
根据这几轮测试结果,我整理了一套配置建议。常规透传场景建议保持TCP客户端模式,打包间隔设置20毫秒到50毫秒,这样既能保证实时性又不至于产生太多小包;而传感器上报之类不要求逐包响应的场景可以切到UDP模式,打包间隔稍微拉大,能把网络负载降得很低。
针对低速控制类协议(比如几条字节一帧的ModbusRTU或自定义协议),有个经验是把打包间隔调成协议一帧的发送总时长再加一点余量。这样固件可以等到完整一帧数据全部收完才发包,下游不用处理拆包重组的逻辑。
对于WiFi射频环境复杂的工厂场景,建议开启保活机制并把保活间隔设置在30秒左右。保活太频繁会增加无谓流量,间隔太长则容易被路由器踢掉。这个值有用户实测过,30秒在大多数商用路由器上是安全区间。
5.3 固件升级迁移注意事项
如果你手里正在用经典版本的设备,想升级到新版固件,有几个点必须提前做好验证。先备份好当前设备的网络配置和服务器参数,因为在跨大版本时我建议直接恢复出厂设置再重新配置,避免旧版写入的配置数据结构和新版不兼容导致读取异常。
刷完固件后的第一件事是接串口看日志。新版固件启动时会打印当前运行模式、WiFi连接状态、配置项摘要,如果日志里出现配置错误提示,说明分区结构或Flash地址还需要调整。
另外在切换串口波特率前,先用默认值连接并确认设备在线,再通过网页配置改成目标波特率保存重启。我见过不少用户直接改完重启结果串口对不上,又得重新恢复默认模式,多绕了一圈。
6. 新固件的一些后续扩展想法
更新到这一步,主版本确实是可以交付的状态了。但我心里清楚,这个项目还远没有结束。谈谈我接下来的一些思路。
自动发现能力是透传设备很实用的一项。新版本其实已经在UDP广播里加入了设备型号和设备名的自描述字段,后续可以做成一个上位机自动扫描工具,局域网里一搜就能列出所有在线设备,免去一台台查IP的麻烦。对接现场调试和批量部署来说,这个功能价值很大。
还有一方面是远程状态监控。很多用户希望知道设备在运行过程中有没有出现断线重连、数据丢包、缓冲区溢出这些隐性故障。但透传固件本身不应该把这些诊断信息混进业务数据里,后续我准备在诊断日志中单独开放一个通道,通过配置开关决定是否输出以及输出到哪里。
另一个想法是把串口转发做成多通道。目前一个设备只能建立一个网络连接,发给串口的数据只能走同一条链路。如果支持多路连接,就可以做到一台上位机管理多个设备,对串口服务器的应用场景很有帮助。不过多路复用也会带来数据仲裁的复杂度,很多细节还得仔细设计。
好在有了这次的平台抽象层作为基础,后续这些方向做起来会顺很多。毕竟架构理顺了,功能只是往上叠加的事情。
7. 最后分享一点维护老项目的体会
这个项目从第一次发布到现在,隔了这么多年还能被用户惦记着要更新,说实话我挺意外的。很多做开源固件的人都会遇到类似的问题:项目发布了,社区用了,然后呢?如果你的项目稳定得让人忘了它的存在,这其实是最高评价;但如果你就此不再管它,总有一天会有人需要你改点什么,而你已经无从下手了。
这次更新给我最大的教训就是:写固件的时候,一定要为五年后的自己留后路。平台抽象别嫌多写几层,配置管理别只图省事用宏定义,即使当时用不到的特性也要在结构上留好位置。你不一定会再碰这个项目,但这些代码将来一定会有人接手,或者就是你自己回来接手。
说实话,重启一个五年没动的项目,付出的成本比预计的多很多。每个坑都得自己踩一遍,每个看似简单的功能都得重新验证,但把老固件里那些积累下来的问题一个个消化掉,再把新固件交给真正需要它的人,这本身就是做这个项目最有意思的地方。希望这次的更新能让它在下一个五年里继续稳定跑下去,也希望各位使用者在部署前都留足测试时间,毕竟透传固件这件事,稳,永远比快重要。