Consystence
Start free
Back to blog
Updated 20 July 2026

C#, not Structured Text: retargeting the PLCnext generator

We built a deterministic generator that emits IEC 61131-3 Structured Text for PLCnext — then retired that target. The controller runtime told us something better was available: the same C# blocks the simulator already runs. Here is the finding, and why one behavioural source now feeds every target.

Garry Thomas
ArchitectureAutomationEngineering

Update — 20 July 2026. Two things have moved since this post. The box we said was next is ticked: the dam-pump-station program running the virtual PLCnext on our workshop rig is now a generated one, produced by this pipeline. And Structured Text is back in the generator’s output — not as the runtime target this post retired, but as a delivery format: for controllers programmed through the vendor’s engineering tool, the pipeline emits the generated program as IEC 61131-3 ST in PLCopen XML, with the hand-authored CSY_* library riding inside it verbatim. The platform’s own runtime path is unchanged — the program the controller runs under eCLR is C# — and the reasoning below stands for it.

Two weeks ago we wrote about generating PLC code instead of writing it, and we owed you a caveat at the end of that post: the generated Structured Text had not yet run on a virtual or physical PLC. Our dam-pump-station proving run had executed a hand-authored, pure-managed C# program on the controller’s eCLR runtime instead. Two artifacts, one honest gap between them.

That gap is now closed — but not the way we expected. We did not get the generated ST running on the controller. We retired the ST target.

What the runtime told us

Phoenix Contact’s PLCnext runs eCLR — a .NET runtime embedded in the controller, alongside the classic IEC 61131-3 environment. That is why our proving run could execute a C# program on a PLC at all, and it is also where our plan met reality.

The plan was tidy on paper: keep one behavioural master for the CSY_* block family in C# — the same blocks the simulator executes — compile it for eCLR, and have the generator emit Structured Text that instantiates those blocks, the way an engineer instantiates a Rockwell AOI. One library, one generated program, two languages meeting in the middle.

They do not meet. A Structured Text program can only instantiate what the runtime registers as a function-block POU, and a C# class compiled for eCLR is not one — it is a plain class, invisible to ST. Making our blocks ST-instantiable would have meant maintaining a second, decorated port of every block: two copies of the behaviour, drifting apart. Two copies of the truth is precisely the disease this whole pipeline exists to cure. We were about to reintroduce it at the deepest layer of the stack.

The decision

So the generator no longer emits Structured Text. It emits the controller program as eCLR C# — a program class that declares the GDS ports the faceplates and historian bind to, instantiates the same CSY blocks the simulator runs, and wires them into the scan. The template expansion is as deterministic as it ever was: the output is a pure function of the request, the type mapping, and the templates. You can diff it, sign it off, and get the same code next time. Nothing probabilistic touched the control code before, and nothing does now.

What changed is what the artifact is. The generated program is now the same species as the one our proving run executed on the virtual PLCnext — a managed program on the controller’s own runtime — rather than a sibling in a different language that had never been to the field. The behavioural master stays in C#, the simulator runs it, the controller runs it, and there is no translation seam between them left to drift.

The hand-authored IEC 61131-3 CSY_* library — the one with the 323-line pump state machine — is not deleted. It is frozen as the reference implementation: the human-readable specification the C# master is held against, and the artifact a controls engineer can read without touching a line of C#. It stopped being a second copy of the behaviour and became the document of record for it.

What this buys

One behavioural source now feeds every target. The simulator wires the blocks natively; the PLCnext program calls the same blocks on eCLR; the ControlLogix side draws on the same master through its own generated library. When a block’s behaviour changes, it changes once, and the equivalence suite — including the full dam-pump-station scenario — holds every consumer to it.

To be precise about the line, as always:

  • Shipped: the retargeted generator, emitting the eCLR C# program and its GDS port declarations from the same device-type mapping as before; the frozen ST reference library; the behavioural-equivalence suite holding master and reference together.
  • Roadmap: a generated program’s own end-to-end field proving. The proving run executed a hand-authored program of the same kind; putting a generated one through the same run is the next box to tick, and we will say so when it is ticked.

We spent weeks building a code path and then argued ourselves out of it, which never feels good on the day. But the pipeline exists to guarantee there is exactly one description of how a pump behaves. The moment the plan required two, the plan was wrong — not the principle.