Debugging Arduino Code Like a Detective

RS
Rishabh Sinha
Author

Last week, I spent four hours debugging a project that should have worked. The code was correct. The wiring looked right. But the servo wasn't moving.

Spoiler: I had accidentally plugged into the wrong row on my breadboard. The power rail on that particular board doesn't run all the way across—there's a gap in the middle.

Four hours. For a breadboard gap.

This is hardware development. The bugs are often not in the code at all. And that makes debugging fundamentally different from software.

Here's the approach I've developed over years of making (and finding) mistakes.

The Three Layers

Every hardware bug lives in one of three layers:

  1. Hardware — Physical connections, power supply, component damage
  2. Driver — Pin configurations, initialization, library issues
  3. Logic — Your actual program behavior

Most debugging failures happen when you're searching in the wrong layer. You're tweaking your code when the problem is a loose wire. Or you're checking connections when the issue is a logic error.

Start by figuring out which layer you're in.

Layer 1: Is the Hardware Working?

Before you debug any code, verify the physical setup.

Power First

Get a multimeter. Check that:

  • Your board is getting the voltage it expects
  • Your 3.3V rail is actually 3.3V
  • Your 5V rail is actually 5V
  • Ground is connected and continuous

I can't count how many "mysterious" bugs were actually power issues. Brownouts cause weird behavior. Voltage drops cause sensors to give bad readings. Floating grounds cause everything to go haywire.

Simplify the Circuit

Disconnect everything except what you're testing. If you're debugging a sensor, remove all other sensors, displays, and peripherals. Get the simplest possible circuit working first.

This sounds obvious. Nobody does it. We all think "but everything else was working yesterday!" Yeah, and now it's not, and you don't know why. Simplify.

Check Connections Physically

Not "look at them"—actually check them. Push on your jumper wires. Reseat your modules. Wiggle things while watching for changes in behavior.

Intermittent connections are the worst. The circuit works when you're not touching it. The moment you try to observe it, something shifts. Breadboard connections degrade over time. Solder joints crack. Headers get bent.

Layer 2: Is the Board Talking to the Hardware?

Once you're confident the physical setup is correct, verify that your microcontroller can actually communicate with your components.

Serial Is Your Best Friend

void setup() {
  Serial.begin(115200);
  while (!Serial) {} // Wait for serial connection
  Serial.println("Starting up...");
}

If you don't see "Starting up..." in your serial monitor, your code isn't running. That's important information.

Test Components in Isolation

Write the simplest possible code to test each component:

// Testing a sensor
void setup() {
  Serial.begin(115200);
  Wire.begin();
  
  // Check if sensor responds
  Wire.beginTransmission(0x40); // sensor address
  int error = Wire.endTransmission();
  
  if (error == 0) {
    Serial.println("Sensor found!");
  } else {
    Serial.print("Sensor error: ");
    Serial.println(error);
  }
}

If this doesn't work, you haven't found the bug yet—but you've narrowed it down to hardware or basic configuration.

Check Pin Assignments

This catches me more often than I'd like to admit. GPIO numbers are not the same as pin labels on your board.

On a NodeMCU, "D5" is GPIO 14. On an ESP32, pin labels vary by board. Print your actual pin assignments:

Serial.print("Using pin: ");
Serial.println(LED_PIN); // What does this actually resolve to?

Layer 3: Is Your Logic Correct?

The hardware works. The components respond. But the behavior is still wrong. Now you're debugging actual code.

State Machines Are Your Friend

If your code has complex behavior, model it as a state machine. Then add logging for state transitions:

enum State { IDLE, READING, SENDING, ERROR };
State currentState = IDLE;
State previousState = IDLE;

void loop() {
  if (currentState != previousState) {
    Serial.print("State: ");
    Serial.println(stateToString(currentState));
    previousState = currentState;
  }
  
  // ... rest of your code
}

When things go wrong, you can see exactly which state you're in and what transition caused the problem.

Binary Search Your Code

If you have no idea where the bug is, comment out half your code. Does the problem persist?

If yes, the bug is in the remaining half. If no, it's in what you commented out.

Repeat until you've narrowed it down. This sounds tedious. It's faster than staring at code hoping to spot the issue.

Check Your Assumptions

Every bug is a failed assumption. Find which one.

  • "The sensor always returns valid data" — What if it doesn't?
  • "This function takes milliseconds" — Does it actually?
  • "The WiFi is always connected" — What happens when it isn't?
  • "This variable never goes negative" — Are you sure?

Add assertions or logging around your assumptions. When they fail, you've found your bug.

The Rubber Duck Method (Upgraded)

Explain your code to something. A rubber duck, a colleague, or an AI.

The act of explaining forces you to actually think through what your code does versus what you think it does. Half the time, I catch my own bug mid-explanation.

With AI assistance in Embedr, this is even more useful. Describe the expected behavior, the actual behavior, and what you've already checked. The AI can often spot patterns or suggest things you haven't tried.

When You're Truly Stuck

Sometimes you've tried everything and nothing works. Here's what I do:

Take a break. Seriously. Walk away for an hour. Your brain keeps processing in the background. I've solved bugs while making coffee.

Rebuild from scratch. Sometimes the fastest path forward is starting over. Build the simplest version that could work, then add complexity one piece at a time until it breaks.

Ask for help. Describe the problem clearly: what you expected, what happened, and what you've already tried. The Embedr Discord has people who've probably seen your exact issue.

Prevention

The best debugging session is the one you never have.

  • Test as you go. Don't build the whole circuit, write all the code, and then try to debug the mess. Add one component, test it, move on.
  • Keep a lab notebook. Write down what works. When you come back to a project in six months, you'll thank yourself.
  • Save working states. Before making changes, commit your code. If things break, you can always go back.

Hardware bugs are frustrating because they can be anywhere. But they're also solvable. With a systematic approach, you will find them.

The servo that wouldn't move? Once I checked the power rail with a multimeter, it took 30 seconds to find the gap. Four hours of code debugging, solved by a $10 tool.

Always check the hardware first.


Got a debugging horror story? I'd love to hear it. Share on our feedback page or join the Discord.

Tags: #debugging #arduino #tutorial #tips