ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

EsDA实现Modbus RTU转UDP:边缘网关协议转换实战指南

EsDA实现Modbus RTU转UDP:边缘网关协议转换实战指南 1. 项目整体思路拆解1.1 应用场景与痛点分析先说说我为什么会做这个项目这会帮你更好判断它适不适合你。工厂里经常有这样的画面车间底层跑着一堆老旧设备PLC、温控表、电表、变频器全是RS485串口走Modbus RTU协议。中控室想上云、想接MES、想做数据大屏但这些设备根本没有网口更谈不上MQTT、HTTP这些以太网协议。要换设备吗一台高精度温控表好几千整条产线换下来是笔大开销而且很多进口设备的通信协议是定死的你想改也改不了。传统做法是加一个DTU数据传输单元把串口数据透传到云端但这种方案有个问题DTU一般做的是透传它不理解Modbus RTU协议。你拿到云端的数据后还是要自己拆报文、解析寄存器、处理字节序全套逻辑都得在服务器或者边缘计算节点里再写一遍。如果下游设备数量多、协议复杂云端要处理的脏活累活更多运维成本直接起飞。而EsDAEmbedded Smart Design Automation嵌入式智能设计自动化平台解决的就是这个问题。它可以直接在边缘侧把Modbus RTU Master的报文解析成标准的结构化数据再以UDP协议打包发给上位机或云平台。简单来说它就是帮你把串口老设备和IP网络之间的那一层翻译官角色在图形化工具里做完不再需要写一堆C代码。我这次做的具体项目就是用EsDA实现一个Modbus RTU Master主动轮询挂在RS485总线上的多个从站设备读取它们的寄存器数据解析后通过UDP Client发送给上层的监控服务器。1.2 为什么选择EsDA而不是传统嵌入式开发很多工程师的第一反应是这不就是一个串口转网口的网关嘛用STM32加一个以太网模块自己写协议栈不就行了我确实也这么干过但实测下来用传统方式开发这套功能有几道坎特别浪费精力。第一Modbus RTU Master逻辑说简单也简单说复杂也复杂。轮询多个从站、处理超时重试、校验CRC16、解析异常码、处理响应帧错位这些逻辑在单线程裸机环境下很脆弱一个边界条件没考虑好整个轮询就卡死了。第二UDP以太网协议栈在MCU上做要么用lwIP这种第三方库要么用厂商SDK调试网络通信本身就是个深坑。第三最扎心的是这类项目往往需求不稳定今天读3个寄存器明天加5个后天又要改上报周期固件每次都要重新编译烧录。EsDA本质上是个图形化低代码开发工具它把MCU上的通信逻辑封装成了一个个功能节点你不需要关心底层中断、DMA、协议栈这些细节只需要拖拽节点、连线、填参数然后把业务逻辑用轻量级的Lua脚本串起来。改数据点、改上报周期、增加设备数量基本上改完配置就能重新生成固件不用动C代码。这个平台的另一大优势是它天生为IoT边缘场景设计串口、Wi-Fi、以太网、MQTT、TCP、UDP、HTTP这些常用通信方式都是内置节点。做我这个项目本质上就是拖几个节点出来连上线再写一小段胶水逻辑开发效率和可维护性比裸写C代码高一个数量级。1.3 整体架构与数据流向在动手之前先把数据流向画清楚。这个项目的架构分四层设备层下位机是各种Modbus RTU从站设备挂在RS485总线上。这里要注意Modbus RTU是主从协议总线上必须有且只有一个Master其余全是SlaveSlave之间不能互相通信。边缘网关层EsDA网关设备支持RS485串口以太网或Wi-Fi作为Modbus RTU Master按轮询表逐个向从站发送读保持寄存器03或读输入寄存器04请求。协议转换层EsDA把从站返回的原始报文按从站地址、功能码、寄存器地址解析成实际工程值比如温度25.3℃、压力1.2MPa通过Lua脚本格式化成自定义UDP报文。上位机层UDP Server监控PC或云服务器接收报文还原数据做显示、存储、告警。我用的EsDA硬件是一个带RS485接口和以太网口的边缘网关具体型号这里不多讲大家用自己手头的EsDA开发板就行关键是它要同时拥有UART连接RS485收发器和以太网/Wi-Fi模块。2. 通信机制与数据帧设计2.1 Modbus RTU与UDP的翻译逻辑要写好这个转换逻辑你得先搞清楚两种协议的语言习惯差异在哪。Modbus RTU走的是串行链路数据是一帧一帧按顺序发的。每一帧包含从站地址1字节、功能码1字节、数据区N字节、CRC16校验2字节低字节在前。它是面向字节流强校验的协议对时序要求严格要求帧间隔必须小于等于1.5个字符时间否则从站会把一帧拆成两帧来处理。UDP走的是IP网络数据是打包丢出去的报文体面向无连接。它不关心对端是否收到不保证顺序不重传好处是实时性好、开销小、实现简单。对于工业数据采集这种周期性上报场景UDP丢个一两包没关系下一包很快就来了反而比TCP更实用。所以这个项目的核心就是用UDP的无状态、轻量来承载Modbus的有序、可靠数据。EsDA负责在末尾侧完成拆包、解析、重组、编码最后把一份干净的JSON或自定义二进制报文从网络发出去。上位机不需要关心Modbus协议细节只需要按约定好的UDP报文格式解数据即可。2.2 数据点表规划最重要的前置工作做Modbus相关项目无论用什么工具第一步永远是列数据点表。这一步不做好后面配置节点时你一定会被绕晕。我这边的现场有3个从站设备从站地址设备类型寄存器地址数据类型倍率工程含义1温控表0x0001无符号16位0.1当前温度℃1温控表0x0002无符号16位0.1目标温度℃1温控表0x0003无符号16位1运行状态2电表0x0000无符号32位两个寄存器0.1总有功电能kWh2电表0x0002无符号16位0.01A相电压V3变频器0x2000有符号16位0.01输出频率Hz3变频器0x2001无符号16位1运行电流A需要注意Modbus协议地址有两种表示方式一种是协议地址从0x0000开始一种是数据地址PLC习惯用40001这种。EsDA节点配置时我使用的是协议地址也就是偏移地址。如果你在配置时发现读出来的数据对不上十有八九是协议地址和数据地址没换算对。这里有个倍率的概念温控表返回的原始值是253倍率是0.1那么实际温度是25.3℃。这个倍率转换可以在EsDA的Lua脚本里统一处理不必下发到上位机去算。2.3 UDP报文格式设计既然上位机要用UDP接收数据双方必须约定一个报文格式。我设计的是定长JSON文本格式每条报文固定包含设备ID、时间戳、数据类型、若干个数据点。用JSON的好处是上位机解析方便不用自己做二进制位拼装代价是体积稍大但在局域网场景下几百字节的UDP包根本不是问题。我最终采用的报文格式是{ device_id: GW-001, timestamp: 1712304923, points: [ {addr: 1, key: temp_cur, value: 25.3, unit: C}, {addr: 1, key: temp_set, value: 30.0, unit: C}, {addr: 2, key: voltage_a, value: 231.5, unit: V} ] }这里有个考量的点如果对实时性要求极高、上位机解析能力弱可以考虑用二进制定长报文如果数据点少、要兼容调试JSON更友好。我选JSON还有一个私心——用网络调试助手一眼就能看出来数据对不对排查问题效率高。3. EsDA工程实操从零搭建Modbus RTU转UDP3.1 创建工程与添加硬件节点打开EsDA的图形化开发环境一般叫EsDA Studio或类似新建一个空白工程选择合适的硬件型号。这里需要说明一点EsDA的平台版本不同界面细节会有差异但核心思路是一致的——把左边面板的节点拖到画布上双击配置参数再连线建立数据流。我要用到的节点有三类UART节点配置RS485串口的波特率、数据位、校验位、停止位。我的设备设置是9600, 8, N, 1。这必须和你的下位机从站完全一致否则所有帧都会因校验失败而被丢弃。Modbus RTU Master节点基于UART节点工作配置每个从站的读取规则从站地址、功能码、起始寄存器、读取长度、轮询周期。UDP Client节点配置远端IP和端口以及本地绑定的端口如果需要的话。先拖一个UART节点进来把串口参数配好。然后拖Modbus RTU Master节点绑到刚才的UART上。最后拖UDP Client节点填上服务器的IP和端口。3.2 Modbus RTU Master节点配置详解Modbus RTU Master节点是整条链路的起点。双击这个节点会看到配置界面。核心配置项如下配置项说明我的配置从站地址目标设备地址1后面会加2、3功能码03读保持寄存器 / 04读输入寄存器03起始地址协议寄存器地址0x0001读取数量连续读取的寄存器个数3轮询周期每次读取的时间间隔1000ms超时时间等待从站响应的最长时间200ms重试次数超时后自动重发的次数2关键点来了Modbus RTU建议一次读取多个连续寄存器而不是一个点一个点地读。比如温控表地址0x0001到0x0003是连续的温度、目标温度、状态那就一次性读3个寄存器回来后在脚本里再切分。这样整条总线的报文数量大大减少轮询效率高很多。如果你一个点一次读3个从站各5个点每轮要发15个请求总线会很挤。EsDA的Master节点支持配置多个从站表项每个表项就是一条独立的读请求规则。我在项目里配了4条规则从站1功能码03起始地址0x0001读3个寄存器每秒一次从站2功能码03起始地址0x0000读4个寄存器为了拿32位电能数据必须一次读够4个每秒一次从站3功能码03起始地址0x2000读2个寄存器每秒一次这里有个轮询周期的估算逻辑总线上3个从站每从站1条请求波特率9600。每条请求大约8字节地址功能码地址2字节数量2字节CRC2字节响应大约5N×2字节。按9600波特率8个数据位1个停止位1个字节约1.04ms一帧请求响应整个事务算下来大概30毫秒左右。3个从站完整轮询一次不超过100ms我设1秒周期已经是很大的余量了。如果现场从站多达十几个就要计算总线时间避免轮询周期设得太短导致总线拥塞。3.3 UDP Client节点配置UDP Client节点相对简单。所谓Client就是主动向服务器发数据的一端。需要配置服务器IP上位机或云服务器的UDP监听地址。我这里设的是192.168.1.100端口6000。本地端口可选。如果你希望从固定的本机端口发出去就填一个比如0表示随机端口也可以指定5253等。心跳包/周期上报EsDA的UDP节点本身不带周期发送逻辑发送时机是通过输入触发或Lua脚本定时控制的。需要注意UDP Client和TCP Client不一样它不需要连接成功的概念。发送方只管把报文发给IP:端口对端是否在线不关心。所以在联调阶段如果UDP Server没开EsDA这边是不会有任何报错提示的。这个特性既是优点省心也是坑你不知道对端到底收到没有。3.4 Lua脚本数据解析、转换与打包EsDA里节点和节点之间传数据是通过输入输出端口连线的但简单的连线只能做透传真正要按业务逻辑处理数据得写一小段Lua脚本。我把Modbus Master节点的输出接到一个脚本处理节点EsDA里名字可能是Script/Lua Node在脚本里完成三件事解析寄存器原始值、乘上倍率转成工程值、组装JSON报文再交给UDP节点发送。下面是我这次的Lua脚本核心逻辑已简化脱敏-- 输入modbus_payload是EsDA框架给脚本的输入数据 -- 结构大致为{addr从站地址, values{寄存器值数组}, length读取长度} function parse_modbus_data(payload) local device_addr payload.addr local values payload.values local points {} if device_addr 1 then -- 温控表温度(0.1度)、目标温度(0.1度)、状态(1) table.insert(points, {addr1, keytemp_cur, valuevalues[1] * 0.1, unitC}) table.insert(points, {addr1, keytemp_set, valuevalues[2] * 0.1, unitC}) table.insert(points, {addr1, keyrun_status, valuevalues[3], unit}) elseif device_addr 2 then -- 电表32位电能在前两个寄存器大端字节序 local energy_hi values[1] local energy_lo values[2] local energy (energy_hi 16) | energy_lo table.insert(points, {addr2, keyenergy_total, valueenergy * 0.1, unitkWh}) table.insert(points, {addr2, keyvoltage_a, valuevalues[3] * 0.01, unitV}) elseif device_addr 3 then -- 变频器频率(0.01Hz有符号)、电流(0.1A) table.insert(points, {addr3, keyfreq, valuevalues[1] * 0.01, unitHz}) table.insert(points, {addr3, keycurrent, valuevalues[2] * 0.01, unitA}) end return points end -- 组包函数 function build_udp_payload(device_id, points) local msg { device_id device_id, timestamp os.time(), points points } return cjson.encode(msg) end -- 主处理逻辑 function main(data) local pts parse_modbus_data(data) if #pts 0 then local out build_udp_payload(GW-001, pts) return out else return nil -- 返回nil表示丢弃不发送 end end这里有几个容易踩坑的细节值得单独说第一有符号数处理。变频器频率返回的是有符号16位值如果设备返回65535表示-1那么直接按无符号数乘倍率就错了。Lua里处理方式是把大于等于0x8000的值减0x10000再乘倍率。EsDA的脚本当然也可以这么写。第二32位数据的大小端和字序。Modbus寄存器是16位一个单元32位数据占据两个寄存器。不同设备厂商对两个字word的先后顺序定义不同有的是高字在前大端有的是低字在前小端。电表返回的能量值我用的是高字在前、低字在后跟设备手册一致。如果发现算出来数值离谱得大大概率是字序反了把(energy_hi 16) | energy_lo换成(energy_lo 16) | energy_hi即可。第三Lua执行环境的工具函数差异。EsDA内置的Lua版本因硬件而异有些板子支持os.time()有些不支持。我在实际调试时就发现目标板不支持返回真实Unix时间戳最后采用的是EsDA框架传入的启动后毫秒值换算成相对时间。业务上报不依赖时间戳的时候这个字段可以直接留空或由上位机打时标省得和硬件较劲。第四json编码库。有些简化的Lua环境不带cjsonEsDA平台文档里一般会说明自带的JSON库名字。如果不确定可以先在脚本里打印一下库的可用性或者直接用平台封装的函数。4. 实测联调与问题排查实录4.1 本地模拟用Modbus Slave模拟从站验证写完了配置和脚本接下来进入联调阶段。我建议一定先在本地搭一个模拟环境验证不要直接上到现场设备上原因很简单现场总线上的干扰、从站地址冲突等因素太多出了问题很难定位。我用的工具是Modbus SlaveModbus从站模拟器在电脑上模拟3个从站设备监听串口。USR-TCP232-Test或网络调试助手在本机开一个UDP Server监听6000端口查看EsDA发来的报文。由于电脑上没有RS485口我需要一个USB转RS485转接器。把转接器的A/B线按极性接到EsDA开发板的RS485接口上。电脑端把Modbus Slave的串口设置成COM口、9600、8、N、1Modbus地址分别设置成1、2、3在界面上手工填入寄存器值比如温度253表示25.3℃电压2315表示231.50V。然后把EsDA工程烧录进开发板上电开机。看网络调试助手的UDP监听窗口正常情况下每秒会陆续收到几条JSON报文。如果收到类似上文那种JSON消息说明Modbus RTU读从站、脚本解析、UDP发送全链路已经打通了。4.2 典型问题速查表在调试过程中我整理了一份问题排查表基本都是实战里经常碰到的现象可能原因排查思路与解决UDP没有收到任何数据上位机IP/端口配置错误或EsDA网络没通先用EsDA自带ping功能测试网络连通性用UDP调试助手绑错端口也可能收不到收到的是乱码/二进制把Modbus原始帧透传发出去了没走脚本解析检查脚本节点是否在数据流中间正确接入脚本是否编译错误收到JSON但数值全是0Modbus请求没成功从站没应答或者数据本来就是0在EsDA的调试日志里看是否有Modbus超时或异常码返回数值翻倍/减半倍率设置错了寄存器数据单位和工程单位不一致对照设备手册重新核对倍率数值巨大且跳变32位数据字序反了或者读取了错误的寄存器地址调整字序核对起始地址用Modbus Poll之类的工具先单独验证偶尔丢包轮询周期太短或UDP网络抖动拉大轮询周期到1s以上UDP本身不保证可靠可以通过上报序号配合上位机补包温度数值一直是65535有符号负数按无符号解析了在Lua里对高位符号位做转换处理RS485一直通信不上A/B极性接反、终端电阻缺失、波特率不一致交换A/B线在链路两端接120欧终端电阻逐个核对参数4.3 现场调试的三个独家经验调试完模拟环境后我还把整套设备搬到现场跑了几天期间积累了几个常规文档里不会写的教训。第一个教训是RS485总线的接地问题。原本在实验室里一切正常到了现场偶尔整个总线超时。排查发现现场有两个从站设备和网关之间的RS485线很长而且没有共地导致共模电压过高平白无故影响通信稳定性。后来我在RS485的A、B线之外单独拉了一根地线把各设备的信号地连在一起问题才彻底消失。如果你发现Modbus在实验室没问题、现场间歇性掉线优先查接地。第二个教训是UDP报文不要发得太碎。一开始我设计的是每个从站解析完就单独发一包JSON1秒3包看着也不算多。但在现场Wi-Fi环境稍微一拥挤丢包率明显上升。后来我把3个从站的数据在Lua脚本里缓存起来凑齐一帧统一上报也就是1秒只发1包单包内容变大但频率降低了实测丢包率几乎为零。这个思路在UDP应用里很重要宁可包大一点也不要高频发小包。第三个教训是轮询周期要可配置。现场接的设备数量可能随时调整我在Lua脚本里把轮询周期做成了变量并且预留了一个快速模式200ms一次和慢速模式5s一次可以在EsDA工程里直接改配置参数重新生成固件。给甲方交付调试的时候这个灵活性帮了大忙因为生产现场往往不希望频繁断电重启改参数。5. 项目扩展方向与最终体会这个项目做完基础通信链路后其实还可以做很多有价值的扩展。最直接的是增加TCP Server/MQTT输出把UDP Client节点换成MQTT Publisher数据就可以直接推到云平台或者加一个TCP Server节点让上位机主动来连网关。EsDA支持一个串口数据流桥接到多个网口输出也就是一份Modbus数据既能UDP上报又能同时转发到MQTT这时候Lua脚本里做一次协议分发即可。还可以增加Modbus写寄存器功能实现从PC下发控制指令上位机通过UDP给EsDA送写请求EsDA解析后以Modbus RTU Master身份向从站写单个或多个寄存器功能码06/16。这样就不仅仅是数据采集了能实现远端读和写的双向闭环比如远程启停变频器、修改温控表的目标温度。再扩展就是边缘计算EsDA可以定时计算某些数据点变化量超过阈值才上报或者做简单的平均值滤波。在设备数量多、云端流量敏感的场景里这类边缘侧预处理能省下大量带宽。我个人在实际操作中的体会是协议转换类项目真正的难点从来不是写代码而是清楚地知道自己要读哪些数据、数据是什么格式、用什么方式交付给上游。只要前期数据点表梳理清楚后面的EsDA配置和脚本只能说是个体力活。这个项目用EsDA做Modbus RTU Master转UDP Client开发的整体节奏大概是半天列点表、半天拖节点配参数、一天写脚本调试再花一天现场联调就能稳定运行。放在以前用C代码从零写至少得一两周起步。如果你手上正好有类似老设备串口数据要上网的需求强烈建议试试这个方案工具链成熟、门槛不高而且后期改动弹性特别好。
返回列表