- 1. 这个问题比你想象的更普遍
- 2. 四种Befehlseite,四张图说清楚
- 3. ABCD(大端序,Modbus 标准排列)
- 4. DCBA(小端序,完全反转)
- 5. CDAB(Wort ausgetauscht.,Register.前后互换)
- 6. BADC(Bytes交换,每个 WORD 内部反转)
- 7. Datentyp远比Befehlseite更隐蔽
- 8. 各主流厂商的实际行为
- 9. 施耐德 Modicon M340/M580/M241
- 10. 西门子 S7-1200/S7-1500(MB_SERVER)
- 11. 台达 DVP 系列
- 12. 三菱 FX3U/FX5U(通过 Modbus Kommunikation模块)
- 13. 欧姆龙 CP1H/CP1L
- 14. 国产 PLC(汇川、步科)
- 15. Modbus Poll 的BefehlseiteWechseln
- 16. 64 位Daten的处理
- 17. Charaktere串的存储
- 18. Tests方法论
- 19. 附录:Modbus CRC-16 查表法 C 语言实现
这个问题比你想象的更普遍
用 Modbus Poll 读zu一个电能表的正向有功总电Quantität,Register. 40001~40002,返回 `42 C8 00 00`。你信心满满地按 IEEE 754 float32 一拼——100.0 kWh,合理。同一个程序,换zu另一家国产电力仪表,Lesen同样的两ein Register.,返回 `00 00 42 C8`,拼出来 0.0,PLC 的触摸屏上直接给你画了条直线。你没写错Register.,也没算错 CRC,纯粹是Befehlseite反了。
这就是 Modbus 多RegisterdatenAnalyse的核心痛点。Modbus-Protokollspezifikation V1.1b Die 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 在传输单ein Register.内的 16 位Daten时用大端序——hohe Bytes在前。但规范没说 32 位或 64 位跨多ein Register.时怎么排。两个连续Register.的先后次序、每ein Register.内部两个Bytes的次序,全留给Ausrüstung厂商自己决定。
这话听起来像甩锅,但 1979 Jahr的 Modicon 工程师确实没必要操心 32 位浮点数——当Jahr PLC 连 16 位整数都嫌奢侈。四十Jahr后,我们得自己扛。
四种Befehlseite,四张图说清楚
假设你Lesen und halten Register从Adresse 0(Modbus Adresse 40001)开始,Bitte 2 ein Register.。Von der Station返回的Meldung里的Datenzone是:
41 DB 85 1F
这是一个 IEEE 754 单精度浮点数的原始Befehlseite列。真实值是 27.440271…(约 27.44)。现在看四种BefehlseiteUnterteilung别怎么拼:
ABCD(大端序,Modbus 标准排列)
Register. 40001 的值是 `0x41DB`,Register. 40002 的值是 `0x851F`。按顺序拼成 32 位:
Register. 40001(高16位) Register. 40002(低16位)
+--------+--------+ +--------+--------+
| 0x41 | 0xDB | | 0x85 | 0x1F |
+--------+--------+ +--------+--------+
A B C D
内存视角(低Adressezu高Adresse):`41 DB 85 1F`
→ float32 = **27.44**
这是施耐德 Modicon 系列 PLC(M340、M580、M241 等)的默认行为。施耐德作为 Das Modbus-Protokoll的亲爹,一直遵循这个标准排列——Register-Adresse小的存高 16 位,Register-Adresse大的存低 16 位。你用 READ_VAR 读回来两个 WORD,直接拼就是对的。
DCBA(小端序,完全反转)
Register. 40001(但内容反转了) Register. 40002
+--------+--------+ +--------+--------+
| 0x1F | 0x85 | | 0xDB | 0x41 |
+--------+--------+ +--------+--------+
内存视角:`1F 85 DB 41`
→ float32 = **1.5468 × 10⁻²⁸** ——一个接近零的极端值,屏显基本就是 0.0 或溢出报错。
部Unterteilung国产 Modbus 模块(尤其是一些基于 51 单片机或早期 ARM 裸跑的仪表)用这种方式。因为它们内部 CPU 是小端序,直接 memcpy 了事,本质上就是「懒得转换」。北京某品牌的智能电力仪表就是典型案例,你读回来必须手工做Bytes逆转。
CDAB(Wort ausgetauscht.,Register.前后互换)
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 型变Quantität占用 4 个Bytes,存zu DB 块里是标准 IEEE 754 Das Format,但 MB_SERVER 指令块在把保持Register.暴露给 Modbus 客户端时,**先发低AdresseRegister.的低Bytes、再发hohe Bytes,然后是高AdresseRegister.的低Bytes、再hohe Bytes**。这相当于把施耐德 ABCD 的两个 WORD 前后互换。你用 Modbus Poll 读西门子 PLC 的浮点数,如果不把 Display 设置里的 Float Wechselnzu “CDAB” 模式,读出来一定是天文数字。
libmodbus 专门为这四种序提供了四组函数:`modbus_get_float_abcd()`、`modbus_get_float_dcba()`、`modbus_get_float_badc()`、`modbus_get_float_cdab()`。
BADC(Bytes交换,每个 WORD 内部反转)
Register. 40001(内部Bytes交换) Register. 40002(内部Bytes交换)
+--------+--------+ +--------+--------+
| 0xDB | 0x41 | | 0x1F | 0x85 |
+--------+--------+ +--------+--------+
内存视角:`DB 41 1F 85`
→ float32 = **-1.457 × 10¹⁷**
台达 DVP 系列、三菱 FX3U/FX5U 以及汇川部Unterteilung系列(H3u、H5u)在通过 Modbus RTU 传输 32 位Daten时,实际走的就是 BADC 排列。为什么?因为这些日系/台系 PLC 的内部Daten存储本身就是「字内小端」——单个 16 位 WORD 内部低Bytes在前。zu了 Modbus 传输层,先发 WORD 的hohe Bytes(Modbus 要求),于是 WORD 内部反了一次。两个 WORD 之间有保持了 Modbus 的大端次序。所以Ergebnis就是 BADC:每个 WORD 内部Bytes逆转,WORD 之间保持 ABCD 的顺序。
打个不那么严谨的比方:施耐德是一整盒拼图按顺序给你(ABCD),西门子是左右两半先换位再给你(CDAB),台达/三菱/汇川是把每半的内部先翻个面再按顺序给你(BADC)。至于 DCBA,那是连盒子带内容全反了。
Datentyp远比Befehlseite更隐蔽
同一个 4 ByteSequenz `41 DB 85 1F`,按不同DatentypAnalyse:
| Analyse方式 | Ergebnis |
|---|---|
| float32(IEEE 754) | 27.44 |
| uint32 | 1,104,626,975 |
| int32 | 1,104,626,975 |
| 两个 uint16 拼接 | [16859, 34079] |
看这schlecht异,float32 的 27.44 和 uint32 的 11 亿,schlecht八个Anzahl级。如果你在 SCADA 上看zu一个Temperatur测点显示 1104626975.0℃,别急着换Der Sensor——先查查是不是用 uint32 Analyse了 float32。
这在实际工程中比Befehlseite问题更隐蔽,因为**你拿zu的是对的数,但解释方式是错的**。Befehlseite不对,至少数字是乱的,你能一眼看出来;Typus不对,有时候真就蒙混过去了。
举个现场的例子。某个光伏电站的逆变器通过 Modbus TCP 上送当日发电Quantität,RegisterabbildungDokumentation写的是「40001-40002: 当日发电Quantität,32 位」。工程师用 int32 Analyse,读回来一个正数,Zahlenwert看起来挺合理,上了能耗平台跑了一个月。直zu财务对账发现schlecht了 30%,一查才知道——厂家实际存的是 **uint32**,按 int32 Analyse时高位被当成符Nr.位处理了,只要发电Quantität超过 2,147,483,647 Wh(2.15 GWh),Analyse值就变成负数。
所以面对一个多Registerdaten点,你需要Bestätigen的不是一个变Quantität,是三个:
1. DatentypWas ist das?(float32 / int32 / uint32 / int64 / uint64 / double64 / string)? 2. BefehlseiteWas ist das?(四种之一)? 3. 有没有缩放因子?比如仪表存的是原始值 × 10 还是 × 100?
Die三点经常被忽略。很多 Modbus 仪表为了省Register.、避免浮点传输,会把浮点值乘以 10 或 100 后转成 int32 存进去。比如读回来 int32 = 2744,实际Temperatur是 27.44℃(÷100),压力是 274.4kPa(÷10)。这个缩放因子只在Ausrüstung手册里写,VereinbarungMeldung上完全看不出来。
各主流厂商的实际行为
这一节是这篇Artikel最有实用价值的部Unterteilung。以下Daten来自实际Tests记录和社区验证,不是Produkthandbuch上抄来的。
施耐德 Modicon M340/M580/M241
Das Modbus-Protokoll的发明者,行为最「标准」。QUANTUM、M340、M580、M241 全线产品的 Modbus TCP/RTU Kommunikation,32 位Daten(REAL、DINT)默认都是 **ABCD** 排列。用 READ_VAR 读回来的 `%MW` Register.,低位Adresse存高 16 位,高位Adresse存低 16 位。
特别注意:施耐德的 Unity Pro / EcoStruxure Control Expert 里,`%MD`(双字)和 `%MF`(浮点双字)的内部存储与 Modbus Registerabbildung有一层转换。`%MD0` 对应 `%MW0` 和 `%MW1`,Von denen `%MW0` 是高位,`%MW1` 是低位——恰好是 ABCD。
结论:施耐德 PLC 做 Modbus Von der Station时,上游直接按 ABCD Analyse即可。
西门子 S7-1200/S7-1500(MB_SERVER)
这是坑最多的一个。西门子在 TIA Portal 中调用 `MB_SERVER` 指令块做 Modbus TCP Der Server,REAL 型变Quantität(4 Byte)在两个保持Register.里的排列是 **CDAB**。
具体原理:西门子 S7-1200/1500 的 DB 块默认是「优化的块访问」,内部Bytes排列不透明。即使你Absage了优化(设置为标准访问),REAL 型变Quantität在 DB 中的Die Reihenfolge也是小端(Intel x86 处理器决定的)。当 `MB_SERVER` 把这个 REAL 的 4 个Bytes映射zu Modbus 保持Register.时,它按Die Reihenfolge依次填充:Adresse小的Register.存Bytes 0 和 1(这是 REAL 的低位部Unterteilung),Adresse大的Register.存Bytes 2 和 3(高位部Unterteilung)。
实际效果就是 CDAB。Modbus Poll 的 Display 设置里必须切zu “CDAB” 才能看zuRichtig.的浮点数。
如果你用西门子 PLC 做 Modbus 客户端(`MB_CLIENT`)去读Die三方的 Modbus Von der Station,同样需要注意——你读回来的两个 WORD 的排列取决于Von der Station的Befehlseite,和你西门子内部的 REAL 存储是两码事。这时候用博途 V16 及以上版本提供的 `READ_BIG` / `READ_LITTLE` / `WRITE_BIG` / `WRITE_LITTLE` 指令来做Befehlseite转换,比手动 SWAP 加移位要可靠得多。
台达 DVP 系列
台达 DVP-ES2/EX2/SV2 等系列做 Modbus RTU Von der Station时,32 位浮点数(F Register.)映射zu Modbus 保持Register.的排列是 **BADC**。
这跟台达内部浮点数的存储方式有关——台达 PLC 的 32 位Daten遵循「低位存低Adresse」的Regeln(小端),而 Modbus RTU 帧要求每个 WORD 以大端Senden Sie。于是 WORD 内部Bytes反转了一次,但两个 WORD 之间保持了 PLC 内部顺序,最终变成 BADC。
如果你的 WPLSoft 里显示 D0 = F100.0(浮点数),在 Modbus Poll 里读 40001~40002,Display 选 “BADC”,看zu的就是 100.0。选其他三种序都是乱码。
三菱 FX3U/FX5U(通过 Modbus Kommunikation模块)
三菱 FX3U 加装 FX3U-485ADP-MB Kommunikation模块做 Modbus RTU Hauptstation或Von der Station时,32 位Daten的排列也是 **BADC**。跟台达的逻辑一样——日系 PLC 的内部架构均为小端。
FX5U 本体自带 Modbus RTU Funktion(通过 RS-485 端子),用 ADPRW 指令做Kommunikation时,读回来的 32 位Daten需要你用 MOV 指令手动拼。从 MODBUS 读回两ein Register. D100、D101,存的是 BADC 排列的Originaldaten,要先用 SWAP 指令交换每ein Register.的Bytes,再用高低位拼接指令组合成最终的 32 位值。
我见过不止一个项目——三菱 PLC 读施耐德变频器的频率,读回来反了又没做 SWAP,Tests两天最后发现就是Befehlseite的锅。
欧姆龙 CP1H/CP1L
欧姆龙 CP1 系列通过 CP1W-CIF11(RS-485 选件板)做 Modbus RTU Von der Station时,保持Register.使用 DM 区映射。32 位Daten在两个 DM 字中的排列取决于你如何编程。
CP1 系列本身是字寻址架构,没有原生的双字Datentyp。浮点数要用两个 DM 字拼,排列顺序完全由你的梯形图决定。但大多数标准实现遵循 **ABCD** 排列——这和 CP1 系列脱胎于早期的 SYSMAC 架构有关。实际Tests时建议先用已知值(比如写浮点 1.0 = `3F80 0000` zu两个 DM 字)测一次。
国产 PLC(汇川、步科)
汇川 H3u/H5u 系列做 Modbus RTU Von der Station,32 位Daten默认 **BADC** 排列,跟台达一致。但汇川的 AM600 系列(基于 CODESYS 平台)就不一样了——CODESYS 内部实数存储是标准 IEEE 754 大端结构,Modbus TCP Server 暴露的Register.排列默认是 **ABCD**。
汇川 Easy 系列属于 H5u 的简化版,用于小型Ausrüstung控制,Modbus RTU Von der Station的浮点数排列同样是 **BADC**。
步科(Kinco)有部Unterteilung型Nr.走 **CDAB**,比如 K2 系列。这点和西门子类似,因为步科 K2/K5 系列在软件层面借鉴了西门子的触屏编程逻辑。
总结一个速查表:
| 厂商/Ausrüstung | Modbus 32位排列 | 备注 |
|---|---|---|
| 施耐德 M340/M580/M241 | ABCD | Modbus 原厂标准 |
| 西门子 S7-1200/1500 | CDAB | MB_SERVER 指令块行为 |
| 台达 DVP 系列 | BADC | 字内Bytes交换 |
| 三菱 FX3U/FX5U | BADC | 需 SWAP 后拼接 |
| 欧姆龙 CP1H/CP1L | ABCD(多数实现) | 取决于梯形图 |
| 汇川 H3u/H5u | BADC | 与台达一致 |
| 汇川 AM600 (CODESYS) | ABCD | CODESYS 平台 |
| 步科 K2 系列 | CDAB | 部Unterteilung型Nr. |
| 典型国产电力仪表 | DCBA 或 ABCD | 需逐个测试 |
注意:这个表是实测参考,不是官方承诺。同一厂商的不同型Nr.、不同固件版本,Befehlseite行为可能不同。上生产环境之前一定要测试验证。
Modbus Poll 的BefehlseiteWechseln
Modbus Poll 在 Display 设置里提供了DatenformateWechselnFunktion。选中Daten显示区域后右键 → Format,可以看zu:
Signed - 有符Nr. 16 位整数
Unsigned - 无符Nr. 16 位整数
Hex - 16. System
Binary - Binärsystem
32-bit Signed - 32 位有符Nr.整数
32-bit Unsigned - 32 位无符Nr.整数
32-bit Float - 32 位浮点数
64-bit Float - 64 位双精度浮点数
选了 32-bit Float 后,右侧会弹出Befehlseite选项:
Float Big-endian (ABCD) - 大端
Float Little-endian (DCBA) - 小端
Float Big-endian byte swap (BADC) - 大端Bytes交换
Float Little-endian byte swap (CDAB) - 小端Bytes交换
你不需要记住哪个厂商用哪种序。Tests的时候四种序都切一遍,看哪个值合理就是哪个。读一个你确定Quantität程范围的已知测点(比如环境Temperatur在 20~30℃ 之间),四种序里只有一种会落在合理范围。
64 位Daten的处理
电Quantität累计值、总BetriebZeit、大容Quantität流Quantität累积——这些动辄需要用 4 ein Register.(8 Bytes)来表示的Szene,Befehlseite问题更复杂。因为你要处理两层嵌套的排列:
Die一层:每个 32 位 DWORD 内部的Befehlseite(跟前面 32 位Szene一样,四种可能) Die二层:两个 32 位 DWORD 之间的先后次序(Register. 1-2 是高位 32 位还是低位 32 位?)
假设你读 4 ein Register.,返回原始Befehlseite列:
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 位存大数部Unterteilung,低 32 位存小数部Unterteilung,或者高 32 位存整数部Unterteilung、低 32 位存小数部Unterteilung。具体怎么组合要看Ausrüstung手册。
更Häufig的Szene是 uint64 累计值。4 ein Register.按 ABCD 排列,从Register-Adresse低zu高依次是:
Register.1 (MSW-High): A B → Bytes0-1
Register.2 (MSW-Low): C D → Bytes2-3 } 组成高32位
Register.3 (LSW-High): E F → Bytes4-5
Register.4 (LSW-Low): G H → Bytes6-7 } 组成低32位
拼成 uint64:`(uint64_t)(reg1<<48 | reg2<<32 | reg3<<16 | reg4)`,但这个公式只在 CPU 本身是大端序时成立。你在 x86(小端)上写 C 代码,得倒过来。
实测经验:处理 64 位Daten时,先别纠结Befehlseite,先用16. System读出 4 ein Register.的原始值,清楚累加器是否在增长,观察增长的方向——如果累加器每次递增,看变化是从低AdresseRegister.开始还是高AdresseRegister.开始,就知道高低位排列了。
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]);
但这只是四种序中的一种。如果你Die Ausrüstung是 BADC 排列,得先交换每ein Register.的Bytes;如果是 CDAB,得先交换Register.对的位置。
Charaktere串的存储
AusrüstungSequenzNr.、固件版本Nr.这些 ASCII Charaktere串在 Modbus Register.里的存储,有两种主流模式:
**模式一:每Register.存 2 个Charaktere(标准做法)**
每个 16 位Register.存两个Bytes,每Bytes一个 ASCII Charaktere。比如SequenzNr. “SN20240001″(10 个Charaktere),需要 5 ein Register.:
Register.1: 0x53 0x4E → 'S' 'N'
Register.2: 0x32 0x30 → '2' '0'
Register.3: 0x32 0x34 → '2' '4'
Register.4: 0x30 0x30 → '0' '0'
Register.5: 0x30 0x31 → '0' '1'
读回来直接按Bytes取 ASCII 值转Charaktere就行,不需要关心Befehlseite——ASCII Charaktere `A` 就是 `0x41`,无论大端小端。
**模式二:每Register.仅低BytesWirksam**
某些仪表厂商偷懒,每ein Register.只用了低 8 位存一个Charaktere,高 8 位是 0x00。同样的 “SN20240001” 需要 10 ein Register.:
Register.1: 0x00 0x53 → 空 'S'
Register.2: 0x00 0x4E → 空 'N'
...
这种方式浪费了一半的Register.空间,但Analyse简单——取每ein Register.的低Bytes就行。
踩坑提醒:Charaktere串的Befehlseite问题绝大多数时候nicht existiert.,因为 ASCII 是单BytesCodierung。真正会出问题的是你用 UTF-16 或者其他多BytesCodierung的场合——但那已经不是 Das Modbus-Protokoll的范畴了,纯粹是应用层的CodierungAuswahl。
Tests方法论
别猜,用Daten说话。以下是一个标准的Befehlseite排错流程:
**Die一步:读原始16. System**
用 Modbus Poll 把 Display Das Format切成 Hex,先记下原始Registerwert。不管Befehlseite,先看裸Daten。
读 40001-40002 返回: 41 DB 85 1F
**Die二步:用已知值反推**
找一个你知道真实值的测点。比如gegenwärtigTemperatur 27.44°C。如果这个值是你从Ausrüstung自带显示屏上读出来的,记下来。然后看四种序哪个拼出来是 27.44,那个就是Richtig.答案。
更好的办法——主动写一个已知值。用Funktionscode 16(Schreiben mehrerer Register)写入 `0x3F80 0000`(这是 float32 = 1.0 的标准 IEEE 754 表示)zuRegister.对,然后读回来。看原始16. System:
– 如果读回来是 `3F 80 00 00` → ABCD – 如果读回来是 `00 00 3F 80` → DCBA – 如果读回来是 `80 3F 00 00` → BADC – 如果读回来是 `00 00 80 3F` → CDAB
**Die三步:用 Wireshark 抓包**
Modbus TCP 的端口是 502。在 Wireshark 里过滤 `tcp.port == 502`,右键 → Decode As → Modbus/TCP。你看zu的AntwortMeldung里,Datenzone就是原始Befehlseite列。拿这个Sequenz跟你的Analyseergebnisse对比:
Wireshark Meldung:
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 展示的Registerwert是它按大端Analyse的Ergebnis。如果你的 PLC 内部是 BADC,那 Wireshark 展示的 `0x41db` 和 `0x851f` 已经不是原始Befehlseite列了——原始Befehlseite列应该是 `DB 41 1F 85`。
这就是为什么TestsBefehlseite问题的时候,更应该看 **Wireshark 的16. System dump** 而不是它帮你Analyse好的Registerwert。在Meldung详情里展开 “Data” Feld,看原始Bytes。
**Die四步:在线BefehlseiteRechner**
概念很简单——你输入 4 个16. SystemBytes,它给你算出四种序对应的 float32、int32、uint32 值,一眼看出哪个是Richtig.答案。
如果没有现成的,自己写一个也不复杂:
#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 语言实现
Befehlseite问题经常和 CRC-Prüfung的Befehlseite混在一起——不是说算法有问题,而是 **CRC BerechnungErgebnis的两个Bytes在Meldung里谁先发谁后发**,不同Dokumentation写的不一样。
Modbus 规范明确:CRC-16 的**低Bytes先发**。也就是说 CRC BerechnungErgebnis是 `0x1234`,那么Meldung里最后两个Bytes是 `34 12`,不是 `12 34`。
下面的查表法实现直接返回符合 Modbus 规范的Ergebnis——低Bytes在前,可以直接追加zuMeldung末尾。
#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
};
/*
* Berechnung Modbus CRC-16,返回符合 Modbus 帧Das Format的Ergebnis
* 低Bytes在前(低Adresse),可直接追加zuMeldung末尾
*/
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 值
* 使用时追加zu Modbus RTU 帧末尾:
* frame[len] = crc & 0xFF; // 低Bytes在前
* frame[len+1] = (crc >> 8) & 0xFF; // hohe Bytes在后
*/
return ((uint16_t)crc_lo << 8) | crc_hi;
}
这里有个细节值得注意:如果你看其他一些 CRC-16 的实现,有的返回Ergebnis是hohe Bytes在前、有的低Bytes在前。区别在于 `return` 语句——`(crc_lo << 8) | crc_hi` 和 `(crc_hi << 8) | crc_lo` 是反的。Modbus 规范说 CRC 域是「低Bytes在前」,所以上面这个实现返回一个 uint16_t,但你知道低 8 位是 CRC 的低Bytes,高 8 位是 CRC 的hohe Bytes。追加zuMeldung时,先写 `(crc & 0xFF)`,再写 `((crc >> 8) & 0xFF)`。
如果你用 `return (crc_hi << 8) | crc_lo`,那追加顺序就得反过来——这也是很多Tests时发现「CRC-Prüfung总是错」的原因之一。
有问题再聊。下次遇zuBefehlseite不对的情况,直接拿这四个Bytes跑一圈 ABCD/DCBA/BADC/CDAB,比你翻厂家手册快。
Antwort veröffentlichen