---
title: "04 - Being a device"
space: "OpenPLC"
url: "https://docs.modbee.org/openplc/modbus-blocks/04-being-a-device"
updated: "2026-10-25"
---

# 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](/openplc/modbus-blocks/05-reading-and-writing)
