# 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:

`````{tab-set}
````{tab-item} Variables table
| 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 |
````

````{tab-item} Text mode
Switch the variables pane to text mode with the **code** icon in its top-right
corner, then paste this in:

```pascal
  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

```pascal
(* 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:

`````{tab-set}
````{tab-item} Variables table
| Name | Class | Type | Location | Initial Value | Documentation |
|---|---|---|---|---|---|
| `hysteresis` | Local | `REAL` | | `0.2` | Deadband, in volts |
````

````{tab-item} Text mode
The whole block, with the new line at the bottom:

```pascal
  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:

```pascal
(* 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.

## Related examples

- [Broken Input Detection](broken-input-detection.md) — the same bar-graph behaviour, modified for a 4-20 mA current-loop input that can distinguish zero from an open circuit.
- [Engineering Units](engineering-units.md) — the same program with the reading format switched to Engineering Units, removing the scaling line.
