Example: Analog Threshold Control#

Read a 0-10 V analog input and light one, two, three or four outputs as the signal rises, like a bar-graph level indicator. It exercises analog scaling, comparison logic and bit manipulation together.

Hardware#

Slot

Module

1

SLM-AI-8-V (8-channel 0-10 V analog input)

2

SLM-DCO-15P (15-channel sourcing DC output)

Feed channel 1 of the analog module from something you can vary across 0-10 V like a potentiometer across a 10 V reference, a signal calibrator, or an SLM-AO-8-V driven by a second program. Wire indicator lamps to output channels 1 through 4.

Backplane configuration#

Add both modules in order. Leave the analog module’s Reading Format on Raw counts and Counts Scaling on Module resolution, the defaults. The SLM-AI-8-V has a 13-bit converter, so the input arrives as a UINT from 0 to 8191.

The generated addresses:

Slot

Channel

Address

Alias

1

AI1

%IW0

Level_Raw

2

DO1

%QX0.0

Bar_1

2

DO2

%QX0.1

Bar_2

2

DO3

%QX0.2

Bar_3

2

DO4

%QX0.3

Bar_4

Recall that the analog input module contributes nothing to the discrete output space, which is why the output module in slot 2 still starts at %QX0.0.

The program#

A POU in OpenPLC Editor is two separate things. The variables pane at the top of the editor window declares what the program owns, and the code body underneath holds the logic. You never type PROGRAM, VAR, END_VAR or END_PROGRAM; the editor generates that scaffolding from the variables when it builds the project.

Variables#

Declare these in the variables pane of the program POU. It has two views, switched with the table/code icons in its top-right corner; fill in the table by hand, or paste the declarations into text mode. Both tabs say the same thing:

Name

Class

Type

Location

Initial Value

Documentation

level_raw

Local

UINT

Level_Raw

0-10 V as 0-8191 counts

bar_1

Local

BOOL

Bar_1

First lamp

bar_2

Local

BOOL

Bar_2

Second lamp

bar_3

Local

BOOL

Bar_3

Third lamp

bar_4

Local

BOOL

Bar_4

Fourth lamp

level_volts

Local

REAL

Scaled reading, in volts

segments

Local

INT

How many lamps should be lit

Switch the variables pane to text mode with the code icon in its top-right corner, then paste this in:

  VAR
    level_raw : UINT AT Level_Raw; (* 0-10 V as 0-8191 counts *)
    bar_1 : BOOL AT Bar_1; (* First lamp *)
    bar_2 : BOOL AT Bar_2; (* Second lamp *)
    bar_3 : BOOL AT Bar_3; (* Third lamp *)
    bar_4 : BOOL AT Bar_4; (* Fourth lamp *)
    level_volts : REAL; (* Scaled reading, in volts *)
    segments : INT; (* How many lamps should be lit *)
  END_VAR

The Location column accepts either the IEC address or the alias defined in Backplane Configuration. Both views above use the aliases, which keep the variables attached to the right channels even if the IEC addresses change.

Code body#

(* At module resolution the 13-bit input reads 0-8191 for 0-10 V. *)
level_volts := UINT_TO_REAL(level_raw) / 8191.0 * 10.0;

(* Map 0-10 V onto 0-4 lit segments. Thresholds at 2, 4 and 6 V;
   the fourth at 7.9 V rather than 8 V so a nominal 8 V signal
   reliably lights it despite converter tolerance. *)
IF    level_volts >= 7.9 THEN segments := 4;
ELSIF level_volts >= 6.0 THEN segments := 3;
ELSIF level_volts >= 4.0 THEN segments := 2;
ELSIF level_volts >= 2.0 THEN segments := 1;
ELSE                          segments := 0;
END_IF;

bar_1 := segments >= 1;
bar_2 := segments >= 2;
bar_3 := segments >= 3;
bar_4 := segments >= 4;

Why the thresholds are not evenly spaced#

Analog inputs do not read exactly their nominal value. Converter tolerance, reference drift and the source’s own accuracy mean a nominal 8 V might read 7.98 V. A threshold placed exactly at a level the source is expected to produce flickers or never triggers. The fourth threshold is therefore 7.9 V, not 8.0 V.

Adding hysteresis#

Real installations need hysteresis: turn on at one level, turn off at a slightly lower one.

Add one more variable:

Name

Class

Type

Location

Initial Value

Documentation

hysteresis

Local

REAL

0.2

Deadband, in volts

The whole block, with the new line at the bottom:

  VAR
    level_raw : UINT AT Level_Raw; (* 0-10 V as 0-8191 counts *)
    bar_1 : BOOL AT Bar_1; (* First lamp *)
    bar_2 : BOOL AT Bar_2; (* Second lamp *)
    bar_3 : BOOL AT Bar_3; (* Third lamp *)
    bar_4 : BOOL AT Bar_4; (* Fourth lamp *)
    level_volts : REAL; (* Scaled reading, in volts *)
    segments : INT; (* How many lamps should be lit *)
    hysteresis : REAL := 0.2; (* Deadband, in volts *)
  END_VAR

Then replace the bar_1 := segments >= 1; line in the code body with:

(* Segment 1: on above 2.0 V, off below 1.8 V *)
IF level_volts >= 2.0 THEN
  bar_1 := TRUE;
ELSIF level_volts < (2.0 - hysteresis) THEN
  bar_1 := FALSE;
END_IF;

Note that neither branch assigns when the value sits inside the band. The output holds its state.

Anything driving a contactor, a pump or a valve needs this. Rapid cycling shortens equipment life.