* [PATCH net-next 2/2] net: mctp: Add Axiado AX3005 IRoT MCTP transport driver
2026-11-01 14:00 [PATCH net-next 0/2] net: mctp: Add MCTP transport for the Axiado AX3005 IRoT Akhila Kavi
2026-11-01 14:00 ` [PATCH net-next 1/2] dt-bindings: net: Add Axiado AX3005 IRoT MCTP transport Akhila Kavi
@ 2026-11-01 14:00 ` Akhila Kavi
2026-11-02 2:55 ` [PATCH net-next 0/2] net: mctp: Add MCTP transport for the Axiado AX3005 IRoT Jeremy Kerr
2 siblings, 0 replies; 5+ messages in thread
From: Akhila Kavi @ 2026-11-01 14:00 UTC (permalink / raw)
To: Sammai Chakravarthy, Prasad Bolisetty, Andrew Lunn,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Rob Herring, Krzysztof Kozlowski, Conor Dooley, Tzu-Hao Wei,
Karthikeyan Mitran, Jeremy Kerr, Matt Johnston
Cc: netdev, devicetree, linux-arm-kernel, linux-kernel, openbmc, Akhila Kavi
Add an MCTP transport driver for the root-of-trust (IRoT) co-processor
on Axiado AX3005 SoCs. It registers an ARPHRD_MCTP point-to-point
netdev, mctpirot%d, with no physical addressing, so SPDM, PLDM and other
MCTP traffic to the IRoT runs over ordinary AF_MCTP sockets.
Packets are carried in shared memory. The mailbox only carries 48-byte
frames: the 16-byte header the mailbox controller and the IRoT firmware
expect, followed by a 32-byte IRoT message. The protocol is:
- At probe, the driver sends cc_get_mctp_buffers. The IRoT answers
with cc_mctp_buffer, which gives an MTU limit and two windows: one
the host reads from and one it writes to. The driver checks that
both windows lie inside the memory-region carve-out, do not
overlap, and hold at least the 68-byte DSP0236 baseline MTU. It
clamps the MTU to the window size.
- Each direction has one packet in flight. The sender writes the
packet into its window and rings the doorbell with cc_mctp, which
carries a counter and the packet's span. The receiver copies the
packet out and answers with cc_mctp_done for that counter. Only
then may the sender reuse its window.
- On RX, the packet is ACKed only after it has been handed to the
network stack. A resend of the last delivered counter is ACKed
again without being delivered twice.
- On TX, the queue stays stopped until the ACK arrives. If no ACK
arrives within 5 seconds, the carrier goes down and the window
stays claimed, because the IRoT may still be reading it. A late ACK
brings the link back up. There is no resync, so if the IRoT resets
without answering, the driver must be rebound.
- cc_ping is answered with cc_ping_rsp.
The mailbox RX callback runs in atomic context. It only records state
and defers all replies and netdev access to an ordered workqueue.
Signed-off-by: Akhila Kavi <akavi@axiado.com>
---
MAINTAINERS | 1 +
drivers/net/mctp/Kconfig | 17 +
drivers/net/mctp/Makefile | 1 +
drivers/net/mctp/axiado-mctp-irot.c | 1089 +++++++++++++++++++++++++++++++++++
4 files changed, 1108 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index b16248974044..db7ee24b2962 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4484,6 +4484,7 @@ M: Prasad Bolisetty <pbolisetty@axiado.com>
L: netdev@vger.kernel.org
S: Supported
F: Documentation/devicetree/bindings/net/axiado,ax3005-mctp-irot.yaml
+F: drivers/net/mctp/axiado-mctp-irot.c
AXI SPI ENGINE
M: Michael Hennerich <michael.hennerich@analog.com>
diff --git a/drivers/net/mctp/Kconfig b/drivers/net/mctp/Kconfig
index c40ac9c665b7..f0fa7f1ecbe1 100644
--- a/drivers/net/mctp/Kconfig
+++ b/drivers/net/mctp/Kconfig
@@ -3,6 +3,23 @@ if MCTP
menu "MCTP Device Drivers"
+config MCTP_TRANSPORT_AXIADO_IROT
+ tristate "MCTP transport over the Axiado AX3005 IRoT mailbox"
+ depends on MAILBOX && OF && OF_RESERVED_MEM
+ depends on ARCH_AXIADO || COMPILE_TEST
+ help
+ MCTP transport for the IRoT co-processor on Axiado AX3005 SoCs.
+ Registers an MCTP network interface so SPDM, PLDM and NSM run as
+ normal AF_MCTP socket traffic.
+
+ Packets flow over two shared-memory windows; the mailbox only
+ carries small doorbell and acknowledgment messages.
+
+ To compile this driver as a module, choose M here: the module will
+ be called axiado-mctp-irot.
+
+ If unsure, say N.
+
config MCTP_SERIAL
tristate "MCTP serial transport"
depends on TTY
diff --git a/drivers/net/mctp/Makefile b/drivers/net/mctp/Makefile
index c870b62d3f1c..fe735012409d 100644
--- a/drivers/net/mctp/Makefile
+++ b/drivers/net/mctp/Makefile
@@ -1,3 +1,4 @@
+obj-$(CONFIG_MCTP_TRANSPORT_AXIADO_IROT) += axiado-mctp-irot.o
obj-$(CONFIG_MCTP_SERIAL) += mctp-serial.o
obj-$(CONFIG_MCTP_TRANSPORT_I2C) += mctp-i2c.o
obj-$(CONFIG_MCTP_TRANSPORT_I3C) += mctp-i3c.o
diff --git a/drivers/net/mctp/axiado-mctp-irot.c b/drivers/net/mctp/axiado-mctp-irot.c
new file mode 100644
index 000000000000..d393a8102f2d
--- /dev/null
+++ b/drivers/net/mctp/axiado-mctp-irot.c
@@ -0,0 +1,1089 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Axiado AX3005 IRoT MCTP transport driver
+ *
+ * Copyright (c) 2026 Axiado Corporation.
+ */
+
+#include <linux/bottom_half.h>
+#include <linux/build_bug.h>
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/if_arp.h>
+#include <linux/if_ether.h>
+#include <linux/io.h>
+#include <linux/ioport.h>
+#include <linux/jiffies.h>
+#include <linux/mailbox_client.h>
+#include <linux/minmax.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/netdevice.h>
+#include <linux/of_reserved_mem.h>
+#include <linux/platform_device.h>
+#include <linux/skbuff.h>
+#include <linux/spinlock.h>
+#include <linux/string.h>
+#include <linux/types.h>
+#include <linux/wordpart.h>
+#include <linux/workqueue.h>
+#include <net/mctp.h>
+#include <net/mctpdevice.h>
+
+/*
+ * Command codes. Even == request, odd == response. Zero is reserved and is
+ * never sent on the wire.
+ */
+enum irot_command_code {
+ IROT_CC_PING = 2,
+ IROT_CC_PING_RSP = 3,
+ IROT_CC_GET_MCTP_BUFFERS = 4,
+ IROT_CC_MCTP_BUFFER = 5,
+ IROT_CC_MCTP = 6,
+ IROT_CC_MCTP_DONE = 7,
+};
+
+/* 64-bit physical address split into two words to avoid padding. */
+struct irot_msg_address {
+ __le32 low;
+ __le32 high;
+};
+
+struct irot_msg_span {
+ struct irot_msg_address address;
+ __le32 size;
+};
+
+struct irot_msg_data_buffers {
+ __le32 mtu_limit;
+ struct irot_msg_span host_read; /* host reads RX packets here */
+ struct irot_msg_span host_write; /* host writes TX packets here */
+};
+
+struct irot_msg_data_mctp {
+ __le32 counter; /* echoed back in the ACK */
+ struct irot_msg_span packet; /* must lie inside its window */
+};
+
+union irot_msg_data {
+ __le32 value; /* ping echo / mctp_done counter */
+ struct irot_msg_data_buffers buffers;
+ struct irot_msg_data_mctp mctp;
+};
+
+struct irot_msg {
+ __le32 command;
+ union irot_msg_data data;
+};
+
+static_assert(sizeof(struct irot_msg) == 32);
+
+/*
+ * One mailbox FIFO frame. @len is the Axiado mailbox controller's length
+ * word: the total frame size in bytes, including itself. The controller
+ * interprets nothing else. @type, @priority and @flag are defined by the
+ * IRoT firmware: the host sends them as zero and ignores them on receive.
+ * The 32-byte IRoT message follows.
+ */
+struct irot_mbox_frame {
+ __le32 len;
+ __le32 type;
+ __le32 priority;
+ __le32 flag;
+ struct irot_msg irot;
+};
+
+static_assert(sizeof(struct irot_mbox_frame) == 16 + sizeof(struct irot_msg),
+ "irot_mbox_frame must be header + one IPC message");
+static_assert(sizeof(struct irot_mbox_frame) == 48,
+ "irot_mbox_frame must be exactly 48 bytes");
+
+#define IROT_MBOX_TX_TOUT_MS 250
+#define IROT_TX_QUEUE_LEN 20
+#define IROT_TX_ACK_TIMEOUT_MS 5000
+#define IROT_BUFFER_RETRIES 3
+#define IROT_BUFFER_TIMEOUT_S 5
+/* DMTF DSP0236 section 8.3: baseline transmission unit */
+#define IROT_MCTP_MIN_MTU 68
+/* Caps the MTU and the size of one RX allocation */
+#define IROT_MCTP_MAX_MTU 2097152
+
+/* A shared-memory window in host byte order. */
+struct irot_region {
+ u64 base;
+ u32 size;
+};
+
+/* One inbound packet notification. */
+struct irot_rx_entry {
+ u32 counter;
+ u64 base;
+ u32 size;
+};
+
+enum irot_rx_result {
+ IROT_RX_DELIVERED, /* handed to the stack: ACK */
+ IROT_RX_INVALID, /* can never be delivered: ACK and drop */
+ IROT_RX_RETRY, /* transient failure: no ACK, peer resends */
+};
+
+/*
+ * All driver state for one IRoT MCTP endpoint; the net_device private area.
+ *
+ * Flow control: each window holds one packet, and each side waits for
+ * cc_mctp_done before it reuses its window. So at most one packet is in
+ * flight per direction, and the RX side needs a single slot. A peer that does
+ * not see its cc_mctp_done in time resends the same counter.
+ *
+ * On TX the netdev queue is the only queue: xmit hands one skb to tx_work and
+ * stops the queue, and the queue is woken once the peer has ACKed it.
+ *
+ * The mailbox callback runs in atomic context; it only records state and
+ * queues work. All sends, and all netdev access, happen on @wq.
+ */
+struct irot_priv {
+ struct device *dev;
+ struct net_device *ndev;
+ struct workqueue_struct *wq;
+
+ struct mbox_client cl;
+ struct mbox_chan *chan_tx;
+ struct mbox_chan *chan_rx;
+ struct mutex tx_chan_lock; /* serialises chan_tx senders */
+ struct irot_mbox_frame tx_frame; /* owned by tx_chan_lock */
+
+ /* shared memory, written once before the netdev is registered */
+ struct irot_region host_write; /* AP writes, IRoT reads */
+ struct irot_region host_read; /* IRoT writes, AP reads */
+ u32 mtu; /* largest packet either way */
+ bool buffers_valid; /* rx_lock */
+ struct completion buffers_done;
+ struct resource pool_res; /* memory-region carve-out */
+ void *pool;
+ void *tx_addr;
+ void *rx_addr;
+
+ struct work_struct tx_work; /* send tx_skb, or wake the queue */
+ struct delayed_work tx_timeout_work; /* cc_mctp_done overdue */
+ u32 next_tx_counter; /* tx_work only */
+
+ spinlock_t tx_state_lock; /* the TX fields below */
+ struct sk_buff *tx_skb; /* handed from xmit to tx_work */
+ bool pending_write; /* window owned by a transfer */
+ bool tx_stalled; /* ACK overdue, link down */
+ u32 pending_tx_counter;
+
+ /* RX path; everything below is protected by rx_lock */
+ struct work_struct rx_work;
+ spinlock_t rx_lock; /* the RX fields below */
+
+ struct irot_rx_entry rx_entry;
+ bool rx_busy; /* rx_entry not yet answered */
+ bool rx_new; /* rx_entry not yet picked up */
+
+ bool dup_ack_pending;
+ u32 dup_ack_counter;
+
+ bool ping_pending;
+ u32 ping_value;
+
+ /* Last counter handed to the stack */
+ bool has_last_rx_counter;
+ u32 last_rx_counter;
+
+ /* Drops seen by the callback, folded into the stats by rx_work */
+ unsigned int rx_dropped;
+};
+
+/*
+ * Statistics are updated from the work items and from xmit. Every update
+ * from process context disables BHs, so the per-CPU update cannot be
+ * interrupted by xmit running in softirq on the same CPU.
+ */
+static void irot_tx_drop(struct irot_priv *p, struct sk_buff *skb)
+{
+ local_bh_disable();
+ dev_dstats_tx_dropped(p->ndev);
+ local_bh_enable();
+ dev_kfree_skb_any(skb);
+}
+
+static void irot_region_from_span(struct irot_region *r,
+ const struct irot_msg_span *s)
+{
+ r->base = ((u64)le32_to_cpu(s->address.high) << 32) |
+ le32_to_cpu(s->address.low);
+ r->size = le32_to_cpu(s->size);
+}
+
+static void irot_span_from_region(struct irot_msg_span *s,
+ const struct irot_region *r)
+{
+ s->address.low = cpu_to_le32(lower_32_bits(r->base));
+ s->address.high = cpu_to_le32(upper_32_bits(r->base));
+ s->size = cpu_to_le32(r->size);
+}
+
+/*
+ * True only if [base, base + size) lies wholly inside @win, with no wrap.
+ * Returns the packet's offset into the window in @off.
+ */
+static bool irot_region_offset(const struct irot_region *win, u64 base,
+ u32 size, u64 *off)
+{
+ if (base < win->base)
+ return false;
+
+ *off = base - win->base;
+
+ if (*off >= win->size)
+ return false;
+
+ return size <= win->size - *off;
+}
+
+/*
+ * Transmit one IPC message. tx_block is set, so the frame lives in driver
+ * state, owned by tx_chan_lock. The mailbox core keeps one tx-done
+ * completion per channel, so blocking senders must not overlap; the lock
+ * serialises probe against the work items.
+ * Sleeps: never call this from the RX callback.
+ */
+static int irot_send(struct irot_priv *p, const struct irot_msg *msg)
+{
+ guard(mutex)(&p->tx_chan_lock);
+
+ memset(&p->tx_frame, 0, sizeof(p->tx_frame));
+ p->tx_frame.len = cpu_to_le32(sizeof(p->tx_frame));
+ memcpy(&p->tx_frame.irot, msg, sizeof(*msg));
+
+ return mbox_send_message(p->chan_tx, &p->tx_frame);
+}
+
+static void irot_send_done(struct irot_priv *p, u32 counter)
+{
+ struct irot_msg ack = {};
+ int rc;
+
+ ack.command = cpu_to_le32(IROT_CC_MCTP_DONE);
+ ack.data.value = cpu_to_le32(counter);
+
+ rc = irot_send(p, &ack);
+ if (rc < 0)
+ dev_warn_ratelimited(p->dev, "Failed to ACK RX counter %u: %d\n",
+ counter, rc);
+}
+
+/*
+ * Release the window for @counter and let tx_work wake the queue. Called from
+ * the mailbox callback on cc_mctp_done, and from tx_work when a send fails.
+ */
+static void irot_tx_mark_done(struct irot_priv *p, u32 counter)
+{
+ scoped_guard(spinlock_irqsave, &p->tx_state_lock) {
+ if (!p->pending_write || p->pending_tx_counter != counter)
+ return;
+
+ p->pending_write = false;
+ }
+
+ cancel_delayed_work(&p->tx_timeout_work);
+ queue_work(p->wq, &p->tx_work);
+}
+
+static int irot_tx_mark_pending(struct irot_priv *p, u32 counter)
+{
+ guard(spinlock_irqsave)(&p->tx_state_lock);
+
+ if (p->pending_write)
+ return -EBUSY;
+
+ p->pending_tx_counter = counter;
+ p->pending_write = true;
+
+ return 0;
+}
+
+static bool irot_tx_ready(struct irot_priv *p)
+{
+ guard(spinlock_irqsave)(&p->tx_state_lock);
+
+ return !p->pending_write && !p->tx_stalled && !p->tx_skb;
+}
+
+/*
+ * The peer owes us an ACK and has not sent it: take the link down. The queue
+ * is already stopped, and the window stays claimed, because the peer may still
+ * be reading it; only a late cc_mctp_done releases it.
+ *
+ * There is no resync: if the IRoT resets and never answers the outstanding
+ * counter, the link stays down until the driver is rebound.
+ */
+static void irot_tx_stall(struct irot_priv *p, const char *why)
+{
+ u32 counter;
+
+ scoped_guard(spinlock_irqsave, &p->tx_state_lock) {
+ if (!p->pending_write || p->tx_stalled)
+ return;
+ p->tx_stalled = true;
+ counter = p->pending_tx_counter;
+ }
+
+ netif_carrier_off(p->ndev);
+
+ dev_warn(p->dev, "TX counter %u: %s, taking link down\n", counter, why);
+}
+
+/* The window is free again: end a stall, and let the stack send. */
+static void irot_tx_wake(struct irot_priv *p)
+{
+ bool recovered = false;
+
+ scoped_guard(spinlock_irqsave, &p->tx_state_lock) {
+ if (p->pending_write || p->tx_skb)
+ return;
+
+ if (p->tx_stalled) {
+ p->tx_stalled = false;
+ recovered = true;
+ }
+
+ /*
+ * Wake under the lock: xmit stops the queue and hands over its
+ * skb under it too, so the wake cannot land in between.
+ */
+ if (netif_running(p->ndev))
+ netif_wake_queue(p->ndev);
+ }
+
+ if (recovered) {
+ dev_dbg(p->dev, "late TX ACK arrived, link up\n");
+ netif_carrier_on(p->ndev);
+ }
+}
+
+/* cc_mctp_buffer: the peer's windows. Write-once; zero mtu means no answer. */
+static void irot_cb_buffer_rsp(struct irot_priv *p, const struct irot_msg *m)
+{
+ u32 mtu = le32_to_cpu(m->data.buffers.mtu_limit);
+
+ scoped_guard(spinlock_irqsave, &p->rx_lock) {
+ if (!p->buffers_valid && mtu) {
+ irot_region_from_span(&p->host_write,
+ &m->data.buffers.host_write);
+ irot_region_from_span(&p->host_read,
+ &m->data.buffers.host_read);
+ p->mtu = mtu;
+ p->buffers_valid = true;
+ }
+ }
+
+ complete(&p->buffers_done);
+}
+
+/*
+ * cc_mctp: an inbound packet is sitting in host_read. Never ACKs from here;
+ * that is rx_work's job once the packet has been handed to the stack.
+ */
+static void irot_cb_mctp(struct irot_priv *p, const struct irot_msg *m)
+{
+ struct irot_rx_entry e;
+ bool wake = false;
+
+ e.counter = le32_to_cpu(m->data.mctp.counter);
+ e.size = le32_to_cpu(m->data.mctp.packet.size);
+ e.base = ((u64)le32_to_cpu(m->data.mctp.packet.address.high) << 32) |
+ le32_to_cpu(m->data.mctp.packet.address.low);
+
+ scoped_guard(spinlock_irqsave, &p->rx_lock) {
+ if (p->has_last_rx_counter && e.counter == p->last_rx_counter) {
+ /* Resend of a delivered packet: our ACK was missed */
+ p->dup_ack_counter = e.counter;
+ p->dup_ack_pending = true;
+ wake = true;
+ } else if (p->rx_busy) {
+ /*
+ * A resend of the packet in rx_entry gets its ACK once
+ * that is delivered. Anything else breaks the one
+ * packet in flight rule; drop it unanswered and the
+ * peer resends.
+ */
+ if (e.counter != p->rx_entry.counter) {
+ p->rx_dropped++;
+ wake = true;
+ }
+ } else {
+ p->rx_entry = e;
+ p->rx_busy = true;
+ p->rx_new = true;
+ wake = true;
+ }
+ }
+
+ if (wake)
+ queue_work(p->wq, &p->rx_work);
+}
+
+static void irot_cb_ping(struct irot_priv *p, const struct irot_msg *m)
+{
+ /* Liveness only: a newer ping supersedes an unanswered one */
+ scoped_guard(spinlock_irqsave, &p->rx_lock) {
+ p->ping_pending = true;
+ p->ping_value = le32_to_cpu(m->data.value);
+ }
+
+ queue_work(p->wq, &p->rx_work);
+}
+
+/*
+ * Runs in the mailbox controller's delivery context: never sleeps, never
+ * touches the netdev, and defers every reply to the work items. @data points
+ * at the controller's frame buffer, which is only refreshed for the length
+ * the peer sent.
+ */
+static void irot_rx_callback(struct mbox_client *cl, void *data)
+{
+ struct irot_priv *p = container_of(cl, struct irot_priv, cl);
+ const struct irot_mbox_frame *frame = data;
+ const struct irot_msg *m = &frame->irot;
+
+ if (le32_to_cpu(frame->len) != sizeof(*frame)) {
+ dev_warn_ratelimited(p->dev, "bad mailbox frame length %u\n",
+ le32_to_cpu(frame->len));
+ return;
+ }
+
+ switch (le32_to_cpu(m->command)) {
+ case IROT_CC_MCTP_BUFFER:
+ irot_cb_buffer_rsp(p, m);
+ break;
+ case IROT_CC_MCTP:
+ irot_cb_mctp(p, m);
+ break;
+ case IROT_CC_MCTP_DONE:
+ irot_tx_mark_done(p, le32_to_cpu(m->data.value));
+ break;
+ case IROT_CC_PING:
+ irot_cb_ping(p, m);
+ break;
+ default:
+ dev_warn_ratelimited(p->dev, "unknown IRoT command %u\n",
+ le32_to_cpu(m->command));
+ break;
+ }
+}
+
+/* tx_block: mbox_send_message() waits for the controller's tx-done. */
+static int irot_request_mbox(struct irot_priv *p)
+{
+ p->cl.dev = p->dev;
+ p->cl.rx_callback = irot_rx_callback;
+ p->cl.tx_block = true;
+ p->cl.knows_txdone = false;
+ p->cl.tx_tout = IROT_MBOX_TX_TOUT_MS;
+
+ p->chan_tx = mbox_request_channel_byname(&p->cl, "tx");
+ if (IS_ERR(p->chan_tx))
+ return dev_err_probe(p->dev, PTR_ERR(p->chan_tx),
+ "Failed to request tx mailbox channel\n");
+
+ p->chan_rx = mbox_request_channel_byname(&p->cl, "rx");
+ if (IS_ERR(p->chan_rx)) {
+ mbox_free_channel(p->chan_tx);
+ return dev_err_probe(p->dev, PTR_ERR(p->chan_rx),
+ "Failed to request rx mailbox channel\n");
+ }
+
+ return 0;
+}
+
+/*
+ * The IRoT picks the windows and reports them over the mailbox, but must
+ * place them inside the memory-region carve-out. Anything outside it is
+ * rejected; a window inside it is addressed as an offset into the one mapping.
+ */
+static void *irot_window_in_pool(struct irot_priv *p,
+ const struct irot_region *r, const char *name)
+{
+ u64 off;
+
+ if (r->base < p->pool_res.start)
+ goto outside;
+
+ off = r->base - p->pool_res.start;
+ if (off >= resource_size(&p->pool_res) ||
+ r->size > resource_size(&p->pool_res) - off)
+ goto outside;
+
+ return p->pool + off;
+
+outside:
+ return ERR_PTR(dev_err_probe(p->dev, -EINVAL,
+ "%s window 0x%llx+%u outside memory-region %pR\n",
+ name, r->base, r->size, &p->pool_res));
+}
+
+static int irot_map_pool(struct irot_priv *p)
+{
+ int rc;
+
+ rc = of_reserved_mem_region_to_resource(p->dev->of_node, 0, &p->pool_res);
+ if (rc)
+ return dev_err_probe(p->dev, rc, "Missing memory-region\n");
+
+ p->pool = devm_memremap(p->dev, p->pool_res.start,
+ resource_size(&p->pool_res), MEMREMAP_WC);
+ if (IS_ERR(p->pool))
+ return dev_err_probe(p->dev, PTR_ERR(p->pool),
+ "Failed to map memory-region %pR\n",
+ &p->pool_res);
+
+ return 0;
+}
+
+/*
+ * Ask the peer for its windows, up to IROT_BUFFER_RETRIES times, then
+ * bounds-check them against the memory-region.
+ *
+ * The IRoT is expected to be up before Linux probes this device. If it does
+ * not answer within IROT_BUFFER_RETRIES * IROT_BUFFER_TIMEOUT_S, probe fails
+ * for good; rebinding the driver retries. Probe is asynchronous, so the wait
+ * does not hold up boot.
+ */
+static int irot_discover_buffers(struct irot_priv *p)
+{
+ struct irot_msg req = {};
+ bool got = false;
+ int i, rc;
+ u32 cap;
+
+ req.command = cpu_to_le32(IROT_CC_GET_MCTP_BUFFERS);
+
+ for (i = 0; i < IROT_BUFFER_RETRIES && !got; i++) {
+ reinit_completion(&p->buffers_done);
+
+ rc = irot_send(p, &req);
+ if (rc < 0)
+ return dev_err_probe(p->dev, rc,
+ "Failed to send cc_get_mctp_buffers\n");
+
+ wait_for_completion_timeout(&p->buffers_done,
+ secs_to_jiffies(IROT_BUFFER_TIMEOUT_S));
+
+ scoped_guard(spinlock_irqsave, &p->rx_lock)
+ got = p->buffers_valid;
+ }
+
+ if (!got)
+ return dev_err_probe(p->dev, -ETIMEDOUT,
+ "No cc_mctp_buffer after %d attempts\n",
+ IROT_BUFFER_RETRIES);
+
+ /* A window holds exactly one packet, so it also caps the MTU */
+ cap = min3(p->host_read.size, p->host_write.size, IROT_MCTP_MAX_MTU);
+ if (cap < IROT_MCTP_MIN_MTU)
+ return dev_err_probe(p->dev, -EINVAL,
+ "windows hold %u bytes, below the MCTP minimum MTU %u\n",
+ cap, IROT_MCTP_MIN_MTU);
+
+ if (p->mtu < IROT_MCTP_MIN_MTU)
+ return dev_err_probe(p->dev, -EINVAL,
+ "mtu_limit %u below the MCTP minimum %u\n",
+ p->mtu, IROT_MCTP_MIN_MTU);
+
+ if (p->mtu > cap) {
+ dev_warn(p->dev, "mtu_limit %u exceeds the windows (%u), clamping\n",
+ p->mtu, cap);
+ p->mtu = cap;
+ }
+
+ p->rx_addr = irot_window_in_pool(p, &p->host_read, "host_read");
+ if (IS_ERR(p->rx_addr))
+ return PTR_ERR(p->rx_addr);
+
+ p->tx_addr = irot_window_in_pool(p, &p->host_write, "host_write");
+ if (IS_ERR(p->tx_addr))
+ return PTR_ERR(p->tx_addr);
+
+ /* Both lie inside the pool, so neither end can wrap */
+ if (p->host_read.base < p->host_write.base + p->host_write.size &&
+ p->host_write.base < p->host_read.base + p->host_read.size)
+ return dev_err_probe(p->dev, -EINVAL,
+ "host_read and host_write windows overlap\n");
+
+ dev_dbg(p->dev, "windows: read 0x%llx+%u, write 0x%llx+%u, mtu %u\n",
+ p->host_read.base, p->host_read.size,
+ p->host_write.base, p->host_write.size, p->mtu);
+
+ return 0;
+}
+
+/*
+ * Copy one inbound packet out of host_read and hand it to the stack. Drops
+ * that the core does not count itself are added to @dropped.
+ */
+static enum irot_rx_result irot_rx_deliver(struct irot_priv *p,
+ const struct irot_rx_entry *e,
+ unsigned int *dropped)
+{
+ struct sk_buff *skb;
+ u64 off;
+
+ /*
+ * host_read bounds the packet, not the negotiated MTU: with asymmetric
+ * windows the peer may send more than p->mtu. The cap only stops an
+ * oversized window from turning into an allocation that fails on every
+ * resend.
+ */
+ if (!e->size || e->size > IROT_MCTP_MAX_MTU ||
+ !irot_region_offset(&p->host_read, e->base, e->size, &off)) {
+ dev_warn_ratelimited(p->dev,
+ "invalid RX span 0x%llx+%u (window 0x%llx+%u)\n",
+ e->base, e->size, p->host_read.base,
+ p->host_read.size);
+ (*dropped)++;
+ return IROT_RX_INVALID;
+ }
+
+ /* ACK and drop: the peer must not be left holding its window */
+ if (!netif_running(p->ndev)) {
+ (*dropped)++;
+ return IROT_RX_INVALID;
+ }
+
+ skb = netdev_alloc_skb(p->ndev, e->size);
+ if (!skb) {
+ (*dropped)++;
+ return IROT_RX_RETRY;
+ }
+
+ /*
+ * Order the payload read after the doorbell that announced it; pairs
+ * with the IRoT's write barrier before it rang the doorbell.
+ */
+ dma_rmb();
+
+ skb_put_data(skb, (u8 *)p->rx_addr + off, e->size);
+
+ skb->protocol = htons(ETH_P_MCTP);
+ skb_reset_mac_header(skb);
+ skb_reset_network_header(skb);
+ __mctp_cb(skb)->halen = 0;
+
+ /* The core counts the drop itself */
+ if (netif_rx(skb) != NET_RX_SUCCESS)
+ return IROT_RX_RETRY;
+
+ return IROT_RX_DELIVERED;
+}
+
+/*
+ * RX work: answers pings, re-ACKs resends of delivered packets, and delivers
+ * new packets. A packet is ACKed only after the stack has it, and the dedup
+ * cursor moves only then, so a packet that could not be delivered is
+ * delivered when the peer resends it. The callback requeues this work for
+ * every message, so one pass per run is enough.
+ */
+static void irot_rx_work(struct work_struct *work)
+{
+ struct irot_priv *p = container_of(work, struct irot_priv, rx_work);
+ enum irot_rx_result res;
+ struct irot_rx_entry e;
+ bool do_ping, do_ack, do_rx;
+ unsigned int dropped;
+ u32 ping_value;
+ u32 ack_counter;
+
+ scoped_guard(spinlock_irqsave, &p->rx_lock) {
+ do_ping = p->ping_pending;
+ ping_value = p->ping_value;
+ p->ping_pending = false;
+
+ do_ack = p->dup_ack_pending;
+ ack_counter = p->dup_ack_counter;
+ p->dup_ack_pending = false;
+
+ /* rx_busy stays set until the packet is answered */
+ do_rx = p->rx_new;
+ e = p->rx_entry;
+ p->rx_new = false;
+
+ dropped = p->rx_dropped;
+ p->rx_dropped = 0;
+ }
+
+ if (do_ping) {
+ struct irot_msg rsp = {};
+ int rc;
+
+ rsp.command = cpu_to_le32(IROT_CC_PING_RSP);
+ rsp.data.value = cpu_to_le32(ping_value);
+
+ rc = irot_send(p, &rsp);
+ if (rc < 0)
+ dev_warn_ratelimited(p->dev, "Failed to answer ping: %d\n",
+ rc);
+ }
+
+ if (do_ack)
+ irot_send_done(p, ack_counter);
+
+ if (do_rx) {
+ res = irot_rx_deliver(p, &e, &dropped);
+
+ scoped_guard(spinlock_irqsave, &p->rx_lock) {
+ p->rx_busy = false;
+ if (res == IROT_RX_DELIVERED) {
+ p->last_rx_counter = e.counter;
+ p->has_last_rx_counter = true;
+ }
+ }
+ }
+
+ local_bh_disable();
+ if (dropped)
+ dev_dstats_rx_dropped_add(p->ndev, dropped);
+ if (do_rx && res == IROT_RX_DELIVERED)
+ dev_dstats_rx_add(p->ndev, e.size);
+ local_bh_enable();
+
+ /* Withhold the ACK and the peer resends */
+ if (do_rx && res != IROT_RX_RETRY)
+ irot_send_done(p, e.counter);
+}
+
+/*
+ * TX work: write the skb handed over by xmit into host_write and ring the
+ * doorbell. The queue stays stopped until the peer ACKs; an overdue ACK
+ * stalls the link rather than freeing the window under the peer's feet.
+ * With no skb to send, it was kicked because the window came free.
+ */
+static void irot_tx_work(struct work_struct *work)
+{
+ struct irot_priv *p = container_of(work, struct irot_priv, tx_work);
+ struct irot_msg m = {};
+ struct sk_buff *skb;
+ u32 tx_counter;
+ u32 len;
+ int rc;
+
+ scoped_guard(spinlock_irqsave, &p->tx_state_lock) {
+ skb = p->tx_skb;
+ p->tx_skb = NULL;
+ }
+
+ if (!skb) {
+ irot_tx_wake(p);
+ return;
+ }
+
+ /*
+ * Claim the window before writing it, so a cc_mctp_done that arrives
+ * immediately still matches an in-flight counter. xmit only runs once
+ * the window is free, so this cannot fail.
+ */
+ tx_counter = p->next_tx_counter;
+ if (WARN_ON_ONCE(irot_tx_mark_pending(p, tx_counter))) {
+ irot_tx_drop(p, skb);
+ return;
+ }
+ p->next_tx_counter++;
+
+ /* skb_copy_bits() handles nonlinear skbs */
+ len = skb->len;
+ if (skb_copy_bits(skb, 0, p->tx_addr, len)) {
+ irot_tx_mark_done(p, tx_counter);
+ irot_tx_drop(p, skb);
+ return;
+ }
+
+ /* Publish the payload before ringing the doorbell */
+ dma_wmb();
+
+ m.command = cpu_to_le32(IROT_CC_MCTP);
+ m.data.mctp.counter = cpu_to_le32(tx_counter);
+ /* Always at the start of host_write: one outstanding write */
+ irot_span_from_region(&m.data.mctp.packet,
+ &(struct irot_region){
+ .base = p->host_write.base,
+ .size = len,
+ });
+
+ /* Arm before sending, so an immediate cc_mctp_done cancels it */
+ mod_delayed_work(p->wq, &p->tx_timeout_work,
+ msecs_to_jiffies(IROT_TX_ACK_TIMEOUT_MS));
+
+ rc = irot_send(p, &m);
+ if (rc == -ETIME) {
+ /*
+ * The controller has queued the message but the FIFO did not
+ * drain in time; the peer may still consume it, so keep the
+ * window claimed.
+ */
+ irot_tx_drop(p, skb);
+ irot_tx_stall(p, "mailbox send timed out");
+ return;
+ }
+ if (rc < 0) {
+ /* Nothing left the controller: the window is ours */
+ dev_err_ratelimited(p->dev, "TX send failed: %d\n", rc);
+ irot_tx_mark_done(p, tx_counter);
+ irot_tx_drop(p, skb);
+ return;
+ }
+
+ local_bh_disable();
+ dev_dstats_tx_add(p->ndev, len);
+ local_bh_enable();
+ consume_skb(skb);
+}
+
+static void irot_tx_timeout_work(struct work_struct *work)
+{
+ struct irot_priv *p = container_of(to_delayed_work(work),
+ struct irot_priv, tx_timeout_work);
+
+ irot_tx_stall(p, "no cc_mctp_done in time");
+}
+
+/* Hand the skb to tx_work; the queue restarts once the peer ACKs it */
+static netdev_tx_t irot_start_xmit(struct sk_buff *skb, struct net_device *ndev)
+{
+ struct irot_priv *p = netdev_priv(ndev);
+ bool busy;
+
+ if (!skb->len || skb->len > p->mtu) {
+ dev_warn_ratelimited(p->dev, "TX invalid len %u (mtu %u), dropping\n",
+ skb->len, p->mtu);
+ irot_tx_drop(p, skb);
+ return NETDEV_TX_OK;
+ }
+
+ /* Stop and hand over under the lock that irot_tx_wake() wakes under */
+ scoped_guard(spinlock_irqsave, &p->tx_state_lock) {
+ netif_stop_queue(ndev);
+ busy = p->tx_skb;
+ if (!busy)
+ p->tx_skb = skb;
+ }
+
+ /* Unreachable: the queue stays stopped while tx_skb is held */
+ if (WARN_ON_ONCE(busy))
+ return NETDEV_TX_BUSY;
+
+ queue_work(p->wq, &p->tx_work);
+
+ return NETDEV_TX_OK;
+}
+
+static int irot_ndo_open(struct net_device *ndev)
+{
+ struct irot_priv *p = netdev_priv(ndev);
+
+ /*
+ * A busy or stalled window keeps the queue stopped; tx_work wakes it
+ * once the ACK arrives, since netif_running() is already true here.
+ */
+ if (irot_tx_ready(p))
+ netif_start_queue(ndev);
+ else
+ netif_stop_queue(ndev);
+
+ return 0;
+}
+
+static int irot_ndo_stop(struct net_device *ndev)
+{
+ struct irot_priv *p = netdev_priv(ndev);
+ struct sk_buff *skb;
+
+ netif_stop_queue(ndev);
+
+ /*
+ * Do not touch tx_timeout_work: a packet in flight still owns the
+ * window, and the stall and late-ACK handling must keep running.
+ */
+ cancel_work_sync(&p->tx_work);
+
+ scoped_guard(spinlock_irqsave, &p->tx_state_lock) {
+ skb = p->tx_skb;
+ p->tx_skb = NULL;
+ }
+
+ if (skb)
+ irot_tx_drop(p, skb);
+
+ return 0;
+}
+
+static const struct net_device_ops irot_netdev_ops = {
+ .ndo_open = irot_ndo_open,
+ .ndo_stop = irot_ndo_stop,
+ .ndo_start_xmit = irot_start_xmit,
+};
+
+/*
+ * A point-to-point MCTP link with no physical addressing. The MTU starts at
+ * the DSP0236 baseline; mtu and max_mtu are raised in probe once the
+ * negotiated mtu_limit is known.
+ */
+static void irot_netdev_setup(struct net_device *ndev)
+{
+ ndev->type = ARPHRD_MCTP;
+ ndev->addr_len = 0;
+ ndev->hard_header_len = 0;
+ ndev->flags = IFF_NOARP;
+ ndev->tx_queue_len = IROT_TX_QUEUE_LEN;
+ ndev->min_mtu = IROT_MCTP_MIN_MTU;
+ ndev->max_mtu = IROT_MCTP_MIN_MTU;
+ ndev->mtu = IROT_MCTP_MIN_MTU;
+ ndev->netdev_ops = &irot_netdev_ops;
+ ndev->pcpu_stat_type = NETDEV_PCPU_STAT_DSTATS;
+}
+
+/*
+ * Stop all deferred work for good. After this, the callback's queue_work()
+ * calls are no-ops, and nothing but probe or remove sends on the mailbox.
+ */
+static void irot_disable_work(struct irot_priv *p)
+{
+ disable_work_sync(&p->rx_work);
+ disable_work_sync(&p->tx_work);
+ disable_delayed_work_sync(&p->tx_timeout_work);
+}
+
+/*
+ * The work items touch the netdev's carrier, queue and stats, so they only
+ * run while it is registered: rx_work is held off until then, and tx_work and
+ * tx_timeout_work are only queued once xmit has run.
+ *
+ * A cc_ping or cc_mctp that arrives while the windows are being negotiated is
+ * recorded by the callback and answered once rx_work is enabled, so the IRoT
+ * sees no reply until registration.
+ */
+static int irot_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct net_device *ndev;
+ struct irot_priv *p;
+ int rc;
+
+ ndev = alloc_netdev(sizeof(*p), "mctpirot%d", NET_NAME_ENUM,
+ irot_netdev_setup);
+ if (!ndev)
+ return -ENOMEM;
+
+ SET_NETDEV_DEV(ndev, dev);
+
+ p = netdev_priv(ndev);
+ p->dev = dev;
+ p->ndev = ndev;
+ spin_lock_init(&p->tx_state_lock);
+ spin_lock_init(&p->rx_lock);
+ mutex_init(&p->tx_chan_lock);
+ init_completion(&p->buffers_done);
+ INIT_WORK(&p->tx_work, irot_tx_work);
+ INIT_WORK(&p->rx_work, irot_rx_work);
+ INIT_DELAYED_WORK(&p->tx_timeout_work, irot_tx_timeout_work);
+
+ /* rx_work hands packets to the stack: hold it off until registration */
+ disable_work(&p->rx_work);
+
+ platform_set_drvdata(pdev, p);
+
+ p->wq = alloc_ordered_workqueue("irot/%s", 0, dev_name(dev));
+ if (!p->wq) {
+ rc = -ENOMEM;
+ goto err_free_netdev;
+ }
+
+ rc = irot_map_pool(p);
+ if (rc)
+ goto err_destroy_wq;
+
+ rc = irot_request_mbox(p);
+ if (rc)
+ goto err_destroy_wq;
+
+ rc = irot_discover_buffers(p);
+ if (rc)
+ goto err_free_mailbox;
+
+ /* The peer cannot reassemble, so advertise the full negotiated MTU */
+ ndev->max_mtu = p->mtu;
+ ndev->mtu = p->mtu;
+
+ netif_carrier_off(ndev);
+
+ rc = mctp_register_netdev(ndev, NULL, MCTP_PHYS_BINDING_VENDOR);
+ if (rc) {
+ rc = dev_err_probe(dev, rc, "Failed to register MCTP netdev\n");
+ goto err_free_mailbox;
+ }
+
+ enable_and_queue_work(p->wq, &p->rx_work);
+ netif_carrier_on(ndev);
+
+ dev_dbg(dev, "MCTP transport ready on %s, max MTU %u\n",
+ ndev->name, ndev->max_mtu);
+
+ return 0;
+
+err_free_mailbox:
+ irot_disable_work(p);
+ mbox_free_channel(p->chan_rx);
+ mbox_free_channel(p->chan_tx);
+err_destroy_wq:
+ destroy_workqueue(p->wq);
+err_free_netdev:
+ free_netdev(ndev);
+ return rc;
+}
+
+static void irot_remove(struct platform_device *pdev)
+{
+ struct irot_priv *p = platform_get_drvdata(pdev);
+ struct net_device *ndev = p->ndev;
+
+ /*
+ * Unlike the usual order, quiesce the work items before unregistering:
+ * they raise and drop the carrier, which must not happen once the
+ * netdev is gone. An xmit that races with this leaves its skb in
+ * tx_skb, which is freed below once unregistering has stopped xmit.
+ */
+ irot_disable_work(p);
+ mctp_unregister_netdev(ndev);
+
+ mbox_free_channel(p->chan_rx);
+ mbox_free_channel(p->chan_tx);
+
+ destroy_workqueue(p->wq);
+ kfree_skb(p->tx_skb);
+ free_netdev(ndev);
+}
+
+static const struct of_device_id irot_of_match[] = {
+ { .compatible = "axiado,ax3005-mctp-irot" },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, irot_of_match);
+
+static struct platform_driver irot_driver = {
+ .probe = irot_probe,
+ .remove = irot_remove,
+ .driver = {
+ .name = "ax3005-mctp-irot",
+ .of_match_table = irot_of_match,
+ .probe_type = PROBE_PREFER_ASYNCHRONOUS,
+ },
+};
+module_platform_driver(irot_driver);
+
+MODULE_AUTHOR("Axiado Corporation");
+MODULE_DESCRIPTION("Axiado AX3005 IRoT MCTP transport driver");
+MODULE_LICENSE("GPL");
--
2.34.1
^ permalink raw reply [flat|nested] 5+ messages in thread