Learning Goals 5 min
By the end of this lesson you will be able to:
- Use three classic kinds of debug print — trace prints ("I got here"), value prints (the current value of a variable), and before/after prints (a value just before and just after a calculation) — to find bugs in sketches that don't behave the way you expected.
- Read a Serial Monitor transcript and use it to locate a bug in the source code — the missing message, the wrong value, the branch that never runs.
- Clean up after yourself: remove or comment out debug prints once a bug is fixed, so the working sketch isn't full of leftover chatter.
Warm-Up 10 min
L01-24 taught you the three calls that let the Arduino talk. Today you put those calls to work. Debugging means finding the difference between what you thought your sketch would do and what it actually does — and the cheapest, most universal tool for that job is the humble println.
Quick-fire puzzle
Look at this short, broken sketch. It's supposed to turn pin 13 ON when the button on D7 is pressed and OFF when it's released. But uploaded to the Arduino, the LED never lights up — no matter how many times you press the button.
void setup() {
pinMode(13, OUTPUT);
pinMode(7, INPUT_PULLUP);
}
void loop() {
if (digitalRead(7) == HIGH) {
digitalWrite(13, HIGH);
}
else {
digitalWrite(13, LOW);
}
}You're sitting in front of the Arduino. The LED is dark. The button click feels normal. What do you check first — the wiring? the code? something else? And how would you check it?
Reveal the answer
The bug is in the if condition: with INPUT_PULLUP (L01-17), pressed reads LOW, not HIGH. So the LED only turns on when the button is released… which is the default state, meaning the LED appears to be permanently off when you keep pressing it, but actually does light briefly when you let go. Hard to see by eye.
The fastest way to discover this is not to stare at the code, but to add one line — Serial.println(digitalRead(7)); — and watch the Monitor while you press and release. You'd see the value swap between 1 (released) and 0 (pressed), realise the condition is backwards, and fix it in seconds.
That's print debugging. Today's lesson teaches you to do it on purpose.
New Concept — the three classic debug prints 15 min
The big idea — make the invisible visible
Most bugs aren't "the chip is broken". Most bugs are a wrong assumption: you think the loop runs ten times a second but it really runs ten thousand; you think the variable holds 3 but it really holds 0; you think the else branch runs but it never does. Each of those assumptions is invisible to you — until you make the Arduino say it out loud. Print debugging is the act of replacing assumptions with evidence.
There are three patterns that solve almost every Cluster A–C bug you'll meet. Memorise these three. The rest of Cluster D is built on them.
Pattern 1 — Trace prints ("I got here!")
Sometimes you suspect a branch of your if/else is never running. Drop a labelled println right inside it. If the message appears in the Monitor, the branch ran; if not, it didn't. That single piece of evidence narrows the search instantly.
if (alarmActive == 0) {
Serial.println("IDLE branch"); // trace print
}
else {
Serial.println("ALARM branch"); // trace print
}Upload, watch the Monitor for a few seconds. You'll see one of three things: only IDLE branch (the alarm never fires), only ALARM branch (the alarm is stuck on), or both alternating. Each tells you something different and immediate.
Pattern 2 — Value prints (the variable's real value)
When a value is wrong, print it. Always include a label — a bare 3 in the Monitor is useless if you've got six prints going. The label should be a short word that says which variable it is.
Serial.print("presses=");
Serial.println(presses);
Serial.print("button=");
Serial.println(digitalRead(7));The Monitor will then show lines like presses=3 and button=1 — short, scannable, and easy to spot when the value changes.
Pattern 3 — Before/after prints
If a calculation gives the wrong answer, you need to know whether the inputs were wrong (so the formula's fine) or the formula is wrong (so the inputs were OK). Print both sides — the value just before the calculation, and again just after.
Serial.print("before: count=");
Serial.println(count);
count = count * 2 + 1;
Serial.print("after: count=");
Serial.println(count);If before already shows the wrong number, the bug is earlier in the sketch. If before is right but after is wrong, the bug is in that one line of arithmetic. Two prints, half the haystack to search.
The debugging loop
- Form a guess. "I bet this
elsebranch never runs." or "I betpressesis still 0 when I print it." - Add one print that proves or disproves the guess. A trace print for "does this run", a value print for "what does this hold", a before/after for "did this line change it".
- Upload, open the Monitor, exercise the sketch. Read what came out.
- If the print appeared as expected, the guess was wrong — move on to the next guess.
If the print disagreed with the guess, you've found the lie. Now you know where the bug is. - Fix the bug. Then remove or comment out the prints. Don't leave them in the working code — they clog the Monitor next time and slow the loop with unnecessary work.
Why it matters
Print debugging is the most-used debugging technique in software, on every platform, in every language, from Python to JavaScript to C++ to embedded firmware. Professional engineers reach for it before fancier tools every single day. Learning the loop above well — guess, prove, read, fix, clean up — is one of the most transferable skills in this whole syllabus. Everything you learn here works on your phone, your laptop, a server farm, anywhere code runs.
Worked Example — fix a broken counter 20 min
Wire one button on D7 with INPUT_PULLUP (the L01-17 circuit; no LED needed). You're going to upload a press-counter sketch that has a subtle bug, watch it misbehave in the Monitor, add three debug prints, find the bug, and fix it.
Step 1 — Upload the broken sketch
Type this in exactly as written. Don't fix anything yet — we're going to diagnose it.
// Press counter — broken; we'll fix it with prints
const int BUTTON_PIN = 7;
int presses = 0;
int lastButton = LOW; // suspicious initial value
void setup() {
Serial.begin(9600);
pinMode(BUTTON_PIN, INPUT_PULLUP);
Serial.println("Ready.");
}
void loop() {
int nowButton = digitalRead(BUTTON_PIN);
if (lastButton == LOW && nowButton == HIGH) {
presses = presses + 1;
Serial.print("Press count: ");
Serial.println(presses);
}
lastButton = nowButton;
delay(20);
}Upload. Open the Monitor at 9600 baud. You should see Ready. appear at start. Now press and release the button a few times.
Step 2 — Observe the bug
Two strange things happen:
- The Monitor prints
Press count: 1instantly the moment the sketch starts — before you press anything. - Pressing the button does not add new lines. But releasing it does.
Two symptoms, one root cause. Time to investigate.
Step 3 — Guess #1 (and a value print)
Guess: "Maybe digitalRead(7) isn't returning what I think it is." A INPUT_PULLUP button reads HIGH when released, LOW when pressed (L01-17). Let's verify with a value print at the very top of loop().
void loop() {
int nowButton = digitalRead(BUTTON_PIN);
Serial.print("now="); // DEBUG
Serial.println(nowButton); // DEBUG
if (lastButton == LOW && nowButton == HIGH) {
presses = presses + 1;
Serial.print("Press count: ");
Serial.println(presses);
}
lastButton = nowButton;
delay(20);
}Upload. Now the Monitor floods with now=1 lines (because the button is normally released). Press the button: lines turn to now=0. Release: lines turn back to now=1. So the read is fine. The guess was wrong — but now we know the read is correct, which is also useful.
Step 4 — Guess #2 (a trace print)
Guess: "If the read is right, then the if condition is wrong — it triggers on release, not press." Add a trace print inside the if body to see exactly when it fires:
if (lastButton == LOW && nowButton == HIGH) {
Serial.println(">>> branch entered"); // DEBUG trace
presses = presses + 1;
Serial.print("Press count: ");
Serial.println(presses);
}Upload. The Monitor shows:
Ready.
>>> branch entered ← appears immediately, no press!
Press count: 1
now=1
now=1
…The branch fires once at startup, before any press. So the bug is in the initial value of lastButton. We set it to LOW, and the first loop pass reads nowButton = HIGH (button is released by default), so LOW && HIGH is true and the branch fires.
Then, on every subsequent release of the button (going from LOW back to HIGH), it fires again — which matches symptom #2.
Step 5 — Fix the bug
Two fixes are valid:
- Fix A: change the initial value of
lastButtontoHIGH(matching its default released state). The branch now only fires on a real press. - Fix B: change the condition to detect HIGH → LOW (release → press), so the initial LOW value doesn't matter as much. This is the more common pattern in real code.
Apply Fix B and the sketch becomes the correct version from L01-24:
int lastButton = HIGH; // fixed: matches default
// …
if (lastButton == HIGH && nowButton == LOW) { // fixed: press = HIGH → LOW
presses = presses + 1;
Serial.print("Press count: ");
Serial.println(presses);
}Step 6 — Clean up the debug prints
The bug is fixed. Now remove (or comment out) the two debug prints we added — the now=… value print and the >>> branch entered trace. Leaving them in floods the Monitor and slows the loop. The only print left is the legitimate Press count: output, which is the sketch's actual job.
Upload the cleaned-up sketch and verify: no message until you press; exactly one message per press; nothing on release. Bug squashed. This whole flow took five minutes — without prints it might have taken an hour.
Try It Yourself — fix three more bugs 20 min
For each task, type in the broken sketch exactly as given, observe what it does wrong, then use one or more of the three patterns (trace, value, before/after) to locate and fix the bug. Don't read ahead to the fix — diagnose first.
Goal: This sketch is supposed to print the number 10 once and stop. Instead it prints nothing at all. Find why.
int answer = 10;
void setup() {
Serial.println(answer);
Serial.begin(9600);
}
void loop() { }Plan: add a trace print Serial.println("setup running"); as the very first line of setup(). Upload. Does the trace appear? If not, what does that tell you?
Questions:
- The trace print doesn't appear either. Yet
setup()obviously runs (it's the entry point). What does that mean? ____ (Hint: re-read the order of the two existing calls.) - Fix the bug by swapping two lines. Which two? ____
- Once fixed, does the original line
Serial.println(answer);need a label? Why or why not? ____
Goal: This sketch should count from 1 to 5 then stop. Instead it prints 1 forever. Find why.
int n = 1;
void setup() {
Serial.begin(9600);
}
void loop() {
if (n <= 5) {
Serial.println(n);
n = n - 1;
delay(500);
}
}Plan: add a before/after value print around the assignment line, so you can see what n is just before and just after the change.
Serial.print("before n=");
Serial.println(n);
n = n - 1;
Serial.print("after n=");
Serial.println(n);Questions:
- What does the Monitor show for the
beforeandaftervalues on the first two passes? ____ - What's the bug — typo in the operator, wrong condition, both, or something else? ____
- Fix the sketch so it really does count 1 → 5 and then stops printing. ____ (Hint:
=means "store this";+adds one to the existing value.)
Goal: Wire one button on D7 (INPUT_PULLUP) and one LED with resistor on D9 (the L01-07 circuit). This sketch is supposed to print HIGH ON when the button is pressed and LOW OFF when released, and drive the LED to match. Instead the LED is permanently on and the Monitor only ever shows HIGH ON — even when the button is released. Find why.
const int BUTTON_PIN = 7;
const int LED_PIN = 9;
void setup() {
Serial.begin(9600);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
int b = digitalRead(BUTTON_PIN);
if (b = LOW) {
Serial.println("LOW OFF");
digitalWrite(LED_PIN, LOW);
}
else {
Serial.println("HIGH ON");
digitalWrite(LED_PIN, HIGH);
}
delay(100);
}Plan: the symptom is "one branch never runs". Add a trace print to each branch (">> in LOW branch" and ">> in HIGH branch") plus a value print of b at the top of loop(). Compare what the read says with which branch runs.
Questions:
- The value print shows
bswapping between 0 and 1 as you press and release — so the read is fine. But the LOW-branch trace never appears. What does that prove? ____ - Look at the
ifcondition very carefully. There's a one-character typo that turns "compare" into "assign". What is it? ____ (Hint: compare is==; assign is=.) - After you fix it, the LED-and-print logic also feels backwards. Swap the two branches' bodies (or the condition) so that pressing turns the LED on, not off. ____
Mini-Challenge — debug the timer 10 min
"Why is my reaction timer always 0 ms?"
Wire one button on D7 (INPUT_PULLUP) and one LED on D9 (the L01-22 circuit). The sketch below is meant to mimic the reaction timer game from L01-22: turn the LED on at a random moment, measure how long the user takes to press the button, print the answer in milliseconds. Instead it always prints Reaction: 0 ms.
// Broken reaction timer — always prints 0 ms
const int BUTTON_PIN = 7;
const int LED_PIN = 9;
void setup() {
Serial.begin(9600);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
delay(2000); // wait before "go"
digitalWrite(LED_PIN, HIGH);
unsigned long start = millis();
unsigned long end = millis(); // suspicious
while (digitalRead(BUTTON_PIN) == HIGH) { }
digitalWrite(LED_PIN, LOW);
Serial.print("Reaction: ");
Serial.print(end - start);
Serial.println(" ms");
}Your task:
- Use a before/after value print on
startandend, printed just before the finalSerial.print("Reaction: "), so you can see what they hold at the moment of the calculation. - Read the Monitor. The two values will be suspiciously similar. Explain why in one sentence.
- Fix the bug so that
endactually captures the time of the button press, not the time of the LED turning on. The reaction time should now be 200–500 ms or so for a quick human. - Once fixed, remove the debug prints.
It works if:
- The LED comes on, you press the button, and the Monitor prints something like
Reaction: 312 ms— a different number every time, in the hundreds of milliseconds. - If you wait a long time before pressing, the printed number is bigger; if you slap it instantly, it's smaller. The number genuinely tracks how fast you were.
Reveal one valid fix
The bug is on the line unsigned long end = millis(); — it runs before the wait loop, so it captures the same moment as start. The two values are nearly equal (just a few microseconds apart) and the subtraction always gives roughly 0.
The fix: move that line to after the while loop, so end is captured at the moment the button is pressed.
void loop() {
delay(2000);
digitalWrite(LED_PIN, HIGH);
unsigned long start = millis();
while (digitalRead(BUTTON_PIN) == HIGH) { }
unsigned long end = millis(); // captured AFTER the press
digitalWrite(LED_PIN, LOW);
Serial.print("Reaction: ");
Serial.print(end - start);
Serial.println(" ms");
}A before/after value print of start and end would have shown the two numbers being only a few milliseconds apart instead of hundreds — instantly pointing at the bug.
Recap 5 min
Debugging is the difference between what you thought would happen and what did happen. Three flavours of Serial.println handle almost all of it: a trace print ("I got here!") to check whether a branch runs; a value print with a label to see what a variable really holds; a before/after print around a calculation to see which side of it is wrong. Work the loop: guess, prove, read, fix, clean up. Then comment out the debug prints — the finished sketch should be quiet again.
- Debugging
- The process of finding and fixing the difference between what a program should do and what it actually does. The word "bug" predates computing — it was used by engineers for any mysterious fault.
- Trace print
- A
Serial.println("...")placed inside a branch of code to confirm whether that branch runs. The message itself is the data — "I got here" is enough. - Value print
- A labelled
Serial.print("name="); Serial.println(name);that shows the current value of a variable. The label is essential — bare numbers in the Monitor are not debug evidence, they're noise. - Before/after print
- A pair of value prints — one immediately before a calculation, one immediately after — used to find out whether the inputs were wrong or the calculation was wrong. Splits the problem cleanly in two.
- Print debugging
- The umbrella term for the technique. Used in every programming language under different names — "console logging" in JavaScript, "printf debugging" in C, the same idea everywhere. The most common debugging technique in the entire industry.
- Symptom vs cause
- A symptom is what you see ("LED never lights"); a cause is what's actually wrong ("condition is backwards"). Prints reveal symptoms; you still have to think to get from symptom to cause.
Homework 5 min
The deliberate-bug exercise. Take any sketch from Cluster B or C that currently works — for example your burglar alarm, your reaction timer, your two-button combiner. Open it.
- Make a copy of the sketch (File → Save As). Don't break the original.
- Introduce one bug on purpose. Pick from this list: flip a
HIGHtoLOWin a condition; change a+to a-; remove theSerial.begin; change a==to a=; swap the two pin numbers; replacedelay(100)withdelay(10000). Pick the one you find most interesting. - Upload your broken copy. Observe what's wrong.
- Pretending you don't know where the bug is, use prints to find it. Aim to use all three patterns at least once across the homework (trace, value, before/after).
- Once you've located the bug, fix it and remove your debug prints.
This is the most important homework of Cluster D so far — you're rehearsing the exact loop that engineers run every day.
Also: a design reflection on paper.
- Which of the three patterns (trace / value / before-after) felt most useful for your bug? Why? ____
- How long did it take you to find the bug? Estimate. ____
- If you didn't know about print debugging at all, what would you have done instead? Honestly — would you have guessed, asked a friend, stared at the code, given up? ____
Bring back next class:
- The saved
.inofile — both the broken-and-then-fixed version (call itcluster-X-debug-practice). - A phone photo of the Serial Monitor at the moment your prints revealed the bug.
- Your three written reflection answers, in your notebook.
Heads up for next class: L01-26 "Reading From Serial" flips the connection around — instead of the Arduino talking to you, you type into the Monitor and the Arduino reads what you typed. Bring today's debug-practice sketch; we'll use the same Monitor window to start sending characters down the wire.