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.