这个問題比你想象的更普遍
用 Modbus Poll 读到一个電力量計的正向有功总电量,register 40001~40002,戻る `42 C8 00 00`。你信心满满地按 IEEE 754 float32 一拼——100.0 kWh,合理。同一1つの手続き,换到另一家国产パワーメーター,読み取り同样的两1つのレジスタ,戻る `00 00 42 C8`,拼出来 0.0,PLC 的タッチパネル上直接给你画了条直线。你没写错レジスタ,也没算错 CRC,纯粹是バイトオーダー反了。
这就是 Modbus 多レジスタのデータの解析的核心痛点。Modbus protocol specification V1.1b No. 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 在传输单1つのレジスタ内的 16 ビット·データ时用ビッグエンディアン序——前の高さ。。但规范没说 32 位或 64 位跨多1つのレジスタ时怎么排。2つ连续レジスタ的先后次序、すべてのレジスタ。内部两バイト単位。的次序,全留给デバイス厂商自己决定。
这话听起来像甩锅,但 1979 年的 Modicon エンジニア确实没必要操心 32 ビット浮動小数点数——当年 PLC 连 16 ビット整数型都嫌奢侈。四十年后,我们得自己扛。
四种バイトオーダー,四张图说清楚
假设你リードホールドレジスタ住所から。 0(Modbus address 40001)开始,Request 2 Registers。スレーブ戻る的报文里的データ区是:
41 DB 85 1F
这是一个 IEEE 754 単精度浮動小数点数的元のバイトオーダー列。真实值是 27.440271...(约 27.44)。现在看四种バイトオーダー分别怎么拼:
ABCD(ビッグエンディアン序,Modbus 標準配列)
register 40001 的值是 `0x41DB`,register 40002 的值是 `0x851F`。按顺序拼成 32 位:
register 40001(高16位) register 40002(低16位)
+--------+--------+ +--------+--------+
| 0x41 | 0xDB | | 0x85 | 0x1F |
+--------+--------+ +--------+--------+
A B C D
内存视角(低アドレス到高アドレス):`41 DB 85 1F`
→ float32 = **27.44**
这是シュナイダー Modicon 系列 PLC(M340、M580、M241 等)的デフォルト行为。シュナイダー作为 Modbus protocol的亲爹,一直遵循这个標準配列——レジスター·アドレス小的存高 16 位,レジスター·アドレス大的存低 16 位。你用 READ_VAR 读回来2つ WORD,直接拼就是对的。
DCBA(リトルエンディアン序,完全反转)
register 40001(但内容反转了) register 40002
+--------+--------+ +--------+--------+
| 0x1F | 0x85 | | 0xDB | 0x41 |
+--------+--------+ +--------+--------+
内存视角:`1F 85 DB 41`
→ float32 = **1.5468 × 10⁻²⁸** ——一个接近零的极端值,屏显基本就是 0.0 或溢出报错。
部分国产 Modbus モジュール(尤其是一些基于 51 单片机或早期 ARM 裸跑的仪表)用这种方式。因为它们内部 CPU 是リトルエンディアン序,直接 memcpy 了事,本质上就是「懒得转换」。北京某ブランド的智能パワーメーター就是典型案例,你读回来必须手工做バイト逆转。
CDAB(Word exchange,レジスタ前后互换)
register 40002 的内容放前面 register 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 format,但 MB_SERVER 指令块在把レジスタを保持する。暴露给 Modbus クライアント側は时,**先发低アドレスレジスタ的低バイト数、再发ハイバイト,然后是高アドレスレジスタ的低バイト数、再ハイバイト**。这相当于把シュナイダー ABCD 的2つ 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 内部反转)
register 40001(内部バイトスワップ) register 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 内部反了一次。2つ WORD 之间有保持了 Modbus 的ビッグエンディアン次序。所以结果就是 BADC:每个 WORD 内部バイト逆转,WORD 之间保持 ABCD 的顺序。
打个不那么严谨的比方:シュナイダー是一整盒拼图按顺序给你(ABCD),シーメンス是左右两半先换位再给你(CDAB),台達/三菱/インバーター是把每半的内部先翻个面再按顺序给你(BADC)。至于 DCBA,那是连盒子带内容全反了。
データ型远比バイトオーダー更隐蔽
同一个 4 bytesSequence `41 DB 85 1F`,按不同データ型解析:
| 解析方式 | common error troubleshooting |
|---|---|
| float32(IEEE 754) | 27.44 |
| uint32 | 1,104,626,975 |
| int32 | 1,104,626,975 |
| 2つ uint16 拼接 | [16859, 34079] |
看这差异,float32 的 27.44 和 uint32 的 11 亿,差八个数量级。もし你在 SCADA 上看到一个温度测点表示 1104626975.0℃,别急着换センサーは——先查查是ではない用 uint32 解析了 float32。
这在实际工程中比バイトオーダー問題更隐蔽,因为**你拿到的是对的数,但解释方式是错的**。バイトオーダー正しくない,至少数字是乱的,你能一眼看出来;タイプ正しくない,有时候真就蒙混过去了。
举个现场的例子。某个光伏电站的逆变器経由 Modbus TCP 上送当日发电量,レジスタマップドキュメント写的是「40001-40002: 当日发电量,32 位」。エンジニア用 int32 Parse,读回来一个正数,数值看起来挺合理,上了能耗プラットフォーム跑了一个月。直到财务对账发现差了 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 protocol的发明者,行为最「標準」。QUANTUM、M340、M580、M241 全线产品的 Modbus TCP/RTU Communication,32 ビット·データ(REAL、DINT)デフォルト都是 **ABCD** 配列。用 READ_VAR 读回来的 `%MW` register,低ビット·アドレス存高 16 位,高ビット·アドレス存低 16 位。
特别注意:シュナイダー的 Unity Pro / EcoStruxure Control Expert 里,`%MD`(双字)和 `%MF`(浮点双字)的内部存储与 Modbus レジスタマップ有一层转换。`%MD0` 对应 `%MW0` 和 `%MW1`,among them `%MW0` 是高位,`%MW1` 是低位——恰好是 ABCD。
结论:シュナイダー PLC 做 Modbus スレーブ时,上游直接按 ABCD 解析即可。
シーメンス S7-1200/S7-1500(MB_SERVER)
这是坑最多的一个。シーメンス在 TIA Portal 中调用 `MB_SERVER` 指令块做 Modbus TCP Server,REAL 型変数の種類(4 bytes)在两レジスタを保持する。里的配列是 **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 slave,同样需要注意——你读回来的2つ WORD 的配列取决于スレーブ的バイトオーダー,和你シーメンス内部的 REAL 存储是两码事。这时候用博途 V16 及以上版本提供的 `READ_BIG` / `READ_LITTLE` / `WRITE_BIG` / `WRITE_LITTLE` 指令来做バイトオーダー変換,比手动 SWAP 加移位要可靠得多。
台達 DVP 系列
台達 DVP-ES2/EX2/SV2 等系列做 Modbus RTU スレーブ时,32 ビット浮動小数点数(F register)マッピング到 Modbus レジスタを保持する。的配列是 **BADC**。
这跟台達内部浮動小数点数的存储方式有关——台達 PLC 的 32 ビット·データ遵循「低位存低アドレス」的ルール(リトルエンディアン),而 Modbus RTU フレーム要求每个 WORD 以ビッグエンディアン送信。于是 WORD 内部バイト反转了一次,但2つ 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 Function(経由 RS-485 端子),用 ADPRW 命令する。通信时,读回来的 32 ビット·データ需要你用 MOV 指令手动拼。从 MODBUS 读回两1つのレジスタ D100、D101,存的是 BADC 配列的生データ,要先用 SWAP 指令交換すべてのレジスタ。的バイト,再用高低位拼接指令组合成最终的 32 位值。
我见过不止一个项目——三菱 PLC 读シュナイダー周波数変換器的频率,读回来反了又没做 SWAP,デバッグ两天最后发现就是バイトオーダー的锅。
オムロン CP1H/CP1L
オムロン CP1 系列経由 CP1W-CIF11(RS-485 选件板)做 Modbus RTU スレーブ时,レジスタを保持する。使用 DM ゾーンマッピング。32 ビット·データ在2つ DM 字中的配列取决于你如何プログラミング。
CP1 系列本身是字寻址架构,没ネイティブがある。的双字データ型。浮動小数点数要用2つ DM 字拼,配列顺序完全由你的ラダー図决定。但大多数標準実装遵循 **ABCD** 配列——这和 CP1 系列脱胎于早期的 SYSMAC 架构有关。实际デバッグ时建议先用已知值(比如写浮点 1.0 = `3F80 0000` 到2つ DM 字)测一次。
国产 PLC(インバーター、步科)
インバーター H3u/H5u 系列做 Modbus RTU slave,32 ビット·データデフォルト **BADC** 配列,跟台達一致。但インバーター的 AM600 系列(基于 CODESYS プラットフォーム)就不一样了——CODESYS 内部实数存储是標準 IEEE 754 ビッグエンディアン構造,Modbus TCP Server 暴露的レジスタ配列デフォルト是 **ABCD**。
インバーター Easy 系列属于 H5u 的简化版,用于小型デバイス控制,Modbus RTU スレーブ的浮動小数点数配列同样是 **BADC**。
步科(Kinco)有部分型号走 **CDAB**,比如 K2 系列。这点和シーメンス类似,因为步科 K2/K5 系列在软件层面借鉴了シーメンス的触屏プログラミング逻辑。
总结一个クイックチェック·テーブル:
| 厂商/device | 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 - hexadecimal
Binary - 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 Registers(8 bytes)来表示的场景,バイトオーダー問題更复杂。因为你要処理两层嵌套的配列:
第一层:每个 32 位 DWORD 内部的バイトオーダー(跟前面 32 位场景一样,四种可能) 第二层:2つ 32 位 DWORD 之间的先后次序(register 1-2 是高位 32 位まだ低位 32 位?)
假设你读 4 Registers,戻る元のバイトオーダー列:
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 1つのレジスタ按 ABCD 配列,从レジスター·アドレス低到高依次是:
register1 (MSW-High): A B → byte0-1
register2 (MSW-Low): C D → byte2-3 } 组成高32位
register3 (LSW-High): E F → byte4-5
register4 (LSW-Low): G H → byte6-7 } 组成低32位
拼成 uint64:`(uint64_t)(reg1<<48 | reg2<<32 | reg3<<16 | reg4)`,但这个公式のみ CPU 本身是ビッグエンディアン序时成立。你在 x86(リトルエンディアン)上写 C 代码,得倒过来。
实测经验:処理 64 ビット·データ时,先别纠结バイトオーダー,先用十六进制读出 4 1つのレジスタ的元の值,清楚累加器是否在增长,观察增长的方向——もし累加器每次递增,看变化是从低アドレスレジスタ开始まだ高アドレスレジスタ开始,就知道高低位配列了。
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 characters。比如序列号 "SN20240001"(10 个文字),需要 5 Registers:
register1: 0x53 0x4E → 'S' 'N'
register2: 0x32 0x30 → '2' '0'
register3: 0x32 0x34 → '2' '4'
register4: 0x30 0x30 → '0' '0'
register5: 0x30 0x31 → '0' '1'
读回来直接按バイト取 ASCII 值转文字就行,必要なし。关心バイトオーダー——ASCII characters `A` 就是 `0x41`,无论ビッグエンディアンリトルエンディアン。
**モード二:每レジスタ仅低バイト数有效**
某些仪表厂商偷懒,すべてのレジスタ。只用了低 8 位存一个文字,高 8 位是 0x00。同样的 "SN20240001" 需要 10 Registers:
register1: 0x00 0x53 → 空 'S'
register2: 0x00 0x4E → 空 'N'
...
这种方式浪费了一半的レジスタ空间,但解析简单——取すべてのレジスタ。的低バイト数就行。
踩坑提醒:ストリング·ストリング的バイトオーダー問題绝大多数时候不存在,因为 ASCII 是单バイト编码。真正会出問題的是你用 UTF-16 或者其他多バイト编码的场合——但那已经ではない Modbus protocol的范畴了,纯粹是アプリケーション層的编码選択。
デバッグ方法论
别猜,用データ说话。以下是一个標準的バイトオーダー排错流程:
**最初のステップ:读元の十六进制**
用 Modbus Poll 把 Display 格式切成 Hex,先记下オリジナルのレジスタ値。不管バイトオーダー,先看裸データ。
读 40001-40002 戻る: 41 DB 85 1F
**第二のステップ:用既知値の推定値**
找一个你知道真实值的测点。比如当前温度 27.44°C。もし这个值是你从デバイス自带表示屏上读出来的,记下来。然后看四种序哪个拼出来是 27.44,那个就是正确答案。
更好的办法——主动写一个已知值。用機能コード 16(write multiple registers)写入 `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 message:
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" field,看元のバイト。
**第四のステップ:オンラインバイトオーダー計算器**
概念很简单——你入力 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 verification的バイトオーダー混在一起——ではない说算法有問題,而是 **CRC calculation结果的两バイト単位。在报文里谁先发谁后发**,不同ドキュメント写的不一样。
Modbus 规范明确:CRC-16 的**低バイト数先发**。也就是说 CRC calculation结果是 `0x1234`,那么报文里最后2つbyteは `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
};
/*
* result 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 verification总是错」的原因之一。
有問題再聊。下次遇到バイトオーダー正しくない的情况,直接拿这四バイト単位。跑一圈 ABCD/DCBA/BADC/CDAB,比你翻厂家手册快。
Leave a Reply