* [PATCH v3 0/7] Add Allwinner A523 message box and XuanTie E906 remoteproc support
@ 2026-11-01 18:57 Tim Michals
2026-11-01 18:57 ` [PATCH v3 1/7] dt-bindings: mailbox: Add Allwinner A523 message box Tim Michals
` (6 more replies)
0 siblings, 7 replies; 12+ messages in thread
From: Tim Michals @ 2026-11-01 18:57 UTC (permalink / raw)
To: Jassi Brar, Bjorn Andersson, Mathieu Poirier, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Chen-Yu Tsai, Jernej Skrabec,
Samuel Holland
Cc: linux-sunxi, linux-remoteproc, devicetree, linux-arm-kernel,
linux-kernel, Tim Michals
This series adds a driver for the message box of the Allwinner A523,
A527 and T527 (sun55i) and a remoteproc driver for their T-Head XuanTie
E906 RISC-V core, with bindings, DT nodes and KUnit tests.
v2: https://lore.kernel.org/linux-sunxi/20260927002021.797069-1-tcmichals@gmail.com/
Changes in v3 (details in each patch):
- Message box: all four banks are clocked and out of reset (an
unclocked bank drops writes). The FIFO is drained before its pending
bit is cleared, and messages are delivered after the controller lock
is dropped. A kick is done once the receiver's FIFO has room; a full
FIFO is never written, and in poll mode the check and the write are
under one lock. The shared reset is never asserted, as in
sun6i-msgbox: the other banks carry traffic between the other
processors. allwinner,tx-done-irq selects the receiver bank's write
interrupt instead of the 1 ms poll. OMAP's ti,mbox-send-noirq is the
same kind of per-mailbox switch, in the other direction: it turns
the TX interrupt off for the WkupM3.
- Remoteproc: one mailbox channel; the receive callback is deferred to
an ordered workqueue; address translation covers only the two SRAM
windows; debug reset and power domains added; all register regions,
clocks and resets are required, the mailbox is optional. Carveouts
and crash dump segments are registered in parse_fw(), and crash
dumps contain both SRAMs and the carveouts.
- Bindings: fixed lists for reg, clocks, resets and interrupts;
"schema" dropped from the subjects (Krzysztof). All four message box
interrupts are required: every A523 has them. The DSP and CPUS
message box bank addresses were swapped in v2 and are fixed. The
SRAMs are named as in the T527 manual (sram-a3-0, sram-a3-1).
- KUnit patches moved after the drivers. Both suites redirect register
access with static stubs to mocks, so they run on any architecture,
UML included. Patches 6 and 7 only add tests and can be dropped or
taken separately.
- Rebased on next-20261030.
Notes for review:
- The remap register (0x07010364, PRCM) is shared with the DSP: bit 1
shares the 512 KiB SRAM A3_2 with the E906's subsystem, bit 0
belongs to the DSP. The driver maps only this register and
read-modify-writes bit 1, which is safe while no other Linux driver
writes it. I can move it to a syscon or the R-CCU, or describe SRAM
A3_2 through the sunxi SRAM driver (allwinner,sram), if preferred.
- memory-region index 0 is the virtio DMA pool, then dram and trace,
as in the TI K3 and MediaTek SCP bindings.
- The optional crash interrupt is supported by the driver but not
wired in any DT and not tested on hardware; the A523 has no
dedicated line for it and the vendor BSP has none.
- System suspend is not supported: the E906 runs next to a Linux that
stays up, so the driver has no PM callbacks.
- Merging: patches 1, 2 and 6 are for the mailbox tree, 3, 4 and 7
for remoteproc, and 5 for sunxi. Patch 5 needs the bindings in
patches 1 and 3.
Testing on a Radxa Cubie A5E (A527), Linux 7.1 PREEMPT_RT and PREEMPT
with the same driver and test files:
- KUnit: 37/37 message box, 26/26 remoteproc, built in and as
modules. Under UML on x86_64 one message box test is skipped (it
needs SMP or spinlock debugging).
- rpmsg ping (10000 messages, about 167 us round trip) and stress
(100000 messages, 16 in flight, no loss), with vrings in DRAM and in
SRAM, TX done by poll and by write interrupt, and message box
unbind/bind with the remoteproc driver unbound. One interrupt per
message.
- The same tests with PROVE_LOCKING and DEBUG_ATOMIC_SLEEP (and
PROVE_RAW_LOCK_NESTING on PREEMPT): no lockdep or sleep-in-atomic
report.
- On next-20261030: W=1 build of each patch with GCC and clang, built
in and as modules, dt_binding_check and dtbs_check are clean;
checkpatch --strict reports only "does MAINTAINERS need updating?"
for the new files, which the entries added in patches 2 and 4 cover.
Tools: I used Claude Code (Anthropic) to help review the code against
the v2 comments, write the KUnit tests, run the board tests and draft
commit messages. I reviewed every change; all tests above ran on my
board.
Thanks to Krzysztof Kozlowski, Chen-Yu Tsai and Rob Herring for
reviewing v1 and v2.
Tim Michals (7):
dt-bindings: mailbox: Add Allwinner A523 message box
mailbox: sun55i: Add Allwinner A523 message box driver
dt-bindings: remoteproc: Add Allwinner A523 RISC-V remote processor
remoteproc: sunxi: Add Allwinner A523 XuanTie E906 driver
arm64: dts: allwinner: a523: Add message box and remoteproc nodes
mailbox: sun55i: Add KUnit tests
remoteproc: sunxi: Add KUnit tests
.../mailbox/allwinner,sun55i-a523-msgbox.yaml | 146 ++
.../allwinner,sun55i-a523-rproc.yaml | 146 ++
MAINTAINERS | 15 +
.../arm64/boot/dts/allwinner/sun55i-a523.dtsi | 47 +
drivers/mailbox/Kconfig | 24 +
drivers/mailbox/Makefile | 3 +
drivers/mailbox/sun55i-msgbox.c | 613 ++++++++
drivers/mailbox/sun55i-msgbox.h | 95 ++
drivers/mailbox/sun55i_msgbox_kunit.c | 1387 +++++++++++++++++
drivers/remoteproc/Kconfig | 26 +
drivers/remoteproc/Makefile | 2 +
drivers/remoteproc/sunxi_rproc.c | 855 ++++++++++
drivers/remoteproc/sunxi_rproc.h | 124 ++
drivers/remoteproc/sunxi_rproc_kunit.c | 1314 ++++++++++++++++
14 files changed, 4797 insertions(+)
create mode 100644 Documentation/devicetree/bindings/mailbox/allwinner,sun55i-a523-msgbox.yaml
create mode 100644 Documentation/devicetree/bindings/remoteproc/allwinner,sun55i-a523-rproc.yaml
create mode 100644 drivers/mailbox/sun55i-msgbox.c
create mode 100644 drivers/mailbox/sun55i-msgbox.h
create mode 100644 drivers/mailbox/sun55i_msgbox_kunit.c
create mode 100644 drivers/remoteproc/sunxi_rproc.c
create mode 100644 drivers/remoteproc/sunxi_rproc.h
create mode 100644 drivers/remoteproc/sunxi_rproc_kunit.c
base-commit: 7649ba73f9e7e1e04d0b46213e39c79ae09dd55c
--
2.53.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v3 1/7] dt-bindings: mailbox: Add Allwinner A523 message box
2026-11-01 18:57 [PATCH v3 0/7] Add Allwinner A523 message box and XuanTie E906 remoteproc support Tim Michals
@ 2026-11-01 18:57 ` Tim Michals
2026-11-01 19:06 ` sashiko-bot
2026-11-01 18:57 ` [PATCH v3 2/7] mailbox: sun55i: Add Allwinner A523 message box driver Tim Michals
` (5 subsequent siblings)
6 siblings, 1 reply; 12+ messages in thread
From: Tim Michals @ 2026-11-01 18:57 UTC (permalink / raw)
To: Jassi Brar, Bjorn Andersson, Mathieu Poirier, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Chen-Yu Tsai, Jernej Skrabec,
Samuel Holland
Cc: linux-sunxi, linux-remoteproc, devicetree, linux-arm-kernel,
linux-kernel, Tim Michals
The Allwinner A523, A527 and T527 (sun55i) message box connects the
Cortex-A55 host with the HiFi4 DSP, the CPUS power management core and
the XuanTie E906 RISC-V core. Each processor has its own register bank.
A sender writes a message into the receiving processor's bank, and a
channel has an 8-entry FIFO in each direction.
The host bank has 12 channels, four for each remote processor. The
binding lists all four banks, since the host writes to and reads the
FIFO status of the remote banks, and lists the bank interrupts in the
same order: the host bank read interrupt and the write interrupts of
the three remote banks, which every A523 has. Each bank has its own bus
clock and reset, and an unclocked bank drops writes, so all four are
described. The optional allwinner,tx-done-irq property selects TX
completion by the receiver bank's write interrupt instead of polling.
Signed-off-by: Tim Michals <tcmichals@gmail.com>
---
Changes in v3:
- One compatible, allwinner,sun55i-a523-msgbox: the A523, A527 and T527
are the same die; the A733 entry is dropped
- reg, clocks and resets list all four banks in a fixed order, with
clock-names and reset-names: every bank has its own bus clock and reset
- interrupts and interrupt-names are a fixed list (arm, dsp, cpus, rv),
all four required: every A523 has the four lines, and
allwinner,tx-done-irq only selects how TX done is detected. This
answers the v1 question of how an interrupt can be optional; no
free-form text (Krzysztof)
- Remote bank interrupts in the example corrected to SPI 181/174/205
(T527 manual, Table 2-15; checked on the board)
- reg: the DSP and CPUS bank addresses were swapped in v2
(0x07120000 is the DSP bank, 0x07094000 the CPUS bank)
- Add allwinner,tx-done-irq
- Description rewritten to describe the hardware; "schema" dropped from
the subject (Krzysztof)
- Samuel Holland dropped from the maintainers
---
.../mailbox/allwinner,sun55i-a523-msgbox.yaml | 146 ++++++++++++++++++
1 file changed, 146 insertions(+)
create mode 100644 Documentation/devicetree/bindings/mailbox/allwinner,sun55i-a523-msgbox.yaml
diff --git a/Documentation/devicetree/bindings/mailbox/allwinner,sun55i-a523-msgbox.yaml b/Documentation/devicetree/bindings/mailbox/allwinner,sun55i-a523-msgbox.yaml
new file mode 100644
index 000000000000..47b6fe304dbc
--- /dev/null
+++ b/Documentation/devicetree/bindings/mailbox/allwinner,sun55i-a523-msgbox.yaml
@@ -0,0 +1,146 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/mailbox/allwinner,sun55i-a523-msgbox.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Allwinner A523 Message Box
+
+maintainers:
+ - Tim Michals <tcmichals@gmail.com>
+
+description:
+ The message box on the Allwinner A523, A527 and T527 (sun55i) SoCs links
+ four processors - the Cortex-A55 cluster, the HiFi4 DSP, the CPUS power
+ management core and the XuanTie E906 RISC-V core. Each processor has its
+ own register bank holding 8-entry receive FIFOs for messages from the
+ other three, and a sender writes into the receiver's bank. The host side
+ therefore uses all four banks and exposes 12 channels, 4 per remote
+ processor. Each bank has its own bus clock and reset, and an unclocked
+ bank drops writes, so the host needs all four. A bank's read interrupt
+ goes to the processor that owns it; its write interrupt, raised while the
+ FIFO has room, goes to the sender on a separate line.
+
+properties:
+ compatible:
+ const: allwinner,sun55i-a523-msgbox
+
+ reg:
+ items:
+ - description: Cortex-A55 host bank
+ - description: HiFi4 DSP bank
+ - description: CPUS bank
+ - description: XuanTie RISC-V bank
+
+ reg-names:
+ items:
+ - const: arm
+ - const: dsp
+ - const: cpus
+ - const: rv
+
+ clocks:
+ items:
+ - description: Cortex-A55 host bank bus clock
+ - description: HiFi4 DSP bank bus clock
+ - description: CPUS bank bus clock
+ - description: XuanTie RISC-V bank bus clock
+
+ clock-names:
+ items:
+ - const: arm
+ - const: dsp
+ - const: cpus
+ - const: rv
+
+ resets:
+ items:
+ - description: Cortex-A55 host bank reset
+ - description: HiFi4 DSP bank reset
+ - description: CPUS bank reset
+ - description: XuanTie RISC-V bank reset
+
+ reset-names:
+ items:
+ - const: arm
+ - const: dsp
+ - const: cpus
+ - const: rv
+
+ interrupts:
+ items:
+ - description: Cortex-A55 bank read interrupt, messages for the host
+ - description: HiFi4 DSP bank write interrupt for the host
+ - description: CPUS bank write interrupt for the host
+ - description: XuanTie RISC-V bank write interrupt for the host
+
+ interrupt-names:
+ items:
+ - const: arm
+ - const: dsp
+ - const: cpus
+ - const: rv
+
+ allwinner,tx-done-irq:
+ type: boolean
+ description:
+ Signal TX done with the write interrupt of the receiver's bank instead
+ of polling. A message is done once the receiver's FIFO has room for the
+ next one either way; the write interrupt is raised while the FIFO has
+ room. Without this property a full FIFO is polled. Received messages
+ use the host bank read interrupt either way.
+
+ '#mbox-cells':
+ const: 1
+ description:
+ Host channel number. Channels 0-3 reach the CPUS, 4-7 the DSP and
+ 8-11 the RISC-V core.
+
+required:
+ - compatible
+ - reg
+ - reg-names
+ - clocks
+ - clock-names
+ - resets
+ - reset-names
+ - interrupts
+ - interrupt-names
+ - '#mbox-cells'
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/clock/sun55i-a523-ccu.h>
+ #include <dt-bindings/clock/sun55i-a523-mcu-ccu.h>
+ #include <dt-bindings/clock/sun55i-a523-r-ccu.h>
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+ #include <dt-bindings/reset/sun55i-a523-ccu.h>
+ #include <dt-bindings/reset/sun55i-a523-mcu-ccu.h>
+ #include <dt-bindings/reset/sun55i-a523-r-ccu.h>
+
+ mailbox@3003000 {
+ compatible = "allwinner,sun55i-a523-msgbox";
+ reg = <0x03003000 0x1000>,
+ <0x07120000 0x1000>,
+ <0x07094000 0x1000>,
+ <0x07136000 0x1000>;
+ reg-names = "arm", "dsp", "cpus", "rv";
+ clocks = <&ccu CLK_BUS_MSGBOX>,
+ <&mcu_ccu CLK_BUS_MCU_DSP_MSGBOX>,
+ <&r_ccu CLK_BUS_R_MSGBOX>,
+ <&mcu_ccu CLK_BUS_MCU_RISCV_MSGBOX>;
+ clock-names = "arm", "dsp", "cpus", "rv";
+ resets = <&ccu RST_BUS_MSGBOX>,
+ <&mcu_ccu RST_BUS_MCU_DSP_MSGBOX>,
+ <&r_ccu RST_BUS_R_MSGBOX>,
+ <&mcu_ccu RST_BUS_MCU_RISCV_MSGBOX>;
+ reset-names = "arm", "dsp", "cpus", "rv";
+ interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>,
+ <GIC_SPI 181 IRQ_TYPE_LEVEL_HIGH>,
+ <GIC_SPI 174 IRQ_TYPE_LEVEL_HIGH>,
+ <GIC_SPI 205 IRQ_TYPE_LEVEL_HIGH>;
+ interrupt-names = "arm", "dsp", "cpus", "rv";
+ #mbox-cells = <1>;
+ };
--
2.53.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v3 2/7] mailbox: sun55i: Add Allwinner A523 message box driver
2026-11-01 18:57 [PATCH v3 0/7] Add Allwinner A523 message box and XuanTie E906 remoteproc support Tim Michals
2026-11-01 18:57 ` [PATCH v3 1/7] dt-bindings: mailbox: Add Allwinner A523 message box Tim Michals
@ 2026-11-01 18:57 ` Tim Michals
2026-11-01 19:17 ` sashiko-bot
2026-11-01 18:57 ` [PATCH v3 3/7] dt-bindings: remoteproc: Add Allwinner A523 RISC-V remote processor Tim Michals
` (4 subsequent siblings)
6 siblings, 1 reply; 12+ messages in thread
From: Tim Michals @ 2026-11-01 18:57 UTC (permalink / raw)
To: Jassi Brar, Bjorn Andersson, Mathieu Poirier, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Chen-Yu Tsai, Jernej Skrabec,
Samuel Holland
Cc: linux-sunxi, linux-remoteproc, devicetree, linux-arm-kernel,
linux-kernel, Tim Michals
Add a mailbox controller driver for the sun55i message box. Linux uses
the Cortex-A55 host bank, which has 12 channels: 0-3 reach the CPUS,
4-7 the HiFi4 DSP and 8-11 the XuanTie E906.
A message is sent by writing the remote processor's FIFO for the
channel. Messages for the host raise the host bank interrupt; the
handler drains each FIFO under the controller lock, bounded by the FIFO
depth, clears the pending status, and delivers the messages after
dropping the lock, so a client may send from its rx callback.
Clocks and shared resets are enabled for all four banks, since the host
writes into and reads status from remote banks; an unclocked bank drops
writes; the shared resets are deasserted at probe and never asserted.
TX completion is polled (last_tx_done() checks FIFO count < 8) or
signaled by the receiver bank's write interrupt (allwinner,tx-done-irq).
Based on the vendor sunxi-msgbox driver by Allwinner Technology Co.,
Ltd.
Signed-off-by: Tim Michals <tcmichals@gmail.com>
---
Changes in v3:
- Enable the bus clocks and resets of all four banks: the host writes
into the remote banks, and an unclocked bank drops the writes
- Interrupt handler: drain the FIFO, then clear the pending bit. The
hardware ignores the clear while the FIFO is not empty, so clearing
first raised a second interrupt for every message
- last_tx_done() reports done while the receiver's FIFO has room
(count < 8), and send_data() never writes a full FIFO: in timer mode
it holds the message and last_tx_done() writes it once a slot is
free (the core polls only after a successful send). Sashiko flagged
"< FIFO_MAX" on v2 and v2 changed to count == 0; that made every
queued kick wait for the 1 ms poll. A kick is a doorbell and the data
is in shared memory, so it is done once the FIFO has taken it
- Optional TX done by the receiver bank's write interrupt
(allwinner,tx-done-irq)
- Request only the host bank interrupt unless tx-done-irq is set; no
array sized from the DT (Sashiko)
- Handler drains under the controller lock, bounded by the FIFO depth
(Sashiko), and delivers outside it; shutdown() synchronizes with it
- Startup and shutdown share one bounded flush
- Our write IRQs are disarmed at probe, and again in shutdown() once
a running TX-done handler has finished
- Register access goes through sun55i_msgbox_readl()/_writel() so the
KUnit tests can model the hardware
- Timer mode: send_data() and last_tx_done() check and write the FIFO
under the controller lock, so the poll timer writing a held message
and a new send cannot overfill it
- A held message whose blocking send timed out is dropped at the next
send instead of being written after it
- probe() sets the channels up before requesting the interrupt (the
handler walks them); devm_mutex_init()
- probe() requires four clocks and four resets; the interrupts are
looked up by name; the TX-done lines are kept per remote bank and
named after it (-cpus, -dsp, -rv)
- The receive handler reads each port's enable and status once
- Allwinner's copyright line: the register definitions come from the
vendor driver
- The shared reset is deasserted at probe and never asserted, as in
sun6i-msgbox: the other banks carry traffic between the other
processors
- sun60i/A733 support dropped from this series
---
MAINTAINERS | 7 +
drivers/mailbox/Kconfig | 12 +
drivers/mailbox/Makefile | 2 +
drivers/mailbox/sun55i-msgbox.c | 613 ++++++++++++++++++++++++++++++++
drivers/mailbox/sun55i-msgbox.h | 95 +++++
5 files changed, 729 insertions(+)
create mode 100644 drivers/mailbox/sun55i-msgbox.c
create mode 100644 drivers/mailbox/sun55i-msgbox.h
diff --git a/MAINTAINERS b/MAINTAINERS
index aaf2825ac945..890092a1e7a4 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -947,6 +947,13 @@ S: Maintained
F: Documentation/devicetree/bindings/hwlock/allwinner,sun6i-a31-hwspinlock.yaml
F: drivers/hwspinlock/sun6i_hwspinlock.c
+ALLWINNER SUN55I MESSAGE BOX DRIVER
+M: Tim Michals <tcmichals@gmail.com>
+L: linux-sunxi@lists.linux.dev
+S: Maintained
+F: Documentation/devicetree/bindings/mailbox/allwinner,sun55i-a523-msgbox.yaml
+F: drivers/mailbox/sun55i*
+
ALLWINNER THERMAL DRIVER
M: Vasily Khoruzhick <anarsoul@gmail.com>
M: Yangtao Li <tiny.windzz@gmail.com>
diff --git a/drivers/mailbox/Kconfig b/drivers/mailbox/Kconfig
index ab869769e278..370a55c8dc91 100644
--- a/drivers/mailbox/Kconfig
+++ b/drivers/mailbox/Kconfig
@@ -332,6 +332,18 @@ config SUN6I_MSGBOX
various Allwinner SoCs. This mailbox is used for communication
between the application CPUs and the power management coprocessor.
+config SUN55I_MSGBOX
+ tristate "Allwinner A523 Message Box"
+ depends on ARCH_SUNXI || COMPILE_TEST
+ help
+ Mailbox driver for the message box in Allwinner A523, A527 and T527
+ (sun55i) SoCs. It provides 12 hardware FIFO channels between the
+ Cortex-A55 host and the CPUS, HiFi4 DSP and XuanTie E906 RISC-V
+ processors.
+
+ To compile this driver as a module, choose M here: the module will
+ be called sun55i-msgbox.
+
config SPRD_MBOX
tristate "Spreadtrum Mailbox"
depends on ARCH_SPRD || COMPILE_TEST
diff --git a/drivers/mailbox/Makefile b/drivers/mailbox/Makefile
index 45dc59bb3e7f..40024aa906a6 100644
--- a/drivers/mailbox/Makefile
+++ b/drivers/mailbox/Makefile
@@ -71,6 +71,8 @@ obj-$(CONFIG_ZYNQMP_IPI_MBOX) += zynqmp-ipi-mailbox.o
obj-$(CONFIG_SUN6I_MSGBOX) += sun6i-msgbox.o
+obj-$(CONFIG_SUN55I_MSGBOX) += sun55i-msgbox.o
+
obj-$(CONFIG_SPRD_MBOX) += sprd-mailbox.o
obj-$(CONFIG_QCOM_CPUCP_MBOX) += qcom-cpucp-mbox.o
diff --git a/drivers/mailbox/sun55i-msgbox.c b/drivers/mailbox/sun55i-msgbox.c
new file mode 100644
index 000000000000..1618c16219fc
--- /dev/null
+++ b/drivers/mailbox/sun55i-msgbox.c
@@ -0,0 +1,613 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Allwinner sun55i (A523) Message Box driver
+ *
+ * Copyright (C) 2020-2023 Allwinner Technology Co., Ltd.
+ * Copyright (C) 2026 Tim Michals <tcmichals@gmail.com>
+ * Based on vendor sunxi-msgbox driver by Allwinner Technology Co., Ltd.
+ */
+
+#include <kunit/static_stub.h>
+#include <kunit/visibility.h>
+#include <linux/bitops.h>
+#include <linux/clk.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/limits.h>
+#include <linux/lockdep.h>
+#include <linux/mailbox_controller.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/reset.h>
+#include <linux/spinlock.h>
+#include <linux/string.h>
+
+#include "sun55i-msgbox.h"
+
+/*
+ * Hardware routing table for the Cortex-A55 host (bank SUN55I_PROC_ARM). The
+ * name is the remote bank's entry in "interrupt-names".
+ * local_n = 0 -> CPUS (remote_n = 0) -> channels 0..3
+ * local_n = 1 -> DSP (remote_n = 0) -> channels 4..7
+ * local_n = 2 -> RV (remote_n = 2) -> channels 8..11
+ */
+static const struct sun55i_route sun55i_msgbox_routes[SUN55I_NUM_ROUTES] = {
+ [0] = { .remote_id = SUN55I_PROC_CPUS, .remote_n = 0, .name = "cpus" },
+ [1] = { .remote_id = SUN55I_PROC_DSP, .remote_n = 0, .name = "dsp" },
+ [2] = { .remote_id = SUN55I_PROC_RV, .remote_n = 2, .name = "rv" },
+};
+
+static inline struct sun55i_msgbox *to_sun55i_msgbox(struct mbox_chan *chan)
+{
+ return chan->con_priv;
+}
+
+/* All register access goes through these, so KUnit can model the hardware */
+VISIBLE_IF_KUNIT
+u32 sun55i_msgbox_readl(void __iomem *addr)
+{
+ KUNIT_STATIC_STUB_REDIRECT(sun55i_msgbox_readl, addr);
+ return readl(addr);
+}
+EXPORT_SYMBOL_IF_KUNIT(sun55i_msgbox_readl);
+
+VISIBLE_IF_KUNIT
+void sun55i_msgbox_writel(u32 val, void __iomem *addr)
+{
+ KUNIT_STATIC_STUB_REDIRECT(sun55i_msgbox_writel, val, addr);
+ writel(val, addr);
+}
+EXPORT_SYMBOL_IF_KUNIT(sun55i_msgbox_writel);
+
+/* @chan_idx comes from the core, which only passes its own channels */
+VISIBLE_IF_KUNIT
+void sun55i_msgbox_chan_to_route(int chan_idx, int *local_n, int *p,
+ int *remote_id, int *remote_n)
+{
+ *local_n = chan_idx / SUN55I_CHANS_PER_PROC;
+ *p = chan_idx % SUN55I_CHANS_PER_PROC;
+ *remote_id = sun55i_msgbox_routes[*local_n].remote_id;
+ *remote_n = sun55i_msgbox_routes[*local_n].remote_n;
+}
+EXPORT_SYMBOL_IF_KUNIT(sun55i_msgbox_chan_to_route);
+
+/*
+ * Read what is queued for us on one channel (at most FIFO_MAX, so a runaway
+ * remote cannot lock up the CPU) into @msgs, then clear its pending bit.
+ * Returns the number of messages read.
+ */
+static int sun55i_msgbox_rx_drain(struct sun55i_msgbox *mbox, int local_n, int p,
+ u32 *msgs)
+{
+ void __iomem *base = mbox->regs[SUN55I_PROC_ARM];
+ int count = 0;
+
+ lockdep_assert_held(&mbox->lock);
+
+ while (count < SUN55I_FIFO_MAX &&
+ (sun55i_msgbox_readl(base + SUN55I_MSGBOX_MSG_STATUS(local_n, p)) &
+ SUN55I_MSGBOX_MSG_NUM_MASK))
+ msgs[count++] = sun55i_msgbox_readl(base + SUN55I_MSGBOX_MSG_FIFO(local_n, p));
+
+ /*
+ * The pending bit follows "FIFO not empty": the hardware ignores the
+ * clear while a message is still waiting. Clearing it before the drain
+ * left it set and raised a second interrupt for every message. A
+ * message that arrives after this clear sets it again.
+ */
+ sun55i_msgbox_writel(SUN55I_MSGBOX_RD_IRQ_PEND_BIT(p),
+ base + SUN55I_MSGBOX_READ_IRQ_STATUS(local_n));
+
+ return count;
+}
+
+VISIBLE_IF_KUNIT
+irqreturn_t sun55i_msgbox_irq(int irq, void *dev_id)
+{
+ struct sun55i_msgbox *mbox = dev_id;
+ void __iomem *base = mbox->regs[SUN55I_PROC_ARM];
+ u32 msgs[SUN55I_CHANS_PER_PROC][SUN55I_FIFO_MAX];
+ int count[SUN55I_CHANS_PER_PROC];
+ irqreturn_t ret = IRQ_NONE;
+ int local_n, p, i;
+
+ for (local_n = 0; local_n < SUN55I_NUM_ROUTES; local_n++) {
+ struct mbox_chan *chans =
+ &mbox->controller.chans[local_n * SUN55I_CHANS_PER_PROC];
+ unsigned long flags;
+ u32 pending;
+
+ spin_lock_irqsave(&mbox->lock, flags);
+ pending = sun55i_msgbox_readl(base + SUN55I_MSGBOX_READ_IRQ_ENABLE(local_n)) &
+ sun55i_msgbox_readl(base + SUN55I_MSGBOX_READ_IRQ_STATUS(local_n));
+ for (p = 0; p < SUN55I_CHANS_PER_PROC; p++)
+ count[p] = pending & SUN55I_MSGBOX_RD_IRQ_PEND_BIT(p) ?
+ sun55i_msgbox_rx_drain(mbox, local_n, p, msgs[p]) : -1;
+ spin_unlock_irqrestore(&mbox->lock, flags);
+
+ /*
+ * Outside the lock: a client may send from its rx callback,
+ * and send_data() takes the lock. shutdown() waits for this
+ * handler before the channel goes away.
+ */
+ for (p = 0; p < SUN55I_CHANS_PER_PROC; p++) {
+ if (count[p] < 0)
+ continue;
+ for (i = 0; i < count[p]; i++)
+ mbox_chan_received_data(&chans[p], &msgs[p][i]);
+ ret = IRQ_HANDLED;
+ }
+ }
+
+ return ret;
+}
+EXPORT_SYMBOL_IF_KUNIT(sun55i_msgbox_irq);
+
+static u32 sun55i_msgbox_tx_count(struct sun55i_msgbox *mbox, int remote_id,
+ int remote_n, int p)
+{
+ return sun55i_msgbox_readl(mbox->regs[remote_id] +
+ SUN55I_MSGBOX_MSG_STATUS(remote_n, p)) &
+ SUN55I_MSGBOX_MSG_NUM_MASK;
+}
+
+/*
+ * The write IRQ of a channel lives in the receiver's bank and is raised while
+ * at least one FIFO slot is free (level). In write IRQ mode it is enabled
+ * after every send: it fires at once if the FIFO still has room, or when the
+ * receiver reads a message, and the handler disables it again.
+ *
+ * The status bit is latched: it stays set after the FIFO fills up again until
+ * it is cleared, and the clear only takes while no slot is free. Clear it
+ * before enabling, so a stale bit does not fire at once; if a slot has been
+ * freed meanwhile the clear is ignored and the IRQ fires, as it should.
+ */
+static void sun55i_msgbox_tx_irq_enable(struct sun55i_msgbox *mbox, int remote_id,
+ int remote_n, int p, bool enable)
+{
+ void __iomem *base = mbox->regs[remote_id];
+ void __iomem *reg = base + SUN55I_MSGBOX_WRITE_IRQ_ENABLE(remote_n);
+ unsigned long flags;
+ u32 val;
+
+ spin_lock_irqsave(&mbox->lock, flags);
+ val = sun55i_msgbox_readl(reg);
+ if (enable) {
+ sun55i_msgbox_writel(SUN55I_MSGBOX_WR_IRQ_PEND_BIT(p),
+ base + SUN55I_MSGBOX_WRITE_IRQ_STATUS(remote_n));
+ val |= SUN55I_MSGBOX_WR_IRQ_EN_BIT(p);
+ } else {
+ val &= ~SUN55I_MSGBOX_WR_IRQ_EN_BIT(p);
+ }
+ sun55i_msgbox_writel(val, reg);
+ spin_unlock_irqrestore(&mbox->lock, flags);
+}
+
+VISIBLE_IF_KUNIT
+irqreturn_t sun55i_msgbox_tx_irq(int irq, void *dev_id)
+{
+ struct sun55i_tx_irq *tx = dev_id;
+ struct sun55i_msgbox *mbox = tx->mbox;
+ const struct sun55i_route *route = &sun55i_msgbox_routes[tx->local_n];
+ void __iomem *base = mbox->regs[route->remote_id];
+ struct mbox_chan *chans = &mbox->controller.chans[tx->local_n * SUN55I_CHANS_PER_PROC];
+ unsigned long flags, done = 0;
+ u32 en, pend;
+ int p;
+
+ spin_lock_irqsave(&mbox->lock, flags);
+
+ en = sun55i_msgbox_readl(base + SUN55I_MSGBOX_WRITE_IRQ_ENABLE(route->remote_n));
+ pend = sun55i_msgbox_readl(base + SUN55I_MSGBOX_WRITE_IRQ_STATUS(route->remote_n));
+
+ for (p = 0; p < SUN55I_CHANS_PER_PROC; p++) {
+ if (!(en & pend & SUN55I_MSGBOX_WR_IRQ_PEND_BIT(p)))
+ continue;
+
+ /* Level: mask it before acking, a slot is free now */
+ en &= ~SUN55I_MSGBOX_WR_IRQ_EN_BIT(p);
+ sun55i_msgbox_writel(en, base + SUN55I_MSGBOX_WRITE_IRQ_ENABLE(route->remote_n));
+ sun55i_msgbox_writel(SUN55I_MSGBOX_WR_IRQ_PEND_BIT(p),
+ base + SUN55I_MSGBOX_WRITE_IRQ_STATUS(route->remote_n));
+ __set_bit(p, &done);
+ }
+
+ spin_unlock_irqrestore(&mbox->lock, flags);
+
+ /* Outside the lock: txdone submits the next message (send_data) */
+ for_each_set_bit(p, &done, SUN55I_CHANS_PER_PROC)
+ mbox_chan_txdone(&chans[p], 0);
+
+ return done ? IRQ_HANDLED : IRQ_NONE;
+}
+EXPORT_SYMBOL_IF_KUNIT(sun55i_msgbox_tx_irq);
+
+/*
+ * A route's TX-done line is requested while at least one channel on it is
+ * started, so lines of banks nobody talks to stay free.
+ */
+static int sun55i_msgbox_tx_irq_get(struct sun55i_msgbox *mbox, int local_n)
+{
+ struct sun55i_tx_irq *tx = &mbox->tx_irqs[local_n];
+ int ret = 0;
+
+ mutex_lock(&mbox->tx_irq_lock);
+ if (!tx->users) {
+ ret = request_irq(tx->irq, sun55i_msgbox_tx_irq, 0, tx->name, tx);
+ if (ret)
+ dev_err(mbox->controller.dev, "failed to request %s: %d\n",
+ tx->name, ret);
+ }
+ if (!ret)
+ tx->users++;
+ mutex_unlock(&mbox->tx_irq_lock);
+
+ return ret;
+}
+
+static void sun55i_msgbox_tx_irq_put(struct sun55i_msgbox *mbox, int local_n)
+{
+ struct sun55i_tx_irq *tx = &mbox->tx_irqs[local_n];
+
+ mutex_lock(&mbox->tx_irq_lock);
+ if (!--tx->users) {
+ free_irq(tx->irq, tx);
+ } else {
+ /* The line stays in use: let a running handler finish with us */
+ synchronize_irq(tx->irq);
+ }
+ mutex_unlock(&mbox->tx_irq_lock);
+}
+
+static int sun55i_msgbox_send_data(struct mbox_chan *chan, void *data)
+{
+ struct sun55i_msgbox *mbox = to_sun55i_msgbox(chan);
+ int n = chan - mbox->controller.chans;
+ int local_n, p, remote_id, remote_n;
+ unsigned long flags;
+ u32 msg = 0;
+ bool full;
+
+ if (data)
+ memcpy(&msg, data, sizeof(msg));
+
+ sun55i_msgbox_chan_to_route(n, &local_n, &p, &remote_id, &remote_n);
+
+ /*
+ * Under the lock: in timer mode the poll timer may write a held
+ * message into the same FIFO from another CPU (last_tx_done()).
+ */
+ spin_lock_irqsave(&mbox->lock, flags);
+
+ /*
+ * The core calls send_data() only with no request active, so a message
+ * still held here is one it gave up on (a blocking send timed out):
+ * drop it rather than send it after this newer one.
+ */
+ __clear_bit(n, &mbox->tx_held);
+
+ /*
+ * Writing a full FIFO corrupts its count, so never do it. In write IRQ
+ * mode the IRQ armed below retries the message once a slot is free. In
+ * timer mode the core polls only a channel whose send succeeded, so
+ * hold the message and let last_tx_done() write it.
+ */
+ full = sun55i_msgbox_tx_count(mbox, remote_id, remote_n, p) >= SUN55I_FIFO_MAX;
+ if (!full) {
+ sun55i_msgbox_writel(msg, mbox->regs[remote_id] +
+ SUN55I_MSGBOX_MSG_FIFO(remote_n, p));
+ } else if (!mbox->tx_done_irq) {
+ mbox->tx_held_msg[n] = msg;
+ __set_bit(n, &mbox->tx_held);
+ }
+
+ spin_unlock_irqrestore(&mbox->lock, flags);
+
+ /*
+ * A kick is a doorbell, the data is in shared memory: it is done once
+ * the FIFO accepted it and has room for the next one. The write IRQ
+ * reports exactly that, at once or when the receiver reads a message.
+ */
+ if (mbox->tx_done_irq) {
+ sun55i_msgbox_tx_irq_enable(mbox, remote_id, remote_n, p, true);
+ return full ? -EBUSY : 0;
+ }
+
+ return 0;
+}
+
+static int sun55i_msgbox_startup(struct mbox_chan *chan)
+{
+ struct sun55i_msgbox *mbox = to_sun55i_msgbox(chan);
+ void __iomem *base = mbox->regs[SUN55I_PROC_ARM];
+ int n = chan - mbox->controller.chans;
+ int local_n, p, remote_id, remote_n;
+ u32 stale[SUN55I_FIFO_MAX];
+ unsigned long flags;
+ u32 val;
+
+ sun55i_msgbox_chan_to_route(n, &local_n, &p, &remote_id, &remote_n);
+
+ /*
+ * Get the TX-done line before the receive IRQ is enabled: on failure the
+ * core clears chan->cl, which a receive handler must not be using.
+ */
+ if (mbox->tx_done_irq) {
+ int ret;
+
+ /* TX done: the write IRQ, raised while one slot is free */
+ sun55i_msgbox_writel(SUN55I_MSGBOX_WR_IRQ_THR_ONE_FREE, mbox->regs[remote_id] +
+ SUN55I_MSGBOX_WRITE_IRQ_THRESHOLD(remote_n, p));
+
+ ret = sun55i_msgbox_tx_irq_get(mbox, local_n);
+ if (ret)
+ return ret;
+ set_bit(n, &mbox->tx_irq_held);
+ }
+
+ /* Drop stale receive data, then enable the receive IRQ */
+ spin_lock_irqsave(&mbox->lock, flags);
+ sun55i_msgbox_rx_drain(mbox, local_n, p, stale);
+ val = sun55i_msgbox_readl(base + SUN55I_MSGBOX_READ_IRQ_ENABLE(local_n));
+ val |= SUN55I_MSGBOX_RD_IRQ_EN_BIT(p);
+ sun55i_msgbox_writel(val, base + SUN55I_MSGBOX_READ_IRQ_ENABLE(local_n));
+ spin_unlock_irqrestore(&mbox->lock, flags);
+
+ return 0;
+}
+
+static void sun55i_msgbox_shutdown(struct mbox_chan *chan)
+{
+ struct sun55i_msgbox *mbox = to_sun55i_msgbox(chan);
+ void __iomem *base = mbox->regs[SUN55I_PROC_ARM];
+ int n = chan - mbox->controller.chans;
+ int local_n, p, remote_id, remote_n;
+ u32 stale[SUN55I_FIFO_MAX];
+ unsigned long flags;
+ u32 val;
+
+ sun55i_msgbox_chan_to_route(n, &local_n, &p, &remote_id, &remote_n);
+
+ /* Disable the receive IRQ; a message held for a full FIFO is dropped */
+ spin_lock_irqsave(&mbox->lock, flags);
+ val = sun55i_msgbox_readl(base + SUN55I_MSGBOX_READ_IRQ_ENABLE(local_n));
+ val &= ~SUN55I_MSGBOX_RD_IRQ_EN_BIT(p);
+ sun55i_msgbox_writel(val, base + SUN55I_MSGBOX_READ_IRQ_ENABLE(local_n));
+ __clear_bit(n, &mbox->tx_held);
+ spin_unlock_irqrestore(&mbox->lock, flags);
+
+ /* The handler delivers outside the lock: let it finish with this channel */
+ synchronize_irq(mbox->irq);
+
+ /* Release the line only if this channel's startup() got it */
+ if (test_and_clear_bit(n, &mbox->tx_irq_held)) {
+ sun55i_msgbox_tx_irq_enable(mbox, remote_id, remote_n, p, false);
+ sun55i_msgbox_tx_irq_put(mbox, local_n);
+ /*
+ * A TX-done handler that was already running submits the next
+ * message through send_data(), which arms the IRQ again. It has
+ * finished now (free_irq() or synchronize_irq() above), so
+ * disarm once more.
+ */
+ sun55i_msgbox_tx_irq_enable(mbox, remote_id, remote_n, p, false);
+ }
+
+ /* Drop what arrived meanwhile */
+ spin_lock_irqsave(&mbox->lock, flags);
+ sun55i_msgbox_rx_drain(mbox, local_n, p, stale);
+ spin_unlock_irqrestore(&mbox->lock, flags);
+}
+
+static bool sun55i_msgbox_last_tx_done(struct mbox_chan *chan)
+{
+ struct sun55i_msgbox *mbox = to_sun55i_msgbox(chan);
+ int n = chan - mbox->controller.chans;
+ int local_n, p, remote_id, remote_n;
+ unsigned long flags;
+ bool done;
+
+ sun55i_msgbox_chan_to_route(n, &local_n, &p, &remote_id, &remote_n);
+
+ /*
+ * Timer mode only. A kick is a doorbell, the data is in shared memory:
+ * it is done once the receiver's FIFO accepted it and has room for the
+ * next one; a full FIFO waits for the next poll.
+ */
+ spin_lock_irqsave(&mbox->lock, flags);
+ done = sun55i_msgbox_tx_count(mbox, remote_id, remote_n, p) < SUN55I_FIFO_MAX;
+
+ /* send_data() found the FIFO full and held the message: write it now */
+ if (done && __test_and_clear_bit(n, &mbox->tx_held)) {
+ sun55i_msgbox_writel(mbox->tx_held_msg[n], mbox->regs[remote_id] +
+ SUN55I_MSGBOX_MSG_FIFO(remote_n, p));
+ done = sun55i_msgbox_tx_count(mbox, remote_id, remote_n, p) < SUN55I_FIFO_MAX;
+ }
+ spin_unlock_irqrestore(&mbox->lock, flags);
+
+ return done;
+}
+
+static bool sun55i_msgbox_peek_data(struct mbox_chan *chan)
+{
+ struct sun55i_msgbox *mbox = to_sun55i_msgbox(chan);
+ int n = chan - mbox->controller.chans;
+ int local_n, p, remote_id, remote_n;
+
+ sun55i_msgbox_chan_to_route(n, &local_n, &p, &remote_id, &remote_n);
+
+ return sun55i_msgbox_readl(mbox->regs[SUN55I_PROC_ARM] +
+ SUN55I_MSGBOX_MSG_STATUS(local_n, p)) &
+ SUN55I_MSGBOX_MSG_NUM_MASK;
+}
+
+VISIBLE_IF_KUNIT
+const struct mbox_chan_ops sun55i_msgbox_chan_ops = {
+ .send_data = sun55i_msgbox_send_data,
+ .startup = sun55i_msgbox_startup,
+ .shutdown = sun55i_msgbox_shutdown,
+ .last_tx_done = sun55i_msgbox_last_tx_done,
+ .peek_data = sun55i_msgbox_peek_data,
+};
+EXPORT_SYMBOL_IF_KUNIT(sun55i_msgbox_chan_ops);
+
+static int sun55i_msgbox_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct clk_bulk_data *clks;
+ struct reset_control *rst;
+ struct mbox_chan *chans;
+ struct sun55i_msgbox *mbox;
+ int i, ret, irq, local_n;
+
+ mbox = devm_kzalloc(dev, sizeof(*mbox), GFP_KERNEL);
+ if (!mbox)
+ return -ENOMEM;
+
+ spin_lock_init(&mbox->lock);
+ ret = devm_mutex_init(dev, &mbox->tx_irq_lock);
+ if (ret)
+ return ret;
+
+ chans = devm_kcalloc(dev, SUN55I_NUM_CHANS, sizeof(*chans), GFP_KERNEL);
+ if (!chans)
+ return -ENOMEM;
+
+ for (i = 0; i < SUN55I_NUM_CHANS; i++)
+ chans[i].con_priv = mbox;
+
+ /* The interrupt handler walks the channels: set them up before it */
+ mbox->controller.dev = dev;
+ mbox->controller.ops = &sun55i_msgbox_chan_ops;
+ mbox->controller.chans = chans;
+ mbox->controller.num_chans = SUN55I_NUM_CHANS;
+
+ for (i = 0; i < SUN55I_MAX_PROCESSORS; i++) {
+ mbox->regs[i] = devm_platform_ioremap_resource(pdev, i);
+ if (IS_ERR(mbox->regs[i]))
+ return dev_err_probe(dev, PTR_ERR(mbox->regs[i]),
+ "failed to map resource %d\n", i);
+ }
+
+ /*
+ * Every bank has its own bus clock and reset. The host writes messages
+ * into the remote banks and reads their FIFO status, so all of them
+ * must be clocked and out of reset; an unclocked bank drops writes and
+ * reads back as zero. A DT missing one would fail silently, so check.
+ */
+ ret = devm_clk_bulk_get_all_enabled(dev, &clks);
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "failed to enable clocks\n");
+ if (ret != SUN55I_MAX_PROCESSORS)
+ return dev_err_probe(dev, -EINVAL, "need %d clocks, found %d\n",
+ SUN55I_MAX_PROCESSORS, ret);
+
+ ret = reset_control_get_count(dev);
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "failed to count resets\n");
+ if (ret != SUN55I_MAX_PROCESSORS)
+ return dev_err_probe(dev, -EINVAL, "need %d resets, found %d\n",
+ SUN55I_MAX_PROCESSORS, ret);
+
+ rst = devm_reset_control_array_get_shared(dev);
+ if (IS_ERR(rst))
+ return dev_err_probe(dev, PTR_ERR(rst), "failed to get resets\n");
+
+ /*
+ * The DSP, CPUS and RISC-V banks also carry traffic between the other
+ * processors, which may already be running. Asserting the reset would
+ * wipe their FIFOs, so it is never asserted, on unbind or on a probe
+ * failure, as in sun6i-msgbox.
+ */
+ ret = reset_control_deassert(rst);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to deassert reset\n");
+
+ /*
+ * Disable all read IRQs and clear status, and disarm our write IRQs in
+ * the remote banks, which an earlier kernel or bind may have left on.
+ */
+ for (local_n = 0; local_n < SUN55I_NUM_ROUTES; local_n++) {
+ const struct sun55i_route *route = &sun55i_msgbox_routes[local_n];
+ void __iomem *base = mbox->regs[SUN55I_PROC_ARM];
+ void __iomem *wr_en = mbox->regs[route->remote_id] +
+ SUN55I_MSGBOX_WRITE_IRQ_ENABLE(route->remote_n);
+ u32 val = sun55i_msgbox_readl(wr_en);
+ int p;
+
+ sun55i_msgbox_writel(0, base + SUN55I_MSGBOX_READ_IRQ_ENABLE(local_n));
+ sun55i_msgbox_writel(U32_MAX, base + SUN55I_MSGBOX_READ_IRQ_STATUS(local_n));
+
+ for (p = 0; p < SUN55I_CHANS_PER_PROC; p++)
+ val &= ~SUN55I_MSGBOX_WR_IRQ_EN_BIT(p);
+ sun55i_msgbox_writel(val, wr_en);
+ }
+
+ /* The ARM bank interrupt signals messages received by this processor */
+ irq = platform_get_irq_byname(pdev, "arm");
+ if (irq < 0)
+ return irq;
+
+ ret = devm_request_irq(dev, irq, sun55i_msgbox_irq, 0, dev_name(dev), mbox);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to request irq\n");
+ mbox->irq = irq;
+
+ /*
+ * The write IRQ of a channel is raised in the receiver's bank, on that
+ * bank's own line to this processor. Look the lines up now so a bad DT
+ * fails the probe; each is requested only while a channel on its route
+ * is started.
+ */
+ mbox->tx_done_irq = of_property_read_bool(dev->of_node, "allwinner,tx-done-irq");
+ for (local_n = 0; mbox->tx_done_irq && local_n < SUN55I_NUM_ROUTES; local_n++) {
+ const struct sun55i_route *route = &sun55i_msgbox_routes[local_n];
+ struct sun55i_tx_irq *tx = &mbox->tx_irqs[local_n];
+
+ tx->mbox = mbox;
+ tx->local_n = local_n;
+ tx->irq = platform_get_irq_byname(pdev, route->name);
+ if (tx->irq < 0)
+ return tx->irq;
+
+ tx->name = devm_kasprintf(dev, GFP_KERNEL, "%s-%s", dev_name(dev), route->name);
+ if (!tx->name)
+ return -ENOMEM;
+ }
+
+ /* TX done by the write IRQ ("allwinner,tx-done-irq") or by a 1 ms poll */
+ if (mbox->tx_done_irq) {
+ mbox->controller.txdone_irq = true;
+ } else {
+ mbox->controller.txdone_poll = true;
+ mbox->controller.txpoll_period = 1;
+ }
+
+ ret = devm_mbox_controller_register(dev, &mbox->controller);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to register controller\n");
+
+ return 0;
+}
+
+static const struct of_device_id sun55i_msgbox_of_match[] = {
+ { .compatible = "allwinner,sun55i-a523-msgbox" },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, sun55i_msgbox_of_match);
+
+static struct platform_driver sun55i_msgbox_driver = {
+ .driver = {
+ .name = "sun55i-msgbox",
+ .of_match_table = sun55i_msgbox_of_match,
+ },
+ .probe = sun55i_msgbox_probe,
+};
+module_platform_driver(sun55i_msgbox_driver);
+
+MODULE_AUTHOR("Tim Michals <tcmichals@gmail.com>");
+MODULE_DESCRIPTION("Allwinner sun55i Message Box driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/mailbox/sun55i-msgbox.h b/drivers/mailbox/sun55i-msgbox.h
new file mode 100644
index 000000000000..98c0634c0532
--- /dev/null
+++ b/drivers/mailbox/sun55i-msgbox.h
@@ -0,0 +1,95 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _SUN55I_MSGBOX_H_
+#define _SUN55I_MSGBOX_H_
+
+#include <linux/bits.h>
+#include <linux/interrupt.h>
+#include <linux/mailbox_controller.h>
+#include <linux/mutex.h>
+#include <linux/spinlock.h>
+#include <linux/types.h>
+
+/* Banks, in "reg" order */
+#define SUN55I_PROC_ARM 0
+#define SUN55I_PROC_DSP 1
+#define SUN55I_PROC_CPUS 2
+#define SUN55I_PROC_RV 3
+
+#define SUN55I_MAX_PROCESSORS 4
+#define SUN55I_CHANS_PER_PROC 4
+#define SUN55I_NUM_ROUTES (SUN55I_MAX_PROCESSORS - 1)
+#define SUN55I_NUM_CHANS (SUN55I_NUM_ROUTES * SUN55I_CHANS_PER_PROC)
+#define SUN55I_FIFO_MAX 8
+
+#define SUN55I_MSGBOX_OFFSET(n) (0x100 * (n))
+#define SUN55I_MSGBOX_READ_IRQ_ENABLE(n) (0x020 + SUN55I_MSGBOX_OFFSET(n))
+#define SUN55I_MSGBOX_READ_IRQ_STATUS(n) (0x024 + SUN55I_MSGBOX_OFFSET(n))
+#define SUN55I_MSGBOX_WRITE_IRQ_ENABLE(n) (0x030 + SUN55I_MSGBOX_OFFSET(n))
+#define SUN55I_MSGBOX_WRITE_IRQ_STATUS(n) (0x034 + SUN55I_MSGBOX_OFFSET(n))
+#define SUN55I_MSGBOX_MSG_STATUS(n, p) (0x060 + SUN55I_MSGBOX_OFFSET(n) + 0x4 * (p))
+#define SUN55I_MSGBOX_MSG_FIFO(n, p) (0x070 + SUN55I_MSGBOX_OFFSET(n) + 0x4 * (p))
+#define SUN55I_MSGBOX_WRITE_IRQ_THRESHOLD(n, p) (0x080 + SUN55I_MSGBOX_OFFSET(n) + 0x4 * (p))
+
+#define SUN55I_MSGBOX_RD_IRQ_EN_BIT(p) BIT_U32((p) * 2)
+#define SUN55I_MSGBOX_RD_IRQ_PEND_BIT(p) BIT_U32((p) * 2)
+#define SUN55I_MSGBOX_WR_IRQ_EN_BIT(p) BIT_U32((p) * 2 + 1)
+#define SUN55I_MSGBOX_WR_IRQ_PEND_BIT(p) BIT_U32((p) * 2 + 1)
+/* Write IRQ threshold field value 0: raised while at least one slot is free */
+#define SUN55I_MSGBOX_WR_IRQ_THR_ONE_FREE 0
+#define SUN55I_MSGBOX_MSG_NUM_MASK GENMASK(3, 0)
+
+/* Where host port local_n sends to: the remote bank and its port for us */
+struct sun55i_route {
+ u8 remote_id;
+ u8 remote_n;
+ const char *name;
+};
+
+struct sun55i_msgbox;
+
+/* The interrupt line carrying our TX-done (write) IRQ for one route */
+struct sun55i_tx_irq {
+ struct sun55i_msgbox *mbox;
+ int local_n;
+ int irq;
+ const char *name;
+ /* Started channels on this route; the IRQ is requested while > 0 */
+ unsigned int users;
+};
+
+struct sun55i_msgbox {
+ struct mbox_controller controller;
+ void __iomem *regs[SUN55I_MAX_PROCESSORS];
+ /*
+ * Protects the read-modify-writes of the IRQ enable registers, the
+ * receive drain with its pending clear, and in timer mode the FIFO
+ * check and write together with the held message.
+ */
+ spinlock_t lock;
+ /* Host bank interrupt: messages received by this processor */
+ int irq;
+ /* "allwinner,tx-done-irq": TX done by the write IRQ, else polled */
+ bool tx_done_irq;
+ /* Indexed by route (host port local_n) */
+ struct sun55i_tx_irq tx_irqs[SUN55I_NUM_ROUTES];
+ /* Channels whose startup() holds their route's TX-done line */
+ unsigned long tx_irq_held;
+ /* Serialises tx_irqs[].users and the matching request_irq()/free_irq() */
+ struct mutex tx_irq_lock;
+ /* Timer mode, under @lock: channels whose message waits for FIFO room */
+ unsigned long tx_held;
+ u32 tx_held_msg[SUN55I_NUM_CHANS];
+};
+
+#if IS_ENABLED(CONFIG_KUNIT)
+extern const struct mbox_chan_ops sun55i_msgbox_chan_ops;
+
+u32 sun55i_msgbox_readl(void __iomem *addr);
+void sun55i_msgbox_writel(u32 val, void __iomem *addr);
+void sun55i_msgbox_chan_to_route(int chan_idx, int *local_n, int *p,
+ int *remote_id, int *remote_n);
+irqreturn_t sun55i_msgbox_irq(int irq, void *dev_id);
+irqreturn_t sun55i_msgbox_tx_irq(int irq, void *dev_id);
+#endif
+
+#endif /* _SUN55I_MSGBOX_H_ */
--
2.53.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v3 3/7] dt-bindings: remoteproc: Add Allwinner A523 RISC-V remote processor
2026-11-01 18:57 [PATCH v3 0/7] Add Allwinner A523 message box and XuanTie E906 remoteproc support Tim Michals
2026-11-01 18:57 ` [PATCH v3 1/7] dt-bindings: mailbox: Add Allwinner A523 message box Tim Michals
2026-11-01 18:57 ` [PATCH v3 2/7] mailbox: sun55i: Add Allwinner A523 message box driver Tim Michals
@ 2026-11-01 18:57 ` Tim Michals
2026-11-01 18:57 ` [PATCH v3 4/7] remoteproc: sunxi: Add Allwinner A523 XuanTie E906 driver Tim Michals
` (3 subsequent siblings)
6 siblings, 0 replies; 12+ messages in thread
From: Tim Michals @ 2026-11-01 18:57 UTC (permalink / raw)
To: Jassi Brar, Bjorn Andersson, Mathieu Poirier, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Chen-Yu Tsai, Jernej Skrabec,
Samuel Holland
Cc: linux-sunxi, linux-remoteproc, devicetree, linux-arm-kernel,
linux-kernel, Tim Michals
The Allwinner A523, A527 and T527 (sun55i) contain a T-Head XuanTie E906
RISC-V core. It runs from a dedicated 256 KiB SRAM and can use a second
256 KiB SRAM and DRAM; a bit in a PRCM remap register shares a further
512 KiB SRAM (SRAM A3_2) with its subsystem. Its reset vector is set in
its configuration block.
Describe the configuration block, both SRAMs and the remap register,
the bus, core and SRAM clocks, the configuration, core, SRAM and debug
resets, the RISC-V and SRAM power domains, one message box channel, the
reserved memory regions (a pool for virtio, DRAM for firmware, a trace
buffer) and an optional crash interrupt.
Signed-off-by: Tim Michals <tcmichals@gmail.com>
---
Changes in v3:
- File renamed to allwinner,sun55i-a523-rproc.yaml to match the
compatible
- reg and reg-names are a fixed list of four (cfg, sram-a3-0,
sram-a3-1, remap), all required (Krzysztof); the SRAMs are named as
in the T527 manual (SRAMA3_0, SRAMA3_1); the description says the
remap bit shares SRAM A3_2, not SRAM space 1
- clocks and resets are fixed lists, no minItems (Krzysztof); add the
debug reset. The msgbox and parent clocks and the msgbox reset are
dropped: they belong to the message box node
- Add the RISC-V and SRAM power domains
- One message box channel; mbox-names dropped. mboxes is optional: a
user can drive the message box from userspace (UIO) instead
- The crash interrupt description says the A523 has no dedicated line
for it; it is the only interrupt, so no interrupt-names
- memory-region[0] is a shared-dma-pool; the example has the memory
regions
- "schema" dropped from the subject (Krzysztof)
- Jernej Skrabec and Samuel Holland dropped from the maintainers; the
MAINTAINERS entry moves to the driver patch
---
.../allwinner,sun55i-a523-rproc.yaml | 146 ++++++++++++++++++
1 file changed, 146 insertions(+)
create mode 100644 Documentation/devicetree/bindings/remoteproc/allwinner,sun55i-a523-rproc.yaml
diff --git a/Documentation/devicetree/bindings/remoteproc/allwinner,sun55i-a523-rproc.yaml b/Documentation/devicetree/bindings/remoteproc/allwinner,sun55i-a523-rproc.yaml
new file mode 100644
index 000000000000..b119acaa2df6
--- /dev/null
+++ b/Documentation/devicetree/bindings/remoteproc/allwinner,sun55i-a523-rproc.yaml
@@ -0,0 +1,146 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/remoteproc/allwinner,sun55i-a523-rproc.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Allwinner A523 XuanTie E906 RISC-V Remote Processor
+
+maintainers:
+ - Tim Michals <tcmichals@gmail.com>
+
+description:
+ The Allwinner A523, A527 and T527 (sun55i) SoCs contain a T-Head XuanTie
+ E906 RISC-V core next to the Cortex-A55 cluster. The core runs from a
+ dedicated 256 KiB SRAM, can also use a second 256 KiB SRAM and DRAM,
+ and a remap register shared with the HiFi4 DSP shares a further 512 KiB
+ SRAM (SRAM A3_2) with the core's subsystem. Its reset vector is set in a
+ register of its configuration block, and it exchanges notifications with
+ the Cortex-A55 host through the message box.
+
+properties:
+ compatible:
+ const: allwinner,sun55i-a523-rproc
+
+ reg:
+ items:
+ - description: Configuration block, including the boot address register
+ - description: SRAM A3_0, the core's dedicated SRAM space 0
+ - description: SRAM A3_1, the core's SRAM space 1
+ - description: SRAM remap control register
+
+ reg-names:
+ items:
+ - const: cfg
+ - const: sram-a3-0
+ - const: sram-a3-1
+ - const: remap
+
+ clocks:
+ items:
+ - description: Configuration block bus clock
+ - description: RISC-V core clock
+ - description: MCU public SRAM bus clock
+
+ clock-names:
+ items:
+ - const: bus
+ - const: core
+ - const: sram
+
+ resets:
+ items:
+ - description: Configuration block reset
+ - description: RISC-V core reset
+ - description: MCU public SRAM reset
+ - description: RISC-V debug module reset
+
+ reset-names:
+ items:
+ - const: cfg
+ - const: core
+ - const: sram
+ - const: debug
+
+ mboxes:
+ description:
+ Message box channel to the RISC-V core. Each channel has a FIFO in
+ each direction, so it carries notifications both ways.
+ maxItems: 1
+
+ firmware-name:
+ maxItems: 1
+
+ memory-region:
+ minItems: 1
+ items:
+ - description: Virtio vring and buffer pool, a shared-dma-pool
+ - description: Firmware code and data in DRAM
+ - description: Trace buffer
+
+ memory-region-names:
+ minItems: 1
+ items:
+ - const: vram
+ - const: dram
+ - const: trace
+
+ interrupts:
+ maxItems: 1
+ description:
+ Interrupt the RISC-V core raises on a fatal error. The A523 has no
+ dedicated line for this, so it is present only where the system
+ integration provides one.
+
+ power-domains:
+ items:
+ - description: RISC-V core power domain
+ - description: SRAM power domain
+
+ power-domain-names:
+ items:
+ - const: riscv
+ - const: sram
+
+required:
+ - compatible
+ - reg
+ - reg-names
+ - clocks
+ - clock-names
+ - resets
+ - reset-names
+ - power-domains
+ - power-domain-names
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/clock/sun55i-a523-mcu-ccu.h>
+ #include <dt-bindings/power/allwinner,sun55i-a523-ppu.h>
+ #include <dt-bindings/reset/sun55i-a523-mcu-ccu.h>
+
+ remoteproc@7130000 {
+ compatible = "allwinner,sun55i-a523-rproc";
+ reg = <0x07130000 0x1000>,
+ <0x07280000 0x40000>,
+ <0x072c0000 0x40000>,
+ <0x07010364 0x4>;
+ reg-names = "cfg", "sram-a3-0", "sram-a3-1", "remap";
+ clocks = <&mcu_ccu CLK_BUS_MCU_RISCV_CFG>,
+ <&mcu_ccu CLK_MCU_RISCV>,
+ <&mcu_ccu CLK_BUS_MCU_PUBSRAM>;
+ clock-names = "bus", "core", "sram";
+ resets = <&mcu_ccu RST_BUS_MCU_RISCV_CFG>,
+ <&mcu_ccu RST_BUS_MCU_RISCV_CORE>,
+ <&mcu_ccu RST_BUS_MCU_PUBSRAM>,
+ <&mcu_ccu RST_BUS_MCU_RISCV_DEBUG>;
+ reset-names = "cfg", "core", "sram", "debug";
+ power-domains = <&ppu PD_RISCV>, <&ppu PD_SRAM>;
+ power-domain-names = "riscv", "sram";
+ mboxes = <&msgbox 9>;
+ memory-region = <&riscv_vram>, <&riscv_dram>;
+ memory-region-names = "vram", "dram";
+ firmware-name = "riscv-firmware.elf";
+ };
--
2.53.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v3 4/7] remoteproc: sunxi: Add Allwinner A523 XuanTie E906 driver
2026-11-01 18:57 [PATCH v3 0/7] Add Allwinner A523 message box and XuanTie E906 remoteproc support Tim Michals
` (2 preceding siblings ...)
2026-11-01 18:57 ` [PATCH v3 3/7] dt-bindings: remoteproc: Add Allwinner A523 RISC-V remote processor Tim Michals
@ 2026-11-01 18:57 ` Tim Michals
2026-11-01 19:24 ` sashiko-bot
2026-11-01 18:57 ` [PATCH v3 5/7] arm64: dts: allwinner: a523: Add message box and remoteproc nodes Tim Michals
` (2 subsequent siblings)
6 siblings, 1 reply; 12+ messages in thread
From: Tim Michals @ 2026-11-01 18:57 UTC (permalink / raw)
To: Jassi Brar, Bjorn Andersson, Mathieu Poirier, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Chen-Yu Tsai, Jernej Skrabec,
Samuel Holland
Cc: linux-sunxi, linux-remoteproc, devicetree, linux-arm-kernel,
linux-kernel, Tim Michals
Add a remoteproc driver for the XuanTie E906 RISC-V core in the
Allwinner A523, A527 and T527 (sun55i).
.prepare() powers the RISC-V and SRAM power domains, holds the core in
reset, releases the configuration, SRAM and debug resets, enables the
clocks, shares SRAM A3_2 with the core's subsystem through bit 1 of the
remap register and clears both SRAM spaces. .parse_fw() registers the
memory-region entries as carveouts and both SRAMs and the carveouts as
crash dump segments, then loads the resource table. .start() writes the
ELF entry point to the boot address register and releases the core
reset.
The driver translates the two SRAM windows, by core and by host address;
DRAM firmware, the trace buffer and the vrings are resolved by the core
from the carveouts. Vring kicks and the core's notifications use one
message box channel. The mailbox receive callback runs in atomic
context, so it queues work on an ordered workqueue, which calls
rproc_vq_interrupt() for every vring. An optional crash interrupt is
reported with rproc_report_crash().
Based on the vendor sunxi-rproc driver by Allwinner Technology Co.,
Ltd.
Signed-off-by: Tim Michals <tcmichals@gmail.com>
---
Changes in v3:
- The core is an E906 (RV32IMAFCX), not an E907
- The mailbox receive callback runs in atomic context; queue work on an
ordered workqueue instead of calling rproc_vq_interrupt() from it
- da_to_va() translates only the two SRAM windows and returns NULL for
anything else; DRAM, the trace buffer and the vrings come from the
core's carveout lookup. Unmatched addresses are no longer used as
host addresses (Sashiko)
- All register regions, clocks and resets are required; no fallback to
the configuration reset for the core reset (Sashiko). The mailbox is
optional
- MAINTAINERS entry added here (it was in the binding patch)
- Handle the debug reset and the RISC-V and SRAM power domains
- One mailbox channel for kicks and notifications; a notification
checks every vring in rproc->notifyids
- prepare() holds the core in reset before clearing the SRAM
- Kicks send static const per-vring payloads with knows_txdone = false
instead of a shared kick_msg (Sashiko)
- Teardown: rproc_del(), then mbox_free_channel(), then
cancel_work_sync() (Sashiko)
- The crash interrupt is armed once the core reset is released, with an
atomic flag (Sashiko); lookup and request errors fail the probe
- Read back the boot address before releasing the core reset (Sashiko)
- Carveouts mapped MEMREMAP_WC; a region inside an SRAM reuses the
SRAM mapping (Sashiko)
- Carveouts and crash dump segments are registered in parse_fw(), which
the core undoes only once it has succeeded; registered in prepare()
they stayed on the lists after a failed parse_fw()
- Crash dumps: the ELF info is set (ELFCLASS32, EM_RISCV) and both SRAMs
and the carveouts are registered as segments; dumps were empty before
- sanity_check() refuses anything but a 32-bit RISC-V ELF
- memory-region[0] must be the DMA pool, released by a devm action;
regions above 4 GiB are refused (the device address is a u32), and a
region outside the SRAMs may not overlap a window of the core's
address map
- The SRAM reg entries are checked against the SoC address map at probe
- A malformed firmware-name fails the probe; only an absent one keeps
the default
- pm_runtime_put_sync() in unprepare(); the power domains are attached
by name; the crash interrupt is looked up by index
- Register and SRAM access goes through helpers so the KUnit tests can
model the hardware
- The SRAM reg-names are sram-a3-0 and sram-a3-1; the remap bit is
described as the manual has it: it shares SRAM A3_2 (512 KiB at
0x07300000) with the MCU subsystem
---
MAINTAINERS | 8 +
drivers/remoteproc/Kconfig | 11 +
drivers/remoteproc/Makefile | 1 +
drivers/remoteproc/sunxi_rproc.c | 855 +++++++++++++++++++++++++++++++
drivers/remoteproc/sunxi_rproc.h | 124 +++++
5 files changed, 999 insertions(+)
create mode 100644 drivers/remoteproc/sunxi_rproc.c
create mode 100644 drivers/remoteproc/sunxi_rproc.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 890092a1e7a4..785baf0fba61 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -954,6 +954,14 @@ S: Maintained
F: Documentation/devicetree/bindings/mailbox/allwinner,sun55i-a523-msgbox.yaml
F: drivers/mailbox/sun55i*
+ALLWINNER SUN55I REMOTEPROC DRIVER
+M: Tim Michals <tcmichals@gmail.com>
+L: linux-remoteproc@vger.kernel.org
+L: linux-sunxi@lists.linux.dev
+S: Maintained
+F: Documentation/devicetree/bindings/remoteproc/allwinner,sun55i-a523-rproc.yaml
+F: drivers/remoteproc/sunxi_rproc*
+
ALLWINNER THERMAL DRIVER
M: Vasily Khoruzhick <anarsoul@gmail.com>
M: Yangtao Li <tiny.windzz@gmail.com>
diff --git a/drivers/remoteproc/Kconfig b/drivers/remoteproc/Kconfig
index 70dec8c3fe4e..d2929a3290c3 100644
--- a/drivers/remoteproc/Kconfig
+++ b/drivers/remoteproc/Kconfig
@@ -337,6 +337,17 @@ config STM32_RPROC
This can be either built-in or a loadable module.
+config SUNXI_REMOTEPROC
+ tristate "Allwinner A523 XuanTie E906 RISC-V remoteproc support"
+ depends on (ARCH_SUNXI || COMPILE_TEST) && MAILBOX
+ help
+ Say y or m here to support the Allwinner XuanTie E906 RISC-V
+ co-processor on Allwinner A523, A527, and T527 SoCs via the
+ remote processor framework.
+
+ To compile this driver as a module, choose M here: the module will
+ be called sunxi_rproc.
+
config TI_K3_DSP_REMOTEPROC
tristate "TI K3 DSP remoteproc support"
depends on ARCH_K3 || COMPILE_TEST
diff --git a/drivers/remoteproc/Makefile b/drivers/remoteproc/Makefile
index 689686de0d41..bf8c96f69a05 100644
--- a/drivers/remoteproc/Makefile
+++ b/drivers/remoteproc/Makefile
@@ -37,6 +37,7 @@ obj-$(CONFIG_RCAR_REMOTEPROC) += rcar_rproc.o
obj-$(CONFIG_ST_REMOTEPROC) += st_remoteproc.o
obj-$(CONFIG_ST_SLIM_REMOTEPROC) += st_slim_rproc.o
obj-$(CONFIG_STM32_RPROC) += stm32_rproc.o
+obj-$(CONFIG_SUNXI_REMOTEPROC) += sunxi_rproc.o
obj-$(CONFIG_TI_K3_DSP_REMOTEPROC) += ti_k3_dsp_remoteproc.o ti_k3_common.o
obj-$(CONFIG_TI_K3_M4_REMOTEPROC) += ti_k3_m4_remoteproc.o ti_k3_common.o
obj-$(CONFIG_TI_K3_R5_REMOTEPROC) += ti_k3_r5_remoteproc.o ti_k3_common.o
diff --git a/drivers/remoteproc/sunxi_rproc.c b/drivers/remoteproc/sunxi_rproc.c
new file mode 100644
index 000000000000..1cee7110f0b8
--- /dev/null
+++ b/drivers/remoteproc/sunxi_rproc.c
@@ -0,0 +1,855 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Allwinner XuanTie E906 RISC-V Remote Processor Driver
+ *
+ * Copyright (C) 2024-2026 Allwinner Technology Co., Ltd.
+ * Copyright (C) 2026 Tim Michals <tcmichals@gmail.com>
+ * Based on vendor sunxi-rproc driver by Allwinner Technology Co., Ltd.
+ *
+ * Loads and runs firmware on the XuanTie E906 RISC-V core of the Allwinner
+ * A523, A527 and T527, from its two SRAMs and from DRAM carveouts.
+ */
+
+#include <kunit/static_stub.h>
+#include <kunit/visibility.h>
+#include <linux/clk.h>
+#include <linux/device.h>
+#include <linux/dma-mapping.h>
+#include <linux/elf.h>
+#include <linux/err.h>
+#include <linux/firmware.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/limits.h>
+#include <linux/mailbox_client.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_reserved_mem.h>
+#include <linux/platform_device.h>
+#include <linux/pm_domain.h>
+#include <linux/pm_runtime.h>
+#include <linux/property.h>
+#include <linux/remoteproc.h>
+#include <linux/reset.h>
+#include <linux/slab.h>
+#include <linux/workqueue.h>
+
+#include "remoteproc_elf_helpers.h"
+#include "remoteproc_internal.h"
+#include "sunxi_rproc.h"
+
+#define DRIVER_NAME "sunxi-rproc"
+
+static const struct sunxi_rproc_att sun55i_rproc_att[] = {
+ /* dev addr (remote) , sys addr (host PA) , size , flags */
+ /* E906 view of SRAM space 0 and space 1 */
+ { SUN55I_E906_SRAM_SPACE0_DA, SUN55I_SRAM_SPACE0_SYS, SUN55I_SRAM_SPACE0_SIZE,
+ SUNXI_RPROC_ATT_IOMEM },
+ { SUN55I_E906_SRAM_SPACE1_DA, SUN55I_SRAM_SPACE1_SYS, SUN55I_SRAM_SPACE1_SIZE,
+ SUNXI_RPROC_ATT_IOMEM },
+
+ /* Host addresses, used for vrings and buffers Linux allocates in SRAM */
+ { SUN55I_SRAM_SPACE0_SYS, SUN55I_SRAM_SPACE0_SYS, SUN55I_SRAM_SPACE0_SIZE,
+ SUNXI_RPROC_ATT_IOMEM },
+ { SUN55I_SRAM_SPACE1_SYS, SUN55I_SRAM_SPACE1_SYS, SUN55I_SRAM_SPACE1_SIZE,
+ SUNXI_RPROC_ATT_IOMEM },
+};
+
+VISIBLE_IF_KUNIT
+const struct sunxi_rproc_cfg sun55i_riscv_cfg = {
+ .name = "XuanTie E906 RISC-V",
+ .att = sun55i_rproc_att,
+ .att_size = ARRAY_SIZE(sun55i_rproc_att),
+ .boot_reg_offset = SUN55I_E906_STA_ADD_REG,
+};
+EXPORT_SYMBOL_IF_KUNIT(sun55i_riscv_cfg);
+
+/* All register and SRAM access goes through these, so KUnit can model it */
+VISIBLE_IF_KUNIT
+u32 sunxi_rproc_readl(void __iomem *addr)
+{
+ KUNIT_STATIC_STUB_REDIRECT(sunxi_rproc_readl, addr);
+ return readl(addr);
+}
+EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_readl);
+
+VISIBLE_IF_KUNIT
+void sunxi_rproc_writel(u32 val, void __iomem *addr)
+{
+ KUNIT_STATIC_STUB_REDIRECT(sunxi_rproc_writel, val, addr);
+ writel(val, addr);
+}
+EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_writel);
+
+VISIBLE_IF_KUNIT
+void sunxi_rproc_memset_io(void __iomem *addr, int val, size_t len)
+{
+ KUNIT_STATIC_STUB_REDIRECT(sunxi_rproc_memset_io, addr, val, len);
+ memset_io(addr, val, len);
+}
+EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_memset_io);
+
+/* The core's address for host address @sys: the first ATT entry covering it */
+static int sunxi_rproc_sys_to_da(const struct sunxi_rproc_cfg *cfg, phys_addr_t sys,
+ size_t len, u64 *da)
+{
+ size_t i;
+
+ for (i = 0; i < cfg->att_size; i++) {
+ const struct sunxi_rproc_att *att = &cfg->att[i];
+
+ if (sys >= att->sa && sys + len <= att->sa + att->size) {
+ *da = att->da + (sys - att->sa);
+ return 0;
+ }
+ }
+
+ return -ENOENT;
+}
+
+/* Does [@da, @da + @len) overlap a window of the core's address map? */
+static bool sunxi_rproc_att_overlaps(const struct sunxi_rproc_cfg *cfg, u64 da, size_t len)
+{
+ size_t i;
+
+ for (i = 0; i < cfg->att_size; i++) {
+ const struct sunxi_rproc_att *att = &cfg->att[i];
+
+ if (da < att->da + att->size && att->da < da + len)
+ return true;
+ }
+
+ return false;
+}
+
+/* The mapping of host range [@sys, @sys + @len) if it lies in one of the SRAMs */
+static void __iomem *sunxi_rproc_sram_va(struct sunxi_rproc *priv, u64 sys, size_t len)
+{
+ if (priv->sram0_va && sys >= priv->sram0_phys &&
+ sys + len <= priv->sram0_phys + priv->sram0_size)
+ return priv->sram0_va + (sys - priv->sram0_phys);
+
+ if (priv->sram1_va && sys >= priv->sram1_phys &&
+ sys + len <= priv->sram1_phys + priv->sram1_size)
+ return priv->sram1_va + (sys - priv->sram1_phys);
+
+ return NULL;
+}
+
+VISIBLE_IF_KUNIT
+int sunxi_rproc_vq_notify(int id, void *ptr, void *data)
+{
+ KUNIT_STATIC_STUB_REDIRECT(sunxi_rproc_vq_notify, id, ptr, data);
+ rproc_vq_interrupt(data, id);
+ return 0;
+}
+EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_vq_notify);
+
+/* One channel carries all notifications: check every vring */
+VISIBLE_IF_KUNIT
+void sunxi_rproc_vq_work(struct work_struct *work)
+{
+ struct sunxi_rproc *priv = container_of(work, struct sunxi_rproc, vq_work);
+
+ idr_for_each(&priv->rproc->notifyids, sunxi_rproc_vq_notify, priv->rproc);
+}
+EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_vq_work);
+
+/*
+ * Mailbox rx callbacks run in atomic context, but the virtio/rpmsg receive
+ * path takes mutexes, so the vring notifications are handled from a work item.
+ */
+VISIBLE_IF_KUNIT
+void sunxi_rproc_mb_rx_callback(struct mbox_client *cl, void *data)
+{
+ struct sunxi_rproc *priv = container_of(cl, struct sunxi_rproc, cl);
+
+ queue_work(priv->workqueue, &priv->vq_work);
+}
+EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_mb_rx_callback);
+
+VISIBLE_IF_KUNIT
+irqreturn_t sunxi_rproc_crash_handler(int irq, void *data)
+{
+ struct sunxi_rproc *priv = data;
+
+ /*
+ * Stays masked until the next start(); recovery runs stop() first. A
+ * stop() that already disarmed it has disabled the line itself.
+ */
+ if (test_and_clear_bit(SUNXI_RPROC_CRASH_IRQ_ARMED, &priv->flags))
+ disable_irq_nosync(irq);
+
+ rproc_report_crash(priv->rproc, RPROC_FATAL_ERROR);
+
+ return IRQ_HANDLED;
+}
+EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_crash_handler);
+
+/*
+ * Crash dumps cover both SRAMs, by the core's addresses so a dump loads into a
+ * debugger as is, and every carveout outside them. rproc_resource_cleanup()
+ * drops the segments after every run; recovery keeps them.
+ */
+static void sunxi_rproc_add_dump_segments(struct rproc *rproc)
+{
+ struct sunxi_rproc *priv = rproc->priv;
+ u64 da0, da1;
+ unsigned int i;
+ int ret;
+
+ /* probe() checked that both SRAMs are in the core's address map */
+ ret = sunxi_rproc_sys_to_da(priv->cfg, priv->sram0_phys, priv->sram0_size, &da0);
+ if (!ret)
+ ret = sunxi_rproc_sys_to_da(priv->cfg, priv->sram1_phys,
+ priv->sram1_size, &da1);
+ if (!ret)
+ ret = rproc_coredump_add_segment(rproc, da0, priv->sram0_size);
+ if (!ret)
+ ret = rproc_coredump_add_segment(rproc, da1, priv->sram1_size);
+ for (i = 0; !ret && i < priv->num_rmem; i++) {
+ if (priv->rmem[i].is_iomem)
+ continue; /* placed on an SRAM, already dumped */
+ ret = rproc_coredump_add_segment(rproc, (u32)priv->rmem[i].phys,
+ priv->rmem[i].size);
+ }
+
+ /* A dump is a debugging aid: a missing segment does not stop the boot */
+ if (ret)
+ dev_warn(priv->dev, "crash dump segments incomplete: %d\n", ret);
+}
+
+VISIBLE_IF_KUNIT
+int sunxi_rproc_prepare(struct rproc *rproc)
+{
+ struct sunxi_rproc *priv = rproc->priv;
+ u32 remap_val;
+ int ret;
+
+ /* 0. Power up the RISC-V and SRAM power domains */
+ ret = pm_runtime_resume_and_get(priv->dev);
+ if (ret) {
+ dev_err(priv->dev, "failed to power up: %d\n", ret);
+ return ret;
+ }
+
+ /*
+ * 1. Hold the core in reset (a bootloader or an earlier kernel may
+ * have left it running), then deassert the configuration, SRAM and
+ * debug resets.
+ */
+ ret = reset_control_assert(priv->rst_core);
+ if (ret) {
+ dev_err(priv->dev, "failed to assert core reset: %d\n", ret);
+ goto err_pm_put;
+ }
+
+ ret = reset_control_deassert(priv->rst_cfg);
+ if (ret) {
+ dev_err(priv->dev, "failed to deassert cfg reset: %d\n", ret);
+ goto err_pm_put;
+ }
+
+ ret = reset_control_deassert(priv->rst_sram);
+ if (ret) {
+ dev_err(priv->dev, "failed to deassert sram reset: %d\n", ret);
+ goto err_assert_cfg;
+ }
+
+ ret = reset_control_deassert(priv->rst_debug);
+ if (ret) {
+ dev_err(priv->dev, "failed to deassert debug reset: %d\n", ret);
+ goto err_assert_sram;
+ }
+
+ /* 2. Enable interconnect bus and SRAM clocks */
+ ret = clk_prepare_enable(priv->clk_bus);
+ if (ret) {
+ dev_err(priv->dev, "failed to enable bus clock: %d\n", ret);
+ goto err_assert_debug;
+ }
+
+ ret = clk_prepare_enable(priv->clk_sram);
+ if (ret) {
+ dev_err(priv->dev, "failed to enable sram clock: %d\n", ret);
+ goto err_disable_bus_clk;
+ }
+
+ /* 3. Enable core clock */
+ ret = clk_prepare_enable(priv->clk_core);
+ if (ret) {
+ dev_err(priv->dev, "failed to enable core clock: %d\n", ret);
+ goto err_disable_sram_clk;
+ }
+
+ /* 4. Share SRAM A3_2 with the MCU subsystem: REMAP bit 1 */
+ remap_val = sunxi_rproc_readl(priv->remap_va) | SUNXI_REMAP_SRAMA3_2_BIT;
+ sunxi_rproc_writel(remap_val, priv->remap_va);
+ sunxi_rproc_readl(priv->remap_va);
+
+ /* 5. Clear both SRAM spaces */
+ sunxi_rproc_memset_io(priv->sram0_va, 0, priv->sram0_size);
+ sunxi_rproc_memset_io(priv->sram1_va, 0, priv->sram1_size);
+
+ return 0;
+
+err_disable_sram_clk:
+ clk_disable_unprepare(priv->clk_sram);
+err_disable_bus_clk:
+ clk_disable_unprepare(priv->clk_bus);
+err_assert_debug:
+ reset_control_assert(priv->rst_debug);
+err_assert_sram:
+ reset_control_assert(priv->rst_sram);
+err_assert_cfg:
+ reset_control_assert(priv->rst_cfg);
+err_pm_put:
+ pm_runtime_put_sync(priv->dev);
+ return ret;
+}
+EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_prepare);
+
+VISIBLE_IF_KUNIT
+int sunxi_rproc_unprepare(struct rproc *rproc)
+{
+ struct sunxi_rproc *priv = rproc->priv;
+
+ sunxi_rproc_writel(sunxi_rproc_readl(priv->remap_va) & ~SUNXI_REMAP_SRAMA3_2_BIT,
+ priv->remap_va);
+ sunxi_rproc_readl(priv->remap_va);
+
+ clk_disable_unprepare(priv->clk_core);
+ clk_disable_unprepare(priv->clk_sram);
+ clk_disable_unprepare(priv->clk_bus);
+
+ reset_control_assert(priv->rst_debug);
+ reset_control_assert(priv->rst_sram);
+ reset_control_assert(priv->rst_cfg);
+
+ /* Synchronous, so the domains are off before an unbind disables runtime PM */
+ pm_runtime_put_sync(priv->dev);
+
+ return 0;
+}
+EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_unprepare);
+
+VISIBLE_IF_KUNIT
+int sunxi_rproc_start(struct rproc *rproc)
+{
+ struct sunxi_rproc *priv = rproc->priv;
+ const struct sunxi_rproc_cfg *cfg = priv->cfg;
+ int ret;
+
+ dev_dbg(priv->dev, "Starting %s core at entry 0x%llx\n",
+ cfg->name, (u64)rproc->bootaddr);
+
+ if (rproc->bootaddr > U32_MAX)
+ return -EINVAL;
+
+ /* Program the reset vector and flush it before releasing the core */
+ sunxi_rproc_writel((u32)rproc->bootaddr, priv->cfg_va + cfg->boot_reg_offset);
+ sunxi_rproc_readl(priv->cfg_va + cfg->boot_reg_offset);
+
+ ret = reset_control_deassert(priv->rst_core);
+ if (ret) {
+ dev_err(priv->dev, "failed to release core reset: %d\n", ret);
+ return ret;
+ }
+
+ if (priv->crash_irq > 0 &&
+ !test_and_set_bit(SUNXI_RPROC_CRASH_IRQ_ARMED, &priv->flags))
+ enable_irq(priv->crash_irq);
+
+ return 0;
+}
+EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_start);
+
+static int sunxi_rproc_stop(struct rproc *rproc)
+{
+ struct sunxi_rproc *priv = rproc->priv;
+ const struct sunxi_rproc_cfg *cfg = priv->cfg;
+
+ dev_dbg(priv->dev, "Halting %s core...\n", cfg->name);
+
+ /* Assert reset so the core stops executing */
+ reset_control_assert(priv->rst_core);
+
+ if (priv->crash_irq > 0 &&
+ test_and_clear_bit(SUNXI_RPROC_CRASH_IRQ_ARMED, &priv->flags))
+ disable_irq(priv->crash_irq);
+
+ return 0;
+}
+
+/*
+ * Kick payloads must outlive sunxi_rproc_kick(): the mailbox core queues the
+ * message *pointer* while a previous request is still active and passes it to
+ * the controller later, from the poll timer or the TX-done interrupt. Use
+ * immutable static storage, one entry per vring, so nothing is rewritten
+ * while queued.
+ */
+static const u32 sunxi_rproc_kick_msg[] = { 0, 1 };
+
+VISIBLE_IF_KUNIT
+void sunxi_rproc_kick(struct rproc *rproc, int vqid)
+{
+ struct sunxi_rproc *priv = rproc->priv;
+ int ret;
+
+ if (!priv->mbox_chan) {
+ dev_warn_once(priv->dev, "vring kick without a mailbox (no mboxes in DT)\n");
+ return;
+ }
+
+ if (vqid < 0 || vqid >= ARRAY_SIZE(sunxi_rproc_kick_msg)) {
+ dev_err_ratelimited(priv->dev, "no kick payload for vring %d\n", vqid);
+ return;
+ }
+
+ ret = mbox_send_message(priv->mbox_chan,
+ (void *)&sunxi_rproc_kick_msg[vqid]);
+ if (ret < 0)
+ dev_err_ratelimited(priv->dev, "failed to send mailbox kick: %d\n", ret);
+}
+EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_kick);
+
+VISIBLE_IF_KUNIT
+void *sunxi_rproc_da_to_va(struct rproc *rproc, u64 da, size_t len, bool *is_iomem)
+{
+ struct sunxi_rproc *priv = rproc->priv;
+ const struct sunxi_rproc_cfg *cfg = priv->cfg;
+ void __iomem *va;
+ size_t i;
+
+ /* A zero length, or a da + len that wraps, matches no window */
+ if (len == 0 || da > U64_MAX - len)
+ return NULL;
+
+ /*
+ * The address translation table holds the core's views of the SRAMs
+ * and their host addresses. Only the SRAMs are handled here; DRAM
+ * firmware, trace, vrings and buffers are registered carveouts, which
+ * rproc_da_to_va() looks up when this returns NULL.
+ */
+ for (i = 0; i < cfg->att_size; i++) {
+ const struct sunxi_rproc_att *att = &cfg->att[i];
+
+ if (da < att->da || da + len > att->da + att->size)
+ continue;
+
+ va = sunxi_rproc_sram_va(priv, att->sa + (da - att->da), len);
+ /* is_iomem is only set for an address that is returned */
+ if (va && is_iomem)
+ *is_iomem = !!(att->flags & SUNXI_RPROC_ATT_IOMEM);
+ return (__force void *)va;
+ }
+
+ return NULL;
+}
+EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_da_to_va);
+
+/* The E906 is RV32: refuse firmware built for another core before it is loaded */
+static int sunxi_rproc_sanity_check(struct rproc *rproc, const struct firmware *fw)
+{
+ int ret;
+
+ ret = rproc_elf_sanity_check(rproc, fw);
+ if (ret)
+ return ret;
+
+ if (fw_elf_get_class(fw) != ELFCLASS32 ||
+ elf_hdr_get_e_machine(ELFCLASS32, fw->data) != EM_RISCV) {
+ dev_err(&rproc->dev, "firmware is not a 32-bit RISC-V ELF\n");
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+/*
+ * The memory regions are registered here rather than in prepare(): the core
+ * cleans up carveouts and dump segments only once parse_fw() has succeeded.
+ * The entries are allocated first, so a failure leaves nothing behind.
+ */
+VISIBLE_IF_KUNIT
+int sunxi_rproc_parse_fw(struct rproc *rproc, const struct firmware *fw)
+{
+ struct sunxi_rproc *priv = rproc->priv;
+ struct rproc_mem_entry *mem[SUNXI_RPROC_MAX_RMEM];
+ unsigned int i;
+ int ret;
+
+ for (i = 0; i < priv->num_rmem; i++) {
+ const struct sunxi_rproc_rmem *rmem = &priv->rmem[i];
+
+ mem[i] = rproc_mem_entry_init(priv->dev, rmem->va, (dma_addr_t)rmem->phys,
+ rmem->size, (u32)rmem->phys, NULL, NULL,
+ "%s", rmem->name);
+ if (!mem[i]) {
+ ret = -ENOMEM;
+ goto err_free_mem;
+ }
+ mem[i]->is_iomem = rmem->is_iomem;
+ }
+
+ /*
+ * -EINVAL: no resource table, or one the core rejects. The firmware
+ * then runs without virtio, as with stm32.
+ */
+ ret = rproc_elf_load_rsc_table(rproc, fw);
+ if (ret == -EINVAL)
+ dev_dbg(&rproc->dev, "no usable resource table in the firmware\n");
+ else if (ret)
+ goto err_free_mem;
+
+ for (i = 0; i < priv->num_rmem; i++)
+ rproc_add_carveout(rproc, mem[i]);
+
+ sunxi_rproc_add_dump_segments(rproc);
+
+ return 0;
+
+err_free_mem:
+ while (i--)
+ kfree(mem[i]);
+ return ret;
+}
+EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_parse_fw);
+
+VISIBLE_IF_KUNIT
+const struct rproc_ops sunxi_rproc_ops = {
+ .prepare = sunxi_rproc_prepare,
+ .unprepare = sunxi_rproc_unprepare,
+ .start = sunxi_rproc_start,
+ .stop = sunxi_rproc_stop,
+ .kick = sunxi_rproc_kick,
+ .da_to_va = sunxi_rproc_da_to_va,
+ .get_boot_addr = rproc_elf_get_boot_addr,
+ .load = rproc_elf_load_segments,
+ .parse_fw = sunxi_rproc_parse_fw,
+ .find_loaded_rsc_table = rproc_elf_find_loaded_rsc_table,
+ .sanity_check = sunxi_rproc_sanity_check,
+};
+EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_ops);
+
+static int sunxi_rproc_map_sram(struct platform_device *pdev, const char *name,
+ void __iomem **va, phys_addr_t *phys, size_t *size)
+{
+ struct device *dev = &pdev->dev;
+ struct sunxi_rproc *priv = platform_get_drvdata(pdev);
+ struct resource *res;
+ u64 da;
+
+ res = platform_get_resource_byname(pdev, IORESOURCE_MEM, name);
+ *va = devm_ioremap_resource_wc(dev, res);
+ if (IS_ERR(*va))
+ return dev_err_probe(dev, PTR_ERR(*va), "failed to map '%s'\n", name);
+
+ /* The address map of the core comes from the SoC table: they must agree */
+ if (sunxi_rproc_sys_to_da(priv->cfg, res->start, resource_size(res), &da))
+ return dev_err_probe(dev, -EINVAL, "'%s' %pR is not in the %s address map\n",
+ name, res, priv->cfg->name);
+
+ *phys = res->start;
+ *size = resource_size(res);
+
+ return 0;
+}
+
+static int sunxi_rproc_register_mem(struct platform_device *pdev, struct sunxi_rproc *priv)
+{
+ struct device *dev = &pdev->dev;
+ struct resource *res;
+ int ret;
+
+ priv->cfg_va = devm_platform_ioremap_resource_byname(pdev, "cfg");
+ if (IS_ERR(priv->cfg_va))
+ return dev_err_probe(dev, PTR_ERR(priv->cfg_va), "failed to map 'cfg'\n");
+
+ ret = sunxi_rproc_map_sram(pdev, "sram-a3-0", &priv->sram0_va, &priv->sram0_phys,
+ &priv->sram0_size);
+ if (ret)
+ return ret;
+
+ ret = sunxi_rproc_map_sram(pdev, "sram-a3-1", &priv->sram1_va, &priv->sram1_phys,
+ &priv->sram1_size);
+ if (ret)
+ return ret;
+
+ /*
+ * The remap register sits in the PRCM block and is shared with the DSP,
+ * so map it without claiming the region.
+ */
+ res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "remap");
+ if (!res)
+ return dev_err_probe(dev, -EINVAL, "missing 'remap' register\n");
+ priv->remap_va = devm_ioremap(dev, res->start, resource_size(res));
+ if (!priv->remap_va)
+ return dev_err_probe(dev, -ENOMEM, "failed to map 'remap'\n");
+
+ return 0;
+}
+
+static void sunxi_rproc_release_dma_pool(void *dev)
+{
+ of_reserved_mem_device_release(dev);
+}
+
+static int sunxi_rproc_parse_memory_regions(struct sunxi_rproc *priv)
+{
+ struct device *dev = priv->dev;
+ struct device_node *np = dev->of_node;
+ int i, count, ret;
+
+ count = of_reserved_mem_region_count(np);
+ if (count <= 0)
+ return 0;
+ if (count > SUNXI_RPROC_MAX_RMEM)
+ return dev_err_probe(dev, -EINVAL, "too many memory-region entries (%d)\n",
+ count);
+
+ /* memory-region[0] is the DMA pool for vrings and vdev buffers */
+ ret = of_reserved_mem_device_init(dev);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to set up the DMA pool\n");
+
+ ret = devm_add_action_or_reset(dev, sunxi_rproc_release_dma_pool, dev);
+ if (ret)
+ return ret;
+
+ for (i = 0; i < count; i++) {
+ struct sunxi_rproc_rmem *rmem = &priv->rmem[i];
+ struct resource res;
+ void __iomem *sram;
+
+ ret = of_reserved_mem_region_to_resource(np, i, &res);
+ if (ret)
+ return dev_err_probe(dev, ret, "invalid memory-region %d\n", i);
+
+ /* The core addresses memory with 32 bits (carveout da is a u32) */
+ if (res.end > U32_MAX)
+ return dev_err_probe(dev, -EINVAL,
+ "memory-region %d above 4 GiB\n", i);
+
+ if (of_property_read_string_index(np, "memory-region-names", i, &rmem->name))
+ rmem->name = devm_kasprintf(dev, GFP_KERNEL, "rmem%d", i);
+ if (!rmem->name)
+ return -ENOMEM;
+
+ rmem->phys = res.start;
+ rmem->size = resource_size(&res);
+
+ /* A pool placed on one of the core's SRAMs reuses its mapping */
+ sram = sunxi_rproc_sram_va(priv, rmem->phys, rmem->size);
+ if (sram) {
+ rmem->va = (__force void *)sram;
+ rmem->is_iomem = true;
+ priv->num_rmem++;
+ continue;
+ }
+
+ /*
+ * DRAM is seen 1:1 by the core, outside the windows of its
+ * address map: a region inside one would reach SRAM instead.
+ */
+ if (sunxi_rproc_att_overlaps(priv->cfg, rmem->phys, rmem->size))
+ return dev_err_probe(dev, -EINVAL,
+ "memory-region %d %pR is not visible to the core\n",
+ i, &res);
+
+ /* Same attribute as the DMA pool, so no WB/WC alias */
+ rmem->va = devm_memremap(dev, rmem->phys, rmem->size, MEMREMAP_WC);
+ if (IS_ERR(rmem->va))
+ return dev_err_probe(dev, PTR_ERR(rmem->va),
+ "failed to map %s\n", rmem->name);
+ priv->num_rmem++;
+ }
+
+ return 0;
+}
+
+static void sunxi_rproc_free_mbox(struct sunxi_rproc *priv)
+{
+ if (priv->mbox_chan)
+ mbox_free_channel(priv->mbox_chan);
+ priv->mbox_chan = NULL;
+
+ /* No rx callback can queue the work any more once the channel is freed */
+ cancel_work_sync(&priv->vq_work);
+}
+
+static int sunxi_rproc_probe(struct platform_device *pdev)
+{
+ static const char * const pd_names[] = { "riscv", "sram" };
+ static const struct dev_pm_domain_attach_data pd_data = {
+ .pd_names = pd_names,
+ .num_pd_names = ARRAY_SIZE(pd_names),
+ };
+ struct device *dev = &pdev->dev;
+ const char *fw_name = "riscv-firmware.elf";
+ struct dev_pm_domain_list *pd_list;
+ struct sunxi_rproc *priv;
+ struct rproc *rproc;
+ int crash_irq;
+ int ret;
+
+ /* firmware-name is optional: the default is kept only when it is absent */
+ ret = rproc_of_parse_firmware(dev, 0, &fw_name);
+ if (ret && ret != -EINVAL)
+ return dev_err_probe(dev, ret, "invalid firmware-name\n");
+
+ rproc = devm_rproc_alloc(dev, dev_name(dev), &sunxi_rproc_ops,
+ fw_name, sizeof(*priv));
+ if (!rproc)
+ return -ENOMEM;
+
+ ret = dma_set_coherent_mask(dev, DMA_BIT_MASK(32));
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to set 32-bit DMA coherent mask\n");
+
+ priv = rproc->priv;
+ priv->rproc = rproc;
+ priv->dev = dev;
+ priv->cfg = device_get_match_data(dev);
+ if (!priv->cfg)
+ return -ENODEV;
+ platform_set_drvdata(pdev, priv);
+
+ /* Crash dumps are 32-bit RISC-V ELF files */
+ rproc_coredump_set_elf_info(rproc, ELFCLASS32, EM_RISCV);
+
+ /*
+ * The core and its SRAM sit in two PPU power domains. With more than one
+ * domain the driver core does not attach them, so do it here; runtime PM
+ * of this device then powers both, from prepare() to unprepare().
+ */
+ ret = devm_pm_domain_attach_list(dev, &pd_data, &pd_list);
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "failed to attach power domains\n");
+
+ ret = devm_pm_runtime_enable(dev);
+ if (ret)
+ return ret;
+
+ priv->clk_bus = devm_clk_get(dev, "bus");
+ if (IS_ERR(priv->clk_bus))
+ return dev_err_probe(dev, PTR_ERR(priv->clk_bus), "failed to get 'bus' clock\n");
+
+ priv->clk_core = devm_clk_get(dev, "core");
+ if (IS_ERR(priv->clk_core))
+ return dev_err_probe(dev, PTR_ERR(priv->clk_core), "failed to get 'core' clock\n");
+
+ priv->clk_sram = devm_clk_get(dev, "sram");
+ if (IS_ERR(priv->clk_sram))
+ return dev_err_probe(dev, PTR_ERR(priv->clk_sram), "failed to get 'sram' clock\n");
+
+ priv->rst_cfg = devm_reset_control_get_exclusive(dev, "cfg");
+ if (IS_ERR(priv->rst_cfg))
+ return dev_err_probe(dev, PTR_ERR(priv->rst_cfg), "failed to get 'cfg' reset\n");
+
+ priv->rst_core = devm_reset_control_get_exclusive(dev, "core");
+ if (IS_ERR(priv->rst_core))
+ return dev_err_probe(dev, PTR_ERR(priv->rst_core), "failed to get 'core' reset\n");
+
+ priv->rst_sram = devm_reset_control_get_exclusive(dev, "sram");
+ if (IS_ERR(priv->rst_sram))
+ return dev_err_probe(dev, PTR_ERR(priv->rst_sram), "failed to get 'sram' reset\n");
+
+ priv->rst_debug = devm_reset_control_get_exclusive(dev, "debug");
+ if (IS_ERR(priv->rst_debug))
+ return dev_err_probe(dev, PTR_ERR(priv->rst_debug),
+ "failed to get 'debug' reset\n");
+
+ /* Configuration block, both SRAM spaces and the remap register */
+ ret = sunxi_rproc_register_mem(pdev, priv);
+ if (ret)
+ return ret;
+
+ /* memory-region: DMA pool and carveouts */
+ ret = sunxi_rproc_parse_memory_regions(priv);
+ if (ret)
+ return ret;
+
+ /* Optional crash interrupt */
+ crash_irq = platform_get_irq_optional(pdev, 0);
+ if (crash_irq > 0) {
+ ret = devm_request_threaded_irq(dev, crash_irq, NULL,
+ sunxi_rproc_crash_handler,
+ IRQF_ONESHOT | IRQF_NO_AUTOEN,
+ "sunxi-rproc-crash",
+ priv);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to request crash IRQ\n");
+ priv->crash_irq = crash_irq;
+ } else if (crash_irq != -ENXIO) {
+ return dev_err_probe(dev, crash_irq, "failed to get crash IRQ\n");
+ }
+
+ /* Mailbox (optional: without it the core runs standalone) */
+ INIT_WORK(&priv->vq_work, sunxi_rproc_vq_work);
+
+ if (of_property_present(dev->of_node, "mboxes")) {
+ priv->workqueue = devm_alloc_ordered_workqueue(dev, "%s", WQ_HIGHPRI,
+ dev_name(dev));
+ if (!priv->workqueue)
+ return -ENOMEM;
+
+ priv->cl.dev = dev;
+ priv->cl.rx_callback = sunxi_rproc_mb_rx_callback;
+ priv->cl.tx_block = false;
+ priv->cl.knows_txdone = false;
+
+ /* One channel carries kicks to the core and notifications from it */
+ priv->mbox_chan = mbox_request_channel(&priv->cl, 0);
+ if (IS_ERR(priv->mbox_chan)) {
+ ret = PTR_ERR(priv->mbox_chan);
+ priv->mbox_chan = NULL;
+ return dev_err_probe(dev, ret, "failed to request mailbox\n");
+ }
+ } else {
+ dev_dbg(dev, "no mboxes configured; operating in standalone mode\n");
+ }
+
+ ret = rproc_add(rproc);
+ if (ret) {
+ sunxi_rproc_free_mbox(priv);
+ return dev_err_probe(dev, ret, "failed to register rproc device\n");
+ }
+
+ return 0;
+}
+
+static void sunxi_rproc_remove(struct platform_device *pdev)
+{
+ struct sunxi_rproc *priv = platform_get_drvdata(pdev);
+
+ /*
+ * rproc_del() stops the core and removes the virtio devices, which may
+ * still kick, so the mailbox must outlive it.
+ */
+ rproc_del(priv->rproc);
+
+ sunxi_rproc_free_mbox(priv);
+}
+
+static const struct of_device_id sunxi_rproc_of_match[] = {
+ /* The A523, A527 and T527 are one die, so one compatible covers all three */
+ { .compatible = "allwinner,sun55i-a523-rproc", .data = &sun55i_riscv_cfg },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, sunxi_rproc_of_match);
+
+static struct platform_driver sunxi_rproc_driver = {
+ .probe = sunxi_rproc_probe,
+ .remove = sunxi_rproc_remove,
+ .driver = {
+ .name = DRIVER_NAME,
+ .of_match_table = sunxi_rproc_of_match,
+ },
+};
+module_platform_driver(sunxi_rproc_driver);
+
+MODULE_AUTHOR("Tim Michals <tcmichals@gmail.com>");
+MODULE_DESCRIPTION("Allwinner XuanTie E906 RISC-V Remoteproc Driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/remoteproc/sunxi_rproc.h b/drivers/remoteproc/sunxi_rproc.h
new file mode 100644
index 000000000000..77176b688443
--- /dev/null
+++ b/drivers/remoteproc/sunxi_rproc.h
@@ -0,0 +1,124 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _SUNXI_RPROC_H_
+#define _SUNXI_RPROC_H_
+
+#include <linux/bits.h>
+#include <linux/irqreturn.h>
+#include <linux/mailbox_client.h>
+#include <linux/types.h>
+#include <linux/workqueue.h>
+
+struct clk;
+struct device;
+struct firmware;
+struct reset_control;
+struct rproc;
+struct rproc_ops;
+
+/*
+ * XuanTie E906 view of its two SRAM spaces (Allwinner sun55iw3 vendor DT
+ * "memory-mappings"). DRAM is seen 1:1 from 0x40040000 up, and 0x20000 is
+ * the HiFi4 DSP RAM (also 1:1); neither goes through this table.
+ */
+#define SUN55I_E906_SRAM_SPACE0_DA 0x3ffc0000UL
+#define SUN55I_E906_SRAM_SPACE1_DA 0x40000000UL
+
+/* Host physical addresses and sizes of the two SRAM spaces */
+#define SUN55I_SRAM_SPACE0_SYS 0x07280000UL
+#define SUN55I_SRAM_SPACE0_SIZE 0x00040000UL /* 256 KiB */
+#define SUN55I_SRAM_SPACE1_SYS 0x072c0000UL
+#define SUN55I_SRAM_SPACE1_SIZE 0x00040000UL /* 256 KiB */
+
+/* Address translation table entry flags */
+#define SUNXI_RPROC_ATT_IOMEM BIT(30)
+
+struct sunxi_rproc_att {
+ u64 da;
+ u64 sa;
+ size_t size;
+ u32 flags;
+};
+
+/* Configuration block: boot address register */
+#define SUN55I_E906_STA_ADD_REG 0x0204
+
+/* Remap register (R-PRCM 0x07010364), shared with the HiFi4 DSP: bit 1 is ours */
+#define SUNXI_REMAP_SRAMA3_2_BIT BIT(1) /* share SRAM A3_2 with the MCU */
+
+#define SUNXI_RPROC_MAX_RMEM 3
+#define SUNXI_RPROC_CRASH_IRQ_ARMED 0
+
+struct sunxi_rproc_rmem {
+ const char *name;
+ void *va;
+ phys_addr_t phys;
+ size_t size;
+ bool is_iomem;
+};
+
+struct sunxi_rproc_cfg {
+ const char *name;
+ /* Core views come first: a host address maps to the first entry covering it */
+ const struct sunxi_rproc_att *att;
+ size_t att_size;
+ u32 boot_reg_offset;
+};
+
+struct sunxi_rproc {
+ struct rproc *rproc;
+ struct device *dev;
+ const struct sunxi_rproc_cfg *cfg;
+
+ /* Clocks and resets */
+ struct clk *clk_bus;
+ struct clk *clk_core;
+ struct clk *clk_sram;
+ struct reset_control *rst_cfg;
+ struct reset_control *rst_core;
+ struct reset_control *rst_sram;
+ struct reset_control *rst_debug;
+
+ /* Configuration block, remap register and the two SRAM spaces */
+ void __iomem *cfg_va;
+
+ void __iomem *remap_va;
+
+ void __iomem *sram0_va;
+ phys_addr_t sram0_phys;
+ size_t sram0_size;
+
+ void __iomem *sram1_va;
+ phys_addr_t sram1_phys;
+ size_t sram1_size;
+
+ /* memory-region entries, the crash interrupt and the mailbox */
+ struct sunxi_rproc_rmem rmem[SUNXI_RPROC_MAX_RMEM];
+ unsigned int num_rmem;
+ unsigned long flags;
+ int crash_irq;
+ struct mbox_client cl;
+ struct mbox_chan *mbox_chan;
+ struct workqueue_struct *workqueue;
+ struct work_struct vq_work;
+};
+
+#if IS_ENABLED(CONFIG_KUNIT)
+extern const struct sunxi_rproc_cfg sun55i_riscv_cfg;
+extern const struct rproc_ops sunxi_rproc_ops;
+
+u32 sunxi_rproc_readl(void __iomem *addr);
+void sunxi_rproc_writel(u32 val, void __iomem *addr);
+void sunxi_rproc_memset_io(void __iomem *addr, int val, size_t len);
+int sunxi_rproc_vq_notify(int id, void *ptr, void *data);
+void sunxi_rproc_vq_work(struct work_struct *work);
+irqreturn_t sunxi_rproc_crash_handler(int irq, void *data);
+int sunxi_rproc_prepare(struct rproc *rproc);
+int sunxi_rproc_unprepare(struct rproc *rproc);
+int sunxi_rproc_start(struct rproc *rproc);
+int sunxi_rproc_parse_fw(struct rproc *rproc, const struct firmware *fw);
+void sunxi_rproc_kick(struct rproc *rproc, int vqid);
+void sunxi_rproc_mb_rx_callback(struct mbox_client *cl, void *data);
+void *sunxi_rproc_da_to_va(struct rproc *rproc, u64 da, size_t len, bool *is_iomem);
+#endif
+
+#endif /* _SUNXI_RPROC_H_ */
--
2.53.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v3 5/7] arm64: dts: allwinner: a523: Add message box and remoteproc nodes
2026-11-01 18:57 [PATCH v3 0/7] Add Allwinner A523 message box and XuanTie E906 remoteproc support Tim Michals
` (3 preceding siblings ...)
2026-11-01 18:57 ` [PATCH v3 4/7] remoteproc: sunxi: Add Allwinner A523 XuanTie E906 driver Tim Michals
@ 2026-11-01 18:57 ` Tim Michals
2026-11-01 18:57 ` [PATCH v3 6/7] mailbox: sun55i: Add KUnit tests Tim Michals
2026-11-01 18:57 ` [PATCH v3 7/7] remoteproc: sunxi: " Tim Michals
6 siblings, 0 replies; 12+ messages in thread
From: Tim Michals @ 2026-11-01 18:57 UTC (permalink / raw)
To: Jassi Brar, Bjorn Andersson, Mathieu Poirier, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Chen-Yu Tsai, Jernej Skrabec,
Samuel Holland
Cc: linux-sunxi, linux-remoteproc, devicetree, linux-arm-kernel,
linux-kernel, Tim Michals
Add the message box node, with its four register banks, bank clocks
and resets, bank interrupts, and the XuanTie E906 remoteproc node,
with the MCU CCU clocks and resets, the RISC-V and SRAM power domains
and message box channel 9.
The remoteproc node is disabled; a board enables it together with its
firmware name and reserved memory regions.
Signed-off-by: Tim Michals <tcmichals@gmail.com>
---
Changes in v3:
- Message box: all four banks with their clocks and resets; the DSP and
CPUS bank addresses were swapped in v2; remote bank interrupts
corrected to SPI 181/174/205
- Message box: interrupt-names
- Remoteproc: debug reset, RISC-V and SRAM power domains, one message
box channel; the message box clock and reset moved to the message box
node; reg-names sram-a3-0 and sram-a3-1
---
.../arm64/boot/dts/allwinner/sun55i-a523.dtsi | 47 +++++++++++++++++++
1 file changed, 47 insertions(+)
diff --git a/arch/arm64/boot/dts/allwinner/sun55i-a523.dtsi b/arch/arm64/boot/dts/allwinner/sun55i-a523.dtsi
index 85ef492ffeae..316698b999c6 100644
--- a/arch/arm64/boot/dts/allwinner/sun55i-a523.dtsi
+++ b/arch/arm64/boot/dts/allwinner/sun55i-a523.dtsi
@@ -605,6 +605,31 @@ dma: dma-controller@3002000 {
#dma-cells = <1>;
};
+ msgbox: mailbox@3003000 {
+ compatible = "allwinner,sun55i-a523-msgbox";
+ reg = <0x03003000 0x1000>,
+ <0x07120000 0x1000>,
+ <0x07094000 0x1000>,
+ <0x07136000 0x1000>;
+ reg-names = "arm", "dsp", "cpus", "rv";
+ clocks = <&ccu CLK_BUS_MSGBOX>,
+ <&mcu_ccu CLK_BUS_MCU_DSP_MSGBOX>,
+ <&r_ccu CLK_BUS_R_MSGBOX>,
+ <&mcu_ccu CLK_BUS_MCU_RISCV_MSGBOX>;
+ clock-names = "arm", "dsp", "cpus", "rv";
+ resets = <&ccu RST_BUS_MSGBOX>,
+ <&mcu_ccu RST_BUS_MCU_DSP_MSGBOX>,
+ <&r_ccu RST_BUS_R_MSGBOX>,
+ <&mcu_ccu RST_BUS_MCU_RISCV_MSGBOX>;
+ reset-names = "arm", "dsp", "cpus", "rv";
+ interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>,
+ <GIC_SPI 181 IRQ_TYPE_LEVEL_HIGH>,
+ <GIC_SPI 174 IRQ_TYPE_LEVEL_HIGH>,
+ <GIC_SPI 205 IRQ_TYPE_LEVEL_HIGH>;
+ interrupt-names = "arm", "dsp", "cpus", "rv";
+ #mbox-cells = <1>;
+ };
+
sid: efuse@3006000 {
compatible = "allwinner,sun55i-a523-sid",
"allwinner,sun50i-a64-sid";
@@ -1144,5 +1169,27 @@ npu: npu@7122000 {
resets = <&mcu_ccu RST_BUS_MCU_NPU>;
power-domains = <&ppu PD_NPU>;
};
+
+ rproc: remoteproc@7130000 {
+ compatible = "allwinner,sun55i-a523-rproc";
+ reg = <0x07130000 0x1000>,
+ <0x07280000 0x40000>,
+ <0x072c0000 0x40000>,
+ <0x07010364 0x4>;
+ reg-names = "cfg", "sram-a3-0", "sram-a3-1", "remap";
+ clocks = <&mcu_ccu CLK_BUS_MCU_RISCV_CFG>,
+ <&mcu_ccu CLK_MCU_RISCV>,
+ <&mcu_ccu CLK_BUS_MCU_PUBSRAM>;
+ clock-names = "bus", "core", "sram";
+ resets = <&mcu_ccu RST_BUS_MCU_RISCV_CFG>,
+ <&mcu_ccu RST_BUS_MCU_RISCV_CORE>,
+ <&mcu_ccu RST_BUS_MCU_PUBSRAM>,
+ <&mcu_ccu RST_BUS_MCU_RISCV_DEBUG>;
+ reset-names = "cfg", "core", "sram", "debug";
+ power-domains = <&ppu PD_RISCV>, <&ppu PD_SRAM>;
+ power-domain-names = "riscv", "sram";
+ mboxes = <&msgbox 9>;
+ status = "disabled";
+ };
};
};
--
2.53.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v3 6/7] mailbox: sun55i: Add KUnit tests
2026-11-01 18:57 [PATCH v3 0/7] Add Allwinner A523 message box and XuanTie E906 remoteproc support Tim Michals
` (4 preceding siblings ...)
2026-11-01 18:57 ` [PATCH v3 5/7] arm64: dts: allwinner: a523: Add message box and remoteproc nodes Tim Michals
@ 2026-11-01 18:57 ` Tim Michals
2026-11-01 18:57 ` [PATCH v3 7/7] remoteproc: sunxi: " Tim Michals
6 siblings, 0 replies; 12+ messages in thread
From: Tim Michals @ 2026-11-01 18:57 UTC (permalink / raw)
To: Jassi Brar, Bjorn Andersson, Mathieu Poirier, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Chen-Yu Tsai, Jernej Skrabec,
Samuel Holland
Cc: linux-sunxi, linux-remoteproc, devicetree, linux-arm-kernel,
linux-kernel, Tim Michals
Add 37 KUnit tests for the sun55i message box driver: the routing of
all 12 channels, sending on every channel, message order on receive and
send, a message for a full FIFO held until there is room (behind the
earlier ones, dropped by shutdown, and dropped when its blocking send
timed out), last_tx_done() and peek_data() against the FIFO count,
startup and shutdown dropping stale messages (bounded by the FIFO
depth), and the interrupt handler (spurious and disabled-channel
interrupts, empty FIFOs, several channels and routes in one pass, the
drain bound, the pending bit cleared after the drain, delivery outside
the lock to a client that replies from its callback, no crosstalk
between channels). The TX-done line runs on a real descriptor of the
dummy irq chip: requested before the receive interrupt is enabled,
shared by the channels of a bank, freed by the last shutdown, TX done
reported to the core, and a refused message resent through it.
The driver's register helpers are redirected with KUnit static stubs to
mock banks that model the hardware: each channel has an 8-deep FIFO,
reading it pops and writing it pushes (a write to a full FIFO fails the
test), a read pending bit clears only once its FIFO is empty, and a
write pending bit is raised while the FIFO has room and cleared only
while it is full, as measured on the board. Under forced interrupt
threading the registered handler is run from the test thread.
Signed-off-by: Tim Michals <tcmichals@gmail.com>
---
Changes in v3:
- Moved after the drivers
- depends on SUN55I_MSGBOX (Sashiko)
- Register access is redirected with KUnit static stubs to mock banks
that model the hardware (8-deep FIFOs, the read and write status as
measured). No readl() on plain memory, so no architecture restriction
and no endianness issue (Sashiko)
- File named sun55i_msgbox_kunit.c (kunit/style.rst)
- The startup flush test now checks that the FIFO is drained; it
passed through the failsafe before (Sashiko)
- New tests: drain before clearing the pending bit, full FIFO never
written (message held in timer mode), TX done by write interrupt,
delivery outside the lock, message order, the abandoned held message,
the TX-done line through a real interrupt descriptor, a refused
message resent through the core
- Duplicate tests merged (routing is one table)
---
drivers/mailbox/Kconfig | 12 +
drivers/mailbox/Makefile | 1 +
drivers/mailbox/sun55i_msgbox_kunit.c | 1387 +++++++++++++++++++++++++
3 files changed, 1400 insertions(+)
create mode 100644 drivers/mailbox/sun55i_msgbox_kunit.c
diff --git a/drivers/mailbox/Kconfig b/drivers/mailbox/Kconfig
index 370a55c8dc91..3a83c1fac13b 100644
--- a/drivers/mailbox/Kconfig
+++ b/drivers/mailbox/Kconfig
@@ -344,6 +344,18 @@ config SUN55I_MSGBOX
To compile this driver as a module, choose M here: the module will
be called sun55i-msgbox.
+config SUN55I_MSGBOX_KUNIT_TEST
+ tristate "KUnit tests for Allwinner sun55i msgbox" if !KUNIT_ALL_TESTS
+ depends on SUN55I_MSGBOX && KUNIT
+ default KUNIT_ALL_TESTS
+ help
+ KUnit tests for the Allwinner sun55i-msgbox driver. They run the
+ channel operations (send, peek, last_tx_done, startup, shutdown)
+ and the interrupt handlers against mock register banks that model
+ the 8-deep FIFOs and the read and write status registers, and
+ request, share and free the TX-done line on a dummy interrupt.
+ Say Y here to run these tests during boot or via kunit.py.
+
config SPRD_MBOX
tristate "Spreadtrum Mailbox"
depends on ARCH_SPRD || COMPILE_TEST
diff --git a/drivers/mailbox/Makefile b/drivers/mailbox/Makefile
index 40024aa906a6..d2c0f705818b 100644
--- a/drivers/mailbox/Makefile
+++ b/drivers/mailbox/Makefile
@@ -72,6 +72,7 @@ obj-$(CONFIG_ZYNQMP_IPI_MBOX) += zynqmp-ipi-mailbox.o
obj-$(CONFIG_SUN6I_MSGBOX) += sun6i-msgbox.o
obj-$(CONFIG_SUN55I_MSGBOX) += sun55i-msgbox.o
+obj-$(CONFIG_SUN55I_MSGBOX_KUNIT_TEST) += sun55i_msgbox_kunit.o
obj-$(CONFIG_SPRD_MBOX) += sprd-mailbox.o
diff --git a/drivers/mailbox/sun55i_msgbox_kunit.c b/drivers/mailbox/sun55i_msgbox_kunit.c
new file mode 100644
index 000000000000..f95f69762f78
--- /dev/null
+++ b/drivers/mailbox/sun55i_msgbox_kunit.c
@@ -0,0 +1,1387 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for the Allwinner sun55i Message Box driver (sun55i-msgbox.c)
+ *
+ * Covers:
+ * - the 12-channel routing table
+ * - send_data, last_tx_done, peek_data, startup and shutdown; in timer
+ * mode a message for a full FIFO is held until last_tx_done() has room
+ * - TX done by write IRQ: armed after every send (and for a full FIFO),
+ * the handler disarms, acks and ignores channels it did not arm; a
+ * refused message is resent through the core once a slot is free; a
+ * startup() that cannot get the line never enables the receive IRQ,
+ * and the shutdown() the core then calls leaves the line alone;
+ * shutdown() disarms again after a handler re-armed the channel
+ * - the interrupt handler: delivery after the drain, outside the lock (a
+ * client may reply from its callback), per-channel isolation, several
+ * channels and ports in one pass, and the FIFO_MAX drain bound
+ *
+ * Directly invokes driver operations. All register access is redirected
+ * (KUnit static stubs) to mock banks that model the hardware: each message
+ * FIFO is an 8-deep queue (a read pops, a write pushes), a read pending bit
+ * only clears once its FIFO is empty, and a write pending bit is raised
+ * while its FIFO has room and only clears while it is full. The TX-done
+ * line tests use a real interrupt descriptor on the dummy irq chip.
+ * The stubs follow the test task, so a real interrupt taken while a test runs
+ * is redirected too: accesses outside the mock banks from interrupt context
+ * go to the hardware.
+ *
+ * Copyright (C) 2026 Tim Michals <tcmichals@gmail.com>
+ */
+
+#include <kunit/device.h>
+#include <kunit/static_stub.h>
+#include <kunit/test.h>
+#include <linux/io.h>
+#include <linux/irq.h>
+#include <linux/irqdesc.h>
+#include <linux/mailbox_client.h>
+#include "sun55i-msgbox.h"
+
+/* =============== Channel Routing Table Tests =============== */
+
+static void test_chan_to_route_all(struct kunit *test)
+{
+ /* local_n, p, remote_id, remote_n for host channels 0..11 */
+ static const int expect[SUN55I_NUM_CHANS][4] = {
+ { 0, 0, SUN55I_PROC_CPUS, 0 }, { 0, 1, SUN55I_PROC_CPUS, 0 },
+ { 0, 2, SUN55I_PROC_CPUS, 0 }, { 0, 3, SUN55I_PROC_CPUS, 0 },
+ { 1, 0, SUN55I_PROC_DSP, 0 }, { 1, 1, SUN55I_PROC_DSP, 0 },
+ { 1, 2, SUN55I_PROC_DSP, 0 }, { 1, 3, SUN55I_PROC_DSP, 0 },
+ { 2, 0, SUN55I_PROC_RV, 2 }, { 2, 1, SUN55I_PROC_RV, 2 },
+ { 2, 2, SUN55I_PROC_RV, 2 }, { 2, 3, SUN55I_PROC_RV, 2 },
+ };
+ int ch, local_n, p, remote_id, remote_n;
+
+ for (ch = 0; ch < SUN55I_NUM_CHANS; ch++) {
+ sun55i_msgbox_chan_to_route(ch, &local_n, &p, &remote_id, &remote_n);
+ KUNIT_EXPECT_EQ_MSG(test, local_n, expect[ch][0], "channel %d", ch);
+ KUNIT_EXPECT_EQ_MSG(test, p, expect[ch][1], "channel %d", ch);
+ KUNIT_EXPECT_EQ_MSG(test, remote_id, expect[ch][2], "channel %d", ch);
+ KUNIT_EXPECT_EQ_MSG(test, remote_n, expect[ch][3], "channel %d", ch);
+ }
+}
+
+/* =============== Functional Driver Ops Tests (Mock MMIO) =============== */
+
+struct mock_rx_sink {
+ struct mbox_client client;
+ int count;
+ u32 last_msg;
+ u32 msgs[SUN55I_FIFO_MAX * 2];
+};
+
+/* One channel's message FIFO */
+struct mock_fifo {
+ u32 msg[SUN55I_FIFO_MAX];
+ int head, len;
+};
+
+struct mock_msgbox_fixture {
+ struct sun55i_msgbox mbox;
+ struct mbox_chan chans[SUN55I_NUM_CHANS];
+ struct mock_rx_sink sinks[SUN55I_NUM_CHANS];
+ u32 regs[SUN55I_MAX_PROCESSORS][0x400 / 4];
+ /* Hardware defect: reading a FIFO does not lower its message count */
+ bool fifo_stuck;
+ int fifo_reads;
+ /* Next write that disarms a channel's write IRQ is undone, as a handler would */
+ bool rearm_on_disarm;
+ /* Every bit ever written to the host bank's READ_IRQ_ENABLE, per port */
+ u32 rd_en_written[SUN55I_NUM_ROUTES];
+ /* Message FIFOs: bank, port n, channel p */
+ struct mock_fifo fifo[SUN55I_MAX_PROCESSORS][SUN55I_MAX_PROCESSORS][SUN55I_CHANS_PER_PROC];
+};
+
+/* Test-side access: the registers as stored, no hardware behaviour */
+static inline void mock_write_reg(struct mock_msgbox_fixture *fix, int proc, u32 reg_idx, u32 val)
+{
+ fix->regs[proc][reg_idx] = val;
+}
+
+static inline u32 mock_read_reg(struct mock_msgbox_fixture *fix, int proc, u32 reg_idx)
+{
+ return fix->regs[proc][reg_idx];
+}
+
+/* Find the mock register behind a driver address: bank, port n, offset in the port */
+static u32 *mock_decode(void __iomem *addr, int *bank, u32 *n, u32 *reg)
+{
+ struct mock_msgbox_fixture *fix = kunit_get_current_test()->priv;
+ uintptr_t a = (uintptr_t)addr;
+ int proc;
+
+ for (proc = 0; proc < SUN55I_MAX_PROCESSORS; proc++) {
+ uintptr_t base = (uintptr_t)fix->regs[proc];
+
+ if (a >= base && a < base + sizeof(fix->regs[proc])) {
+ *bank = proc;
+ *n = (a - base) / SUN55I_MSGBOX_OFFSET(1);
+ *reg = (a - base) % SUN55I_MSGBOX_OFFSET(1);
+ return &fix->regs[proc][(a - base) / 4];
+ }
+ }
+ /* A real interrupt taken while this test runs: not ours to fail */
+ if (in_task())
+ KUNIT_FAIL(kunit_get_current_test(), "register access outside the mock banks");
+ return NULL;
+}
+
+/*
+ * Messages signalled on host port @n for the channels in @bits: enabled and
+ * pending (the read enable and pending bits of a channel are the same bit)
+ */
+static void mock_rx_pending(struct mock_msgbox_fixture *fix, int n, u32 bits)
+{
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_READ_IRQ_ENABLE(n) / 4, bits);
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_READ_IRQ_STATUS(n) / 4, bits);
+}
+
+/* MSG_STATUS of the channel whose FIFO register is @fifo (same port, same p) */
+static u32 *mock_fifo_count(u32 *fifo)
+{
+ return fifo - (SUN55I_MSGBOX_MSG_FIFO(0, 0) - SUN55I_MSGBOX_MSG_STATUS(0, 0)) / 4;
+}
+
+static bool mock_is_fifo(u32 reg)
+{
+ return reg >= SUN55I_MSGBOX_MSG_FIFO(0, 0) &&
+ reg <= SUN55I_MSGBOX_MSG_FIFO(0, SUN55I_CHANS_PER_PROC - 1);
+}
+
+static u32 *mock_msg_count(struct mock_msgbox_fixture *fix, int bank, int n, int p)
+{
+ return &fix->regs[bank][SUN55I_MSGBOX_MSG_STATUS(n, p) / 4];
+}
+
+/*
+ * Queue a message in a FIFO, as its sender would. Tests that only set the
+ * count and FIFO registers get the register value back on every read.
+ */
+static void mock_fifo_push(struct mock_msgbox_fixture *fix, int bank, int n, int p, u32 val)
+{
+ struct mock_fifo *f = &fix->fifo[bank][n][p];
+
+ KUNIT_ASSERT_LT(kunit_get_current_test(), f->len, SUN55I_FIFO_MAX);
+ f->msg[(f->head + f->len++) % SUN55I_FIFO_MAX] = val;
+ (*mock_msg_count(fix, bank, n, p))++;
+}
+
+/* Take the oldest queued message from a FIFO, as its receiver would */
+static u32 mock_fifo_pop(struct mock_msgbox_fixture *fix, int bank, int n, int p)
+{
+ struct mock_fifo *f = &fix->fifo[bank][n][p];
+ u32 val;
+
+ KUNIT_ASSERT_GT(kunit_get_current_test(), f->len, 0);
+ val = f->msg[f->head];
+ f->head = (f->head + 1) % SUN55I_FIFO_MAX;
+ f->len--;
+ (*mock_msg_count(fix, bank, n, p))--;
+ return val;
+}
+
+/*
+ * The write pending bit of a channel is raised while its FIFO has room and
+ * stays latched; a clear only takes while the FIFO is full.
+ */
+static void mock_raise_write_pending(struct mock_msgbox_fixture *fix, int bank, int n, u32 *sta)
+{
+ int p;
+
+ for (p = 0; p < SUN55I_CHANS_PER_PROC; p++)
+ if ((*mock_msg_count(fix, bank, n, p) & SUN55I_MSGBOX_MSG_NUM_MASK) <
+ SUN55I_FIFO_MAX)
+ *sta |= SUN55I_MSGBOX_WR_IRQ_PEND_BIT(p);
+}
+
+static u32 mock_readl(void __iomem *addr)
+{
+ struct mock_msgbox_fixture *fix = kunit_get_current_test()->priv;
+ int bank;
+ u32 n, reg, *r = mock_decode(addr, &bank, &n, ®);
+
+ if (!r)
+ return in_task() ? 0 : readl(addr);
+
+ /*
+ * Reading the message FIFO pops one message. A FIFO that a test filled
+ * through its registers, not mock_fifo_push(), reads back its register.
+ */
+ if (mock_is_fifo(reg)) {
+ int p = (reg - SUN55I_MSGBOX_MSG_FIFO(0, 0)) / 4;
+ u32 *count = mock_fifo_count(r);
+ u32 val = *r;
+
+ fix->fifo_reads++;
+ if (fix->fifo_stuck || !(*count & SUN55I_MSGBOX_MSG_NUM_MASK))
+ return val;
+ if (fix->fifo[bank][n][p].len)
+ return mock_fifo_pop(fix, bank, n, p);
+ (*count)--;
+ return val;
+ }
+
+ if (reg == SUN55I_MSGBOX_WRITE_IRQ_STATUS(0))
+ mock_raise_write_pending(fix, bank, n, r);
+
+ return *r;
+}
+
+static void mock_writel(u32 val, void __iomem *addr)
+{
+ struct mock_msgbox_fixture *fix = kunit_get_current_test()->priv;
+ int bank;
+ u32 n, reg, *r = mock_decode(addr, &bank, &n, ®);
+ int p;
+
+ if (!r) {
+ if (!in_task())
+ writel(val, addr);
+ return;
+ }
+
+ switch (reg) {
+ case SUN55I_MSGBOX_WRITE_IRQ_ENABLE(0):
+ /*
+ * A TX-done handler still running on another CPU submits the
+ * next message, and send_data() arms the channel again.
+ */
+ if (fix->rearm_on_disarm && (*r & ~val)) {
+ fix->rearm_on_disarm = false;
+ val = *r;
+ }
+ break;
+ case SUN55I_MSGBOX_READ_IRQ_ENABLE(0):
+ if (r >= fix->regs[0] && r < fix->regs[0] + ARRAY_SIZE(fix->regs[0]) &&
+ n < SUN55I_NUM_ROUTES)
+ fix->rd_en_written[n] |= val;
+ break;
+ case SUN55I_MSGBOX_READ_IRQ_STATUS(0):
+ /* W1C; a read pending bit stays set while its FIFO is not empty */
+ for (p = 0; p < SUN55I_CHANS_PER_PROC; p++) {
+ u32 *count = r + (SUN55I_MSGBOX_MSG_STATUS(0, p) -
+ SUN55I_MSGBOX_READ_IRQ_STATUS(0)) / 4;
+
+ if ((val & SUN55I_MSGBOX_RD_IRQ_PEND_BIT(p)) &&
+ !(*count & SUN55I_MSGBOX_MSG_NUM_MASK))
+ *r &= ~SUN55I_MSGBOX_RD_IRQ_PEND_BIT(p);
+ }
+ return;
+ case SUN55I_MSGBOX_WRITE_IRQ_STATUS(0):
+ /* Latched; the clear only takes while the FIFO is full */
+ mock_raise_write_pending(fix, bank, n, r);
+ for (p = 0; p < SUN55I_CHANS_PER_PROC; p++)
+ if ((val & SUN55I_MSGBOX_WR_IRQ_PEND_BIT(p)) &&
+ (*mock_msg_count(fix, bank, n, p) & SUN55I_MSGBOX_MSG_NUM_MASK) >=
+ SUN55I_FIFO_MAX)
+ *r &= ~SUN55I_MSGBOX_WR_IRQ_PEND_BIT(p);
+ return;
+ }
+
+ /* Writing the message FIFO pushes one message; a full FIFO is corrupted */
+ if (mock_is_fifo(reg)) {
+ u32 *count = mock_fifo_count(r);
+
+ p = (reg - SUN55I_MSGBOX_MSG_FIFO(0, 0)) / 4;
+ KUNIT_EXPECT_LT_MSG(kunit_get_current_test(), *count & SUN55I_MSGBOX_MSG_NUM_MASK,
+ (u32)SUN55I_FIFO_MAX, "write to a full FIFO");
+ if ((*count & SUN55I_MSGBOX_MSG_NUM_MASK) < SUN55I_FIFO_MAX)
+ mock_fifo_push(fix, bank, n, p, val);
+ }
+ *r = val;
+}
+
+static void mock_rx_cb(struct mbox_client *cl, void *data)
+{
+ struct mock_rx_sink *sink = container_of(cl, struct mock_rx_sink, client);
+
+ if (sink->count < ARRAY_SIZE(sink->msgs))
+ sink->msgs[sink->count] = *(u32 *)data;
+ sink->count++;
+ sink->last_msg = *(u32 *)data;
+}
+
+static void mock_free_irq_desc(void *irq)
+{
+ irq_free_desc((unsigned long)irq);
+}
+
+/* A fresh interrupt on the dummy chip, so request_irq() and free_irq() run */
+static int mock_alloc_irq(struct kunit *test)
+{
+ int irq = irq_alloc_desc(NUMA_NO_NODE);
+
+ KUNIT_ASSERT_GE(test, irq, 0);
+ irq_set_chip_and_handler(irq, &dummy_irq_chip, handle_simple_irq);
+ /* Some architectures (arm, powerpc) mark new descriptors unrequestable */
+ irq_clear_status_flags(irq, IRQ_NOREQUEST | IRQ_NOPROBE);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, mock_free_irq_desc,
+ (void *)(unsigned long)irq), 0);
+ return irq;
+}
+
+static struct mock_msgbox_fixture *create_mock_fixture(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix;
+ int i;
+
+ fix = kunit_kzalloc(test, sizeof(*fix), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, fix);
+ test->priv = fix;
+ kunit_activate_static_stub(test, sun55i_msgbox_readl, mock_readl);
+ kunit_activate_static_stub(test, sun55i_msgbox_writel, mock_writel);
+
+ spin_lock_init(&fix->mbox.lock);
+ /* The host bank line: shutdown() waits for its handler */
+ fix->mbox.irq = mock_alloc_irq(test);
+ fix->mbox.controller.dev = kunit_device_register(test, "sun55i-msgbox-test");
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fix->mbox.controller.dev);
+ fix->mbox.controller.chans = fix->chans;
+ fix->mbox.controller.num_chans = SUN55I_NUM_CHANS;
+
+ for (i = 0; i < SUN55I_MAX_PROCESSORS; i++)
+ fix->mbox.regs[i] = (void __iomem *)fix->regs[i];
+
+ for (i = 0; i < SUN55I_NUM_CHANS; i++) {
+ fix->chans[i].con_priv = &fix->mbox;
+ fix->sinks[i].client.rx_callback = mock_rx_cb;
+ fix->chans[i].cl = &fix->sinks[i].client;
+ }
+
+ return fix;
+}
+
+static void test_functional_send_data_all_twelve_channels(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ int ch;
+
+ /* Send unique test pattern over every channel from 0 to 11 */
+ for (ch = 0; ch < SUN55I_NUM_CHANS; ch++) {
+ int local_n, p, remote_id, remote_n;
+ u32 val = 0x55000000U | (u32)ch;
+ u32 reg_idx;
+
+ sun55i_msgbox_chan_to_route(ch, &local_n, &p, &remote_id, &remote_n);
+ sun55i_msgbox_chan_ops.send_data(&fix->chans[ch], &val);
+
+ reg_idx = SUN55I_MSGBOX_MSG_FIFO(remote_n, p) / 4;
+ KUNIT_EXPECT_EQ(test,
+ mock_read_reg(fix, remote_id, reg_idx),
+ val);
+ }
+}
+
+static void test_functional_send_data_null(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 reg_idx;
+
+ /* NULL data pointer writes 0 to FIFO without dereferencing */
+ sun55i_msgbox_chan_ops.send_data(&fix->chans[8], NULL);
+
+ reg_idx = SUN55I_MSGBOX_MSG_FIFO(2, 0) / 4;
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, reg_idx), 0U);
+}
+
+static void test_functional_last_tx_done_sweep(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 reg_idx = SUN55I_MSGBOX_MSG_STATUS(2, 0) / 4;
+ u32 count;
+
+ /* Counts 0..7: the FIFO has room for the next kick -> done */
+ for (count = 0; count < SUN55I_FIFO_MAX; count++) {
+ mock_write_reg(fix, 3, reg_idx, count);
+ KUNIT_EXPECT_TRUE(test, sun55i_msgbox_chan_ops.last_tx_done(&fix->chans[8]));
+ }
+
+ /* Count 8 (full) and anything above: wait */
+ for (count = SUN55I_FIFO_MAX; count <= 15; count++) {
+ mock_write_reg(fix, 3, reg_idx, count);
+ KUNIT_EXPECT_FALSE(test, sun55i_msgbox_chan_ops.last_tx_done(&fix->chans[8]));
+ }
+}
+
+static void test_functional_send_data_holds_for_full_fifo(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 status = SUN55I_MSGBOX_MSG_STATUS(2, 1) / 4;
+ u32 fifo = SUN55I_MSGBOX_MSG_FIFO(2, 1) / 4;
+ u32 val = 0xA5A5A5A5;
+
+ /*
+ * Writing a full FIFO corrupts its count: never do it. The core only
+ * polls a channel whose send succeeded, so timer mode accepts the
+ * message and last_tx_done() writes it once a slot is free.
+ */
+ mock_write_reg(fix, 3, status, SUN55I_FIFO_MAX);
+ mock_write_reg(fix, 3, fifo, 0);
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_chan_ops.send_data(&fix->chans[9], &val), 0);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, fifo), 0U);
+
+ /* Still full: not done, nothing written */
+ KUNIT_EXPECT_FALSE(test, sun55i_msgbox_chan_ops.last_tx_done(&fix->chans[9]));
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, fifo), 0U);
+
+ /* The receiver read one: the held message goes in and fills the FIFO */
+ mock_write_reg(fix, 3, status, SUN55I_FIFO_MAX - 1);
+ KUNIT_EXPECT_FALSE(test, sun55i_msgbox_chan_ops.last_tx_done(&fix->chans[9]));
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, fifo), val);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, status), SUN55I_FIFO_MAX);
+
+ /* Room again: done, and it is written only once */
+ mock_write_reg(fix, 3, status, SUN55I_FIFO_MAX - 1);
+ mock_write_reg(fix, 3, fifo, 0);
+ KUNIT_EXPECT_TRUE(test, sun55i_msgbox_chan_ops.last_tx_done(&fix->chans[9]));
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, fifo), 0U);
+
+ /* With room it writes at once */
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_chan_ops.send_data(&fix->chans[9], &val), 0);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, fifo), val);
+
+ /* Timer mode: neither path arms the write IRQ */
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, SUN55I_MSGBOX_WRITE_IRQ_ENABLE(2) / 4), 0U);
+}
+
+static void test_functional_shutdown_drops_held_message(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 status = SUN55I_MSGBOX_MSG_STATUS(2, 1) / 4;
+ u32 fifo = SUN55I_MSGBOX_MSG_FIFO(2, 1) / 4;
+ u32 val = 0xA5A5A5A5;
+
+ mock_write_reg(fix, 3, status, SUN55I_FIFO_MAX);
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_chan_ops.send_data(&fix->chans[9], &val), 0);
+ sun55i_msgbox_chan_ops.shutdown(&fix->chans[9]);
+
+ /* A held message does not reach the next user of the channel */
+ mock_write_reg(fix, 3, status, 0);
+ mock_write_reg(fix, 3, fifo, 0);
+ KUNIT_EXPECT_TRUE(test, sun55i_msgbox_chan_ops.last_tx_done(&fix->chans[9]));
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, fifo), 0U);
+}
+
+/* Write IRQ mode: fixture channels wired to the mailbox core's txdone path */
+static struct mock_msgbox_fixture *create_tx_irq_fixture(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ int i;
+
+ fix->mbox.tx_done_irq = true;
+ mutex_init(&fix->mbox.tx_irq_lock);
+ fix->mbox.controller.ops = &sun55i_msgbox_chan_ops;
+ for (i = 0; i < SUN55I_NUM_ROUTES; i++) {
+ fix->mbox.tx_irqs[i].mbox = &fix->mbox;
+ fix->mbox.tx_irqs[i].local_n = i;
+ }
+ for (i = 0; i < SUN55I_NUM_CHANS; i++) {
+ spin_lock_init(&fix->chans[i].lock);
+ fix->chans[i].mbox = &fix->mbox.controller;
+ fix->chans[i].active_req = MBOX_NO_MSG;
+ fix->chans[i].txdone_method = MBOX_TXDONE_BY_IRQ;
+ }
+ return fix;
+}
+
+static void test_tx_irq_armed_after_every_send(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_tx_irq_fixture(test);
+ u32 status = SUN55I_MSGBOX_MSG_STATUS(2, 1) / 4;
+ u32 fifo = SUN55I_MSGBOX_MSG_FIFO(2, 1) / 4;
+ u32 wr_en = SUN55I_MSGBOX_WRITE_IRQ_ENABLE(2) / 4;
+ u32 wr_sta = SUN55I_MSGBOX_WRITE_IRQ_STATUS(2) / 4;
+ u32 val = 0x5A5A5A5A;
+
+ /*
+ * Room in the FIFO: the kick is written and the write IRQ of this
+ * channel (p = 1) is armed, after clearing its latched status bit only.
+ */
+ mock_write_reg(fix, 3, status, SUN55I_FIFO_MAX - 1);
+ mock_write_reg(fix, 3, wr_sta, 0xAA);
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_chan_ops.send_data(&fix->chans[9], &val), 0);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, fifo), val);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, wr_en), SUN55I_MSGBOX_WR_IRQ_EN_BIT(1));
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, wr_sta),
+ 0xAAU & ~SUN55I_MSGBOX_WR_IRQ_PEND_BIT(1));
+
+ /* Full: refused without writing, the IRQ is armed to retry it */
+ mock_write_reg(fix, 3, status, SUN55I_FIFO_MAX);
+ mock_write_reg(fix, 3, fifo, 0);
+ mock_write_reg(fix, 3, wr_en, 0);
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_chan_ops.send_data(&fix->chans[9], &val), -EBUSY);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, fifo), 0U);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, wr_en), SUN55I_MSGBOX_WR_IRQ_EN_BIT(1));
+}
+
+static void test_tx_irq_handler_disarms_and_acks(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_tx_irq_fixture(test);
+ struct sun55i_tx_irq *rv = &fix->mbox.tx_irqs[2];
+ u32 wr_en = SUN55I_MSGBOX_WRITE_IRQ_ENABLE(2) / 4;
+ u32 wr_sta = SUN55I_MSGBOX_WRITE_IRQ_STATUS(2) / 4;
+
+ /*
+ * Armed and raised (the receiver read one): handled and disarmed. The
+ * ack does not take while the FIFO has room, so masking the enable is
+ * what stops the level interrupt.
+ */
+ mock_write_reg(fix, 3, wr_en, SUN55I_MSGBOX_WR_IRQ_EN_BIT(1));
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_tx_irq(0, rv), IRQ_HANDLED);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, wr_en), 0U);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, wr_sta) & SUN55I_MSGBOX_WR_IRQ_PEND_BIT(1),
+ SUN55I_MSGBOX_WR_IRQ_PEND_BIT(1));
+
+ /* Not armed any more: the next pass leaves the channel alone */
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_tx_irq(0, rv), IRQ_NONE);
+}
+
+static void test_tx_irq_handler_ignores_unarmed(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_tx_irq_fixture(test);
+ struct sun55i_tx_irq *rv = &fix->mbox.tx_irqs[2];
+ u32 wr_en = SUN55I_MSGBOX_WRITE_IRQ_ENABLE(2) / 4;
+ u32 wr_sta = SUN55I_MSGBOX_WRITE_IRQ_STATUS(2) / 4;
+
+ /* Status is level and set whenever there is room: not ours unless armed */
+ mock_write_reg(fix, 3, wr_en, 0);
+ mock_write_reg(fix, 3, wr_sta, 0xAA);
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_tx_irq(0, rv), IRQ_NONE);
+
+ /* Armed on p = 0 only: p = 1 pending is ignored, p = 0 handled */
+ mock_write_reg(fix, 3, wr_en, SUN55I_MSGBOX_WR_IRQ_EN_BIT(0));
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_tx_irq(0, rv), IRQ_HANDLED);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, wr_en), 0U);
+
+ /* The CPUS bank line never handles the RV bank's channels */
+ mock_write_reg(fix, 3, wr_en, SUN55I_MSGBOX_WR_IRQ_EN_BIT(1));
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_tx_irq(0, &fix->mbox.tx_irqs[0]),
+ IRQ_NONE);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, wr_en), SUN55I_MSGBOX_WR_IRQ_EN_BIT(1));
+}
+
+static void test_functional_peek_data_sweep(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 reg_idx = SUN55I_MSGBOX_MSG_STATUS(2, 0) / 4;
+ u32 count;
+
+ /* Count 0: no messages -> returns false */
+ mock_write_reg(fix, 0, reg_idx, 0);
+ KUNIT_EXPECT_FALSE(test, sun55i_msgbox_chan_ops.peek_data(&fix->chans[8]));
+
+ /* Counts 1..15: has messages -> returns true */
+ for (count = 1; count <= 15; count++) {
+ mock_write_reg(fix, 0, reg_idx, count);
+ KUNIT_EXPECT_TRUE(test, sun55i_msgbox_chan_ops.peek_data(&fix->chans[8]));
+ }
+}
+
+static void test_functional_startup_and_shutdown(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 en_idx = SUN55I_MSGBOX_READ_IRQ_ENABLE(2) / 4;
+ u32 stat_idx = SUN55I_MSGBOX_READ_IRQ_STATUS(2) / 4;
+
+ /* Startup on Channel 8: enables IRQ and clears a stale pending bit */
+ mock_write_reg(fix, 0, stat_idx, SUN55I_MSGBOX_RD_IRQ_PEND_BIT(0));
+ sun55i_msgbox_chan_ops.startup(&fix->chans[8]);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 0, en_idx) & SUN55I_MSGBOX_RD_IRQ_EN_BIT(0),
+ SUN55I_MSGBOX_RD_IRQ_EN_BIT(0));
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 0, stat_idx) & SUN55I_MSGBOX_RD_IRQ_PEND_BIT(0),
+ 0U);
+
+ /* Shutdown on Channel 8: disables IRQ */
+ sun55i_msgbox_chan_ops.shutdown(&fix->chans[8]);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 0, en_idx) & SUN55I_MSGBOX_RD_IRQ_EN_BIT(0), 0U);
+}
+
+static void test_functional_shutdown_isolation(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 en_idx = SUN55I_MSGBOX_READ_IRQ_ENABLE(0) / 4;
+ u32 both;
+
+ /* Enable both Channel 0 and Channel 1 on Port 0 */
+ sun55i_msgbox_chan_ops.startup(&fix->chans[0]);
+ sun55i_msgbox_chan_ops.startup(&fix->chans[1]);
+ both = SUN55I_MSGBOX_RD_IRQ_EN_BIT(0) | SUN55I_MSGBOX_RD_IRQ_EN_BIT(1);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 0, en_idx) & both, both);
+
+ /* Shutting down Channel 0 must leave Channel 1 enabled */
+ sun55i_msgbox_chan_ops.shutdown(&fix->chans[0]);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 0, en_idx) & SUN55I_MSGBOX_RD_IRQ_EN_BIT(0), 0U);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 0, en_idx) & SUN55I_MSGBOX_RD_IRQ_EN_BIT(1),
+ SUN55I_MSGBOX_RD_IRQ_EN_BIT(1));
+}
+
+static void test_functional_startup_flushes_stale_fifo(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 stat_idx = SUN55I_MSGBOX_READ_IRQ_STATUS(2) / 4;
+ u32 msg_stat_idx = SUN55I_MSGBOX_MSG_STATUS(2, 0) / 4;
+
+ /* Messages left from before the channel was opened (e.g. old firmware) */
+ mock_write_reg(fix, 0, msg_stat_idx, 4);
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_FIFO(2, 0) / 4, 0xDEADBEEF);
+ mock_write_reg(fix, 0, stat_idx, SUN55I_MSGBOX_RD_IRQ_PEND_BIT(0));
+
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_chan_ops.startup(&fix->chans[8]), 0);
+
+ /* Drained and dropped, then the pending bit cleared */
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 0, msg_stat_idx), 0U);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 0, stat_idx) & SUN55I_MSGBOX_RD_IRQ_PEND_BIT(0),
+ 0U);
+ KUNIT_EXPECT_EQ(test, fix->sinks[8].count, 0);
+}
+
+static void test_functional_startup_capped_at_fifo_max(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 msg_stat_idx = SUN55I_MSGBOX_MSG_STATUS(2, 0) / 4;
+ u32 fifo_idx = SUN55I_MSGBOX_MSG_FIFO(2, 0) / 4;
+ int ret;
+
+ /* Hardware defect: MSG_STATUS always returns non-zero */
+ fix->fifo_stuck = true;
+ mock_write_reg(fix, 0, msg_stat_idx, 15);
+ mock_write_reg(fix, 0, fifo_idx, 0xCAFEBABE);
+
+ /* Must stop after SUN55I_FIFO_MAX reads and return 0 */
+ ret = sun55i_msgbox_chan_ops.startup(&fix->chans[8]);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, fix->fifo_reads, SUN55I_FIFO_MAX);
+}
+
+static void test_functional_shutdown_flushes(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 en_idx = SUN55I_MSGBOX_READ_IRQ_ENABLE(2) / 4;
+ u32 stat_idx = SUN55I_MSGBOX_READ_IRQ_STATUS(2) / 4;
+ u32 msg_stat_idx = SUN55I_MSGBOX_MSG_STATUS(2, 0) / 4;
+ u32 fifo_idx = SUN55I_MSGBOX_MSG_FIFO(2, 0) / 4;
+
+ /* First startup */
+ sun55i_msgbox_chan_ops.startup(&fix->chans[8]);
+
+ /* Residual messages arriving before shutdown */
+ mock_write_reg(fix, 0, msg_stat_idx, 3);
+ mock_write_reg(fix, 0, fifo_idx, 0x11223344);
+ mock_write_reg(fix, 0, stat_idx, SUN55I_MSGBOX_RD_IRQ_PEND_BIT(0));
+
+ /* IRQ disabled, FIFO drained, then the pending bit cleared */
+ sun55i_msgbox_chan_ops.shutdown(&fix->chans[8]);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 0, en_idx) & SUN55I_MSGBOX_RD_IRQ_EN_BIT(0), 0U);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 0, msg_stat_idx), 0U);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 0, stat_idx) & SUN55I_MSGBOX_RD_IRQ_PEND_BIT(0),
+ 0U);
+ KUNIT_EXPECT_EQ(test, fix->sinks[8].count, 0);
+}
+
+/* =============== Interrupt Handler (Hardirq) Simulation =============== */
+
+static void test_irq_spurious_returns_none(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ irqreturn_t ret;
+
+ /* All IRQ enable and status registers are 0 -> IRQ_NONE */
+ ret = sun55i_msgbox_irq(0, &fix->mbox);
+ KUNIT_EXPECT_EQ(test, ret, IRQ_NONE);
+}
+
+static void test_irq_single_message_received(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 en_idx = SUN55I_MSGBOX_READ_IRQ_ENABLE(2) / 4;
+ u32 stat_idx = SUN55I_MSGBOX_READ_IRQ_STATUS(2) / 4;
+ u32 msg_stat_idx = SUN55I_MSGBOX_MSG_STATUS(2, 0) / 4;
+ u32 fifo_idx = SUN55I_MSGBOX_MSG_FIFO(2, 0) / 4;
+ irqreturn_t ret;
+
+ /* Configure pending IRQ on Channel 8 (local_n = 2, p = 0) */
+ mock_write_reg(fix, 0, en_idx, SUN55I_MSGBOX_RD_IRQ_EN_BIT(0));
+ mock_write_reg(fix, 0, stat_idx, SUN55I_MSGBOX_RD_IRQ_PEND_BIT(0));
+ mock_write_reg(fix, 0, msg_stat_idx, 1); /* 1 message in FIFO */
+ mock_write_reg(fix, 0, fifo_idx, 0xCAFEF00D);
+
+ ret = sun55i_msgbox_irq(0, &fix->mbox);
+ KUNIT_EXPECT_EQ(test, ret, IRQ_HANDLED);
+
+ /* Verify data delivered to mailbox client */
+ KUNIT_EXPECT_EQ(test, fix->sinks[8].count, 1);
+ KUNIT_EXPECT_EQ(test, fix->sinks[8].last_msg, 0xCAFEF00DU);
+}
+
+static void test_irq_clears_pending_after_drain(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 en_idx = SUN55I_MSGBOX_READ_IRQ_ENABLE(2) / 4;
+ u32 stat_idx = SUN55I_MSGBOX_READ_IRQ_STATUS(2) / 4;
+ irqreturn_t ret;
+
+ mock_write_reg(fix, 0, en_idx, SUN55I_MSGBOX_RD_IRQ_EN_BIT(0));
+ mock_write_reg(fix, 0, stat_idx, SUN55I_MSGBOX_RD_IRQ_PEND_BIT(0));
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_STATUS(2, 0) / 4, 2);
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_FIFO(2, 0) / 4, 0xCAFEF00D);
+
+ /*
+ * The hardware ignores the pending clear while the FIFO is not empty:
+ * a clear written before the drain leaves the bit set and the IRQ
+ * fires a second time for the same messages.
+ */
+ ret = sun55i_msgbox_irq(0, &fix->mbox);
+ KUNIT_EXPECT_EQ(test, ret, IRQ_HANDLED);
+ KUNIT_EXPECT_EQ(test, fix->sinks[8].count, 2);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 0, SUN55I_MSGBOX_MSG_STATUS(2, 0) / 4), 0U);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 0, stat_idx) & SUN55I_MSGBOX_RD_IRQ_PEND_BIT(0),
+ 0U);
+}
+
+/* rx callback that records the state the driver delivers in */
+struct mock_rx_state {
+ struct mock_rx_sink sink;
+ struct mock_msgbox_fixture *fix;
+ int calls;
+ u32 fifo_count[SUN55I_FIFO_MAX];
+};
+
+static void mock_rx_state_cb(struct mbox_client *cl, void *data)
+{
+ struct mock_rx_state *st = container_of(cl, struct mock_rx_state, sink.client);
+ struct mock_msgbox_fixture *fix = st->fix;
+ u32 val = 0x11;
+
+ if (st->calls < SUN55I_FIFO_MAX)
+ st->fifo_count[st->calls] =
+ mock_read_reg(fix, 0, SUN55I_MSGBOX_MSG_STATUS(2, 1) / 4);
+ st->calls++;
+
+ /* A client may reply from its callback; send_data() takes the lock */
+ sun55i_msgbox_chan_ops.send_data(&fix->chans[9], &val);
+}
+
+static void test_irq_callback_may_reply(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_tx_irq_fixture(test);
+ struct mock_rx_state *st = kunit_kzalloc(test, sizeof(*st), GFP_KERNEL);
+ int i;
+
+ KUNIT_ASSERT_NOT_NULL(test, st);
+ st->fix = fix;
+ st->sink.client.rx_callback = mock_rx_state_cb;
+ fix->chans[9].cl = &st->sink.client;
+
+ /* Channel 9 (local_n = 2, p = 1): three messages waiting */
+ mock_rx_pending(fix, 2, SUN55I_MSGBOX_RD_IRQ_PEND_BIT(1));
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_STATUS(2, 1) / 4, 3);
+
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_irq(0, &fix->mbox), IRQ_HANDLED);
+ KUNIT_EXPECT_EQ(test, st->calls, 3);
+
+ /* The drain and the pending clear finished before the first callback */
+ for (i = 0; i < 3; i++)
+ KUNIT_EXPECT_EQ(test, st->fifo_count[i], 0U);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 0, SUN55I_MSGBOX_READ_IRQ_STATUS(2) / 4), 0U);
+
+ /* The replies went out */
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, SUN55I_MSGBOX_MSG_STATUS(2, 1) / 4), 3U);
+}
+
+static void test_irq_pending_with_empty_fifo(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 en_idx = SUN55I_MSGBOX_READ_IRQ_ENABLE(2) / 4;
+ u32 stat_idx = SUN55I_MSGBOX_READ_IRQ_STATUS(2) / 4;
+ u32 msg_stat_idx = SUN55I_MSGBOX_MSG_STATUS(2, 0) / 4;
+ irqreturn_t ret;
+
+ /* Pending IRQ bit set, but MSG_STATUS has 0 messages */
+ mock_write_reg(fix, 0, en_idx, SUN55I_MSGBOX_RD_IRQ_EN_BIT(0));
+ mock_write_reg(fix, 0, stat_idx, SUN55I_MSGBOX_RD_IRQ_PEND_BIT(0));
+ mock_write_reg(fix, 0, msg_stat_idx, 0);
+
+ ret = sun55i_msgbox_irq(0, &fix->mbox);
+ KUNIT_EXPECT_EQ(test, ret, IRQ_HANDLED);
+
+ /* No messages should be dispatched to client */
+ KUNIT_EXPECT_EQ(test, fix->sinks[8].count, 0);
+}
+
+static void test_irq_multi_channel_concurrency(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ irqreturn_t ret;
+
+ /* Both Channel 0 (p=0) and Channel 1 (p=1) pending on Port 0 */
+ mock_rx_pending(fix, 0,
+ SUN55I_MSGBOX_RD_IRQ_PEND_BIT(0) | SUN55I_MSGBOX_RD_IRQ_PEND_BIT(1));
+
+ /* Ch 0 has message 0x11111111 */
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_STATUS(0, 0) / 4, 1);
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_FIFO(0, 0) / 4, 0x11111111);
+
+ /* Ch 1 has message 0x22222222 */
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_STATUS(0, 1) / 4, 1);
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_FIFO(0, 1) / 4, 0x22222222);
+
+ ret = sun55i_msgbox_irq(0, &fix->mbox);
+ KUNIT_EXPECT_EQ(test, ret, IRQ_HANDLED);
+
+ /* Both channels must be serviced in the same interrupt invocation */
+ KUNIT_EXPECT_EQ(test, fix->sinks[0].count, 1);
+ KUNIT_EXPECT_EQ(test, fix->sinks[0].last_msg, 0x11111111U);
+
+ KUNIT_EXPECT_EQ(test, fix->sinks[1].count, 1);
+ KUNIT_EXPECT_EQ(test, fix->sinks[1].last_msg, 0x22222222U);
+}
+
+static void test_irq_fifo_drain_capped_at_max(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 en_idx = SUN55I_MSGBOX_READ_IRQ_ENABLE(2) / 4;
+ u32 stat_idx = SUN55I_MSGBOX_READ_IRQ_STATUS(2) / 4;
+ u32 msg_stat_idx = SUN55I_MSGBOX_MSG_STATUS(2, 0) / 4;
+ u32 fifo_idx = SUN55I_MSGBOX_MSG_FIFO(2, 0) / 4;
+ irqreturn_t ret;
+
+ /*
+ * Simulate hardware defect or runaway remote: MSG_STATUS is permanently
+ * non-zero. The ISR MUST NOT hang in an infinite loop; it must drain
+ * at most SUN55I_FIFO_MAX (8) messages and return.
+ */
+ mock_write_reg(fix, 0, en_idx, SUN55I_MSGBOX_RD_IRQ_EN_BIT(0));
+ mock_write_reg(fix, 0, stat_idx, SUN55I_MSGBOX_RD_IRQ_PEND_BIT(0));
+ fix->fifo_stuck = true;
+ mock_write_reg(fix, 0, msg_stat_idx, 15); /* Always reports data */
+ mock_write_reg(fix, 0, fifo_idx, 0x12345678);
+
+ ret = sun55i_msgbox_irq(0, &fix->mbox);
+ KUNIT_EXPECT_EQ(test, ret, IRQ_HANDLED);
+
+ /* Drain must be strictly bounded at SUN55I_FIFO_MAX */
+ KUNIT_EXPECT_EQ(test, fix->sinks[8].count, SUN55I_FIFO_MAX);
+}
+
+static void test_irq_channel_crosstalk_isolation(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 en_idx = SUN55I_MSGBOX_READ_IRQ_ENABLE(0) / 4;
+ u32 stat_idx = SUN55I_MSGBOX_READ_IRQ_STATUS(0) / 4;
+ irqreturn_t ret;
+ int ch;
+
+ /* Only Channel 2 (Port 0, p=2) has an interrupt enabled and pending */
+ mock_write_reg(fix, 0, en_idx, SUN55I_MSGBOX_RD_IRQ_EN_BIT(2));
+ mock_write_reg(fix, 0, stat_idx, SUN55I_MSGBOX_RD_IRQ_PEND_BIT(2));
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_STATUS(0, 2) / 4, 1);
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_FIFO(0, 2) / 4, 0x33333333);
+
+ ret = sun55i_msgbox_irq(0, &fix->mbox);
+ KUNIT_EXPECT_EQ(test, ret, IRQ_HANDLED);
+
+ /* Exactly Channel 2 received the message */
+ KUNIT_EXPECT_EQ(test, fix->sinks[2].count, 1);
+ KUNIT_EXPECT_EQ(test, fix->sinks[2].last_msg, 0x33333333U);
+
+ /* All other 11 channels must have received 0 messages */
+ for (ch = 0; ch < SUN55I_NUM_CHANS; ch++) {
+ if (ch == 2)
+ continue;
+ KUNIT_EXPECT_EQ(test, fix->sinks[ch].count, 0);
+ }
+}
+
+static void test_irq_disabled_channel_ignored(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 en_idx = SUN55I_MSGBOX_READ_IRQ_ENABLE(0) / 4;
+ u32 stat_idx = SUN55I_MSGBOX_READ_IRQ_STATUS(0) / 4;
+ irqreturn_t ret;
+
+ /* Channel 1 has pending bit set, but IRQ_ENABLE is 0 (channel disabled) */
+ mock_write_reg(fix, 0, en_idx, 0);
+ mock_write_reg(fix, 0, stat_idx, SUN55I_MSGBOX_RD_IRQ_PEND_BIT(1));
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_STATUS(0, 1) / 4, 1);
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_FIFO(0, 1) / 4, 0x12345678);
+
+ ret = sun55i_msgbox_irq(0, &fix->mbox);
+ /* Must return IRQ_NONE and must NOT deliver message */
+ KUNIT_EXPECT_EQ(test, ret, IRQ_NONE);
+ KUNIT_EXPECT_EQ(test, fix->sinks[1].count, 0);
+}
+
+static void test_irq_multi_port_burst_interleaved(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ irqreturn_t ret;
+
+ /* Port 0 (Ch 0: CPUS) has 2 messages */
+ mock_rx_pending(fix, 0, SUN55I_MSGBOX_RD_IRQ_PEND_BIT(0));
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_STATUS(0, 0) / 4, 2);
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_FIFO(0, 0) / 4, 0xAA01);
+
+ /* Port 1 (Ch 4: DSP) has 2 messages */
+ mock_rx_pending(fix, 1, SUN55I_MSGBOX_RD_IRQ_PEND_BIT(0));
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_STATUS(1, 0) / 4, 2);
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_FIFO(1, 0) / 4, 0xBB01);
+
+ /* Port 2 (Ch 8: RV) has 2 messages */
+ mock_rx_pending(fix, 2, SUN55I_MSGBOX_RD_IRQ_PEND_BIT(0));
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_STATUS(2, 0) / 4, 2);
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_FIFO(2, 0) / 4, 0xCC01);
+
+ ret = sun55i_msgbox_irq(0, &fix->mbox);
+ KUNIT_EXPECT_EQ(test, ret, IRQ_HANDLED);
+
+ /* Each route's messages reach its own channel, all in one pass */
+ KUNIT_EXPECT_EQ(test, fix->sinks[0].count, 2);
+ KUNIT_EXPECT_EQ(test, fix->sinks[0].last_msg, 0xAA01U);
+ KUNIT_EXPECT_EQ(test, fix->sinks[4].count, 2);
+ KUNIT_EXPECT_EQ(test, fix->sinks[4].last_msg, 0xBB01U);
+ KUNIT_EXPECT_EQ(test, fix->sinks[8].count, 2);
+ KUNIT_EXPECT_EQ(test, fix->sinks[8].last_msg, 0xCC01U);
+}
+
+/* =============== Message order =============== */
+
+static void test_irq_delivers_in_fifo_order(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ int i;
+
+ /* A full FIFO of distinct messages on channel 10 (local_n = 2, p = 2) */
+ for (i = 0; i < SUN55I_FIFO_MAX; i++)
+ mock_fifo_push(fix, 0, 2, 2, 0xC0DE0000 | i);
+ mock_rx_pending(fix, 2, SUN55I_MSGBOX_RD_IRQ_PEND_BIT(2));
+
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_irq(0, &fix->mbox), IRQ_HANDLED);
+ KUNIT_ASSERT_EQ(test, fix->sinks[10].count, SUN55I_FIFO_MAX);
+ for (i = 0; i < SUN55I_FIFO_MAX; i++)
+ KUNIT_EXPECT_EQ_MSG(test, fix->sinks[10].msgs[i], 0xC0DE0000U | i,
+ "message %d", i);
+}
+
+static void test_send_data_keeps_order(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 val;
+ int i;
+
+ /* Channel 5 goes to the DSP bank, port 0, p = 1 */
+ for (i = 0; i < SUN55I_FIFO_MAX; i++) {
+ val = 0x5E4D0000 | i;
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_chan_ops.send_data(&fix->chans[5], &val), 0);
+ }
+ KUNIT_EXPECT_FALSE(test, sun55i_msgbox_chan_ops.last_tx_done(&fix->chans[5]));
+
+ for (i = 0; i < SUN55I_FIFO_MAX; i++)
+ KUNIT_EXPECT_EQ_MSG(test, mock_fifo_pop(fix, SUN55I_PROC_DSP, 0, 1),
+ 0x5E4D0000U | i, "message %d", i);
+}
+
+static void test_held_message_follows_earlier_ones(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 val;
+ int i;
+
+ /* Fill channel 9's FIFO, then one more: held, not written */
+ for (i = 0; i <= SUN55I_FIFO_MAX; i++) {
+ val = 0x4E1D0000 | i;
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_chan_ops.send_data(&fix->chans[9], &val), 0);
+ }
+
+ /* The E906 reads one; the poll writes the held message behind the rest */
+ KUNIT_EXPECT_EQ(test, mock_fifo_pop(fix, SUN55I_PROC_RV, 2, 1), 0x4E1D0000U);
+ KUNIT_EXPECT_FALSE(test, sun55i_msgbox_chan_ops.last_tx_done(&fix->chans[9]));
+ for (i = 1; i <= SUN55I_FIFO_MAX; i++)
+ KUNIT_EXPECT_EQ_MSG(test, mock_fifo_pop(fix, SUN55I_PROC_RV, 2, 1),
+ 0x4E1D0000U | i, "message %d", i);
+}
+
+static void test_held_message_dropped_after_timeout(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ u32 old = 0x01D, new = 0x2E3;
+ int i;
+
+ /* Channel 9's FIFO is full: the message is held */
+ for (i = 0; i < SUN55I_FIFO_MAX; i++)
+ mock_fifo_push(fix, SUN55I_PROC_RV, 2, 1, i);
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_chan_ops.send_data(&fix->chans[9], &old), 0);
+
+ /*
+ * A blocking client times out and the core drops the request; the
+ * remote then empties the FIFO and the client sends again.
+ */
+ for (i = 0; i < SUN55I_FIFO_MAX; i++)
+ mock_fifo_pop(fix, SUN55I_PROC_RV, 2, 1);
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_chan_ops.send_data(&fix->chans[9], &new), 0);
+ KUNIT_EXPECT_TRUE(test, sun55i_msgbox_chan_ops.last_tx_done(&fix->chans[9]));
+
+ /* Only the new message went out; the abandoned one never follows it */
+ KUNIT_EXPECT_EQ(test, mock_fifo_pop(fix, SUN55I_PROC_RV, 2, 1), new);
+ KUNIT_EXPECT_EQ(test, fix->fifo[SUN55I_PROC_RV][2][1].len, 0);
+}
+
+/* =============== Bounds =============== */
+
+static void test_functional_shutdown_drain_capped_at_fifo_max(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_chan_ops.startup(&fix->chans[8]), 0);
+
+ /* Hardware defect: the count never drops. shutdown() must still return */
+ fix->fifo_stuck = true;
+ fix->fifo_reads = 0;
+ mock_write_reg(fix, 0, SUN55I_MSGBOX_MSG_STATUS(2, 0) / 4, 15);
+ sun55i_msgbox_chan_ops.shutdown(&fix->chans[8]);
+ KUNIT_EXPECT_EQ(test, fix->fifo_reads, SUN55I_FIFO_MAX);
+ KUNIT_EXPECT_EQ(test, fix->sinks[8].count, 0);
+}
+
+/* =============== Lock held across delivery =============== */
+
+struct mock_lock_probe {
+ struct mbox_client client;
+ /* The controller lock, which must be free during delivery */
+ spinlock_t *lock;
+ int calls;
+ bool held;
+};
+
+static void mock_lock_probe_cb(struct mbox_client *cl, void *data)
+{
+ struct mock_lock_probe *lp = container_of(cl, struct mock_lock_probe, client);
+
+ lp->calls++;
+ if (spin_trylock(lp->lock))
+ spin_unlock(lp->lock);
+ else
+ lp->held = true;
+}
+
+static void test_irq_callback_runs_unlocked(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_mock_fixture(test);
+ struct mock_lock_probe *lp = kunit_kzalloc(test, sizeof(*lp), GFP_KERNEL);
+
+ /* On UP without spinlock debugging a spinlock is never seen as taken */
+ if (!IS_ENABLED(CONFIG_SMP) && !IS_ENABLED(CONFIG_DEBUG_SPINLOCK))
+ kunit_skip(test, "needs CONFIG_SMP or CONFIG_DEBUG_SPINLOCK");
+
+ KUNIT_ASSERT_NOT_NULL(test, lp);
+ lp->lock = &fix->mbox.lock;
+ lp->client.rx_callback = mock_lock_probe_cb;
+ fix->chans[8].cl = &lp->client;
+
+ mock_fifo_push(fix, 0, 2, 0, 0x10C4);
+ mock_fifo_push(fix, 0, 2, 0, 0x10C5);
+ mock_rx_pending(fix, 2, SUN55I_MSGBOX_RD_IRQ_PEND_BIT(0));
+
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_irq(0, &fix->mbox), IRQ_HANDLED);
+ KUNIT_EXPECT_EQ(test, lp->calls, 2);
+ KUNIT_EXPECT_FALSE(test, lp->held);
+}
+
+/* =============== TX done through a real interrupt line =============== */
+
+struct mock_tx_client {
+ struct mbox_client client;
+ int done;
+ int last_r;
+};
+
+static void mock_tx_done_cb(struct mbox_client *cl, void *msg, int r)
+{
+ struct mock_tx_client *tc = container_of(cl, struct mock_tx_client, client);
+
+ tc->done++;
+ tc->last_r = r;
+}
+
+static struct mock_msgbox_fixture *create_tx_line_fixture(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_tx_irq_fixture(test);
+ struct sun55i_tx_irq *rv = &fix->mbox.tx_irqs[2];
+
+ rv->irq = mock_alloc_irq(test);
+ rv->name = "sun55i-msgbox-test-rv";
+ return fix;
+}
+
+static void mock_shutdown_action(void *chan)
+{
+ sun55i_msgbox_chan_ops.shutdown(chan);
+}
+
+/*
+ * startup(), with shutdown() as a cleanup action: the line is released before
+ * its descriptor is freed even when an assertion ends the test early. A test
+ * may shut the channel down itself; a second shutdown() is harmless.
+ */
+static void mock_startup(struct kunit *test, struct mbox_chan *chan)
+{
+ KUNIT_ASSERT_EQ(test, sun55i_msgbox_chan_ops.startup(chan), 0);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, mock_shutdown_action, chan), 0);
+}
+
+/*
+ * Fire the TX-done line. With forced IRQ threading (PREEMPT_RT, threadirqs)
+ * its handler would run in the line's IRQ thread, where the MMIO stubs, which
+ * follow the test thread, do not apply: run the handler startup() registered
+ * here instead, as irq_forced_thread_fn() runs it.
+ */
+static void fire_tx_irq(struct kunit *test, int irq)
+{
+ /* irq_to_desc() is not exported, so go through the irq_data */
+ struct irqaction *action = container_of(irq_get_irq_data(irq), struct irq_desc,
+ irq_data)->action;
+ irqreturn_t ret;
+
+ KUNIT_ASSERT_NOT_NULL(test, action);
+ if (!action->thread_fn) {
+ KUNIT_ASSERT_EQ(test, generic_handle_irq_safe(irq), 0);
+ return;
+ }
+
+ local_bh_disable();
+ if (!IS_ENABLED(CONFIG_PREEMPT_RT))
+ local_irq_disable();
+ ret = action->thread_fn(irq, action->dev_id);
+ if (!IS_ENABLED(CONFIG_PREEMPT_RT))
+ local_irq_enable();
+ local_bh_enable();
+ KUNIT_EXPECT_EQ(test, ret, IRQ_HANDLED);
+}
+
+static void test_tx_irq_handler_reports_tx_done(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_tx_line_fixture(test);
+ struct mock_tx_client *tc = kunit_kzalloc(test, sizeof(*tc), GFP_KERNEL);
+ struct sun55i_tx_irq *rv = &fix->mbox.tx_irqs[2];
+ static u32 msg = 0xD0E;
+
+ KUNIT_ASSERT_NOT_NULL(test, tc);
+ tc->client.tx_done = mock_tx_done_cb;
+ fix->chans[9].cl = &tc->client;
+
+ mock_startup(test, &fix->chans[9]);
+
+ /* A kick in flight on channel 9; the receiver has room again */
+ fix->chans[9].active_req = &msg;
+ mock_write_reg(fix, 3, SUN55I_MSGBOX_WRITE_IRQ_ENABLE(2) / 4,
+ SUN55I_MSGBOX_WR_IRQ_EN_BIT(1));
+
+ /* Through the line startup() requested: the core hears TX done */
+ fire_tx_irq(test, rv->irq);
+ KUNIT_EXPECT_EQ(test, tc->done, 1);
+ KUNIT_EXPECT_EQ(test, tc->last_r, 0);
+ KUNIT_EXPECT_PTR_EQ(test, fix->chans[9].active_req, MBOX_NO_MSG);
+}
+
+/*
+ * A message refused for a full FIFO stays queued in the core. Once the
+ * receiver reads one, the write IRQ fires and the core resubmits it.
+ */
+static void test_tx_irq_refused_message_resent(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_tx_line_fixture(test);
+ struct mock_tx_client *tc = kunit_kzalloc(test, sizeof(*tc), GFP_KERNEL);
+ struct sun55i_tx_irq *rv = &fix->mbox.tx_irqs[2];
+ static u32 msg = 0x8E5E;
+ int i;
+
+ KUNIT_ASSERT_NOT_NULL(test, tc);
+ tc->client.tx_done = mock_tx_done_cb;
+ fix->chans[9].cl = &tc->client;
+ mock_startup(test, &fix->chans[9]);
+
+ for (i = 0; i < SUN55I_FIFO_MAX; i++)
+ mock_fifo_push(fix, SUN55I_PROC_RV, 2, 1, i);
+
+ /* Full: queued in the core, nothing written, the write IRQ armed */
+ KUNIT_ASSERT_GE(test, mbox_send_message(&fix->chans[9], &msg), 0);
+ KUNIT_EXPECT_PTR_EQ(test, fix->chans[9].active_req, MBOX_NO_MSG);
+ KUNIT_EXPECT_EQ(test, fix->chans[9].msg_count, 1U);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, SUN55I_MSGBOX_WRITE_IRQ_ENABLE(2) / 4),
+ SUN55I_MSGBOX_WR_IRQ_EN_BIT(1));
+
+ /* The E906 reads one: the IRQ makes the core send it again */
+ mock_fifo_pop(fix, SUN55I_PROC_RV, 2, 1);
+ fire_tx_irq(test, rv->irq);
+ KUNIT_EXPECT_EQ(test, fix->chans[9].msg_count, 0U);
+ KUNIT_EXPECT_PTR_EQ(test, fix->chans[9].active_req, &msg);
+ for (i = 1; i < SUN55I_FIFO_MAX; i++)
+ mock_fifo_pop(fix, SUN55I_PROC_RV, 2, 1);
+ KUNIT_EXPECT_EQ(test, mock_fifo_pop(fix, SUN55I_PROC_RV, 2, 1), msg);
+
+ /* The resend armed the IRQ again; it now reports the message done */
+ fire_tx_irq(test, rv->irq);
+ KUNIT_EXPECT_EQ(test, tc->done, 1);
+ KUNIT_EXPECT_PTR_EQ(test, fix->chans[9].active_req, MBOX_NO_MSG);
+}
+
+static void test_tx_irq_line_shared_by_channels(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_tx_line_fixture(test);
+ struct sun55i_tx_irq *rv = &fix->mbox.tx_irqs[2];
+ u32 wr_en = SUN55I_MSGBOX_WRITE_IRQ_ENABLE(2) / 4;
+ u32 val = 0x11;
+
+ /* Two channels to the same bank share its line */
+ mock_startup(test, &fix->chans[8]);
+ mock_startup(test, &fix->chans[9]);
+ KUNIT_EXPECT_EQ(test, rv->users, 2U);
+ KUNIT_EXPECT_TRUE(test, irq_has_action(rv->irq));
+
+ /* Both armed by a send */
+ sun55i_msgbox_chan_ops.send_data(&fix->chans[8], &val);
+ sun55i_msgbox_chan_ops.send_data(&fix->chans[9], &val);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, wr_en),
+ SUN55I_MSGBOX_WR_IRQ_EN_BIT(0) | SUN55I_MSGBOX_WR_IRQ_EN_BIT(1));
+
+ /* The first shutdown disarms its own channel and keeps the line */
+ sun55i_msgbox_chan_ops.shutdown(&fix->chans[8]);
+ KUNIT_EXPECT_EQ(test, rv->users, 1U);
+ KUNIT_EXPECT_TRUE(test, irq_has_action(rv->irq));
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, wr_en), SUN55I_MSGBOX_WR_IRQ_EN_BIT(1));
+ KUNIT_EXPECT_FALSE(test, test_bit(8, &fix->mbox.tx_irq_held));
+
+ /* The last one frees it */
+ sun55i_msgbox_chan_ops.shutdown(&fix->chans[9]);
+ KUNIT_EXPECT_EQ(test, rv->users, 0U);
+ KUNIT_EXPECT_FALSE(test, irq_has_action(rv->irq));
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, wr_en), 0U);
+ KUNIT_EXPECT_EQ(test, fix->mbox.tx_irq_held, 0UL);
+
+ /* A second shutdown without a startup does not touch the line count */
+ sun55i_msgbox_chan_ops.shutdown(&fix->chans[9]);
+ KUNIT_EXPECT_EQ(test, rv->users, 0U);
+}
+
+/*
+ * A TX-done handler already running when shutdown() starts submits the next
+ * message, which arms the channel again after shutdown() disarmed it. Once
+ * the handler is done, shutdown() must disarm it once more.
+ */
+static void test_tx_irq_shutdown_disarms_after_handler(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_tx_line_fixture(test);
+ u32 wr_en = SUN55I_MSGBOX_WRITE_IRQ_ENABLE(2) / 4;
+ u32 val = 0x11;
+
+ mock_startup(test, &fix->chans[9]);
+ sun55i_msgbox_chan_ops.send_data(&fix->chans[9], &val);
+ KUNIT_ASSERT_EQ(test, mock_read_reg(fix, 3, wr_en), SUN55I_MSGBOX_WR_IRQ_EN_BIT(1));
+
+ fix->rearm_on_disarm = true;
+ sun55i_msgbox_chan_ops.shutdown(&fix->chans[9]);
+ KUNIT_EXPECT_FALSE(test, fix->rearm_on_disarm);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, wr_en), 0U);
+}
+
+static void test_tx_irq_startup_enables_rx_after_line(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_tx_line_fixture(test);
+
+ /* With the line available, startup() enables the receive IRQ too */
+ mock_startup(test, &fix->chans[8]);
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 0, SUN55I_MSGBOX_READ_IRQ_ENABLE(2) / 4) &
+ SUN55I_MSGBOX_RD_IRQ_EN_BIT(0), SUN55I_MSGBOX_RD_IRQ_EN_BIT(0));
+ KUNIT_EXPECT_TRUE(test, test_bit(8, &fix->mbox.tx_irq_held));
+ KUNIT_EXPECT_EQ(test, mock_read_reg(fix, 3, SUN55I_MSGBOX_WRITE_IRQ_THRESHOLD(2, 0) / 4),
+ (u32)SUN55I_MSGBOX_WR_IRQ_THR_ONE_FREE);
+}
+
+/*
+ * startup() that cannot get the TX-done line fails, and the core then clears
+ * chan->cl: the receive IRQ must never have been enabled, or a handler on
+ * another CPU could deliver to the cleared client. The core then calls
+ * shutdown(), which must not release a line this channel never got.
+ */
+static void test_tx_irq_startup_failure_leaves_rx_off(struct kunit *test)
+{
+ struct mock_msgbox_fixture *fix = create_tx_line_fixture(test);
+ struct sun55i_tx_irq *cpus = &fix->mbox.tx_irqs[0];
+ struct sun55i_tx_irq *rv = &fix->mbox.tx_irqs[2];
+
+ /* Channel 9 holds the RV line; channel 0 cannot get the CPUS line */
+ mock_startup(test, &fix->chans[9]);
+ cpus->irq = IRQ_NOTCONNECTED;
+ cpus->name = "sun55i-msgbox-test-cpus";
+
+ KUNIT_EXPECT_EQ(test, sun55i_msgbox_chan_ops.startup(&fix->chans[0]), -ENOTCONN);
+ KUNIT_EXPECT_EQ(test, fix->rd_en_written[0] & SUN55I_MSGBOX_RD_IRQ_EN_BIT(0), 0U);
+ KUNIT_EXPECT_EQ(test, cpus->users, 0U);
+ KUNIT_EXPECT_FALSE(test, test_bit(0, &fix->mbox.tx_irq_held));
+
+ /* The core's shutdown() of channel 0 leaves both lines alone */
+ sun55i_msgbox_chan_ops.shutdown(&fix->chans[0]);
+ KUNIT_EXPECT_EQ(test, cpus->users, 0U);
+ KUNIT_EXPECT_EQ(test, rv->users, 1U);
+ KUNIT_EXPECT_TRUE(test, irq_has_action(rv->irq));
+}
+
+/* =============== Test Suite Registration =============== */
+
+static struct kunit_case sun55i_msgbox_cases[] = {
+ /* Routing Table Tests */
+ KUNIT_CASE(test_chan_to_route_all),
+ /* Functional Driver Ops Tests */
+ KUNIT_CASE(test_functional_send_data_all_twelve_channels),
+ KUNIT_CASE(test_functional_send_data_null),
+ KUNIT_CASE(test_functional_last_tx_done_sweep),
+ KUNIT_CASE(test_functional_send_data_holds_for_full_fifo),
+ KUNIT_CASE(test_functional_shutdown_drops_held_message),
+ KUNIT_CASE(test_functional_peek_data_sweep),
+ KUNIT_CASE(test_functional_startup_and_shutdown),
+ KUNIT_CASE(test_functional_shutdown_isolation),
+ KUNIT_CASE(test_functional_startup_flushes_stale_fifo),
+ KUNIT_CASE(test_functional_startup_capped_at_fifo_max),
+ KUNIT_CASE(test_functional_shutdown_flushes),
+ KUNIT_CASE(test_functional_shutdown_drain_capped_at_fifo_max),
+ /* Message order */
+ KUNIT_CASE(test_irq_delivers_in_fifo_order),
+ KUNIT_CASE(test_send_data_keeps_order),
+ KUNIT_CASE(test_held_message_follows_earlier_ones),
+ KUNIT_CASE(test_held_message_dropped_after_timeout),
+ /* Hardirq Simulation Tests */
+ KUNIT_CASE(test_irq_spurious_returns_none),
+ KUNIT_CASE(test_irq_disabled_channel_ignored),
+ KUNIT_CASE(test_irq_single_message_received),
+ KUNIT_CASE(test_irq_clears_pending_after_drain),
+ KUNIT_CASE(test_irq_callback_may_reply),
+ KUNIT_CASE(test_irq_callback_runs_unlocked),
+ KUNIT_CASE(test_irq_pending_with_empty_fifo),
+ KUNIT_CASE(test_irq_multi_channel_concurrency),
+ KUNIT_CASE(test_irq_multi_port_burst_interleaved),
+ KUNIT_CASE(test_irq_fifo_drain_capped_at_max),
+ KUNIT_CASE(test_irq_channel_crosstalk_isolation),
+ /* TX done by write IRQ ("allwinner,tx-done-irq") */
+ KUNIT_CASE(test_tx_irq_armed_after_every_send),
+ KUNIT_CASE(test_tx_irq_handler_disarms_and_acks),
+ KUNIT_CASE(test_tx_irq_handler_ignores_unarmed),
+ KUNIT_CASE(test_tx_irq_startup_failure_leaves_rx_off),
+ KUNIT_CASE(test_tx_irq_startup_enables_rx_after_line),
+ KUNIT_CASE(test_tx_irq_handler_reports_tx_done),
+ KUNIT_CASE(test_tx_irq_refused_message_resent),
+ KUNIT_CASE(test_tx_irq_line_shared_by_channels),
+ KUNIT_CASE(test_tx_irq_shutdown_disarms_after_handler),
+ {}
+};
+
+static struct kunit_suite sun55i_msgbox_test_suite = {
+ .name = "sun55i_msgbox",
+ .test_cases = sun55i_msgbox_cases,
+};
+
+kunit_test_suite(sun55i_msgbox_test_suite);
+
+MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
+MODULE_AUTHOR("Tim Michals <tcmichals@gmail.com>");
+MODULE_DESCRIPTION("KUnit tests for Allwinner sun55i Message Box driver");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v3 7/7] remoteproc: sunxi: Add KUnit tests
2026-11-01 18:57 [PATCH v3 0/7] Add Allwinner A523 message box and XuanTie E906 remoteproc support Tim Michals
` (5 preceding siblings ...)
2026-11-01 18:57 ` [PATCH v3 6/7] mailbox: sun55i: Add KUnit tests Tim Michals
@ 2026-11-01 18:57 ` Tim Michals
2026-11-01 19:13 ` sashiko-bot
6 siblings, 1 reply; 12+ messages in thread
From: Tim Michals @ 2026-11-01 18:57 UTC (permalink / raw)
To: Jassi Brar, Bjorn Andersson, Mathieu Poirier, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Chen-Yu Tsai, Jernej Skrabec,
Samuel Holland
Cc: linux-sunxi, linux-remoteproc, devicetree, linux-arm-kernel,
linux-kernel, Tim Michals
Add 26 KUnit tests for the sunxi remoteproc driver: translation of both
SRAM windows by core and host address, their edges (the windows are
adjacent, and an access may not straddle them), rejection of zero
length, overflowing da + len and addresses near SRAM that are not E906
SRAM, DRAM and trace left to the core's carveout lookup, boot address
programming and the rejection of addresses above 32 bits, the remap bit
(leaving the DSP's bit alone) and SRAM clearing, the reset and clock
order of .prepare(), .start(), .stop() and .unprepare(), every
.prepare() failure unwound (one parameterised test per step, and a
power domain that does not power up), a core reset that fails in
.start(), the crash interrupt armed only while the core runs, the crash
handler through a real threaded interrupt, the firmware machine check,
.parse_fw() with no, a rejected and a valid resource table and the
memory it registers across two boots, a kick queued behind another
keeping its payload, the mailbox receive callback deferring to the
workqueue, and the work servicing every vring.
Register and SRAM access is redirected with KUnit static stubs to fake
buffers; fake clocks and a fake reset controller on a software node
stand in for the hardware, so the tests run on any architecture.
Signed-off-by: Tim Michals <tcmichals@gmail.com>
---
Changes in v3:
- Moved after the drivers
- depends on SUNXI_REMOTEPROC (Sashiko)
- Register and SRAM access goes through KUnit static stubs, no readl()
on plain memory, so no architecture restriction and no endianness
issue (Sashiko); depends on COMMON_CLK and RESET_CONTROLLER for the
fake clocks and reset controller
- File named sunxi_rproc_kunit.c (kunit/style.rst)
- The kick_msg test is replaced by one that queues a kick behind
another and checks the payload when it is sent (Sashiko)
- New tests: DRAM left to the core carveouts, addresses near SRAM that
are not E906 SRAM, both window edges, remap bit 0 (DSP) preserved,
boot address rejected before STA_ADD is written, receive callback
deferring to the workqueue, every prepare() failure unwound, reset
and clock order, crash interrupt arming and handler, machine check,
parse_fw(), the vring work
- Duplicate tests merged
---
drivers/remoteproc/Kconfig | 15 +
drivers/remoteproc/Makefile | 1 +
drivers/remoteproc/sunxi_rproc_kunit.c | 1314 ++++++++++++++++++++++++
3 files changed, 1330 insertions(+)
create mode 100644 drivers/remoteproc/sunxi_rproc_kunit.c
diff --git a/drivers/remoteproc/Kconfig b/drivers/remoteproc/Kconfig
index d2929a3290c3..6516e272689e 100644
--- a/drivers/remoteproc/Kconfig
+++ b/drivers/remoteproc/Kconfig
@@ -348,6 +348,21 @@ config SUNXI_REMOTEPROC
To compile this driver as a module, choose M here: the module will
be called sunxi_rproc.
+config SUNXI_REMOTEPROC_KUNIT_TEST
+ tristate "KUnit tests for Allwinner sunxi remoteproc" if !KUNIT_ALL_TESTS
+ depends on SUNXI_REMOTEPROC && KUNIT && COMMON_CLK && RESET_CONTROLLER
+ default KUNIT_ALL_TESTS
+ help
+ KUnit tests for the Allwinner sunxi_rproc driver: da_to_va()
+ address translation for both SRAM spaces and DRAM carveouts,
+ prepare()/unprepare() (reset and clock order, every failure
+ unwound, remap bit, SRAM clear), start()/stop() (boot address,
+ crash interrupt), the crash handler, parse_fw() (resource table,
+ carveouts, crash dump segments), kicks, and the vring
+ notifications. Fake clocks and a fake reset controller stand in
+ for the hardware.
+ Say Y here to run these tests during boot or via kunit.py.
+
config TI_K3_DSP_REMOTEPROC
tristate "TI K3 DSP remoteproc support"
depends on ARCH_K3 || COMPILE_TEST
diff --git a/drivers/remoteproc/Makefile b/drivers/remoteproc/Makefile
index bf8c96f69a05..33fa226f26e3 100644
--- a/drivers/remoteproc/Makefile
+++ b/drivers/remoteproc/Makefile
@@ -38,6 +38,7 @@ obj-$(CONFIG_ST_REMOTEPROC) += st_remoteproc.o
obj-$(CONFIG_ST_SLIM_REMOTEPROC) += st_slim_rproc.o
obj-$(CONFIG_STM32_RPROC) += stm32_rproc.o
obj-$(CONFIG_SUNXI_REMOTEPROC) += sunxi_rproc.o
+obj-$(CONFIG_SUNXI_REMOTEPROC_KUNIT_TEST) += sunxi_rproc_kunit.o
obj-$(CONFIG_TI_K3_DSP_REMOTEPROC) += ti_k3_dsp_remoteproc.o ti_k3_common.o
obj-$(CONFIG_TI_K3_M4_REMOTEPROC) += ti_k3_m4_remoteproc.o ti_k3_common.o
obj-$(CONFIG_TI_K3_R5_REMOTEPROC) += ti_k3_r5_remoteproc.o ti_k3_common.o
diff --git a/drivers/remoteproc/sunxi_rproc_kunit.c b/drivers/remoteproc/sunxi_rproc_kunit.c
new file mode 100644
index 000000000000..0931564dc22b
--- /dev/null
+++ b/drivers/remoteproc/sunxi_rproc_kunit.c
@@ -0,0 +1,1314 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * KUnit tests for the Allwinner XuanTie E906 remoteproc driver (sunxi_rproc.c)
+ *
+ * Covers:
+ * - da_to_va() translation of both SRAM windows, their bounds, overflow
+ * and zero-length handling, and that DRAM is left to the core carveouts
+ * - start()/stop(): STA_ADD programming, bootaddr validation, the core
+ * reset and the crash interrupt armed only while the core runs
+ * - the crash handler: disarms the line it was armed on, never twice,
+ * and reports the crash to the core
+ * - prepare()/unprepare(): reset and clock order, every failure unwound,
+ * remap bit, SRAM clearing
+ * - sanity_check(): only 32-bit RISC-V firmware
+ * - parse_fw(): firmware with and without a resource table; carveouts and
+ * crash dump segments registered for every boot and released by the core
+ * - kick(): one immutable payload per vring, valid while queued, and a
+ * full mailbox queue
+ * - mailbox rx callback deferring to the workqueue, and the work notifying
+ * every vring in rproc->notifyids
+ *
+ * Register and SRAM access is redirected (KUnit static stubs) to buffers in
+ * the test context, so the tests run on any architecture, UML included.
+ *
+ * Copyright (C) 2026 Tim Michals <tcmichals@gmail.com>
+ */
+
+#include <kunit/clk.h>
+#include <kunit/device.h>
+#include <kunit/static_stub.h>
+#include <kunit/test.h>
+#include <linux/clk-provider.h>
+#include <linux/elf.h>
+#include <linux/firmware.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/irq.h>
+#include <linux/irqdesc.h>
+#include <linux/mailbox_controller.h>
+#include <linux/pm_domain.h>
+#include <linux/pm_runtime.h>
+#include <linux/property.h>
+#include <linux/remoteproc.h>
+#include <linux/reset.h>
+#include <linux/reset-controller.h>
+#include <linux/sched.h>
+#include <linux/sched/task_stack.h>
+#include <linux/slab.h>
+#include <linux/workqueue.h>
+#include "remoteproc_internal.h"
+#include "sunxi_rproc.h"
+
+/*
+ * Fake VA addresses — sentinel pointers used to verify base + offset arithmetic
+ * without requiring real ioremap MMIO allocations.
+ */
+#define FAKE_SRAM_PTR ((void *)0xA0000000UL)
+#define FAKE_SRAM1_PTR ((void *)0xB0000000UL)
+#define FAKE_SRAM_VA ((void __force __iomem *)FAKE_SRAM_PTR)
+#define FAKE_SRAM1_VA ((void __force __iomem *)FAKE_SRAM1_PTR)
+#define FAKE_DRAM_VA ((void *)0xC0000000UL)
+#define FAKE_TRACE_VA ((void *)0xD0000000UL)
+
+/* Remap register bit 0 belongs to the HiFi4 DSP: the driver leaves it alone */
+#define REMAP_DSP_BIT BIT(0)
+
+/* Standard A527 hardware parameters */
+#define A527_SRAM_PHYS SUN55I_SRAM_SPACE0_SYS
+#define A527_SRAM_SIZE SUN55I_SRAM_SPACE0_SIZE
+#define A527_SRAM1_PHYS SUN55I_SRAM_SPACE1_SYS
+#define A527_SRAM1_SIZE SUN55I_SRAM_SPACE1_SIZE
+#define A527_DRAM_PHYS 0x48000000ULL
+#define A527_DRAM_SIZE 0x100000 /* 1 MB */
+#define A527_TRACE_PHYS 0x50000000ULL
+#define A527_TRACE_SIZE 0x1000 /* 4 KB */
+
+struct test_context {
+ struct rproc rproc;
+ struct sunxi_rproc priv;
+ u32 mock_cfg_regs[0x400 / 4];
+ u32 mock_remap_reg;
+ u8 mock_sram_buf[1024];
+ u8 mock_sram1_buf[1024];
+ struct rproc *vq_rproc;
+ int notified[4];
+ int nr_notified;
+ bool notify_in_kworker;
+ void *kick_data[4];
+ int kick_count;
+};
+
+/* The test context owns every buffer the driver may access as MMIO */
+static void *mock_mmio(void __iomem *addr, size_t len)
+{
+ struct test_context *ctx = kunit_get_current_test()->priv;
+ void *p = (__force void *)addr;
+ void *bufs[][2] = {
+ { ctx->mock_cfg_regs, ctx->mock_cfg_regs + ARRAY_SIZE(ctx->mock_cfg_regs) },
+ { &ctx->mock_remap_reg, &ctx->mock_remap_reg + 1 },
+ { ctx->mock_sram_buf, ctx->mock_sram_buf + sizeof(ctx->mock_sram_buf) },
+ { ctx->mock_sram1_buf, ctx->mock_sram1_buf + sizeof(ctx->mock_sram1_buf) },
+ };
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(bufs); i++)
+ if (p >= bufs[i][0] && p + len <= bufs[i][1])
+ return p;
+ KUNIT_FAIL(kunit_get_current_test(), "MMIO access outside the mock buffers");
+ return NULL;
+}
+
+static u32 mock_readl(void __iomem *addr)
+{
+ u32 *r = mock_mmio(addr, sizeof(*r));
+
+ return r ? *r : 0;
+}
+
+static void mock_writel(u32 val, void __iomem *addr)
+{
+ u32 *r = mock_mmio(addr, sizeof(*r));
+
+ if (r)
+ *r = val;
+}
+
+static void mock_memset_io(void __iomem *addr, int val, size_t len)
+{
+ void *p = mock_mmio(addr, len);
+
+ if (p)
+ memset(p, val, len);
+}
+
+/* What rproc_resource_cleanup() frees after a run, for the fake rproc */
+static void free_carveouts(struct rproc *rproc)
+{
+ struct rproc_mem_entry *entry, *tmp;
+
+ list_for_each_entry_safe(entry, tmp, &rproc->carveouts, node) {
+ list_del(&entry->node);
+ kfree(entry);
+ }
+}
+
+static void mock_rproc_cleanup(void *data)
+{
+ struct rproc *rproc = data;
+
+ free_carveouts(rproc);
+ rproc_coredump_cleanup(rproc);
+ kfree(rproc->cached_table);
+}
+
+static void mock_rproc_free(void *rproc)
+{
+ rproc_free(rproc);
+}
+
+/* A real rproc, for code that needs the core's state: freed after the test */
+static struct rproc *mock_real_rproc(struct kunit *test, struct test_context *ctx)
+{
+ struct rproc *rproc;
+
+ rproc = rproc_alloc(ctx->priv.dev, "sunxi-rproc-test", &sunxi_rproc_ops,
+ NULL, sizeof(struct sunxi_rproc));
+ KUNIT_ASSERT_NOT_NULL(test, rproc);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, mock_rproc_free, rproc), 0);
+ return rproc;
+}
+
+static struct test_context *create_test_ctx(struct kunit *test)
+{
+ struct test_context *ctx;
+
+ ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+
+ test->priv = ctx;
+ kunit_activate_static_stub(test, sunxi_rproc_readl, mock_readl);
+ kunit_activate_static_stub(test, sunxi_rproc_writel, mock_writel);
+ kunit_activate_static_stub(test, sunxi_rproc_memset_io, mock_memset_io);
+
+ ctx->rproc.priv = &ctx->priv;
+ ctx->priv.rproc = &ctx->rproc;
+ INIT_LIST_HEAD(&ctx->rproc.carveouts);
+ INIT_LIST_HEAD(&ctx->rproc.dump_segments);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, mock_rproc_cleanup, &ctx->rproc), 0);
+
+ /* prepare()/unprepare() take a runtime PM reference on the device */
+ ctx->priv.dev = kunit_device_register(test, "sunxi-rproc-test");
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx->priv.dev);
+ pm_runtime_no_callbacks(ctx->priv.dev);
+ KUNIT_ASSERT_EQ(test, devm_pm_runtime_enable(ctx->priv.dev), 0);
+
+ ctx->priv.sram0_va = FAKE_SRAM_VA;
+ ctx->priv.sram0_phys = A527_SRAM_PHYS;
+ ctx->priv.sram0_size = A527_SRAM_SIZE;
+
+ ctx->priv.sram1_va = FAKE_SRAM1_VA;
+ ctx->priv.sram1_phys = A527_SRAM1_PHYS;
+ ctx->priv.sram1_size = A527_SRAM1_SIZE;
+
+ ctx->priv.cfg = &sun55i_riscv_cfg;
+
+ return ctx;
+}
+
+/* ==================== da_to_va: SRAM windows ==================== */
+
+static void test_da_to_va_zero_length_returns_null(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+
+ KUNIT_EXPECT_NULL(test,
+ sunxi_rproc_da_to_va(&ctx->rproc,
+ SUN55I_E906_SRAM_SPACE0_DA, 0, NULL));
+}
+
+/* A crafted ELF segment must not wrap da + len past U64_MAX */
+static void test_da_to_va_rejects_overflow(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+
+ KUNIT_EXPECT_NULL(test, sunxi_rproc_da_to_va(&ctx->rproc, U64_MAX - 0x10, 0x20, NULL));
+ KUNIT_EXPECT_NULL(test, sunxi_rproc_da_to_va(&ctx->rproc, U64_MAX, 1, NULL));
+ KUNIT_EXPECT_NULL(test, sunxi_rproc_da_to_va(&ctx->rproc, SUN55I_E906_SRAM_SPACE0_DA,
+ (size_t)-1, NULL));
+ KUNIT_EXPECT_NULL(test, sunxi_rproc_da_to_va(&ctx->rproc, SUN55I_E906_SRAM_SPACE1_DA,
+ (size_t)-16, NULL));
+}
+
+/* Each window is reached by its core address and by its host address */
+static void expect_window(struct kunit *test, struct test_context *ctx, u64 da, void *base)
+{
+ bool is_iomem = false;
+
+ KUNIT_EXPECT_PTR_EQ(test, sunxi_rproc_da_to_va(&ctx->rproc, da, 0x100, &is_iomem), base);
+ KUNIT_EXPECT_TRUE(test, is_iomem);
+ KUNIT_EXPECT_PTR_EQ(test, sunxi_rproc_da_to_va(&ctx->rproc, da + 0x1000, 0x100, NULL),
+ base + 0x1000);
+}
+
+static void test_da_to_va_sram_space0(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+
+ expect_window(test, ctx, SUN55I_E906_SRAM_SPACE0_DA, FAKE_SRAM_PTR);
+ expect_window(test, ctx, A527_SRAM_PHYS, FAKE_SRAM_PTR);
+}
+
+static void test_da_to_va_sram_space1(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+
+ expect_window(test, ctx, SUN55I_E906_SRAM_SPACE1_DA, FAKE_SRAM1_PTR);
+ expect_window(test, ctx, A527_SRAM1_PHYS, FAKE_SRAM1_PTR);
+}
+
+/*
+ * The two windows are adjacent in the core's address space: space 0 ends at
+ * 0x3fffffff and space 1 starts at 0x40000000. Each edge byte resolves to its
+ * own window, an access may not straddle the two, and nothing resolves below
+ * space 0 or above space 1.
+ */
+static void test_da_to_va_sram_bounds(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+ struct rproc *rproc = &ctx->rproc;
+ u64 s0_end = SUN55I_E906_SRAM_SPACE0_DA + A527_SRAM_SIZE;
+ u64 s1_end = SUN55I_E906_SRAM_SPACE1_DA + A527_SRAM1_SIZE;
+
+ KUNIT_ASSERT_EQ(test, s0_end, (u64)SUN55I_E906_SRAM_SPACE1_DA);
+
+ KUNIT_EXPECT_NULL(test,
+ sunxi_rproc_da_to_va(rproc, SUN55I_E906_SRAM_SPACE0_DA - 1, 1, NULL));
+ KUNIT_EXPECT_PTR_EQ(test, sunxi_rproc_da_to_va(rproc, s0_end - 1, 1, NULL),
+ FAKE_SRAM_PTR + A527_SRAM_SIZE - 1);
+ KUNIT_EXPECT_NULL(test, sunxi_rproc_da_to_va(rproc, s0_end - 1, 2, NULL));
+ KUNIT_EXPECT_PTR_EQ(test, sunxi_rproc_da_to_va(rproc, SUN55I_E906_SRAM_SPACE1_DA, 1, NULL),
+ FAKE_SRAM1_PTR);
+ KUNIT_EXPECT_PTR_EQ(test, sunxi_rproc_da_to_va(rproc, s1_end - 1, 1, NULL),
+ FAKE_SRAM1_PTR + A527_SRAM1_SIZE - 1);
+ KUNIT_EXPECT_NULL(test, sunxi_rproc_da_to_va(rproc, s1_end - 1, 2, NULL));
+ KUNIT_EXPECT_NULL(test, sunxi_rproc_da_to_va(rproc, s1_end, 1, NULL));
+}
+
+/* Addresses that look close to SRAM but are not E906 SRAM */
+static void test_da_to_va_rejects_non_sram(struct kunit *test)
+{
+ static const u64 not_sram[] = {
+ 0x3ff80000, /* not an E906 SRAM alias */
+ 0x00020000, /* HiFi4 DSP RAM */
+ 0x40040000, /* start of the E906's 1:1 DRAM view */
+ 0x10000000,
+ 0x60000000,
+ };
+ struct test_context *ctx = create_test_ctx(test);
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(not_sram); i++)
+ KUNIT_EXPECT_NULL_MSG(test,
+ sunxi_rproc_da_to_va(&ctx->rproc, not_sram[i], 0x100, NULL),
+ "da 0x%llx", not_sram[i]);
+}
+
+static void test_da_to_va_unmapped_regions_return_null(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+ struct rproc *rproc = &ctx->rproc;
+ u64 da0 = SUN55I_E906_SRAM_SPACE0_DA;
+ bool is_iomem = false;
+
+ ctx->priv.sram0_va = NULL;
+ ctx->priv.sram1_va = NULL;
+ KUNIT_EXPECT_NULL(test, sunxi_rproc_da_to_va(rproc, da0, 0x100, &is_iomem));
+ /* Nothing returned, so nothing reported about it either */
+ KUNIT_EXPECT_FALSE(test, is_iomem);
+ KUNIT_EXPECT_NULL(test, sunxi_rproc_da_to_va(rproc, A527_SRAM_PHYS, 0x100, NULL));
+ KUNIT_EXPECT_NULL(test,
+ sunxi_rproc_da_to_va(rproc, SUN55I_E906_SRAM_SPACE1_DA, 0x100, NULL));
+ KUNIT_EXPECT_NULL(test, sunxi_rproc_da_to_va(rproc, A527_SRAM1_PHYS, 0x100, NULL));
+}
+
+/* ==================== da_to_va: DRAM & Trace ==================== */
+
+/*
+ * DRAM (firmware code/data, trace, vrings) is not in the ATT: the driver op
+ * returns NULL and the remoteproc core resolves it from the carveout list.
+ * This is the path the ELF loader takes for segments linked into DRAM.
+ */
+static void add_dram_carveout(struct kunit *test, struct test_context *ctx,
+ void *va, u64 phys, size_t size, const char *name)
+{
+ struct rproc_mem_entry *mem;
+
+ mem = rproc_mem_entry_init(ctx->priv.dev, va, (dma_addr_t)phys, size,
+ (u32)phys, NULL, NULL, "%s", name);
+ KUNIT_ASSERT_NOT_NULL(test, mem);
+ rproc_add_carveout(&ctx->rproc, mem);
+}
+
+static void test_rproc_da_to_va_dram_carveout(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+ bool is_iomem = true;
+ void *va;
+
+ /* rproc->ops is not const: give the core a writable copy */
+ ctx->rproc.ops = kunit_kzalloc(test, sizeof(*ctx->rproc.ops), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->rproc.ops);
+ *ctx->rproc.ops = sunxi_rproc_ops;
+ add_dram_carveout(test, ctx, FAKE_DRAM_VA, A527_DRAM_PHYS, A527_DRAM_SIZE, "dram");
+ add_dram_carveout(test, ctx, FAKE_TRACE_VA, A527_TRACE_PHYS, A527_TRACE_SIZE, "trace");
+
+ /* The driver op leaves DRAM to the core */
+ KUNIT_EXPECT_NULL(test, sunxi_rproc_da_to_va(&ctx->rproc, A527_DRAM_PHYS, 0x100, NULL));
+
+ /* A firmware segment linked into DRAM */
+ va = rproc_da_to_va(&ctx->rproc, A527_DRAM_PHYS + 0x1000, 0x100, &is_iomem);
+ KUNIT_EXPECT_PTR_EQ(test, va, FAKE_DRAM_VA + 0x1000);
+ KUNIT_EXPECT_FALSE(test, is_iomem);
+
+ /* Last byte of DRAM resolves; one byte beyond does not */
+ va = rproc_da_to_va(&ctx->rproc, A527_DRAM_PHYS + A527_DRAM_SIZE - 1, 1, NULL);
+ KUNIT_EXPECT_PTR_EQ(test, va, FAKE_DRAM_VA + A527_DRAM_SIZE - 1);
+ KUNIT_EXPECT_NULL(test, rproc_da_to_va(&ctx->rproc,
+ A527_DRAM_PHYS + A527_DRAM_SIZE - 1, 2, NULL));
+
+ /* Trace buffer */
+ va = rproc_da_to_va(&ctx->rproc, A527_TRACE_PHYS, 0x100, NULL);
+ KUNIT_EXPECT_PTR_EQ(test, va, FAKE_TRACE_VA);
+
+ /* SRAM still goes through the driver's ATT */
+ va = rproc_da_to_va(&ctx->rproc, SUN55I_E906_SRAM_SPACE0_DA, 0x100, &is_iomem);
+ KUNIT_EXPECT_PTR_EQ(test, va, FAKE_SRAM_PTR);
+ KUNIT_EXPECT_TRUE(test, is_iomem);
+}
+
+/* ==================== Lifecycle: start ==================== */
+
+/*
+ * start(), prepare() and unprepare() access CFG, REMAP and both SRAMs
+ * unconditionally (all four are required resources), so point them at real
+ * buffers instead of the fake translation-test pointers.
+ */
+static void use_mmio_mocks(struct test_context *ctx)
+{
+ ctx->priv.cfg_va = (void __iomem *)ctx->mock_cfg_regs;
+ ctx->priv.remap_va = (void __iomem *)&ctx->mock_remap_reg;
+ ctx->priv.sram0_va = (void __iomem *)ctx->mock_sram_buf;
+ ctx->priv.sram0_size = sizeof(ctx->mock_sram_buf);
+ ctx->priv.sram1_va = (void __iomem *)ctx->mock_sram1_buf;
+ ctx->priv.sram1_size = sizeof(ctx->mock_sram1_buf);
+}
+
+static void test_start_bootaddr_programming(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+ int ret;
+
+ use_mmio_mocks(ctx);
+ ctx->rproc.bootaddr = 0x40014000;
+
+ ret = sunxi_rproc_start(&ctx->rproc);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ /* Check that bootaddr was written to STA_ADD_REG (offset 0x204) */
+ KUNIT_EXPECT_EQ(test, ctx->mock_cfg_regs[SUN55I_E906_STA_ADD_REG / 4], 0x40014000U);
+}
+
+static void test_start_rejects_bootaddr_overflow(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+ int ret;
+
+ use_mmio_mocks(ctx);
+ ctx->mock_cfg_regs[SUN55I_E906_STA_ADD_REG / 4] = SUN55I_E906_SRAM_SPACE0_DA;
+
+ /* Addresses beyond 32-bit range are invalid for 32-bit XuanTie E906 */
+ ctx->rproc.bootaddr = 0x100000000ULL;
+
+ ret = sunxi_rproc_start(&ctx->rproc);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ /* Rejected before STA_ADD is touched */
+ KUNIT_EXPECT_EQ(test, ctx->mock_cfg_regs[SUN55I_E906_STA_ADD_REG / 4],
+ (u32)SUN55I_E906_SRAM_SPACE0_DA);
+}
+
+/* ==================== Lifecycle: prepare & unprepare ==================== */
+
+static void test_prepare_and_unprepare_remap(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+ int ret;
+
+ use_mmio_mocks(ctx);
+
+ /* Bit 0 belongs to the DSP (reset value 1): only bit 1 may change */
+ ctx->mock_remap_reg = REMAP_DSP_BIT;
+
+ /* prepare() should set SUNXI_REMAP_SRAMA3_2_BIT (bit 1) */
+ ret = sunxi_rproc_prepare(&ctx->rproc);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->mock_remap_reg,
+ REMAP_DSP_BIT | SUNXI_REMAP_SRAMA3_2_BIT);
+
+ /* unprepare() should clear SUNXI_REMAP_SRAMA3_2_BIT */
+ ret = sunxi_rproc_unprepare(&ctx->rproc);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->mock_remap_reg, REMAP_DSP_BIT);
+}
+
+static void test_prepare_clears_sram(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+ int i, ret;
+
+ use_mmio_mocks(ctx);
+ memset(ctx->mock_sram_buf, 0xAA, sizeof(ctx->mock_sram_buf));
+ memset(ctx->mock_sram1_buf, 0x55, sizeof(ctx->mock_sram1_buf));
+
+ ret = sunxi_rproc_prepare(&ctx->rproc);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ /* Both SRAM buffers must be cleanly zeroed to prevent stale data / ECC faults */
+ for (i = 0; i < sizeof(ctx->mock_sram_buf); i++)
+ KUNIT_EXPECT_EQ(test, ctx->mock_sram_buf[i], 0);
+
+ for (i = 0; i < sizeof(ctx->mock_sram1_buf); i++)
+ KUNIT_EXPECT_EQ(test, ctx->mock_sram1_buf[i], 0);
+
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_unprepare(&ctx->rproc), 0);
+}
+
+/* ==================== kick ==================== */
+
+static int kick_send_data(struct mbox_chan *chan, void *data)
+{
+ struct test_context *ctx = chan->con_priv;
+
+ if (ctx->kick_count < ARRAY_SIZE(ctx->kick_data))
+ ctx->kick_data[ctx->kick_count] = data;
+ ctx->kick_count++;
+ return 0;
+}
+
+static const struct mbox_chan_ops kick_chan_ops = {
+ .send_data = kick_send_data,
+};
+
+/*
+ * The mailbox core keeps the message pointer while a previous request is
+ * active and passes it to the controller later. A kick queued behind another
+ * one must still carry its own vring index when it is finally sent.
+ */
+static void test_kick_payload_outlives_call(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+ struct mbox_controller *ctrl;
+ struct mbox_chan *chan;
+ int i;
+
+ ctrl = kunit_kzalloc(test, sizeof(*ctrl), GFP_KERNEL);
+ chan = kunit_kzalloc(test, sizeof(*chan), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ctrl);
+ KUNIT_ASSERT_NOT_NULL(test, chan);
+
+ ctrl->dev = ctx->priv.dev;
+ ctrl->ops = &kick_chan_ops;
+ spin_lock_init(&chan->lock);
+ chan->mbox = ctrl;
+ chan->cl = &ctx->priv.cl;
+ chan->con_priv = ctx;
+ chan->active_req = MBOX_NO_MSG;
+ /* The test completes each request itself */
+ chan->txdone_method = MBOX_TXDONE_BY_ACK;
+ ctx->priv.mbox_chan = chan;
+
+ /* vring 0 goes out at once; vring 1 is queued behind it */
+ sunxi_rproc_kick(&ctx->rproc, 0);
+ sunxi_rproc_kick(&ctx->rproc, 1);
+ KUNIT_ASSERT_EQ(test, ctx->kick_count, 1);
+
+ /* Completing vring 0 sends the queued kick */
+ mbox_client_txdone(chan, 0);
+ KUNIT_ASSERT_EQ(test, ctx->kick_count, 2);
+ mbox_client_txdone(chan, 0);
+
+ KUNIT_EXPECT_EQ(test, *(u32 *)ctx->kick_data[0], 0U);
+ KUNIT_EXPECT_EQ(test, *(u32 *)ctx->kick_data[1], 1U);
+ KUNIT_EXPECT_FALSE(test, object_is_on_stack(ctx->kick_data[0]));
+ KUNIT_EXPECT_FALSE(test, object_is_on_stack(ctx->kick_data[1]));
+
+ /* A vring without a payload is refused, not sent as another vring */
+ sunxi_rproc_kick(&ctx->rproc, 2);
+ sunxi_rproc_kick(&ctx->rproc, -1);
+ KUNIT_EXPECT_EQ(test, ctx->kick_count, 2);
+
+ /* A full mailbox queue: the kick is dropped with an error, not blocked */
+ chan->active_req = (void *)&ctx->kick_count;
+ for (i = 0; i < MBOX_TX_QUEUE_LEN; i++)
+ sunxi_rproc_kick(&ctx->rproc, i & 1);
+ KUNIT_EXPECT_EQ(test, chan->msg_count, (unsigned int)MBOX_TX_QUEUE_LEN);
+ sunxi_rproc_kick(&ctx->rproc, 0);
+ KUNIT_EXPECT_EQ(test, chan->msg_count, (unsigned int)MBOX_TX_QUEUE_LEN);
+ KUNIT_EXPECT_EQ(test, ctx->kick_count, 2);
+
+ /* No mailbox channel: kick is a no-op */
+ ctx->priv.mbox_chan = NULL;
+ sunxi_rproc_kick(&ctx->rproc, 0);
+ KUNIT_EXPECT_EQ(test, ctx->kick_count, 2);
+}
+
+/* ==================== Mailbox rx callback ==================== */
+
+/*
+ * The mailbox rx callback runs in hardirq context, and the rpmsg receive
+ * path takes mutexes. The callback must only queue the vring work, and the
+ * work must run in a kworker (regression test for calling
+ * rproc_vq_interrupt() from the callback).
+ */
+static void rx_work_probe_fn(struct work_struct *work)
+{
+ struct test_context *ctx = container_of(work, struct test_context, priv.vq_work);
+
+ ctx->nr_notified++;
+ ctx->notify_in_kworker = !!(current->flags & PF_WQ_WORKER);
+}
+
+static void test_rx_callback_defers_to_workqueue(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+
+ ctx->priv.workqueue = alloc_ordered_workqueue("sunxi-rproc-test", WQ_HIGHPRI);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->priv.workqueue);
+ INIT_WORK(&ctx->priv.vq_work, rx_work_probe_fn);
+
+ local_irq_disable();
+ sunxi_rproc_mb_rx_callback(&ctx->priv.cl, NULL);
+ local_irq_enable();
+
+ flush_workqueue(ctx->priv.workqueue);
+ destroy_workqueue(ctx->priv.workqueue);
+
+ KUNIT_EXPECT_EQ(test, ctx->nr_notified, 1);
+ KUNIT_EXPECT_TRUE(test, ctx->notify_in_kworker);
+}
+
+/*
+ * One channel carries every notification, so the driver's work checks every
+ * vring in rproc->notifyids (regression test for servicing only vrings 0 and
+ * 1). Only the per-vring notification is stubbed; the stub follows the test
+ * task, so the work function is called here rather than from a kworker.
+ */
+static int mock_vq_notify(int id, void *ptr, void *data)
+{
+ struct test_context *ctx = kunit_get_current_test()->priv;
+
+ if (ctx->nr_notified < ARRAY_SIZE(ctx->notified))
+ ctx->notified[ctx->nr_notified] = id;
+ ctx->nr_notified++;
+ KUNIT_EXPECT_PTR_EQ(kunit_get_current_test(), data, (void *)ctx->vq_rproc);
+ return 0;
+}
+
+static void test_vq_work_notifies_every_vring(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+ static int vring[3];
+ struct rproc *rproc;
+ int i;
+
+ /* A real rproc, so its notify ID table is set up as the core does */
+ rproc = mock_real_rproc(test, ctx);
+ ctx->priv.rproc = rproc;
+ ctx->vq_rproc = rproc;
+
+ kunit_activate_static_stub(test, sunxi_rproc_vq_notify, mock_vq_notify);
+ for (i = 0; i < ARRAY_SIZE(vring); i++)
+ KUNIT_ASSERT_EQ(test, idr_alloc(&rproc->notifyids, &vring[i], 0, 0,
+ GFP_KERNEL), i);
+
+ sunxi_rproc_vq_work(&ctx->priv.vq_work);
+
+ KUNIT_ASSERT_EQ(test, ctx->nr_notified, 3);
+ for (i = 0; i < 3; i++)
+ KUNIT_EXPECT_EQ(test, ctx->notified[i], i);
+}
+
+/*
+ * Without a stub the notification reaches the core: a notify ID with no vring
+ * is ignored there. A real rproc, so the core's vring lookup is initialised.
+ */
+static void test_vq_notify_unknown_vring(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_vq_notify(5, NULL, mock_real_rproc(test, ctx)), 0);
+}
+
+/* ==================== Lifecycle: stop and the crash interrupt ==================== */
+
+static void mock_free_irq_desc(void *irq)
+{
+ irq_free_desc((unsigned long)irq);
+}
+
+static void mock_free_crash_irq(void *data)
+{
+ struct test_context *ctx = data;
+
+ free_irq(ctx->priv.crash_irq, &ctx->priv);
+}
+
+/* A crash line on the dummy chip, requested like probe() does: not enabled */
+static int mock_crash_line(struct kunit *test, struct test_context *ctx)
+{
+ int irq = irq_alloc_desc(NUMA_NO_NODE);
+
+ KUNIT_ASSERT_GE(test, irq, 0);
+ irq_set_chip_and_handler(irq, &dummy_irq_chip, handle_simple_irq);
+ /* Some architectures (arm, powerpc) mark new descriptors unrequestable */
+ irq_clear_status_flags(irq, IRQ_NOREQUEST | IRQ_NOPROBE);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, mock_free_irq_desc,
+ (void *)(unsigned long)irq), 0);
+ KUNIT_ASSERT_EQ(test, request_threaded_irq(irq, NULL, sunxi_rproc_crash_handler,
+ IRQF_ONESHOT | IRQF_NO_AUTOEN,
+ "sunxi-rproc-test-crash", &ctx->priv), 0);
+ ctx->priv.crash_irq = irq;
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, mock_free_crash_irq, ctx), 0);
+ return irq;
+}
+
+static bool crash_irq_enabled(int irq)
+{
+ return !irqd_irq_disabled(irq_get_irq_data(irq));
+}
+
+/* Disable depth; irq_to_desc() is not exported, so go through the irq_data */
+static unsigned int crash_irq_depth(int irq)
+{
+ return container_of(irq_get_irq_data(irq), struct irq_desc, irq_data)->depth;
+}
+
+static void test_start_stop_arm_crash_irq(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+ int irq;
+
+ use_mmio_mocks(ctx);
+ irq = mock_crash_line(test, ctx);
+ KUNIT_EXPECT_FALSE(test, crash_irq_enabled(irq));
+
+ /* Armed once the core runs; a second start() does not enable it twice */
+ KUNIT_ASSERT_EQ(test, sunxi_rproc_start(&ctx->rproc), 0);
+ KUNIT_ASSERT_EQ(test, sunxi_rproc_start(&ctx->rproc), 0);
+ KUNIT_EXPECT_TRUE(test, crash_irq_enabled(irq));
+ KUNIT_EXPECT_EQ(test, crash_irq_depth(irq), 0U);
+
+ /* stop() disarms it; a second stop() does not disable it twice */
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_ops.stop(&ctx->rproc), 0);
+ KUNIT_EXPECT_FALSE(test, crash_irq_enabled(irq));
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_ops.stop(&ctx->rproc), 0);
+ KUNIT_EXPECT_EQ(test, crash_irq_depth(irq), 1U);
+
+ /* And arms it again on the next start() */
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_start(&ctx->rproc), 0);
+ KUNIT_EXPECT_TRUE(test, crash_irq_enabled(irq));
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_ops.stop(&ctx->rproc), 0);
+}
+
+static void test_stop_without_crash_irq(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+
+ use_mmio_mocks(ctx);
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_start(&ctx->rproc), 0);
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_ops.stop(&ctx->rproc), 0);
+ KUNIT_EXPECT_FALSE(test, test_bit(SUNXI_RPROC_CRASH_IRQ_ARMED, &ctx->priv.flags));
+}
+
+/* Fire the crash line and wait for its threaded handler */
+static void fire_crash_irq(struct kunit *test, int irq)
+{
+ KUNIT_ASSERT_EQ(test, generic_handle_irq_safe(irq), 0);
+ synchronize_irq(irq);
+}
+
+static void test_crash_handler_disarms_and_reports(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+ struct rproc *rproc = mock_real_rproc(test, ctx);
+ int irq;
+
+ use_mmio_mocks(ctx);
+ irq = mock_crash_line(test, ctx);
+
+ /* The crash is reported to a running core; no recovery in a test */
+ ctx->priv.rproc = rproc;
+ rproc->state = RPROC_RUNNING;
+ rproc->recovery_disabled = true;
+
+ /* Armed by start(): the handler masks the line until the next start() */
+ KUNIT_ASSERT_EQ(test, sunxi_rproc_start(&ctx->rproc), 0);
+ fire_crash_irq(test, irq);
+ flush_work(&rproc->crash_handler);
+ KUNIT_EXPECT_FALSE(test, test_bit(SUNXI_RPROC_CRASH_IRQ_ARMED, &ctx->priv.flags));
+ KUNIT_EXPECT_FALSE(test, crash_irq_enabled(irq));
+ KUNIT_EXPECT_EQ(test, crash_irq_depth(irq), 1U);
+ KUNIT_EXPECT_EQ(test, rproc->state, RPROC_CRASHED);
+ KUNIT_EXPECT_EQ(test, rproc->crash_cnt, 1U);
+
+ /* stop() of the crashed core leaves the line masked once */
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_ops.stop(&ctx->rproc), 0);
+ KUNIT_EXPECT_EQ(test, crash_irq_depth(irq), 1U);
+
+ /*
+ * A crash racing stop(): stop() has disarmed the line and disables it
+ * itself, so the handler must not disable it a second time.
+ */
+ rproc->state = RPROC_RUNNING;
+ KUNIT_ASSERT_EQ(test, sunxi_rproc_start(&ctx->rproc), 0);
+ clear_bit(SUNXI_RPROC_CRASH_IRQ_ARMED, &ctx->priv.flags);
+ fire_crash_irq(test, irq);
+ flush_work(&rproc->crash_handler);
+ KUNIT_EXPECT_EQ(test, crash_irq_depth(irq), 0U);
+ KUNIT_EXPECT_EQ(test, rproc->crash_cnt, 2U);
+ disable_irq(irq);
+}
+
+/* ==================== prepare/start with real clocks and resets ==================== */
+
+/*
+ * Fake providers behind the real clock and reset frameworks: a reset
+ * controller on a software node, and clocks registered through the KUnit
+ * clock helpers. Any reset line or clock can be made to fail, so every step
+ * of prepare() and its unwinding is exercised.
+ */
+enum { RST_CFG, RST_CORE, RST_SRAM, RST_DEBUG, NR_RST };
+enum { CLK_BUS, CLK_SRAM, CLK_CORE, NR_CLK };
+
+struct mock_providers {
+ struct reset_controller_dev rcdev;
+ bool asserted[NR_RST];
+ unsigned long fail_rst; /* BIT(id): assert and deassert fail */
+ char log[32]; /* "A1D0..." in call order */
+ int log_len;
+ struct mock_clk {
+ struct clk_hw hw;
+ struct mock_providers *mp;
+ bool fail;
+ } clk[NR_CLK];
+};
+
+static void mock_log(struct mock_providers *mp, char op, unsigned long id)
+{
+ if (mp->log_len < sizeof(mp->log) - 2) {
+ mp->log[mp->log_len++] = op;
+ mp->log[mp->log_len++] = '0' + id;
+ }
+}
+
+static int mock_rst_assert(struct reset_controller_dev *rcdev, unsigned long id)
+{
+ struct mock_providers *mp = container_of(rcdev, struct mock_providers, rcdev);
+
+ if (mp->fail_rst & BIT(id))
+ return -EIO;
+ mock_log(mp, 'A', id);
+ mp->asserted[id] = true;
+ return 0;
+}
+
+static int mock_rst_deassert(struct reset_controller_dev *rcdev, unsigned long id)
+{
+ struct mock_providers *mp = container_of(rcdev, struct mock_providers, rcdev);
+
+ if (mp->fail_rst & BIT(id))
+ return -EIO;
+ mock_log(mp, 'D', id);
+ mp->asserted[id] = false;
+ return 0;
+}
+
+static const struct reset_control_ops mock_rst_ops = {
+ .assert = mock_rst_assert,
+ .deassert = mock_rst_deassert,
+};
+
+static int mock_clk_prepare(struct clk_hw *hw)
+{
+ struct mock_clk *c = container_of(hw, struct mock_clk, hw);
+
+ if (c->fail)
+ return -EIO;
+ mock_log(c->mp, 'C', c - c->mp->clk);
+ return 0;
+}
+
+static const struct clk_ops mock_clk_ops = {
+ .prepare = mock_clk_prepare,
+};
+
+static const struct software_node mock_rst_node = {
+ .name = "sunxi-rproc-test-rst",
+ .properties = (const struct property_entry[]) {
+ PROPERTY_ENTRY_U32("#reset-cells", 1),
+ { }
+ },
+};
+
+static const struct software_node_ref_args mock_rst_refs[] = {
+ SOFTWARE_NODE_REFERENCE(&mock_rst_node, RST_CFG),
+ SOFTWARE_NODE_REFERENCE(&mock_rst_node, RST_CORE),
+ SOFTWARE_NODE_REFERENCE(&mock_rst_node, RST_SRAM),
+ SOFTWARE_NODE_REFERENCE(&mock_rst_node, RST_DEBUG),
+};
+
+static const char * const mock_rst_names[] = { "cfg", "core", "sram", "debug" };
+
+static const struct software_node mock_rproc_node = {
+ .name = "sunxi-rproc-test-rproc",
+ .properties = (const struct property_entry[]) {
+ PROPERTY_ENTRY_REF_ARRAY("resets", mock_rst_refs),
+ PROPERTY_ENTRY_STRING_ARRAY("reset-names", mock_rst_names),
+ { }
+ },
+};
+
+static void mock_remove_swnode(void *dev)
+{
+ device_remove_software_node(dev);
+}
+
+static struct device *mock_swnode_device(struct kunit *test, const char *name,
+ const struct software_node *node)
+{
+ struct device *dev = kunit_device_register(test, name);
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev);
+ KUNIT_ASSERT_EQ(test, device_add_software_node(dev, node), 0);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, mock_remove_swnode, dev), 0);
+ return dev;
+}
+
+static struct mock_providers *use_mock_providers(struct kunit *test, struct test_context *ctx)
+{
+ static const char * const clk_names[NR_CLK] = { "bus", "sram", "core" };
+ struct mock_providers *mp = kunit_kzalloc(test, sizeof(*mp), GFP_KERNEL);
+ struct device *rst_dev, *dev;
+ struct clk *clks[NR_CLK];
+ int i;
+
+ KUNIT_ASSERT_NOT_NULL(test, mp);
+ use_mmio_mocks(ctx);
+
+ /* The reset controller; every line starts asserted */
+ rst_dev = mock_swnode_device(test, "sunxi-rproc-test-rcdev", &mock_rst_node);
+ mp->rcdev.ops = &mock_rst_ops;
+ mp->rcdev.owner = THIS_MODULE;
+ mp->rcdev.dev = rst_dev;
+ mp->rcdev.nr_resets = NR_RST;
+ for (i = 0; i < NR_RST; i++)
+ mp->asserted[i] = true;
+ KUNIT_ASSERT_EQ(test, devm_reset_controller_register(rst_dev, &mp->rcdev), 0);
+
+ /* The remote processor device, with runtime PM like probe() sets up */
+ dev = mock_swnode_device(test, "sunxi-rproc-test-dev", &mock_rproc_node);
+ pm_runtime_no_callbacks(dev);
+ KUNIT_ASSERT_EQ(test, devm_pm_runtime_enable(dev), 0);
+ ctx->priv.dev = dev;
+
+ ctx->priv.rst_cfg = devm_reset_control_get_exclusive(dev, "cfg");
+ ctx->priv.rst_core = devm_reset_control_get_exclusive(dev, "core");
+ ctx->priv.rst_sram = devm_reset_control_get_exclusive(dev, "sram");
+ ctx->priv.rst_debug = devm_reset_control_get_exclusive(dev, "debug");
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx->priv.rst_cfg);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx->priv.rst_core);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx->priv.rst_sram);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx->priv.rst_debug);
+
+ /* Clock names are global: a test may set up several providers */
+ for (i = 0; i < NR_CLK; i++) {
+ static atomic_t seq = ATOMIC_INIT(0);
+ char *name = kunit_kzalloc(test, 48, GFP_KERNEL);
+ struct clk_init_data init = {
+ .name = name,
+ .ops = &mock_clk_ops,
+ };
+
+ KUNIT_ASSERT_NOT_NULL(test, name);
+ snprintf(name, 48, "sunxi-rproc-test-%s-%d", clk_names[i],
+ atomic_inc_return(&seq));
+
+ mp->clk[i].mp = mp;
+ mp->clk[i].hw.init = &init;
+ KUNIT_ASSERT_EQ(test, clk_hw_register_kunit(test, NULL, &mp->clk[i].hw), 0);
+ clks[i] = clk_hw_get_clk_kunit(test, &mp->clk[i].hw, clk_names[i]);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, clks[i]);
+ }
+ ctx->priv.clk_bus = clks[CLK_BUS];
+ ctx->priv.clk_sram = clks[CLK_SRAM];
+ ctx->priv.clk_core = clks[CLK_CORE];
+
+ return mp;
+}
+
+/* Everything as before prepare(): resets asserted, clocks off, no PM reference */
+static void expect_powered_off(struct kunit *test, struct test_context *ctx,
+ struct mock_providers *mp, bool core_held)
+{
+ int i;
+
+ KUNIT_EXPECT_TRUE(test, mp->asserted[RST_CFG]);
+ KUNIT_EXPECT_TRUE(test, mp->asserted[RST_SRAM]);
+ KUNIT_EXPECT_TRUE(test, mp->asserted[RST_DEBUG]);
+ KUNIT_EXPECT_EQ(test, mp->asserted[RST_CORE], core_held);
+ for (i = 0; i < NR_CLK; i++)
+ KUNIT_EXPECT_FALSE_MSG(test, clk_hw_is_prepared(&mp->clk[i].hw), "clock %d", i);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->priv.dev->power.usage_count), 0);
+}
+
+static void test_prepare_unprepare_order(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+ struct mock_providers *mp = use_mock_providers(test, ctx);
+ int i;
+
+ /* The core is held first, then cfg, SRAM and debug are released, then the clocks */
+ KUNIT_ASSERT_EQ(test, sunxi_rproc_prepare(&ctx->rproc), 0);
+ KUNIT_EXPECT_STREQ(test, mp->log, "A1D0D2D3C0C1C2");
+ KUNIT_EXPECT_TRUE(test, mp->asserted[RST_CORE]);
+ for (i = 0; i < NR_CLK; i++)
+ KUNIT_EXPECT_TRUE(test, clk_hw_is_prepared(&mp->clk[i].hw));
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->priv.dev->power.usage_count), 1);
+
+ /* start() releases the core; stop() holds it again */
+ ctx->rproc.bootaddr = SUN55I_E906_SRAM_SPACE0_DA;
+ KUNIT_ASSERT_EQ(test, sunxi_rproc_start(&ctx->rproc), 0);
+ KUNIT_EXPECT_FALSE(test, mp->asserted[RST_CORE]);
+ KUNIT_ASSERT_EQ(test, sunxi_rproc_ops.stop(&ctx->rproc), 0);
+ KUNIT_EXPECT_TRUE(test, mp->asserted[RST_CORE]);
+
+ KUNIT_ASSERT_EQ(test, sunxi_rproc_unprepare(&ctx->rproc), 0);
+ expect_powered_off(test, ctx, mp, true);
+}
+
+struct prepare_fail_case {
+ const char *step;
+ int rst; /* reset line that fails, or -1 */
+ int clk; /* clock that fails, or -1 */
+};
+
+static const struct prepare_fail_case prepare_fail_cases[] = {
+ { "assert core", RST_CORE, -1 },
+ { "deassert cfg", RST_CFG, -1 },
+ { "deassert sram", RST_SRAM, -1 },
+ { "deassert debug", RST_DEBUG, -1 },
+ { "bus clock", -1, CLK_BUS },
+ { "sram clock", -1, CLK_SRAM },
+ { "core clock", -1, CLK_CORE },
+};
+
+static void prepare_fail_desc(const struct prepare_fail_case *c, char *desc)
+{
+ strscpy(desc, c->step, KUNIT_PARAM_DESC_SIZE);
+}
+
+KUNIT_ARRAY_PARAM(prepare_fail, prepare_fail_cases, prepare_fail_desc);
+
+/* Each step of prepare() fails in turn; every earlier step is undone */
+static void test_prepare_unwinds_failure(struct kunit *test)
+{
+ const struct prepare_fail_case *c = test->param_value;
+ struct test_context *ctx = create_test_ctx(test);
+ struct mock_providers *mp = use_mock_providers(test, ctx);
+
+ ctx->mock_remap_reg = REMAP_DSP_BIT;
+
+ if (c->rst >= 0)
+ mp->fail_rst = BIT(c->rst);
+ if (c->clk >= 0)
+ mp->clk[c->clk].fail = true;
+
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_prepare(&ctx->rproc), -EIO);
+ expect_powered_off(test, ctx, mp, true);
+ KUNIT_EXPECT_EQ(test, ctx->mock_remap_reg, REMAP_DSP_BIT);
+}
+
+static void test_start_core_reset_failure(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+ struct mock_providers *mp = use_mock_providers(test, ctx);
+ int irq = mock_crash_line(test, ctx);
+
+ mp->fail_rst = BIT(RST_CORE);
+ ctx->rproc.bootaddr = SUN55I_E906_SRAM_SPACE0_DA;
+
+ /* The core stays held and the crash interrupt stays off */
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_start(&ctx->rproc), -EIO);
+ KUNIT_EXPECT_TRUE(test, mp->asserted[RST_CORE]);
+ KUNIT_EXPECT_FALSE(test, crash_irq_enabled(irq));
+}
+
+/* ==================== prepare: power-up failure ==================== */
+
+static int mock_runtime_resume_fails(struct device *dev)
+{
+ return -EIO;
+}
+
+static struct dev_pm_domain mock_failing_pm_domain = {
+ .ops = { .runtime_resume = mock_runtime_resume_fails },
+};
+
+static void mock_release_dev(struct device *dev)
+{
+ kfree(dev);
+}
+
+static void mock_remove_pd_dev(void *data)
+{
+ struct device *dev = data;
+
+ pm_runtime_disable(dev);
+ dev_pm_domain_set(dev, NULL);
+ device_unregister(dev);
+}
+
+/*
+ * A device whose power domain refuses to power up. A PM domain may only be
+ * set on a device no driver is bound to, so not a KUnit device.
+ */
+static struct device *mock_failing_pd_device(struct kunit *test)
+{
+ struct device *dev = kzalloc(sizeof(*dev), GFP_KERNEL);
+
+ KUNIT_ASSERT_NOT_NULL(test, dev);
+ device_initialize(dev);
+ dev->release = mock_release_dev;
+ if (dev_set_name(dev, "sunxi-rproc-test-pd") || device_add(dev)) {
+ put_device(dev);
+ KUNIT_FAIL_AND_ABORT(test, "cannot add the device");
+ }
+ dev_pm_domain_set(dev, &mock_failing_pm_domain);
+ pm_runtime_enable(dev);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, mock_remove_pd_dev, dev), 0);
+ return dev;
+}
+
+static void test_prepare_power_up_failure(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+ struct device *dev;
+
+ use_mmio_mocks(ctx);
+ ctx->mock_remap_reg = REMAP_DSP_BIT;
+
+ dev = mock_failing_pd_device(test);
+ ctx->priv.dev = dev;
+
+ /* Nothing is touched */
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_prepare(&ctx->rproc), -EIO);
+ KUNIT_EXPECT_EQ(test, ctx->mock_remap_reg, REMAP_DSP_BIT);
+ KUNIT_EXPECT_EQ(test, atomic_read(&dev->power.usage_count), 0);
+}
+
+/* ==================== parse_fw ==================== */
+
+/* A minimal ELF32 file with a section header table and an optional table */
+struct mock_elf {
+ struct elf32_hdr ehdr;
+ struct elf32_shdr shdr[3];
+ char shstrtab[32];
+ struct resource_table table;
+} __packed;
+
+static void mock_elf_init(struct mock_elf *elf, bool with_table, u32 ver)
+{
+ static const char names[] = "\0.resource_table\0.shstrtab";
+
+ memset(elf, 0, sizeof(*elf));
+ memcpy(elf->ehdr.e_ident, ELFMAG, SELFMAG);
+ elf->ehdr.e_ident[EI_CLASS] = ELFCLASS32;
+ elf->ehdr.e_ident[EI_DATA] = IS_ENABLED(CONFIG_CPU_BIG_ENDIAN) ? ELFDATA2MSB : ELFDATA2LSB;
+ elf->ehdr.e_machine = EM_RISCV;
+ /* One program header, so the core's sanity check accepts the file */
+ elf->ehdr.e_phoff = offsetof(struct mock_elf, shdr);
+ elf->ehdr.e_phnum = 1;
+ elf->ehdr.e_shoff = offsetof(struct mock_elf, shdr);
+ elf->ehdr.e_shentsize = sizeof(struct elf32_shdr);
+ elf->ehdr.e_shnum = 3;
+ elf->ehdr.e_shstrndx = 2;
+ memcpy(elf->shstrtab, names, sizeof(names));
+
+ /* Section 1: the resource table, or an unrelated name */
+ elf->shdr[1].sh_name = with_table ? 1 : 17;
+ elf->shdr[1].sh_offset = offsetof(struct mock_elf, table);
+ elf->shdr[1].sh_size = sizeof(elf->table);
+ elf->table.ver = ver;
+
+ /* Section 2: the section name table */
+ elf->shdr[2].sh_name = 17;
+ elf->shdr[2].sh_offset = offsetof(struct mock_elf, shstrtab);
+ elf->shdr[2].sh_size = sizeof(names);
+}
+
+/* Only 32-bit RISC-V firmware is loaded into the E906's memory */
+static void test_sanity_check_machine(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+ struct mock_elf *elf = kunit_kzalloc(test, sizeof(*elf), GFP_KERNEL);
+ struct firmware fw = { .size = sizeof(*elf) };
+
+ KUNIT_ASSERT_NOT_NULL(test, elf);
+ fw.data = (const u8 *)elf;
+
+ mock_elf_init(elf, false, 1);
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_ops.sanity_check(&ctx->rproc, &fw), 0);
+
+ elf->ehdr.e_machine = EM_ARM;
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_ops.sanity_check(&ctx->rproc, &fw), -EINVAL);
+
+ /* What the core itself rejects stays rejected */
+ mock_elf_init(elf, false, 1);
+ elf->ehdr.e_ident[EI_MAG0] = 0;
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_ops.sanity_check(&ctx->rproc, &fw), -EINVAL);
+}
+
+static void test_parse_fw_resource_table(struct kunit *test)
+{
+ struct test_context *ctx = create_test_ctx(test);
+ struct mock_elf *elf = kunit_kzalloc(test, sizeof(*elf), GFP_KERNEL);
+ struct firmware fw = { .size = sizeof(*elf) };
+
+ KUNIT_ASSERT_NOT_NULL(test, elf);
+ fw.data = (const u8 *)elf;
+
+ /* No resource table: the firmware runs without vrings */
+ mock_elf_init(elf, false, 1);
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_ops.parse_fw(&ctx->rproc, &fw), 0);
+ KUNIT_EXPECT_NULL(test, ctx->rproc.table_ptr);
+
+ /* A table the core rejects is handled the same way (stm32 does too) */
+ mock_elf_init(elf, true, 2);
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_ops.parse_fw(&ctx->rproc, &fw), 0);
+ KUNIT_EXPECT_NULL(test, ctx->rproc.table_ptr);
+
+ /* A valid table is cached for the core */
+ mock_elf_init(elf, true, 1);
+ KUNIT_EXPECT_EQ(test, sunxi_rproc_ops.parse_fw(&ctx->rproc, &fw), 0);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->rproc.table_ptr);
+ KUNIT_EXPECT_EQ(test, ctx->rproc.table_ptr->ver, 1U);
+}
+
+/*
+ * parse_fw() registers the memory regions as carveouts and adds the crash
+ * dump segments: both SRAMs by the core's addresses, then each region that is
+ * not on an SRAM. The core frees both after every run, so they are added
+ * again on the next boot.
+ */
+static void test_parse_fw_registers_memory(struct kunit *test)
+{
+ static const struct {
+ dma_addr_t da;
+ size_t size;
+ } want[] = {
+ { SUN55I_E906_SRAM_SPACE0_DA, A527_SRAM_SIZE },
+ { SUN55I_E906_SRAM_SPACE1_DA, A527_SRAM1_SIZE },
+ { A527_DRAM_PHYS, A527_DRAM_SIZE },
+ };
+ struct test_context *ctx = create_test_ctx(test);
+ struct mock_elf *elf = kunit_kzalloc(test, sizeof(*elf), GFP_KERNEL);
+ struct firmware fw = { .size = sizeof(*elf) };
+ struct rproc *rproc = mock_real_rproc(test, ctx);
+ struct rproc_dump_segment *seg;
+ struct rproc_mem_entry *mem;
+ int boot, n;
+
+ KUNIT_ASSERT_NOT_NULL(test, elf);
+ fw.data = (const u8 *)elf;
+ mock_elf_init(elf, false, 1);
+
+ rproc->priv = &ctx->priv;
+ ctx->priv.rproc = rproc;
+ ctx->priv.num_rmem = 2;
+ ctx->priv.rmem[0].name = "vram"; /* a pool on SRAM: already dumped */
+ ctx->priv.rmem[0].va = FAKE_SRAM1_PTR;
+ ctx->priv.rmem[0].phys = A527_SRAM1_PHYS;
+ ctx->priv.rmem[0].size = 0x1000;
+ ctx->priv.rmem[0].is_iomem = true;
+ ctx->priv.rmem[1].name = "dram";
+ ctx->priv.rmem[1].va = FAKE_DRAM_VA;
+ ctx->priv.rmem[1].phys = A527_DRAM_PHYS;
+ ctx->priv.rmem[1].size = A527_DRAM_SIZE;
+
+ for (boot = 0; boot < 2; boot++) {
+ KUNIT_ASSERT_EQ(test, sunxi_rproc_ops.parse_fw(rproc, &fw), 0);
+
+ n = 0;
+ list_for_each_entry(mem, &rproc->carveouts, node) {
+ KUNIT_ASSERT_LT(test, n, 2);
+ KUNIT_EXPECT_STREQ(test, mem->name, ctx->priv.rmem[n].name);
+ KUNIT_EXPECT_EQ(test, mem->da, (u32)ctx->priv.rmem[n].phys);
+ KUNIT_EXPECT_EQ(test, mem->is_iomem, ctx->priv.rmem[n].is_iomem);
+ n++;
+ }
+ KUNIT_EXPECT_EQ_MSG(test, n, 2, "boot %d", boot);
+
+ n = 0;
+ list_for_each_entry(seg, &rproc->dump_segments, node) {
+ KUNIT_ASSERT_LT(test, n, (int)ARRAY_SIZE(want));
+ KUNIT_EXPECT_EQ_MSG(test, seg->da, want[n].da, "segment %d", n);
+ KUNIT_EXPECT_EQ_MSG(test, seg->size, want[n].size, "segment %d", n);
+ n++;
+ }
+ KUNIT_EXPECT_EQ_MSG(test, n, (int)ARRAY_SIZE(want), "boot %d", boot);
+
+ /* The core's cleanup after a run */
+ rproc_resource_cleanup(rproc);
+ KUNIT_EXPECT_TRUE(test, list_empty(&rproc->carveouts));
+ KUNIT_EXPECT_TRUE(test, list_empty(&rproc->dump_segments));
+ }
+}
+
+static struct kunit_case sunxi_rproc_test_cases[] = {
+ /* da_to_va: SRAM windows */
+ KUNIT_CASE(test_da_to_va_zero_length_returns_null),
+ KUNIT_CASE(test_da_to_va_rejects_overflow),
+ KUNIT_CASE(test_da_to_va_sram_space0),
+ KUNIT_CASE(test_da_to_va_sram_space1),
+ KUNIT_CASE(test_da_to_va_sram_bounds),
+ KUNIT_CASE(test_da_to_va_rejects_non_sram),
+ KUNIT_CASE(test_da_to_va_unmapped_regions_return_null),
+ /* da_to_va: DRAM and trace through the core */
+ KUNIT_CASE(test_rproc_da_to_va_dram_carveout),
+ /* Lifecycle: start */
+ KUNIT_CASE(test_start_bootaddr_programming),
+ KUNIT_CASE(test_start_rejects_bootaddr_overflow),
+ KUNIT_CASE(test_start_stop_arm_crash_irq),
+ KUNIT_CASE(test_stop_without_crash_irq),
+ KUNIT_CASE(test_crash_handler_disarms_and_reports),
+ /* Lifecycle: prepare and unprepare */
+ KUNIT_CASE(test_prepare_and_unprepare_remap),
+ KUNIT_CASE(test_prepare_clears_sram),
+ KUNIT_CASE(test_prepare_power_up_failure),
+ KUNIT_CASE(test_prepare_unprepare_order),
+ KUNIT_CASE_PARAM(test_prepare_unwinds_failure, prepare_fail_gen_params),
+ KUNIT_CASE(test_start_core_reset_failure),
+ /* Firmware */
+ KUNIT_CASE(test_sanity_check_machine),
+ KUNIT_CASE(test_parse_fw_resource_table),
+ KUNIT_CASE(test_parse_fw_registers_memory),
+ /* kick and mailbox rx callback */
+ KUNIT_CASE(test_kick_payload_outlives_call),
+ KUNIT_CASE(test_rx_callback_defers_to_workqueue),
+ KUNIT_CASE(test_vq_work_notifies_every_vring),
+ KUNIT_CASE(test_vq_notify_unknown_vring),
+ {}
+};
+
+static struct kunit_suite sunxi_rproc_test_suite = {
+ .name = "sunxi_rproc",
+ .test_cases = sunxi_rproc_test_cases,
+};
+
+kunit_test_suite(sunxi_rproc_test_suite);
+
+MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
+MODULE_AUTHOR("Tim Michals <tcmichals@gmail.com>");
+MODULE_DESCRIPTION("KUnit tests for Allwinner sunxi remoteproc driver");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH v3 1/7] dt-bindings: mailbox: Add Allwinner A523 message box
2026-11-01 18:57 ` [PATCH v3 1/7] dt-bindings: mailbox: Add Allwinner A523 message box Tim Michals
@ 2026-11-01 19:06 ` sashiko-bot
0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-11-01 19:06 UTC (permalink / raw)
To: Tim Michals; +Cc: linux-sunxi, devicetree, robh, conor+dt
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] dt-bindings: mailbox: sun55i-a523: allwinner,tx-done-irq encodes driver policy
--
commit 84f2f43ac2a1769e84ee3a5f62703aa7b08889d3
Author: Tim Michals <tcmichals@gmail.com>
dt-bindings: mailbox: Add Allwinner A523 message box
This commit adds the Devicetree binding schema for the Allwinner A523, A527,
and T527 (sun55i) message box controller, describing the four processor
register banks, clocks, resets, interrupts, and an optional
allwinner,tx-done-irq property.
> diff --git a/Documentation/devicetree/bindings/mailbox/allwinner,sun55i-a523-msgbox.yaml b/Documentation/devicetree/bindings/mailbox/allwinner,sun55i-a523-msgbox.yaml
> new file mode 100644
> index 0000000000000..47b6fe304dbc8
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/mailbox/allwinner,sun55i-a523-msgbox.yaml
[ ... ]
> + interrupts:
> + items:
> + - description: Cortex-A55 bank read interrupt, messages for the host
> + - description: HiFi4 DSP bank write interrupt for the host
> + - description: CPUS bank write interrupt for the host
> + - description: XuanTie RISC-V bank write interrupt for the host
> +
> + interrupt-names:
> + items:
> + - const: arm
> + - const: dsp
> + - const: cpus
> + - const: rv
> +
> + allwinner,tx-done-irq:
> + type: boolean
> + description:
> + Signal TX done with the write interrupt of the receiver's bank instead
> + of polling. A message is done once the receiver's FIFO has room for the
> + next one either way; the write interrupt is raised while the FIFO has
> + room. Without this property a full FIFO is polled. Received messages
> + use the host bank read interrupt either way.
[Severity: Medium]
Since the schema unconditionally requires all four hardware interrupt lines
(arm, dsp, cpus, and rv) in interrupts and interrupt-names, and every A523
SoC wires the three remote bank write interrupts to the GIC, this boolean
property appears to only select between interrupt-driven and polled TX
completion in the driver rather than describing a hardware difference.
Given that Devicetree bindings should describe hardware capabilities and
wiring rather than OS driver runtime policy, should allwinner,tx-done-irq be
dropped from the binding so the driver decides how to handle TX completion?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261101185735.762559-1-tcmichals@gmail.com?part=1
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v3 7/7] remoteproc: sunxi: Add KUnit tests
2026-11-01 18:57 ` [PATCH v3 7/7] remoteproc: sunxi: " Tim Michals
@ 2026-11-01 19:13 ` sashiko-bot
0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-11-01 19:13 UTC (permalink / raw)
To: Tim Michals; +Cc: linux-sunxi, devicetree, robh, conor+dt
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] remoteproc: sunxi: missing CONFIG_PM dependency in KUnit test
- [Low] remoteproc: sunxi: missing __force on __iomem casts in use_mmio_mocks()
--
commit 9db9877d3095c9678cf19b6cc5cf48fbc1c0e95d
Author: Tim Michals <tcmichals@gmail.com>
remoteproc: sunxi: Add KUnit tests
This commit adds 26 KUnit tests for the Allwinner sunxi remoteproc driver,
covering address translation, lifecycle callbacks, crash handling, firmware
parsing, and mailbox notifications using KUnit static stubs and mock
providers.
> diff --git a/drivers/remoteproc/Kconfig b/drivers/remoteproc/Kconfig
> index d2929a3290c3f..6516e272689e4 100644
> --- a/drivers/remoteproc/Kconfig
> +++ b/drivers/remoteproc/Kconfig
> @@ -348,6 +348,21 @@ config SUNXI_REMOTEPROC
> To compile this driver as a module, choose M here: the module will
> be called sunxi_rproc.
>
> +config SUNXI_REMOTEPROC_KUNIT_TEST
> + tristate "KUnit tests for Allwinner sunxi remoteproc" if !KUNIT_ALL_TESTS
> + depends on SUNXI_REMOTEPROC && KUNIT && COMMON_CLK && RESET_CONTROLLER
> + default KUNIT_ALL_TESTS
[Severity: Medium]
Should SUNXI_REMOTEPROC_KUNIT_TEST also depend on PM?
In include/linux/pm.h, the usage_count member of struct dev_pm_info is only
defined when CONFIG_PM is enabled:
#ifdef CONFIG_PM
...
atomic_t usage_count;
When CONFIG_PM is disabled, expect_powered_off(),
test_prepare_unprepare_order(), and test_prepare_power_up_failure() in
drivers/remoteproc/sunxi_rproc_kunit.c fail to compile because they read
dev->power.usage_count unconditionally.
In addition, test_prepare_power_up_failure() relies on dev_pm_domain_set()
and pm_runtime_enable() so that sunxi_rproc_prepare() ->
pm_runtime_resume_and_get() invokes mock_failing_pm_domain.ops.runtime_resume
and returns -EIO. When CONFIG_PM is disabled, those helpers are no-op stubs
and pm_runtime_resume_and_get() returns 0 instead of -EIO.
[ ... ]
> diff --git a/drivers/remoteproc/sunxi_rproc_kunit.c b/drivers/remoteproc/sunxi_rproc_kunit.c
> new file mode 100644
> index 0000000000000..0931564dc22bb
> --- /dev/null
> +++ b/drivers/remoteproc/sunxi_rproc_kunit.c
[ ... ]
> +static void use_mmio_mocks(struct test_context *ctx)
> +{
> + ctx->priv.cfg_va = (void __iomem *)ctx->mock_cfg_regs;
> + ctx->priv.remap_va = (void __iomem *)&ctx->mock_remap_reg;
> + ctx->priv.sram0_va = (void __iomem *)ctx->mock_sram_buf;
> + ctx->priv.sram0_size = sizeof(ctx->mock_sram_buf);
> + ctx->priv.sram1_va = (void __iomem *)ctx->mock_sram1_buf;
> + ctx->priv.sram1_size = sizeof(ctx->mock_sram1_buf);
> +}
[Severity: Low]
Unlike FAKE_SRAM_VA and FAKE_SRAM1_VA, which use (void __force __iomem *),
the casts in use_mmio_mocks() omit __force when casting normal kernel
memory buffers to __iomem pointers. Should these casts use
(void __force __iomem *) to avoid Sparse address-space warnings when
building with C=1?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261101185735.762559-1-tcmichals@gmail.com?part=7
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v3 2/7] mailbox: sun55i: Add Allwinner A523 message box driver
2026-11-01 18:57 ` [PATCH v3 2/7] mailbox: sun55i: Add Allwinner A523 message box driver Tim Michals
@ 2026-11-01 19:17 ` sashiko-bot
0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-11-01 19:17 UTC (permalink / raw)
To: Tim Michals; +Cc: linux-sunxi, devicetree, robh, conor+dt
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] mailbox: sun55i: TX IRQ race in sun55i_msgbox_shutdown() on shared route
- [High] mailbox: sun55i: UAF and race returning -EBUSY in sun55i_msgbox_send_data()
- [Medium] mailbox: sun55i: lock drop race enabling TX IRQ in sun55i_msgbox_send_data()
--
commit e3cf2bfaf2bd6f778a72431227fd527e0aef2173
Author: Tim Michals <tcmichals@gmail.com>
mailbox: sun55i: Add Allwinner A523 message box driver
This commit adds a mailbox controller driver for the Allwinner sun55i (A523)
message box, supporting 12 hardware FIFO channels between the Cortex-A55 host
and the CPUS, HiFi4 DSP, and XuanTie E906 processors with either polled or
write-IRQ TX completion.
> diff --git a/drivers/mailbox/sun55i-msgbox.c b/drivers/mailbox/sun55i-msgbox.c
> new file mode 100644
> index 0000000000000..1618c16219fc2
> --- /dev/null
> +++ b/drivers/mailbox/sun55i-msgbox.c
> @@ -0,0 +1,613 @@
[ ... ]
> +static void sun55i_msgbox_tx_irq_put(struct sun55i_msgbox *mbox, int local_n)
> +{
> + struct sun55i_tx_irq *tx = &mbox->tx_irqs[local_n];
> +
> + mutex_lock(&mbox->tx_irq_lock);
> + if (!--tx->users) {
> + free_irq(tx->irq, tx);
> + } else {
> + /* The line stays in use: let a running handler finish with us */
> + synchronize_irq(tx->irq);
> + }
> + mutex_unlock(&mbox->tx_irq_lock);
> +}
> +
> +static int sun55i_msgbox_send_data(struct mbox_chan *chan, void *data)
> +{
> + struct sun55i_msgbox *mbox = to_sun55i_msgbox(chan);
> + int n = chan - mbox->controller.chans;
> + int local_n, p, remote_id, remote_n;
> + unsigned long flags;
> + u32 msg = 0;
> + bool full;
> +
> + if (data)
> + memcpy(&msg, data, sizeof(msg));
> +
> + sun55i_msgbox_chan_to_route(n, &local_n, &p, &remote_id, &remote_n);
> +
> + /*
> + * Under the lock: in timer mode the poll timer may write a held
> + * message into the same FIFO from another CPU (last_tx_done()).
> + */
> + spin_lock_irqsave(&mbox->lock, flags);
> +
> + /*
> + * The core calls send_data() only with no request active, so a message
> + * still held here is one it gave up on (a blocking send timed out):
> + * drop it rather than send it after this newer one.
> + */
> + __clear_bit(n, &mbox->tx_held);
> +
> + /*
> + * Writing a full FIFO corrupts its count, so never do it. In write IRQ
> + * mode the IRQ armed below retries the message once a slot is free. In
> + * timer mode the core polls only a channel whose send succeeded, so
> + * hold the message and let last_tx_done() write it.
> + */
> + full = sun55i_msgbox_tx_count(mbox, remote_id, remote_n, p) >= SUN55I_FIFO_MAX;
> + if (!full) {
> + sun55i_msgbox_writel(msg, mbox->regs[remote_id] +
> + SUN55I_MSGBOX_MSG_FIFO(remote_n, p));
> + } else if (!mbox->tx_done_irq) {
> + mbox->tx_held_msg[n] = msg;
> + __set_bit(n, &mbox->tx_held);
> + }
> +
> + spin_unlock_irqrestore(&mbox->lock, flags);
[Severity: Medium]
Can dropping mbox->lock here before calling sun55i_msgbox_tx_irq_enable()
cause a premature TX completion when a previous blocking send timed out?
If an earlier blocking send timed out in mbox_send_message() ->
tx_tick(chan, -ETIME), SUN55I_MSGBOX_WR_IRQ_EN_BIT(p) stays enabled in the
remote bank. When the remote processor later reads a message and frees a slot,
SUN55I_MSGBOX_WR_IRQ_PEND_BIT(p) is latched in
SUN55I_MSGBOX_WRITE_IRQ_STATUS(remote_n).
When the next sun55i_msgbox_send_data() writes to SUN55I_MSGBOX_MSG_FIFO and
fills the FIFO to SUN55I_FIFO_MAX, releasing mbox->lock before clearing the
latched status bit in sun55i_msgbox_tx_irq_enable() opens a window where
sun55i_msgbox_tx_irq() on another CPU can acquire mbox->lock, see both
WR_IRQ_EN_BIT(p) and the stale latched WR_IRQ_PEND_BIT(p) set, and call
mbox_chan_txdone() to complete the new message while the FIFO is still full.
Should the write IRQ status clear and enable in sun55i_msgbox_tx_irq_enable()
be performed under the same mbox->lock critical section as the FIFO write?
> +
> + /*
> + * A kick is a doorbell, the data is in shared memory: it is done once
> + * the FIFO accepted it and has room for the next one. The write IRQ
> + * reports exactly that, at once or when the receiver reads a message.
> + */
> + if (mbox->tx_done_irq) {
> + sun55i_msgbox_tx_irq_enable(mbox, remote_id, remote_n, p, true);
> + return full ? -EBUSY : 0;
> + }
[Severity: High]
What happens in write-IRQ mode (mbox->tx_done_irq) when the hardware FIFO is
full and sun55i_msgbox_send_data() arms the write IRQ and returns -EBUSY?
In drivers/mailbox/mailbox.c, msg_submit() only sets chan->active_req = data
and decrements chan->msg_count when send_data() returns 0:
drivers/mailbox/mailbox.c:msg_submit() {
...
if (chan->cl->tx_prepare)
chan->cl->tx_prepare(chan->cl, data);
/* Try to submit a message to the MBOX controller */
err = chan->mbox->ops->send_data(chan, data);
if (!err) {
chan->active_req = data;
chan->msg_count--;
}
...
}
When send_data() returns -EBUSY, chan->active_req remains NULL and the
caller's data pointer stays in chan->msg_data[]. If a blocking send in
mbox_send_message() then times out in wait_for_completion_timeout(), it calls
tx_tick(chan, -ETIME):
drivers/mailbox/mailbox.c:tx_tick() {
...
spin_lock_irqsave(&chan->lock, flags);
mssg = chan->active_req;
chan->active_req = NULL;
spin_unlock_irqrestore(&chan->lock, flags);
/* Submit next message */
msg_submit(chan);
if (!mssg)
return;
...
}
Because chan->active_req is NULL, tx_tick() does not remove the timed-out
message from chan->msg_data[]. Instead, mbox_send_message() returns -ETIME to
the caller while its message pointer (often stack-allocated in blocking mode)
is still queued in chan->msg_data[] with the write IRQ enabled. When the
remote processor later drains a FIFO slot, sun55i_msgbox_tx_irq() ->
mbox_chan_txdone() -> tx_tick() -> msg_submit() will call cl->tx_prepare(),
sun55i_msgbox_send_data() (which runs memcpy(&msg, data, sizeof(msg))), and
cl->tx_done() on the dangling pointer.
Even without a timeout, while chan->active_req is NULL and the first message
waits in chan->msg_data[] for the write IRQ, a second mbox_send_message() call
on another CPU can run msg_submit() just as a FIFO slot frees: CPU 0 sees
chan->active_req == NULL, calls cl->tx_prepare() a second time on the first
message, writes it into the single free FIFO slot (refilling the FIFO), and
sets chan->active_req. The concurrent sun55i_msgbox_tx_irq() on CPU 1 then
calls mbox_chan_txdone() and prematurely completes the message while the FIFO
is full.
Should write-IRQ mode also buffer the message in mbox->tx_held_msg[n] and
return 0 when the FIFO is full, writing the held message from
sun55i_msgbox_tx_irq() once a slot is free?
[ ... ]
> +static void sun55i_msgbox_shutdown(struct mbox_chan *chan)
> +{
> + struct sun55i_msgbox *mbox = to_sun55i_msgbox(chan);
> + void __iomem *base = mbox->regs[SUN55I_PROC_ARM];
> + int n = chan - mbox->controller.chans;
> + int local_n, p, remote_id, remote_n;
> + u32 stale[SUN55I_FIFO_MAX];
> + unsigned long flags;
> + u32 val;
> +
> + sun55i_msgbox_chan_to_route(n, &local_n, &p, &remote_id, &remote_n);
> +
> + /* Disable the receive IRQ; a message held for a full FIFO is dropped */
> + spin_lock_irqsave(&mbox->lock, flags);
> + val = sun55i_msgbox_readl(base + SUN55I_MSGBOX_READ_IRQ_ENABLE(local_n));
> + val &= ~SUN55I_MSGBOX_RD_IRQ_EN_BIT(p);
> + sun55i_msgbox_writel(val, base + SUN55I_MSGBOX_READ_IRQ_ENABLE(local_n));
> + __clear_bit(n, &mbox->tx_held);
> + spin_unlock_irqrestore(&mbox->lock, flags);
> +
> + /* The handler delivers outside the lock: let it finish with this channel */
> + synchronize_irq(mbox->irq);
> +
> + /* Release the line only if this channel's startup() got it */
> + if (test_and_clear_bit(n, &mbox->tx_irq_held)) {
> + sun55i_msgbox_tx_irq_enable(mbox, remote_id, remote_n, p, false);
> + sun55i_msgbox_tx_irq_put(mbox, local_n);
> + /*
> + * A TX-done handler that was already running submits the next
> + * message through send_data(), which arms the IRQ again. It has
> + * finished now (free_irq() or synchronize_irq() above), so
> + * disarm once more.
> + */
> + sun55i_msgbox_tx_irq_enable(mbox, remote_id, remote_n, p, false);
> + }
[Severity: High]
Can a second sun55i_msgbox_tx_irq() invocation still race with channel
teardown here when another channel on the same route remains active
(tx->users > 1)?
In mbox_free_channel(), chan->mbox->ops->shutdown(chan) runs before chan->cl
is cleared, and queued messages may still be present in chan->msg_data[]. When
tx->users > 1 on local_n, sun55i_msgbox_tx_irq_put() only calls
synchronize_irq(tx->irq) and leaves tx->irq enabled at the interrupt
controller.
If an in-flight sun55i_msgbox_tx_irq() was already past mbox->lock on another
CPU, it calls:
sun55i_msgbox_tx_irq()
mbox_chan_txdone()
tx_tick()
msg_submit()
sun55i_msgbox_send_data()
sun55i_msgbox_tx_irq_enable(mbox, remote_id, remote_n, p, true)
Because the write IRQ is level-triggered whenever at least one FIFO slot is
free, re-enabling SUN55I_MSGBOX_WR_IRQ_EN_BIT(p) in sun55i_msgbox_send_data()
can immediately assert tx->irq again as soon as the first handler returns.
Waiters in sun55i_msgbox_tx_irq_put()'s synchronize_irq(tx->irq) only wait for
that first handler invocation to complete, so a second sun55i_msgbox_tx_irq()
can fire before the second sun55i_msgbox_tx_irq_enable(..., false) runs and
execute mbox_chan_txdone() -> tx_tick() concurrently with mbox_free_channel()
setting chan->cl = NULL (or re-arm the write IRQ again if more messages were
queued).
Would checking test_bit(n, &mbox->tx_irq_held) under mbox->lock before arming
the write IRQ in sun55i_msgbox_send_data() avoid re-enabling the IRQ after
sun55i_msgbox_shutdown() has begun?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261101185735.762559-1-tcmichals@gmail.com?part=2
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v3 4/7] remoteproc: sunxi: Add Allwinner A523 XuanTie E906 driver
2026-11-01 18:57 ` [PATCH v3 4/7] remoteproc: sunxi: Add Allwinner A523 XuanTie E906 driver Tim Michals
@ 2026-11-01 19:24 ` sashiko-bot
0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-11-01 19:24 UTC (permalink / raw)
To: Tim Michals; +Cc: linux-sunxi, devicetree, robh, conor+dt
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] remoteproc: sunxi: missing IRQ sync in sunxi_rproc_stop() races crash handler
- [High] remoteproc: sunxi: race and UAF in sunxi_rproc_vq_work() on notifyids
--
commit f895edd61f6cf2e80a5341959b347c747139fe48
Author: Tim Michals <tcmichals@gmail.com>
remoteproc: sunxi: Add Allwinner A523 XuanTie E906 driver
This commit adds a remoteproc driver for the XuanTie E906 RISC-V core on
Allwinner A523, A527, and T527 SoCs, handling power domains, clocks, resets,
SRAM mappings, memory-region carveouts, mailbox notifications, and crash
reporting.
> diff --git a/drivers/remoteproc/sunxi_rproc.c b/drivers/remoteproc/sunxi_rproc.c
> new file mode 100644
> index 0000000000000..1cee7110f0b8c
> --- /dev/null
> +++ b/drivers/remoteproc/sunxi_rproc.c
> @@ -0,0 +1,855 @@
[ ... ]
> +VISIBLE_IF_KUNIT
> +int sunxi_rproc_vq_notify(int id, void *ptr, void *data)
> +{
> + KUNIT_STATIC_STUB_REDIRECT(sunxi_rproc_vq_notify, id, ptr, data);
> + rproc_vq_interrupt(data, id);
> + return 0;
> +}
> +EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_vq_notify);
> +
> +/* One channel carries all notifications: check every vring */
> +VISIBLE_IF_KUNIT
> +void sunxi_rproc_vq_work(struct work_struct *work)
> +{
> + struct sunxi_rproc *priv = container_of(work, struct sunxi_rproc, vq_work);
> +
> + idr_for_each(&priv->rproc->notifyids, sunxi_rproc_vq_notify, priv->rproc);
> +}
> +EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_vq_work);
[Severity: High]
As reported by Sashiko on v2 [1], in drivers/remoteproc/sunxi_rproc.c, can
sunxi_rproc_vq_work() race with rproc_resource_cleanup() during
rproc_shutdown(), rproc_boot_recovery(), or sunxi_rproc_remove() ->
rproc_del() and trigger a use-after-free?
Because priv->mbox_chan stays active across remoteproc stops, crash
recoveries, and throughout rproc_del() in sunxi_rproc_remove(), and neither
sunxi_rproc_stop() nor sunxi_rproc_unprepare() cancels priv->vq_work,
sunxi_rproc_vq_work() can run concurrently with rproc_resource_cleanup().
In sunxi_rproc_vq_work(), idr_for_each() iterates priv->rproc->notifyids
without holding priv->rproc->lock (unlike stm32_rproc_mb_vq_work() or
imx_dsp_rproc_vq_work()) and without RCU read-side protection (which cannot
be held across sunxi_rproc_vq_notify() because rproc_vq_interrupt() invokes
virtqueue callbacks that can sleep):
lib/idr.c:idr_for_each()
radix_tree_for_each_slot(slot, &idr->idr_rt, &iter, 0)
fn(id, rcu_dereference_raw(*slot), data)
While sunxi_rproc_vq_notify() is executing, a concurrent teardown runs:
rproc_resource_cleanup()
platform_device_unregister(rvdev->pdev)
rproc_virtio_remove()
rproc_free_vring()
idr_remove(&rproc->notifyids, rvring->notifyid)
If removing the vring frees the underlying struct radix_tree_node via
call_rcu(), the grace period can finish while sunxi_rproc_vq_notify() sleeps,
leaving radix_tree_next_slot() in idr_for_each() to dereference a freed slot
pointer.
In addition, although __rproc_virtio_del_vqs() clears rvring->vq and runs
synchronize_srcu(&rproc->vq_srcu) during rproc_stop_subdevices(), the rvring
entry stays in rproc->notifyids until platform_device_unregister(rvdev->pdev)
removes it and immediately frees the devm-allocated struct rproc_vdev
without another synchronize_srcu(&rproc->vq_srcu). If rproc_vq_interrupt()
looks up rvring in rproc->notifyids just before idr_remove(), it can
dereference rvring->vq after rvdev has been freed.
Should sunxi_rproc_vq_work() hold mutex_lock(&priv->rproc->lock) around
idr_for_each(), or should the driver synchronize and stop queuing vq_work
before rproc_resource_cleanup() tears down the vrings?
> +
> +/*
> + * Mailbox rx callbacks run in atomic context, but the virtio/rpmsg receive
> + * path takes mutexes, so the vring notifications are handled from a work item.
> + */
> +VISIBLE_IF_KUNIT
> +void sunxi_rproc_mb_rx_callback(struct mbox_client *cl, void *data)
> +{
> + struct sunxi_rproc *priv = container_of(cl, struct sunxi_rproc, cl);
> +
> + queue_work(priv->workqueue, &priv->vq_work);
> +}
> +EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_mb_rx_callback);
> +
> +VISIBLE_IF_KUNIT
> +irqreturn_t sunxi_rproc_crash_handler(int irq, void *data)
> +{
> + struct sunxi_rproc *priv = data;
> +
> + /*
> + * Stays masked until the next start(); recovery runs stop() first. A
> + * stop() that already disarmed it has disabled the line itself.
> + */
> + if (test_and_clear_bit(SUNXI_RPROC_CRASH_IRQ_ARMED, &priv->flags))
> + disable_irq_nosync(irq);
> +
> + rproc_report_crash(priv->rproc, RPROC_FATAL_ERROR);
> +
> + return IRQ_HANDLED;
> +}
> +EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_crash_handler);
[ ... ]
> +VISIBLE_IF_KUNIT
> +int sunxi_rproc_start(struct rproc *rproc)
> +{
> + struct sunxi_rproc *priv = rproc->priv;
> + const struct sunxi_rproc_cfg *cfg = priv->cfg;
> + int ret;
> +
> + dev_dbg(priv->dev, "Starting %s core at entry 0x%llx\n",
> + cfg->name, (u64)rproc->bootaddr);
> +
> + if (rproc->bootaddr > U32_MAX)
> + return -EINVAL;
> +
> + /* Program the reset vector and flush it before releasing the core */
> + sunxi_rproc_writel((u32)rproc->bootaddr, priv->cfg_va + cfg->boot_reg_offset);
> + sunxi_rproc_readl(priv->cfg_va + cfg->boot_reg_offset);
> +
> + ret = reset_control_deassert(priv->rst_core);
> + if (ret) {
> + dev_err(priv->dev, "failed to release core reset: %d\n", ret);
> + return ret;
> + }
> +
> + if (priv->crash_irq > 0 &&
> + !test_and_set_bit(SUNXI_RPROC_CRASH_IRQ_ARMED, &priv->flags))
> + enable_irq(priv->crash_irq);
> +
> + return 0;
> +}
> +EXPORT_SYMBOL_IF_KUNIT(sunxi_rproc_start);
> +
> +static int sunxi_rproc_stop(struct rproc *rproc)
> +{
> + struct sunxi_rproc *priv = rproc->priv;
> + const struct sunxi_rproc_cfg *cfg = priv->cfg;
> +
> + dev_dbg(priv->dev, "Halting %s core...\n", cfg->name);
> +
> + /* Assert reset so the core stops executing */
> + reset_control_assert(priv->rst_core);
> +
> + if (priv->crash_irq > 0 &&
> + test_and_clear_bit(SUNXI_RPROC_CRASH_IRQ_ARMED, &priv->flags))
> + disable_irq(priv->crash_irq);
> +
> + return 0;
> +}
[Severity: High]
As reported by Sashiko on v2 [2], in drivers/remoteproc/sunxi_rproc.c, what
happens if sunxi_rproc_stop() runs right after sunxi_rproc_crash_handler()
clears SUNXI_RPROC_CRASH_IRQ_ARMED, while the threaded handler is still
executing?
Because sunxi_rproc_crash_handler() is a preemptible IRQ thread, it can clear
SUNXI_RPROC_CRASH_IRQ_ARMED and be preempted before calling
disable_irq_nosync(irq) or rproc_report_crash(priv->rproc, RPROC_FATAL_ERROR):
CPU0 (IRQ thread) CPU1 (rproc_shutdown / recovery)
sunxi_rproc_crash_handler()
test_and_clear_bit(
SUNXI_RPROC_CRASH_IRQ_ARMED,
&priv->flags) -> true
sunxi_rproc_stop()
test_and_clear_bit(
SUNXI_RPROC_CRASH_IRQ_ARMED,
&priv->flags) -> false
/* returns without waiting */
If a subsequent rproc_boot() calls sunxi_rproc_start() before CPU0 reaches
disable_irq_nosync(irq), sunxi_rproc_start() sets SUNXI_RPROC_CRASH_IRQ_ARMED
and calls enable_irq(priv->crash_irq) while the IRQ disable depth is still 0,
triggering an unbalanced enable warning in __enable_irq(). When CPU0 then
runs disable_irq_nosync(irq), the IRQ is left disabled while
SUNXI_RPROC_CRASH_IRQ_ARMED is set, and the next sunxi_rproc_stop() will
increment the disable depth to 2, leaving the crash IRQ permanently masked on
later boots.
Furthermore, because sunxi_rproc_stop() does not call
synchronize_irq(priv->crash_irq) when test_and_clear_bit() returns false, the
in-flight sunxi_rproc_crash_handler() thread can call rproc_report_crash()
after the stop (and a subsequent restart) has already completed.
Should sunxi_rproc_stop() still call synchronize_irq(priv->crash_irq) when
priv->crash_irq > 0 and SUNXI_RPROC_CRASH_IRQ_ARMED was already cleared by
the crash handler?
[1] https://lore.kernel.org/r/20260927003338.CB0E21F000FF@smtp.kernel.org
[2] https://lore.kernel.org/r/20260927003338.CB0E21F000FF@smtp.kernel.org
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261101185735.762559-1-tcmichals@gmail.com?part=4
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-11-01 19:24 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-01 18:57 [PATCH v3 0/7] Add Allwinner A523 message box and XuanTie E906 remoteproc support Tim Michals
2026-11-01 18:57 ` [PATCH v3 1/7] dt-bindings: mailbox: Add Allwinner A523 message box Tim Michals
2026-11-01 19:06 ` sashiko-bot
2026-11-01 18:57 ` [PATCH v3 2/7] mailbox: sun55i: Add Allwinner A523 message box driver Tim Michals
2026-11-01 19:17 ` sashiko-bot
2026-11-01 18:57 ` [PATCH v3 3/7] dt-bindings: remoteproc: Add Allwinner A523 RISC-V remote processor Tim Michals
2026-11-01 18:57 ` [PATCH v3 4/7] remoteproc: sunxi: Add Allwinner A523 XuanTie E906 driver Tim Michals
2026-11-01 19:24 ` sashiko-bot
2026-11-01 18:57 ` [PATCH v3 5/7] arm64: dts: allwinner: a523: Add message box and remoteproc nodes Tim Michals
2026-11-01 18:57 ` [PATCH v3 6/7] mailbox: sun55i: Add KUnit tests Tim Michals
2026-11-01 18:57 ` [PATCH v3 7/7] remoteproc: sunxi: " Tim Michals
2026-11-01 19:13 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox