# The kernel for the T-Display-K230. # # Mainline cannot boot this SoC. Linux 6.18 carries pinctrl-k230.c and # reset-k230.c but ships no K230 device tree -- arch/riscv/boot/dts/canaan/ # is K210-only -- and Kconfig.socs has no SOC_CANAAN_K230, only # SOC_CANAAN_K210, itself `depends on !MMU`. See # docs/evidence/why-xuantie-kernel.txt. # # So we take the tree Canaan's own Linux SDK pins, and build it from source # rather than vendoring a kernel binary. { lib, buildLinux, fetchFromGitHub, applyPatches, ... }@args: let # The pin itself lives in nix/kernel-src.nix, because nix/device-tree.nix # needs the same tree for its headers and the two must not drift. kernelSrc = import ./kernel-src.nix { inherit fetchFromGitHub; }; in buildLinux (args // { version = "6.6.36-xuantie"; modDirVersion = "6.6.36"; # arch/riscv/boot/dts/canaan/Makefile lists k230-canmv, k230d-canmv and # k230-evb but NOT the v3 board, even though k230-canmv-v3.dts and # k230-canmv-v3-lcd.dts are both in the tree. Canaan's buildroot sidesteps # that by naming DTBs explicitly in BR2_LINUX_KERNEL_INTREE_DTS_NAME, so # `make dtbs` alone never produces them. # # Patched into the SOURCE, not via postPatch: buildLinux does not forward # postPatch to the kernel derivation, so setting it there is a silent # no-op -- the build returns the same store path and the DTB is still # missing. Found by the derivation hash not changing. src = applyPatches { name = "linux-xuantie-k230-src"; src = kernelSrc; # A real patch file, not a sed, because this replaces a whole function # body. canaan_dsi_dcs_read() ships as "// TODO; return 1", so the panel # cannot be asked anything -- and six hypotheses about why the screen # stays dark have now died for want of exactly that. Reading DCS 0x04 # (RDDID) or 0x0A (RDDPM) separates "the panel never receives the init # sequence" from "it receives it, acknowledges it, and still does not # light". See docs/evidence/dsi-phy-hang.md. patches = [ # The vendor Kconfig V probe omits M despite the target ABI needing it; # use the correction physically tested in the isolated vector kernel. ../nix/patches/riscv-vector-toolchain-probe.patch ../nix/patches/canaan-dsi-implement-dcs-read.patch # Implementing the read is useless on its own -- nothing in the # panel driver ever issues one. This adds the caller: read RDDID # (0x04) and RDDPM (0x0A) right after the init sequence, and log # both. RDDPM bit 4 is sleep-out and bit 2 is display-on, which is # precisely what the 0x11 and 0x29 at the tail of the sequence are # supposed to have set. Diagnostic only; failures are logged, never # fatal to prepare(). ../nix/patches/canaan-panel-read-back-id-and-power-mode.patch # canaan-drm-defer-reg-load-to-vblank.patch is deliberately NOT applied: # it left the bottom-band flicker unchanged (sway max_render_time 8 # fixed it) and a kernel carrying it panicked at boot in # __insert_inode_hash right after canaan-drm bound. See # docs/evidence/card-shell/bottom-band-flicker/kernel-patch-boot-panic.md. # Expose the SAI's internal-codec/external-I2S-pad route switch as # an ALSA control instead of leaving it hardcoded to the internal # Inno codec. sound/soc/canaan/canaan_k230_inno.c already calls # audio_i2s_enable_audio_codec(true) unconditionally at probe; that # exported function (canaan_k230_audio.c:50-59) is the SoC's own # digital mux between the on-die codec and the raw I2S pads a # MAX98357A amp listens to -- there is no second sound card and no # DAPM path involved. This patch is a close port of LILYGO's own # T-Display-K230 kernel fork, # k230_bsp/overlay/buildroot-overlay/linux/0059-asoc-canaan-add-external-i2s-output-switch.patch # in Xinyuan-LilyGO/T-Display-K230 (commit 37b66b38, "ASoC: canaan: # add external I2S output switch"), which this file matches # byte-for-byte at the hunk context, so it applies unmodified to our # pinned tree. See docs/evidence/max98357a-speaker.md. ../nix/patches/canaan-audio-external-i2s-switch.patch # the-clock-survives-a-reboot, board finding: k230_rtc_read_time() # masks the day-of-month field with 0xf (4 bits) instead of 0x1f # (5 bits, needed for 1..31), silently dropping bit 4. Board # evidence: day 26 read back as 10 (0b11010 & 0b01111 == 0b01010). # See docs/evidence/rtc/. ../nix/patches/k230-rtc-mday-mask.patch # display/hdmi capability (openspec/changes/plugging-in-hdmi-moves-the-display): # canaan_dsi_bind()'s bridge branch attaches the LT9611 (or any DSI # bridge) to the encoder with DRM_BRIDGE_ATTACH_NO_CONNECTOR and never # creates a drm_connector -- so no DTB, ours or the vendor's, ever # produces a usable HDMI-A-1 under this kernel, independent of which # device tree loads it. This adds the one missing call, # drm_bridge_connector_init() + drm_connector_attach_encoder(), # mirroring the existing panel branch's connector setup immediately # above it. Inert on the panel boot path: it only executes when # dsi->bridge is non-NULL, which requires a bridge node under &dsi's # port@1 that no device tree we ship today provides (see task 2.3). # See docs/research/hdmi-hotplug.md ยง4 and design.md decision 2. ../nix/patches/canaan-dsi-bridge-connector.patch # Backport of mainline "drm/bridge: lt9611: Add support for DSI port B # input" (Hongyang Zhao, drm-misc-next 2026-02): accept a port@1-only # graph and select Port B + its byte clock (0x8303 bit 6, 0x8250 bits # 3:2). This board wires DSI to the LT9611's MLRXB pins only # (schematic sheet "HDMI+ETH"); without it the chip sees no video. ../nix/patches/lt9611-dsi-port-b.patch ]; postPatch = '' # Seed dependencies before oldconfig traverses NFT_COMPAT/NFT_CT. # Their prerequisites occur later in Kconfig; answering new prompts # against the vendor's modular prerequisites otherwise loops forever. cat ${./kernel-firewall.config} >> arch/riscv/configs/k230_defconfig echo 'dtb-$(CONFIG_ARCH_CANAAN) += k230-canmv-v3.dtb' >> arch/riscv/boot/dts/canaan/Makefile echo 'dtb-$(CONFIG_ARCH_CANAAN) += k230-canmv-v3-lcd.dtb' >> arch/riscv/boot/dts/canaan/Makefile # NOT our board's device tree. nix/dts/k230-tdisplay.dts and # nix/dts/display-rm69a10-568x1232.dtsi used to be copied in here and # added to that Makefile, which put them in the kernel's `src` -- so a # one-byte edit to panel-init-sequence invalidated the whole kernel # and cost a ~20 minute cross-compile. They are compiled in their own # derivation now (nix/device-tree.nix) and the image takes the DTB # from there; the output is byte-identical. Nothing in this kernel # depends on them any more, which is the entire point -- do not add # them back. # Vendor PMU input path, adapted from Xinyuan-LILYGO/T-Display-K230 # patch 0064 at 9991ebe362bdd0b21f880545ad0c325ff21eedce. # Only input edges are ported: the vendor 5-second automatic shutdown # and system-off callback would bypass the shell confirmation sheet. cp ${../nix/patches/k230-pmu-pwrkey.c} drivers/input/misc/k230-pmu-pwrkey.c test "$(tail -n 1 drivers/input/misc/Kconfig)" = endif sed -i '$d' drivers/input/misc/Kconfig cat >> drivers/input/misc/Kconfig <<'EOK' config INPUT_K230_PMU_PWRKEY tristate "Kendryte K230 PMU power key" depends on OF depends on ARCH_CANAAN || COMPILE_TEST help Report PMU INT0 power key edges through the Linux input subsystem. endif EOK cat >> drivers/input/misc/Makefile <<'EOM' obj-$(CONFIG_INPUT_K230_PMU_PWRKEY) += k230-pmu-pwrkey.o EOM # goodix_berlin, backported from v6.12. The pinned 6.6 tree has only # the older GT9xx goodix.c. See docs/evidence/gt9895-touch.md -- note # this driver does NOT match the GT9895 upstream, so whether it can # drive this panel's controller is an open experiment. cp ${../nix/patches/goodix-berlin}/goodix_berlin*.{c,h} \ drivers/input/touchscreen/ # 6.12 moved asm/unaligned.h to linux/unaligned.h; 6.6 predates that. sed -i 's|#include |#include |' \ drivers/input/touchscreen/goodix_berlin_core.c \ drivers/input/touchscreen/goodix_berlin_i2c.c \ drivers/input/touchscreen/goodix_berlin_spi.c cat >> drivers/input/touchscreen/Kconfig <<'EOK' config TOUCHSCREEN_GOODIX_BERLIN_CORE tristate select REGMAP config TOUCHSCREEN_GOODIX_BERLIN_I2C tristate "Goodix Berlin I2C touchscreen" depends on I2C select REGMAP_I2C select TOUCHSCREEN_GOODIX_BERLIN_CORE help Backported from v6.12 for the T-Display-K230's GT9895. EOK cat >> drivers/input/touchscreen/Makefile <<'EOM' obj-$(CONFIG_TOUCHSCREEN_GOODIX_BERLIN_CORE) += goodix_berlin_core.o obj-$(CONFIG_TOUCHSCREEN_GOODIX_BERLIN_I2C) += goodix_berlin_i2c.o EOM # fbdev emulation asks canaan-drm for 32 bpp, which means XRGB8888. # The driver's RGB planes advertise AR24 AR12 AR15 RG24 RG16 BG24 and # NOT XR24, so the format negotiation fails and there is no /dev/fb0: # # [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 costs colour depth on # the fbdev console only; DRM clients still negotiate AR24 for # themselves and are unaffected. sed -i 's|drm_fbdev_generic_setup(drm_dev, 32);|drm_fbdev_generic_setup(drm_dev, 16);|' \ drivers/gpu/drm/canaan/canaan_drv.c grep -q 'drm_fbdev_generic_setup(drm_dev, 16);' drivers/gpu/drm/canaan/canaan_drv.c # Instrument the panel bring-up path. # # The panel probes and a modeset succeeds, but the glass stays dark and # the DCS init sequence appears never to be written -- inferred from the # ABSENCE of log lines, which is weak evidence. This one line turns # that into a positive statement either way on the next boot, so a # wrong hypothesis costs a boot rather than a card swap and a rebuild. # See docs/evidence/panel-dark.md. Remove once the panel is up. sed -i 's|\tif (p->init_set_v1_flag) {|\tdev_info(panel->dev, "canaan_panel_prepare: entered, init_set_v1_flag=%u\\n", p->init_set_v1_flag);\n\tif (p->init_set_v1_flag) {|' \ drivers/gpu/drm/panel/panel-canaan-universal.c grep -q 'canaan_panel_prepare: entered' drivers/gpu/drm/panel/panel-canaan-universal.c # Reset the panel immediately before the init sequence. # # canaan_panel_prepare() has the vendor's own reset pulses commented # out, so the only reset happens in probe, seconds earlier. RT-Smart # drives three resets on GPIO22 immediately before it writes the init # sequence -- docs/evidence/rm69a10-init-sequence.md, taken from the # shipped firmware's boot log. With the reset that far away the panel # does not answer, and once fbdev started actually performing a modeset # the DCS write blocked forever: "soft lockup - CPU#0 stuck for 130s!". # See docs/evidence/panel-dark.md. sed -i 's|\tif (p->init_set_v1_flag) {|\tif (p->reset) {\n\t\tret = gpiod_direction_output(p->reset, 1);\n\t\tif (ret) {\n\t\t\tdev_err(panel->dev, "failed to set panel reset output: %d\\n", ret);\n\t\t\treturn ret;\n\t\t}\n\t\tgpiod_set_value_cansleep(p->reset, 1);\n\t\tpanel_simple_sleep(20);\n\t\tgpiod_set_value_cansleep(p->reset, 0);\n\t\tpanel_simple_sleep(20);\n\t\tgpiod_set_value_cansleep(p->reset, 1);\n\t\tpanel_simple_sleep(120);\n\t}\n\n\tif (p->init_set_v1_flag) {|' \ drivers/gpu/drm/panel/panel-canaan-universal.c grep -q 'panel_simple_sleep(120);' drivers/gpu/drm/panel/panel-canaan-universal.c # Preserve a panel that this boot's stage 1 successfully initialized. # # U-Boot writes the empty boolean canaan,stage1-splash in /chosen only # after it has loaded logo.xrgb and completed the RM69A10 init path. # This is deliberately read at runtime: absent or failed stage 1 means # the ordinary probe/reset/init/fbdev path stays intact. Do not turn it # into a static board-DT property. # # The reset GPIO needs GPIOD_ASIS in that successful-stage-1 case. The # vendor's GPIOD_OUT_LOW request immediately drives GPIO22 and would # extinguish the image before prepare() can decide to preserve it. sed -i 's|#include |#include \n#include |' \ drivers/gpu/drm/panel/panel-canaan-universal.c grep -q '#include ' drivers/gpu/drm/panel/panel-canaan-universal.c sed -i 's|\tu32 init_set_v1_flag;|\tu32 init_set_v1_flag;\n\tbool stage1_splash;|' \ drivers/gpu/drm/panel/panel-canaan-universal.c grep -q 'bool stage1_splash;' drivers/gpu/drm/panel/panel-canaan-universal.c sed -i 's|\tctx->reset = devm_gpiod_get(&dsi->dev, "dsi_reset", GPIOD_OUT_LOW);|\tctx->stage1_splash = of_property_read_bool(of_chosen, "canaan,stage1-splash");\n\n\tctx->reset = devm_gpiod_get(\&dsi->dev, "dsi_reset",\n\t\t\t\t ctx->stage1_splash ? GPIOD_ASIS : GPIOD_OUT_LOW);|' \ drivers/gpu/drm/panel/panel-canaan-universal.c grep -q 'ctx->stage1_splash = of_property_read_bool(of_chosen, "canaan,stage1-splash");' \ drivers/gpu/drm/panel/panel-canaan-universal.c sed -i '/ctx->reset = devm_gpiod_get/,/ctx->power_on =/ s|^\t} else {$|\t} else if (!ctx->stage1_splash) {|' \ drivers/gpu/drm/panel/panel-canaan-universal.c grep -q 'else if (!ctx->stage1_splash)' drivers/gpu/drm/panel/panel-canaan-universal.c sed -i '/static int canaan_panel_prepare/,/\/\/ set power on/ s|\tstruct canaan_panel \*p = panel_to_canaan_panel(panel);|\tstruct canaan_panel *p = panel_to_canaan_panel(panel);\n\tint ret;\n\n\tif (p->stage1_splash) {\n\t\tdev_info(panel->dev, "canaan_panel_prepare: left as stage 1 set it\\n");\n\t\tp->stage1_splash = false;\n\t\treturn 0;\n\t}|' \ drivers/gpu/drm/panel/panel-canaan-universal.c grep -q 'canaan_panel_prepare: left as stage 1 set it' \ drivers/gpu/drm/panel/panel-canaan-universal.c # fbdev's initial hotplug/modeset allocates a zeroed buffer and would # overwrite the preserved U-Boot scanout. This is also keyed only to # this boot's /chosen flag; a boot without it still creates fb0. sed -i 's|#include |#include \n#include |' \ drivers/gpu/drm/canaan/canaan_drv.c grep -q '#include ' drivers/gpu/drm/canaan/canaan_drv.c sed -i 's|\tdrm_fbdev_generic_setup(drm_dev, 16);|\tif (of_property_read_bool(of_chosen, "canaan,stage1-splash"))\n\t\tDRM_DEV_INFO(dev, "stage 1 splash: leaving fbdev unset\\n");\n\telse\n\t\tdrm_fbdev_generic_setup(drm_dev, 16);|' \ drivers/gpu/drm/canaan/canaan_drv.c grep -q 'stage 1 splash: leaving fbdev unset' drivers/gpu/drm/canaan/canaan_drv.c # Preserve U-Boot's already-running VO/DSI through exactly the first # matching DRM modeset. The stage-1 flag is only a claim that U-Boot # initialized this boot; both drivers independently require the exact # RM69A10 timing before trusting its live hardware state. A mismatch, # a disable before the first enable, or every later enable follows the # ordinary vendor initialization path. # # This does not adopt a framebuffer: the same atomic commit still runs # the Linux OSD plane update and register load. It only avoids the # preceding VO/DSI reprogramming that disturbed the physical panel in # the retained-logo trial. Keep the narrowly-scoped fallback visible. sed -i 's|#include |#include \n#include |' \ drivers/gpu/drm/canaan/canaan_vo.c grep -q '#include ' drivers/gpu/drm/canaan/canaan_vo.c sed -i 's|#include |#include \n#include |' \ drivers/gpu/drm/canaan/canaan_dsi.c grep -q '#include ' drivers/gpu/drm/canaan/canaan_dsi.c sed -i 's|#include