资讯动态

ST语言按位拆解BYTE数组:Modbus字节序解析实战

发布时间:2026/10/26 1:11:58 来源:尧图企业网站定制
1. 字节序踩坑现场为什么我的Modbus数据全是乱的搞工业通讯的朋友大概率都遇到过这种场景PLC跟仪表、变频器、温控器通过Modbus RTU或者Modbus TCP连上了通讯状态看着正常寄存器也能读回来但一解析数据就傻眼——温度显示成了几千度压力值变成了负数电表读数大得离谱。排查半天线路、波特率、站号、功能码全都没问题最后发现是字节序在作怪。这个坑我自己踩过不止一次。最早做西门子S7-200 SMART跟第三方Modbus仪表通讯的时候读回来的32位浮点数完全对不上后来用Modbus Poll一对比才发现仪表发上来的四个字节顺序跟PLC默认解析的顺序是反的。更麻烦的是有些设备是字内字节交换有些是字间交换还有些两个都交换排列组合下来能把人绕晕。标题里提到的“ST按位拆解BYTE数组”说的就是在STStructured Text结构化文本语言环境下面对Modbus返回的一串BYTE数组怎么通过按位操作把数据正确还原出来。这不是一个炫技的操作而是工业现场非常实用的基本功。因为很多PLC编程环境比如CODESYS、汇川、倍福、施耐德的部分机型对Modbus的字节序处理并不统一库函数封装得也不一定符合你的设备最终往往需要自己动手拆字节。这篇文章适合谁看如果你正在用ST语言做Modbus通讯读回来的数据总是对不上或者你刚接触Modbus协议搞不清楚大端小端、高字低字到底怎么排又或者你手上有几台不同品牌的设备字节序各不相同每次都要重新调——那这篇内容应该能帮你省下不少调试时间。我会从字节序的本质讲起把BYTE数组按位拆解的完整思路、ST代码实现、常见问题排查都过一遍尽量做到你看完就能直接抄作业。2. 字节序的本质不是玄学是排列组合2.1 大端与小端谁在前谁在后字节序Endianness这个概念说白了就是多字节数据在内存或者通讯线上排列的顺序。一个16位整数0x1234需要两个字节来存高字节是0x12低字节是0x34。如果先发0x12再发0x34这叫大端序Big-Endian如果先发0x34再发0x12这叫小端序Little-Endian。Modbus协议本身在规范里是明确要求大端序的也就是说寄存器的高字节先传输。但问题在于很多设备厂商在实现的时候并不严格遵守尤其是涉及到32位数据比如浮点数、双字整数的时候字的顺序又成了一个新的变量。我用一个生活化的类比来解释假设你要寄一个快递里面有两层盒子外层盒子写着“高字”内层盒子写着“低字”每个盒子里又有两个小格子分别放“高字节”和“低字节”。大端序就是外层先寄、每个盒子里高字节格子先打开小端序就是反过来。而Modbus的麻烦在于外层盒子的顺序和内层格子的顺序可以独立变化于是就有了四种组合。2.2 Modbus里32位数据的四种字节排列这是实际调试中最容易搞混的地方。假设有一个32位浮点数在内存中标准的大端表示是A B C DA是最高字节D是最低字节那么通过Modbus两个寄存器传输时可能出现以下四种排列排列方式字节顺序常见叫法典型设备ABCDA B C D大端、Big-Endian标准Modbus设备、部分西门子CDABC D A B字交换、Word Swap施耐德部分机型、部分电表BADCB A D C字节交换、Byte Swap部分温控器、流量计DCBAD C B A全交换、Little-Endian部分国产仪表、PLC内部小端这张表建议你截图保存调试的时候对着看。我遇到过最离谱的情况是同一台设备的不同寄存器区域字节序居然不一样——比如测量值用CDAB累计量用ABCD。所以不要假设一台设备只有一种字节序读回来的每个数据都要验证。2.3 为什么ST语言里特别容易出问题ST语言本身对字节序的处理是依赖底层平台的。CODESYS系的PLC大多是小端架构而Modbus协议要求大端传输这中间的转换如果库函数没做好就会出问题。更麻烦的是很多Modbus库函数返回的是ARRAY[0..N] OF BYTE这样的字节数组而不是直接给你解析好的REAL或者DWORD。这时候你就得自己动手从字节数组里按正确的顺序把数据拼出来。标题里说的“按位拆解BYTE数组”核心就是这个场景你拿到了一串字节但不知道它们该怎么组合于是需要根据设备的实际字节序用位操作和移位运算把数据还原。这个过程听起来简单但细节很多下面我会一步步拆开讲。3. ST语言按位拆解BYTE数组的完整实现3.1 准备工作明确数据长度和目标类型在动手写代码之前先要搞清楚三件事第一你要解析的数据是几位。16位WORD/INT、32位DWORD/DINT/REAL、还是64位LWORD/LREAL不同长度的数据拆解方式完全不同。第二目标数据类型是什么。是整数、浮点数、还是有符号数浮点数的处理最麻烦因为涉及到IEEE 754格式的还原。第三设备的字节序是哪种。这个必须通过实验确定不能靠猜。我的做法是给设备写一个已知值比如写一个浮点数123.456到某个保持寄存器然后读回来用Modbus Poll或者自己写的调试工具看原始字节对照上面的四种排列就能确定。提示如果设备不支持写入那就找一个已知物理量的寄存器比如室温25.3度读回来对照。实在不行就四种排列都试一遍哪个结果合理就是哪个。3.2 16位数据的拆解最简单的情况16位数据只涉及两个字节的顺序问题。假设读回来的字节数组是byData[0]和byData[1]那么大端序wValue : SHL(byData[0], 8) OR byData[1];小端序wValue : SHL(byData[1], 8) OR byData[0];这里的SHL是左移操作OR是按位或。这个逻辑很直观高字节左移8位变成高8位低字节直接放在低8位两者或运算就拼成了一个完整的16位值。在ST语言里代码大概长这样FUNCTION_BLOCK FB_Modbus16 VAR_INPUT byData : ARRAY[0..1] OF BYTE; bSwap : BOOL; // TRUE表示小端FALSE表示大端 END_VAR VAR_OUTPUT wValue : WORD; END_VAR IF bSwap THEN wValue : SHL(BYTE_TO_WORD(byData[1]), 8) OR BYTE_TO_WORD(byData[0]); ELSE wValue : SHL(BYTE_TO_WORD(byData[0]), 8) OR BYTE_TO_WORD(byData[1]); END_IF;这段代码里有个细节BYTE_TO_WORD这个类型转换是必须的因为ST语言是强类型语言BYTE直接移位可能会报错或者结果不对。不同品牌的PLC类型转换函数名可能不一样CODESYS里是BYTE_TO_WORD西门子STEP 7里可能是WORD直接赋值具体看你的编程环境。3.3 32位数据的拆解四种排列全覆盖32位数据是重灾区。假设读回来四个字节byData[0]到byData[3]按照前面说的四种排列解析方式如下ABCD大端dwValue : SHL(BYTE_TO_DWORD(byData[0]), 24) OR SHL(BYTE_TO_DWORD(byData[1]), 16) OR SHL(BYTE_TO_DWORD(byData[2]), 8) OR BYTE_TO_DWORD(byData[3]);CDAB字交换dwValue : SHL(BYTE_TO_DWORD(byData[2]), 24) OR SHL(BYTE_TO_DWORD(byData[3]), 16) OR SHL(BYTE_TO_DWORD(byData[0]), 8) OR BYTE_TO_DWORD(byData[1]);BADC字节交换dwValue : SHL(BYTE_TO_DWORD(byData[1]), 24) OR SHL(BYTE_TO_DWORD(byData[0]), 16) OR SHL(BYTE_TO_DWORD(byData[3]), 8) OR BYTE_TO_DWORD(byData[2]);DCBA全交换dwValue : SHL(BYTE_TO_DWORD(byData[3]), 24) OR SHL(BYTE_TO_DWORD(byData[2]), 16) OR SHL(BYTE_TO_DWORD(byData[1]), 8) OR BYTE_TO_DWORD(byData[0]);这四种写法看起来只是索引顺序不同但实际调试的时候如果没有一个清晰的对照表很容易写错。我的建议是封装成一个函数块用一个枚举变量来选择字节序模式这样代码可读性和可维护性都好很多。3.4 浮点数的特殊处理IEEE 754还原如果32位数据是浮点数REAL那么拼出DWORD之后还不能直接用需要按照IEEE 754标准还原成浮点数。在ST语言里通常有现成的转换函数比如CODESYS里的DWORD_TO_REAL但要注意这个函数的行为——它是直接按位重新解释还是做了数值转换大部分情况下是按位重新解释这正是我们需要的。代码大概是这样FUNCTION_BLOCK FB_ModbusReal VAR_INPUT byData : ARRAY[0..3] OF BYTE; eOrder : E_ByteOrder; // 枚举ABCD, CDAB, BADC, DCBA END_VAR VAR_OUTPUT rValue : REAL; END_VAR VAR dwTemp : DWORD; END_VAR CASE eOrder OF E_ByteOrder.ABCD: dwTemp : SHL(BYTE_TO_DWORD(byData[0]), 24) OR SHL(BYTE_TO_DWORD(byData[1]), 16) OR SHL(BYTE_TO_DWORD(byData[2]), 8) OR BYTE_TO_DWORD(byData[3]); E_ByteOrder.CDAB: dwTemp : SHL(BYTE_TO_DWORD(byData[2]), 24) OR SHL(BYTE_TO_DWORD(byData[3]), 16) OR SHL(BYTE_TO_DWORD(byData[0]), 8) OR BYTE_TO_DWORD(byData[1]); E_ByteOrder.BADC: dwTemp : SHL(BYTE_TO_DWORD(byData[1]), 24) OR SHL(BYTE_TO_DWORD(byData[0]), 16) OR SHL(BYTE_TO_DWORD(byData[3]), 8) OR BYTE_TO_DWORD(byData[2]); E_ByteOrder.DCBA: dwTemp : SHL(BYTE_TO_DWORD(byData[3]), 24) OR SHL(BYTE_TO_DWORD(byData[2]), 16) OR SHL(BYTE_TO_DWORD(byData[1]), 8) OR BYTE_TO_DWORD(byData[0]); END_CASE; rValue : DWORD_TO_REAL(dwTemp);这里有个坑要注意有些PLC的DWORD_TO_REAL函数在输入是非法浮点数比如NaN或者无穷大时会报错或者输出异常值。如果你读回来的数据本身就有问题转换函数可能会让程序跑飞。稳妥的做法是加一个范围检查或者用IS_VALID之类的函数先判断一下。4. 实操全流程从读取到验证的每一步4.1 硬件连接与通讯参数确认在写代码之前先把物理层和通讯参数搞定。Modbus RTU走RS485的话确认A接A、B接B终端电阻该加就加。波特率、数据位、停止位、校验位这些必须跟设备手册一致。我遇到过好几次通讯不上最后发现是校验位设错了——设备默认是偶校验我设成了无校验数据能读回来但全是乱的。Modbus TCP的话确认IP地址、端口号默认502、单元标识符Unit ID都对。有些网关设备对Unit ID有要求不是随便填的。提示调试阶段强烈建议先用Modbus Poll或者Modbus Slave这类工具确认通讯正常能读到正确的原始数据再去写PLC代码。这样可以把问题范围缩小避免在通讯和解析之间来回怀疑。4.2 读取原始字节并存入数组在ST语言里Modbus读取函数通常会把数据放到一个字节数组或者寄存器数组里。以CODESYS为例如果用Modbus Master库读保持寄存器的函数可能是这样的VAR abyReadBuffer : ARRAY[0..9] OF BYTE; xReadDone : BOOL; xReadError : BOOL; END_VAR ModbusMaster.ReadHoldingRegisters( iUnitID : 1, iStartAddr : 0, iQuantity : 5, pBuffer : ADR(abyReadBuffer), xDone xReadDone, xError xReadError );这里读回来5个寄存器对应10个字节。注意pBuffer传的是地址读回来的数据会按顺序填到这个数组里。不同库函数的参数名和用法可能不同但核心逻辑是一样的。4.3 按位拆解并还原数据拿到字节数组之后就进入拆解环节。假设我们要解析两个32位浮点数分别占用前4个字节和后4个字节代码可以这样写VAR abyReadBuffer : ARRAY[0..9] OF BYTE; rValue1 : REAL; rValue2 : REAL; fbReal1 : FB_ModbusReal; fbReal2 : FB_ModbusReal; END_VAR // 解析第一个浮点数假设字节序是CDAB fbReal1( byData : [abyReadBuffer[0], abyReadBuffer[1], abyReadBuffer[2], abyReadBuffer[3]], eOrder : E_ByteOrder.CDAB ); rValue1 : fbReal1.rValue; // 解析第二个浮点数假设字节序是ABCD fbReal2( byData : [abyReadBuffer[4], abyReadBuffer[5], abyReadBuffer[6], abyReadBuffer[7]], eOrder : E_ByteOrder.ABCD ); rValue2 : fbReal2.rValue;这里我把字节数组的四个元素传给了函数块。有些ST环境不支持这种数组字面量写法那就需要先赋值到一个临时数组再传进去。4.4 验证结果用已知值反推解析出来的数据对不对不能只看“看起来合理”最好用已知值验证。我的做法是如果设备支持写入写一个已知的浮点数比如123.456到某个寄存器。读回来用四种字节序分别解析看哪个结果等于123.456。记录下这个设备的字节序以后就固定用这种。如果设备不支持写入那就找一个物理量比如室温。用温度计测一下实际温度然后看哪种解析结果跟实际值最接近。这个方法虽然粗糙但实际用下来很有效。注意有些设备的浮点数精度有限或者有偏移量、缩放因子解析出来的值可能需要再经过一次线性变换才是真实物理量。这个要结合设备手册确认。5. 常见问题与排查技巧实录5.1 数据跳变或者偶尔正确偶尔错误这种情况通常是字节序不稳定或者数据更新不同步导致的。如果设备是双寄存器传输32位数据而你在读取的时候正好赶上设备更新数据就可能读到“半新半旧”的组合解析出来就是乱码。解决办法有两个一是读取两次比较结果一致才采用二是用设备的“数据就绪”标志位确认数据稳定后再读。Modbus协议本身没有原子性保证所以这个问题只能靠软件层面处理。5.2 浮点数解析出来是NaN或者无穷大这通常意味着字节序完全错了拼出来的DWORD不符合IEEE 754格式。比如指数位全是1尾数位非零就是NaN。遇到这种情况不要怀疑转换函数先把四种字节序都试一遍大概率能解决。还有一种可能是数据本身就有问题——设备还没初始化完成或者传感器故障发上来的就是无效值。这时候需要结合设备的状态字判断。5.3 整数解析出来是负数但实际应该是正数这是符号位处理的问题。16位整数如果最高位是1按有符号数解析就是负数。如果实际物理量不可能是负数那要么是字节序错了导致最高位被误置1要么是应该按无符号数解析。ST语言里WORD和INT的区别就在这里选对类型很重要。5.4 不同寄存器区域字节序不一致前面提过有些设备确实会这样。我的应对策略是每个数据点单独配置字节序不要假设整台设备统一。在代码里用一个结构体数组每个元素包含寄存器地址、数据类型、字节序模式这样管理起来清晰也不容易出错。问题现象可能原因排查方法解决措施数据完全乱码字节序错误四种排列逐一测试确定正确字节序并固定偶尔跳变数据更新不同步连续读取两次比较加数据就绪判断或双读校验浮点数为NaN字节序错误或数据无效检查原始字节重试字节序或检查设备状态整数符号错误类型选择错误确认物理量范围改用无符号类型或检查字节序部分寄存器正常部分异常设备字节序不统一逐点验证每个数据点单独配置5.5 调试工具的选择和使用调试Modbus字节序问题手边有几个工具会方便很多Modbus PollWindows平台的老牌工具能直观看到原始字节和解析后的值支持多种数据类型和字节序设置。我通常用它来确认设备的字节序。Modbus Slave模拟从站设备用来测试自己写的主站代码。可以自定义寄存器值和字节序非常灵活。串口调试助手看原始报文确认字节顺序。适合深入分析协议层面。PLC自带的在线监控CODESYS、TIA Portal这些都能在线看变量值但要注意监控的是解析后的值还是原始字节。提示用Modbus Poll的时候在显示设置里可以切换字节序边切换边看数值变化很快就能找到正确的排列。这个技巧我教过好几个新手比对着手册算半天快多了。6. 经验总结与进阶建议6.1 封装通用函数块一次投入长期受益字节序解析这个事几乎每个Modbus项目都会遇到。与其每次重新写一遍不如花点时间封装一个通用的函数块支持16位、32位、浮点数、四种字节序模式。以后新项目直接调用省时省力。我的函数块接口大概是这样设计的FUNCTION_BLOCK FB_ModbusParser VAR_INPUT abyData : ARRAY[0..7] OF BYTE; // 最多支持64位 eDataType : E_DataType; // WORD, DWORD, REAL, LWORD, LREAL eByteOrder : E_ByteOrder; // ABCD, CDAB, BADC, DCBA END_VAR VAR_OUTPUT wValue : WORD; dwValue : DWORD; rValue : REAL; xValid : BOOL; END_VAR这样不管什么数据类型一个函数块全搞定。内部用CASE语句分派代码也不复杂。6.2 文档化每个设备的字节序项目做完之后把每个设备的字节序、寄存器地址、数据类型、缩放因子整理成一张表存档。下次维护或者换设备的时候直接查表不用重新调试。这个习惯我坚持了好几年帮我省下了大量重复劳动。6.3 注意ST语言平台的差异不同PLC品牌的ST语言在语法细节上有差异比如类型转换函数名、移位操作符、数组索引方式等。CODESYS、西门子、三菱、欧姆龙的ST都有自己的特点。写代码的时候要针对目标平台调整不能直接照搬。比如西门子的STSCL里移位操作是SHL和SHR但类型转换可能需要用WORD_TO_INT之类的函数。而CODESYS里可能更灵活一些。具体查各自的手册。6.4 性能考虑位操作比类型转换快在高速数据采集场景下位操作的效率比调用类型转换函数高。如果数据量很大比如每秒要解析几百个寄存器那就尽量用位操作直接拼减少函数调用开销。当然大多数工业场景下这点性能差异可以忽略代码可读性更重要。6.5 测试用例要覆盖边界值解析代码写完之后用边界值测试一下最大值、最小值、零、负数、非法浮点数。确保各种情况下代码都不会跑飞。特别是浮点数的NaN和无穷大很容易让程序出错。我在实际项目里遇到过一次设备故障时发上来的浮点数是NaN结果解析之后参与运算整个控制逻辑都乱了。后来加了有效性检查才避免了这个隐患。这个教训分享出来希望大家少走弯路。6.6 关于Modbus TCP和RTU的字节序差异有人问过Modbus TCP和RTU在字节序上有没有区别。答案是协议层面没有区别都是大端序。但实际设备的实现可能不同TCP设备可能更规范一些RTU设备因为历史原因各种奇怪的字节序都有。所以不管用哪种都要实际验证。另外Modbus TCP的报文头里有个事务标识符和协议标识符这些是多字节字段也是大端序。如果你自己解析报文这些地方也要注意。6.7 结合具体设备调试的案例最后分享一个实际案例。之前做一个项目用汇川PLC通过Modbus RTU读某品牌电表的数据。电压、电流读回来都正常但功率读回来是负数而且数值明显不对。用Modbus Poll一看功率寄存器是32位浮点数字节序是CDAB而电压电流是16位整数字节序是AB。也就是说同一台设备16位和32位数据的字节序不一样。解决办法就是在代码里针对不同数据点配置不同的字节序。16位用大端32位用CDAB。改完之后数据就对了。这个案例说明不要假设一台设备只有一种字节序每个数据点都要验证。字节序这个问题说大不大说小不小。搞清楚了就是一层窗户纸搞不清楚能折腾好几天。希望这篇内容能帮你把这层窗户纸捅破。后面如果再遇到类似问题欢迎一起交流。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