首页 / 资讯中心 / 文章详情

工控人必知:字节序与16进制实战解析

工控人必知:字节序与16进制实战解析 ★ FEATURED ARTICLE
1. 从一个让人抓狂的现场说起为什么工控人躲不开字节和16进制干工控这行不管你是做PLC编程、上位机开发、还是现场调试迟早会撞上这么一幕设备通讯突然断了抓包工具里刷出一屏密密麻麻的十六进制数据你盯着01 03 00 00 00 02 C4 0B这种串发呆心里想的是这到底啥意思。或者更常见的场景——上位机读回来的浮点数是个天文数字明明传感器量程只有0到100屏幕上却显示1.2e38你怀疑人生地重启了三遍设备问题依旧。这些问题的根子几乎都指向同一个地方字节序和十六进制表示。工控领域跟互联网软件开发最大的区别在于我们打交道的对象是真实的物理设备——PLC、变频器、仪表、传感器、伺服驱动器。这些设备来自不同厂商有西门子、三菱、欧姆龙、施耐德、汇川、台达每一家对数据的组织方式都有自己的脾气。Modbus协议规定了寄存器是16位的但一个32位浮点数要占两个寄存器先发高字还是先发低字厂商A和厂商B的答案可能完全相反。你要是搞不清楚字节和16进制这套底层逻辑调试现场就只能靠猜而靠猜的代价往往是半夜被叫起来返工。这篇内容我想聊的不是教科书上那种字节是8位二进制的定义而是工控实战中真正会用到的字节与16进制知识怎么读懂抓包数据、怎么处理大小端、怎么把4字节转成浮点数、怎么排查无效字符这类报错、以及那些文档里不会写但现场一定会遇到的坑。适合刚入行的工控新人也适合做了几年但一直靠库函数黑盒处理数据、想搞明白底层原理的老手。看完你至少能做到拿到一串16进制数据能自己拆出它代表什么而不是只能复制粘贴到在线转换器里碰运气。2. 字节和16进制在工控里到底扮演什么角色2.1 为什么工控偏爱16进制而不是十进制先解决一个很多人没想明白的问题为什么工控协议、寄存器地址、报文数据几乎清一色用16进制表示而不是我们日常习惯的十进制答案藏在硬件层面。计算机和PLC内部所有数据本质上都是二进制一个字节就是8个bit。8个bit能表示0到255写成二进制是11111111写成十进制是255写成16进制是FF。你对比一下这三种写法二进制太长一串11001010看久了眼睛花十进制跟二进制之间的转换不直观你看到200根本反应不出它的bit位长啥样而16进制完美解决了这个问题——一个16进制位正好对应4个bit两个16进制位正好对应一个字节。这个对应关系是工控人必须刻进肌肉记忆里的16进制位二进制十进制000000100011.........910019A101010B101111C110012D110113E111014F111115所以当你看到0x3F脑子里应该立刻能拆成0011 1111也就是十进制的63。看到0xFF就是1111 1111255。这种一眼看穿bit的能力是后面处理位操作、掩码、状态字的基础。在Modbus RTU报文里这个优势体现得淋漓尽致。比如功能码03读保持寄存器的请求帧01 03 00 00 00 02 C4 0B每个字节都是两个16进制字符你能直接看出从站地址01、功能码03、起始地址0000、寄存器数量0002、CRC校验C40B。如果换成十进制写1 3 0 0 0 2 196 11你根本分不清哪个是地址哪个是数量可读性直接崩盘。2.2 一个字节的边界0到255背后的门道工控里大量数据是以字节为最小单位传输的但很多新手对一个字节能装什么没有清晰概念导致选型和处理时踩坑。一个字节8个bit无符号情况下范围是0到255。这个范围决定了它能直接表示的东西很有限一个0到255的整数、一个ASCII字符、8个独立的开关状态每个bit一个bool。但凡超过255的数值就必须用多个字节拼。这就是为什么工控里到处是2字节整数4字节整数4字节浮点数这些说法。这里有个特别容易混淆的点字Word和字节Byte的关系。在工控和Modbus语境里一个寄存器是16位也就是2个字节我们管它叫一个字。所以当你听到读两个字意思是读32位数据占两个连续的寄存器。而双字Double Word是32位4个字节。这个术语体系跟PC端编程里int是4字节的习惯不一样新人最容易在这里翻车。我见过一个真实案例某项目用Modbus读一个32位累计流量值工程师按读2个寄存器配置结果读回来的数一直是实际值的65536倍。原因就是他以为一个寄存器能装下32位实际上一个寄存器只有16位他把高16位当成了独立的一个值去解析自然错得离谱。记住Modbus一个寄存器16位2字节32位数据必须占两个寄存器。2.3 16进制字符串和真实字节的区别还有一个概念必须掰扯清楚否则后面处理无效字符报错时会一头雾水16进制字符串和真实字节是两码事。你在抓包工具里看到的01 03 00 00这是给人看的16进制字符串表示每个字节被渲染成两个可读字符。但在内存和传输线路上真实的数据是8个bit一组的二进制流。这两者之间的转换就是编码和解码。举个例子字符串01在内存里占2个字节分别是字符0ASCII 0x30和字符1ASCII 0x31。而真实的字节0x01在内存里只占1个字节值是1。如果你把字符串01直接当字节发给设备设备收到的是0x30 0x31完全不是你要的0x01。这个坑在串口通讯、Socket通讯里极其常见尤其是用Python、C#这类语言做上位机时字符串和字节数组的转换稍不注意就出错。提示任何时候往设备发数据先确认你手里的是16进制字符串还是字节数组。前者需要先转成后者才能发送转换规则是每两个字符合成一个字节。3. 大小端工控调试里最容易翻车的那道坎3.1 大端小端的本质谁在前谁在后大小端Endianness这个词听起来玄乎本质其实特别简单一个多字节数据在内存或报文里高位字节排在前面还是后面。假设有个32位整数0x12345678它由4个字节组成12最高位、34、56、78最低位。传输或存储时有两种排法大端Big-Endian高位在前12 34 56 78。这种排法符合人类阅读习惯从左到右就是从高位到低位。小端Little-Endian低位在前78 56 34 12。这是x86架构CPU的原生顺序PC端内存里就是这么存的。工控领域的麻烦在于不同厂商、不同协议的选择不统一。Modbus协议本身规定寄存器内部是大端高字节在前但一个32位数据跨两个寄存器时先发哪个寄存器协议没强制规定于是厂商各玩各的。西门子PLC默认大端三菱部分型号是小端施耐德又有一套自己的规则。这就是为什么同一个浮点数换个PLC读出来就变成乱码。3.2 四种字节序组合ABCD、CDAB、BADC、DCBA实战中处理32位数据时你会遇到四种常见的字节排列方式业内习惯用ABCD来标记排列方式字节顺序常见来源ABCD12 34 56 78标准大端Modbus部分设备CDAB56 78 12 34字交换很多国产仪表BADC34 12 78 56字节交换DCBA78 56 34 12标准小端x86这里的A、B、C、D分别代表原始数据的第1、2、3、4个字节从高位到低位。ABCD就是原样DCBA是完全颠倒CDAB是两个字AB和CD交换位置BADC是每个字内部两个字节交换。我处理过一个汇川变频器的项目读频率设定值文档写的是32位浮点数但没写字节序。我按标准ABCD解析读出来是-1.2e-38这种鬼数。换成CDAB一试立刻正常。后来翻手册才发现汇川默认是字交换模式。这种试错过程在工控里太常见了所以我的习惯是拿到一个不熟悉的设备先用一个已知的简单值比如设定频率50.0Hz去试四种排列哪个能解析出50.0就用哪个。3.3 用Python快速验证字节序的实操方法与其在PLC编程软件里反复下载调试不如用Python在电脑上先把字节序验证清楚。下面这段代码是我常用的验证脚本import struct # 假设从设备读回的4个字节16进制字符串形式 hex_str 42 48 00 00 # 这是50.0的大端表示 raw bytes.fromhex(hex_str.replace( , )) # 四种排列方式逐一尝试 # ABCD原样 print(ABCD:, struct.unpack(f, raw)[0]) # DCBA完全反转 print(DCBA:, struct.unpack(f, raw)[0]) # CDAB字交换 cdab raw[2:4] raw[0:2] print(CDAB:, struct.unpack(f, cdab)[0]) # BADC字节交换 badc bytes([raw[1], raw[0], raw[3], raw[2]]) print(BADC:, struct.unpack(f, badc)[0])运行结果里哪个输出接近你预期的物理值比如50.0设备用的就是哪种字节序。这个方法比在PLC里改来改去高效得多尤其是远程调试时能省下大量来回下载的时间。注意struct.unpack(f, ...)里的表示大端表示小端f表示4字节浮点数。这个格式字符表建议背下来处理二进制数据时天天用。3.4 浮点数的IEEE 754表示为什么50.0是0x42480000既然聊到浮点数就得说说IEEE 754标准否则你永远理解不了为什么50.0会变成42 48 00 00这种看起来毫无规律的东西。IEEE 754把32位浮点数分成三段1位符号位 8位指数位 23位尾数位。以50.0为例50.0的二进制是110010.0规格化后是1.100100 × 2^5符号位0正数指数位5 127偏移量 132 10000100尾数位去掉隐含的1取100100后面补0到23位 10010000000000000000000拼起来0 10000100 10010000000000000000000按4位一组转16进制就是0100 0010 0100 1000 0000 0000 0000 0000即42 48 00 00。看到没这就是为什么你读到的浮点数字节看起来乱七八糟——它根本不是简单的整数编码而是这种科学计数法的二进制变体。理解了这一点你就能明白为什么浮点数的字节序问题比整数更棘手整数颠倒字节还能看出点规律浮点数一颠倒就完全是个天文数字或接近0的极小值。4. 手把手拆解从一串16进制到真实物理值4.1 完整案例解析Modbus读回的4字节浮点数现在把前面所有知识串起来走一遍完整流程。假设你用Modbus RTU读某温湿度变送器功能码03读2个寄存器抓到的响应帧是01 03 04 41 C8 00 00 XX XX拆解一下01是从站地址03是功能码04是后续数据字节数4个字节41 C8 00 00是数据最后两字节是CRC。现在处理数据41 C8 00 00第一步判断字节序。先按大端ABCD试41 C8 00 00。用Python验证import struct raw bytes.fromhex(41C80000) print(struct.unpack(f, raw)[0]) # 输出 25.0结果是25.0符合温度变送器25摄氏度的预期说明这个设备用的是标准大端ABCD。第二步如果结果不对就依次试CDAB、BADC、DCBA。假设设备实际是CDAB那数据应该重排成00 00 41 C8解析出来是1.8e-41明显不对那就继续换。第三步确认物理量纲。25.0是原始值如果变送器量程是-40到80度那这个25.0就是直接的摄氏度值不需要额外换算。但有些设备返回的是放大10倍或100倍的整数那就得除以倍数。这个流程看起来简单但每一步都有坑。比如CRC校验如果你不先校验CRC就急着解析数据很可能解析的是一帧错误数据。再比如数据字节数04如果你读的是2个寄存器但设备只返回了1个寄存器的数据字节数会是02这时候你按4字节解析就会越界。4.2 高低字节拆分与合并的代码实现工控上位机开发中最频繁的操作就是把两个字节合成一个16位整数和把一个32位整数拆成4个字节。这些操作看似基础但写错了就是灾难。下面是我常用的工具函数def bytes_to_u16(high, low): 两个字节合成16位无符号整数高字节在前 return (high 8) | low def u16_to_bytes(value): 16位整数拆成高低两个字节 return [(value 8) 0xFF, value 0xFF] def bytes_to_u32(b3, b2, b1, b0): 4字节合成32位无符号整数b3为最高字节 return (b3 24) | (b2 16) | (b1 8) | b0 def bytes_to_i16(high, low): 两个字节合成16位有符号整数 val (high 8) | low if val 0x8000: val - 0x10000 return val这里有个细节值得说有符号和无符号的处理。16位数据如果最高位是1无符号解释是32768到65535有符号解释是-32768到-1。工控里温度、位置这类可能为负的物理量必须用有符号解析。我见过有人读一个可以到-20度的冷库温度结果一直显示65456这种数就是因为用了无符号解析。4.3 位操作从状态字里抠出每一个开关量工控里还有一种高频操作位解析。很多设备的状态字是一个16位寄存器每一位代表一个开关状态或报警标志。比如某伺服驱动器的状态字0x0233你需要知道哪些位是1。status 0x0233 # 检查第0位bit0 bit0 status 0x0001 # 检查第4位bit4 bit4 (status 4) 0x0001 # 检查第9位bit9 bit9 (status 9) 0x0001 # 或者用更直观的方式列出所有为1的位 for i in range(16): if status (1 i): print(fbit{i} 1)0x0233拆成二进制是0000 0010 0011 0011为1的位是bit0、bit1、bit4、bit9。对照手册你就能知道设备当前处于什么状态。这种位操作是工控上位机开发的基本功写多了自然就熟了。提示做位操作时是掩码提取|是置位 ~是清零^是翻转。这四个操作配合移位能搞定所有位级处理需求。5. 那些年踩过的坑无效字符、多字节字符集与CRC5.1 出现无效字符报错的真实原因热词里提到出现无效字符。多字节字符集必须包含一前导字节且无结尾字节这个报错在工控上位机开发里太典型了。它通常出现在字符串编码转换的场景比如你把设备返回的字节数组直接当UTF-8字符串解码而字节里恰好有不符合UTF-8规则的序列。举个例子设备返回的字节是0x81 0x40你试图用bytes.decode(utf-8)解码Python会直接抛UnicodeDecodeError因为0x81在UTF-8里是个多字节字符的前导字节但后面跟的0x40不是合法的后续字节。这就是多字节字符集必须包含一前导字节且无结尾字节的含义——解码器期待一个完整的字符序列结果只拿到半截。根本原因你把二进制数据当文本处理了。工控设备返回的字节绝大多数是二进制协议数据不是文本。正确的做法是用bytes类型处理需要显示时才转成16进制字符串raw b\x81\x40 # 错误做法直接解码 # text raw.decode(utf-8) # 抛异常 # 正确做法转16进制字符串显示 hex_display raw.hex() # 8140 print(hex_display)如果确实需要处理文本比如设备返回的是ASCII字符串那要先确认编码方式。工控里常见的是ASCII和GBKUTF-8反而不多。用错编码中文就会变乱码。5.2 CRC校验为什么你的报文总是被设备拒绝Modbus RTU的CRC校验是另一个高频坑点。很多新手自己拼报文发给设备设备毫无反应八成是CRC算错了。Modbus CRC是16位多项式0xA001反向的0x8005初始值0xFFFF。计算时对每个字节做异或和移位。下面是我验证过的实现def modbus_crc(data): crc 0xFFFF for byte in data: crc ^ byte for _ in range(8): if crc 0x0001: crc (crc 1) ^ 0xA001 else: crc 1 return crc # 测试01 03 00 00 00 02 frame bytes.fromhex(010300000002) crc modbus_crc(frame) # 结果应该是 0x0BC4 # 注意发送时低字节在前所以是 C4 0B print(fCRC: {crc:04X}) # 输出 0BC4注意最后一步CRC算出0x0BC4但发送时要低字节在前写成C4 0B。这是Modbus RTU的规矩很多人算对了CRC但顺序放反设备照样不认。这个低字节在前跟数据区的大端规则是矛盾的属于协议的特殊约定必须记牢。5.3 4字节无符号float数值范围的边界问题热词里还有个4字节无符号float数值范围这里其实有个概念混淆需要澄清IEEE 754的32位浮点数本身是有符号的没有无符号float这个标准类型。所谓4字节无符号float通常是指某些设备把浮点数的字节当成无符号整数来处理或者是指32位无符号整数的范围。32位无符号整数的范围是0到42949672950xFFFFFFFF。32位有符号整数是-2147483648到2147483647。而32位浮点数IEEE 754单精度能表示的范围大约是±1.18e-38到±3.4e38精度约7位有效数字。实战中要注意的是精度丢失。浮点数不能精确表示所有小数比如0.1在浮点里是近似值。如果你用浮点数累加100次0.1结果不会是10.0而是9.999999...。工控里做累计流量、电量计算时如果精度要求高建议用整数放大倍数而不是浮点数。类型字节数范围精度16位无符号整数20 ~ 65535精确16位有符号整数2-32768 ~ 32767精确32位无符号整数40 ~ 4294967295精确32位有符号整数4-2147483648 ~ 2147483647精确32位浮点数4±1.18e-38 ~ ±3.4e38约7位有效数字5.4 排查链路一帧数据解析失败的完整诊断过程把上面的坑串成一条排查链路下次遇到数据解析问题可以照着走确认物理层串口参数波特率、数据位、停止位、校验位对不对接线A/B有没有接反这一步错了后面全白搭。确认帧完整性收到的字节数对不对Modbus RTU帧有最小长度要求太短肯定是残帧。校验CRC先算CRC对不上直接丢弃别浪费时间解析错误数据。确认字节序用已知值试四种排列确定设备用哪种。确认数据类型是整数还是浮点数有符号还是无符号放大倍数是多少确认量纲解析出的原始值要不要做线性变换比如4-20mA对应0-100度。交叉验证改一个已知的物理量比如把传感器放到冰水里看解析值是否跟着变变化方向对不对。这条链路我用了很多年基本上90%的数据解析问题都能定位到具体环节。剩下的10%往往是设备本身的问题或者文档错误那就只能靠经验和厂商支持了。6. 把字节玩明白之后工控调试会变成什么样说实话刚入行那两年我对字节和16进制是有点发怵的。每次看到抓包数据就头大遇到浮点数解析错误就只会重启设备或者找同事帮忙。后来逼着自己把IEEE 754、大小端、CRC这些底层东西啃了一遍又用Python写了个小工具专门做字节序验证情况就完全不一样了。现在拿到一个陌生设备我的流程很固定先用调试助手发一帧最简单的读命令看返回的字节然后用Python脚本把可能的四种字节序都跑一遍哪个合理用哪个最后把解析逻辑封装成函数接到上位机里。整个过程快的话十几分钟就能搞定慢的话也就一两个小时。这种效率的提升本质上来自于对字节层面的掌控力——你不再是个只会调库的黑盒使用者而是能看穿数据本质的人。最后分享一个我个人的小习惯随身带一个16进制和二进制对照表手机备忘录里存一份电脑桌面放一份。刚开始需要经常查查着查着就记住了。等你看到0x0F能条件反射出0000 1111看到0x80知道是最高位为1看到0x7F知道是127那工控数据这块基本就出师了。剩下的就是多练、多踩坑、多总结每个项目都会让你对这套底层逻辑理解得更深一层。
阅读完成 · 觉得有帮助?
咨询建站