How to Debug Embedded Systems Without Hardware Access

It’s 2 AM and a device 1,000 miles away is rebooting every 47 minutes. The on-site technician can power-cycle it and read you the blinking LED pattern over the phone. That’s it. No probe, no serial cable, no logic analyzer. You’re staring at source code, a git log, and a vague field report that says “it worked fine last week.”
Every embedded engineer hits this wall eventually. The entire discipline grew up assuming you’d have a bench, a probe, and a board within arm’s reach. That assumption is broken now: field-deployed IoT fleets, distributed teams, shared labs with a three-week booking queue. Remote firmware debug isn’t an edge case anymore. It’s the default for a growing percentage of embedded work.
This article walks through five concrete techniques for debugging embedded systems without physical hardware access, when each one fits, and what each one can’t do. They range from lightweight and passive to heavyweight and interactive, and the right choice depends on your connectivity, your constraints, and how badly things are broken.
[Diagram: Remote Debug Techniques Spectrum, from lightweight/passive (structured logging) through emulation/simulation, remote HIL, to heavyweight/interactive (remote JTAG/SWD), annotated with connectivity requirements and debug fidelity.]
Structured Embedded Logging: The Highest-ROI Investment You’ll Make
If you build nothing else for remote debuggability, build good logging. It’s always on, it’s passive, it works over the thinnest transports, and it doesn’t require a debug probe anywhere near the device. Logging is the technique that actually works on field-deployed hardware at scale.
But most embedded logging is terrible. Printf strings over UART at 115200 baud, no severity levels, no timestamps, no persistence across reboots. That’s not a debug system. It’s a hope system.
Here’s what production-grade embedded logging looks like:
Design principles that matter:
- Log levels with runtime configurability. You need to crank verbosity up on a misbehaving device without reflashing it. A command interface (shell, BLE characteristic, MQTT topic) that adjusts log level at runtime is non-negotiable.
- Structured binary formats, not human-readable strings. A printf string like
"ADC channel 3 read 4091 at tick 583920"burns 45+ bytes. A binary log entry encoding the same data takes 12. Bandwidth matters when your transport is NB-IoT or LoRa. Decode on the host side. - Circular buffers with crash-safe persistence. A ring buffer in retained RAM survives soft resets. Periodic flush to external flash or EEPROM survives hard power loss. If your logs disappear when the device crashes, they’re useless for debugging crashes.
- Monotonic timestamps. Wall-clock time is nice for correlation, but a monotonic tick count is what you need for sequencing events. Use both if you have them.
Transport mechanisms depend on what your device has available. UART piped through a cellular gateway works for field devices. BLE log exfiltration works for nearby-but-inaccessible devices. For cloud-connected products, a lightweight uplink (MQTT, CoAP) carrying binary log packets to a server-side decoder is the gold standard.
Tools worth knowing: Zephyr’s logging subsystem gets this right out of the box: binary log mode with deferred processing, per-module filtering, and multiple backends. Memfault’s SDK provides crash-safe log capture plus cloud-side decoding with no infrastructure work. For bare-metal projects, a custom ring-buffer implementation over retained RAM is a weekend project that pays dividends for years.
A compact binary log macro might look like this:
#define LOG_EVENT(module, severity, code, ctx) do { \
log_entry_t entry = { \
.timestamp_ms = systick_get_ms(), \
.module_id = (module), \
.severity = (severity), \
.error_code = (code), \
.context = (ctx), \
}; \
ringbuf_write(&g_log_buf, &entry, sizeof(entry)); \
} while (0)The 5 fields every embedded log entry needs:
- Timestamp — monotonic tick count (and wall-clock if available)
- Module ID — which subsystem generated this entry
- Severity — error, warning, info, debug, verbose
- Error/event code — machine-parseable, not a string
- Context data — the 4–8 bytes of payload that explain what happened (register value, state machine state, counter)
Logging won’t tell you everything. It can’t replace stepping through code, and it gives you only what you instrumented. But it’s the one technique that works on 10,000 devices simultaneously, requires no infrastructure on the device beyond firmware, and catches the intermittent bugs that vanish the moment you attach a debugger.
Remote JTAG/SWD: Your Bench, Over the Network
When you need full interactive debugging (breakpoints, register inspection, memory reads, stepping) but the hardware is in a lab across the building or across the country, remote JTAG/SWD access bridges the gap.
The setup is straightforward. A debug probe connected to the target board exposes its debug interface over TCP/IP instead of USB:
- SEGGER J-Link Remote Server runs on any machine connected to the J-Link via USB. Your local IDE (Ozone, VS Code, any GDB client) connects to it over the network as if the probe were local. Setup takes minutes.
- OpenOCD with TCP/IP GDB server works with ST-Link, CMSIS-DAP, and dozens of other probes. Launch OpenOCD on the remote machine, connect your local GDB to
remote-host:3333. Open-source and free. - Lauterbach TRACE32 remote API supports full trace and debug over network connections, with PowerTrace hardware handling the high-speed capture locally.
Latency is the limiting factor. On a LAN, remote JTAG feels nearly local. Over a WAN or VPN with 50+ ms round-trip, single-stepping becomes painful, since each step command round-trips. Batch operations (flash programming, memory dumps) work fine. Real-time trace streaming does not.
Security is non-negotiable. A JTAG port is root access to your device. Never expose a debug server directly to the internet. SSH tunneling is the minimum: ssh -L 19020:localhost:19020 lab-gateway forwards J-Link Remote Server traffic through an encrypted tunnel. A proper VPN is better for shared team setups.
Remote JTAG makes sense for lab hardware you can’t physically visit. It does not make sense for field-deployed devices. You’d need a debug probe attached to every unit, and the security surface is unacceptable. That’s what logging is for.
Emulation and Simulation: Debug Without Any Hardware at All
Sometimes the device doesn’t exist yet, or you can’t reproduce the bug on real hardware, or you want to test a fix before burning a field OTA update. Emulators and simulators let you run your firmware on your laptop.
QEMU supports a range of ARM Cortex-M and Cortex-A targets. You can boot a firmware binary, attach GDB, set breakpoints, and step through code. It models CPU cores and some peripherals (UART, timers, interrupt controllers), but its peripheral coverage for specific MCU families is incomplete. You’ll get correct execution of CPU instructions, but don’t trust QEMU’s opinion on SPI timing or DMA behavior.
Renode (renode.io) goes further. It models specific boards and SoCs (STM32, nRF52, RISC-V platforms) with detailed peripheral simulations. Its killer feature is multi-node simulation: you can spin up a network of emulated devices and test communication protocols between them. It integrates with Robot Framework for automated test scripting and with GDB for interactive debug.
Vendor simulators (TI CCS simulator, STM32CubeIDE simulation mode, Microchip MPLAB simulator) model their own silicon most accurately but are locked to their ecosystems.
Be honest about the blind spots. Emulation is excellent for logic bugs, protocol state machines, algorithm validation, and RTOS scheduling issues. It’s poor-to-useless for analog peripheral behavior, precise interrupt timing, race conditions involving DMA, and anything that depends on electrical characteristics of the real hardware. If your bug is “the SPI peripheral returns garbage on every third transaction after the device heats up,” simulation won’t help you.
Remote Hardware-in-the-Loop: The On-Bench Experience, Delivered Over VPN
A remote HIL bench gives you nearly everything you’d have sitting in front of the hardware. The investment is modest, and for teams that need regular access to real targets without traveling to a lab, the payback is measured in weeks, not months.
What a remote HIL bench looks like:
- Target board connected to a networked debug probe (J-Link, ST-Link via OpenOCD)
- USB-to-serial adapter for console UART access, exposed via
ser2netor similar - Smart power relay (web-controlled) for hard power cycling
- Optional: USB webcam pointed at the board for visual state (LEDs, display) and a USB-controlled relay for button presses
- A Raspberry Pi or small Linux box acting as the gateway for all of the above
[Architecture Diagram: Target board ↔ J-Link/ST-Link ↔ Raspberry Pi (ser2net, J-Link Remote Server, power relay GPIO, webcam) ↔ VPN ↔ Engineer’s workstation]
The total cost for one remote bench runs $200–$500: the Pi, a debug probe, a serial adapter, a relay module, and a webcam. That’s less than one round-trip flight to a lab.
At scale, teams automate this. A firmware CI/CD pipeline triggers pytest + pyOCD to flash a build onto the target, run integration tests, capture serial output, and report pass/fail, all on real hardware, all unattended. Lab reservation systems (even a shared calendar) prevent contention when multiple engineers need the same board. Automated git-bisect on real hardware can isolate the commit that introduced a regression overnight while you sleep.
The limiting factor is that each bench is one board. You can’t test a fleet, and you can’t reproduce environmental conditions (temperature, RF interference, brownouts) without additional lab equipment. But for interactive debugging and automated regression testing on real silicon, remote HIL is as good as being there.
Defensive Firmware Architecture: Build Debuggability In From Day One
The techniques above all become dramatically more effective if the firmware itself cooperates. Debuggability is an architecture decision, not something you bolt on after the first field failure.
Patterns that pay for themselves:
- Fault handlers that persist state. On Cortex-M, a HardFault handler can capture R0–R12, LR, PC, xPSR, and the faulting stack frame into retained RAM or flash before resetting. That’s a full crash report, the remote equivalent of seeing the debugger stopped at the fault.
- Watchdog-triggered core dumps. If the watchdog fires, you have a few milliseconds before reset. Use them to snapshot critical state.
- OTA-updatable debug builds. Maintain a build variant with extra instrumentation (verbose logging, runtime assertions, trace hooks) that you can push to a misbehaving field device via OTA firmware update. Pull it back once you’ve captured the data.
- Runtime-configurable trace points. Don’t use compile-time
#ifdef DEBUG. Use a command interface that enables trace output for specific modules on specific devices at runtime. This eliminates the “I need to rebuild and reflash to get more data” cycle that kills remote debug velocity.
This is the shift-left argument for debugging. Every hour you invest in defensive firmware architecture saves ten hours of painful remote triage later.
Picking the Right Technique for Your Situation
| Scenario | Recommended Approach |
|---|---|
| Device is deployed in the field, reachable over a network | Structured embedded logging + OTA debug builds |
| Device is in a lab you can’t physically visit | Remote JTAG/SWD or remote HIL bench |
| You can’t reproduce the bug on real hardware | Emulation/simulation (logic bugs) or enhanced logging (timing/hardware bugs) |
| You’re starting a new project | Build in all of the above from day one: structured logging, crash persistence, remote debug hooks, OTA debug build variant |
| Device has no network connectivity at all | Crash-safe log persistence to flash + physical log retrieval on next service visit |
Building Remote Debug Into Your Next Project
Remote firmware debug is a solved problem, not with any single tool, but with a layered approach. Structured logging is the foundation that works everywhere. Remote JTAG and HIL cover interactive debugging when you have lab access but not physical presence. Emulation fills the gap when you have no hardware at all. And defensive firmware architecture is the multiplier that makes everything else work better.
The industry is moving toward always-observable firmware as a baseline expectation, not a luxury. The teams that treat debuggability as a first-class design requirement, rather than a response to the first field crisis, ship faster, resolve issues cheaper, and spend far fewer sleepless nights talking a technician through a power cycle at 2 AM.
The best time to build this infrastructure was at project kickoff. The second-best time is the commit you’re about to push.
Hubble Network enables always-observable firmware across billions of devices—connecting directly from Bluetooth chips to satellites, no gateways or local infrastructure required. See how it works →