Action unknown: siteexport_addpage

Encrypted Modbus RTU

This page describes how to integrate a NiLAB integrated-drive linear motor (NLi / GDi family) into a PLC-based machine controller using the NiLAB Encrypted Modbus RTU protocol.

The encryption layer runs transparently on top of standard Modbus RTU over RS-485. No changes to the physical wiring or to the standard Modbus RTU register map are required. Drives with encryption disabled communicate as standard Modbus RTU slaves and are fully backward-compatible with any generic Modbus master.

The cryptographic implementation is provided as a closed, pre-compiled library for each supported development environment (CODESYS, C/C++, .NET). The internal algorithm is proprietary to NiLAB GmbH. The library is available to integrators under a NiLAB technology partner agreement — contact NiLAB for access.


If encryption is enabled on the drive, the drive will only respond to encrypted frames. Any plaintext Modbus request (FC3, FC6, FC16) will be silently discarded. You must use the NiLAB encryption library to communicate with an encryption-enabled drive.

1. Concept and protocol overview

1.1 What the encryption layer provides

The NiLAB Encrypted Modbus RTU layer adds three security properties to the standard Modbus RTU protocol:

* Authenticity — every frame carries a cryptographic tag. The drive verifies this tag before processing any request. Forged, corrupted or replayed frames are silently discarded without any response.
* Confidentiality — the Modbus PDU (function code and register data) is encrypted. An observer on the RS-485 bus cannot read register values or setpoints.
* Anti-replay — a monotonic frame counter embedded in each frame prevents a captured valid frame from being re-injected at a later time.

The protection is bidirectional: both the master→drive request and the drive→master response are encrypted and authenticated.

1.2 Key

Each NiLAB drive is provisioned at the factory with a unique 128-bit AES key, stored in protected flash memory. The key is tied to the drive serial number in the NiLAB key database.

To obtain the key for a specific drive, contact NiLAB GmbH with the drive serial number. The key is delivered as a 32-character hexadecimal string, for example:

1D23586E43A218620EC5C7486274CAD0

This key must be treated as a secret. It must be stored in protected memory on the PLC side and must never be transmitted over the bus or written to system logs.

1.3 Encrypted frame format

The encrypted frame replaces the standard Modbus function code and PDU with a fixed-structure wrapper identified by function code 0x65:

Standard Modbus RTU (plaintext):
[ Node ID (1) ][ FC (1) ][ PDU (N) ][ CRC16 (2) ]

NiLAB Encrypted Modbus RTU:
[ Node ID (1) ][ 0x65 (1) ][ Counter (4) ][ Ciphertext (N) ][ Tag (8) ][ CRC16 (2) ]
FieldSize (bytes)Description
Node ID1Modbus slave address, unchanged
0x651NiLAB encrypted PDU marker
Counter4Frame counter, big-endian, strictly increasing
CiphertextNEncrypted Modbus PDU (same content as the plaintext PDU would be)
Tag8Cryptographic authentication tag over Counter + Ciphertext
CRC162Standard Modbus CRC16 over all preceding bytes

The overhead added by the encryption wrapper is 12 bytes per frame (4 counter + 8 tag) compared to the equivalent plaintext frame.

The CRC16 uses the standard Modbus polynomial and byte order (low byte first) and is calculated over the entire frame including Node ID, function code 0x65, counter, ciphertext and tag — exactly as in standard Modbus RTU.

1.4 Frame counter

The frame counter is a 32-bit unsigned integer, transmitted big-endian (most significant byte first). It is independent per direction:

* The master (PLC) increments its TX counter with every request it sends.
* The drive tracks the last accepted master TX counter and rejects any frame with a counter that is not strictly greater than the last accepted value.
* The drive uses its own TX counter for encrypted responses; the master tracks this to detect replayed responses.

Counters start at 0 on first commissioning. They must be saved to non-volatile memory and restored on startup — see Section 5.

