11
\$\begingroup\$

Warning

Wall of text, little bit of code. This question is about as much as introducing the language as it is about whether or not I still know how to write the language.


Introduction

Programmable Logic Controllers (PLCs) are industrial, heavy-duty computers used to control (semi-)automated processes in factories. They can be programmed in a large amount of languages and programmed using a multitude of languages. Up till '93, every brand had its own language. Thanks to the IEC 61131-3, languages are more (but not completely) standardized now.

One of the most used languages of the standard 5 described in the earlier mentioned IEC document is "Structured text" (ST), not to be confused with reStructuredText. ST looks a bit like Pascal and is, usually, quite easy to read.

In ST, there's a main program (traditionally called PLC_PRG) which can be accompanied by multiple Function Blocks (FBs). Every function gets its own FB. Variable declaration is separated from variable usage.

To program a PLC, you need a programmer. Those are usually packed within an IDE. An ever growing amount of PLCs can be programmed with the standard languages using CODESYS.

A typical set-up for a PLC is having a Human Machine Interface (HMI), a PLC, sensors and actuators. Most of those can be simulated and visualized so no actual hardware is required.

Problem and approach

How FizzBuzz works doesn't need much explanation. Except mine doesn't work quite like that. You see, I had a little trouble getting the text-box to work and by the time I figured it out it already looked just fine. So unlike an ordinary FizzBuzz, mine will always show the number. There will be extra output when the number is divisible by 3, 5 or both (15). All working as intended.

To keep it more interesting, I added a couple of extras. A switch to turn the program off, for example. After all, it's a PLC. It might be driving a chainsaw instead of a couple of indicators next. Everything you build in hardware needs a switch to turn it off.

The number indicator is limited to 90 or 255. Why? Because they're divisible by 15 and 100 isn't. It just looks better. There's also a reset button (self-explanatory) and an automated reset at 90. The automated reset can be toggled. If the reset isn't used, the count will go till 255 and overflow to 0. If the indicator is set to a scale end of 90, 91-255 will be indicated as 90.

The FizzBuzzer has its own FB. I've considered putting the IF statements within it in their own FB as well, passing 3 and 5 as arguments from the FizzBuzzer, but that got surprisingly messy. Must've done something wrong. I'm also unable to make CONST variables out of fizz_num and buzz_num, I kind-of forgot the proper syntax for that.

The simulation is controlled by mouse. Works like a train.

Code

PLC_PRG (PRG)

PROGRAM PLC_PRG
VAR
    state : BOOL := FALSE;
    reset : BOOL := FALSE;
    toggle_reset: BOOL := FALSE;

    i: USINT;
    reset_above : USINT := 90;

    fizzbuzz: FIZZBUZZ;
    fizz: BOOL;
    buzz: BOOL;
END_VAR

IF state THEN
    i := i+1;
    fizzbuzz(in:=i, fizz=>fizz, buzz=>buzz);
END_IF;

IF reset THEN
    i := 0;
END_IF

IF toggle_reset AND i >= reset_above THEN
    i := 0;
END_IF

FIZZBUZZ (FB)

FUNCTION_BLOCK FIZZBUZZ
VAR_INPUT
    in : USINT;
END_VAR
VAR_OUTPUT
    fizz : BOOL := FALSE;
    buzz : BOOL := FALSE;
END_VAR
VAR
    fizz_num : USINT := 3;
    buzz_num : USINT := 5;
END_VAR

IF in MOD fizz_num = 0 THEN
    fizz := TRUE;
ELSE
    fizz := FALSE;
END_IF;

IF in MOD buzz_num = 0 THEN
    buzz := TRUE;
ELSE
    buzz := FALSE;
END_IF

Project overview

Project overview

Result

To 90:

FizzBuzz visualized HMI to 90

To 255:

FizzBuzz visualized HMI to 255

From left to right, top to bottom, including the variable they're linked with:

  • Index indicator (PLC_PRG.i)
  • Fizz indicator (PLC_PRG.fizz)
  • Buzz indicator (PLC_PRG.buzz)
  • Reset button (PLC_PRG.reset)
  • State switch (PLC_PRG.state)
  • Automatic reset switch (PLC_PRG.toggle_reset)

Whether or not that naming is logical depends on how you look at it and what you're used to, I guess. The State switch probably needs a better name.

Both animations use a MainTask interval of 250 ms and a VISU_TASK interval of 100 ms.

Footnote

This particular FizzBuzz is written in ST using CODESYS V3.5 SP12.

