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.
The rule that catches everyone first
Section titled “The rule that catches everyone first”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.
The three places
Section titled “The three places”A text routine
Section titled “A text routine”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 body
Section titled “An MBS body”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 parameterif (self->Enable) self->Count++; // an input parameter and a localPer-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.
Where your code runs in the scan
Section titled “Where your code runs in the scan”The generated loop() runs in this order, every scan:
- Apply any writes or forces sent from the IDE
- Read the input pins into their tags
- Apply input forces
- Service input devices, then every LIB body
- Run the Main routine — and anything it JSRs to, including text routines
- Service motion and communications
- Service output devices
- Apply output forces
- Write the output pins from their tags
- 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-behavedloop()needs.
Reaching your tags
Section titled “Reaching your tags”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;Do not call digitalWrite on a ladder pin
Section titled “Do not call digitalWrite on a ladder pin”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.
Including a library header
Section titled “Including a library header”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.
Which one should you write?
Section titled “Which one should you write?”- 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
.mbsfile 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