LILYGO T-Display-K230 · hardware note · observed
The device tree carried a correct kernel command line. U‑Boot replaced it with a vendor string that had no init= in it, NixOS stage 1 gave up, and the board stopped in emergency mode with the root account locked. One if in k230_img.c explains all of it.
the symptom
This is everything the console said. It looks like a catastrophe and it is very nearly a success — which is the useful thing about reading it carefully rather than reaching for the card reader.
[ OK ] Finished Create Volatile Files and Directories in the Real Root. You are in emergency mode. After logging in, type "journalctl -xb" to view system logs, "systemctl reboot" to reboot, or "exit" to continue bootup. Cannot open access to console, the root account is locked. See sulogin(8) man page for more details.
“in the Real Root” is the /sysroot variant of
the tmpfiles unit. It cannot possibly run before
sysroot.mount has succeeded. So the card, the partition
table, the ext4 filesystem and the MMC driver are all fine.
Something ordered immediately after that tmpfiles unit failed, and
failed fatally to initrd.target. That is a short list.
Emergency mode normally drops you into a shell. NixOS leaves root
locked, so sulogin has nothing to offer and the boot simply
stops. That is a NixOS default meeting a bad day, not a second fault.
the unit
From nixos/modules/system/boot/systemd/initrd.nix. It reads
/proc/cmdline, looks for init=, and if it does not
find one it refuses to continue — because without it, stage 1 does not
know which of the closures on the disk it is supposed to be booting.
# initrd-find-nixos-closure.service
# RequiresMountsFor = "/sysroot/nix/store"
# requiredBy = [ "initrd.target" ]
closure=
for o in $(< /proc/cmdline); do
case $o in
init=*) … closure="${initParam[1]}" ;;
esac
done
if [ -z "${closure:-}" ]; then
echo 'No init= parameter on the kernel command line' >&2
exit 1
fi RequiresMountsFor = "/sysroot/nix/store" is what orders it
immediately after the sysroot tmpfiles unit, and
requiredBy = [ "initrd.target" ] is what turns its
exit 1 into emergency mode. The entire failure therefore reduces
to one sentence: there was no init= on the kernel command
line.
the cause
The SD image derivation writes the command line into the device tree with
fdtput -t s … /chosen bootargs, and that step is genuinely
required — an earlier boot with no /chosen/bootargs at all
was silent on both UARTs. It is also, on its own, useless. Vendored U-Boot
rewrites the property on its way past.
// u-boot/board/canaan/common/k230_img.c:110
char *board_fdt_chosen_bootargs(void){
char *bootargs = env_get("bootargs");
if(NULL == bootargs) {
…
bootargs = "root=/dev/mmcblk1p2 loglevel=8 rw rootdelay=4 \
rootfstype=ext4 console=ttyS0,115200 earlycon=sbi";
}
printf("g_bootmod = %d, bootargs=%s\n", g_bootmod, bootargs);
return bootargs;
}
The vendor environment never defines bootargs, so every boot took
the second branch. The g_bootmod = 3 line printed on every one of
those boots is line 126 of this same function — it had been sitting in
the log the whole time, naming the function that was doing it.
fdtput wrote into /chosen/bootargs is discarded before the kernel ever sees the tree. The device tree is not where the kernel command line lives on this board.the fix
env_get("bootargs") is checked first, so anything that defines it
wins. The interesting part is where the value is allowed to live: the
command line names the NixOS closure and therefore changes on every rebuild,
while the stage 1 environment image must not, because it is vendored firmware
data with a recorded hash.
blinux = k230_set_dtb &&
ext4load mmc ${mmc_boot_dev_num}:1 0x7000000 /bootargs.txt &&
env import -t 0x7000000 ${filesize} &&
ext4load mmc ${mmc_boot_dev_num}:1 0x8000000 /fw_jump_add_uboot_head.bin &&
ext4load mmc ${mmc_boot_dev_num}:1 0x200000 /${k} &&
ext4load mmc ${mmc_boot_dev_num}:1 0x8400000 /${dtb} &&
ext4load mmc ${mmc_boot_dev_num}:1 0x9000000 /initrd.uimg &&
bootm 0x8000000 0x9000000 0x8400000
mmc_boot_dev_num = 1 k = Image dtb = force.dtb nix/sd-image.nix writes bootargs.txt from
config.boot.kernelParams plus init= —
the same string it puts in the DTB, so the two cannot drift apart.
env.env stays closure-independent.
env import is available because the build enables it CONFIG_CMD_IMPORTENV=y in this U‑Boot build.
ext4load sets the filesize variable as a side
effect (fs/fs.c:743), which the very next command consumes
— the two lines only work as a pair, and in that order.
Above the ~60 MB kernel that lands at 0x200000 and
below fw_jump at 0x8000000. The highlighted
addresses in the listing are the same problem solved once before: the
vendor layout assumes a kernel under 32 MiB, ours is not, and the
device tree and OpenSBI payload had to move above it.
corrections
Written from the QEMU side before the board had ever run our image, repeated in this repository, and wrong. Each one cost a boot cycle, which is why they are named here rather than quietly edited out.
blinux, an environment variable holding
a chain of ext4load commands that fetch each file by literal
filename, ending in bootm. There is no generation menu and no
APPEND line to put init= on, which is why
nix/hardware.nix sets
boot.loader.generic-extlinux-compatible.enable = false.
opensbi-1.4-overlay — OpenSBI 1.4 plus Canaan’s
T‑Head overlay, FW_TEXT_START=0 — and the version
matters because the overlay carries
THEAD_QUIRK_DISABLE_MAEE.
-append is
simply honoured. On the board the command line is a property of the
environment image and the boot partition. Writing it into the device tree
is still necessary, and is not sufficient.
Analysis: docs/evidence/stage1-emergency-mode.md. Console evidence: docs/evidence/hardware-boot.txt for the failure, docs/evidence/hardware-userspace.md for /proc/cmdline after the fix. The vendor environment as shipped is in docs/evidence/uboot-env.txt; the differences from QEMU are tabulated in docs/evidence/boot-path-differences.md.
Vendor source: u-boot/board/canaan/common/k230_img.c line 110 (the function) and line 126 (the g_bootmod printf). NixOS source: nixos/modules/system/boot/systemd/initrd.nix. Our side: nix/sd-image.nix, nix/hardware.nix, tools/gen-stage1.sh and firmware/stage1/PROVENANCE.txt, which records every byte of stage 1 that this project generates rather than vendors.
The U-Boot and SPL binaries are untouched. Only the environment image —
plain data produced with mkenvimage from a text file — is
ours, which is what image/boot-chain requires.