kernel/rv1106: PVTM VERIFIED — both monitors probe + read (7th driver)

Whole-driver port (mainline has no rockchip pvtm): copied vendor rockchip_pvtm.c
+ pvtm.h. 6.18 deltas: thermal_zone opaque -> thermal_zone_get_temp; of_device.h.
Real blocker: vendor of_match_table wrapped rv1106 entries in #ifdef
CONFIG_CPU_RV1106 (nonexistent in mainline) -> devices+driver present but never
bound, probe silent. Dropped the guard -> both bind.

Verified: ff240000.pvtm + ff390000.pvtm probed; /sys/kernel/debug/pvtm/{core,pmu}/
value read ring-osc counts (71682 90462 / 35772). pvtm/PORT-DONE.md.

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 07:25:49 -06:00
co-authored by Claude Opus 4.8
parent aa25577095
commit 54f961c6b6
4 changed files with 582 additions and 0 deletions
@@ -42,6 +42,7 @@ c8a3, not just compiled.
| RGA 2D (rga2) | rga2 | ported (vendor char-dev) | ✅ /dev/rga, hw 3.3.87975 | | 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) | | 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 | | Audio codec (acodec) | rockchip,rv1106-codec | ported (rv1106_codec.c) | ✅ card `rv1106-acodec`, pcmC0D0p/c (`audio/`); audible test @ bench |
| 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) | | FIQ debugger (ttyFIQ0) | fiq_glue | rockchip | ⬜ optional (we use ttyS2) |
Legend: ✅ verified on hardware · 🔨 built, not yet verified · ⬜ not started. Legend: ✅ verified on hardware · 🔨 built, not yet verified · ⬜ not started.
+252
View File
@@ -0,0 +1,252 @@
# HPMCU mailbox to 100% — plan, feasibility, hardware round-trip verify
**Goal:** a working, fully-open A7 ↔ HPMCU (SCR1) mailbox link on our self-built
Linux 6.18.46, **verified by a message round-trip over serial on hardware.**
**Bottom line up front:** this is genuinely achievable and far more tractable than
the NPU. Every piece is open (GPL kernel driver + our own bare-metal SCR1 firmware),
the controller driver **already binds on our 6.18 kernel with zero code change**,
and we already have a **hardware-validated way to load and run custom firmware on
the SCR1 core**. The only real work is: (1) enable the controller in DT + config,
(2) write a ~dozen-line SCR1 echo handler, (3) write a small Linux client, and
(4) run the round-trip on `warden-c8a3` (the recovery-capable rig) over serial.
The one substantive design decision — **hardware mailbox IP vs. our current
`/dev/mem` polled-SRAM scheme** — is settled below: build the real mailbox link
(it is the open, IRQ-driven, general-purpose answer), but keep the proven SRAM
watchdog exactly as-is (different threat model, different job).
---
## 1. Feasibility — honest assessment
**Fully feasible. Two halves, both open, both with working reference code to copy.**
- **Controller (Linux side): non-issue.** `drivers/mailbox/rockchip-mailbox.c` is
upstream in mainline 6.18 and **already binds on our exact kernel** via the
generic `rockchip,rk3368-mailbox` **fallback compatible** with **zero patching**
— recorded in `CAPABILITIES-AUDIT.md:30`, confirmed by source read. RV1106's DT
declares both instances with that fallback string. Gated today only by
`status="disabled"` + `CONFIG_ROCKCHIP_MBOX` being absent from the defconfig.
- **HPMCU firmware (MCU side): we already do the hard part.** WardenOS has a
**hardware-validated (2026-08-14, PR #25)** bare-metal SCR1 firmware built with
the vendor's own `sysdrv/source/mcu/` toolchain (xPack `riscv-none-embed-gcc`
10.2.0, `-march=rv32imc`), loaded via `/dev/mem`-mmap by `flared/src/hpmcu.rs`,
running a real state machine and firing a real CRU reset. Adding a mailbox echo
handler to that firmware is small, additive work — and the vendor ships a
register-level template for the MCU side (`hal_mbox.c`) plus a working two-ended
example (`battery-ipc/stream.c` + `rockchip_thunderboot_service.c`).
- **The only genuinely new/greenfield piece** is that no general Linux-side mailbox
_client_ for RV1106 exists in-tree (the one real client,
`rockchip_thunderboot_service.c`, is a hardwired one-shot `{0xf00d,0xdeadbeef}`
"MCU done" signal, and isn't in our board's DT). But a mailbox client using the
stock upstream `mailbox_client.h` API is a small, well-understood piece of code —
not a driver port. `mailbox-test.c` (stock kernel debugfs exerciser, present in
our tree, `compatible="mailbox-test"`) lets us prove the Linux→controller path
**before writing any client at all.**
**Confidence in a verifiable open link: high.** The only area flagged risky is
_starting the SCR1 with our own entry point_ — and we have already solved and
hardware-proven exactly that (the `hpmcu.rs` load/release sequence), so it is a
known quantity here, not the open problem it would be for a team starting cold.
**Decision — hardware mailbox vs. `/dev/mem` SRAM:** build the **hardware mailbox**
as the open, general, IRQ-driven bidirectional channel (this is "the real mailbox
client" the task wants). **Keep the existing SRAM polled-word watchdog untouched**
ADR-0002 deliberately chose it as a dead-man's-switch (no IRQ, no dependency on the
mailbox controller being up, survives A7 hangs by design). They coexist: the SRAM
word is the safety watchdog; the mailbox is the general IPC channel. Do **not** rip
out `hpmcu.rs`'s watchdog to route it through the mailbox — that would trade a
proven fail-safe for a more complex path with no safety gain.
**Do NOT (for a first cut):** port `rockchip_rpmsg.c` or NXP `rpmsg-lite`. Both are
real Rockchip patterns but genuinely un-ported to RV1106 (rpmsg-lite has platform
files only for RK3308/RK3568; `rockchip_rpmsg.c` matches only `rk3562/rk3568-rpmsg`;
neither is upstream in mainline; `CONFIG_RPMSG_ROCKCHIP` is `# not set` on every
RV1106 defconfig). They add virtio/vring machinery we don't need to prove a link.
If a richer multi-message channel is later wanted, layer a small hand-rolled ring
buffer in `hpmcu_sram` on top of the doorbell — that's the natural next step, not
a full rpmsg port.
---
## 2. Key technical facts
### The two mailbox instances (both `status="disabled"` in base `rv1106.dtsi`)
| Node | Base | IRQ | Clock | Consumer | Use |
|---|---|---|---|---|---|
| `mailbox@ff5c0000` | `0xff5c0000` (`reg` size `0x200`) | `GIC_SPI 1` | `PCLK_MAILBOX` | `thunder-boot-service` (ch1, `"amp-rx"`) — not on our board | **HPMCU-connected — use this one** |
| `pmu_mailbox@ff378000` | `0xff378000` (`0x200`) | `GIC_SPI 114` | `PCLK_PMU_MAILBOX` | none anywhere | PMU-domain, undocumented purpose — **avoid** |
**`@ff5c0000` is the HPMCU-connected instance, proven from both ends:** the Linux
`thunder_boot_service` node's `resets` are literally named `SRST_CORE_MCU*`; the
MCU-side vendor CMSIS header hard-codes `#define MBOX_BASE 0xFF5C0000U`
(`.../hal/lib/CMSIS/Device/RV1106/Include/rv1106.h:815`) — the same physical
address the A7 sees. Both clock IDs are real CRU gates (`PCLK_MAILBOX` at
`clk-rv1106.c:343`, `PCLK_PMU_MAILBOX` at `:737`), so the fallback-compatible match
is trustworthy: the register block is IP-identical to rk3368's.
### Register model (fixed offsets, hardcoded in the driver; identical both instances)
```
MAILBOX_A2B_INTEN 0x00 // AP -> MCU direction
MAILBOX_A2B_STATUS 0x04
MAILBOX_A2B_CMD(x) 0x08 + x*8 // x = channel 0..3
MAILBOX_A2B_DAT(x) 0x0c + x*8
MAILBOX_B2A_INTEN 0x28 // MCU -> AP direction
MAILBOX_B2A_STATUS 0x2C
MAILBOX_B2A_CMD(x) 0x30 + x*8
MAILBOX_B2A_DAT(x) 0x34 + x*8
```
**4 channels, 32-bit `{cmd,data}` per message per direction. Doorbell + 8-byte
payload — NOT a bulk channel.** Sender writes CMD then DAT (two `writel_relaxed`,
fire-and-forget, no ack/poll — `rockchip-mailbox.c:46-70`); the peer's STATUS bit
raises an IRQ; receiver reads CMD/DAT, dispatches to the registered client via
`mbox_chan_received_data`, clears the STATUS bit to ack. `#mbox-cells = <1>`
(channel index). Exported `rockchip_mbox_read_msg()` (`EXPORT_SYMBOL_GPL`,
`rockchip-mailbox.c:108-125`) pulls the last `{cmd,data}` pair out for a client.
Only OF match in the driver: `"rockchip,rk3368-mailbox"``.num_chans = 4`.
### HPMCU / SCR1 facts (from our own hardware-validated work)
- SCR1 = Syntacore RV32IMC, machine-mode only, 16KB unified cache, "HPMCU".
- **On our board the SCR1 is idle from reset** (non-TB `RV1106MINIALL.ini`, no
`Hpmcu=` loader stage) — a clean slate, not even the stock camera-AE blob.
- **Usable MCU code SRAM is a hard 8KB**: `hpmcu_sram` at offset `0x3e000` inside
`system_sram@ff6c0000` → absolute **`0xFF6FE000`, 8KB** (`reg=<0x3e000 0x2000>`,
`rv1106.dtsi:1149-1151`), shared region with 248KB `rkisp_sram`. Our firmware +
mailbox echo handler must fit this.
- **Proven load/release sequence** (`flared/src/hpmcu.rs:26-76`, hardware-validated):
`CORECRU_CORESOFTRST_CON01` (`0xff3b8000+0xa04`) hold `0x1e001e` → write firmware
to `0xff6fe000``SGRF_HPMCU_BOOT_ADDR` (`0xff076000+0x44`) = load addr →
release CORECRU to `0x1e0000`. This is the WiFi-independent way we start the core
with our own entry point — the piece source-level analysis flagged as unknown is
already solved and proven here.
- **MCU-side reference to copy:** `hal_mbox.c`/`hal_mbox.h` (generic 4-channel
register driver, `HAL_MBOX_Init`/send/ack) + working example
`battery-ipc/stream.c:380-425` (sends `struct MBOX_CMD_DAT {cmd,data}` over
`MBOX_CH_1`, client name `"mcu-status"` — exactly matches the Linux
`struct rockchip_mbox_msg`).
### Current comms mechanism (what exists today, keep it)
WardenOS's watchdog uses a **software mailbox in shared SRAM**, not the mailbox IP:
last 256 bytes of `hpmcu_sram` at absolute base **`0xFF6FFF00`** — `+0x00 magic`
(Linux: `WARD`/`DISA`), `+0x04 counter` (heartbeat), `+0x08 mcu_state`
(`BOOT/ARMD/DISA/FIRE`), `+0x0c`/`+0x10` debug. 5s heartbeat, fires after 90s
no-advance. CRU reset via `GLB_SRST_FST` at **`0xff3b0c08`** magic `0xfdb9` (the
only working whole-SoC reset — `reboot -f` is a no-op: no PSCI/restart handler).
Reset constant is CI-guarded across 4 sites. **`dd of=/dev/mem` faults for the SRAM
region on this ARM kernel — only the `mmap()` path writes** (busybox `devmem` or
`libc::mmap`); `STRICT_DEVMEM` is off in our config.
### The `0x40000` boot-load hazard (load-bearing — for any _boot-time_ MCU path only)
Boot-loading firmware to the DDR carve-out `0x40000` (240KB) **bricked warden-c8a3
on 2026-08-23** because our non-TB kernel DT does **not** reserve `0x40000` — it
collides with kernel RAM → early-boot hang before eth0. **This mailbox plan avoids
the hazard entirely** by using the runtime SRAM-load path (`0xff6fe000`), not a
boot-time DDR load. If a boot-time path is ever pursued, a `reserved-memory` DT node
for `0x40000/0x3c000` must be added and verified via `/proc/iomem` **before**
flashing the idblock. (Standing memory: `boot-loaded-mcu-0x40000-hazard.md`
worth promoting into `riscv-mcu.md` open-questions; not yet captured there.)
### URLs / upstream status
- `drivers/mailbox/rockchip-mailbox.c` — upstream mainline v6.18 (Bootlin Elixir
confirms), binds via `rk3368-mailbox` fallback, zero patch.
- No `drivers/rpmsg/rockchip*` and no `drivers/remoteproc/rockchip*` in mainline
(GitHub API enumeration of both dirs — none Rockchip). Vendor `rockchip_rpmsg.c`
never merged; `lore.kernel.org` 403-walled, submission history unpinnable.
- Community RV1106 HPMCU prior art beyond LED-blink: **~zero** (GitHub search
`rv1106 hpmcu` / `rv1106 scr1 coprocessor` → only SDK/rkbin mirrors). ADR-0002:
"expect to be first." Our PR #25 firmware is the only known custom SCR1 code.
---
## 3. The simplest fully-open, hardware-verifiable path (milestones)
### M-MBOX-1 — Controller wiring proof (Tier 1: today, ZERO HPMCU risk, no firmware)
Proves DT status/clock/IRQ/probe/send all work on real hardware without touching
the SCR1 at all.
1. Kernel config: `CONFIG_ROCKCHIP_MBOX=y` + `CONFIG_MAILBOX_TEST=y` (mainline
driver, **no patch**`rockchip-mailbox.c` binds on the fallback compatible).
2. Board DTS: override `&mailbox { status = "okay"; };` (the `@ff5c0000` instance —
main CRU clock, `GIC_SPI 1`, no PMU-domain complications). Add a
`compatible = "mailbox-test"` node with `mboxes = <&mailbox N>` on a **free
channel — avoid channel 1** (vendor reserves it for `"amp-rx"` semantics).
3. **Verify on serial console** (via the `warden-c8a3` `_b`-slot one-shot loop):
confirm `rockchip-mailbox` probes clean in `dmesg`; write an 8-byte `{cmd,data}`
pair to the debugfs `message` file (`mailbox-test.c`); then read back the
physical `A2B_CMD(N)`/`A2B_DAT(N)` registers directly with busybox `devmem` at
`0xff5c0008+8N` / `0xff5c000c+8N` and confirm the value landed. **Proves the AP→
controller send path end-to-end, serial-only, no risk to the HPMCU.**
### M-MBOX-2 — Real round-trip: SCR1 echo + Linux client (Tier 2: the deliverable)
The actual "message round-trip over serial on hardware" the task asks for.
1. **SCR1 echo firmware** (additive to our existing bare-metal firmware, must fit
the 8KB `hpmcu_sram` budget): on A2B IRQ (or a tight poll of `A2B_STATUS` — poll
is simpler and safe for a bring-up echo, avoids MCU IRQ-controller setup), read
`A2B_CMD(x)`/`A2B_DAT(x)`, write them back to `B2A_CMD(x)`/`B2A_DAT(x)`, set the
`B2A_STATUS` bit, clear `A2B_STATUS`. Copy the register sequence from vendor
`hal_mbox.c` / `battery-ipc/stream.c`. Load and start the core with the
**proven `hpmcu.rs` load/release sequence** (SRAM path, `0xff6fe000`**not**
the `0x40000` boot path, hazard §2).
2. **Linux client**: a minimal in-tree `mbox_client` (stock `mailbox_client.h`:
`mbox_request_channel_byname` / `mbox_send_message` + an rx callback reading via
the exported `rockchip_mbox_read_msg()`), or — to avoid writing a kernel client
for the first proof — reuse `mailbox-test`'s debugfs send and read the B2A
registers from userspace with `devmem`. Prefer the `mailbox-test` route for
M-MBOX-2's first light; promote to a real `mbox_client` once the round-trip is
green.
3. **Verify on serial console** (`warden-c8a3`, `_b`-slot): send a known
`{cmd,data}` (e.g. `{0x1234, 0xcafef00d}`) from Linux; confirm the SCR1 echoed
it back — either via the client's rx callback logging the received pair, or by
`devmem` reading `B2A_CMD(N)`/`B2A_DAT(N)` (`0xff5c0030+8N`/`0xff5c0034+8N`) and
matching it byte-for-byte against what was sent. Capture the serial transcript
as the evidence. **This is the 100%, verified, fully-open mailbox link.**
**Risk controls:** all boot testing via the `_b`-slot one-shot loop (never touches
the working `_a` slot, auto-reverts on hang). The SRAM-load path sidesteps the
`0x40000` brick hazard. The existing SRAM watchdog keeps running throughout,
independent of the mailbox, as it did during the 2026-08-14 validation.
### M-MBOX-3 — (Optional, later) richer channel
Only if a real multi-message need appears: layer a small ring buffer in
`hpmcu_sram`, mailbox used as the doorbell ("look at address X"). Still **do not**
adopt rpmsg/virtio unless the payload complexity genuinely demands it.
---
## 4. Risks / open questions
- **SCR1 IRQ setup** — for M-MBOX-2 a polled echo avoids configuring the MCU's
interrupt controller; fine for bring-up. IRQ-driven B2A on the Linux side works
regardless (that's the controller's job). Note as a simplification, not a gap.
- **Channel choice** — use a channel ≠ 1 (ch1 = vendor `"amp-rx"` reservation).
- **`pmu_mailbox@ff378000` purpose** — undocumented, no consumer anywhere; leave
disabled, do not use, until a TRM section or Rockchip engineer clarifies.
- **RS-485 UART reachability from the HPMCU** — never checked (pinmux vs Linux
ownership); irrelevant to the mailbox link but open for future MCU apps.
- **Whether Rockchip-official (non-RE) MCU/mailbox docs exist under NDA/partner
access** — never asked Luckfox support directly; worth a query.
- **`mcutool.c` exact `/dev/mem` sequence** — inferred, not read line-by-line;
moot, since our `hpmcu.rs` is an independent hardware-validated reimplementation.
---
_Cross-refs: `../CAPABILITIES-AUDIT.md:30`, `../REMAINING-PORTS.md §7`,
`../../luckfox-pico-86-panel/riscv-mcu.md`,
`.../raw/followup-riscv-mcu.md`,
`flare-edge/major-app-additions/docs/decisions/0002-hpmcu-watchdog.md`,
`flare-edge/major-app-additions/flared/src/hpmcu.rs`,
`flare-edge/major-app-additions/hpmcu/watchdog/main.c`,
`flare-edge/sdk/.../drivers/mailbox/rockchip-mailbox.c`,
`flare-edge/sdk/.../mcu/rt-thread/.../hal/lib/hal/src/hal_mbox.c`,
`battery-ipc/stream.c`. Standing memory: `boot-loaded-mcu-0x40000-hazard.md`._
@@ -0,0 +1,303 @@
# Open NPU to 100% — plan, feasibility, and the honest ceiling
**Goal as stated:** take the RV1106 NPU to "100% — open source, verified on
hardware." **Bottom line up front:** two very different things live under that
sentence, and only one of them is reachable soon.
1. **Open, on-hardware-verified _kernel driver_** (`/dev/dri/cardN` binds, answers
a version-query ioctl on our self-built Linux 6.18.46). **Achievable now**,
low-single-digit engineer-days, license-clean GPL forward-port. This is the
`PORT-PLAN.md` work in this directory — a real, bounded milestone (M6).
2. **Open _userspace_ that runs a real model** (an open compiler/encoder emitting a
valid `regcmd` stream this driver can submit, no closed `librknnrt`, no `.rknn`
blob). **Not achievable on any near horizon.** It is a from-scratch,
per-SoC register reverse-engineering project with **zero existing public prior
art for RV1106/RV1103** — the least-covered tier of the entire RKNPU family.
This document scopes both, states the most ambitious end state that is actually
achievable, and defines the concrete first milestone toward open compute.
`PORT-PLAN.md` remains the authoritative, file-by-file plan for item (1); this
document is the strategic wrapper and the item-(2) reality check. Read both.
---
## 1. Brutally honest feasibility
### 1.1 "Port the GPL kernel driver" — TRACTABLE (do it)
- The vendor driver (`flare-edge/sdk/sysdrv/source/kernel/drivers/rknpu/`, v0.9.2,
`DRIVER_DATE 20230825`) is **SPDX GPL-2.0 at the file level**, `MODULE_LICENSE("GPL v2")`
(`rknpu_drv.c:2176`), authored by Rockchip (Felix Zeng), mirrored at
`github.com/airockchip/rknpu`. Legitimately open, cleanly forward-portable.
- It is a **single multi-SoC codebase that already targets RV1106 natively**:
`of_match` entry `{ .compatible = "rockchip,rv1106-rknpu", .data = &rv1106_rknpu_config }`
(`rknpu_drv.c:196-198`) + a dedicated config struct (`rknpu_drv.c:143-160`:
`dma_mask=32`, `pc_data_amount_scale=2`, `pc_task_number_bits=16`,
`pc_task_status_offset=0x3c`, single-core `irqs`/`resets` arrays,
`nbuf_phyaddr=0/nbuf_size=0`).
- The DT node is **already fully specified** in the base tree, only disabled —
`npu@ff660000`, `reg=<0xff660000 0x10000>`, `GIC_SPI 109`, clocks
`ACLK_RKNN`/`HCLK_RKNN`, `assigned-clock-rates=<420000000>`, resets
`SRST_A_RKNN`/`SRST_H_RKNN`, `status="disabled"` (`rv1106.dtsi:1127-1138`). One
board-DTS override (`&npu { status="okay"; }`) enables it.
- **No IOMMU** on this board (`"rknpu iommu device-tree entry not found!, using
non-iommu mode"` — CMA/contiguous DMA only), **no power-domains** (single-rail),
**no OPP table** (fixed clock). This _shrinks_ the port: the multi-domain/DVFS/
thermal code is all provably dead for RV1106's DT.
- The one real build blocker is four vendor-only downstream headers
(`soc/rockchip/rockchip_{iommu,opp_select,system_monitor,ipa}.h`) absent from
mainline 6.18 — all four back **dead-code call sites** for this SoC, fixable with
small local compat-shim stubs (same pattern already used for `clk-rv1106.c`'s
`panic_notifier_list` move). Full delta in `PORT-PLAN.md §2.4`.
**Verdict: real, bounded, worth doing.** Same class as the RGA port. Ends at a
clean probe + `RKNPU_GET_DRV_VERSION`/`RKNPU_GET_HW_VERSION` ioctl round-trip on
hardware — **and stops there**, because of §1.2.
### 1.2 "Open userspace that runs a model" — HARD → effectively BLOCKED for RV1106
The kernel driver's entire hardware contract is: power/clock/reset, DMA/GEM buffer
management, then it drops a **pointer + length to a userspace-authored `regcmd`
blob** into four registers and pulses "go," then fields one IRQ. **It never
inspects the regcmd contents.** (Traced through `rknpu_job.c:266-364`,
`rknpu_job_subcore_commit_pc()`.)
The four PC registers the driver programs per task (offsets into the 0xFF660000
block, from `rknpu_ioctl.h:21-39` used via `REG_WRITE` in `rknpu_job.c`):
| Offset | Name | Meaning |
|---|---|---|
| `0x00`/`0x04` | VERSION / VERSION_NUM | read-only HW version |
| `0x08` | PC_OP_EN | pulse 1→0 kicks the job; `0x1`=slave mode before commit |
| `0x10` | PC_DATA_ADDR | device address of the regcmd buffer |
| `0x14` | PC_DATA_AMOUNT | `(regcfg_amount + 4 + scale-1)/scale - 1` |
| `0x20`/`0x24`/`0x28`/`0x2c` | INT_MASK / INT_CLEAR / INT_STATUS / INT_RAW_STATUS | interrupt handshake |
| `0x30` | PC_TASK_CONTROL | `((0x6\|pingpong)<<pc_task_number_bits)\|task_number` (the `0x6` is an undecoded fixed mode field) |
| `0x34` | PC_DMA_BASE_ADDR | `task_base_addr` |
**This is the entire openly-documented register map** — the wrapper/sequencer
only. **None of it describes the compute engine** (convolution/pooling/activation
config, feature/weight DMA descriptors). Those live _inside_ the opaque regcmd
buffer. The op-code semantics of that stream:
- **Have no public register-level documentation anywhere.** TRM Part 2 does not
exist publicly — confirmed by exhausting Rockchip doc mirrors and confirmed
directly by a Luckfox engineer that **Luckfox itself was never given one.** Even
Rockchip's own 90-page ROCKIVA SDK guide is pure userspace-API reference with
zero register offsets. This is a deliberate architectural gate, not a
hard-to-find doc.
- **Only the closed RKNN-Toolkit2 compiler emits valid regcmd streams.** The
on-device closed `librknnrt` parses `.rknn` blobs and issues the same
`RKNPU_MEM_*`/`RKNPU_SUBMIT` ioctls. `DRM_IOCTL_RKNPU_SUBMIT` technically accepts
a raw command buffer directly — nothing _requires_ `librknnrt` — but since the
buffer format is closed, that ioctl is **not** a "write your own kernel" door.
### 1.3 Why there is no shortcut through the existing open efforts
| Open effort | Chip(s) | Reaches RV1106? |
|---|---|---|
| Mainline `accel/rocket` (merged 2025-07, in our `research/linux-6.18.46/`) | RK3588 only, as shipped | **No.** `Kconfig`: `depends on (ARCH_ROCKCHIP && ARM64)`. RV1106 is 32-bit `arch/arm` — excluded before the register question. `of_match` is hardcoded `"rockchip,rk3588-rknn-core"`. |
| Mesa **Teflon** (TFLite delegate) | Only etnaviv or rocket backends | **No.** Entirely downstream of Rocket — its Rockchip reach _is_ Rocket's reach: RK3588 only. |
| `gahingwoo/linux-rk3576-npu` + `charsiu` (active, days-old as of 2026-08) | RK3576 | **No — wrong generation.** MobileNetV1 end-to-end + a Llama-3.2-1B INT4 open runtime (`charsiu`, opens `/dev/accel/accel0`, emits its own register stream) reportedly work byte-exact — but on RK3576's NPU, a **newer generation** than RV1106's. Unmerged (LKML "[PATCH v9] accel/rocket: RK3576" in review; unverifiable by second source — Anubis-walled). |
| RK3568/RK3566 Armbian fork | RK3568 | **No.** Built by byte-level diff against captured vendor command streams; multi-task still imperfect; unmerged. |
| `phhusson/rknpu-reverse-engineering` | RK3588 only, dormant since 2024-03, no license | **No.** Exploratory `strings`/`strace`/GEM-dump notes; **no compiler, no encoder, no runtime.** ~1 person-year got RK3588 to "structures identified." |
| **RV1106/RV1103** | — | **Zero hits of any kind.** No driver, no fork, no RE writeup, no captured traces. Least-covered tier in the family. |
RV1106 sits in the RKNPU2 **"v1" family** (RK3566/68, RV1109/26, RV1103/06 —
INT8-only, `quantize=8` mandatory). RK3576/RK3588 are a **different, newer**
generation. Precedent (RK3568 needing byte-level RE off RK3588) shows this is a
**bespoke per-SoC RE project even between adjacent generations, not a recompile.**
### 1.4 The most ambitious _achievable_ end state
Ranked by realism:
- **Tier A — ACHIEVABLE NOW (commit to this):** open GPL kernel driver, statically
built into our 6.18.46, `/dev/dri/cardN` binds, `/proc/rknpu/load` live for the
Monitor page, `RKNPU_GET_HW_VERSION`/`RKNPU_GET_DRV_VERSION` verified on
hardware. **No blob shipped.** This is item (1); it is the honest "100% open" for
the _driver_.
- **Tier B — ACHIEVABLE AS A RESEARCH SPIKE, bounded, low-value:** a **single
hand-written op** (one conv or one matmul) executed via a `regcmd` **captured**
from the closed stack and **replayed** through our open `DRM_IOCTL_RKNPU_SUBMIT`,
byte-for-byte, with an open host tool — proving the open submit path can drive
real compute. This does **not** require understanding the regcmd ISA; it requires
capturing one. It is the concrete first milestone toward open compute (§3, M-NPU-2)
and the only way to get an on-hardware compute number without the RE project.
- **Tier C — NOT ACHIEVABLE on any near horizon:** an open **compiler/encoder** that
emits novel regcmd streams for arbitrary models. This is the ~person-year+
from-scratch RE project (§3, M-NPU-3+). We should **scope it, not staff it**,
unless a strategic reason to own open NPU compute emerges.
**Honest recommendation:** ship Tier A. Do Tier B only as a time-boxed spike _if_ a
real non-visual inference use case (audio/RS-485/touch-gesture classifier — the
only sensible NPU use here; no camera exists on this board) is actually wanted.
Treat Tier C as closed-vendor-runtime-only for the foreseeable future. This matches
the standing `PROVENANCE.md` policy: **we do not ship the closed runtime blob**;
the open path is a from-scratch regcmd encoder tracked separately, never a vendored
binary — and until that encoder exists, open NPU _compute_ does not exist for us.
---
## 2. Key technical facts (for the implementer)
### Register / memory map (TRM Part1 — Part2 does not exist publicly)
| Block | Base | Size |
|---|---|---|
| NPU core | `0xFF660000` | 64KB (`reg=<0xff660000 0x10000>`) |
| NPU_GRF | `0xFF018000` | 32KB |
| NPU_SGRF | `0xFF074000` | 8KB |
| NPU_CRU | `0xFF3B6000` | 8KB |
| NPU_CBUF (scratch SRAM) | `0xFF680000` | 256KB (unused: config sets `nbuf=0`) |
IRQ `GIC_SPI 109 LEVEL_HIGH`, node `ff660000.npu`. Clocks `ACLK_RKNN`/`HCLK_RKNN`
off the CRU matrix; only measured clock point anywhere is **NPU 500MHz** (Rockchip
internal power-test xlsx, "typical IPC workload" corner). A commented-out board-DTS
override would set 700MHz — never applied.
### UAPI (the seam an open userspace must target)
Two memory-manager paths, Kconfig-selected. **RV1106's shipping defconfig picks
`CONFIG_ROCKCHIP_RKNPU_DMA_HEAP=y`** (`/dev/rknpu` misc device, bare `IOCTL_RKNPU_*`
codes, magic `'r'`) — but `PORT-PLAN.md` deliberately chooses **DRM_GEM** for the
6.18 port (`/dev/dri/cardN`, `DRM_IOCTL_RKNPU_*`, `DRM_RENDER_ALLOW`), because M4
already pulls DRM in for the panel and DRM_GEM is the vendor default. Same struct
payloads either way. Six ioctls: `RKNPU_ACTION`, `RKNPU_SUBMIT`,
`RKNPU_MEM_{CREATE,MAP,DESTROY,SYNC}` (`rknpu_ioctl.h:288-322`).
```c
struct rknpu_task { // rknpu_ioctl.h:218-228
__u32 flags, op_idx, enable_mask;
__u32 int_mask, int_clear, int_status;
__u32 regcfg_amount, regcfg_offset;
__u64 regcmd_addr; // <-- points at the OPAQUE compute program
} __packed;
struct rknpu_submit { // rknpu_ioctl.h:260-274
__u32 flags, timeout, task_start, task_number, task_counter;
__s32 priority;
__u64 task_obj_addr, regcfg_obj_addr, task_base_addr, user_data;
__u32 core_mask; __s32 fence_fd;
struct rknpu_subcore_task subcore_task[5];
};
```
An open userspace must produce: **(a) the `struct rknpu_task[]` list — fully
inferable from the kernel driver alone, trivial;** and **(b) the regcmd byte stream
each task points to — NOT inferable from the driver or any published TRM.** (b) is
the whole problem.
### `/proc/rknpu` — safety (hardware-confirmed)
- `/proc/rknpu/load` — **safe** (50 clean reads verified); the Monitor page polls it.
- `/proc/rknpu/freq`, `/power` — exposed, unverified.
- `/proc/rknpu/volt` — **CONFIRMED SIGSEGV** (null deref — no regulator wired). Never poll.
### Provenance / policy
`PROVENANCE.md`: the kernel driver is portable GPL; the closed piece is the
userspace RKNN runtime + regcmd format (a blob). **Per directive we do not ship
that blob.** `CAPABILITIES-AUDIT.md:32` rates NPU "not worth shipping" until an
open encoder exists.
### URLs
- Vendor driver: `github.com/airockchip/rknpu`, `github.com/rockchip-linux/rknpu`
- Mainline Rocket: `drivers/accel/rocket/` (RK3588-only); blog:
`blog.tomeuvizoso.net/2025/07/rockchip-npu-update-6-we-are-in-mainline.html`
- Teflon: `docs.mesa3d.org/teflon.html`; NLnet: `nlnet.nl/project/Rockchip-NPU-driver/`
- RK3576 RE: `github.com/gahingwoo/{linux-rk3576-npu,charsiu,kiln}` (active, unmerged)
- RK3588 RE: `github.com/phhusson/rknpu-reverse-engineering` (dormant, no license)
- Official closed docs (RV1106/RV1103 quick-start PDFs, API-only): `github.com/airockchip/rknn-toolkit2` `doc/`
---
## 3. Milestones
### M-NPU-1 — Open kernel driver, verified on hardware (ACHIEVABLE — the real deliverable)
Follows `PORT-PLAN.md` in full. Summary of the gates:
1. **Build**: `CONFIG_ROCKCHIP_RKNPU=y` + `_DRM_GEM=y` + `_DEBUG_FS`/`_PROC_FS`;
compile `rknpu_{drv,job,gem,reset,iommu,debugger}.o` clean into `built-in.a`.
Prove the four §2.4 compat shims (`compat/soc/rockchip/rockchip_{iommu,opp_select,
system_monitor,ipa}.h`) against real 6.18 headers — this is where they get
verified, not just inspected.
2. **DT**: board override `&npu { status="okay"; }`; `dtc -W` clean; re-check the
"place the fdt high" DTB-placement fix (bigger `built-in.a`).
3. **Boot** via the `warden-c8a3` `_b`-slot one-shot loop (never touches `_a`,
auto-reverts on hang): `dmesg | grep -i rknpu` shows clean probe — clock/reset/
IRQ acquired, no panic, no `-EPROBE_DEFER` stall.
4. **Node**: `/dev/dri/cardN` (or `renderD1xx`) appears — classic DRM node, **not**
`/dev/accel/`.
5. **Verify ioctl**: host-buildable C program opens the node, issues
`DRM_IOCTL_RKNPU_ACTION {.flags=RKNPU_GET_DRV_VERSION}`, checks `value` decodes
to `0.9.2`; `RKNPU_GET_HW_VERSION` returns something plausible. Exercises full
dispatch → power-get/put → clock/reset with no regcmd dependency.
6. **Capture the durable delta** as a patch series in this dir (compat shims + DT
fragment + config fragment); vendor source stays in `research/linux-6.18.46/`.
**Effort:** low-single-digit engineer-days. **Risk:** a surprise vendor-only symbol
not surfaced by the read-through; DTB placement. **This is "NPU driver = 100% open,
verified."** Done here for the driver half.
### M-NPU-2 — First open compute: capture-and-replay one op (SPIKE, only if wanted)
The concrete first milestone toward open _compute_, and the honest Tier-B ceiling.
It does **not** require decoding the ISA.
1. On a unit with the closed stack available (build host / a dev panel with the
closed `librknnrt` + a trivial single-conv or single-matmul `.rknn`), capture
the regcmd buffer(s) the runtime DMAs in — via `strace` of the `RKNPU_MEM_*`/
`RKNPU_SUBMIT` ioctls + `GEM_FLINK`/`GEM_OPEN` buffer dumps (phhusson's method),
plus the input/output tensor buffers.
2. Write an **open host tool** that: allocates the same GEM buffers via
`RKNPU_MEM_CREATE`, writes the captured regcmd bytes + captured input tensor,
builds the `struct rknpu_task[]` (fully open, §2), and submits via
`DRM_IOCTL_RKNPU_SUBMIT` on our M-NPU-1 driver.
3. **Verify**: output tensor matches the closed runtime's output **byte-for-byte**
on hardware, and matches a CPU/NEON reference for the same op. Record the
**measured job-submission latency** (ioctl+DMA+blocking-IRQ) — a number that
exists nowhere today and settles the "NPU vs NEON dispatch overhead" argument.
**Deliverable:** proof the open submit path drives real compute + first latency
number. **Not** a general runtime — the regcmd is a fixed captured blob for one op
shape. **Value:** low; do only as a time-boxed research spike behind a real
use case. **Never ship a captured Rockchip regcmd as a product artifact** (it is
their compiler's output) — this stays in `research/`.
### M-NPU-3+ — Open regcmd encoder (SCOPE ONLY — do not staff without a strategic reason)
The Tier-C from-scratch RE project. Method (the only demonstrated one):
differentially decode captured regcmd streams across many hand-built minimal ONNX
models (isolate one op/param at a time), reconstruct the compute-engine register
semantics, build an encoder for a first op, then grow the op set. Precedent cost:
~1 person-year got RK3588 to "exploratory, no compiler"; RK3568 needed byte-level
diffing and still isn't merged; **RV1106 starts from literal zero.** Track here if
ever begun; otherwise this is the documented reason open NPU compute is deferred.
---
## 4. Risks / open questions
- **The core risk is misframing.** "The driver is open source" must not be sold as
"the NPU is open-source-usable." M-NPU-1 delivers an open driver that can submit
jobs; it delivers **no open way to produce a valid job.** Guard this in every
status line.
- Additional vendor-only symbols beyond the four headers may surface only at
compile — M-NPU-1 gate 1 is the real test.
- **SKU unconfirmed** (G2 128MB / 0.5-TOPS vs G3 256MB / 1.0-TOPS; datasheet Rev 2.0
retracted the split to "both 1.0 TOPS"). Determines the compute ceiling if
M-NPU-2/3 are ever pursued; needs an on-hardware chip-ID/TOPS register read.
- **No open regcmd project for RV1106 exists** (re-confirm periodically — community
moves fast, esp. the `gahingwoo` RK3576 work; but it is the wrong generation).
- **No NPU use case is currently established for this board** (no camera; RGA2 +
RKIVE already cover the 2D/CNN-shaped image tasks better — see
`npu-graphics-feasibility.md`). Absent a concrete non-visual classifier need,
M-NPU-2 and beyond have no pull, and M-NPU-1 alone (driver binds, load graph
works, no blob) is the correct stopping point.
---
_Cross-refs: `PORT-PLAN.md` (authoritative file-by-file kernel port),
`../../docs/npu-graphics-feasibility.md`, `../CAPABILITIES-AUDIT.md:32`,
`../PROVENANCE.md`, `../DRIVER-PARITY.md:41`, `../REMAINING-PORTS.md §6`._
@@ -0,0 +1,26 @@
# PVTM (Process-Voltage-Temperature Monitor) — ✅ VERIFIED on warden-c8a3 (2026-08-25)
Whole-driver port: mainline 6.18 has **no** rockchip pvtm driver; the vendor
`drivers/soc/rockchip/rockchip_pvtm.c` (GPL-2.0, 1046L) supports rv1106. Copied it in
+ `include/linux/soc/rockchip/pvtm.h`; Kconfig `ROCKCHIP_PVTM` + Makefile; `=y`.
## API-delta fixes (6.18)
- `struct thermal_zone_device` is now opaque → replaced `pvtm->tz->ops->get_temp(...)`
with the public `thermal_zone_get_temp(pvtm->tz, &cur_temp)`.
- added `#include <linux/of_device.h>` for `of_match_device()`.
- copied the vendor-only header `linux/soc/rockchip/pvtm.h`.
## The real blocker (why it first probed silently)
The vendor of_match_table wraps the rv1106 entries in `#ifdef CONFIG_CPU_RV1106` — a
vendor per-SoC symbol that does **not exist** in mainline. Result: devices
(`ff240000.pvtm`, `ff390000.pvtm`) were created + the driver registered, but the
compatibles were compiled out of the match table, so nothing bound and probe never
ran (no dmesg at all). **Fix: drop the `#ifdef CONFIG_CPU_RV1106` guard** — our tree
only builds for rv1106, so the entries are unconditional. (Watch for this guard in any
other vendor driver ported by verbatim copy.)
## Evidence
`rockchip-pvtm ff240000.pvtm: pvtm@0 probed` + `ff390000.pvtm: pvtm@0 probed`;
`/sys/kernel/debug/pvtm/core/value = pvtm: 71682 90462`,
`/sys/kernel/debug/pvtm/pmu/value = pvtm: 35772` (ring-oscillator counts, the PVT
signal). Exports `rockchip_get_pvtm_value()` for future DVFS voltage margining.