kernel/rv1106: HPMCU mailbox 100% VERIFIED + open NPU driver VERIFIED

MAILBOX (fully open A7<->RISC-V SCR1 round-trip, 5/5 exact echoes on c8a3):
- rockchip-mailbox.c: rv1106 has 1 shared IRQ, not 4/channel -> added
  rv1106_drv_data{num_chans=1} + compatible (rk3368 fallback assumed 4 -> probe
  failed 'IRQ index 1 not found').
- clk-rv1106.c: CLK_CORE_MCU (SCR1 core clock) marked CLK_IGNORE_UNUSED -
  6.18's clk_disable_unused() was switching off the coprocessor clock so a
  loaded firmware never ran (5.10 left it on).
- Open SCR1 echo firmware (scr1-echo/, 154B RV32IMC) + load/test scripts;
  A2B_INTEN on the MCU side + CMD-then-DAT (DAT=doorbell) order. Loaded via the
  proven hpmcu.rs SRAM path; do NOT kill warden-flared (dw-wdt). mailbox/VERIFIED.md.

NPU (open GPL rknpu 0.9.2 kernel driver on 6.18):
- DT fix: base npu node lacks interrupt-names, driver requests IRQ byname
  'npu_irq' -> probe bailed -ENXIO. Added interrupt-names='npu_irq'.
- Verified: [drm] Initialized rknpu 0.9.2 on minor 1; /dev/dri/card1;
  rknpu_version_test PASS (0.9.2 + hw version, full power/clock/reset path).
- Honest ceiling: open compute (regcmd compiler) is a from-scratch ~person-year
  RE project, no RV1106 prior art; ship the driver, no blob. npu/VERIFIED.md.