\$\endgroup\$
  • 1
    \$\begingroup\$ I don't know anything about PLC programming, but is this an interval of 100ms for detecting button presses? It sounds far too slow. Humans starts to react around 150ms delays. Does the PLC handle the debouncing through "magic" or where does that happen? \$\endgroup\$ – Lundin Feb 6 at 12:45
  • \$\begingroup\$ @Lundin Good question, I'd have to test that using longer intervals. This immediately points out a possible design flaw. The reset button (the only button that would be influenced by a bounce problem since the rest are switches) might need to be latched to keep it high at least until the next pulse. I'll get back to you on that. \$\endgroup\$ – Mast Feb 6 at 13:53
  • \$\begingroup\$ All switches need debouncing no matter if they are push buttons, toggle switches or anything else. Even relay coils need it. \$\endgroup\$ – Lundin Feb 6 at 14:00
  • \$\begingroup\$ @Lundin Apparently it's all about the hardware used whether there's magic or not. In the program written with the settings used, the button press detection is worse than you expected. The visualisation is responsible for just that, the visualisation. The button press detection, since it's not running on it's own timer while it should (a major oversight on my part), is checked every 250ms. \$\endgroup\$ – Mast Feb 7 at 2:18
  • \$\begingroup\$ The obvious solutions include using configurable I/O cards (which are overkill for this solution) or splitting up the program to have inputs on their own timer with the debounce, much faster than the main logic. \$\endgroup\$ – Mast Feb 7 at 2:19
2
\$\begingroup\$

Thanks to Lundin for pointing out in the comments that the button bounce was improperly handled. This led to 'missed triggers', the system ignoring a button being pushed because it wasn't looking at that particular moment. Quite an unfortunate bug.

Quite easily fixed as well. There are a couple of methods to solve this, but in this case the easiest is by using a (somewhat ridiculous looking) latch and a dedicated handler for every input. Since the problem is most pressing for the manual reset button, the behaviour of switches won't change much if the same technique is used on them as well, I'll show how that's done.

A list of global variable names is introduced, which I'll name GVL. A new GVL.reset_triggered will be added which will be reset at the end of each PLC_PRG run. The top of GVL will carry an attribute 'qualified_only' pragma to force the programs only to call the global variables by it's name inside the list (GVL.reset is valid while reset is not valid with this pragma, keeps it clean). Afterwards, a task handler is created for the manual reset and a few modifications to PLC_PRG are required to make it all fit again. Note how at the end of PLC_PRG the 'reset flag' is reset.

GVL

{attribute 'qualified_only'}
VAR_GLOBAL
    reset_triggered : BOOL := FALSE;
    fizz: BOOL;
    buzz: BOOL;
END_VAR

PLC_PRG (PRG)

PROGRAM PLC_PRG
VAR
    state : BOOL := FALSE;
    toggle_reset: BOOL := FALSE;

    i: USINT;
    reset_above : USINT := 90;

    fizzbuzz: FIZZBUZZ;
END_VAR

IF state THEN
    i := i+1;
    fizzbuzz(in:=i, fizz=>GVL.fizz, buzz=>GVL.buzz);
END_IF

IF GVL.reset_triggered THEN
    i := 0;
END_IF

IF toggle_reset AND i >= reset_above THEN
    i := 0;
END_IF

IF GVL.reset_triggered THEN
    GVL.reset_triggered := FALSE;
END_IF

MANUAL_RESET (PRG)

PROGRAM MANUAL_RESET
VAR
    reset : BOOL := FALSE;
END_VAR

IF reset THEN
    GVL.reset_triggered := TRUE;
    GVL.fizz := FALSE;
    GVL.buzz := FALSE;
END_IF

FIZZBUZZ (FB) has not changed.

Now, the manual reset is set-prioritized (not necessarily set-dominant) latched. With the new task running every 10 ms, all is fine again.

The PLC_PRG.fizz and PLC_PRG.buzz moved to respectively GVL.fizz and GVL.buzz and now they can be reset by the MANUAL_RESET task to fix another bug (the lights kept burning if they were before the reset was hit and the PLC_PRG.state was no longer active).

Just to be pedantic, the original code had a couple of semi-colons inconsistently placed behind END_IF statements. Those were not necessary.


Is it clean code now? Not by a long shot. For starters, there is no proper separation of concerns. There should be a clear FizzBuzzer block, a clear HMI block and a clear LetsTestThisDeviceUnderTest block. That's insufficiently separated at the moment. Besides, I'm not convinced this is much prettier. Less buggy, by the looks of it. But not pretty.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.