这个문제比你想象的更普遍
用 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 节"데이터 Encoding"说得非常坦诚:**"Modbus uses a 'big-endian' representation for 주소es and 데이터 items. This means that when a numerical 수량 larger than a single byte is transmitted, the most significant byte is sent first."** 翻译成人话:Modbus 在传输单个레지스터内的 16 位데이터时用大端序——高바이트在前。但사양没说 32 位或 64 位跨多个레지스터时怎么排。两个连续레지스터的先后次序、每个레지스터内部两个바이트的次序,全留给设备厂商自己决定。
这话听起来像甩锅,但 1979 年的 Modicon 엔지니어링师确实没必要操心 32 位浮点数——当年 PLC 连 16 位整数都嫌奢侈。四十年后,我们得自己扛。
四种바이트序,四张图说清楚
假设你유지 레지스터 읽기 (FC03)从주소 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 - 16진수
Binary - 2진수
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 位데이터时,先别纠结바이트序,先用16진수读出 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 프로토콜的范畴了,纯粹是적용层的编码选择。
디버깅方法论
别猜,用데이터说话。以下是一个표준的바이트序排错流程:
**第一步:读原始16진수**
用 Modbus Poll 把 Display 형식切成 Hex,先记下原始레지스터 값。不管바이트序,先看裸데이터。
读 40001-40002 返回: 41 DB 85 1F
**第二步:用已知值反推**
找一个你知道真实值的测点。比如当前온도 27.44°C。如果这个值是你从设备自带显示屏上读出来的,记下来。然后看四种序哪个拼出来是 27.44,那个就是正确答案。
更好的办法——主动写一个已知值。用기능 코드 16(写多个레지스터)쓰기 `0x3F80 0000`(这是 float32 = 1.0 的표준 IEEE 754 表示)到레지스터对,然后读回来。看原始16진수:
- 如果读回来是 `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
프로토콜 식별자: 0
Length: 7
Unit Identifier: 1
기능 코드: 3 (Read Holding 회원가입s)
Byte Count: 4
회원가입 0 (40001): 0x41db
회원가입 1 (40002): 0x851f
Wireshark 展示的레지스터 값是它按大端분석的结果。如果你的 PLC 内部是 BADC,那 Wireshark 展示的 `0x41db` 和 `0x851f` 已经不是原始바이트시퀀스了——原始바이트시퀀스应该是 `DB 41 1F 85`。
这就是为什么디버깅바이트序문제的时候,更应该看 **Wireshark 的16진수 dump** 而不是它帮你분석好的레지스터 값。在프레임详情里展开 "데이터" 필드,看原始바이트。
**第四步:在线바이트序계산器**
概念很简单——你입력 4 个16진수바이트,它给你算出四种序对应的 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 데이터[4] = {0x41, 0xDB, 0x85, 0x1F};
uint8_t abcd[4] = {데이터[0], 데이터[1], 데이터[2], 데이터[3]};
uint8_t dcba[4] = {데이터[3], 데이터[2], 데이터[1], 데이터[0]};
uint8_t badc[4] = {데이터[1], 데이터[0], 데이터[3], 데이터[2]};
uint8_t cdab[4] = {데이터[2], 데이터[3], 데이터[0], 데이터[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 *데이터, uint16_t len)
{
uint8_t crc_hi = 0xFF;
uint8_t crc_lo = 0xFF;
uint8_t idx;
while (len--) {
idx = crc_hi ^ *데이터++;
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,比你翻厂家手册快。
답글 작성