| // 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"); |