Getting an RV1106 camera to boot in ~1 second
This is a write-up of getting an embedded camera product based around the
Rockchip RV1106
to boot from cold power-on to a running application in about a second. The unit boots from
SPI NAND flash, and the boot chain is the usual embedded Linux stack: bootrom → U-Boot
SPL → U-Boot → Linux → BusyBox init → the application (written in Go).
None of the individual changes here are clever. The interesting part is that almost all of
them fall out of two questions:
what is being read from flash that doesn't need to be? and
what is running at boot that the product doesn't need?
Measure first
Every timing measurement was taken by timestamping the debug serial port with
grabserial, with the console running at
1.5 Mbaud so the logging itself doesn't distort the numbers.
| Stage |
Budget |
| Bootrom |
~300 ms |
| U-Boot SPL |
~250 ms |
| U-Boot |
~350 ms |
| Linux kernel |
~600 ms |
| Init & application start |
~300 ms |
U-Boot also gained a small patch to display read speeds in the mtd command
output, so raw NAND throughput could be verified rather than guessed at.
A lot of "boot speed" changes are really "flash throughput" changes in disguise,
and you want to know which one you're looking at.
U-Boot: do less, read less
- Zero boot delay, quiet console
-
CONFIG_BOOTDELAY=0, plus CONFIG_SYS_CONSOLE_INFO_QUIET and
turning off CONFIG_DISPLAY_CPUINFO. You can still interrupt autoboot by
holding a key from power-on; you just don't pay for a delay when you don't. With the
console at 1.5 Mbaud, what little logging remains doesn't stall anything.
- Static environment
-
The U-Boot environment is compiled into U-Boot itself rather than stored on flash. This
eliminates an SPI NAND read (and CRC scan) of the environment partition, along with all
the machinery for locating and validating it. As a bonus, a corrupted environment can no
longer render a unit unbootable, since there is no writable environment to corrupt.
- No MMC support
-
The unit boots from SPI NAND, so MMC support was ripped out of U-Boot entirely. Probing
MMC controllers that have nothing useful attached was costing roughly 200 ms of pure
waste.
- Only read as much kernel as actually exists
-
The stock boot command read the entire 6.5 MB kernel partition into RAM, even though
the FIT image in it is much smaller. A small patch added
fdt header get <var> totalsize, so the boot command now reads just the
first 4 KB of the partition, extracts the real FIT image size from its header, then
reads exactly that many bytes.
- Tiny SPL FIT handling
-
CONFIG_SPL_FIT_IMAGE_TINY with the image size capped via
CONFIG_SPL_FIT_IMAGE_KB=512 keeps the SPL stage small and its load of U-Boot
proper short.
Linux kernel
- Faster flash for everyone
-
The SPI NAND was clocked up from 75 MHz to 125 MHz with quad-read enabled. This
is the single most broadly useful change, because every stage after the bootrom loads from
the same chip (SPL, U-Boot, kernel and root filesystem) all benefit at once.
- Aggressive config pruning
-
The kernel config was trimmed across several passes: Ethernet dropped (the unit only uses
USB), then WiFi, Bluetooth, sound, F2FS, YAFFS and NFS, then the second MMC controller,
debugfs, IKCONFIG, KSM and the unused squashfs decompressors. A smaller kernel means less
NAND to read, less to decompress, and fewer drivers probing at boot.
- Quiet, and no bootargs handoff
-
quiet on the kernel command line, with the whole command line baked into the
device tree chosen node.
- Read-only root,
panic=1
-
With a read-only root filesystem there is nothing to fsck and no journal to replay, and
panic=1 means a failed boot reboots and retries rather than sitting at a dead
console.
Root filesystem: squashfs on UBI, with every redundant check removed
- zstd squashfs on ubiblock
-
The root filesystem moved from YAFFS to a zstd-compressed squashfs on a
ubiblock device. Mounting a read-only squashfs is far faster than bringing up
a writable NAND filesystem, and zstd at level 22 with 128 KB blocks minimises how
much flash has to be read in the first place.
- UBI fastmap instead of a full attach scan
-
Normally UBI attaches by scanning the headers of every eraseblock on the device. Fastmap
(
ubi.fm_autoconvert=1) stores the attach information in a few blocks instead,
making attach time roughly constant.
There is a wrinkle: because the volume is read-only, UBI would never actually write the
fastmap out. So a late init script checks dmesg for "attached by fastmap"
and, if it's absent, creates and deletes a scratch volume. This forces the fastmap to
be written so the next boot gets the fast path. A dedicated 8 MiB "spacer"
volume in the UBI image gives the relocated volume table somewhere to live.
- Skip the whole-volume CRC check
-
ubiblock opens a static UBI volume at every boot, and by default that
triggers a full read-and-CRC pass over the entire ~32 MB volume. The
skip-check volume flag turns that off. This is safe here
because the data is already protected by three other layers:
squashfs metadata CRCs, per-eraseblock UBI header CRCs, and the NAND's own ECC. That read
was pure redundancy.
- Layout tuned for the boot path
-
The UBI volume layout was arranged so the root filesystem volume sits exactly where boot
expects it, with root on
ubiblock0_1 and no searching or indirection at mount
time.
Userspace: nothing on the critical path except the app
- Minimal init
-
BusyBox init with a minimal
inittab: mount, hostname, run rcS,
done. The unused buildroot init scripts (crond, sysctl, dropbear) are deleted at
image-build time, and other stale startup scripts were dropped rather than left to run
harmlessly-but-slowly.
- The application starts early, in the background
-
The camera application's init script runs early in the
rcS sequence: the
camera sensor's kernel module is insmodded and the application launched immediately, in
the background. Everything after it in the init sequence is housekeeping that the user
never waits on.
- Debug services are event-driven, not boot-time
-
All the developer conveniences still exist, they just don't run at boot. A monitor
script polls the USB detect GPIO, and only when a USB host is actually plugged in does it
construct the USB gadget (ACM serial and ECM ethernet via configfs), then start udhcpd and
dropbear (ssh). In the field none of that ever runs, so it costs zero
boot time and zero power.
- Buildroot trimmed to match
-
Unused packages removed, WiFi and BlueZ dumped, a pared-down custom BusyBox config, and a
uclibc toolchain to keep the binaries small.
Themes
Almost every win above is a variation on "stop reading flash you don't need": the
environment partition, the empty tail of the kernel partition, the UBI attach scan, the
redundant CRC pass, the oversized kernel. On SPI NAND at tens of MB/s, every megabyte not
read is tens of milliseconds back.
The second theme is "nothing runs at boot unless the product needs it". The debug and
development conveniences (ssh, serial gadget) weren't deleted. They
were instead moved off the boot path and gated on the physical USB detect line instead.
Finally, redundancy was only removed where another layer already provided the same
guarantee. Skipping the UBI volume CRC is safe because NAND ECC, UBI header CRCs and
squashfs CRCs all still stand behind the data. Don't enable
skip-check onto writable volumes without confirming the downsides.