1.5 Drive behavior summary

State of drivePlaintext FC3/FC6 from masterEncrypted FC 0x65 from master (correct key)Broadcast sync (FC 0x80, node 0x00)
Encryption disabled (factory default)Accepted, normal responseNot understood, exception responseAlways accepted
Encryption enabled Silently discardedAccepted, encrypted responseAlways accepted

2. Getting started

2.1 Prerequisites

- A NiLAB NLi or GDi integrated-drive linear motor with encryption firmware (ask NiLAB for the firmware version that supports encryption).
- The 16-byte AES key for the specific drive, obtained from NiLAB (see Section 1.2).
- The NiLAB crypto library for your development environment (see Section 3).
- RS-485 wiring: standard Modbus RTU bus, 2-wire, with correct termination.

2.2 Communication parameters

The encrypted protocol uses the same serial parameters as standard NiLAB Modbus RTU:

ParameterValue
Baud rate115200 bps (default)
Data bits8
ParityNone
Stop bits1
ProtocolModbus RTU
Node IDConfigured on drive (default: 1)

2.3 Frame sizes for timing calculations

The total frame size depends on the inner PDU size. For the most common operations:

OperationInner PDU (bytes)Total encrypted frame (bytes)
FC6 write, 1 register51+1+4+5+8+2 = 21
FC3 read, 1 register5 (request)1+1+4+5+8+2 = 21
FC3 read, 1 register4 (response)1+1+4+4+8+2 = 20
FC3 read, N registers3+2N (response)1+1+4+(3+2N)+8+2 = 19+2N

Use these values to set appropriate serial receive timeouts on the PLC side.


3. NiLAB crypto library

NiLAB provides a closed, pre-compiled library for each supported PLC development environment. The library exposes a minimal, well-defined API(see Section 4) and handles all cryptographic operations internally. No knowledge of the underlying algorithm is required for integration.

3.2 Obtaining the library

The library is distributed under a NiLAB technology partner agreement. To request access:

* Contact NiLAB GmbH with subject line “Encrypted Modbus Library Request”
* Specify your development environment (see table above)
* Provide the serial number(s) of the NiLAB drive(s) you are integrating

NiLAB will supply the library package together with the corresponding drive key(s).


4. Library API

All library variants expose the same logical API, adapted to the conventions of the target environment. The following description uses pseudocode notation.

4.1 Initialization

\
NiLAB_Init(ctx, key[16], tx_counter, rx_counter)\

Initialises a crypto context for one drive connection.

ParameterTypeDescription
ctxopaque handleContext object — allocate one per drive
keybyte[16]The 16-byte AES key for this drive
tx_counteruint32Initial TX counter (0 for first commissioning, saved value on restart)
rx_counteruint32Initial RX counter (0 for first commissioning, saved value on restart)

4.2 Build a write request frame (FC6)

frame_len = NiLAB_BuildWriteFrame(ctx, node_id, reg_address, value, frame_buf)

Builds a complete encrypted FC6 write frame, ready to transmit over RS-485.

ParameterTypeDescription
ctxhandleInitialised context
node_iduint8Modbus slave address
reg_addressuint16Register address to write
valueuint16Value to write
frame_bufbyte[]Output buffer (minimum 23 bytes)
return valueintNumber of bytes to transmit

4.3 Build a read request frame (FC3)

frame_len = NiLAB_BuildReadFrame(ctx, node_id, reg_address, qty, frame_buf)

Builds a complete encrypted FC3 read frame for qty consecutive registers.

4.4 Parse and decrypt a response frame

status = NiLAB_ParseResponse(ctx, raw_frame, frame_len, node_id, reg_values_out)

Verifies the authentication tag, checks the counter, and decrypts the drive response.

