What this project carries on top of the pinned kernel
Required by the system/kernel requirement: “every patch this project
carries on top SHALL be recorded with what it does and why it is needed”, and
“so a later kernel bump can tell whether it is still required”.
Pin: ruyisdk/linux-xuantie-kernel at
7d4e1f444f461dbe3833bd99a4640e7b6c2cd529 (6.6.36), k230_defconfig. This is
the revision and defconfig k230_canmv_v3_defconfig builds in
kendryte/k230_linux_sdk. Why not mainline: docs/evidence/why-xuantie-kernel.txt.
Everything below lives in nix/kernel.nix. All of it is applied through
applyPatches on src, not through the derivation’s postPatch —
buildLinux does not forward postPatch, so setting it there is a silent
no-op that returns the same store path with none of the changes applied. That
cost a debugging cycle and was only caught because the derivation hash did not
change.
1. Three device trees the vendor Makefile omits
Added to arch/riscv/boot/dts/canaan/Makefile | Why |
|---|---|
k230-canmv-v3.dtb | The reference board’s DTB. The vendor Makefile genuinely does not list it, so the tree cannot build the board its own defconfig is named after. |
k230-canmv-v3-lcd.dtb | The reference board’s LCD variant, kept as the comparison point for our divergence record. |
Drop when: upstream lists them itself.
k230-tdisplay.dtb used to be a third row here. It is not built by the
kernel any more — see below.
2. This board’s device tree — NO LONGER IN THE KERNEL
nix/dts/k230-tdisplay.dts and nix/dts/display-rm69a10-568x1232.dtsi were
copied into arch/riscv/boot/dts/canaan/ from applyPatches’ postPatch,
which put them in the kernel’s src. Correct, and very slow: a one-byte edit
to panel-init-sequence — the thing actively being iterated on to bring the
panel up — invalidated the whole kernel and cost a ~20 minute cross-compile.
A DTB needs the kernel’s headers, not a built kernel. So the pin moved to
nix/kernel-src.nix and the DTS is preprocessed and compiled on its own in
nix/device-tree.nix, exposed as nix build --impure .#deviceTree. That
takes about 1.5 seconds, and nix/sd-image.nix takes the DTB from there
instead of from ${kernel}/dtbs/canaan/.
The output is byte-identical to what the kernel build produced: same
preprocessor flags as scripts/Makefile.lib’s cmd_dtc, plus the -@ that
nixpkgs’ kernel builder adds to every make dtbs
(pkgs/os-specific/linux/kernel/build.nix). Verified by cmp against
${kernel}/dtbs/canaan/k230-tdisplay.dtb from the previous build.
Configuration rather than a code patch: panel-canaan-universal is already in
the pinned tree and is driven entirely from the device tree, so the RM69A10 is
described rather than coded. Divergences from the k230-canmv-v3-lcd
reference — which describes a different panel, an ST7701 at 480x800 — are
recorded in docs/evidence/dts-divergence.md.
Drop when: never, unless the board is upstreamed.
3. goodix_berlin, backported from v6.12
Source in nix/patches/goodix-berlin/, copied into
drivers/input/touchscreen/, with a Kconfig and Makefile stanza appended.
The pinned 6.6 tree has only the older GT9xx goodix.c, which does not speak
the Berlin protocol at all. This driver does not match the GT9895 upstream
either — see gt9895-touch.md; mainline has no GT9895 support at any
version. The device tree therefore declares "goodix,gt9895", "goodix,gt9916"
so the backport can attach via the fallback. Whether a GT9895 answers the
Berlin protocol is an open experiment, not an established fact.
One edit to the backported source:
sed -i 's|#include <linux/unaligned.h>|#include <asm/unaligned.h>|'
v6.12 moved asm/unaligned.h to linux/unaligned.h; 6.6 predates the move.
Drop when: the pinned kernel reaches a version that carries
goodix_berlin itself (v6.12+). The unaligned.h sed drops at the same time.
If the experiment fails, this whole item is replaced by a port of LilyGO’s
RT-Smart gt9895.c instead.
4. autoModules = false
Not a patch, but it changes what is built and belongs in this record.
nixpkgs defaults autoModules to true, which enables every module it can on
top of the defconfig. Against a vendor tree that is actively harmful: it turns
on drivers the vendor never compiles and whose bugs have therefore never been
hit. The first build died in drivers/rpmsg/th1520_rpmsg.c with redefinition of init_module — a driver for the TH1520, an unrelated SoC that
k230_defconfig does not enable. Greybus was compiling too.
RPMSG_TH1520 is additionally forced off with lib.mkForce no.
Drop when: never, while building against a vendor tree.
5. Config that NixOS needs and a vendor defconfig does not set
DEVTMPFS, DEVTMPFS_MOUNT, CGROUPS, INOTIFY_USER, SIGNALFD,
TIMERFD, EPOLL, NET, SYSFS, PROC_FS, FHANDLE,
CRYPTO_USER_API_HASH, CRYPTO_HMAC, CRYPTO_SHA256, TMPFS,
TMPFS_POSIX_ACL, SECCOMP, BLK_DEV_INITRD, RD_GZIP, RD_ZSTD.
systemd refuses to boot without most of these, and the failure mode is an initrd panic that says nothing about the real cause.
TOUCHSCREEN_GOODIX_BERLIN_CORE and ..._I2C turn on item 3.
Drop when: never, while running NixOS.
Not carried, and worth knowing
- No patch for the panel driver.
panel-canaan-universalis stock. - No patch for thermal.
canaan_thermalis stock and built in, and its limitation — a tripless zone, so temperature is readable but nothing acts on it — is a property of the vendor driver, not of anything we changed. Seedocs/thermal.md. - No SMP patch. Linux runs on one hart here because no K230 device tree
declares a
cpu@1. Seehardware-userspace.md.
Update after the first display bring-up (2026-09-20)
Task 4.1b resolved to “no patch”. The GT9895 answers the Berlin protocol.
The v6.12 goodix_berlin backport already in item 3 binds it through a
"goodix,gt9895", "goodix,gt9916" fallback in the device tree, and registers
a touchscreen input device. Neither branch of 4.1b was taken: no chip entry
(there is no chip table), and no port of LilyGO’s gt9895.c. Evidence:
docs/evidence/touch-probe.txt.
One patch added: the DRM fbdev colour depth.
sed -i 's|drm_fbdev_generic_setup(drm_dev, 32);|drm_fbdev_generic_setup(drm_dev, 16);|' \
drivers/gpu/drm/canaan/canaan_drv.c
canaan_drv.c:272 asks DRM’s generic fbdev emulation for 32 bpp, which means
XRGB8888. The driver’s own RGB planes advertise AR24 AR12 AR15 RG24 RG16 BG24 — XR24 is not among them — so format negotiation fails and no
/dev/fb0 is created:
[drm] bpp/depth value of 32/24 not supported
[drm] No compatible format found
[drm] *ERROR* fbdev: Failed to setup generic emulation (ret=-22)
Observed on hardware, docs/evidence/panel-probe.txt. 16 bpp maps to
RGB565, which those planes do advertise.
This affects the fbdev console only. DRM clients negotiate format for
themselves and already work — modetest -s 48:568x1232@AR24 sets a mode on
this panel with the unpatched driver.
Drop when: the vendor driver either advertises XR24 on its RGB planes
or stops hardcoding 32 in the fbdev setup call. Worth re-checking on any
kernel bump, since a one-line constant is easy for upstream to change.
Stage-1 splash preservation
The U-Boot handoff patch supplies an empty runtime /chosen property,
canaan,stage1-splash, only after its image load and RM69A10 initialization
succeed. The kernel does not treat that as a board property: an absent flag
keeps the ordinary reset, initialization, and fbdev setup path.
panel-canaan-universal.c reads the flag at probe into per-panel state. On a
flagged boot it requests dsi_reset with GPIOD_ASIS, leaving GPIO22 at the
stage-1 level instead of the vendor driver’s GPIOD_OUT_LOW. Its first
prepare() logs canaan_panel_prepare: left as stage 1 set it, clears that
per-panel state, and returns before both the project reset pulses and the DCS
init sequence. A later prepare therefore runs the complete existing reset and
init path. The change is one-shot and does not persist through any later boot.
canaan_drv.c reads the same runtime property and skips
drm_fbdev_generic_setup() only when it is present. That avoids the initial
fbdev hotplug/modeset from scanning out a zeroed buffer over the U-Boot frame;
/dev/fb0 is consequently absent on a flagged boot. A boot with no stage-1
success flag still creates the existing RGB565 fbdev console.
The source was checked before making this change. In the pinned tree,
canaan_vo_enable_crtc() initializes VO, programs timing/background, and
enables register load; it does not call the display reset. The
0x91101090 write of zero then 0xffffffff is in
canaan_vo_disable_crtc() only. Earlier text that placed that reset on the
enable path was wrong. This task intentionally does not change VO reset or
DSI/VO reprogramming; the first-modeset hardware task decides whether either
still causes a visible dark interval.
Drop when: stage 1 no longer owns a pre-kernel panel image, or a later
kernel handoff implementation adopts the running display state without the
fbdev modeset and has hardware evidence for both splash and no-splash boots.
If the first-modeset measurement shows a dark interval, restore the reset and
init work in prepare() as task 4.4 requires; do not hide that fallback.
First-VO/DSI handoff preservation — opt-in splash path
The first stage-1 preservation patch deliberately stopped at the panel reset,
DCS sequence, and fbdev. The first real DRM client still runs the Canaan
runtime-PM commit order: CRTC enable calls canaan_vo_enable_crtc(), then the
encoder calls canaan_dsi_encoder_enable(), then the active plane update
writes the Linux framebuffer and canaan_vo_flush_config() loads it. In the
pinned source this is drm_atomic_helper_commit_tail_rpm(), which enables
modesets before committing active planes.
The diagnostic implementation is carried in nix/kernel.nix. Each VO and DSI driver
reads the runtime /chosen/canaan,stage1-splash boolean into an independent
one-shot flag. It bypasses its first hardware reprogramming only when the
adjusted mode exactly matches the U-Boot RM69A10 mode: 49.5 MHz, 568x1232,
hsync 668..708 of 748, and vsync 1236..1252 of 1268. On that path the DSI
still calls drm_panel_prepare() and drm_panel_enable(): the panel’s
existing one-shot bookkeeping consumes its flag without reset or DCS writes.
The plane update and register load in that same atomic commit remain the
handoff point; this is not framebuffer adoption.
The VO bypass writes only the IRQ1 timing value which
canaan_vo_set_timing() would calculate, leaving visual registers alone;
normal DRM vblank enable remains responsible for its interrupt-enable bit.
A timing mismatch logs a warning and uses the ordinary VO/DSI setup. The
panel sees that the DSI did not authorize preservation, clears its flag, then
explicitly restores the reset GPIO to output-high before its normal reset
pulses; a failure to set that direction is returned from prepare(). A
disable before the first enable clears all three one-shot states, so a later
enable is also ordinary.
The exact kernel cross-build and physical comparison are now recorded in
docs/evidence/splash-preserve-trial/README.md: three automatic warm boots
ended with the expected portrait shell; successive color updates were visible;
removing the logo exercised full initialization and the framebuffer console;
and display off/on exercised later full prepare with visible shell recovery.
The first preservation-only image still had an approximately 1.1–1.2 second
compositor startup dark gap. The kernel patch alone does not solve that gap,
and the camera did not resolve the default keyboard well enough for complete
keyboard geometry acceptance. Cold/battery boot and remaining real-touch
acceptance are still open. The daily k230.panelConsole=true choice omits the
logo and does not select the preservation path.
The source does not independently read back or prove every inherited U-Boot register. The runtime flag and exact mode are the contract for this vendor path, with the normal initialization fallback retained. The separate opt-in Sway scene seed addresses the measured empty first compositor frame.
Drop when: the physical first-frame evidence shows that ordinary VO/DSI programming is the correct handoff, or a supported DRM handoff replaces this one-shot vendor-driver experiment. Do not retain it merely because the stage-1 flag is present.
Mainline kernel: stage-1 splash handoff (already ported; no new patch)
Laptop evidence only (source reading and a build; no board). Task 1.2 of
the-mainline-kernel-keeps-the-boot-splash. The vendor sed patches above do
not apply to the mainline tree; the mainline panel/DRM sources under
nix/patches/mainline/drm/ already carry the same behaviour, so no driver was
changed. Each row names the file, the flag, the log string, and when to drop it.
| Behaviour | File and lines | Log string | Drop when |
|---|---|---|---|
Flag read: /chosen/canaan,stage1-splash (set by nix/patches/uboot-k230/0005-k230-stage1-splash-readiness.patch) | panel-canaan-universal.c:458, canaan_dsi.c:913-914, canaan_vo.c:835-836, canaan_drv.c:277 | none | stage 1 stops owning a pre-kernel panel image |
Panel reset line requested GPIOD_ASIS on a flagged boot; normal boot keeps GPIOD_OUT_LOW and the reset pulses | panel-canaan-universal.c:460-466 | none | same |
First prepare() returns before reset pulses and DCS init when DSI preserved the mode; one-shot | panel-canaan-universal.c:139-143 | canaan_panel_prepare: left as stage 1 set it | same |
Mode mismatch or disable before first enable: flag cleared, full init, reset pin driven by gpiod_direction_output on the ordinary path | panel-canaan-universal.c:183-186, :251-254, :166-176 | stage 1 splash: mode mismatch; reinitializing panel, stage 1 splash: panel disabled before handoff; reinitializing later | same |
| DSI first enable skipped only when the adjusted mode equals the U-Boot mode | canaan_dsi.c:412-451, :492-496 | stage 1 splash: preserving DSI to first plane update, stage 1 splash: DSI mode differs; reinitializing, stage 1 splash: DSI disabled before handoff; reinitializing later | same |
| VO first enable writes only the vblank timing value when the mode matches | canaan_vo.c:654-683, :701-703 | stage 1 splash: preserving VO to first plane update, stage 1 splash: VO mode differs; reinitializing, stage 1 splash: VO disabled before handoff; reinitializing later | same |
| No fbdev/DRM client set up over the logo on a flagged boot | canaan_drv.c:277-281 (drm_client_setup_with_fourcc() is in the else) | stage 1 splash: leaving fbdev unset | a handoff that adopts the running display state without a client modeset, with hardware evidence for splash and no-splash boots |
Fbdev-client question (is a client created by another route?). In the pinned
source (/nix/store/4jrzy64mg619y75lgrlihz78darglas6-linux-mainline-k230-drm-src),
drivers/gpu/drm/clients/drm_client_setup.c is the only place the in-kernel
clients (fbdev, DRM log) are registered, and its doc comment states drivers
call it after drm_dev_register(); drm_drv.c and drm_probe_helper.c do
not call it. Within nix/patches/mainline/ the only drm_client_setup*
caller is canaan_drv.c:280. The configuration is CONFIG_DRM_FBDEV_EMULATION=y,
CONFIG_DRM_CLIENT_DEFAULT_FBDEV=y, CONFIG_FRAMEBUFFER_CONSOLE=y, CONFIG_VT=y
(the kernel .config is the .configfile of the kernel below), so the fbdev
client exists in the image but is only instantiated by that skipped call; there
is no DRM-log client (DRM_CLIENT_LOG absent). CONFIG_FB_EFI=y needs an EFI
framebuffer which this boot path does not provide. Source reading cannot
exclude a registration from a driver outside this tree, so whether /dev/fb0
is really absent and the panel really stays lit is hardware gate 2.3.
Build proof: flock /tmp/k230-nix-build.lock nix build .#nixosConfigurations.k230-mainline-drm-shell.config.boot.kernelPackages.kernel --max-jobs 1 --cores 4 produced
/nix/store/f3rnipvbc5vqam8kwdmn3wgcl5rbz9yx-linux-riscv64-unknown-linux-gnu-7.3.0-rc5,
the kernel .#sdImage and the splash variant both use (the panelConsole
option does not change the kernel derivation).
Drop when: the vendor and mainline panel paths are unified, or a kernel
bump removes the driver; re-check canaan_drv.c:277-281 and the
drm_client_setup callers on every kernel bump.