1540  Software Prototyping: Testing and Debugging

1540.1 Learning Objectives

By the end of this chapter, you will be able to:

  • Write Unit Tests: Create testable firmware functions using Unity and PlatformIO
  • Debug with Serial Output: Implement structured logging with debug levels
  • Use Hardware Debuggers: Set breakpoints and inspect variables with JTAG/SWD
  • Debug Remotely: Monitor deployed devices via OTA logging and telnet

1540.2 Prerequisites

Before diving into this chapter, you should be familiar with:


1540.3 Unit Testing

PlatformIO Unit Tests:

#include <Arduino.h>
#include <unity.h>

void test_temperature_reading() {
    float temp = readTemperature();
    TEST_ASSERT_TRUE(temp > -40.0 && temp < 85.0);
}

void test_sensor_initialization() {
    bool initialized = initSensor();
    TEST_ASSERT_TRUE(initialized);
}

void setup() {
    UNITY_BEGIN();
    RUN_TEST(test_temperature_reading);
    RUN_TEST(test_sensor_initialization);
    UNITY_END();
}

void loop() {
    // Tests run once in setup
}

Test Directory Structure:

test/
β”œβ”€β”€ test_sensor_logic/
β”‚   └── test_main.cpp      # Tests on HOST (your PC)
└── test_embedded/
    └── test_main.cpp      # Tests on TARGET (ESP32)

Running Tests:

# Run tests on host machine (fast, no hardware)
pio test -e native

# Run tests on actual hardware
pio test -e esp32dev

# Run specific test folder
pio test -e native -f test_sensor_logic

1540.4 Serial Debugging

Debug Macros:

#define DEBUG 1

#if DEBUG
  #define DEBUG_PRINT(x) Serial.print(x)
  #define DEBUG_PRINTLN(x) Serial.println(x)
#else
  #define DEBUG_PRINT(x)
  #define DEBUG_PRINTLN(x)
#endif

void loop() {
  float temp = readSensor();
  DEBUG_PRINT("Temperature: ");
  DEBUG_PRINTLN(temp);
}

Structured Logging:

enum LogLevel {
  LOG_ERROR,
  LOG_WARN,
  LOG_INFO,
  LOG_DEBUG
};

LogLevel currentLevel = LOG_INFO;

void log(LogLevel level, const char* message) {
  if (level > currentLevel) return;

  const char* levelStr[] = {"ERROR", "WARN", "INFO", "DEBUG"};
  Serial.print("[");
  Serial.print(millis());
  Serial.print("] [");
  Serial.print(levelStr[level]);
  Serial.print("] ");
  Serial.println(message);
}

// Usage
log(LOG_INFO, "Sensor initialized");
log(LOG_ERROR, "Wi-Fi connection failed");
log(LOG_DEBUG, "Temperature: 25.5");

Formatted Logging:

void logf(LogLevel level, const char* format, ...) {
  if (level > currentLevel) return;

  char buffer[256];
  va_list args;
  va_start(args, format);
  vsnprintf(buffer, sizeof(buffer), format, args);
  va_end(args);

  log(level, buffer);
}

// Usage
logf(LOG_INFO, "Temperature: %.2f C, Humidity: %.1f%%", temp, humidity);

1540.5 Hardware Debugging

JTAG/SWD Debugging: - Set breakpoints in code - Step through execution - Inspect variables - View call stack - Supported by professional IDEs (STM32CubeIDE, PlatformIO with debugger)

PlatformIO Debug Configuration:

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino

; Debug configuration
debug_tool = esp-builtin     ; For ESP32-S3/C3
; debug_tool = esp-prog      ; For ESP32 with ESP-PROG
debug_init_break = tbreak setup
debug_speed = 5000
build_type = debug

Common GDB Commands:

# In PlatformIO Debug Console
info registers         # Show CPU registers
print variable_name    # Print variable value
watch variable_name    # Break when variable changes
bt                     # Backtrace (call stack)
list                   # Show source code
continue               # Resume execution

Logic Analyzer Debugging: - Capture I2C, SPI, UART communication - Verify timing and protocols - Identify bus conflicts or errors - Decode protocol messages automatically

LED Debugging:

// Blink patterns indicate status
void indicateError(int errorCode) {
  for(int i = 0; i < errorCode; i++) {
    digitalWrite(LED_PIN, HIGH);
    delay(200);
    digitalWrite(LED_PIN, LOW);
    delay(200);
  }
  delay(2000);
}

// Error codes
// 1 blink: Sensor error
// 2 blinks: Wi-Fi error
// 3 blinks: MQTT error
// 4 blinks: OTA error

