Skip to content

Writing Arduino C++

Ladder is not the only way to write logic in LadderIDE. Three things can be written in Arduino C++, and each one compiles into a different shape. Getting that shape right is most of the battle — almost every first attempt fails for the same reason.

Do not write void setup() or void loop().

Everything you write is already inside a function that LadderIDE generates for you. The sketch has exactly one setup() and one loop(), and they are written by the code generator, which calls your code from the right place at the right time. Pasting an Arduino example straight in — which almost always arrives wrapped in setup() and loop() — produces a function defined inside a function, which is not legal C++.

Take the contents of the example’s loop() and write those statements. Take the contents of its setup() and put them where that kind of code belongs for the thing you are writing, which the next three sections give for each.

This is checked for you, in both places. A function definition in a text routine or an MBS body is refused by validation, with a message saying what to do, before the compiler ever sees it — so you get the real reason rather than a C++ error from arduino-cli pointing a long way from the cause.

Create a routine and set its language to text. Its code becomes the body of a generated function:

void routine_MyRoutine() {
// your code, exactly as written
}

A text routine runs only when a JSR calls it. Main is always a ladder routine and is the only thing the scan calls on its own, so a text routine that nothing JSRs to is compiled and never executed.

Because the code is a function body, this is the right place for per-scan statements and local variables, and the wrong place for anything that has to live at file scope.

An MBS with its language set to C++ becomes a struct type and a method. The body you write lands here:

void MBS_MYBLOCK_run(MBS_MYBLOCK* self) {
// your code
}

It runs every scan in which the rung feeding the block is true. A false rung freezes the block: the body does not run, and everything inside the struct holds its value.

Parameters and local tags are fields of that struct, reached through self:

self->Speed = 1200; // an output parameter
if (self->Enable) self->Count++; // an input parameter and a local

Per-instance state belongs in a local tag, never a static. A static inside the body is one variable shared by every instance of the block on every rung — two placed blocks would tread on each other. A local tag is a struct field, so each instance gets its own.

A LIB written in C or C++ has three slots rather than one, because a peripheral needs code in three different places:

Slot Lands at Runs
Declarations File scope, above the instance Never — it declares
Setup Inside setup() Once at power-up
Body Inside loop() Every scan, ungated

This is where an Arduino example’s setup() contents go: into the Setup slot. Objects the library needs — a Servo, a sensor driver — are declared in Declarations, begun in Setup, and read or written in Body.

A LIB’s Body is not gated by a rung. LIBs are serviced peripherals, not rung instructions; they run every scan whatever the ladder is doing. If you want something conditional, that is an MBS.

The three slots accept {tokens} that are substituted when the sketch is generated: {ident} is the instance’s C identifier, {type} its struct type, and any input parameter’s name is replaced by its value. Braces of ordinary C++ are untouched, so no escaping is needed.

The generated loop() runs in this order, every scan:

  1. Apply any writes or forces sent from the IDE
  2. Read the input pins into their tags
  3. Apply input forces
  4. Service input devices, then every LIB body
  5. Run the Main routine — and anything it JSRs to, including text routines
  6. Service motion and communications
  7. Service output devices
  8. Apply output forces
  9. Write the output pins from their tags
  10. Send live monitoring data

Two consequences worth holding on to:

  • Your code runs between the input read and the output write. That is what makes a ladder scan predictable, and it is why the next section matters.
  • Anything slow you write — a delay(), a blocking read — stalls the whole scan, including the Online link. Write non-blocking code, the same discipline a well-behaved loop() needs.

Every tag is one C variable named T_ plus the tag name. The prefix exists so a tag called int or loop cannot collide with the Arduino globals of that name; any character that is not a letter, digit or underscore becomes _.

You wrote In C++
D13 T_D13
A0 T_A0
Count (DINT) T_Count
Tmr.ACC T_Tmr.ACC
Speeds[2] T_Speeds[2]
Count.3 (a bit of a word) read ((T_Count >> 3) & 1), write bitWrite(T_Count, 3, v)

So a text routine that copies an analog reading into a named tag is simply:

T_Level = T_A0;
if (T_Level > 800) T_HighAlarm = true;

Write the tag instead. Step 9 of the scan writes every output pin from its tag, so a digitalWrite you make yourself is overwritten a few microseconds later in the same scan. It will look like your code did nothing.

The same applies to inputs. Read T_D2, not digitalRead(2): LadderIDE configures inputs as INPUT_PULLUP and reads them active-low, so the tag is already inverted to mean what you expect, and forces have already been applied.

A pin that is not used anywhere in the ladder is yours — nothing in the scan touches it, and digitalWrite on it behaves normally.

You cannot #include inside a function body, which means not inside a text routine and not inside an MBS body. Validation refuses it in both, for the same reason it refuses a stray setup().

Add the library to the project instead — Assets ▸ Add Library. Its #include lines are emitted at the top of the sketch, and the library is installed for the build. Your code then just uses it.

A LIB is the exception: its Declarations slot is at file scope, so an #include there is legal. Even so, adding the library as an asset is the better habit, because that is what makes the build install it.

  • A text routine — one-off logic for this project that is awkward in ladder: a parsing loop, a lookup table, some arithmetic that would be ten rungs.
  • An MBS — logic you want to reuse, with parameters, private state, and a rung that gates it. It exports to a .mbs file you can use in another project.
  • A LIB — a piece of hardware to service every scan, or a computation that behaves like one. It owns its pins and exposes its readings as tags.

And often: none of them. Ladder is diagnosable while the machine is running — you can see which rung is true. C++ is not, and the whole reason this IDE exists is that the visual form tells you what is happening. Reach for C++ when ladder genuinely fights you, not by default.

Applies to LadderIDE >=1.2.2 · Last reviewed 2026-09-15