Every board that boots Linux without a keyboard attached to a serial console is running an
automated U-Boot boot script somewhere. In this lecture of our free embedded Linux
development course, we open up that mechanism: the bootcmd environment
variable, the countdown timer that guards it, and the escaping rules you need to write a script
that actually survives being stored as a single environment string. You will build and run a real
multi-step boot script on QEMU, not just read about one.
u-boot bootcmd
u-boot scripts
free linux kernel development course
bootdelay
What You Will Learn
- Why U-Boot needs an explicit script variable instead of just “always booting Linux”
- How
bootdelayandbootcmdcooperate during power-up autoboot - The exact escaping rule for chaining commands with semicolons inside one environment string
- How to build, save, and test your own
bootcmdon QEMU’s ARMvirtmachine - Common ways a bad boot script locks you out of your own board, and how to recover
Prerequisites
- A working U-Boot build for QEMU ARM (see our earlier lecture on building U-Boot from source)
- Basic familiarity with the U-Boot shell:
printenv,setenv,saveenv - QEMU installed on a Linux host
Why Autoboot Needs Two Variables, Not One
When U-Boot finishes low-level hardware init, it does not immediately jump into Linux. It first
checks a special environment variable named bootdelay, expressed in seconds. On the
serial console you’ll see a countdown like Hit any key to stop autoboot: 3... 2... 1....
That window exists so a human at the console can interrupt the process — press any key and you land
in the interactive U-Boot shell instead of booting Linux. If nobody presses anything before the
countdown reaches zero, U-Boot evaluates whatever command string is stored in the
bootcmd variable, exactly as if you had typed it into the shell yourself.
This separation matters in production: bootdelay=0 disables the interrupt window
entirely, which is common on shipped consumer devices where nobody should ever reach a boot prompt.
During development you’ll usually want a delay of 1–3 seconds so you can break in when something
goes wrong.
│
▼
SPL / ROM code hands off to U-Boot proper
│
▼
U-Boot prints countdown from bootdelay seconds
│
├── key pressed? ──yes──▶ Drop into interactive U-Boot shell
│
no
│
▼
Evaluate bootcmd as if typed at the shell
│
▼
Kernel (and optionally initrd/DTB) loaded and started
Writing A Multi-Command Boot Script
A U-Boot environment variable is a single string, but bootcmd is usually a sequence
of several logical steps: find storage, load a kernel image, maybe load a device tree, then boot it.
U-Boot’s shell lets you chain commands with a semicolon, but because the whole thing is stored and
re-parsed as one string, each semicolon inside a script destined for bootcmd must be
escaped with a backslash so it survives being written into the environment rather than being
consumed immediately by the shell that’s setting the variable.
A minimal two-step script — load an image from flash, then boot it — looks like this when you set
it interactively:
=> setenv bootcmd 'nand read 82000000 400000 200000\;bootm 82000000'
=> saveenv
Note the single quotes around the whole value in this example — they stop your host shell (or the
U-Boot shell’s own line parsing) from doing anything unexpected with the semicolon, while the
backslash inside is what actually gets stored in the environment. Miss the backslash and U-Boot will
try to execute bootm immediately while you’re still defining the variable, which is not
what you want.
Hands-On: A Custom Boot Script On QEMU ARM
Let’s build something more realistic than a single load-and-boot pair: a script that checks for an
SD card image first, and falls back to a network (TFTP) boot if the card isn’t present. This is the
same pattern real boards use to support both a “normal” boot path and a recovery/factory path.
# Boot QEMU's virt board with a virtio block device attached as our "SD card"
$ qemu-system-arm -M virt -m 512M -nographic \
-bios u-boot.bin \
-drive file=sdcard.img,if=none,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0 \
-netdev user,id=net0 -device virtio-net-device,netdev=net0
Inside the U-Boot shell that comes up, define the fallback script and store it:
=> setenv ep_sd_boot 'if load virtio 0:1 82000000 zImage; then bootz 82000000; fi'
=> setenv ep_net_boot 'dhcp; tftpboot 82000000 zImage; bootz 82000000'
=> setenv bootcmd 'run ep_sd_boot\;run ep_net_boot'
=> saveenv
=> boot
Here ep_sd_boot only reaches bootz if load actually
succeeds, thanks to the if ... then ... fi guard — a script that returns non-zero simply
falls through. Because run ep_net_boot only executes if run ep_sd_boot
never handed off to Linux, this gives you an automatic SD-first, network-second boot order with zero
manual intervention. Expected console output when the SD image is missing:
Hit any key to stop autoboot: 0
## Booting from SD card...
Card did not respond to voltage select! : -110
## No image found on virtio device, falling back to network
BOOTP broadcast 1
DHCP client bound to address 10.0.2.15
Using virtio-net#0 device
TFTP from server 10.0.2.2; our IP address is 10.0.2.15
Filename 'zImage'.
Load address: 0x82000000
Loading: ################ 1.8 MiB
## Starting application at 0x82000000 ...
Common Mistakes And Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
| bootcmd never even parses the second command | Missing backslash before ; |
Re-set the variable with \; between commands |
| Board boots straight to Linux, console shows no countdown | bootdelay=0 |
Interrupt from reset before bootdelay was set to 0, or use the board’s dedicated recovery button/strap if one exists |
| Environment changes disappear after reboot | Forgot saveenv |
Always run saveenv after setenv when the change should persist |
| Script “works” once then corrupts environment | Environment storage device is smaller than the region U-Boot is told to use | Check CONFIG_ENV_SIZE against your actual flash/eMMC environment partition |
Best Practices
- Keep
bootcmditself short — define named helper variables (ep_sd_boot,
ep_net_bootabove) and havebootcmdjustrunthem in order.
This keeps each piece independently testable from the shell. - Always test a new
bootcmdwithrun bootcmdfrom the interactive shell
before yousaveenvand reboot — a broken saved script is much more annoying to fix
than one still sitting only in RAM. - Keep a serial console attached during development. On production hardware you will eventually
setbootdelay=0, but do all your scripting with a nonzero delay first.
Security Considerations
An unauthenticated bootcmd is also an attacker’s easiest way in if they get physical
or serial access to your board: U-Boot will happily execute whatever is stored there, including a
script that loads and boots an attacker-supplied kernel from removable media. Production devices
should combine a locked-down bootcmd with U-Boot’s verified boot support so that only
signed images are ever passed to bootm/bootz — that mechanism is its own
topic and we’ll cover it in a later lecture.
Summary And Key Takeaways
bootdelaycontrols how long U-Boot waits for a key press before autobooting.bootcmdholds the script that runs automatically once that window closes.- Semicolons chaining commands destined for the environment must be backslash-escaped.
- Splitting logic into named helper variables that
bootcmdjustruns
makes scripts far easier to debug and reuse.
Conclusion
Boot scripting is the glue between “U-Boot is alive” and “Linux is running,” and it’s the first
place worth practising on real hardware or QEMU before you touch anything more advanced like device
tree overlays or verified boot. Once your fallback logic — SD first, network second, or whatever
order suits your board — is solid, you have a reliable, unattended boot path you can build the rest
of your embedded Linux course pipeline on top of.
Frequently Asked Questions
What happens if bootcmd is empty?
U-Boot drops straight into the interactive shell after the countdown, since there is nothing to
autoboot.
Can I use if/else logic inside a saved bootcmd?
Yes — U-Boot’s hush-style shell supports if ... then ... else ... fi, exactly as
shown in the SD/network fallback example above.
How do I recover if a bad bootcmd bricks my board?
On most boards you can hold a boot-strap button or short a pin during reset to force U-Boot into
the interactive shell regardless of bootcmd, then run env default -a; saveenv to reset
the environment.
Is bootdelay the same as a watchdog timeout?
No — bootdelay only governs the autoboot countdown window. A watchdog, if enabled, is a separate
mechanism that resets the board if software doesn’t “pet” it in time.
Why use run instead of pasting the whole script into bootcmd directly?
Named variables can be tested individually from the shell, reused across multiple boot paths, and
are far easier to read and diff than one long escaped string.
Does saveenv work the same way on every board?
The command is the same, but where it writes to — SPI-NOR, NAND, eMMC, or an environment file —
depends on your board’s CONFIG_ENV_IS_IN_* configuration.
Continue The Free Linux Kernel Development Course
Next we look at porting U-Boot itself to a brand-new board.