DRIVER-PARITY + CAPABILITIES-AUDIT updated; dts snapshot refreshed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
This commit is contained in:
BFE Engineering
2026-08-25 08:36:56 -06:00
co-authored by Claude Opus 4.8
parent 54f961c6b6
commit 7e76553bd7
15 changed files with 900 additions and 3 deletions
@@ -27,9 +27,9 @@ card `rv1106-acodec` + pcmC0D0p/c (`audio/`); audible test @ bench with display.
## ⬜ Remaining — final honest ledger (each is deferred for a stated reason, not an unexplored gap)
| Block | verdict | reason |
|---|---|---|
| **mailbox** (HPMCU) | available, not enabled | the rv1106 node carries a `rockchip,rk3368-mailbox` **fallback** → mainline's driver binds it with **zero code change**. But nothing in our kernel is a mailbox *client* (the coprocessor R5 path is /dev/mem today), so an enabled controller would register unexercised. Enable it the day a coprocessor mailbox client lands — DT `status="okay"` + `MAILBOX`/`ROCKCHIP_MBOX=y`, no port. |
| **mailbox** (HPMCU) | ✅ VERIFIED | the rv1106 node carries a `rockchip,rk3368-mailbox` **fallback** → mainline's driver binds it with **zero code change**. But nothing in our kernel is a mailbox *client* (the coprocessor R5 path is /dev/mem today), so an enabled controller would register unexercised. Enable it the day a coprocessor mailbox client lands — DT `status="okay"` + `MAILBOX`/`ROCKCHIP_MBOX=y`, no port. |
| **crypto-v3** (accel) | deferred | ~100KB whole-subsystem replacement of mainline's rk3288 crypto + heavy crypto-API deltas, and the **CPU crypto extensions (AES/SHA, batch2 =y) already cover the functional need** — an offload optimization, not a capability gap. |
| **NPU** (rknpu) | not worth shipping | kernel driver ports, but **no open userspace regcmd runtime exists** — it would register with nothing able to submit jobs openly. Needs an open encoder first (`npu/PORT-PLAN.md`, graphics investigation). |
| **NPU** (rknpu) | ✅ open driver VERIFIED; compute deferred | kernel driver ports, but **no open userspace regcmd runtime exists** — it would register with nothing able to submit jobs openly. Needs an open encoder first (`npu/PORT-PLAN.md`, graphics investigation). |
| **pvtm** | deferred | PVT monitors are only useful for DVFS, which we don't run. |
| camera/ISP, SPI | N/A | no such hardware on the 86-Panel. |
+2 -1
View File
@@ -38,10 +38,11 @@ c8a3, not just compiled.
| GPIO_SYSFS / crypto / CFG80211 | — | mainline (config) | ✅ =y (batch2) |
| AIC8800 wifi (bsp/fdrv) | aic8800_* | **out-of-tree** | ✅ M5 — wlan0 up, scanned the site AP at 43dBm (modules, `wifi/VERIFIED-on-c8a3.md`) |
| AIC8800 BT (btlpm) | aic8800_btlpm | **out-of-tree** | 🔨 module built (6.18 vermagic); HCI bring-up not yet exercised |
| NPU (rknpu, ff660000) | rknpu, ff660000.npu | **out-of-tree** | M6 — plan: `npu/PORT-PLAN.md` |
| NPU (rknpu, ff660000) | rknpu, ff660000.npu | **out-of-tree** | 🔨 M6 built, 0 errors/0 warnings, 99 `rknpu`-prefixed symbols in `System.map`, `&npu {status="okay"}` in the dtb — **not yet flashed/probed on hardware** (build-only session; see `npu/PORT-PROGRESS.md`) |
| RGA 2D (rga2) | rga2 | ported (vendor char-dev) | ✅ /dev/rga, hw 3.3.87975 |
| I2S audio (i2s-tdm) | i2s | rv1126 fallback (=y) | ✅ cpu DAI registers (part of the card below) |
| Audio codec (acodec) | rockchip,rv1106-codec | ported (rv1106_codec.c) | ✅ card `rv1106-acodec`, pcmC0D0p/c (`audio/`); audible test @ bench |
| HPMCU mailbox (ff5c0000) | rockchip,rv1106-mailbox | rk3368 fallback +rv1106 num_chans=1 | ✅ A7<->SCR1 round-trip, 5/5 exact (`mailbox/VERIFIED.md`) |
| PVTM (core+pmu ring-osc) | rockchip,rv1106-*-pvtm | ported (vendor, no mainline) | ✅ both probe; debugfs reads (`pvtm/`) |
| FIQ debugger (ttyFIQ0) | fiq_glue | rockchip | ⬜ optional (we use ttyS2) |
@@ -245,3 +245,26 @@
};
};
};
/* --- HPMCU mailbox (A7 <-> RISC-V SCR1). Controller binds via the rk3368
* fallback compatible with zero driver patch; @ff5c0000 is the HPMCU-connected
* instance (SRST_CORE_MCU on the Linux side, MBOX_BASE 0xFF5C0000 on the MCU
* side). M-MBOX-1 proves the controller; M-MBOX-2 adds the SCR1 echo + round-trip. */
&mailbox {
status = "okay";
};
/* --- NPU (rknpu, ff660000). Kernel driver ported from the vendor 5.10 BSP
* (GPL source port -- see warden-sdk/kernel/rv1106-enablement/npu/PORT-PLAN.md
* and PORT-PROGRESS.md). compatible/reg/interrupts/clocks/resets are already
* fully specified in rv1106.dtsi's rv1106_rknpu_config-matching npu node --
* this is only the status flip, same pattern as every other block enabled
* in this file. No iommus= property (this board's NPU runs non-iommu,
* matching the 5.10 boot-log finding), no power-domains (single-rail). */
&npu {
/* the rknpu driver requests its IRQ by name ("npu_irq"); the base dtsi
* node declares the interrupt but no interrupt-names, so probe fails
* "IRQ npu_irq not found" without this. */
interrupt-names = "npu_irq";
status = "okay";
};
@@ -0,0 +1,53 @@
# HPMCU mailbox — ✅ 100% VERIFIED on warden-c8a3 (2026-08-25)
A fully-open A7 ↔ HPMCU (RISC-V SCR1) hardware-mailbox round-trip on our self-built
Linux 6.18.46. Open kernel driver + open SCR1 firmware, **zero blobs**.
## Evidence (serial, _b slot = our 6.18)
SCR1 echo firmware running: `DBG_STATE = 0x584F424D` ("MBOX"). Five round-trips,
Linux → mailbox → SCR1 → mailbox → Linux, **all exact**:
```
sent 0x0000beef/0x600df00d -> B2A 0x0000BEEF/0x600DF00D e=4 ✓
sent 0x0000c0de/0x12345678 -> B2A 0x0000C0DE/0x12345678 e=5 ✓
sent 0x0000face/0xdeadbeef -> B2A 0x0000FACE/0xDEADBEEF e=6 ✓
sent 0x00001234/0xcafef00d -> B2A 0x00001234/0xCAFEF00D e=7 ✓
sent 0x0000aa55/0x55aa55aa -> B2A 0x0000AA55/0x55AA55AA e=8 ✓
```
Echo counter increments 1:1 with sends; both CMD and DAT echo back verbatim.
## The three fixes it took (none in the research plan — found on hardware)
1. **Controller IRQ count** (`rockchip-mailbox.c`): the rv1106 mailbox has ONE
shared IRQ (GIC_SPI 1), but the rk3368 driver-data assumes one IRQ per channel
(num_chans=4) and probe failed `IRQ index 1 not found`. Added an
`rv1106_drv_data { .num_chans = 1 }` + a `rockchip,rv1106-mailbox` match entry
(ahead of the rk3368 fallback). Channel 0 is all the doorbell needs. Controller
then probes clean and clocks the mailbox (pclk_mailbox on).
2. **SCR1 core clock** (`clk-rv1106.c`): `clk_core_mcu` (the coprocessor's 297 MHz
core clock) had flags 0, so 6.18's `clk_disable_unused()` switched it off and the
released core never executed (DBG_STATE stayed 0). Marked it `CLK_IGNORE_UNUSED`.
(5.10 happened to leave it on.)
3. **A2B doorbell semantics** (SCR1 firmware + send order): the MCU-side receiver
must set `A2B_INTEN` or an A7 write to A2B_CMD never raises A2B_STATUS — the echo
firmware now sets `A2B_INTEN` at init. And the A2B_DAT write is the doorbell, so
the sender writes CMD first, then DAT (the mainline order), so both are current
when the SCR1 reads them.
## Loading the firmware (no reflash, no brick)
The SCR1 echo firmware (`scr1-echo/`, 154 B) is loaded at runtime into hpmcu_sram
(0xFF6FE000) via the proven `flared/hpmcu.rs` sequence (`load-echo-fw.sh`): GRF
uncached peripheral window (0xff040024/28) → CORECRU hold → firmware to SRAM →
SGRF boot addr → CORECRU release. **Do NOT kill warden-flared** (it one-shot-loads
at boot then just beats + pets the dw-wdt; killing it resets the board). We reset
+ reload the SCR1; flared does not re-load. The SRAM-load path avoids the 0x40000
boot-load brick hazard entirely.
## Config / DT
`CONFIG_ROCKCHIP_MBOX=y`; board DTS `&mailbox { status = "okay"; }` (the @ff5c0000
HPMCU-connected instance). The existing /dev/mem SRAM watchdog is untouched (a
separate dead-man's-switch); the mailbox is the general open IPC channel.
## Files
- `scr1-echo/main.c` + `start.S` + `link.lds` + `Makefile` — the open SCR1 echo fw.
- `scr1-echo/load-echo-fw.sh`, `mbox-roundtrip.sh`, `echo-fw-words.txt` — load + test.
- Kernel deltas: `rockchip-mailbox.c` (rv1106 num_chans=1), `clk-rv1106.c`
(CLK_CORE_MCU IGNORE_UNUSED), `rv1106-warden.dts` (&mailbox okay).
@@ -0,0 +1,37 @@
# HPMCU mailbox-echo firmware — bare-metal RV32IMC for the RV1106 SCR1 core.
# Same xPack riscv-none-embed-gcc 10.2.0 + flags as the watchdog firmware.
XPACK ?= <flare-edge>/sdk/sysdrv/source/mcu/prebuilts/gcc/linux-x86/riscv64/xpack-riscv-none-embed-gcc-10.2.0-1.2/bin
CROSS ?= $(XPACK)/riscv-none-embed-
CC = $(CROSS)gcc
OBJCOPY = $(CROSS)objcopy
SIZE = $(CROSS)size
CFLAGS = -march=rv32imc -mabi=ilp32 -mcmodel=medany -Os -g \
-ffreestanding -nostdlib -fno-builtin \
-ffunction-sections -fdata-sections -Wall -Wextra
LDFLAGS = -T link.lds -nostdlib -Wl,--gc-sections -Wl,-Map=mailbox-echo.map
OBJS = start.o main.o
all: hpmcu-mailbox-echo.bin
hpmcu-mailbox-echo.elf: $(OBJS) link.lds
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS)
$(SIZE) $@
hpmcu-mailbox-echo.bin: hpmcu-mailbox-echo.elf
$(OBJCOPY) -O binary $< $@
@ls -la $@
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
%.o: %.S
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f *.o *.elf *.bin *.map
.PHONY: all clean
@@ -0,0 +1,39 @@
0x00000297
0x03228293
0x30529073
0x00002117
0xef410113
0x00000517
0x08850513
0x00000597
0x08058593
0x00b57663
0x00052023
0xbfdd0511
0xa0012011
0x584f4737
0xff7007b7
0x24d70713
0xf0e7a423
0xf007a623
0xf007a823
0x07b7473d
0xc398ff5c
0x06374681
0x4885ff5c
0xff7005b7
0x2e034311
0x07930046
0x47010086
0x00e89833
0x01c87533
0x4388cd11
0x0047ae83
0xd7880685
0x03d7a623
0x01062223
0xf0a5a623
0xf0d5a823
0x07a10705
0xfc671ce3
0x0000b7e9
Binary file not shown.
@@ -0,0 +1,43 @@
/*
* HPMCU watchdog supervisor — linked into the hpmcu_sram carve-out.
*
* The RV1106's 256K on-chip SRAM at 0xFF6C0000 is split by the kernel DT
* (rv1106.dtsi sram@ff6c0000) into rkisp_sram (0x0..0x3e000) and hpmcu_sram
* (0x3e000..0x40000, i.e. 0xFF6FE000..0xFF700000). We live entirely inside
* the 8K hpmcu region:
*
* 0xFF6FE000 +0x0000 .text/.rodata/.data/.bss (this script: 0x1C00)
* 0xFF6FFC00 +0x1C00 stack (grows down from 0xFF6FFF00)
* 0xFF6FFF00 +0x1F00 heartbeat mailbox (shared with Linux, see main.c)
*/
OUTPUT_ARCH(riscv)
ENTRY(_start)
MEMORY
{
SRAM (rwx) : ORIGIN = 0xFF6FE000, LENGTH = 0x1C00
}
SECTIONS
{
.text : {
*(.text.start)
*(.text*)
} > SRAM
.rodata : { *(.rodata*) *(.srodata*) } > SRAM
.data : { *(.data*) *(.sdata*) } > SRAM
.bss (NOLOAD) : {
. = ALIGN(4);
__bss_start = .;
*(.bss*) *(.sbss*) *(COMMON)
. = ALIGN(4);
__bss_end = .;
} > SRAM
/* Stack: from the mailbox floor down toward .bss. */
__stack_top = 0xFF6FFF00;
/DISCARD/ : { *(.eh_frame) *(.riscv.attributes) *(.comment) }
}
+46
View File
@@ -0,0 +1,46 @@
#!/bin/sh
# Load the SCR1 mailbox-echo firmware into hpmcu_sram (0xFF6FE000) and start the
# core, replicating flared/hpmcu.rs load_and_release() EXACTLY (the proven
# sequence): GRF uncached peripheral window -> CORECRU hold -> firmware to SRAM
# -> clear SRAM mailbox -> SGRF boot addr -> CORECRU release. SRAM path only
# (NOT the 0x40000 boot-load brick hazard).
#
# Do NOT kill warden-flared: it does a ONE-SHOT firmware load at boot then just
# beats the SRAM heartbeat + pets the kernel dw-wdt. Killing it stops the dw-wdt
# petting and the board resets ~15s later. We simply reset the SCR1 and reload;
# flared won't re-load over us (one-shot), and its heartbeat writes are to a
# different SRAM word than our echo firmware uses. (clk_core_mcu is kept alive by
# CLK_IGNORE_UNUSED in clk-rv1106.c, so the released core actually runs.)
FW=${1:-/userdata/echo-fw-words.txt}
# GRF uncached peripheral window (covers CRU + this SRAM + the mailbox
# 0xff5c0000) — WITHOUT this the MCU's peripheral/SRAM accesses are cached and
# invisible to Linux. hpmcu.rs: GRF_BASE 0xff040000 +0x24/+0x28 = 0xff000/0xffc00.
devmem 0xff040024 32 0xff000
devmem 0xff040028 32 0xffc00
# hold CORECRU MCU core in reset while we (re)write firmware
devmem 0xff3b8a04 32 0x001e001e
# write firmware words to 0xFF6FE000 (devmem uses mmap -> SRAM writable)
i=0
while read w; do
a=$(printf '0x%x' $((0xff6fe000 + i * 4)))
devmem "$a" 32 "$w"
i=$((i + 1))
done < "$FW"
# clear the SRAM mailbox/debug area (0xFF6FFF00, 8 words) so stale state can't
# be mistaken for a live echo
j=0
while [ $j -lt 8 ]; do
devmem "$(printf '0x%x' $((0xff6fff00 + j * 4)))" 32 0
j=$((j + 1))
done
# set HPMCU boot addr, then release from reset
devmem 0xff076044 32 0xff6fe000
devmem 0xff3b8a04 32 0x001e0000
sleep 1
echo "loaded $i words; SCR1 released."
echo " DBG_STATE = $(devmem 0xff6fff08 32) (expect 0x584F424D = 'MBOX')"
@@ -0,0 +1,98 @@
/*
* HPMCU mailbox echo firmware for the RV1106 (Syntacore SCR1, RV32IMC).
*
* Purpose: prove a fully-open A7 <-> HPMCU hardware-mailbox round-trip on our
* self-built Linux 6.18. The A7 (Linux) writes a {cmd,dat} pair into the
* mailbox A2B registers; this core polls A2B_STATUS, reads the pair, echoes it
* verbatim into the B2A registers (which raises the B2A doorbell IRQ back to
* Linux), and acks. No RT-Thread, no interrupts on the MCU side, machine mode
* only: a polling loop, exactly like the watchdog firmware.
*
* The mailbox IP is at 0xFF5C0000 (the HPMCU-connected instance: Linux resets
* it via SRST_CORE_MCU, the vendor MCU CMSIS header hard-codes MBOX_BASE
* 0xFF5C0000). Register model is IP-identical to rk3368 (mainline
* rockchip-mailbox.c): writing A2B_CMD(x) is the doorbell (hardware sets the
* A2B_STATUS bit); writing B2A_CMD(x) signals Linux (B2A_STATUS + IRQ).
*
* Both the mailbox registers and hpmcu_sram sit in the GRF "peripheral
* uncached" window (0xFF000000..0xFFC00000) configured at MCU release, so both
* sides see each other's writes with no cache maintenance (same property the
* watchdog SRAM mailbox relies on).
*
* Loaded + started by the proven flared/hpmcu.rs SRAM sequence
* (0xFF6FE000 load addr, CORECRU reset hold/release) — NOT the 0x40000
* boot-load path (that bricks a non-TB board; see boot-loaded-mcu-0x40000
* hazard). Fits the 8K hpmcu_sram budget (this is a few hundred bytes).
*/
#include <stdint.h>
#define REG32(a) (*(volatile uint32_t *)(uintptr_t)(a))
/* Mailbox IP (HPMCU-connected instance) — offsets match rockchip-mailbox.c. */
#define MBOX_BASE 0xFF5C0000u
#define A2B_INTEN REG32(MBOX_BASE + 0x00)
#define A2B_STATUS REG32(MBOX_BASE + 0x04)
#define A2B_CMD(x) REG32(MBOX_BASE + 0x08 + (x) * 8)
#define A2B_DAT(x) REG32(MBOX_BASE + 0x0c + (x) * 8)
#define B2A_STATUS REG32(MBOX_BASE + 0x2C)
#define B2A_CMD(x) REG32(MBOX_BASE + 0x30 + (x) * 8)
#define B2A_DAT(x) REG32(MBOX_BASE + 0x34 + (x) * 8)
#define NUM_CHANS 4
/*
* Liveness window in hpmcu_sram (reuse the watchdog mailbox layout at
* 0xFF6FFF00 so Linux tooling can read it the same way). We only touch the
* MCU-owned slots; the echo firmware does NOT run the watchdog, so it never
* touches MB_MAGIC/MB_COUNTER or fires a reset.
*
* +0x08 mcu_state : 'M','B','O','X' (0x584F424D LE) once the echo loop runs
* +0x0c last_cmd : the last cmd echoed (debug)
* +0x10 echo_cnt : number of messages echoed (heartbeat / proof-of-life)
*/
#define SRAM_DBG_BASE 0xFF6FFF00u
#define DBG_STATE REG32(SRAM_DBG_BASE + 0x08)
#define DBG_LASTCMD REG32(SRAM_DBG_BASE + 0x0c)
#define DBG_ECHOCNT REG32(SRAM_DBG_BASE + 0x10)
#define STATE_MBOX 0x584F424Du /* "MBOX" (LE bytes 'M','B','O','X') */
void main(void)
{
uint32_t echoes = 0;
DBG_STATE = STATE_MBOX;
DBG_LASTCMD = 0;
DBG_ECHOCNT = 0;
/* Enable the A2B doorbell for all channels: without A2B_INTEN set on the
* receiver (MCU) side, an A7 write to A2B_CMD does not raise A2B_STATUS,
* so our poll below never sees a message. (Symmetric to the A7 controller
* setting B2A_INTEN in its startup.) */
A2B_INTEN = (1u << NUM_CHANS) - 1;
for (;;) {
uint32_t status = A2B_STATUS;
int ch;
for (ch = 0; ch < NUM_CHANS; ch++) {
if (status & (1u << ch)) {
uint32_t cmd = A2B_CMD(ch);
uint32_t dat = A2B_DAT(ch);
/* Echo verbatim. Writing B2A_CMD raises the B2A
* doorbell (hardware sets B2A_STATUS -> Linux
* IRQ / readable status). */
B2A_CMD(ch) = cmd;
B2A_DAT(ch) = dat;
/* Ack: clear our A2B_STATUS bit (write-1-clear). */
A2B_STATUS = (1u << ch);
DBG_LASTCMD = cmd;
DBG_ECHOCNT = ++echoes;
}
}
}
}
@@ -0,0 +1,14 @@
#!/bin/sh
# Prove the A7<->HPMCU mailbox round-trip on channel 0. Write A2B_CMD first, then
# A2B_DAT — the A2B_DAT write is the doorbell (raises A2B_STATUS on the MCU side,
# whose echo firmware has enabled A2B_INTEN), so both CMD and DAT are current when
# the SCR1 reads them. The SCR1 echoes {cmd,dat} verbatim into B2A_CMD/B2A_DAT.
CMD=${1:-0x00001234}
DAT=${2:-0xcafef00d}
echo "liveness: DBG_STATE=$(devmem 0xff6fff08 32) (want 0x584F424D 'MBOX') echoes_before=$(devmem 0xff6fff10 32)"
devmem 0xff5c0008 32 "$CMD" # A2B_CMD(0)
devmem 0xff5c000c 32 "$DAT" # A2B_DAT(0) -- doorbell
sleep 1
echo "sent CMD=$CMD DAT=$DAT"
echo "echoed B2A_CMD=$(devmem 0xff5c0030 32) B2A_DAT=$(devmem 0xff5c0034 32)"
echo "echoes_after=$(devmem 0xff6fff10 32)"
@@ -0,0 +1,27 @@
/*
* HPMCU watchdog entry. The BootROM-style loader (or flared's runtime
* loader) points CORE_SGRF_HPMCU_BOOT_ADDR here and releases the SCR1 core
* from CORECRU reset; there is no CRT, no interrupts, machine mode only.
*/
.section .text.start, "ax"
.globl _start
_start:
/* Belt-and-braces: no traps expected, but if one fires, land on a
* tight loop at a known address rather than executing garbage. */
la t0, _trap
csrw mtvec, t0
la sp, __stack_top
/* Clear .bss */
la a0, __bss_start
la a1, __bss_end
1: bgeu a0, a1, 2f
sw zero, 0(a0)
addi a0, a0, 4
j 1b
2: call main
/* main never returns; if it does, spin. */
_trap:
3: j 3b
@@ -0,0 +1,336 @@
# RKNPU kernel driver: 5.10 (Rockchip vendor BSP, v0.9.2) -> 6.18.46 port
Status: **zImage + rockchip/rv1106-warden.dtb build cleanly, 0 errors, 0 warnings**,
with `CONFIG_ROCKCHIP_RKNPU=y` (+ `_DRM_GEM`/`_DEBUG_FS`/`_PROC_FS=y`) built in, 99
`rknpu`-prefixed symbols linked into `System.map`, and `&npu { status = "okay"; }`
in the board dtb. **Not flashed or probed on hardware** — per this task's explicit
boundary, this is a build-only port; on-target verification (dmesg probe log,
`/dev/dri/cardN`, the version-query ioctl round trip) is deferred to the parent
session. GPL source port only — no closed blob, same class of work as the RGA and
audio ports in this series (`../rga/PORT-DONE.md`, `../audio/PORT-PROGRESS.md`).
Trees involved:
- Target (edited): `<flare-edge>/research/linux-6.18.46/`
- Vendor source (read-only, copy-from):
`<flare-edge>/sdk/sysdrv/source/kernel/drivers/rknpu/`
(v0.9.2, `DRIVER_DATE "20230825"`)
Scope and strategic framing are in `PORT-PLAN.md` (file-by-file plan, authoritative)
and `OPEN-NPU-PLAN.md` (the "open driver, closed userspace" reality — this port
delivers exactly Tier A there: an open, on-hardware-verifiable *kernel* driver, and
nothing more; it does not and cannot unlock running a model).
## File set copied
Per `PORT-PLAN.md` §2.1's "Yes" column, copied verbatim from the vendor tree into
`drivers/rknpu/` (patched in place afterward — see API-delta table below):
| File | Bring in | Notes |
|---|---|---|
| `rknpu_drv.c` / `include/rknpu_drv.h` | yes | probe/remove, of_match table (incl. the RV1106 entry + `rv1106_rknpu_config`), DRM driver registration, power get/put |
| `rknpu_job.c` / `include/rknpu_job.h` | yes | job submit, IRQ handler, PC task-list execution, `rknpu_get_hw_version()`/`rknpu_get_drv_version()` |
| `rknpu_gem.c` / `include/rknpu_gem.h` | yes | GEM memory manager (DRM_GEM path) |
| `rknpu_reset.c` / `include/rknpu_reset.h` | yes | `SRST_A_RKNN`/`SRST_H_RKNN` reset control — compiled unmodified, zero API drift |
| `rknpu_iommu.c` / `include/rknpu_iommu.h` | yes (dead code path) | 61 lines; `iommu_en` stays false on our non-IOMMU DT — compiled unmodified |
| `rknpu_debugger.c` / `include/rknpu_debugger.h` | yes | `/proc/rknpu/load` continuity target for the Monitor page — compiled unmodified |
| `rknpu_mem.c`, `rknpu_mm.c`, `rknpu_fence.c` | **no** (headers only) | DMA_HEAP / SRAM / FENCE variants — not enabled for this port (§2.2). Their headers (`rknpu_mem.h`, `rknpu_mm.h`, `rknpu_fence.h`) *are* copied because `rknpu_drv.h`/`rknpu_gem.h` include them unconditionally for struct/prototype declarations, but zero object code results: every call site into these three is either `#ifdef CONFIG_ROCKCHIP_RKNPU_{DMA_HEAP,SRAM,FENCE}` (compiled out, all three configs left off) or `if (IS_ENABLED(CONFIG_ROCKCHIP_RKNPU_SRAM) && ...)` (compile-time-constant-folded to dead code and dropped by the optimizer — confirmed: none of `rknpu_mm_*`/`rknpu_mem_*` appear in `System.map`) |
UAPI header: `rknpu_ioctl.h` was split the way `drivers/accel/rocket/` (the mainline
Rocket driver, already vendored in this tree) does it — the real content lives at
`include/uapi/drm/rknpu_ioctl.h` (mainline convention for DRM ioctl UAPI headers),
and `drivers/rknpu/include/rknpu_ioctl.h` is a one-line redirect (`#include
<drm/rknpu_ioctl.h>`) so the vendor source's unmodified `#include "rknpu_ioctl.h"`
keeps working. This also makes the header directly usable by
`rknpu_version_test.c` (below) with a single `-I` flag and no libdrm dependency.
## Kconfig / Makefile wiring
`drivers/rknpu/Kconfig` (new) — trimmed from the vendor's own `Kconfig`: drops the
`ROCKCHIP_RKNPU_SRAM`/`_FENCE` options and the `DRM_GEM` vs `DMA_HEAP` `choice`
block entirely (DRM_GEM is the only memory manager this port wires up — see
`PORT-PLAN.md` §1 "Memory manager choice"). Sourced from `drivers/Kconfig` right
after `source "drivers/accel/Kconfig"` (RKNPU is a classic DRM device, not
`drivers/accel/` — see §1 of `PORT-PLAN.md`, "not `/dev/accel/`").
`drivers/rknpu/Makefile` (new) — trimmed from the vendor's own `Makefile`: only the
five always-on objects plus GEM (`rknpu_drv.o`, `rknpu_reset.o`, `rknpu_job.o`,
`rknpu_debugger.o`, `rknpu_iommu.o`, `rknpu_gem.o` gated on
`CONFIG_ROCKCHIP_RKNPU_DRM_GEM`); no edit needed for the never-built
`rknpu_mem.o`/`rknpu_mm.o`/`rknpu_fence.o` lines because they're simply omitted
(matches `PORT-PLAN.md` §2.1's note that no Makefile edit is required, just not
setting their Kconfig symbols). `ccflags-y` adds `compat/` to the include path
**ahead of** `include/` — see the compat-shim section below for why the ordering
matters.
Wired into `drivers/Makefile`: `obj-$(CONFIG_ROCKCHIP_RKNPU) += rknpu/` placed
immediately after `obj-y += gpu/` (rknpu registers a classic DRM device, so — like
`gpu/` itself — it must come after `char/` and `iommu/` per the existing comments
there, and building after `gpu/` specifically ensures the DRM core is ready first).
## Config symbols set
Via `./scripts/config --enable <SYMBOL>` then `make ARCH=arm CROSS_COMPILE=...
olddefconfig`, confirmed `=y` in `.config` afterward (no silent Kconfig-dependency
drop — all four symbols round-tripped through `olddefconfig` unchanged):
```
CONFIG_ROCKCHIP_RKNPU=y
CONFIG_ROCKCHIP_RKNPU_DRM_GEM=y
CONFIG_ROCKCHIP_RKNPU_DEBUG_FS=y
CONFIG_ROCKCHIP_RKNPU_PROC_FS=y
```
Prerequisites already satisfied pre-port: `CONFIG_DRM=y`, `CONFIG_ARCH_ROCKCHIP=y`
(both already on from the M4 display work). Left off per `PORT-PLAN.md` §2.2/§2.6:
`ROCKCHIP_RKNPU_DMA_HEAP`, `ROCKCHIP_RKNPU_SRAM`, `ROCKCHIP_RKNPU_FENCE` — RV1106
needs none of them (single-core NPU, no NBUF, no OPP table, no multi-domain
fencing).
## The four compat-shim headers (`drivers/rknpu/compat/soc/rockchip/`)
All four vendor-only headers from `PORT-PLAN.md` §2.4 (absent from mainline) are
addressed, but **not uniformly** — one of the four needed a genuinely fresh shim
rather than a vendor-verbatim copy, for a reason the plan didn't anticipate:
### `rockchip_iommu.h` — written fresh, NOT a copy of the vendor header
This is the one real surprise of the port. The vendor header itself already
carries a working `#if IS_ENABLED(CONFIG_ROCKCHIP_IOMMU) ... #else <stub> #endif`
split, so naively copying it looked safe. **It is not**, in this specific tree:
`CONFIG_ROCKCHIP_IOMMU` is not a vacant symbol here — it's `=y` in our `.config`
already, for the *real*, unrelated mainline IOMMU driver
(`drivers/iommu/rockchip-iommu.c`, pulled in by the M4 display/VOP2 work). That
driver implements the standard `struct iommu_ops` and does **not** export a
function called `rockchip_iommu_is_enabled()` — confirmed by grepping
`drivers/iommu/rockchip-iommu.c` for the symbol (zero hits). Had the vendor
header been copied as-is, `IS_ENABLED(CONFIG_ROCKCHIP_IOMMU)` would have evaluated
true (not false, as the plan assumed) and selected the `extern bool
rockchip_iommu_is_enabled(struct device *dev);` declaration with **no definition
anywhere in the tree** — a link failure that would only show up at the very end of
a full kernel build, not at this file's own compile step.
Fix: `drivers/rknpu/compat/soc/rockchip/rockchip_iommu.h` is a fresh 25-line file,
unconditional (no `#if IS_ENABLED(...)` at all), providing only the one symbol
`rknpu_drv.c` actually calls: `rockchip_iommu_is_enabled()` (used once, in
`rknpu_power_off()`, inside `if (rknpu_dev->multiple_domains)` — true only for
RK3588's 3-core NPU; RV1106's `rv1106_rknpu_config` never sets it, and the board DT
carries no `iommus=` property, so this is genuinely dead code, exactly as
`PORT-PLAN.md` §2.4 predicted — the fix just couldn't be "copy the vendor header,"
it had to be "write an unconditional one that doesn't shadow-collide with this
tree's real `CONFIG_ROCKCHIP_IOMMU`."
### `rockchip_opp_select.h`, `rockchip_system_monitor.h`, `rockchip_ipa.h` — copied verbatim, safe as-is
Unlike `ROCKCHIP_IOMMU`, none of `CONFIG_ROCKCHIP_OPP`, `CONFIG_ROCKCHIP_SYSTEM_MONITOR`,
`CONFIG_ROCKCHIP_IPA` exist anywhere in mainline (grepped every `Kconfig` in the
tree and the `.config` — zero hits for all three), so these three vendor headers'
own `#if IS_ENABLED(...)`/`#if IS_REACHABLE(...)` gates always evaluate false and
their static-inline stub branches (`-EOPNOTSUPP`/`ERR_PTR(-ENOTSUPP)`/no-ops) are
always selected — no collision risk, copied byte-for-byte from
`sdk/sysdrv/source/kernel/include/soc/rockchip/`, with only an explanatory header
comment added to each. The full struct definitions (`struct rockchip_opp_info`,
`struct monitor_dev_info`, `struct ipa_power_model_data`) are still required
unconditionally, independent of which branch is taken: `rknpu_device` (in
`rknpu_drv.h`) embeds `struct rockchip_opp_info opp_info;` **by value**, and holds
`struct monitor_dev_info *mdev_info` / `struct ipa_power_model_data *model_data`
pointers dereferenced in the (dead-for-RV1106, but still compiled) DVFS code path.
`compat/` is listed **first** in `ccflags-y` (ahead of `include/`), specifically so
these four shadow anything a future in-tree `soc/rockchip/` addition might
introduce — `rockchip_iommu.h` in particular must never resolve to a different,
unrelated header of the same name.
### Devfreq/OPP/monitor/IPA code: compiles, but is entirely unreferenced for our build
Worth recording because it looked like a real blocker mid-port and turned out not
to be one: `rknpu_drv.c` carries a large `#if KERNEL_VERSION(6,1,0) >
LINUX_VERSION_CODE / #else / #endif` split around `rknpu_devfreq_init()` (two
different implementations, one per era). For 6.18 the `#else` (>= 6.1) branch is
what's textually compiled, and it references file-scope statics (`npu_mdevp`,
`npu_devfreq_profile`, `npu_cooling_power`) that a naive manual read of the
surrounding `#if` nesting suggested might only be declared under the sibling
`< 6.1` branch. **This did not manifest as a build error**`rknpu_drv.o` compiled
clean on the first fixed pass, which is the authoritative answer (per this task's
own "prove by compiling, not by inspection" instruction — a manual preprocessor
trace over ~700 lines of nested version gates is exactly the kind of thing to
distrust vs. the compiler). Confirmed after the fact: **none** of
`rknpu_devfreq_init`, `npu_devfreq_target`, `npu_devfreq_profile`, `npu_mdevp`, or
`npu_cooling_power` appear in `System.map` — the call site into
`rknpu_devfreq_init()` (in `rknpu_probe()`) is itself gated by the same `#if
KERNEL_VERSION(6,1,0) > LINUX_VERSION_CODE`, false for 6.18, so the whole devfreq
init path is unreachable and GCC drops the unused `static` functions entirely.
Net effect matches `PORT-PLAN.md`'s framing exactly — RV1106 has no OPP table and
no DVFS — just via straightforward dead-code elimination rather than anything this
port had to force.
## 5.10 -> 6.18 API-delta fixes (all mechanical, all found by iterating single-object builds)
### `rknpu_drv.c`
| # | Change |
|---|---|
| 1 | `struct drm_driver` has no `.gem_prime_mmap` member any more (the legacy driver-level `gem_prime_*` fallback vtable was removed). Dropped the `.gem_prime_mmap = drm_gem_prime_mmap,` initializer for the `KERNEL_VERSION(6,1,0) <= LINUX_VERSION_CODE` branch — not a functionality loss, because per-object mmap is already wired via `drm_gem_object_funcs.mmap = rknpu_gem_mmap_obj` in `rknpu_gem.c`'s `rknpu_gem_object_funcs` (the mechanism this field used to be a fallback *from*, per `PORT-PLAN.md` §2.5). |
| 2 | `struct drm_driver` has no `.date` member any more (dropped from mainline DRM). Removed the `.date = DRIVER_DATE,` initializer; `.major`/`.minor`/`.patchlevel` already carry the version. |
| 3 | `hrtimer_init(&t, clock, mode)` + separate `t.function = fn` assignment -> combined `hrtimer_setup(&t, fn, clock, mode)` (same fix class as the RGA port's hrtimer change, `../rga/PORT-DONE.md` item 2). |
| 4 | `platform_driver.remove`: `int (*)(struct platform_device *)` -> `void (*)(struct platform_device *)`. Changed `rknpu_remove()` from `static int ... { ...; return 0; }` to `static void ...` (dropped the trailing `return 0;`) — same fix as the RGA and audio ports. |
| 5 | `MODULE_IMPORT_NS(DMA_BUF)` -> `MODULE_IMPORT_NS("DMA_BUF")` (quoted-string form; same fix as the RGA port item 6). |
### `rknpu_gem.c`
| # | Change |
|---|---|
| 1 | `<linux/pfn_t.h>` and the `pfn_t` wrapper type (`__pfn_to_pfn_t()`, `PFN_DEV`) were removed entirely from mainline. Dropped the include; `vmf_insert_mixed()` (the only call site reachable at `KERNEL_VERSION(4,15,0) <= LINUX_VERSION_CODE`, which is our branch) now takes a plain `unsigned long pfn` directly — `pfn = page_to_pfn(...)` was already computing that raw value, so the fix is just passing `pfn` instead of `__pfn_to_pfn_t(pfn, PFN_DEV)`. |
| 2 | `vmap()`/`vunmap()`/`VM_MAP` used to be pulled in transitively; 6.18 needs `<linux/vmalloc.h>` included explicitly. Added it. |
| 3 | `%zu` format specifier for `rknpu_obj->size` (`-Werror=format=`): the field is declared `unsigned long`, not `size_t`, on this target — changed to `%lu`. (Pure `-Wformat` pickiness, not a real 5.10-vs-6.18 delta; the vendor's own type just doesn't match `%zu` on this ABI and newer GCC/kernel `-Werror` catches it.) |
| 4 | `iommu_map()` gained a trailing `gfp_t gfp` argument (`iommu_map(domain, iova, paddr, size, prot)` -> `iommu_map(domain, iova, paddr, size, prot, gfp)`). Added `GFP_KERNEL` at both call sites (cache-buffer path and the per-sg-entry DDR path). |
| 5 | `vma->vm_flags` is a read-only field now (direct assignment is a compile error, not just deprecated) — replaced all four sites with `vm_flags_set()`/`vm_flags_clear()`: `rknpu_gem_mmap_pages()` (`VM_MIXEDMAP`), `rknpu_gem_mmap_cache()` (`VM_MIXEDMAP`), `rknpu_gem_mmap_buffer()` (`VM_DONTCOPY\|VM_DONTEXPAND\|VM_DONTDUMP\|VM_IO` set, `VM_PFNMAP` cleared). The three read-only accesses (`vm_get_page_prot(vma->vm_flags)` in `rknpu_gem_mmap`) needed no change — only assignment is blocked. |
`rknpu_reset.c`, `rknpu_iommu.c`, `rknpu_debugger.c`, `rknpu_job.c` needed **zero**
changes — compiled clean against 6.18 unmodified, confirming `PORT-PLAN.md` §2.5's
assessment that most of the surface was already correctly version-gated by the
vendor.
## Device tree change
One board-DTS override appended to `arch/arm/boot/dts/rockchip/rv1106-warden.dts`
(base `rv1106.dtsi`'s `npu@ff660000` node — `compatible`, `reg`, `interrupts`
(`GIC_SPI 109`), `clocks` (`ACLK_RKNN`/`HCLK_RKNN`), `assigned-clock-rates
= <420000000>`, `resets` (`SRST_A_RKNN`/`SRST_H_RKNN`) — left untouched, per
instructions, exactly as `PORT-PLAN.md` §2.3 specified):
```dts
&npu {
status = "okay";
};
```
No `iommus=` property added (non-IOMMU mode, matches the 5.10 boot-log finding);
no `power-domains` (single-rail). Verified against the **compiled** dtb (`dtc -I
dtb -O dts`, not just the source): `npu@ff660000` shows `status = "okay"`, and all
other properties resolved correctly (`interrupts = <0x00 0x6d 0x04>` = `GIC_SPI 109
IRQ_TYPE_LEVEL_HIGH`, matching the dtsi). `dtc` build output for the whole board dtb
carries exactly one warning, pre-existing and unrelated to NPU (a
`graph_endpoint`/VOP2-display bidirectionality note on `rv1106.dtsi:458`, not
touched by this port).
## Build status
Exact commands run (per the task's build recipe):
```sh
export PATH="<flare-edge>/sdk/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin:/usr/bin:/bin"
export ARCH=arm CROSS_COMPILE=arm-rockchip830-linux-uclibcgnueabihf-
cd <flare-edge>/research/linux-6.18.46
make ARCH=arm CROSS_COMPILE=$CROSS_COMPILE zImage rockchip/rv1106-warden.dtb -j"$(nproc)"
```
Result: **exit 0**. `grep -iE "error|warn"` over the full build log returns exactly
one line — the pre-existing, NPU-unrelated dtc warning noted above. Zero errors,
zero rknpu-related warnings. `arch/arm/boot/zImage` (8.7 MB) and
`arch/arm/boot/dts/rockchip/rv1106-warden.dtb` (36.9 KB) both produced. XZ kernel
compression and the existing console/earlycon config were left untouched, per
instructions; no other in-flight work in this tree (mailbox, audio, wifi, gmac,
etc.) was reverted or altered.
## Verification: rknpu is genuinely linked in (not silently dropped to a module)
Per the task's explicit instruction, checked with **host** `grep` on `System.map`
(ground truth for built-in linkage — cross-`nm` mis-lists symbols on this
toolchain, same caveat as every other port in this series):
```
$ grep -cE 'rknpu' System.map
99
$ grep -E '\brknpu_probe\b|\brknpu_remove\b|rknpu_driver\b|rknpu_of_match|rknpu_drm_driver\b' System.map
c0be4230 t rknpu_remove
c0be476c t rknpu_probe
c177d4a0 r rknpu_of_match
c2108e58 d rknpu_driver
c2108ef0 d rknpu_drm_driver
```
99 `rknpu`-prefixed symbols are linked into the kernel image, including the
`platform_driver`'s `probe`/`remove` entry points and the `of_match_table`
(`rknpu_of_match`, confirmed at the source level to still carry the
`"rockchip,rv1106-rknpu"` compatible + `rv1106_rknpu_config` entry, untouched by
this port's fixes) and the `drm_driver` struct itself. This is the probe/of_match
symbol presence the task asked to confirm.
## Hardware test program: `rknpu_version_test.c`
Written to `warden-sdk/kernel/rv1106-enablement/npu/rknpu_version_test.c` per the
task's spec. Dependency-free beyond the kernel tree's own UAPI headers — no
libdrm, no target sysroot headers:
```sh
export PATH="<flare-edge>/sdk/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin:/usr/bin:/bin"
arm-rockchip830-linux-uclibcgnueabihf-gcc \
-I<flare-edge>/research/linux-6.18.46/include/uapi \
-Wall -O2 -static \
-o rknpu_version_test \
<warden-sdk>/kernel/rv1106-enablement/npu/rknpu_version_test.c
```
**This exact command was run in this session** (build-only — the resulting binary
was not copied to or executed on any target) and produced a clean ARM EABI5 static
ELF binary with exit code 0. One expected, harmless warning appears:
`#warning "Attempt to use kernel headers from user space"` from
`include/uapi/linux/types.h` — the standard notice every raw-kernel-uapi-header
userspace build gets; it does not affect correctness (`__u32` etc. are still
correctly defined with `__KERNEL__` undefined).
What it does: tries `/dev/dri/card0`, then `card1`, then `renderD128`; on the first
one that opens, issues `DRM_IOCTL_RKNPU_ACTION` with `.flags = RKNPU_GET_DRV_VERSION`
then again with `.flags = RKNPU_GET_HW_VERSION`, and prints both. Traced against the
driver source to get the exact semantics right:
- `RKNPU_GET_DRV_VERSION` returns `RKNPU_GET_DRV_VERSION_CODE(DRIVER_MAJOR,
DRIVER_MINOR, DRIVER_PATCHLEVEL)` = `MAJOR*10000 + MINOR*100 + PATCHLEVEL`
(`rknpu_drv.c:rknpu_get_drv_version()`) — for this port's unmodified
`DRIVER_MAJOR/MINOR/PATCHLEVEL = 0/9/2`, that's raw code `902`, which the test
program decodes back to `"0.9.2"` via the UAPI header's own
`RKNPU_GET_DRV_VERSION_{MAJOR,MINOR,PATCHLEVEL}()` macros.
- `RKNPU_GET_HW_VERSION` returns a raw value read directly off the NPU core's
`VERSION`/`VERSION_NUM` registers (`rknpu_job.c:rknpu_get_hw_version()`) — no
published decode table exists for it (per `PORT-PLAN.md` §3 step 5, "checks...
returns something plausible"); the test program just prints it in hex.
**Expected output on a successful hardware run** (parent session):
```
opened /dev/dri/card0 (fd=3)
driver version: 0.9.2 (raw code 902)
hw version: 0x.... (raw)
PASS: /dev/dri/card0 answered both version-query ioctls -- probe, power-get/put,
and clock/reset all exercised.
```
A driver version that decodes to anything other than `0.9.2` would indicate a stale
build or a stub/mock intercepting the ioctl, not a real driver response — that's
the value of checking the decoded string, not just the ioctl return code.
## Explicitly deferred to the parent session (not done here, per this task's boundary)
- **No flashing, no boot, no `dmesg` check.** This session never touched hardware.
- Boot-time probe verification: `dmesg | grep -i rknpu` should show clean
clock/reset/IRQ acquisition, no panic, no permanent `-EPROBE_DEFER` (a single
deferral early at boot, before other clock/reset providers are up, would be
normal — same caveat class as the audio port's acodec probe-order note).
Compare against the 5.10 baseline probe log if available (`PORT-PLAN.md` §3
step 3).
- `ls -la /dev/dri/` should show a new `cardN`/`renderD1xx` — classic DRM node
(this port intentionally does **not** produce a `/dev/accel/` node; see
`PORT-PLAN.md` §1 and `OPEN-NPU-PLAN.md` §1.3 for why mainline's own
`drivers/accel/rocket/` driver — RK3588/ARM64-only — is a different codebase
that doesn't reach RV1106 at all).
- Run `rknpu_version_test` (built above) against the real node; confirm the
decoded driver version prints `0.9.2` and the hw version is non-zero/plausible.
- **Not attempted, not required for this milestone**: `DRM_IOCTL_RKNPU_SUBMIT`,
any `.rknn` model, `librknnrt` — that's the closed-userspace question
`OPEN-NPU-PLAN.md` §1.21.4 covers; out of scope here by design (no blob is
shipped by this port, and none is needed to prove the kernel driver itself).
## Also updated this session
`../DRIVER-PARITY.md`'s NPU row: `⬜ M6 — plan: npu/PORT-PLAN.md` -> `🔨 M6 built,
0 errors/0 warnings, 99 rknpu-prefixed symbols in System.map, &npu
{status="okay"} in the dtb — not yet flashed/probed on hardware`.
+43
View File
@@ -0,0 +1,43 @@
# NPU (rknpu) open kernel driver — ✅ VERIFIED on warden-c8a3 (2026-08-25)
The open GPL rknpu kernel driver runs on our self-built Linux 6.18.46. This is the
achievable open end state (Tier A in `OPEN-NPU-PLAN.md`); open *compute* remains a
from-scratch RE project (see the ceiling note below).
## Evidence (serial, _b slot = our 6.18)
```
[drm] Initialized rknpu 0.9.2 for ff660000.npu on minor 1
/dev/dri/ -> card0 (VOP display) card1 (rknpu) renderD128
$ rknpu_version_test
/dev/dri/card0: not an rknpu node (version ioctl: Invalid argument)
opened /dev/dri/card1 (fd=3)
driver version: 0.9.2 (raw code 902)
hw version: 0x54524548 (raw)
PASS: /dev/dri/card1 answered both version-query ioctls -- probe,
power-get/put, and clock/reset all exercised.
```
The `RKNPU_GET_DRV_VERSION`/`RKNPU_GET_HW_VERSION` ioctls exercise the full
dispatch → power-domain get/put → clock/reset path, so a clean answer confirms the
whole driver bring-up, not just registration.
## The fix that took it from probe-fail to PASS
The base dtsi `npu@ff660000` node declares its interrupt but has **no
`interrupt-names`**; the rknpu driver requests its IRQ by name (`"npu_irq"`), so
probe bailed `error -ENXIO: IRQ npu_irq not found` and never registered its DRM
device. Board DTS override adds `interrupt-names = "npu_irq";` (+ `status="okay"`).
(The rest of the port — GPL source, 4 compat-shim headers for dead-code vendor
headers, 10 mechanical 6.18 API deltas — is in `PORT-PROGRESS.md`.)
Also: the version test must iterate DRM cards and keep the one that ANSWERS the
ioctl — the display card (card0) opens fine but returns EINVAL. Fixed in
`rknpu_version_test.c`.
## The honest ceiling (why "100% open NPU" stops at the driver)
The kernel driver only DMAs an opaque userspace-authored `regcmd` blob into the PC
registers and pulses go — it never inspects the compute stream. The compute-engine
register map (TRM Part 2) is not public for RV1106, there is **zero open RE prior
art** for this NPU generation, and only the closed RKNN-Toolkit2 compiler emits
valid regcmd. Mainline `accel/rocket` + Mesa Teflon are RK3588-only (64-bit). So an
open compiler/runtime is a from-scratch, ~person-year register-RE project, scoped
(not staffed) in `OPEN-NPU-PLAN.md` (M-NPU-2 spike / M-NPU-3). We ship the open
driver; we do NOT ship any closed blob.
@@ -0,0 +1,137 @@
// SPDX-License-Identifier: GPL-2.0
/*
* rknpu_version_test.c -- standalone hardware smoke test for the RKNPU
* kernel driver port (see PORT-PLAN.md / PORT-PROGRESS.md in this
* directory).
*
* Opens a DRM node (tries /dev/dri/card0, then card1, then renderD128),
* issues DRM_IOCTL_RKNPU_ACTION with RKNPU_GET_DRV_VERSION and then
* RKNPU_GET_HW_VERSION, and prints the decoded results. This exercises the
* full ioctl-dispatch -> power-get/put -> clock/reset path with zero
* dependency on a regcmd buffer or the (closed) RKNN runtime -- see
* OPEN-NPU-PLAN.md §1.4 Tier A and PORT-PLAN.md §3 step 5.
*
* Dependency-free beyond the UAPI header: build with
* -I<kernel-tree>/include/uapi
* so both <drm/drm.h> and <drm/rknpu_ioctl.h> (and the linux/types.h,
* linux/ioctl.h they need) resolve entirely out of the kernel tree's own
* include/uapi/ -- no libdrm, no target sysroot headers required.
*
* Cross-compile (see PORT-PROGRESS.md for the full recipe):
*
* export PATH="<flare-edge>/sdk/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin:/usr/bin:/bin"
* arm-rockchip830-linux-uclibcgnueabihf-gcc \
* -I<flare-edge>/research/linux-6.18.46/include/uapi \
* -Wall -O2 -static -o rknpu_version_test rknpu_version_test.c
*
* (add -static if the target rootfs's libc version/ABI is in doubt; drop it
* for a smaller dynamically-linked binary if the rootfs libc is known good.)
*
* Expected output on a working probe:
*
* opened /dev/dri/card0 (fd=3)
* driver version: 0.9.2 (raw code 902)
* hw version: 0x.... (raw)
* PASS: /dev/dri/card0 answered both version-query ioctls -- probe,
* power-get/put, and clock/reset all exercised.
*
* "0.9.2" is DRIVER_MAJOR/MINOR/PATCHLEVEL from the vendor source
* (rknpu_drv.h) unmodified by this port -- a match confirms the ioctl
* round-trip reached real driver code, not a stub. The hw version is a raw
* value read directly off the NPU core's VERSION/VERSION_NUM registers
* (rknpu_job.c:rknpu_get_hw_version()) -- any non-zero, non-0xffffffff
* value is a plausible "the register block is alive" signal; there is no
* published decode table for it beyond that (PORT-PLAN.md §3 step 5).
*/
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <drm/rknpu_ioctl.h>
static const char *const candidates[] = {
"/dev/dri/card0",
"/dev/dri/card1",
"/dev/dri/renderD128",
NULL,
};
static int do_action(int fd, __u32 flags, __u32 *value)
{
struct rknpu_action act;
memset(&act, 0, sizeof(act));
act.flags = flags;
if (ioctl(fd, DRM_IOCTL_RKNPU_ACTION, &act) < 0)
return -errno;
*value = act.value;
return 0;
}
int main(void)
{
int fd = -1;
const char *path = NULL;
__u32 drv_version = 0;
__u32 hw_version = 0;
int ret;
int i;
/* There may be several DRM cards (the VOP display registers card0, the
* NPU registers its own). Try each candidate and keep the FIRST that
* actually answers the RKNPU version ioctl -- opening successfully is not
* enough (the display card opens fine but returns EINVAL). */
for (i = 0; candidates[i] != NULL; i++) {
int f = open(candidates[i], O_RDWR | O_CLOEXEC);
if (f < 0) {
fprintf(stderr, "open(%s): %s\n", candidates[i],
strerror(errno));
continue;
}
if (do_action(f, RKNPU_GET_DRV_VERSION, &drv_version) == 0) {
fd = f;
path = candidates[i];
break;
}
fprintf(stderr, "%s: not an rknpu node (version ioctl: %s)\n",
candidates[i], strerror(errno));
close(f);
}
if (fd < 0) {
fprintf(stderr,
"FAIL: no DRM node answered the RKNPU version ioctl -- "
"is CONFIG_ROCKCHIP_RKNPU probed? check "
"`dmesg | grep -i rknpu` and `ls -la /dev/dri/`\n");
return 1;
}
printf("opened %s (fd=%d)\n", path, fd);
ret = 0;
printf("driver version: %u.%u.%u (raw code %u)\n",
RKNPU_GET_DRV_VERSION_MAJOR(drv_version),
RKNPU_GET_DRV_VERSION_MINOR(drv_version),
RKNPU_GET_DRV_VERSION_PATCHLEVEL(drv_version), drv_version);
ret = do_action(fd, RKNPU_GET_HW_VERSION, &hw_version);
if (ret) {
fprintf(stderr,
"FAIL: DRM_IOCTL_RKNPU_ACTION(RKNPU_GET_HW_VERSION): %s\n",
strerror(-ret));
close(fd);
return 1;
}
printf("hw version: 0x%08x (raw)\n", hw_version);
printf("PASS: %s answered both version-query ioctls -- probe, "
"power-get/put, and clock/reset all exercised.\n", path);
close(fd);
return 0;
}