PLC-LadderBlog
Blog4 min readPLC-Ladder Team

Testing PLC Logic Like a Protection Engineer: CSV Injection Files

Protection engineers inject a table of quantities and check what trips. The same method for PLC logic: CSV scenarios, checked at the outputs, not internals.

Protection engineers have a testing culture that most PLC programming does not. Before a relay goes into service, someone connects a test set, injects a table of currents and voltages, and records what operated and how fast. The table is the specification. If the relay disagrees with a row, either the setting or the row is wrong, and the two are reconciled before anyone energises anything.

We wanted that for control logic, so we built it into the workbench as a CSV upload. This post is about the method, the file format, and the one lesson that changed how we write tests.

The file is the test plan

A scenario file has one header row and one data row per case. The header names the program's inputs, plus an expected_ column for every output you want checked:

GridFreq,BusVoltage,ROCOF,expected_Stage1,expected_Stage2,expected_RocofTrip
50.0,1.00,0.0,FALSE,FALSE,FALSE
49.0,1.00,-0.1,TRUE,FALSE,FALSE
48.5,0.60,-0.3,FALSE,FALSE,FALSE

The third row is the interesting one: frequency is low enough for both stages, but the bus voltage has collapsed to 0.6, and the logic must not shed load, because a depressed voltage means a fault, not a generation deficit. That is a real under-frequency load-shedding rule, and it is exactly the kind of case that gets forgotten when testing means clicking inputs by hand.

For each row the runner resets the program, applies the inputs, runs a fixed number of scan cycles, captures every output, and compares any cell that has an expectation. Blank cells are not checked. The result is a table with a tick or a cross per cell and a summary such as "18/20 scenarios passed", which exports back to CSV for the file you send to the customer.

Timers on virtual time

A test that had to wait real seconds for a T#5s delay would be useless, so the runner advances a virtual clock instead. Each scan adds a configurable step, 400 ms by default because that is the live simulator's tick. Twenty scans cover 7.6 s of preset. There is one subtlety we document because it bit us: the first scan only starts a timer, so a preset is reached when (scans − 1) × ms/scan ≥ PT. A two-second timer at 400 ms per scan needs six scans, not five. If a timer test fails by one scan, that is why.

Guardrails keep the runner honest for a public service: 200 rows, 100 KB, 500 scans per row, and a rate limit. Nothing about a run involves the AI, and it costs no quota.

The lesson: test outputs, not internals

Our first internal scenario files checked everything, including intermediate variables such as Stage1Pickup and VoltOK. They broke constantly. Not because the logic was wrong, but because every time the drafter rewrote a program the internals were named or structured slightly differently, while the behaviour at the outputs was identical.

The protection engineers' habit was right all along. A test set does not check the relay's internal state machine; it checks the trip contact. So the runner now checks coils and numeric targets, the things wired to the world, and we write expectations for those alone. When the AI redrafts a program with a new intermediate signal, the CSV still applies. When it changes the behaviour at an output, the CSV catches it. That is the correct sensitivity.

Two supporting features fell out of this. First, canonical signal names: the drafter is instructed to use a fixed vocabulary (GridFreq, Trip1, StartButton, EStop and so on) so redrafts converge on the same output names and old CSVs keep matching. Second, column-name tolerance: a header that says Expected_Stage1 or stage 1 instead of expected_Stage1 gets a mapping preview rather than a rejection, and the mapping you accept is kept for re-runs. The file is never rewritten.

Discrepancy is not failure

When we started running a design partner's files, we had to decide what a mismatch means. In unit testing a mismatch is a failure. In acceptance testing between two parties it is a discrepancy: either the number in the file is wrong or the simulator is, and it gets settled by talking. Our acceptance suite reports discrepancies as a table and a warning, and only turns them into hard failures in a strict sign-off mode. Broken files, a header that no longer matches the program, an unparsable cell, are failures immediately, because that is not a disagreement about physics.

How to start

Download the template for your program from the Tests tab. It has the right input columns and an expected_ column per output. Fill one row per case you would test on a bench: the normal state, each trip condition alone, each blocking condition, the boundary values, and the combination that should not operate. Ten rows is a good first file. Then keep it next to the program, because the day someone edits the logic is the day the file earns its keep.

Try it: describe your logic in plain English at PLC-Ladder, download the CSV template, and inject your first ten scenarios.