First Boot Debugging: What to Do When Your Firmware Flashes but Nothing Happens

Your flash tool says “success.” Your terminal is blank. No serial output, no blinking LED, no sign of life. You unplug the USB cable, plug it back in, flash again. Same result. You Google the error, except there is no error. That’s the whole problem.
This is the most common wall in embedded development, and there’s nothing to react to. No error message to search. No stack trace to read. Just… nothing.
Here’s the thing you need to internalize: silence is the default state of a microcontroller. An MCU does nothing visible unless your code explicitly configures a peripheral and tells it to produce output. If you flashed a minimal app with no printk, no UART config, no GPIO toggle, then silence is correct behavior.
The good news: there’s a systematic way to figure out which link in the chain is broken.
What “Flash Succeeded” Actually Means
When your flash tool reports success, it confirms exactly one thing: bytes were written to flash memory. That’s it.
Between “bytes in flash” and “text on your screen,” there’s a whole chain of things that each need to work independently:
FLASH TOOL
|
| "Flash succeeded" only confirms this step
v
+-------------+
| Flash Memory | <-- Are the right bytes here?
+-------------+
|
v
[ RESET ] <-- Did the MCU actually reset?
|
v
+-------------+
| Startup Code | <-- Vector table, clock init, C runtime
+-------------+
|
v
[ main() ] <-- Does execution reach here?
|
v
+-----------------+
| Peripheral Init | <-- Is UART/GPIO configured?
+-----------------+
/ \
v v
[UART] [GPIO/LED]
| |
v v
Serial Output LED BlinkYour debugging job is to figure out which link is broken. Test from the top down.
Layer 0: Did the Flash Actually Work?
Before anything else, make sure the bytes actually landed where they should.
Check your flash tool output carefully. Look for an explicit “Verify OK” or equivalent, not just the absence of errors. With nRF52 and west flash, the output can scroll by fast. For nrfjprog specifically, you can run nrfjprog --verify against your hex file to confirm.
Then check the obvious stuff:
Is your binary non-empty? Run ls -la build/zephyr/zephyr.hex and make sure it’s a reasonable size (not 0 bytes, not just a header).
Did you build for the right board? If you ran west build -b nrf52840dk_nrf52840 but you’re holding an nRF52833-DK, the binary might flash without errors but produce code that won’t run correctly on your chip. Typos in the board name can cause silent failures too.
The fastest sanity check: flash Zephyr’s built-in blinky sample. If that doesn’t work, the problem isn’t in your code.
Layer 1: Is the MCU Actually Running?
This is where a blinking LED becomes your best friend. Forget serial output for now; serial involves a long chain of configuration. An LED toggle in main() proves execution with almost zero dependencies.
Here’s the minimal Zephyr blinky for an nRF52840-DK:
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#define LED0_NODE DT_ALIAS(led0)
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
int main(void) {
gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
while (1) {
gpio_pin_toggle_dt(&led);
k_msleep(500);
}
return 0;
}If the LED blinks, your MCU is alive and running your code. Skip ahead to Layer 2; your problem is in the output path.
If the LED doesn’t blink, something is preventing execution from reaching main(). Common culprits:
The board didn’t reset after flash. Some flash configurations don’t trigger a reset automatically. Hit the physical reset button on your dev kit. For nRF52 boards, try west flash --recover, which performs a full erase and reset.
Flash protection is enabled. Nordic’s APPROTECT feature can lock you out. Run nrfjprog --recover to clear it. This erases everything on the chip, which is fine; you’re about to reflash anyway.
Wrong build target. If your build generated code for a different chip’s memory map, the vector table won’t be where the MCU expects it. Double-check your west build -b argument.
If you want even more certainty, attach a debugger. Run west debug to start a GDB session through J-Link, then set a breakpoint at main. If it hits, execution is reaching your code. If it doesn’t, the problem is in startup code or the vector table. The Zephyr quick-start guide for bare metal covers some of the fundamentals of getting a board to first boot.
Layer 2: Is the Output Path Configured?
This is where most beginners get stuck. If you’re coming from Arduino, serial output “just works” because the Arduino framework configures everything behind the scenes. In Zephyr (and most professional embedded environments), you have to wire up every piece yourself.
UART output requires four things to all be correct simultaneously:
- Hardware UART pins physically connected to something (on the nRF52840-DK, UART0 routes through the onboard J-Link to a USB CDC ACM port)
- The UART peripheral enabled and configured in your firmware
- A console subsystem routing
printkoutput to that UART - Your host terminal listening on the right port at the right baud rate
Miss any one of these, and you get silence.
Firmware-side (Zephyr/nRF52):
Your prj.conf needs at minimum:
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y
CONFIG_SERIAL=yWithout these, printk has nowhere to send its output. The calls still compile and run; they just vanish into nothing.
In the devicetree, the chosen node must specify zephyr,console = &uart0;. For the nRF52840-DK, this is already set in the board’s default DTS file. But if you’ve added a custom overlay, you might have accidentally overridden it. Check your overlay files for anything touching uart0 or the chosen node. Make sure the UART node has status = "okay";.
Host-side:
Are you on the right serial port? On Linux, it’s typically /dev/ttyACM0. On Windows, check Device Manager for the COM port number. On macOS, look for /dev/tty.usbmodem*. Run ls /dev/tty* before and after plugging in the board to spot which device appears.
Is your baud rate correct? Zephyr defaults to 115200. If your terminal is set to 9600 (a common default in some tools), you’ll see either garbage or nothing.
Here’s the one that catches everyone at least once: is your terminal open before you reset the board? Boot messages print once, during startup. If you open your terminal after the board has already booted, you’ve missed them. Open the terminal first, then press the reset button.
On Linux, you might also have a permissions issue. If you get “permission denied” on the serial port, add yourself to the dialout group: sudo usermod -aG dialout $USER, then log out and back in.
Here’s a minimal test. Put this in main():
#include <zephyr/kernel.h>
int main(void) {
printk("Hello from main\n");
while (1) {
k_msleep(1000);
}
return 0;
}With this prj.conf:
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y
CONFIG_SERIAL=y
CONFIG_PRINTK=yIf “Hello from main” appears, your output path works. If it doesn’t, work through the host-side checklist above.
Layer 3: Is Your Application Code Doing What You Think?
You’ve confirmed the MCU runs (blinky works) and UART works (printk “hello” appears). But your actual project is still silent. The problem is in your application code.
Common traps:
main() returns immediately. If your main function doesn’t have a loop or a k_sleep, the thread exits. In Zephyr, once main() returns, the main thread terminates, and any output you expected from later code never runs.
A crash before output. Your code might be hitting a fault before it reaches any printk. Zephyr has a built-in fault handler, but you need CONFIG_PRINTK=y for it to actually print the fault info. Without that, the crash is silent too.
Blocking on initialization. If your code waits for a sensor, network interface, or external resource that isn’t present or responding, it can hang before producing any output. Add printk calls before and after each initialization step to find where it stalls. This one tends to be sneaky because there’s no crash, just an indefinite wait.
Logging configured wrong. If you’re using Zephyr’s LOG_MODULE_REGISTER instead of raw printk, you need CONFIG_LOG=y and CONFIG_LOG_BACKEND_UART=y in your prj.conf. Also check your log level. If you registered at LOG_LEVEL_DBG but the global level is LOG_LEVEL_ERR, your debug messages get filtered out silently.
The single best habit: always put a printk as the very first line of main() when debugging. If you see it, execution reached main. If you don’t, the problem is earlier. If you’re working with Hubble’s Bluetooth SDK, the device SDK introduction walks through the expected boot sequence and what output to expect at each stage.
The Checklist
Here’s everything consolidated. Work through it top to bottom. Don’t skip ahead.
POST-FLASH SILENCE DEBUGGING CHECKLIST
=======================================
1. [ ] Flash tool reported success + verify
2. [ ] Correct board target in build command
3. [ ] Binary file is non-zero size
4. [ ] Board was reset after flash
5. [ ] Blinky sample works on this board
6. [ ] printk("hello") as first line of main()
7. [ ] prj.conf: CONFIG_CONSOLE=y
8. [ ] prj.conf: CONFIG_UART_CONSOLE=y
9. [ ] prj.conf: CONFIG_SERIAL=y
10. [ ] Correct serial port selected on host
11. [ ] Baud rate = 115200
12. [ ] Terminal opened BEFORE board reset
13. [ ] main() doesn't return (has loop/sleep)
14. [ ] No crash before output (attach debugger)Most of the time, the answer is somewhere in items 7 through 12. The output path has a lot of moving parts, and embedded toolchains don’t hold your hand the way application frameworks do.
Silence Is Information
Every microcontroller boots into a state where nothing is visible unless you explicitly set it up. Once you accept that, debugging becomes methodical: start from the physical layer, verify each link, and don’t assume anything works until you’ve proven it.
You’ll hit this same wall with different symptoms throughout your career: a sensor that doesn’t respond, a radio that won’t transmit, a display that stays dark. The layered, hardware-up approach works every time.
Bookmark that checklist. You’ll use it more than once.
When you’re ready to go deeper, learning JTAG/SWD debugging and Zephyr’s logging subsystem will give you even more visibility into what’s happening inside your firmware. But for now, if you can systematically walk through these layers and get a printk on screen, you’re in better shape than you think.
Hubble Network connects Bluetooth devices directly to satellite — no gateways, no ground infrastructure to debug. See how it works →