Debugging Your Program

Note

Debugging is early and in development. Breakpoints, stepping, and variable inspection work for Structured Text programs, but some capabilities are missing or limited. Each section notes the limits that apply to it.

Your doorbell works. In this chapter you add a feature to it, the feature goes wrong, and you use the debugger to find out why.

That is what the debugger is for: not touring buttons, but seeing what your program actually does between one scan and the next.

Add a Ring Counter

Suppose you want to know how many times the doorbell has rung. Open main.st and add a counter:

main.st — doorbell with a ring counter
PROGRAM main
   VAR
      Button : BOOL;
      Buzzer : BOOL;
      RingCount : DINT;
      PulseTimer : TON;
   END_VAR

   PulseTimer(IN := NOT Button, PT := T#500ms);
   Buzzer := PulseTimer.Q;

   IF Buzzer THEN
      RingCount := RingCount + 1;
   END_IF;

END_PROGRAM

CONFIGURATION config
   RESOURCE res ON PLC
      TASK plc_task(INTERVAL := T#100ms, PRIORITY := 1);
      PROGRAM plc_task_instance WITH plc_task : main;
   END_RESOURCE
END_CONFIGURATION

The logic reads correctly in English: when the buzzer sounds, add one to the count.

Click Run Program and let it run for a few seconds, as you did in the last chapter. RingCount is not 1. It is in the hundreds of thousands, and still climbing.

Set Up the Debugger

Rather than guess, stop the program on the line that is wrong and watch it work.

  1. Click the gutter to the left of the line RingCount := RingCount + 1;. A red dot appears.

  2. Open the Run and Debug view and select create a launch.json file. Choose IronPLC when prompted, then set stopOnEntry to true:

    .vscode/launch.json
    {
      "version": "0.2.0",
      "configurations": [
        {
          "type": "ironplc",
          "request": "launch",
          "name": "IronPLC: Debug Active File",
          "program": "${file}",
          "stopOnEntry": true
        }
      ]
    }
    

stopOnEntry pauses the program before it does anything, so you can watch from the very first scan.

  1. Press F5.

The program compiles, starts, and pauses immediately on the first line of the program body. Nothing has run yet.

Read the Variables

Open the Variables view. It shows two groups.

Program holds the variables you declared:

Button     BOOL    FALSE
Buzzer     BOOL    FALSE
RingCount  DINT    0
PulseTimer TON     0

Runtime holds values the virtual machine owns rather than your program, for example:

scanCount  ULINT   0
...

scanCount is the number of scan cycles the program has completed. It is 0, because none have. This is the number to watch.

Run to the Breakpoint

Press F5 to continue.

The program runs, and stops on the line you marked.

Two things to notice. Buzzer is now TRUE: the timer has finished, and scanCount tells you how many scan cycles that took.

RingCount is still 0. A breakpoint pauses before the line runs, so you are looking at the values on the way in.

Continue, and Watch

Press F5 again.

The program does not run to the end — there is no end. It finishes the scan, starts the next one, and stops on the same line again, because a breakpoint in a PLC program pauses on every scan that reaches it.

RingCount has gone up by one. So has scanCount. Buzzer has not changed.

Press F5 twice more, and the same thing happens twice more: RingCount follows scanCount, one for one.

There it is. RingCount rises by one every scan, and Buzzer was already TRUE on every one of those scans. The doorbell rang once. The program counted the scans during which it was ringing.

Fix the Bug

IF Buzzer THEN asks “is the buzzer on?” — and it is on for many scans in a row. What you meant to ask is “has the buzzer just turned on?”

To answer that, the program has to remember what the buzzer was doing on the previous scan. Stop the session with Shift+F5 and change the program:

main.st — counting the rising edge
PROGRAM main
   VAR
      Button : BOOL;
      Buzzer : BOOL;
      RingCount : DINT;
      PrevBuzzer : BOOL;
      PulseTimer : TON;
   END_VAR

   PulseTimer(IN := NOT Button, PT := T#500ms);
   Buzzer := PulseTimer.Q;

   IF Buzzer AND NOT PrevBuzzer THEN
      RingCount := RingCount + 1;
   END_IF;
   PrevBuzzer := Buzzer;

END_PROGRAM

PrevBuzzer holds what Buzzer was on the previous scan. The count now changes only on the scan where the buzzer turns on — the rising edge.

Press F5, then F5 again to leave the entry pause. The program stops on the counter line once, at the same point as before.

Press F5 again. Nothing stops. The program keeps scanning, the condition is never true again, and RingCount stays where it is.

Next Steps

You can now stop a running program and watch it work a scan at a time. As your project grows, you will want to organize code across multiple files.

Continue to Working with Multiple Files.