Devicetree
 help / color / mirror / Atom feed
From: Tim Michals <tcmichals@gmail.com>
To: Jassi Brar <jassisinghbrar@gmail.com>,
	Bjorn Andersson <andersson@kernel.org>,
	Mathieu Poirier <mathieu.poirier@linaro.org>,
	Rob Herring <robh@kernel.org>,
	Krzysztof Kozlowski <krzk+dt@kernel.org>,
	Conor Dooley <conor+dt@kernel.org>, Chen-Yu Tsai <wens@csie.org>,
	Jernej Skrabec <jernej.skrabec@gmail.com>,
	Samuel Holland <samuel@sholland.org>
Cc: linux-sunxi@lists.linux.dev, linux-remoteproc@vger.kernel.org,
	devicetree@vger.kernel.org, linux-arm-kernel@lists.infradead.org,
	linux-kernel@vger.kernel.org, Tim Michals <tcmichals@gmail.com>
Subject: [PATCH v3 2/7] mailbox: sun55i: Add Allwinner A523 message box driver
Date: Sun,  1 Nov 2026 12:57:30 -0600	[thread overview]
Message-ID: <20261101185735.762559-3-tcmichals@gmail.com> (raw)
In-Reply-To: <20261101185735.762559-1-tcmichals@gmail.com>

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


  parent reply	other threads:[~2026-11-01 18:57 UTC|newest]

Thread overview: 14+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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-02 10:17   ` Krzysztof Kozlowski
2026-11-01 18:57 ` Tim Michals [this message]
2026-11-01 19:17   ` [PATCH v3 2/7] mailbox: sun55i: Add Allwinner A523 message box driver 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-02 10:19   ` Krzysztof Kozlowski
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

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261101185735.762559-3-tcmichals@gmail.com \
    --to=tcmichals@gmail.com \
    --cc=andersson@kernel.org \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=jassisinghbrar@gmail.com \
    --cc=jernej.skrabec@gmail.com \
    --cc=krzk+dt@kernel.org \
    --cc=linux-arm-kernel@lists.infradead.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-remoteproc@vger.kernel.org \
    --cc=linux-sunxi@lists.linux.dev \
    --cc=mathieu.poirier@linaro.org \
    --cc=robh@kernel.org \
    --cc=samuel@sholland.org \
    --cc=wens@csie.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox