The Horror of PLC Programming


Consider this simple code snippet…

10  VALVE1_POS = S405_VH(1400)
20  VALVE2_POS = 80.0
30  IF VALVE1_POS > 50.0 THEN VALVE3_POS = 99.5 ELSE VALVE3_POS = 0.5

…and let me follow your observations:

  • you notice a weird line enumeration, just like in good old BASIC,
  • you see three variables, VALVE1_POS, VALVE2_POS and VALVE3_POS,
  • you find S405_VH(1400) most likely to be a function call retrieving a position of the valve 1,
  • position of the valve 2 is set to 80.0,
  • following the last if-then-else statement, you realize that the position of valve 3 is defined based on the position of the valve 1.

Assuming that the code above compiles to a valid program, can you think of a bug/feature that would cause all three variables to contain the value 99.5?

Here’s another two-liner:

10  VALVE1_STEP = 10.0
20  VALVE1_END = 90.0

Considering that the first example compiles and runs successfully, can you think of a reason why the second one fails?

If you know the answers, then it’s a good idea to stop reading now. No need to feed your professional anxieties. The rest are invited to follow along and discover the Horror of PLC Programming.

Why so BASIC?

The code above is indeed written in limited and slightly deformed subset of BASIC. You may or may not find the lack of LETs awkward, but otherwise these snippets are plain and simple BASIC. There are no arrays with DIMs and REDIMs that can screw things up or colon controversies. It’s as basic as BASIC gets (sorry).

“What a weird language choice for controlling valves!” you say. “What on flat-earth does this have to deal with 2019?” you ask. Keep on reading.

PLCs are made to be robust and more often than not programmed using ladder logic. PLC programs aren’t meant to be easy to write, but are easy to debug. You’ll never find tail-recursion or polymorphism capabilities in PLC languages. There is no unit-testing, you’ll test it in the field, attached to a big, heavy, piece of industrial machinery. Many PLCs lack the most basic concepts like variables. Ladder logic is never mentioned at hackathons, your local meetups, or praised on hacker news.


As a natural evolution of electromechanical relay control systems, ladder logic programming follows similar rules. Here’s how a simple PLC program controlling an AC unit might look like:

+----[ ]----+----[ ]----+----( )
|  Switch   |   HiTemp  |    A/C
|           |           |
|           +----[ ]----+
|               Humid
+----[ ]---------[\]---------( )
     A/C         Heat      Cooling

Inputs “relays” are shown as --[ ]--, outputs are --( )--. When enough inputs are activated to create a “current path” to the output, it becomes “energized”. This worked well for early systems that were designed to replace relay boxes. However spoiled engineers demanded fancier features, such as integer arithmetic, floating-point operations and even string manipulations. Following the lead of a semiconductor industry, PLC manufacturers complemented their systems with coprocessors for floating-point math and whatnot. Unfortunately, many of them haven’t got any further, albeit some systems nowadays support structured text or even subsets of C/C++.

Back to 2019, a current generation of mid-level PLCs from a well known North American manufacturer requires a coprocessor module to communicate with serial interfaces. This coprocessor is fancy enough to support floating point math and string manipulations and, you guessed it, is programmed in BASIC. Our happy PLC programmer is eager to use it to its full potential, multiply floats, truncate strings and sometimes go crazy extracting square roots.

Let’s get back to the first example:

10  VALVE1_POS = S405_VH(1400)
20  VALVE2_POS = 80.0
30  IF VALVE1_POS > 50.0 THEN VALVE3_POS = 99.5 ELSE VALVE3_POS = 0.5

The call of mysterious S405_VH

I know what you’re thinking. The mysterious function that appears in the first example has some magical powers and is going to change the global state, hang the execution, or throw a weird uncatchable exception. This is not the case.

S405_VH(1400) simply reads an octal word from the specified memory address V1400 in HEX format, hence _VH. What is an octal word, you ask? Well, it’s just two 8-bit memory chunks stitched together, aka 16-bit. No, you can’t call it a “byte”. It’s an octal.

Why is memory addressed in such a weird way, V1400? There are two parts: V indicates a type of memory range. Along with X, Y, C, T, CT, S, SP, GX and GY, it partitions the PLC memory into various “types”. Some types are inputs, some are outputs, some store timers and counters, some store system configuration parameters. They can store octal words (16 bit), single octals (8 bit), or deal with individual bits directly.

