OpenPLC

OpenPLC

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

04 - Being a device

04 - Being a device

MODBUS_MAP makes your PLC a Modbus device. A SCADA package, an HMI, a
datalogger or another PLC can then read it — and write it, where you let them.

The whole idea

Set the node's role, then say what lives where:

VAR
    node  : MODBUS_NODE;
    pub   : MODBUS_MAP;
    trend : ARRAY [0..9] OF WORD;
END_VAR

node( TRANSPORT := MODBUS_RUNTIME_SERIAL,
      SERIAL_DEVICE    := '/dev/ttyUSB0', SERIAL_BAUD := 19200,
      ROLE      := MODBUS_ROLE_BOTH,
      SERIAL_UNIT_ID   := 7 );

pub( NODE := node, TABLE := MODBUS_INPUT_REGISTERS,
     ADDRESS := 100, VALUE := trend );

trend now is input registers 100 to 109.

There is no request pin, and nothing is copied. A client reading address
103 gets whatever trend[3] holds at that instant. A client writing a holding
register changes your variable directly. There is nothing to schedule and
nothing to keep in step.

The array's lower bound maps to ADDRESS, whatever that bound is.
ARRAY [0..9] at ADDRESS := 100 is 100 to 109; so is ARRAY [1..10].

Choose the list on purpose

MODBUS_COILS             on/off,  a client may write it   FC 1, 5, 15
MODBUS_DISCRETE_INPUTS   on/off,  read only               FC 2
MODBUS_HOLDING_REGISTERS 16 bits, a client may write it   FC 3, 6, 16, 22
MODBUS_INPUT_REGISTERS   16 bits, read only               FC 4

Put anything a client must not change in a read-only list. This is worth
being deliberate about. It is not a permission setting that somebody can alter
later — the list itself has no write function code, so a client that tries is
answered with an exception. A measured value, a status word, a running hours
counter: input registers. A setpoint somebody is meant to adjust: holding
registers.

The four lists are separate, so you can use address 0 in all four without
collision, and most devices do.

A BOOL goes in a bit list, a number in a register list

Crossing them is refused with MODBUS_INVALID_ARGUMENT rather than
reinterpreted. A coil holds one bit and a register holds sixteen, so binding
one to the other would leave half a register pointing at whatever happens to
sit beside your variable in memory.

Use WORD for registers, not INT. A register is sixteen raw bits, and
WORD is what a program can mask, shift and take a bit of — statusWord.6 for
bit 6 — while AND, OR and SHL are all typed ANY_BIT, which INT is
not. Use WORD_TO_INT where you want a signed number.

An array must have fixed bounds. A generic pin takes a variable, and
ARRAY [*] is not one — it is a view, and views ride in-out pins only.

A mapped number is sixteen bits

INT, UINT and WORD map. REAL, DINT and the 64-bit types do not —
they are refused with MODBUS_INVALID_ARGUMENT. This is the one place
MODBUS_MAP asks more of you than MODBUS_READ, which takes all of them, so
it is worth saying why.

Nothing is copied here. The register a client reads is your variable, at
that address in memory, this instant. A REAL is not a pair of registers; it
is four bytes in a layout that ORDER would have to rearrange, and
rearranging means copying, and copying means a client can read half of an old
value and half of a new one with nothing on the wire to say so. A torn reading
is worse than a refused binding, so the block refuses.

Publish a scaled integer instead, which is what a real device does:

(* 4.7 mA becomes 470 - 0.01 mA of resolution, and the client divides *)
scaled := REAL_TO_INT( milliamps * 100.0 );
MODBUS_MAP( NODE := node, TABLE := MODBUS_INPUT_REGISTERS,
            ADDRESS := 0, VALUE := scaled );

Say the scaling in your address list, the same way every instrument manual
does. If you would rather not scale, put the REAL behind MODBUS_READ on
the other end instead: a read completes as one operation, so it has no torn
value to cause.

Several ranges

Call the block more than once in a scan and it maps that many ranges. That is
how two non-consecutive ranges are expressed:

VAR
    measured : MODBUS_MAP;
    commands : MODBUS_MAP;
    lamps    : MODBUS_MAP;
END_VAR

measured( NODE := node, TABLE := MODBUS_INPUT_REGISTERS,
          ADDRESS := 0,   VALUE := readings );

commands( NODE := node, TABLE := MODBUS_HOLDING_REGISTERS,
          ADDRESS := 100, VALUE := setpoints );

lamps   ( NODE := node, TABLE := MODBUS_COILS,
          ADDRESS := 0,   VALUE := indicators );

Use one block instance per range. They are independent: each has its own
BOUND, COUNT and WRITTEN.

Knowing when a client wrote

WRITTEN pulses for one scan when a client writes any address in this
block's range, whether or not the value changed:

commands( NODE := node, TABLE := MODBUS_HOLDING_REGISTERS,
          ADDRESS := 100, VALUE := setpoints );

IF commands.WRITTEN THEN
    (* validate before acting on anything a stranger sent *)
    IF setpoints[0] > 1500 THEN setpoints[0] := 1500; END_IF;
    newSetpoint := TRUE;
END_IF;

It is counted per binding, so two blocks mapping two ranges never take each
other's edges.

Validate what arrives. A client can write any sixteen-bit value it likes
into a holding register, including one that will hurt. The register is your
variable, so the check belongs in your program and nowhere else.

Failsafe — what happens when the client stops

A machine being driven over Modbus needs an answer to "what if the master goes
away". Two pins:

cfg( NODE := node, TIMEOUT := T#0s, FAILSAFE := T#5s );   (* MODBUS_CONFIG *)

speed( NODE := node, TABLE := MODBUS_HOLDING_REGISTERS, ADDRESS := 40,
       ON_LOSS := MODBUS_RESET_TO_VALUE, SAFE_VALUE := 0,
       VALUE := demandSpeed );

FAILSAFE on MODBUS_CONFIG is the silence that counts as loss. ON_LOSS
on the map says what that address does about it. SAFE_APPLIED pulses when it
happens.

Both are needed. Without FAILSAFE there is no silence to measure, so
ON_LOSS never fires — which is a quiet way to have no safety at all, and
worth checking on commissioning by pulling the cable and watching the pin.

MODBUS_RESET_TO_VALUE applies to a single value only. A range has no one
safe value, and guessing one would be worse than holding. Bind the few
addresses that need a safe state individually.

MODBUS_HOLD_LAST_VALUE, the default, is a real choice and often the right
one
— a temperature setpoint should probably stay where it was. A speed
demand probably should not.

Reading your own registers back

Nothing special is needed: the variable is the register, so read the variable.
MODBUS_MAP does not give you a copy because there is no copy.

What it costs

Nothing per scan. Binding is done once; after that a client's request is served
straight out of your variable by the node, in the node's own task. Mapping two
thousand registers costs the same per scan as mapping two.

Next: 05 - Reading and writing

Last updated 2 hours ago
Was this helpful?
Thanks!