1540.6 Remote Debugging

OTA Logging (MQTT):

void logToCloud(String message) {
  if (WiFi.status() == WL_CONNECTED) {
    client.publish("device/logs", message.c_str());
  }
}

// Usage
logToCloud("Boot complete, firmware v1.2.3");
logToCloud("Sensor error: NaN reading");

Telnet Debugging:

#include <TelnetStream.h>

void setup() {
  Serial.begin(115200);
  TelnetStream.begin();
}

void loop() {
  // Output goes to both Serial and Telnet
  TelnetStream.println("Debug message over telnet");
  Serial.println("Debug message over serial");

  // Read Telnet commands
  if (TelnetStream.available()) {
    char cmd = TelnetStream.read();
    handleCommand(cmd);
  }
}

Remote Console:

#include <RemoteDebug.h>

RemoteDebug Debug;

void setup() {
  Debug.begin("esp32-device");
  Debug.setResetCmdEnabled(true);
}

void loop() {
  Debug.handle();

  // Debug levels: verbose, debug, info, warning, error
  debugV("Verbose message");
  debugD("Debug message");
  debugI("Info message");
  debugW("Warning message");
  debugE("Error message");
}

1540.7 Debugging Tools Comparison

Tool Best For Advantages Limitations
Serial.print() Quick debugging Simple, universal Adds timing overhead
JTAG/SWD Complex bugs Real-time, non-intrusive Requires hardware probe
Logic Analyzer Protocol issues Shows timing, decodes protocols External equipment
OTA Logging Deployed devices Remote monitoring Requires connectivity
Unit Tests Regression prevention Automated, fast Can’t test hardware interaction

1540.8 Debugging Technique Selection

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22', 'tertiaryColor': '#ECF0F1'}}}%%
flowchart TD
    START["Debug Issue"] --> Q1{"Type of<br/>problem?"}
    Q1 -->|"Logic bug"| UNIT["Unit Tests +<br/>Serial Debug"]
    Q1 -->|"Timing issue"| LOGIC["Logic Analyzer"]
    Q1 -->|"Memory/crash"| JTAG["JTAG Debugger"]
    Q1 -->|"Field failure"| REMOTE["Remote Logging"]
    Q1 -->|"Protocol error"| WIRE["Wireshark +<br/>Logic Analyzer"]

    style START fill:#E67E22,stroke:#2C3E50,color:#fff
    style UNIT fill:#16A085,stroke:#2C3E50,color:#fff
    style LOGIC fill:#16A085,stroke:#2C3E50,color:#fff
    style JTAG fill:#16A085,stroke:#2C3E50,color:#fff
    style REMOTE fill:#16A085,stroke:#2C3E50,color:#fff
    style WIRE fill:#16A085,stroke:#2C3E50,color:#fff

1540.9 Knowledge Check

Question: An IoT sensor is randomly freezing in production. During debugging, you discover the crash occurs in an interrupt service routine (ISR). Which debugging tool would be MOST effective for identifying the root cause?

Explanation: Hardware debuggers (JTAG/SWD) are essential for ISR debugging because they allow stepping through code without altering timing, inspecting registers, examining stack traces, and detecting race conditions. Serial.print() adds delays that mask timing bugs and can’t be used in ISRs safely. Oscilloscopes show signals but not code execution. Logic analyzers capture signals but can’t examine variables or call stacks.

Question: When debugging a memory leak in embedded firmware that causes the device to crash after 48 hours of operation, which technique would be MOST effective?

Explanation: Hardware debuggers enable real-time heap monitoring without modifying code or timing. Set breakpoints in malloc/free equivalents, track allocation sizes, and identify growing heap usage patterns. Serial logging adds overhead that changes timing and may hide bugs. Increasing heap size delays the crash but doesn’t fix the leak. Periodic reboots mask the problem rather than solving it.

Question: Match each software debugging technique with its PRIMARY advantage:

Techniques: 1. Serial printf, 2. JTAG/SWD, 3. Logic analyzer, 4. Unit tests, 5. HIL simulation

Advantages: A. Test without hardware, B. Real-time breakpoints, C. Simple/universal, D. Protocol timing, E. Validate algorithms

Explanation: 1-C: Serial printf is simple, works everywhere. 2-B: JTAG provides real-time breakpoints. 3-D: Logic analyzer shows protocol timing/integrity. 4-E: Unit tests validate algorithms on PC. 5-A: HIL tests firmware without physical hardware.

1540.10 What’s Next

The next section covers Best Practices and Common Pitfalls, where you’ll learn code organization, configuration management, power optimization, error handling, and how to avoid the most common mistakes in IoT firmware development.