The second part, 1400, is the address of a memory location within the V memory type range. Since 1400 is an octal number, you would write it as 01400 in C, or 0o1400 in Python, but it’s just the semantics of this particular PLC. Everything is an octal. This particular value, 0o1400 is the beginning of a specialized sub-range of the V memory type that spans from 0o1400 to 0o7377, it is a so called “User Data Types” range. V memory ranges start at 0o700, not 0o0 (remember typing org 0x100 in x86 assembly, like, 30 years ago?). There are no variables, only an “accumulator”. Data management hasn’t improved much since mov ax, dx. The point being that BASIC is an improvement from a native PLC code.

(You remember I said that everything is an octal? I lied. When you deal with data, then everything is interpreted as HEX. Unless it is a binary-coded decimal (BCD), sometimes. If that’s the case, it is the programmer’s responsibility to ensure that BCD-HEX conversions are handled properly, compile time checks don’t catch that.)

What’s wrong with the valves

But I digress. We are trying to understand the first line of the first example, VALVE1_POS = S405_VH(1400). “I know!” you say. “It returns HEX instead of BCD! Or BCD instead of HEX! Or an integer instead of a float! That’s where the bug is!” But that would be too easy. So let’s assume that S405_VH(1400) returns exactly what we expect and the variable VALVE1_POS now contains a legitimate value. And yet, after all three lines are executed, VALVE1_POS, VALVE2_POS and VALVE3_POS all contain value 99.5. In fact they are all the same variable!

Here’s how this particular implementation of BASIC identifies variables by their names:

  • get the first character of the variable name (A);
  • get the last character of the variable name (B);
  • get the length of the variable name (N);
  • the combination of A, B and N defines a variable, so VALVE1_POS, VALVE2_POS and VALVE3_POS are interpreted as identical names.

Imagine the world where:

  • TEMP1_C and TEMP2_C are identical variables;
  • VOLT_DAC and VOLT_ADC are identical variables;
  • TIMER_SET and TIMER_RST are identical variables;
  • INPUT1_CH3 and INPUT2_CH3 are identical variables;
  • PRESSURE_PIPE, PRESSURE_TUBE and PRESSURE_GAGE are all the same thing.

Happy debugging.

Two bugs in two lines

10  VALVE1_STEP = 10.0
20  VALVE1_END = 90.0

This example is similarly annoying. In this particular BASIC world, variables cannot contain any keywords as a part of their names.

STEP is a keyword. END is a keyword. VALVE1_STEP and VALVE1_END aren’t valid variable names.

Docu… mentation?

The fun way of discovering these “features” is of course through the endless debugging (there is no actual debugger, you just print your variables to a serial port and catch them at the other end). Okay, let’s assume you’re keen to RTFM and download manuals from the manufacturer’s web-site. Unfortunately, they don’t contain language specifications. You’re not giving up and going through the manuals for similar and discontinued modules. One of them describes this behavior as a “helpful hint” [sic] at the end of a code sample, in the “TRANSFER INSTRUCTION” chapter of the manual:

Helpful Hint: The variables LSB10 and LSB20, CODE15 and CODE25 or REG400 and REG410 will return the same value due to the way that BASIC stores variables. To avoid this problem, use dimensioned variables such as CODE(index) or REG(index).

No wonder you missed that. So you keep debugging. Your programming tool doesn’t show any useful messages so you give up trying to google the problem. Days go by, and you make no progress. Desperate, you are seeking for advice. You are knocking on the door of “Bob the PLC guy” who is retiring in two days. You’re hiding your tears and trying to stay calm. He looks at your code and smiles, pulls up an old IBM Thinkpad with Windows 2000 and launches a weird looking version of the programming tool. He then presses F1 and Microsoft Windows Help shows up. He types “variables” in the search bar and finds the page he needs. You look at the page, and you look at the guy. You’re angry and happy at the same time. You copy the help file and thank Bob. You leave early that day and head to your favorite bar to have a little celebration. You are now a “Level 2 PLC guy”.

All characters and other entities appearing in this story are fictitious. Any resemblance to real persons, dead or alive, or other real-life entities, past or present, is purely coincidental.

Tagged #PLC , #ICS , #control system , #ladder logic , #BASIC , #horror