- 1. 这个问题比你想象的更普遍
- 2. 四种字节序,四张图说清楚
- 3. ABCD(大端序,Modbus 标准排列)
- 4. DCBA(小端序,完全反转)
- 5. CDAB(字交换,寄存器前后互换)
- 6. BADC(字节交换,每个 WORD 内部反转)
- 7. 数据类型远比字节序更隐蔽
- 8. 各主流厂商的实际行为
- 9. 施耐德 Modicon M340/M580/M241
- 10. 西门子 S7-1200/S7-1500(MB_SERVER)
- 11. 台达 DVP 系列
- 12. 三菱 FX3U/FX5U(通过 Modbus 通信模块)
- 13. 欧姆龙 CP1H/CP1L
- 14. 国产 PLC(汇川、步科)
- 15. Modbus Poll 的字节序切换
- 16. 64 位数据的处理
- 17. 字符串的存储
- 18. 调试方法论
- 19. 附录:Modbus CRC-16 查表法 C 语言实现
这个问题比你想象的更普遍
用 Modbus Poll 读到一个电能表的正向有功总电量,寄存器 40001~40002,返回 `42 C8 00 00`。你信心满满地按 IEEE 754 float32 一拼——100.0 kWh,合理。同一个程序,换到另一家国产电力仪表,读取同样的两个寄存器,返回 `00 00 42 C8`,拼出来 0.0,PLC 的触摸屏上直接给你画了条直线。你没写错寄存器,也没算错 CRC,纯粹是字节序反了。
这就是 Modbus 多寄存器数据解析的核心痛点。Modbus 协议规范 V1.1b 第 4.2 节”Data Encoding”说得非常坦诚:**”Modbus uses a ‘big-endian’ representation for addresses and data items. This means that when a numerical quantity larger than a single byte is transmitted, the most significant byte is sent first.”** 翻译成人话:Modbus 在传输单个寄存器内的 16 位数据时用大端序——高字节在前。但规范没说 32 位或 64 位跨多个寄存器时怎么排。两个连续寄存器的先后次序、每个寄存器内部两个字节的次序,全留给设备厂商自己决定。
这话听起来像甩锅,但 1979 年的 Modicon 工程师确实没必要操心 32 位浮点数——当年 PLC 连 16 位整数都嫌奢侈。四十年后,我们得自己扛。
四种字节序,四张图说清楚
假设你读取保持寄存器从地址 0(Modbus 地址 40001)开始,请求 2 个寄存器。从站返回的报文里的数据区是:
41 DB 85 1F
这是一个 IEEE 754 单精度浮点数的原始字节序列。真实值是 27.440271…(约 27.44)。现在看四种字节序分别怎么拼:
ABCD(大端序,Modbus 标准排列)
寄存器 40001 的值是 `0x41DB`,寄存器 40002 的值是 `0x851F`。按顺序拼成 32 位:
寄存器 40001(高16位) 寄存器 40002(低16位)
+--------+--------+ +--------+--------+
| 0x41 | 0xDB | | 0x85 | 0x1F |
+--------+--------+ +--------+--------+
A B C D
内存视角(低地址到高地址):`41 DB 85 1F`
→ float32 = **27.44**
这是施耐德 Modicon 系列 PLC(M340、M580、M241 等)的默认行为。施耐德作为 Modbus 协议的亲爹,一直遵循这个标准排列——寄存器地址小的存高 16 位,寄存器地址大的存低 16 位。你用 READ_VAR 读回来两个 WORD,直接拼就是对的。
DCBA(小端序,完全反转)
寄存器 40001(但内容反转了) 寄存器 40002
+--------+--------+ +--------+--------+
| 0x1F | 0x85 | | 0xDB | 0x41 |
+--------+--------+ +--------+--------+
内存视角:`1F 85 DB 41`
→ float32 = **1.5468 × 10⁻²⁸** ——一个接近零的极端值,屏显基本就是 0.0 或溢出报错。
部分国产 Modbus 模块(尤其是一些基于 51 单片机或早期 ARM 裸跑的仪表)用这种方式。因为它们内部 CPU 是小端序,直接 memcpy 了事,本质上就是「懒得转换」。北京某品牌的智能电力仪表就是典型案例,你读回来必须手工做字节逆转。
CDAB(字交换,寄存器前后互换)
寄存器 40002 的内容放前面 寄存器 40001 的内容放后面
+--------+--------+ +--------+--------+
| 0x85 | 0x1F | | 0x41 | 0xDB |
+--------+--------+ +--------+--------+
内存视角:`85 1F 41 DB`
→ float32 = **-1.096 × 10⁻³³**
这是西门子 S7-1200/S7-1500 做 Modbus TCP Server 时的实际行为。西门子 PLC 内部 REAL 型变量占用 4 个字节,存到 DB 块里是标准 IEEE 754 格式,但 MB_SERVER 指令块在把保持寄存器暴露给 Modbus 客户端时,**先发低地址寄存器的低字节、再发高字节,然后是高地址寄存器的低字节、再高字节**。这相当于把施耐德 ABCD 的两个 WORD 前后互换。你用 Modbus Poll 读西门子 PLC 的浮点数,如果不把 Display 设置里的 Float 切换到 “CDAB” 模式,读出来一定是天文数字。
libmodbus 专门为这四种序提供了四组函数:`modbus_get_float_abcd()`、`modbus_get_float_dcba()`、`modbus_get_float_badc()`、`modbus_get_float_cdab()`。
BADC(字节交换,每个 WORD 内部反转)
寄存器 40001(内部字节交换) 寄存器 40002(内部字节交换)
+--------+--------+ +--------+--------+
| 0xDB | 0x41 | | 0x1F | 0x85 |
+--------+--------+ +--------+--------+
内存视角:`DB 41 1F 85`
→ float32 = **-1.457 × 10¹⁷**
台达 DVP 系列、三菱 FX3U/FX5U 以及汇川部分系列(H3u、H5u)在通过 Modbus RTU 传输 32 位数据时,实际走的就是 BADC 排列。为什么?因为这些日系/台系 PLC 的内部数据存储本身就是「字内小端」——单个 16 位 WORD 内部低字节在前。到了 Modbus 传输层,先发 WORD 的高字节(Modbus 要求),于是 WORD 内部反了一次。两个 WORD 之间有保持了 Modbus 的大端次序。所以结果就是 BADC:每个 WORD 内部字节逆转,WORD 之间保持 ABCD 的顺序。
打个不那么严谨的比方:施耐德是一整盒拼图按顺序给你(ABCD),西门子是左右两半先换位再给你(CDAB),台达/三菱/汇川是把每半的内部先翻个面再按顺序给你(BADC)。至于 DCBA,那是连盒子带内容全反了。
数据类型远比字节序更隐蔽
同一个 4 字节序列 `41 DB 85 1F`,按不同数据类型解析:
| 解析方式 | 结果 |
|---|---|
| float32(IEEE 754) | 27.44 |
| uint32 | 1,104,626,975 |
| int32 | 1,104,626,975 |
| 两个 uint16 拼接 | [16859, 34079] |
看这差异,float32 的 27.44 和 uint32 的 11 亿,差八个数量级。如果你在 SCADA 上看到一个温度测点显示 1104626975.0℃,别急着换传感器——先查查是不是用 uint32 解析了 float32。
这在实际工程中比字节序问题更隐蔽,因为**你拿到的是对的数,但解释方式是错的**。字节序不对,至少数字是乱的,你能一眼看出来;类型不对,有时候真就蒙混过去了。
举个现场的例子。某个光伏电站的逆变器通过 Modbus TCP 上送当日发电量,寄存器映射文档写的是「40001-40002: 当日发电量,32 位」。工程师用 int32 解析,读回来一个正数,数值看起来挺合理,上了能耗平台跑了一个月。直到财务对账发现差了 30%,一查才知道——厂家实际存的是 **uint32**,按 int32 解析时高位被当成符号位处理了,只要发电量超过 2,147,483,647 Wh(2.15 GWh),解析值就变成负数。
所以面对一个多寄存器数据点,你需要确认的不是一个变量,是三个:
1. 数据类型是什么(float32 / int32 / uint32 / int64 / uint64 / double64 / string)? 2. 字节序是什么(四种之一)? 3. 有没有缩放因子?比如仪表存的是原始值 × 10 还是 × 100?
第三点经常被忽略。很多 Modbus 仪表为了省寄存器、避免浮点传输,会把浮点值乘以 10 或 100 后转成 int32 存进去。比如读回来 int32 = 2744,实际温度是 27.44℃(÷100),压力是 274.4kPa(÷10)。这个缩放因子只在设备手册里写,协议报文上完全看不出来。
各主流厂商的实际行为
这一节是这篇文章最有实用价值的部分。以下数据来自实际调试记录和社区验证,不是产品手册上抄来的。
施耐德 Modicon M340/M580/M241
Modbus 协议的发明者,行为最「标准」。QUANTUM、M340、M580、M241 全线产品的 Modbus TCP/RTU 通信,32 位数据(REAL、DINT)默认都是 **ABCD** 排列。用 READ_VAR 读回来的 `%MW` 寄存器,低位地址存高 16 位,高位地址存低 16 位。
特别注意:施耐德的 Unity Pro / EcoStruxure Control Expert 里,`%MD`(双字)和 `%MF`(浮点双字)的内部存储与 Modbus 寄存器映射有一层转换。`%MD0` 对应 `%MW0` 和 `%MW1`,其中 `%MW0` 是高位,`%MW1` 是低位——恰好是 ABCD。
结论:施耐德 PLC 做 Modbus 从站时,上游直接按 ABCD 解析即可。
西门子 S7-1200/S7-1500(MB_SERVER)
这是坑最多的一个。西门子在 TIA Portal 中调用 `MB_SERVER` 指令块做 Modbus TCP 服务器,REAL 型变量(4 字节)在两个保持寄存器里的排列是 **CDAB**。
具体原理:西门子 S7-1200/1500 的 DB 块默认是「优化的块访问」,内部字节排列不透明。即使你取消了优化(设置为标准访问),REAL 型变量在 DB 中的字节顺序也是小端(Intel x86 处理器决定的)。当 `MB_SERVER` 把这个 REAL 的 4 个字节映射到 Modbus 保持寄存器时,它按字节顺序依次填充:地址小的寄存器存字节 0 和 1(这是 REAL 的低位部分),地址大的寄存器存字节 2 和 3(高位部分)。
实际效果就是 CDAB。Modbus Poll 的 Display 设置里必须切到 “CDAB” 才能看到正确的浮点数。
如果你用西门子 PLC 做 Modbus 客户端(`MB_CLIENT`)去读第三方的 Modbus 从站,同样需要注意——你读回来的两个 WORD 的排列取决于从站的字节序,和你西门子内部的 REAL 存储是两码事。这时候用博途 V16 及以上版本提供的 `READ_BIG` / `READ_LITTLE` / `WRITE_BIG` / `WRITE_LITTLE` 指令来做字节序转换,比手动 SWAP 加移位要可靠得多。
台达 DVP 系列
台达 DVP-ES2/EX2/SV2 等系列做 Modbus RTU 从站时,32 位浮点数(F 寄存器)映射到 Modbus 保持寄存器的排列是 **BADC**。
这跟台达内部浮点数的存储方式有关——台达 PLC 的 32 位数据遵循「低位存低地址」的规则(小端),而 Modbus RTU 帧要求每个 WORD 以大端发送。于是 WORD 内部字节反转了一次,但两个 WORD 之间保持了 PLC 内部顺序,最终变成 BADC。
如果你的 WPLSoft 里显示 D0 = F100.0(浮点数),在 Modbus Poll 里读 40001~40002,Display 选 “BADC”,看到的就是 100.0。选其他三种序都是乱码。
三菱 FX3U/FX5U(通过 Modbus 通信模块)
三菱 FX3U 加装 FX3U-485ADP-MB 通信模块做 Modbus RTU 主站或从站时,32 位数据的排列也是 **BADC**。跟台达的逻辑一样——日系 PLC 的内部架构均为小端。
FX5U 本体自带 Modbus RTU 功能(通过 RS-485 端子),用 ADPRW 指令做通信时,读回来的 32 位数据需要你用 MOV 指令手动拼。从 MODBUS 读回两个寄存器 D100、D101,存的是 BADC 排列的原始数据,要先用 SWAP 指令交换每个寄存器的字节,再用高低位拼接指令组合成最终的 32 位值。
我见过不止一个项目——三菱 PLC 读施耐德变频器的频率,读回来反了又没做 SWAP,调试两天最后发现就是字节序的锅。
欧姆龙 CP1H/CP1L
欧姆龙 CP1 系列通过 CP1W-CIF11(RS-485 选件板)做 Modbus RTU 从站时,保持寄存器使用 DM 区映射。32 位数据在两个 DM 字中的排列取决于你如何编程。
CP1 系列本身是字寻址架构,没有原生的双字数据类型。浮点数要用两个 DM 字拼,排列顺序完全由你的梯形图决定。但大多数标准实现遵循 **ABCD** 排列——这和 CP1 系列脱胎于早期的 SYSMAC 架构有关。实际调试时建议先用已知值(比如写浮点 1.0 = `3F80 0000` 到两个 DM 字)测一次。
国产 PLC(汇川、步科)
汇川 H3u/H5u 系列做 Modbus RTU 从站,32 位数据默认 **BADC** 排列,跟台达一致。但汇川的 AM600 系列(基于 CODESYS 平台)就不一样了——CODESYS 内部实数存储是标准 IEEE 754 大端结构,Modbus TCP Server 暴露的寄存器排列默认是 **ABCD**。
汇川 Easy 系列属于 H5u 的简化版,用于小型设备控制,Modbus RTU 从站的浮点数排列同样是 **BADC**。
步科(Kinco)有部分型号走 **CDAB**,比如 K2 系列。这点和西门子类似,因为步科 K2/K5 系列在软件层面借鉴了西门子的触屏编程逻辑。
总结一个速查表:
| 厂商/设备 | Modbus 32位排列 | 备注 |
|---|---|---|
| 施耐德 M340/M580/M241 | ABCD | Modbus 原厂标准 |
| 西门子 S7-1200/1500 | CDAB | MB_SERVER 指令块行为 |
| 台达 DVP 系列 | BADC | 字内字节交换 |
| 三菱 FX3U/FX5U | BADC | 需 SWAP 后拼接 |
| 欧姆龙 CP1H/CP1L | ABCD(多数实现) | 取决于梯形图 |
| 汇川 H3u/H5u | BADC | 与台达一致 |
| 汇川 AM600 (CODESYS) | ABCD | CODESYS 平台 |
| 步科 K2 系列 | CDAB | 部分型号 |
| 典型国产电力仪表 | DCBA 或 ABCD | 需逐个测试 |
注意:这个表是实测参考,不是官方承诺。同一厂商的不同型号、不同固件版本,字节序行为可能不同。上生产环境之前一定要测试验证。
Modbus Poll 的字节序切换
Modbus Poll 在 Display 设置里提供了数据格式切换功能。选中数据显示区域后右键 → Format,可以看到:
Signed - 有符号 16 位整数
Unsigned - 无符号 16 位整数
Hex - 十六进制
Binary - 二进制
32-bit Signed - 32 位有符号整数
32-bit Unsigned - 32 位无符号整数
32-bit Float - 32 位浮点数
64-bit Float - 64 位双精度浮点数
选了 32-bit Float 后,右侧会弹出字节序选项:
Float Big-endian (ABCD) - 大端
Float Little-endian (DCBA) - 小端
Float Big-endian byte swap (BADC) - 大端字节交换
Float Little-endian byte swap (CDAB) - 小端字节交换
你不需要记住哪个厂商用哪种序。调试的时候四种序都切一遍,看哪个值合理就是哪个。读一个你确定量程范围的已知测点(比如环境温度在 20~30℃ 之间),四种序里只有一种会落在合理范围。
64 位数据的处理
电量累计值、总运行时间、大容量流量累积——这些动辄需要用 4 个寄存器(8 字节)来表示的场景,字节序问题更复杂。因为你要处理两层嵌套的排列:
第一层:每个 32 位 DWORD 内部的字节序(跟前面 32 位场景一样,四种可能) 第二层:两个 32 位 DWORD 之间的先后次序(寄存器 1-2 是高位 32 位还是低位 32 位?)
假设你读 4 个寄存器,返回原始字节序列:
41 DB 85 1F 42 C8 00 00
如果按照「先高 32 位后低 32 位、每个 32 位按 ABCD」——这是最规矩的排列:
高32位 (ABCD): 41 DB 85 1F → float32 = 27.44
低32位 (ABCD): 42 C8 00 00 → float32 = 100.0
注意:这是一个 totalizer(累加器)的典型构建方式。高 32 位存大数部分,低 32 位存小数部分,或者高 32 位存整数部分、低 32 位存小数部分。具体怎么组合要看设备手册。
更常见的场景是 uint64 累计值。4 个寄存器按 ABCD 排列,从寄存器地址低到高依次是:
寄存器1 (MSW-High): A B → 字节0-1
寄存器2 (MSW-Low): C D → 字节2-3 } 组成高32位
寄存器3 (LSW-High): E F → 字节4-5
寄存器4 (LSW-Low): G H → 字节6-7 } 组成低32位
拼成 uint64:`(uint64_t)(reg1<<48 | reg2<<32 | reg3<<16 | reg4)`,但这个公式只在 CPU 本身是大端序时成立。你在 x86(小端)上写 C 代码,得倒过来。
实测经验:处理 64 位数据时,先别纠结字节序,先用十六进制读出 4 个寄存器的原始值,清楚累加器是否在增长,观察增长的方向——如果累加器每次递增,看变化是从低地址寄存器开始还是高地址寄存器开始,就知道高低位排列了。
libmodbus 没有直接提供 64 位的转换函数,你需要自己拼:
uint16_t regs[4];
modbus_read_registers(ctx, addr, 4, regs);
// 假设 ABCD 排列
uint64_t value = ((uint64_t)regs[0] << 48) |
((uint64_t)regs[1] << 32) |
((uint64_t)regs[2] << 16) |
((uint64_t)regs[3]);
但这只是四种序中的一种。如果你的设备是 BADC 排列,得先交换每个寄存器的字节;如果是 CDAB,得先交换寄存器对的位置。
字符串的存储
设备序列号、固件版本号这些 ASCII 字符串在 Modbus 寄存器里的存储,有两种主流模式:
**模式一:每寄存器存 2 个字符(标准做法)**
每个 16 位寄存器存两个字节,每字节一个 ASCII 字符。比如序列号 “SN20240001″(10 个字符),需要 5 个寄存器:
寄存器1: 0x53 0x4E → 'S' 'N'
寄存器2: 0x32 0x30 → '2' '0'
寄存器3: 0x32 0x34 → '2' '4'
寄存器4: 0x30 0x30 → '0' '0'
寄存器5: 0x30 0x31 → '0' '1'
读回来直接按字节取 ASCII 值转字符就行,不需要关心字节序——ASCII 字符 `A` 就是 `0x41`,无论大端小端。
**模式二:每寄存器仅低字节有效**
某些仪表厂商偷懒,每个寄存器只用了低 8 位存一个字符,高 8 位是 0x00。同样的 “SN20240001” 需要 10 个寄存器:
寄存器1: 0x00 0x53 → 空 'S'
寄存器2: 0x00 0x4E → 空 'N'
...
这种方式浪费了一半的寄存器空间,但解析简单——取每个寄存器的低字节就行。
踩坑提醒:字符串的字节序问题绝大多数时候不存在,因为 ASCII 是单字节编码。真正会出问题的是你用 UTF-16 或者其他多字节编码的场合——但那已经不是 Modbus 协议的范畴了,纯粹是应用层的编码选择。
调试方法论
别猜,用数据说话。以下是一个标准的字节序排错流程:
**第一步:读原始十六进制**
用 Modbus Poll 把 Display 格式切成 Hex,先记下原始寄存器值。不管字节序,先看裸数据。
读 40001-40002 返回: 41 DB 85 1F
**第二步:用已知值反推**
找一个你知道真实值的测点。比如当前温度 27.44°C。如果这个值是你从设备自带显示屏上读出来的,记下来。然后看四种序哪个拼出来是 27.44,那个就是正确答案。
更好的办法——主动写一个已知值。用功能码 16(写多个寄存器)写入 `0x3F80 0000`(这是 float32 = 1.0 的标准 IEEE 754 表示)到寄存器对,然后读回来。看原始十六进制:
– 如果读回来是 `3F 80 00 00` → ABCD – 如果读回来是 `00 00 3F 80` → DCBA – 如果读回来是 `80 3F 00 00` → BADC – 如果读回来是 `00 00 80 3F` → CDAB
**第三步:用 Wireshark 抓包**
Modbus TCP 的端口是 502。在 Wireshark 里过滤 `tcp.port == 502`,右键 → Decode As → Modbus/TCP。你看到的响应报文里,数据区就是原始字节序列。拿这个序列跟你的解析结果对比:
Wireshark 报文:
Modbus/TCP
Transaction Identifier: 1
Protocol Identifier: 0
Length: 7
Unit Identifier: 1
Function Code: 3 (Read Holding Registers)
Byte Count: 4
Register 0 (40001): 0x41db
Register 1 (40002): 0x851f
Wireshark 展示的寄存器值是它按大端解析的结果。如果你的 PLC 内部是 BADC,那 Wireshark 展示的 `0x41db` 和 `0x851f` 已经不是原始字节序列了——原始字节序列应该是 `DB 41 1F 85`。
这就是为什么调试字节序问题的时候,更应该看 **Wireshark 的十六进制 dump** 而不是它帮你解析好的寄存器值。在报文详情里展开 “Data” 字段,看原始字节。
**第四步:在线字节序计算器**
概念很简单——你输入 4 个十六进制字节,它给你算出四种序对应的 float32、int32、uint32 值,一眼看出哪个是正确答案。
如果没有现成的,自己写一个也不复杂:
#include <stdio.h>
#include <stdint.h>
#include <string.h>
void print_float(uint8_t *bytes) {
float f;
memcpy(&f, bytes, 4);
printf("float32: %fn", f);
}
int main() {
uint8_t data[4] = {0x41, 0xDB, 0x85, 0x1F};
uint8_t abcd[4] = {data[0], data[1], data[2], data[3]};
uint8_t dcba[4] = {data[3], data[2], data[1], data[0]};
uint8_t badc[4] = {data[1], data[0], data[3], data[2]};
uint8_t cdab[4] = {data[2], data[3], data[0], data[1]};
printf("ABCD: "); print_float(abcd);
printf("DCBA: "); print_float(dcba);
printf("BADC: "); print_float(badc);
printf("CDAB: "); print_float(cdab);
return 0;
}
附录:Modbus CRC-16 查表法 C 语言实现
字节序问题经常和 CRC 校验的字节序混在一起——不是说算法有问题,而是 **CRC 计算结果的两个字节在报文里谁先发谁后发**,不同文档写的不一样。
Modbus 规范明确:CRC-16 的**低字节先发**。也就是说 CRC 计算结果是 `0x1234`,那么报文里最后两个字节是 `34 12`,不是 `12 34`。
下面的查表法实现直接返回符合 Modbus 规范的结果——低字节在前,可以直接追加到报文末尾。
#include <stdint.h>
/* CRC-16 Modbus 查找表 */
static const uint16_t crc16_table[256] = {
0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040
};
/*
* 计算 Modbus CRC-16,返回符合 Modbus 帧格式的结果
* 低字节在前(低地址),可直接追加到报文末尾
*/
uint16_t crc16_modbus(const uint8_t *data, uint16_t len)
{
uint8_t crc_hi = 0xFF;
uint8_t crc_lo = 0xFF;
uint8_t idx;
while (len--) {
idx = crc_hi ^ *data++;
crc_hi = crc_lo ^ (uint8_t)(crc16_table[idx] >> 8);
crc_lo = (uint8_t)(crc16_table[idx] & 0xFF);
}
/*
* 返回 16 位 CRC 值
* 使用时追加到 Modbus RTU 帧末尾:
* frame[len] = crc & 0xFF; // 低字节在前
* frame[len+1] = (crc >> 8) & 0xFF; // 高字节在后
*/
return ((uint16_t)crc_lo << 8) | crc_hi;
}
这里有个细节值得注意:如果你看其他一些 CRC-16 的实现,有的返回结果是高字节在前、有的低字节在前。区别在于 `return` 语句——`(crc_lo << 8) | crc_hi` 和 `(crc_hi << 8) | crc_lo` 是反的。Modbus 规范说 CRC 域是「低字节在前」,所以上面这个实现返回一个 uint16_t,但你知道低 8 位是 CRC 的低字节,高 8 位是 CRC 的高字节。追加到报文时,先写 `(crc & 0xFF)`,再写 `((crc >> 8) & 0xFF)`。
如果你用 `return (crc_hi << 8) | crc_lo`,那追加顺序就得反过来——这也是很多调试时发现「CRC 校验总是错」的原因之一。
有问题再聊。下次遇到字节序不对的情况,直接拿这四个字节跑一圈 ABCD/DCBA/BADC/CDAB,比你翻厂家手册快。
发表回复