Loading header...

Defensive C, Debugging, and Testing

Defensive embedded C does not mean slow or bloated code. It means code that states assumptions, handles invalid inputs deliberately, and is structured so failures can be reproduced.

Learning Objectives

By the end of this lesson, you should be able to:

  • add useful checks without hiding failures;
  • design functions for host-side tests;
  • use assertions, return codes, and fault flags appropriately;
  • plan a debug session around evidence rather than guesses.

Defensive Interfaces

bool sensor_read(sensor_sample_t *out_sample)
{
    if (out_sample == 0) {
        return false;
    }

    if (!sensor_bus_ready()) {
        return false;
    }

    return sensor_read_registers(out_sample);
}

The function refuses invalid output storage and reports failure explicitly.

Assertions

Assertions are good for programmer errors and impossible states.

#include <assert.h>

void queue_push(queue_t *q, uint8_t value)
{
    assert(q != 0);
    assert(q->count <= q->capacity);
}

Do not rely on assertions for normal runtime error handling if they may be disabled in release builds.

Host Tests

Code that separates hardware access from logic can be tested on a PC.

flowchart LR TEST["Host unit tests"] --> LOGIC["driver logic"] FAKE["fake register block"] --> LOGIC LOGIC --> TARGET["same code on MCU"]

Ring buffers, parsers, state machines, and register mask helpers are excellent host-test targets.

Debugging Process

  1. State the expected behavior.
  2. Capture the actual behavior.
  3. Reduce the test case.
  4. Inspect inputs, outputs, and state transitions.
  5. Change one thing.
  6. Record the result.

Useful Embedded Evidence

  • compiler warnings;
  • map file size and symbol placement;
  • reset reason registers;
  • fault status registers;
  • GPIO timing pins;
  • UART logs with timestamps;
  • debugger watchpoints;
  • logic analyzer captures.

Common Mistakes

  • Debugging with optimization disabled and shipping with optimization enabled without retesting.
  • Adding prints that change timing, then trusting the result.
  • Ignoring compiler warnings.
  • Treating intermittent failures as random.
  • Testing only the happy path.

Summary

Defensive C makes assumptions visible and failures easier to isolate. The best embedded tests run much of the logic on a host computer, then reserve hardware debugging for the parts that truly depend on hardware timing and peripherals.

Further Reading