Copying an example out asks nothing of you. Understanding it is what lets you go on to modify it, correct it and reuse it. And a program is not read like a piece of prose, from the first line to the last: you get into it through landmarks, in an order that is not the one it was written in. This page gives a way in to an unfamiliar C++ program, the language mechanisms that come back from one example to the next, and a commented reading of a program from this wiki, taken as it stands. It closes the C++ path: the syntax is assumed known, and what you learn here is how to use it to read.
What is it for?
On a project, most of the code you handle was not written by you: a library example, a teammate’s sketch, last year’s project picked up again. Three situations come up constantly, and all three call for reading before writing:
“This example almost does what I want — what do I change?”;
“My partner wrote that part, I have to plug mine into it”;
“This code worked last week, someone has modified it.”
Reading is not debugging. Here, we assume nothing is broken: the program is taken to work, and the aim is to find out what it does. When a behaviour is wrong and you are looking for why, that is the other job — see debugging an Arduino program. The two follow on from each other: you cannot usefully debug code you have not read.
How to get into an unfamiliar program
Four stages, in this order. The first reflex to unlearn is starting at the top and working down.
1. Find the boundaries
In Arduino C++, every program has the same frame: setup() runs once at start-up, loop()in a loop after that, indefinitely. Look for those two functions first, before reading a single line: they cut the file into three zones, the header (before setup()), the initialisation, and the repeated body.
That split already answers a question of substance: what is in setup() is a setting, what is in loop() is a behaviour. If you are looking for “why the LED blinks like that”, it is in loop(). If you are looking for “how fast the serial port is opened”, it is in setup().
2. Read the header: wired, set, remembered
The header usually runs to twenty lines and says three distinct things. Learning to tell them apart at a glance saves most of the reading time.
The #include lines — what the program depends on. An included library announces a capability (a display, a servo, an SD card) before any other clue;
the const and #define lines, generally in capitals — the settings. These are the values you can change without touching the logic: pin numbers, durations, thresholds. This is where you act first to adapt an example;
the global variables in lower case, often initialised to 0 or false — what the program remembers from one turn of loop() to the next. There are few of them, and they are the ones that carry the whole behaviour (see local and global variables).
The typographic convention is not enforced by the language, but it is followed throughout the Arduino world and throughout this wiki: capitals = never changes, lower case = changes. A name in capitals that you see assigned inside loop() is a signal that something is off in the code you are reading.
3. Take stock of the states
Take the two or three lower-case global variables spotted at the previous stage and, for each of them, look for every place where it is assigned. Your editor’s search function is enough. A typical embedded program has only three to five state variables, and knowing where each one changes is having understood the logic: the rest of the code is just wiring around it.
4. Play one turn, once
Finally, walk through one complete turn of loop() telling yourself a concrete scenario: “the button is not pressed, 3 seconds have gone by since start-up. What is each variable worth, which branch is taken?“. A single turn, played seriously, teaches more than three skimmed readings.
Tip
The single-sentence test. You have read a program when you can answer two questions without reopening the file: what does this program do, in one sentence? and what changes between two turns of the loop? As long as one of the two resists, the reading is not finished. Conversely, understanding every line is not the goal: you can perfectly well read a program without knowing what one particular library call does.
The C++ mechanisms you find everywhere
The points below are not syntax to be learnt (that is already on the path) but reading clues: what the presence of a keyword or of a form of writing tells you about the program, before you even understand the detail.
Braces delimit, indentation does not count
In C++, a block is what sits between { and }. Indenting to the left is only a presentation convention: the compiler ignores it. An if with no braces governs only the line immediately following it, even if several lines look lined up underneath.
if (mesure > SEUIL) digitalWrite(LED, HIGH); // inside the if compteur++; // NOT inside the if - always runs
Reading reflex: as soon as an if, a for or a while does not open a brace, treat only the following line as belonging to it, whatever the layout says. This is exactly the opposite of MicroPython, where the indentation is the structure.
The type tells the size, and warns of overflow
The type of a variable is not decoration: it fixes the range of values it can take. Only one reading rule really pays off in embedded work: every date coming from millis() is stored in an unsigned long. Seeing unsigned long tDebut is normal. Seeing int tDebut on a time variable is an anomaly worth flagging, because an int on a Uno overflows after 32 seconds.
A compound condition reads “or else … and that”
When a test mixes && and ||, reading it out loud lifts the ambiguity. The point to know: && binds tighter than ||, so A || B && C reads “A, or else B and that C” — and not “A or B, and that C”.
That test reads: “we leave if the full time has elapsed, or else if a pedestrian has asked and that the minimum is reached”. A || at the top level always opens two ways out. Spotting them is understanding the logic of the branch.
volatile: the variable is modified elsewhere
The volatile keyword in front of a global variable is a signpost: this variable is written outside the normal flow of the program, almost always by an interrupt routine. Its presence tells you to look for the ISR that modifies it before trying to understand the loop(). Careful not to over-read it: volatile only stops the compiler optimising reads away, it does not protect concurrent access (see interrupts on Arduino).
What you open, you close
In C++, releasing a resource is the programmer’s job. A file opened with SD.open() has to be closed again with close(). A transmission opened with beginTransmission() has to be concluded with endTransmission(). Reading reflex: for every opening you spot, look for the matching close. Its absence is not a matter of style. Without close(), a file can stay empty on the card.
This is the point where the wiki’s two languages diverge most sharply: on the MicroPython side, the with block closes on its own, so a missing close() is not an oversight there.
A table of tasks: struct and function pointer
Some programs replace a stack of repeated tests with an array they walk through. The form comes up often enough to be worth recognising:
struct Tache { void (*fonction)(); // the address of a function to call unsigned long intervalle; unsigned long dernier;};
Three things can be read there. A structgroups several pieces of information under a single name, here everything that describes a task. The void (*fonction)() field is a function pointer: a slot that holds not a number but the address of a function, which makes it possible to call it later without knowing which one. And the loop that follows, for (Tache &t : taches), is a range-based for: it walks the elements of the array one by one, with no index to manage. The general sense reads without any grasp of pointers: the program keeps a list of things to do and works through it. The detail is developed in non-blocking programming on Arduino.
Commented reading: the traffic light
Here is the method applied from end to end to the program from programming a state machine, taken as it stands. Follow the four stages without looking at the wiki’s own commentary.
The boundaries.setup() configures six pins and dates the start. loop() contains an if then a switch with three branches. All the behaviour is therefore in the switch.
The header. No #include: the program uses nothing but the Arduino core. The capitals give the settings — six pin numbers, four durations in milliseconds. Three lower-case names give the memory: etat, tDebut, demandePieton. We already know that the program remembers where it is up to, since when, and whether someone has pressed.
Taking stock of the states.etat is assigned in the three branches of the switch, nowhere else. That is the green → amber → red → green cycle. tDebut is reassigned to millis() at exactly the same places, so at every change of state: it re-dates the entry into the branch, which makes the duration comparisons start again from zero. demandePieton goes to true at the top of loop(), outside the switch (so the press is listened for at all times, whatever the state) and returns to false on entering ROUGE, once the request has been met.
One turn. Scenario: three seconds after start-up, nobody has pressed. digitalRead(BOUTON) is HIGH (the pin is in INPUT_PULLUP, so at rest it sits at HIGH), the first if is false. The switch enters case VERT, sets the five outputs back to the right configuration, then tests the compound condition: millis() - tDebut is 3000, which is below DUREE_VERT (6000) — false. demandePieton is false, so the second branch is false too. Nothing changes, the turn ends, and the next one starts again identically.
The program then boils down to one sentence: a light that runs on fixed durations, and that cuts its green short if a pedestrian has pressed, without ever dropping below a minimum green. What the turn showed is the role of the capitals in the test. It is enough to change DUREE_VERT to retime the whole light, without touching a single line of logic.
Getting help without being replaced
Having a program explained by a conversational assistant is a legitimate use, on one condition: that whatever it states is checkable on screen. The criterion is the same one as for simulation, and it does not depend on which way the tool is used.
“The demandePieton variable is modified in the first if of loop()” is a checkable statement: a search in the file confirms or denies it in three seconds. “This test overflows after 32 seconds if the variable is an int” is checked by looking at the declaration. Statements like those save time without taking anything away from the work: they are hypotheses, and validating them stays your job.
“This code is correct” is not of that kind. That is the conclusion: the only deliverable of the reading, and the only one that cannot be delegated. An assistant can show you where to look. Deciding that you have understood remains your part.
Common pitfalls
Reading from top to bottom. A program is not a text: start with setup() and loop(), then go back up to the header to shed light on what you saw there.
Trying to understand everything. An obscure library call does not stop you reading the program. Note it, carry on, come back to it if the question comes up again.
Trusting the indentation. It is decorative in C++. Only the braces count, and an if with no braces is the classic trap.
Confusing a setting with a state. A value in capitals never changes while the program runs. Mistaking it for a state variable sends you looking for logic where there is only a parameter.
Skipping the stock-take of global variables. It is the most profitable of the four stages, and the one you are most tempted to cut short.
Exercises
Exercise 1 — Two ways out
In the traffic light program above, the case VERT branch can end in two ways. Describe each one in a sentence, then say what happens if a pedestrian presses one second after the light turns green.
Answer to exercise 1
The two ways out are the two members of the ||:
the green has lasted its full time — millis() - tDebut >= DUREE_VERT, that is 6 seconds;
a pedestrian has called and the minimum has elapsed — demandePieton is true andmillis() - tDebut >= DUREE_VERT_MIN, that is 2 seconds.
If the press happens in the first second, demandePieton goes to true immediately, but the second condition is still false (1000 < 2000). The light stays green until the second second, then switches to amber without waiting for the six seconds. That is precisely the role of DUREE_VERT_MIN: stopping a press from cutting a green that has only just started.
Exercise 2 — Three clues, three conclusions
Without looking for the complete program, say what each of these three lines tells you, on its own, about the code it was taken from.
volatile unsigned long impulsions = 0;File f = SD.open("mesures.csv", FILE_WRITE);const unsigned long PERIODE = 250;
Answer to exercise 2
volatile unsigned long impulsions — the volatile announces a variable written by an interrupt: there is therefore an ISR elsewhere in the file, and that is where to start. The unsigned long points to a counter meant to climb high, or to a date.
File f = SD.open(...) — a resource is opened: there has to be an f.close() further down, and its absence would be a defect. Incidentally, the line announces that an SD library is included at the top and that the program logs measurements.
const unsigned long PERIODE = 250 — capitals and const: this is a setting, not a state. It will never change while the program runs, and it is a good candidate for adapting the program to another use.
Where it fits in the project
Step 4 of the proof of concept phase — most of the software building blocks of a PoC start from an existing example that gets adapted. The quality of the initial reading decides how much time is lost afterwards.
Code review — reviewing a teammate’s code first calls for being able to read it. It is the direct prerequisite of the collective routine.