Modbus 多レジスタのデータの解析终极ガイド:endianness(Endianness)完全に。ソリューション

freeFree Technical Resource

This content is free to read, suitable for basic learning and search traffic.

这个問題比你想象的更普遍

用 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
uint321,104,626,975
int321,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 系列在软件层面借鉴了シーメンス的触屏プログラミング逻辑。

总结一个クイックチェック·テーブル:

厂商/deviceModbus 32位配列備考
シュナイダー M340/M580/M241ABCDModbus 原厂標準
シーメンス S7-1200/1500CDABMB_SERVER 指令块行为
台達 DVP 系列BADC字内バイトスワップ
三菱 FX3U/FX5UBADC需 SWAP 后拼接
オムロン CP1H/CP1LABCD(多数実装)取决于ラダー図
インバーター H3u/H5uBADC与台達一致
インバーター AM600 (CODESYS)ABCDCODESYS プラットフォーム
步科 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,比你翻厂家手册快。

Put this resource to use in a real project?

Go to the Tool Center for message parsing, CRC verification and device debugging, or submit your requirements for selection and integration advice.

Engineer Membership

Turn this article into actionable debugging resources

After activation, you can use advanced message parsing, resource pack downloads, code examples, engineering cases and priority technical support, suitable for real project delivery.

Unlimited Advanced Tools
Resource & Code Packs
Complete Engineering Case Library
Priority Technical Support

Leave a Reply

Your email address will not be published. Required fields are marked *.