diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusCrc.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusCrc.cs
new file mode 100644
index 00000000..0e81ed1a
--- /dev/null
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusCrc.cs
@@ -0,0 +1,48 @@
+namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
+
+///
+/// CRC-16/MODBUS helper: reflected polynomial 0xA001, initial value 0xFFFF.
+/// On the wire the CRC is appended low byte first, high byte second —
+/// does that; just returns the 16-bit value. Byte-stream-agnostic:
+/// no socket or framing knowledge lives here.
+///
+public static class ModbusCrc
+{
+ /// Computes the CRC-16/MODBUS checksum over .
+ /// The bytes to checksum (e.g. the RTU frame minus the trailing CRC).
+ /// The 16-bit CRC value.
+ public static ushort Compute(ReadOnlySpan data)
+ {
+ ushort crc = 0xFFFF;
+ foreach (var b in data)
+ {
+ crc ^= b;
+ for (var i = 0; i < 8; i++)
+ {
+ var carry = (crc & 0x0001) != 0;
+ crc >>= 1;
+ if (carry)
+ {
+ crc ^= 0xA001;
+ }
+ }
+ }
+
+ return crc;
+ }
+
+ ///
+ /// Returns with its CRC-16/MODBUS appended, low byte first.
+ ///
+ /// The frame body to checksum.
+ /// A new array: followed by [crc & 0xFF, crc >> 8].
+ public static byte[] Append(ReadOnlySpan body)
+ {
+ var crc = Compute(body);
+ var result = new byte[body.Length + 2];
+ body.CopyTo(result);
+ result[^2] = (byte)(crc & 0xFF);
+ result[^1] = (byte)(crc >> 8);
+ return result;
+ }
+}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusCrcTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusCrcTests.cs
new file mode 100644
index 00000000..cd4c2a56
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusCrcTests.cs
@@ -0,0 +1,27 @@
+using Shouldly;
+using Xunit;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests;
+
+public sealed class ModbusCrcTests
+{
+ // Known CRC-16/MODBUS vectors (init 0xFFFF, poly 0xA001). CRC returned as ushort;
+ // on the wire it is appended low-byte-first.
+ [Theory]
+ // FC03 read HR: unit 1, addr 0, qty 1 -> CRC 0x0A84 (bytes 84 0A on the wire)
+ [InlineData(new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x01 }, (ushort)0x0A84)]
+ // "123456789" canonical check value for CRC-16/MODBUS = 0x4B37
+ [InlineData(new byte[] { 0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39 }, (ushort)0x4B37)]
+ public void Compute_matches_known_vectors(byte[] frame, ushort expected)
+ => ModbusCrc.Compute(frame).ShouldBe(expected);
+
+ [Fact]
+ public void AppendLowByteFirst_writes_lo_then_hi()
+ {
+ var body = new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x01 };
+ var withCrc = ModbusCrc.Append(body);
+ withCrc.Length.ShouldBe(body.Length + 2);
+ withCrc[^2].ShouldBe((byte)0x84); // CRC low byte
+ withCrc[^1].ShouldBe((byte)0x0A); // CRC high byte
+ }
+}