12 KiB
atMb usage and behavior
This guide describes the last supplied working IEC export, including ClientTCP.
The current repository stores the working file at library/CmpAtomModbus.library.
Use that file in CODESYS. No IEC or C implementation is changed by repository cleanup.
The binary library format was not decoded to reverify its member declarations;
when changing IEC code, update this guide against a fresh CODESYS export.
Choosing the block
| Application | Block | Data |
|---|---|---|
| TCP master/client | atMb.ClientTCP |
IPv4 byte array and TCP port |
| RTU master/client | atMb.ClientRTU |
Linux serial device and serial settings |
| Read coils, FC1 | atMb.ReqReadCoil |
BOOL array |
| Read discrete inputs, FC2 | atMb.ReqReadDiscrete |
BOOL array |
| Read holding registers, FC3 | atMb.ReqReadHolding |
UINT array |
| Read input registers, FC4 | atMb.ReqReadInput |
UINT array |
| Write coils, FC15 | atMb.ReqWriteCoil |
BOOL array |
| Write holding registers, FC16 | atMb.ReqWriteHolding |
UINT array |
| TCP slave/server | atMb.SlaveTCP or atMb.ModbusSlave |
Four memory arrays |
The two slave blocks have identical code/interfaces. They do not provide RTU slave operation. ClientBase and RequestBase are abstract and are not instantiated directly.
TCP read example
Declarations:
VAR
mbClient : atMb.ClientTCP;
readRegisters : atMb.ReqReadHolding;
registers : ARRAY[0..9] OF UINT;
dataUsable : BOOL;
END_VAR
Cyclic body:
mbClient(
in_ipAddress := atMb.To_IPAddress(192,168,50,10),
in_port := 502,
in_enable := TRUE,
in_connectTimeout := T#2S,
in_reconnectDelay := T#100MS,
in_timeout := T#500MS);
readRegisters(
client := mbClient,
in_unitId := 1,
in_startAddr := 0,
in_quantity := 10,
in_dataOffset := 0,
in_minDelay := T#100MS,
in_staleAfter := T#2S,
data := registers);
dataUsable := readRegisters.out_validData AND NOT readRegisters.out_stale;
Call the client and each request every scan from one IEC task. On the first scan the client initializes its native mode, and the request registers afterwards; polling can begin on a subsequent scan. The request body binds the current array and usable count; the client dispatches the request method. No separate execute trigger is required for cyclic polling.
Keep instances and arrays stable. Avoid temporarily allocated request objects or sharing native contexts across tasks. Multiple requests can share a client; they are serialized. The supplied C implementation has 16 client contexts shared between TCP/RTU, 100 registrations per IEC client, and one in-flight transaction per client.
Addresses and lengths
Protocol addresses are zero-based: a manual describing its first holding register as 40001 often maps to in_startAddr := 0. Confirm the device's mapping; do not pass 40001 automatically.
in_dataOffset is the actual array index. With ARRAY[1..10], use 1 to access the first element. in_quantity is clamped to the remaining array capacity rather than always rejected. Check stats.reqLength for the effective count.
Limits per transaction: FC1/2 2000 bits, FC3/4 125 registers, FC15 1968 coils, FC16 123 registers. A one-element write still uses FC15/16 in the public write blocks. Register values are unsigned 16-bit words; converting floats, signed values or multi-register values and selecting word order are application responsibilities.
RTU example
Use the same requests with this client:
VAR
serialClient : atMb.ClientRTU;
readRegisters : atMb.ReqReadHolding;
registers : ARRAY[0..9] OF UINT;
END_VAR
serialClient(
in_device := '/dev/ttyUSB0',
in_baudRate := 9600,
in_parity := 0,
in_dataBits := 8,
in_stopBits := 1,
in_rs485 := FALSE,
in_enable := TRUE,
in_timeout := T#500MS,
in_reconnectDelay := T#100MS);
readRegisters(client := serialClient, in_unitId := 1,
in_startAddr := 0, in_quantity := 10, in_dataOffset := 0,
data := registers);
Set RTU unit ID explicitly to 1..247; the inherited request default is 0 and broadcast is unsupported. The Linux runtime account needs serial device permissions. Use a stable device path where available. in_rs485 requests kernel-managed direction control and requires driver support; USB adapters that control direction automatically generally use FALSE. Serial-port configuration must match the device.
The supplied native source supports 1200/2400/4800/9600/19200/38400/57600/115200 baud, 8 bits, parity none/odd/even and one/two stop bits. Timing uses nonblocking I/O and scan-driven millisecond deadlines; physical adapter timing must be verified on the target hardware.
Polling and priority
Requests become active when their effective length is nonzero. They register automatically and poll while the client is enabled. in_quantity := 0 makes a registered request inactive; it does not remove it from the registry.
in_minDelay controls eligibility by age since the previous start. It is not a guaranteed polling period. Lowest in_priorityCurrent wins, and age breaks equal-priority ties. On success, current priority is restored to in_prioritySetpoint. On error it becomes 10 * MIN(consecutive errors,3)^2: 10, 40, then 90. The current scheduler does not require priority to reach zero before submitting. Avoid overwriting current priority every scan unless that is intended.
Writes and uncertainty
Write arrays are copied at submission, so changes made afterwards affect a later transaction. An active write request is cyclic, just like a read; writeNow() is a priority/hold-release operation, not a single-shot write API.
When a connection fails after a write may have been transmitted, out_writeUncertain means the target could already have applied it. With in_holdUncertainWrites := FALSE, normal scheduling can retry. With TRUE, that write request is held until its writeNow() is called. Other eligible requests can continue.
// On an application-controlled event, after preparing the source array:
writeRegisters.writeNow();
This clears the hold/uncertain output and sets priority 0. The client must still be enabled/connected, the request active and its minimum-delay condition satisfied. Confirm target behavior before retrying commands whose effects should occur only once.
Timeouts and reconnects
Reply timeout precedence:
- Request
in_timeout: SYSTIME numeric milliseconds, when nonzero. - Client
in_timeout: TIME, when nonzero. - Calculated timeout using client dataRate.
- A one-second fallback if dataRate is zero.
The internal property calculates microseconds; submission rounds up to milliseconds with a minimum of 1 ms. TCP defaults its dataRate hint to 105200; the calculated timeout is approximately 38 ms. RTU uses its baud rate; at 9600 the formula gives approximately 411 ms. Explicit client/request timeouts make behavior easier to control.
in_connectTimeout is the TCP connection deadline, separate from reply timeout. in_reconnectDelay defaults to 100 ms. It is the recovery wait after detecting failure, not a guarantee of recovery within 100 ms: scan time, connection timeout, reply timeout, OS behavior and target readiness also matter.
Endpoint/configuration changes release and recreate the native context. recreateClient() forces this process; it does not permanently disable the client. release() also sets in_enable := FALSE. To restart, set TRUE and continue scanning. Supplying TRUE as a call parameter every scan will override that disabled input on the next call.
Data quality and statistics
| Diagnostic | Meaning |
|---|---|
Client out_connected |
Native transport is ready/sending/receiving |
Request stats.online |
Valid response evidence, including Modbus exceptions |
Request out_validData |
A successful read has populated data since invalidation |
Request out_stale |
No valid data, or last successful read is older than threshold |
Request out_dataAgeMs |
Age of successful read data |
Request out_nativeError |
Last native request result |
Request stats.lastErrorId |
Mapped IEC error classification |
Request stats.lastExceptionCode |
Modbus exception or local statistics classification |
out_socketError |
OS errno snapshot for socket/serial failure |
These represent different conditions. Exception replies can make communication online while the read fails. Read failures retain old values and may retain out_validData; use freshness as well. Releasing the client marks cached data invalid but does not erase arrays. Some completion paths return before updating age, so freshness outputs can refresh on the following scan.
reqDone/reqError are internal completion flags; the light outputs use a 100 ms off-delay for monitoring. timeoutCnt currently counts non-exception errors that statistics classify as TIMEOUT, not only native reply deadlines. Prefer native result 6 for a precise reply timeout.
Client resetStats() resets IEC send/reply/window counters and each request's four success/error counters. Request stats.resetStats() resets only doneCnt, doneCntRst, errorCnt, errorCntRst. Durations, timestamps, offline count, timeout count and native reconnect/errno history are retained. in_resetStats is consumed and cleared by the client; providing TRUE every scan keeps resetting it.
TCP slave example
VAR
server : atMb.SlaveTCP;
coils : ARRAY[0..15] OF BOOL;
discreteInputs : ARRAY[0..15] OF BOOL;
holding : ARRAY[0..31] OF UINT;
inputs : ARRAY[0..31] OF UINT;
END_VAR
// Publish application inputs before the server call.
inputs[0] := 123;
server(in_enable := TRUE, in_ipAddress := DWORD#0, port := 1502,
aCoilsMemory := coils,
aDiscreteInputsMemory := discreteInputs,
aHoldingRegistersMemory := holding,
aInputRegistersMemory := inputs);
// Consume network-written coils/holding values after this call.
Port 1502 is an example; select the application's port. Keep interfaceName empty. in_ipAddress := 0 binds all interfaces. A specific bind IPv4 is packed into DWORD, e.g. DWORD#16#C0A8321F for 192.168.50.31.
Each table has independent addresses. ARRAY[100..109] OF UINT maps addresses 100..109, not 0..9. All tables must use nonnegative bounds within 0..65535 and at most 2000 elements. The current API requires four arrays; a small placeholder can represent an otherwise unused table.
The wrapper snapshots all four application tables into native memory, processes bounded network work, then copies the tables back. Network writes affect coils and holding registers. Avoid overwriting externally controlled values unconditionally in application code each scan unless that override is intentional. The supplied C engine supports four server contexts and eight peers per server. Server contexts are separate from the 16-client pool.
out_running means the listener is bound; out_connectedClients indicates peers. in_reset is consumed and cleared; a constant TRUE recreates the server every scan. Listener bind failures retry while enabled. Counters reset with server context recreation. Some early-return paths leave prior outputs unchanged; interpret diagnostic outputs together.
Native result codes and client states
| Result | Meaning |
|---|---|
| 0 | Success |
| 1 | Invalid argument |
| 2 | Native client busy |
| 3 | Client offline/not ready |
| 4 | Socket/serial OS error |
| 5 | TCP connection deadline |
| 6 | Reply deadline |
| 7 | Peer closed |
| 8 | Invalid protocol/frame/reply |
| 9 | Modbus exception response |
| 10 | Cancelled |
| 256 | Invalid/stale native handle |
| 257 | Native pool full |
| 258 | Insufficient/invalid buffer capacity |
| 259 | Unknown command |
These are distinct from the sparse IEC Error enum. IEC mapping combines 4/7 into TcpSocketError, 1/258 into RequestParameterError; other unmapped results become InternalError. TCP names also classify RTU failures.
States: 0 disabled, 1 retry wait, 2 connecting, 3 ready, 4 sending, 5 receiving.
Maintenance and scope
The cleanup preserves the supplied working library and standalone native binary. Internal native fields are sometimes exposed as VAR_INPUT for compatibility; leave them under library control. Several legacy BOOL methods do not assign a return value; the API reference identifies them so callers rely on their side effects.
Lifecycle hooks exist, but reliable online-change ownership and pointer relocation remain future work. Documentation adds no behavior changes or online-change guarantee. Use the API reference for every method/property, including internal accessors.