ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Modbus字节序错乱?用ST按位拆解BYTE数组,一次搞定

Modbus字节序错乱?用ST按位拆解BYTE数组,一次搞定 干Modbus调试最难受的不是连不上而是数据通了一看全是乱码。我有回在现场调一台温控仪表CRC校验全过、寄存器也读得出来结果PLC上面板温度显示65280℃。仪表那边明明输出的是25.5℃可程序里读出来的值怎么都对不上。折腾了将近三个小时最后才确认就是字节序反了——仪表寄存器里存的是0x00FFPLC按大端当0xFF00读出来65280就这么来的。这种问题用ST语言结构化文本按位拆解BYTE数组往往几分钟就能解决。所谓按位拆解不是简单把两个字节对调而是把通信缓冲区里的BYTE数组按你需要的方式逐字节、逐位重组。这篇文章我会把几种常见的字节序错乱情况、ST的拆解代码、以及这几年我踩过的坑全部整理出来正在做PLC和第三方设备Modbus联调的工程师可以直接照用刚接触协议解析的朋友也能顺着思路搞懂原理。1. 字节序这事儿先搞明白再动手1.1 四种字节序同一个数四种长相先把32位数据拿出来举例。假设一个DWORD数是0x12345678拆开是四个字节0x12、0x34、0x56、0x78我分别记为A、B、C、D。这四个字节在报文里、在内存里出现的顺序常见的有四种顺序类型字节排列典型表现标准高字节在前12 34 56 78寄存器10x1234寄存器20x5678符合Modbus大端约定字内字节交换34 12 78 56每个16位寄存器内部高低字节颠倒寄存器顺序没变字顺序交换56 78 12 34两个16位寄存器前后顺序反了各自内部字节正常全部反序78 56 34 12字内字节和寄存器顺序全都反了很多资料里把这四种叫法写成ABCD、CDAB、BADC、DCBA不同文章里名字还可能互相打架所以不要死记名字关键是看最终的字节排列。我项目里干脆用数字模式编号0到3现场对号入座。生活里也有类似的事。同样是“三月二十一号”有人写03-21有人写21-03还有人写2025-03-21。纸面上都是对的但两边习惯不一样看到的日子就不是同一个。Modbus字节序就是这样设备端发出来的字节排列和PLC解析时用的排列不一致数据就被读“歪”了。1.2 Modbus协议明明规定了设备为什么还是乱来Modbus协议对单条16位寄存器报文的传输格式是有明确规定的先发高字节再发低字节。比如寄存器值是0x1234RTU报文里的原始字节是0x12、0x34。从这个角度说Modbus线缆上的字节序是标准的“大端”。问题出在哪呢协议规定了线缆上怎么传却管不到设备内部固件怎么存储这个值。现在很多设备主控芯片是ARM或x86架构内存默认小端。如果固件直接把内存里的WORD按连续字节丢给Modbus栈发出来的就是低字节在前。还有一些设备做32位浮点数的寄存器拼接时自己定了“寄存器地址从低到高分别对应数据高字还是低字”各家并不统一。加上有些上位机驱动默认按本机字节序解析应用层看到的数值自然就反了。所以“字节序反了”不一定是设备坏了也不一定是PLC搞错了更多时候是两边的“读法”没对上。你拿Modbus调试助手抓包的时候也一样——工具如果默认按某个字节序显示原始寄存器值你还以为报文没问题其实问题早藏在显示层之下了。1.3 比字节序更闹心的还有位序反的字节序好歹按字节为单位肉眼还能比对。有些冷门设备会离谱到把16位寄存器整个按位倒过来bit0放原本bit15的数据bit1放bit14以此类推。这种数据你光做字节交换和字序交换都解决不了因为每一个位都挪了家。这也是为什么我在标题里特别强调“按位拆解”——普通字节交换是“整箱搬家”位序反了就必须把箱子拆开一个一个零件重新装。处理这种场景ST语言的位运算几乎是唯一正路。2. 为什么推荐用ST按位拆解BYTE数组2.1 ST和BYTE数组是协议解析的天然搭档Modbus RTU和Modbus TCP的数据区本质都是一长串BYTE数组。PLC通信指令读回来的数据就放在连续缓冲区里很多主站指令还会给你一个数据指针。而ST语言的特点就是可以像普通数组一样按下标访问、写循环、做位运算和协议解析的需求天然匹配。这种做法最大的好处是解析逻辑全掌握在自己手里。缓冲区里哪个下标对应哪个字节、哪个字节该挪到哪一位代码里写得明明白白。出问题的时候可以一步步监控追踪而不是依赖厂商封装好的“黑盒指令”出了问题只能瞎猜。如果遇到字节序问题就随手调用“字节交换指令”经常会碰到几种尴尬有些PLC的交换指令只做16位交换32位场景完全不适用有些指令处理不了字序和字节序同时反的情况还有的指令在不同型号PLC上行为不完全一致。按位拆解配合模式枚举反而是一次性解决所有排列问题的通用方案。2.2 四个位运算“武器”本质是拆积木、拼积木按位拆解听着玄乎核心其实就四个操作AND按位与用来“摘出”某一位。比如SHR(Value, i) AND 16#0001就是把第i位搬到最低位后用掩码把其他位清零剩下的就是那一位的值。OR按位或用来“放入”某一位。比如Result OR SHL(WORD#1, 15 - i)就是把某一位放到结果的第15-i位而且不影响已经拼好的其他位。SHL左移把位往高位挪。左移1位相当于乘2左移8位相当于乘256左移24位就能把一个BYTE放到DWORD的最高8位上。SHR右移把位往低位挪配合AND就能提取任意指定位置的位。打个比方这就像拼乐高一堆零件散在地上AND负责从某个零件堆里精准拔出一颗SHL/SHR负责把零件挪到目标卡槽OR负责把它卡进结构里。一个字节一个字节、一个位一个位地组合。这里要特别提醒一下优先级问题。IEC 61131-3标准里ST的逻辑运算符优先级有规定NOT大于AND大于XOR大于OR但不同厂商的编译器在实际处理时仍可能有细微差别。我的习惯是只要表达式里同时出现位移、按位与、按位或一律加括号把顺序写死不给编译器自由发挥的空间。2.3 自写功能块比临时改代码更靠谱也许有人会说有些PLC库已经提供了字节序转换功能。实际项目里我仍然更倾向自己写功能块原因有四个不依赖特定PLC品牌换平台逻辑照样能用四种常见乱序一次覆盖现场出问题切个模式就行可以批量处理以后一次性解析几十个寄存器也无需改动核心逻辑可读性好新接手的同事看到代码能立刻明白这台设备是按哪种顺序重组数据的。这套东西说白了就是“沉淀标准件”。这次调完这台仪表下次遇到一个字节序同样混乱的变频器直接把功能块拖过来选好OrderMode活就干完了。3. 完整实操ST拆解BYTE数组的全套代码3.1 功能块接口定义我设计了一个功能块FB_ModbusBytesToDword输入是一个4字节数组和模式号输出是重组后的DWORD和REAL。为什么同时输出两种类型因为DWORD和REAL在内存里位模式完全相同重组完DWORD后一句DWORD_TO_REAL就能得到浮点数一次搞定整型和浮点两种场景。FUNCTION_BLOCK FB_ModbusBytesToDword VAR_INPUT RawData : ARRAY[0..3] OF BYTE; OrderMode : INT; // 0-标准排列1-字内字节交换2-字顺序交换3-全部反序 END_VAR VAR_OUTPUT ResultDword : DWORD; ResultReal : REAL; END_VAR VAR_TEMP b0, b1, b2, b3 : BYTE; nTmp : DWORD; END_VAROrderMode的编号含义我在现场就直接印在注释里防止过两个月自己都忘了。这里以4个字节为例是因为多数Modbus项目里最常用的32位浮点数正好是4个字节。如果以后遇到64位数据思路完全一样扩展到8个字节即可。3.2 四种模式的重组核心代码主体代码就是一个CASE分支把原始数组的四个字节按不同顺序移位拼接。具体实现如下b0 : RawData[0]; b1 : RawData[1]; b2 : RawData[2]; b3 : RawData[3]; CASE OrderMode OF 0: // 标准排列12 34 56 78 nTmp : SHL(TO_DWORD(b0), 24) OR SHL(TO_DWORD(b1), 16) OR SHL(TO_DWORD(b2), 8) OR TO_DWORD(b3); 1: // 字内字节交换34 12 78 56 nTmp : SHL(TO_DWORD(b1), 24) OR SHL(TO_DWORD(b0), 16) OR SHL(TO_DWORD(b3), 8) OR TO_DWORD(b2); 2: // 字顺序交换56 78 12 34 nTmp : SHL(TO_DWORD(b2), 24) OR SHL(TO_DWORD(b3), 16) OR SHL(TO_DWORD(b0), 8) OR TO_DWORD(b1); 3: // 全部反序78 56 34 12 nTmp : SHL(TO_DWORD(b3), 24) OR SHL(TO_DWORD(b2), 16) OR SHL(TO_DWORD(b1), 8) OR TO_DWORD(b0); END_CASE; ResultDword : nTmp; ResultReal : DWORD_TO_REAL(nTmp);这段代码的核心思路就是把每个字节当独立零件通过SHL移动到它在目标DWORD中的位置然后用OR拼起来。模式1到模式3只是改了移动的映射关系并没有增加任何新操作。比如RawData数组是[12, 34, 56, 78]模式0得到0x12345678模式1得到0x34127856模式2得到0x56781234模式3得到0x78563412。设备手册如果写明“32位数据按两个寄存器呈低字节在前排列”就直接选模式3。对不上就换一个模式试通常一次就能命中。代码里的OR拼接就是热词“按位或赋值”的实际应用不过ST语言没有C语言那种|写法通常直接在一个表达式里用多个OR完成。有一点经验值得提BYTE参与移位前我习惯先TO_DWORD显式转换不要依赖自动类型提升否则遇到0x80这类高位置1的字节有些编译器会做符号扩展把高位污染成一堆FFFF。3.3 位反转场景暴力逐位拆解如果设备把16位寄存器整个位序都反了字节交换完全不奏效这时候就要上循环逐位处理。我写了一个位反转功能块FB_ReverseBitsInWordFUNCTION_BLOCK FB_ReverseBitsInWord VAR_INPUT Value : WORD; END_VAR VAR_OUTPUT Result : WORD; END_VAR VAR_TEMP i : INT; bitMask : WORD; END_VAR Result : WORD#16#0000; FOR i : 0 TO 15 DO bitMask : SHR(Value, i) AND WORD#16#0001; IF bitMask WORD#16#0001 THEN Result : Result OR SHL(WORD#16#0001, 15 - i); END_IF; END_FOR逻辑不复杂循环16次先把第i位移到最低位再用AND 1提取出来。如果是1就把它放到结果的第15-i位上。每循环一次就等于把一个位从对称位置搬了一次家。拿16#0001举例二进制就是最低位为1其它位全是0。按这个代码执行一遍i0时提取到bit0是1放到第15位Result变成0x8000i1到15时全是0最终结果就是16#8000。一个最低位的1被翻到了最高位位序反转完成。再比如16#1234二进制是0001 0010 0011 0100逐位反转后是0010 1100 0100 1000也就是16#2C48。这类数据在普通仪表里不常见但在某些定制传感器和老式计量模块上真遇到过没有这套逐位拆解代码当时还真没辙。3.4 把4个BYTE拼成REAL浮点数场景Modbus项目里最常见的32位数据是REAL浮点变频器频率、流量、压力都是。重组得到DWORD后用DWORD_TO_REAL转换就得到浮点值这也是为什么功能块同时输出DWORD和REAL两个变量。我经常在调试现场用一个“已知数值验证法”让设备输出一个固定已知值比如10.0Hz。IEEE 754单精度表示下10.0的十六进制是0x41200000标准报文字节就是41 20 00 00。如果设备存储习惯是全反序缓冲区里读出来就变成00 00 20 41。这时用模式3重组得到0x41200000DWORD_TO_REAL之后就变回10.0。还有一种情况更容易被忽略设备寄存器顺序是对的但浮点字节顺序在内部反了读出来是一个极其接近0的“天文小数字”。这种值看起来不像明显乱码但明显不合理容易让人误判成量程问题。其实只要把四种模式都试一遍哪个能解出正常工程值就用哪个模式。3.5 在主程序里怎么调用功能块写好后调用非常直接。假设MB_MASTER已经把数据读到DataBuffer数组里VAR DataBuffer : ARRAY[0..3] OF BYTE; conv : FB_ModbusBytesToDword; f : REAL; END_VAR conv(RawData : DataBuffer, OrderMode : 3); f : conv.ResultReal;如果一次读了一大片数据、需要连续解析多个浮点数外层套个FOR循环就行每次取4个字节调用功能块输出放进REAL数组。模式号统一不到十行代码就能批量转换。有人可能会纠结实参是数组怎么传的问题。实际项目中如果缓冲区比较大、数据地址不连续可以把功能块输入改成POINTER TO BYTE或者先用MEMCPY把目标数据拷贝到RawData数组里。为了演示清晰这里用静态数组现场按实际需求调整即可。4. 踩坑实录与排查速查表4.1 坑一数组下标0不一定是报文的第一个字节这是我最早期犯过的错。Modbus主站指令把数据写进缓冲区时下标0到底对应寄存器高字节还是低字节由通信库实现决定。有的PLC还会在缓冲区前面塞地址、长度之类的附加信息。我一开始默认下标0是高字节结果回回解析都是反的。后来学乖了先给设备写入一个已知值比如0x1234读回来用监控表看数组内容。亲眼确认哪个下标放的是0x12、哪个放的是0x34再往下写解析逻辑。这个动作几乎能消除后面所有关于字节位置的猜测成本。4.2 坑二把缩放系数问题当成字节序问题字节序出错的时候数据通常会非常离谱要么是六位数要么是极小数。但如果读到的数值只是差一个固定倍数比如仪表输出25.5℃、程序里读出255或者10.0倍率读成100那一般不是字节序问题而是单位倍率和量程转换没做对。我有一次在现场陪客户折腾了一下午的字节交换最后翻手册才发现设备寄存器单位是0.1程序里少乘了10。所以动手拆字节前先把数据合理性评估一遍如果数值和期望值只差倍数先查手册里的量纲定义别在字节序这个方向死磕。4.3 坑三负数被读成巨大正数Modbus寄存器里存0xFFFE表示有符号数-2如果程序直接按WORD解析读出来是65534。这其实不属于字节序问题而是类型转换没做对。正确做法是字节序重组完成后16位有符号数用WORD_TO_INT转32位有符号数用DWORD_TO_DINT转。顺序一定要先重组、再转换。如果反过来先把WORD转成INT再去换字节、做位运算符号扩展会干扰你的移位结果最后怎么调都差一位。4.4 坑四隐式类型转换和符号扩展趁虚而入ST语言有些环境下BYTE值0x80如果参与自动类型提升可能被当成有符号数扩展成0xFF80再参与左移高位就出现一堆FFF。这类问题排查起来特别隐蔽测试数据在0x7F以下一切正常一碰到0x80以上的数据就全乱。所以我在所有位拼接场景都坚持写显式转换TO_BYTE、TO_WORD、TO_DWORD绝不指望编译器自动完成。一句话类型转换写得越啰嗦半夜被叫起来处理现场的可能性越低。4.5 问题排查速查表现象可能原因建议动作16位数据高低完全反0x00FF变成0xFF00字内字节序反用OrderMode1试一遍32位数据像两个16位数交换了位置寄存器字序反用OrderMode2试一遍32位数据毫无规律每个字节都错位字节序和字序都反用OrderMode3试一遍数据等于-1时读出65535没有做有符号转换重组后加WORD_TO_INT浮点数读出极小值或NaN32位浮点字节序不对四种模式顺序验证数值正确但差固定倍数缩放系数或单位倍率查手册别在字节序上死磕单寄存器内BIT位倒序设备位序LSB-first调FB_ReverseBitsInWord这个表是我每次排查Modbus数据异常时的行动清单。先确认数值离不离谱再确认数组下标位置最后套模式。前两步排除了第三步基本一击就中。调试时还有一个实用技巧在触摸屏或监控界面把四种模式的解析结果同时显示出来直接用肉眼判断哪个读数像正常工程值。很多时候不用纸面推算看一眼界面就知道该选哪个模式比对着报文一格格推演快得多。说到底Modbus字节序反了不是什么大病但确实磨人。我现在接手新设备时第一件事就是确认设备手册里的“寄存器存储格式”然后用一个已知固定值验证数组位置最后才决定用哪个OrderMode。这套功能块沉淀成标准库之后新项目再遇到同类问题基本三分钟解决偶尔碰到位序更诡异的设备也只需要在最外层再加一层位反转循环就行——思路是通的代码是复用的剩下的只是时间问题。
返回列表