Return valueMeaning
NILAB_OK (0)Frame authenticated and decrypted successfully. reg_values_out valid.
NILAB_ERR_AUTH (1)Authentication tag mismatch — frame forged, corrupted or wrong key
NILAB_ERR_REPLAY (2)Frame counter not increasing — possible replay attack
NILAB_ERR_SHORT (3)Frame too short to be a valid encrypted response
NILAB_ERR_TIMEOUT(4)No response received within timeout

4.5 Read current counter values (for NVM persistence)


tx_counter = NiLAB_GetTxCounter(ctx)
rx_counter = NiLAB_GetRxCounter(ctx)

Returns the current counter values. Save these to non-volatile memory periodically and on shutdown (see Section 5).


5. Counter persistence

The anti-replay mechanism requires that the master TX counter is strictly increasing across power cycles. Both tx_counter and rx_counter must be saved to non-volatile memory and restored on startup.

Recommended strategy: on every PLC startup, initialise the context with the last saved counter value plus a safety margin (e.g. +1000). This ensures that even if the NVM write was delayed at shutdown, the restored counter is still higher than any counter the drive accepted in the previous session.

Edit

5.1 CODESYS (RETAIN variables)

VAR \RETAIN
  nTxCounterNVM : UDINT := 0;
  nRxCounterNVM : UDINT := 0;
END_VAR

(* On startup, once: *)
NiLAB_Init(ctx, key, nTxCounterNVM + 1000, nRxCounterNVM);

(* After each successful transaction: *)
nTxCounterNVM := NiLAB_GetTxCounter(ctx);
nRxCounterNVM := NiLAB_GetRxCounter(ctx);

CODESYS RETAIN variables are automatically saved to flash on power cycle — no additional NVM write is needed.

5.2 C / bare-metal

/* On startup: */
uint32_t tx_saved, rx_saved;
NVM_Read(&tx_saved, &rx_saved);   /* your NVM read function */
NiLAB_Init(&ctx, key, tx_saved + 1000, rx_saved);

/* After each successful transaction: */
NVM_Write(NiLAB_GetTxCounter(&ctx), NiLAB_GetRxCounter(&ctx));

5.3 First commissioning

On first commissioning (no NVM data available), pass 0 for both counters. The drive will accept the first frame from counter 0 and set its internal reference accordingly.


6. Integration examples

The following examples show the typical call sequence for the three supported environments. They assume the library has been imported into the project and the context has been initialised in the startup section.

6.1 CODESYS V3.5 — Structured Text

Add the NiLAB library to your CODESYS project via Tools → Library Manager → Add Library → NiLAB_ModbusCrypto.

(*
 ---------------------------------------------------------------
 Startup / initialisation (call once from PLC_PRG, first scan)
 --------------------------------------------------------------- *)
\VAR
  ctx   : NiLAB_Ctx;          (* Crypto context -- one per drive *)
  key          : ARRAY[0..15] OF BYTE := [
                    16#1D, 16#23, 16#58, 16#6E, 16#43, 16#A2, 16#18, 16#62,
                    16#0E, 16#C5, 16#C7, 16#48, 16#62, 16#74, 16#CA, 16#D0 ];
  aFrame       : ARRAY[0..31] OF BYTE;
  aResponse    : ARRAY[0..31] OF BYTE;
  nFrameLen    : UDINT;
  nStatus      : INT;
  nRegValue    : WORD;
  bInitDone    : BOOL := FALSE;
END_VAR

IF NOT bInitDone \THEN
  NiLAB_Init(ctx := ctx,
              key := key,
              tx_counter := nTxCounterNVM + 1000,
              rx_counter := nRxCounterNVM);
  bInitDone := TRUE;
END_IF

(*
 ---------------------------------------------------------------
 Write register 0x0010 = 0x1234 on node \1
 --------------------------------------------------------------- *)
nFrameLen := NiLAB_BuildWriteFrame(
              ctx         := ctx,
              node_id     := 1,
              reg_address := 16#0010,
              value       := 16#1234,
              frame_buf   := aFrame);

(* Send aFrame (nFrameLen bytes) via your serial/RS-485 block *)
ComSend(port := COM1, data := aFrame, length := nFrameLen);

(* Wait for response, then: *)
ComReceive(port := COM1, data := aResponse, length => nRespLen);

nStatus := NiLAB_ParseResponse(
             ctx       := ctx,
             raw_frame := aResponse,
             frame_len := nRespLen,
             node_id   := 1,
             reg_values_out := nRegValue);

IF nStatus = NILAB_OK \THEN
  (* write confirmed, nRegValue contains echoed value *)
  nTxCounterNVM := NiLAB_GetTxCounter(ctx);
  nRxCounterNVM := NiLAB_GetRxCounter(ctx);
\ELSE
  (* handle error: nStatus = NILAB_ERR_AUTH, NILAB_ERR_REPLAY, etc. *)
END_IF

(*
 ---------------------------------------------------------------
 Read register 0x0020 on node \1
 --------------------------------------------------------------- *)
nFrameLen := NiLAB_BuildReadFrame(
              ctx         := ctx,
              node_id     := 1,
              reg_address := 16#0020,
              qty         := 1,
              frame_buf   := aFrame);

ComSend(port := COM1, data := aFrame, length := nFrameLen);
ComReceive(port := COM1, data := aResponse, length => nRespLen);

nStatus := NiLAB_ParseResponse(
             ctx            := ctx,
             raw_frame      := aResponse,
             frame_len      := nRespLen,
             node_id        := 1,
             reg_values_out := nRegValue);

IF nStatus = NILAB_OK \THEN
  wMotorStatus := nRegValue;   (* use the register value *)
\END_IF

Replace ComSend / ComReceive with the actual serial function blocks available in your CODESYS runtime (e.g. SL_SER_SND / SL_SER_RCV on Wago, RS on generic IEC 61131-3, ModbusSerial master blocks if you bypass the function code layer). The NiLAB library operates at raw byte level and is independent of the serial transport block used.

6.2 C / C++ (bare-metal, RTOS, Linux)

Add nilab_modbus_crypto.h to your include path and link against libnilab_modbus_crypto.a (ARM) or nilab_modbus_crypto.lib (x86 Windows).

#include "nilab_modbus_crypto.h"

/*
 --- Initialisation (call once at startup) --- */
static NiLAB_Ctx g_ctx;

static const uint8_t g_key[16] = {
  0x1D, 0x23, 0x58, 0x6E, 0x43, 0xA2, 0x18, 0x62,
  0x0E, 0xC5, 0xC7, 0x48, 0x62, 0x74, 0xCA, \0xD0
};

void motor_comm_init(void)
{
  uint32_t tx_saved, rx_saved;
  NVM_Read(&tx_saved, &rx_saved);   /* your NVM read */
  NiLAB_Init(&g_ctx, g_key, tx_saved + 1000, rx_saved);
}

/*
 --- Write register 0x0010 = 0x1234 on node 1 --- */
int motor_write_register(uint16_t reg_addr, uint16_t value)
{
  uint8_t frame[32];
  uint8_t response[32];

  int flen = NiLAB_BuildWriteFrame(&g_ctx, 1, reg_addr, value, frame);

  rs485_send(frame, flen);   /* your RS-485 send */
  int rlen = rs485_receive(response, sizeof(response), 200 /*ms*/);

  NiLAB_Status st = NiLAB_ParseResponse(&g_ctx, response, rlen, 1, NULL);

  if (st == NILAB_OK) {
      NVM_Write(NiLAB_GetTxCounter(&g_ctx), NiLAB_GetRxCounter(&g_ctx));
  }
  return (int)st;
}

/*
 --- Read register 0x0020 on node 1 --- */
int motor_read_register(uint16_t reg_addr, uint16_t *value_out)
{
  uint8_t frame[32];
  uint8_t response[32];

  int flen = NiLAB_BuildReadFrame(&g_ctx, 1, reg_addr, 1, frame);

  rs485_send(frame, flen);
  int rlen = rs485_receive(response, sizeof(response), 200);

  NiLAB_Status st = NiLAB_ParseResponse(&g_ctx, response, rlen, 1, value_out);
  return (int)st;
}

6.3 Ladder Logic (generic)

Ladder Logic does not have native support for byte-level cryptographic operations. The recommended approach for Ladder-based PLCs is:

- Declare the NiLAB function block (FB_NiLAB_ModbusCrypto) as a called block in the project. In most Ladder environments that also support IEC 61131-3, function blocks can be called from Ladder networks as a box instruction. - Use the Ladder network only for trigger logic, mode selection and status monitoring. The function block handles all cryptographic and communication operations internally.

Network 1: Enable block on rising edge of StartComm \contact
--[P]--[StartComm]----[FB_NiLAB.xEnable := TRUE]--

Network 2: Select write \mode
--[WriteCmd]----------[FB_NiLAB.xWrite := TRUE]---
--[/WriteCmd]---------[FB_NiLAB.xWrite := FALSE]--

Network 3: Pass register address and \value
--[MOVE]-- nTargetReg  --> FB_NiLAB.\nRegAddress
--[MOVE]-- nSetpoint   --> FB_NiLAB.nWriteValue

Network 4: Process result on \Done
--[FB_NiLAB.xDone]---[ProcessResultSubroutine]----

Network 5: Error \handling
--[FB_NiLAB.xError]--[SET]--AlarmBit--------------

For PLCs that do not support function blocks or Structured Text subroutines at all (pure relay-ladder systems), direct encryption is not feasible at the PLC level. In this case, contact NiLAB for information on the NiLAB Crypto Gateway — a compact external module that acts as a transparent encryption bridge between the PLC's standard Modbus RTU port and the drive bus.


7. Error handling

All APIfunctions return a status code. The following table describes the correct response to each error condition in a machine application:

Status codeMeaningRecommended action
NILAB_OKTransaction successfulNormal operation, update NVM counters
NILAB_ERR_AUTHFrame authentication failedLog event, retry up to 3 times; if persistent, check key
NILAB_ERR_REPLAYCounter mismatch or replay detectedLog event, reset context with NiLAB_Init and saved counters
NILAB_ERR_SHORTResponse frame too short or wrong node IDCheck RS-485 wiring and termination
NILAB_ERR_TIMEOUTNo response received within timeoutCheck drive power, address and baud rate

In a safety-relevant axis application, any persistent NILAB_ERR_AUTH or NILAB_ERR_REPLAY error should trigger an immediate axis stop and operator alarm. These errors should not occur in normal operation and may indicate tampering or a hardware fault.


8. Troubleshooting

SymptomLikely causeSolution
Drive does not respond to any frameEncryption enabled, PLC sending plaintextUse NiLAB library. All plaintext frames are rejected by design.
Drive responds only to first frame after power-onCounter mismatch after PLC restartRestore saved counters at startup (Section 5)
Persistent NILAB_ERR_AUTH errorsWrong key for this driveVerify key against NiLAB key database using drive serial number
Intermittent NILAB_ERR_AUTH errorsRS-485 bus noise corrupting framesCheck cable, termination resistors (120 Ω at both ends), cable length
NILAB_ERR_REPLAY after PLC restartSaved counter is lower than drive expectsIncrease NVM safety margin from +1000 to +10000 and recommission
Communication works but drive ignores motion commandsDrive in error/protection stateUse NiLAB Starter to check drive fault register before PLC control

9. Reference

Contact and support

For library access, key provisioning or technical integration support:

* Web: www.nilab.at
* Technical support portal: www.ni-lab.online

To report a security vulnerability in NiLAB products: Report a vulnerability