[PATCH v2 2/2] mailbox: add Axiado AX3005 mailbox driver
Swark Yang <[email protected]>
| Newsgroups | org.ozlabs.lists.openbmc,org.infradead.lists.linux-arm-kernel,org.kernel.vger.linux-devicetree,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <20260817-upstream-axiado-ax3005-mailbox-upstream-v2-2-25e713eccff3@axiado.com> |
Add a mailbox controller driver for the Axiado AX3005 SoC. The controller provides communication channels between the host CPU and the coprocessor. The hardware provides 8 TX channels and 8 RX channels through separate register regions. RX channels use per-channel interrupts, while TX completion is detected by polling the FIFO status. Add the driver path to the existing Axiado mailbox entry in MAINTAINERS. Signed-off-by: Swark Yang <[email protected]> --- MAINTAINERS | 1 + drivers/mailbox/Kconfig | 10 + drivers/mailbox/Makefile | 2 + drivers/mailbox/axiado-mailbox.c | 395 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 408 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index af6c3571c9f2..80db69111889 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4443,6 +4443,7 @@ M: Prasad Bolisetty <[email protected]> M: Swark Yang <[email protected]> S: Supported F: Documentation/devicetree/bindings/mailbox/axiado,ax3005-mailbox.yaml +F: drivers/mailbox/axiado-mailbox.c AXI SPI ENGINE M: Michael Hennerich <[email protected]> diff --git a/drivers/mailbox/Kconfig b/drivers/mailbox/Kconfig index 3062ee352f78..ab869769e278 100644 --- a/drivers/mailbox/Kconfig +++ b/drivers/mailbox/Kconfig @@ -399,4 +399,14 @@ config RISCV_SBI_MPXY_MBOX or HS-mode hypervisor). Say Y here, unless you are sure you do not need this. +config AXIADO_MAILBOX + tristate "Axiado mailbox driver" + depends on ARCH_AXIADO || COMPILE_TEST + depends on OF + help + Enable support for the Axiado mailbox controller. The driver provides + communication channels between the host CPU and the coprocessor on + Axiado SoCs. + If unsure, say N. + endif diff --git a/drivers/mailbox/Makefile b/drivers/mailbox/Makefile index 944d8ea39f34..45dc59bb3e7f 100644 --- a/drivers/mailbox/Makefile +++ b/drivers/mailbox/Makefile @@ -84,3 +84,5 @@ obj-$(CONFIG_CIX_MBOX) += cix-mailbox.o obj-$(CONFIG_BCM74110_MAILBOX) += bcm74110-mailbox.o obj-$(CONFIG_RISCV_SBI_MPXY_MBOX) += riscv-sbi-mpxy-mbox.o + +obj-$(CONFIG_AXIADO_MAILBOX) += axiado-mailbox.o diff --git a/drivers/mailbox/axiado-mailbox.c b/drivers/mailbox/axiado-mailbox.c new file mode 100644 index 000000000000..3a3cf7421863 --- /dev/null +++ b/drivers/mailbox/axiado-mailbox.c @@ -0,0 +1,395 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (c) 2021-2026 Axiado Corporation (or its affiliates). + */ + +#include <linux/atomic.h> +#include <linux/interrupt.h> +#include <linux/io.h> +#include <linux/bitfield.h> +#include <linux/debugfs.h> +#include <linux/iopoll.h> +#include <linux/mailbox_controller.h> +#include <linux/math.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/property.h> +#include <linux/slab.h> +#include <linux/unaligned.h> + +#define AXIADO_MBOX_TX_CHANS 8 /* 0-7 */ +#define AXIADO_MBOX_RX_CHANS 8 /* 8-15 */ +#define AXIADO_MBOX_CHAN_STRIDE 0x4 +#define AXIADO_MBOX_TX_REG_STRIDE 0x40 +#define AXIADO_MBOX_RX_REG_STRIDE 0x30 +#define AXIADO_MBOX_RX_POLL_US 10 +#define AXIADO_MBOX_RX_TIMEOUT_US 1000 + +/* Mailbox CSR bit definitions */ +#define AXIADO_MBOX_CSR_EMPTY BIT(0) /* 1 = FIFO empty; 0 = data available */ +#define AXIADO_MBOX_CSR_OVERFLOW BIT(2) /* write 1 to clear (W1C) */ +#define AXIADO_MBOX_CSR_UNDERFLOW BIT(3) /* write 1 to clear (W1C) */ +#define AXIADO_MBOX_CSR_FLUSH BIT(4) /* W1TRG flush; startup/shutdown */ +#define AXIADO_MBOX_CSR_LEVEL GENMASK(11, 5) + +#define AXIADO_MBOX_CSR_ERRORS (AXIADO_MBOX_CSR_OVERFLOW | \ + AXIADO_MBOX_CSR_UNDERFLOW) + +struct axiado_mbox_data { + u8 num_chans; + u16 msg_size; +}; + +struct axiado_channel_data { + void __iomem *mbox_reg; + void __iomem *csr_reg; + void *rx_buffer; + u8 channel_num; + int irq; + struct mbox_chan *chan; + bool active; + atomic_t fifo_errors; +}; + +/* mailbox side */ +struct axiado_mbox { + struct mbox_controller mbox; + const struct axiado_mbox_data *drv_data; + void __iomem *tx_base; + void __iomem *rx_base; + struct dentry *debugfs_dir; +}; + +/* + * Checks OVERFLOW/UNDERFLOW, logs and W1C-clears them if set. Shared by the + * TX and RX paths; RX additionally flushes the FIFO afterwards to resync + * framing, which TX must not do since it could discard data the peer has + * not read yet. + */ +static bool axiado_mbox_clear_fifo_errors(struct axiado_channel_data *priv) +{ + struct device *dev = priv->chan->mbox->dev; + u32 errors; + + errors = readl(priv->csr_reg) & AXIADO_MBOX_CSR_ERRORS; + if (!errors) + return false; + + writel(errors, priv->csr_reg); + atomic_inc(&priv->fifo_errors); + + dev_warn_ratelimited(dev, "Channel %u FIFO error: %#x\n", + priv->channel_num, errors); + + return true; +} + +static int axiado_mbox_send_data(struct mbox_chan *chan, void *data) +{ + struct axiado_mbox *mb = dev_get_drvdata(chan->mbox->dev); + struct axiado_channel_data *priv = chan->con_priv; + unsigned int idx = priv->channel_num; + u8 tail[sizeof(u32)] = { 0 }; + const u8 *buf = data; + unsigned int tail_len; + unsigned int offset; + u32 msg_len; + + if (!data) + return -EINVAL; + + /* + * The Axiado mailbox message ABI stores the total message length, + * including this length word, as a little-endian byte count in the + * first word of every message. + */ + msg_len = get_unaligned_le32(data); + if (msg_len < sizeof(u32) || msg_len > mb->drv_data->msg_size) + return -EINVAL; + + /* Only touch the hardware after validating the message. */ + axiado_mbox_clear_fifo_errors(priv); + + if (!(readl(priv->csr_reg) & AXIADO_MBOX_CSR_EMPTY)) { + dev_warn_ratelimited(mb->mbox.dev, "Channel %u is busy\n", idx); + return -EBUSY; + } + + for (offset = 0; offset + sizeof(u32) <= msg_len; + offset += sizeof(u32)) + writel(get_unaligned_le32(buf + offset), priv->mbox_reg); + + tail_len = msg_len - offset; + if (tail_len) { + memcpy(tail, buf + offset, tail_len); + writel(get_unaligned_le32(tail), priv->mbox_reg); + } + + dev_dbg(mb->mbox.dev, "%s: Ch-%u sent\n", __func__, idx); + + return 0; +} + +static irqreturn_t axiado_rx_thread(int irq, void *dev_id) +{ + struct axiado_channel_data *priv = dev_id; + struct mbox_chan *chan = priv->chan; + struct axiado_mbox *mb = dev_get_drvdata(chan->mbox->dev); + u8 *buf = priv->rx_buffer; + unsigned int num_words; + unsigned int remaining; + unsigned int i; + u32 msg_len; + u32 word; + u32 csr; + int ret; + + /* + * Clear and log/count any pending errors, but don't discard data + * on their account alone: a rejected overflow write doesn't + * corrupt what was already safely queued ahead of it, so let the + * length-based read below decide whether what's here is usable. + */ + axiado_mbox_clear_fifo_errors(priv); + + csr = readl(priv->csr_reg); + if (csr & AXIADO_MBOX_CSR_EMPTY) + return IRQ_NONE; + + /* + * The first word contains the total message length in bytes, + * including the length word itself. + */ + word = readl(priv->mbox_reg); + msg_len = word; + put_unaligned_le32(word, buf); + + if (msg_len < sizeof(u32) || msg_len > mb->drv_data->msg_size) + goto invalid_message; + + num_words = DIV_ROUND_UP(msg_len, sizeof(u32)); + remaining = num_words - 1; + + /* + * The not-empty interrupt may occur as soon as the first DW enters + * the FIFO. Wait until all remaining DWs of this message arrive. + */ + if (remaining) { + ret = readl_poll_timeout(priv->csr_reg, csr, + (csr & AXIADO_MBOX_CSR_ERRORS) || + FIELD_GET(AXIADO_MBOX_CSR_LEVEL, csr) >= + remaining, + AXIADO_MBOX_RX_POLL_US, + AXIADO_MBOX_RX_TIMEOUT_US); + if (ret) + goto incomplete_message; + + axiado_mbox_clear_fifo_errors(priv); + } + + for (i = 1; i < num_words; i++) { + word = readl(priv->mbox_reg); + put_unaligned_le32(word, buf + i * sizeof(u32)); + } + + axiado_mbox_clear_fifo_errors(priv); + + if (READ_ONCE(priv->active)) + mbox_chan_received_data(chan, priv->rx_buffer); + + return IRQ_HANDLED; + +incomplete_message: + dev_warn_ratelimited(chan->mbox->dev, + "Channel %u received an incomplete message\n", + priv->channel_num); + +invalid_message: + writel(AXIADO_MBOX_CSR_FLUSH, priv->csr_reg); + + return IRQ_HANDLED; +} + +static int axiado_mbox_startup(struct mbox_chan *chan) +{ + struct axiado_channel_data *priv = chan->con_priv; + + /* + * Only flush on a genuine error. A blind flush here would discard + * a message the peer legitimately sent before this side started + * up (e.g. during normal boot sequencing), which is not corrupt + * and does not need resyncing. + */ + if (axiado_mbox_clear_fifo_errors(priv)) + writel(AXIADO_MBOX_CSR_FLUSH, priv->csr_reg); + + WRITE_ONCE(priv->active, true); + + if (priv->channel_num >= AXIADO_MBOX_TX_CHANS) + enable_irq(priv->irq); + + return 0; +} + +static void axiado_mbox_shutdown(struct mbox_chan *chan) +{ + struct axiado_channel_data *priv = chan->con_priv; + + WRITE_ONCE(priv->active, false); + + if (priv->channel_num >= AXIADO_MBOX_TX_CHANS) + disable_irq(priv->irq); + + if (axiado_mbox_clear_fifo_errors(priv)) + writel(AXIADO_MBOX_CSR_FLUSH, priv->csr_reg); +} + +static bool axiado_mbox_last_tx_done(struct mbox_chan *chan) +{ + struct axiado_channel_data *priv = chan->con_priv; + + return !!(readl(priv->csr_reg) & AXIADO_MBOX_CSR_EMPTY); +} + +static const struct mbox_chan_ops axiado_mbox_chan_ops = { + .send_data = axiado_mbox_send_data, + .startup = axiado_mbox_startup, + .shutdown = axiado_mbox_shutdown, + .last_tx_done = axiado_mbox_last_tx_done, +}; + +static void axiado_mbox_debugfs_remove(void *dentry) +{ + debugfs_remove_recursive(dentry); +} + +static int axiado_mbox_probe(struct platform_device *pdev) +{ + const struct axiado_mbox_data *drv_data; + struct axiado_channel_data *ch_data; + struct device *dev = &pdev->dev; + struct axiado_mbox *mb; + unsigned int irq_idx = 0; + unsigned int rx_chan; + unsigned int i; + int ret; + + drv_data = device_get_match_data(dev); + if (!drv_data) + return -ENODEV; + + mb = devm_kzalloc(dev, sizeof(*mb), GFP_KERNEL); + if (!mb) + return -ENOMEM; + + mb->mbox.dev = dev; + mb->mbox.num_chans = drv_data->num_chans; + + mb->mbox.chans = devm_kcalloc(&pdev->dev, + drv_data->num_chans, + sizeof(*mb->mbox.chans), + GFP_KERNEL); + if (!mb->mbox.chans) + return -ENOMEM; + + mb->tx_base = devm_platform_ioremap_resource_byname(pdev, "tx"); + if (IS_ERR(mb->tx_base)) + return PTR_ERR(mb->tx_base); + + mb->rx_base = devm_platform_ioremap_resource_byname(pdev, "rx"); + if (IS_ERR(mb->rx_base)) + return PTR_ERR(mb->rx_base); + + ch_data = devm_kcalloc(&pdev->dev, + drv_data->num_chans, + sizeof(*ch_data), + GFP_KERNEL); + if (!ch_data) + return -ENOMEM; + + mb->debugfs_dir = debugfs_create_dir(dev_name(dev), NULL); + ret = devm_add_action_or_reset(dev, axiado_mbox_debugfs_remove, + mb->debugfs_dir); + if (ret) + return ret; + + for (i = 0; i < drv_data->num_chans; i++) { + char name[16]; + + ch_data[i].channel_num = i; + ch_data[i].chan = &mb->mbox.chans[i]; + + mb->mbox.chans[i].con_priv = &ch_data[i]; + + snprintf(name, sizeof(name), "chan%u-errors", i); + debugfs_create_atomic_t(name, 0444, mb->debugfs_dir, + &ch_data[i].fifo_errors); + + if (i < AXIADO_MBOX_TX_CHANS) { + ch_data[i].mbox_reg = mb->tx_base + (i * AXIADO_MBOX_CHAN_STRIDE); + ch_data[i].csr_reg = mb->tx_base + AXIADO_MBOX_TX_REG_STRIDE + + (i * AXIADO_MBOX_CHAN_STRIDE); + ch_data[i].irq = -1; + continue; + } + + ch_data[i].rx_buffer = devm_kzalloc(dev, drv_data->msg_size, + GFP_KERNEL); + if (!ch_data[i].rx_buffer) + return -ENOMEM; + + rx_chan = i - AXIADO_MBOX_TX_CHANS; + ch_data[i].mbox_reg = mb->rx_base + + (rx_chan * AXIADO_MBOX_CHAN_STRIDE); + ch_data[i].csr_reg = mb->rx_base + AXIADO_MBOX_RX_REG_STRIDE + + (rx_chan * AXIADO_MBOX_CHAN_STRIDE); + ch_data[i].irq = platform_get_irq(pdev, irq_idx++); + if (ch_data[i].irq < 0) + return dev_err_probe(dev, ch_data[i].irq, + "Failed to get IRQ for channel %u\n", i); + + ret = devm_request_threaded_irq(dev, ch_data[i].irq, NULL, + axiado_rx_thread, + IRQF_ONESHOT | IRQF_NO_AUTOEN, + dev_name(dev), &ch_data[i]); + if (ret) + return dev_err_probe(dev, ret, + "Failed to request IRQ for channel %u\n", i); + + dev_dbg(dev, "RX chan %u -> irq %d\n", i, ch_data[i].irq); + } + + platform_set_drvdata(pdev, mb); + mb->drv_data = drv_data; + mb->mbox.ops = &axiado_mbox_chan_ops; + mb->mbox.txdone_irq = false; + mb->mbox.txdone_poll = true; + mb->mbox.txpoll_period = 5; + + return devm_mbox_controller_register(dev, &mb->mbox); +} + +static const struct axiado_mbox_data axiado_drv_data = { + .num_chans = AXIADO_MBOX_TX_CHANS + AXIADO_MBOX_RX_CHANS, + .msg_size = 256, +}; + +static const struct of_device_id axiado_mbox_of_match[] = { + { .compatible = "axiado,ax3005-mailbox", .data = &axiado_drv_data }, + { } +}; +MODULE_DEVICE_TABLE(of, axiado_mbox_of_match); + +static struct platform_driver axiado_mbox_driver = { + .driver = { + .name = "axiado-mailbox", + .of_match_table = axiado_mbox_of_match, + }, + .probe = axiado_mbox_probe, +}; +module_platform_driver(axiado_mbox_driver); + +MODULE_AUTHOR("Axiado Corporation"); +MODULE_DESCRIPTION("Axiado Mailbox driver"); +MODULE_LICENSE("GPL"); -- 2.34.1