All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v4 0/3] raw/ntb: add AMD NTB support
@ 2026-10-26  8:55 Raghavendra Ningoji
  2026-10-26  8:55 ` [PATCH v4 1/3] raw/ntb: validate vendor ops at probe Raghavendra Ningoji
                   ` (3 more replies)
  0 siblings, 4 replies; 7+ messages in thread
From: Raghavendra Ningoji @ 2026-10-26  8:55 UTC (permalink / raw)
  To: dev
  Cc: jingjing.wu, thomas, selwin.sebastian, bhagyada.modali,
	bruce.richardson, david.marchand, sachin.saxena, hemant.agrawal,
	Raghavendra Ningoji

This series adds support for the Non-Transparent Bridge (NTB) endpoints
integrated in AMD EPYC Embedded "Turin", "Genoa" and "Siena" processors
to the raw/ntb driver.

The common NTB code validates the vendor ops once at probe time, and the
vendor abstraction is built around a small set of mandatory ops instead
of optional hooks with built-in fallbacks:

- Patch 1 is a preparation commit that enforces the vendor ops once, at
  probe time, and removes the per-call NULL checks that are scattered
  throughout the common code.

- Patch 2 generalizes the framework for multiple vendors. The link
  bring-up/tear-down is now expressed through mandatory ops
  (dev_handshake, peer_mw_validate, read_peer_config, link_cleanup); the
  former built-in scratchpad logic moved into the Intel driver. The
  mw_set_trans op is gone (it is now a vendor-internal concern of the
  handshake), and there is a single common interrupt handler with just
  one optional op (handle_hw_event) for vendor-specific hardware events.

- Patch 3 adds the AMD hardware driver, its documentation, release note
  and MAINTAINERS entry.

A few AMD-specific hardware details worth noting for reviewers:

- Primary/secondary topology. One endpoint enumerates as the primary
  (device ID 0x14c0) and the other as the secondary (0x14c3), rather
  than the Intel back-to-back model.

- Packed scratchpad handshake. The hardware exposes a single shared
  16-register scratchpad bank split into two disjoint 8-register sets
  (one per side), so the driver uses a packed handshake layout that
  fits in 8 registers, plugged in through the dev_handshake,
  peer_mw_validate and read_peer_config ops.

- Outbound translation window. AMD NTB uses an outbound translation
  window: a write to a BARxx memory-window offset is forwarded to
  (xlat_base | offset) in the peer's memory rather than
  (xlat_base + offset). The translation base must therefore be aligned
  to a power of two >= the window length so that no offset bit collides
  with a set bit in the base. This is reported to applications through
  the rte_pmd_ntb_get_mem_align() API and honoured by the ntb example
  when reserving the memory window memzone.

- Secondary-side link status. On the secondary side the device's own
  PCIe link status does not reflect the true inter-host link, so the
  speed and width are read from the upstream switch port via sysfs.

Tested on a two-node setup (primary + secondary EPYC Embedded hosts)
with the dpdk-ntb example: the link comes up at the expected speed and
width on both sides, and file transfers are byte-correct end to end for
both small and large files.

Changes since v3:

- Simplified the probe-time vendor ops check (patch 1) to a plain
  per-member macro instead of an offsetof() table.

- Removed the mw_size_align field from struct ntb_dev_info entirely
  (patch 2): applications must now use rte_pmd_ntb_get_mem_align(). This
  is documented in the API changes section of the release notes.

- Removed the unused pmd_private field from struct ntb_hw and the whole
  struct amd_ntb_hw (patch 3): self_mmio is just ntb->hw_addr, peer_mmio
  becomes AMD_PEER_MMIO(ntb), the scratchpad offsets become an
  AMD_SPAD_BASE(ntb, peer) macro, and int_mask/peer_status/ctl_status
  were unused.

- Sorted AMD alphabetically before Intel in meson.build, the ntb_hw
  header include, the PCI id mapping table and usertools/dpdk-devbind.py.

- Moved the primary/secondary topology definitions (NTB_TOPO_PRI/SEC)
  from patch 2 to patch 3 where they are first used.

- Updated the Limitation section of doc/guides/rawdevs/ntb.rst to list
  the supported AMD processors.

Changes since v2:

- The series is freshly rebased on main; the release note and
  dpdk-devbind.py now apply cleanly and CI should pass.

- New preparation patch (patch 1): the vendor ops are validated once at
  probe time and all the runtime ntb_ops NULL checks in the common code
  are removed.

- Reworked the vendor abstraction (patch 2): dev_handshake,
  peer_mw_validate, read_peer_config and link_cleanup are now mandatory
  ops provided by both the Intel and AMD drivers. The built-in
  scratchpad handshake/validate/cleanup/peer-config logic that used to
  live in the common code moved into the Intel driver.

- Dropped the mw_set_trans op: it is now a vendor-internal detail of the
  handshake.

- Dropped the vendor interrupt_handler op: there is now a single common
  interrupt handler, with handle_hw_event as the only optional op, used
  by AMD for its SMU/power-management events.

Changes since v1:

- Replaced the mw_addr_align field in struct ntb_dev_info with a
  mem_align op and the rte_pmd_ntb_get_mem_align() API, so the
  base-address alignment computation lives in the driver and the
  application just queries it (Bruce Richardson's review).

- Squashed the documentation, release note and MAINTAINERS update into
  the AMD driver patch, and documented the alignment API in the guide.

Raghavendra Ningoji (3):
  raw/ntb: validate vendor ops at probe
  raw/ntb: generalize framework for multiple vendors
  raw/ntb: add AMD NTB support

 MAINTAINERS                            |   2 +
 doc/guides/rawdevs/ntb.rst             |  31 +-
 doc/guides/rel_notes/release_26_11.rst |  13 +
 drivers/raw/ntb/meson.build            |   7 +-
 drivers/raw/ntb/ntb.c                  | 310 ++++--------
 drivers/raw/ntb/ntb.h                  |  24 +-
 drivers/raw/ntb/ntb_hw_amd.c           | 658 +++++++++++++++++++++++++
 drivers/raw/ntb/ntb_hw_amd.h           | 111 +++++
 drivers/raw/ntb/ntb_hw_intel.c         | 165 ++++++-
 drivers/raw/ntb/rte_pmd_ntb.h          |  28 +-
 examples/ntb/ntb_fwd.c                 |   6 +-
 usertools/dpdk-devbind.py              |   1 +
 12 files changed, 1124 insertions(+), 232 deletions(-)
 create mode 100644 drivers/raw/ntb/ntb_hw_amd.c
 create mode 100644 drivers/raw/ntb/ntb_hw_amd.h

-- 
2.34.1


^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH v4 1/3] raw/ntb: validate vendor ops at probe
  2026-10-26  8:55 [PATCH v4 0/3] raw/ntb: add AMD NTB support Raghavendra Ningoji
@ 2026-10-26  8:55 ` Raghavendra Ningoji
  2026-10-26  8:55 ` [PATCH v4 2/3] raw/ntb: generalize framework for multiple vendors Raghavendra Ningoji
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 7+ messages in thread
From: Raghavendra Ningoji @ 2026-10-26  8:55 UTC (permalink / raw)
  To: dev
  Cc: jingjing.wu, thomas, selwin.sebastian, bhagyada.modali,
	bruce.richardson, david.marchand, sachin.saxena, hemant.agrawal,
	Raghavendra Ningoji

The NTB driver dereferences the vendor ntb_ops hooks unconditionally
throughout the common code, yet also guards many of those calls with
runtime NULL checks. Those checks can never fire for a correctly
populated vendor ops table and only make the code harder to follow.

Validate the mandatory ops once, right after the vendor ops table is
selected at probe time, and drop the scattered per-call NULL checks.

Signed-off-by: Raghavendra Ningoji <raghavendra.ningoji@amd.com>
---
 drivers/raw/ntb/ntb.c | 103 ++++++++++++++----------------------------
 1 file changed, 35 insertions(+), 68 deletions(-)

diff --git a/drivers/raw/ntb/ntb.c b/drivers/raw/ntb/ntb.c
index d54f2fb783..09cf325c9a 100644
--- a/drivers/raw/ntb/ntb.c
+++ b/drivers/raw/ntb/ntb.c
@@ -46,12 +46,6 @@ ntb_link_cleanup(struct rte_rawdev *dev)
 	struct ntb_hw *hw = dev->dev_private;
 	int status, i;
 
-	if (hw->ntb_ops->spad_write == NULL ||
-	    hw->ntb_ops->mw_set_trans == NULL) {
-		NTB_LOG(ERR, "Not supported to clean up link.");
-		return;
-	}
-
 	/* Clean spad registers. */
 	for (i = 0; i < hw->spad_cnt; i++) {
 		status = (*hw->ntb_ops->spad_write)(dev, i, 0, 0);
@@ -74,12 +68,6 @@ ntb_handshake_work(const struct rte_rawdev *dev)
 	uint32_t val;
 	int ret, i;
 
-	if (hw->ntb_ops->spad_write == NULL ||
-	    hw->ntb_ops->mw_set_trans == NULL) {
-		NTB_LOG(ERR, "Scratchpad/MW setting is not supported.");
-		return -ENOTSUP;
-	}
-
 	/* Tell peer the mw info of local side. */
 	ret = (*hw->ntb_ops->spad_write)(dev, SPAD_NUM_MWS, 1, hw->mw_cnt);
 	if (ret < 0)
@@ -150,13 +138,6 @@ ntb_dev_intr_handler(void *param)
 	uint8_t peer_mw_cnt;
 	int i = 0;
 
-	if (hw->ntb_ops->db_read == NULL ||
-	    hw->ntb_ops->db_clear == NULL ||
-	    hw->ntb_ops->peer_db_set == NULL) {
-		NTB_LOG(ERR, "Doorbell is not supported.");
-		return;
-	}
-
 	db_bits = (*hw->ntb_ops->db_read)(dev);
 	if (!db_bits)
 		NTB_LOG(ERR, "No doorbells");
@@ -174,11 +155,6 @@ ntb_dev_intr_handler(void *param)
 		if (hw->peer_dev_up)
 			return;
 
-		if (hw->ntb_ops->spad_read == NULL) {
-			NTB_LOG(ERR, "Scratchpad read is not supported.");
-			return;
-		}
-
 		/* Check if mw setting on the peer is the same as local. */
 		peer_mw_cnt = (*hw->ntb_ops->spad_read)(dev, SPAD_NUM_MWS, 0);
 		if (peer_mw_cnt != hw->mw_cnt) {
@@ -214,10 +190,6 @@ ntb_dev_intr_handler(void *param)
 		}
 
 		/* To get the link info. */
-		if (hw->ntb_ops->get_link_status == NULL) {
-			NTB_LOG(ERR, "Not supported to get link status.");
-			return;
-		}
 		(*hw->ntb_ops->get_link_status)(dev);
 		NTB_LOG(INFO, "Link is up. Link speed: %u. Link width: %u",
 			hw->link_speed, hw->link_width);
@@ -511,11 +483,6 @@ ntb_queue_init(struct rte_rawdev *dev, uint16_t qp_id)
 	void *bar_addr;
 	uint16_t i;
 
-	if (hw->ntb_ops->get_peer_mw_addr == NULL) {
-		NTB_LOG(ERR, "Getting peer mw addr is not supported.");
-		return -EINVAL;
-	}
-
 	/* Put queue info into the start of shared memory. */
 	hdr_offset = hw->hdr_size_per_queue * qp_id;
 	local_hdr = (volatile struct ntb_header *)
@@ -615,11 +582,6 @@ ntb_enqueue_bufs(struct rte_rawdev *dev,
 	void *buf_addr;
 	int i;
 
-	if (unlikely(hw->ntb_ops->ioremap == NULL)) {
-		NTB_LOG(ERR, "Ioremap not supported.");
-		return nb_tx;
-	}
-
 	if (unlikely(dev->started == 0 || hw->peer_dev_up == 0)) {
 		NTB_LOG(DEBUG, "Link is not up.");
 		return nb_tx;
@@ -929,11 +891,6 @@ ntb_dev_start(struct rte_rawdev *dev)
 		goto err_q_init;
 	}
 
-	if (hw->ntb_ops->spad_read == NULL) {
-		ret = -ENOTSUP;
-		goto err_up;
-	}
-
 	peer_val = (*hw->ntb_ops->spad_read)(dev, SPAD_Q_SZ, 0);
 	if (peer_val != hw->queue_size) {
 		NTB_LOG(ERR, "Inconsistent queue size! (local: %u peer: %u)",
@@ -988,10 +945,6 @@ ntb_dev_stop(struct rte_rawdev *dev)
 	ntb_link_cleanup(dev);
 
 	/* Notify the peer that device will be down. */
-	if (hw->ntb_ops->peer_db_set == NULL) {
-		NTB_LOG(ERR, "Peer doorbell setting is not supported.");
-		return;
-	}
 	status = (*hw->ntb_ops->peer_db_set)(dev, 1);
 	if (status) {
 		NTB_LOG(ERR, "Failed to tell peer device is down.");
@@ -1012,10 +965,6 @@ ntb_dev_stop(struct rte_rawdev *dev)
 
 clean:
 	/* Clear doorbells mask. */
-	if (hw->ntb_ops->db_set_mask == NULL) {
-		NTB_LOG(ERR, "Doorbell mask setting is not supported.");
-		return;
-	}
 	status = (*hw->ntb_ops->db_set_mask)(dev,
 				(((uint64_t)1 << hw->db_cnt) - 1));
 	if (status)
@@ -1084,8 +1033,6 @@ ntb_attr_set(struct rte_rawdev *dev, const char *attr_name,
 	hw = dev->dev_private;
 
 	if (!strncmp(attr_name, NTB_SPAD_USER, NTB_SPAD_USER_LEN)) {
-		if (hw->ntb_ops->spad_write == NULL)
-			return -ENOTSUP;
 		index = atoi(&attr_name[NTB_SPAD_USER_LEN]);
 		if (index < 0 || index >= NTB_SPAD_USER_MAX_NUM) {
 			NTB_LOG(ERR, "Invalid attribute (%s)", attr_name);
@@ -1182,8 +1129,6 @@ ntb_attr_get(struct rte_rawdev *dev, const char *attr_name,
 	}
 
 	if (!strncmp(attr_name, NTB_SPAD_USER, NTB_SPAD_USER_LEN)) {
-		if (hw->ntb_ops->spad_read == NULL)
-			return -ENOTSUP;
 		index = atoi(&attr_name[NTB_SPAD_USER_LEN]);
 		if (index < 0 || index >= NTB_SPAD_USER_MAX_NUM) {
 			NTB_LOG(ERR, "Attribute (%s) out of range", attr_name);
@@ -1364,6 +1309,37 @@ static const struct rte_rawdev_ops ntb_ops = {
 	.xstats_reset         = ntb_xstats_reset,
 };
 
+/* Every vendor ops entry is dereferenced unconditionally in the common
+ * code, so enforce their presence once at probe time instead of guarding
+ * every call site at runtime.
+ */
+static int
+ntb_vendor_ops_check(const struct ntb_hw *hw)
+{
+#define NTB_OPS_REQ(member) do { \
+	if (hw->ntb_ops->member == NULL) { \
+		NTB_LOG(ERR, "Vendor ops lacks mandatory ." #member); \
+		return -EINVAL; \
+	} \
+} while (0)
+	NTB_OPS_REQ(ntb_dev_init);
+	NTB_OPS_REQ(get_peer_mw_addr);
+	NTB_OPS_REQ(mw_set_trans);
+	NTB_OPS_REQ(ioremap);
+	NTB_OPS_REQ(get_link_status);
+	NTB_OPS_REQ(set_link);
+	NTB_OPS_REQ(spad_read);
+	NTB_OPS_REQ(spad_write);
+	NTB_OPS_REQ(db_read);
+	NTB_OPS_REQ(db_clear);
+	NTB_OPS_REQ(db_set_mask);
+	NTB_OPS_REQ(peer_db_set);
+	NTB_OPS_REQ(vector_bind);
+#undef NTB_OPS_REQ
+
+	return 0;
+}
+
 static int
 ntb_init_hw(struct rte_rawdev *dev, struct rte_pci_device *pci_dev)
 {
@@ -1387,16 +1363,16 @@ ntb_init_hw(struct rte_rawdev *dev, struct rte_pci_device *pci_dev)
 		return -EINVAL;
 	}
 
-	if (hw->ntb_ops->ntb_dev_init == NULL)
-		return -ENOTSUP;
+	ret = ntb_vendor_ops_check(hw);
+	if (ret)
+		return ret;
+
 	ret = (*hw->ntb_ops->ntb_dev_init)(dev);
 	if (ret) {
 		NTB_LOG(ERR, "Unable to init ntb dev.");
 		return ret;
 	}
 
-	if (hw->ntb_ops->set_link == NULL)
-		return -ENOTSUP;
 	ret = (*hw->ntb_ops->set_link)(dev, 1);
 	if (ret)
 		return ret;
@@ -1404,8 +1380,6 @@ ntb_init_hw(struct rte_rawdev *dev, struct rte_pci_device *pci_dev)
 	/* Init doorbell. */
 	hw->db_valid_mask = RTE_LEN2MASK(hw->db_cnt, uint64_t);
 	/* Clear all valid doorbell bits before registering intr handler */
-	if (hw->ntb_ops->db_clear == NULL)
-		return -ENOTSUP;
 	(*hw->ntb_ops->db_clear)(dev, hw->db_valid_mask);
 
 	intr_handle = pci_dev->intr_handle;
@@ -1423,19 +1397,12 @@ ntb_init_hw(struct rte_rawdev *dev, struct rte_pci_device *pci_dev)
 	 */
 	if (!rte_intr_cap_multiple(intr_handle)) {
 		for (i = 0; i < hw->db_cnt; i++) {
-			if (hw->ntb_ops->vector_bind == NULL)
-				return -ENOTSUP;
 			ret = (*hw->ntb_ops->vector_bind)(dev, i, 0);
 			if (ret)
 				return ret;
 		}
 	}
 
-	if (hw->ntb_ops->db_set_mask == NULL ||
-	    hw->ntb_ops->peer_db_set == NULL) {
-		NTB_LOG(ERR, "Doorbell is not supported.");
-		return -ENOTSUP;
-	}
 	hw->db_mask = 0;
 	ret = (*hw->ntb_ops->db_set_mask)(dev, hw->db_mask);
 	if (ret) {
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 7+ messages in thread

* [PATCH v4 2/3] raw/ntb: generalize framework for multiple vendors
  2026-10-26  8:55 [PATCH v4 0/3] raw/ntb: add AMD NTB support Raghavendra Ningoji
  2026-10-26  8:55 ` [PATCH v4 1/3] raw/ntb: validate vendor ops at probe Raghavendra Ningoji
@ 2026-10-26  8:55 ` Raghavendra Ningoji
  2026-10-26  8:55 ` [PATCH v4 3/3] raw/ntb: add AMD NTB support Raghavendra Ningoji
  2026-10-26 15:26 ` [PATCH v4 0/3] " David Marchand
  3 siblings, 0 replies; 7+ messages in thread
From: Raghavendra Ningoji @ 2026-10-26  8:55 UTC (permalink / raw)
  To: dev
  Cc: jingjing.wu, thomas, selwin.sebastian, bhagyada.modali,
	bruce.richardson, david.marchand, sachin.saxena, hemant.agrawal,
	Raghavendra Ningoji

The NTB rawdev framework was written around the Intel back-to-back
topology and the built-in scratchpad handshake protocol. Rework the
common code so other vendors can plug into the same framework through
a mandatory vendor ops table, without runtime op presence checks.

- Add NTB_TOPO_PRI/NTB_TOPO_SEC topology types for hardware that uses
  a primary/secondary topology instead of back-to-back.
- Turn the link bring-up/tear-down logic into mandatory vendor ops:
  dev_handshake, peer_mw_validate, read_peer_config and link_cleanup.
  The former built-in scratchpad logic now lives in the Intel driver.
- Drop the mw_set_trans op: programming the memory-window translation
  is a vendor-internal concern of the handshake, not a framework op.
- Keep a single common interrupt handler and let it dispatch through the
  new ops, with an optional handle_hw_event op for vendor-specific
  hardware events (none for Intel).
- Add a mem_align op and the rte_pmd_ntb_get_mem_align() API so an
  application can query the base-address alignment a memory window
  memzone needs, without embedding hardware-specific rules in the app.
  The Intel driver reports its memory-window size.
- Add a pmd_private pointer to struct ntb_hw for vendor-specific state.
- Guard the receive path against a malformed stream with no end-of-packet
  marker so it cannot overflow the descriptor ring.

Signed-off-by: Raghavendra Ningoji <raghavendra.ningoji@amd.com>
---
 doc/guides/rel_notes/release_26_11.rst |   5 +
 drivers/raw/ntb/ntb.c                  | 202 ++++++-------------------
 drivers/raw/ntb/ntb.h                  |  21 ++-
 drivers/raw/ntb/ntb_hw_intel.c         | 165 +++++++++++++++++++-
 drivers/raw/ntb/rte_pmd_ntb.h          |  28 +++-
 examples/ntb/ntb_fwd.c                 |   6 +-
 6 files changed, 266 insertions(+), 161 deletions(-)

diff --git a/doc/guides/rel_notes/release_26_11.rst b/doc/guides/rel_notes/release_26_11.rst
index be758a0c02..30f2f3009f 100644
--- a/doc/guides/rel_notes/release_26_11.rst
+++ b/doc/guides/rel_notes/release_26_11.rst
@@ -545,6 +545,11 @@ API Changes
   * ``rte_tel_data_add_dict_uint_hex``
   * ``rte_telemetry_register_cmd_arg``
 
+* raw/ntb: Removed the ``mw_size_align`` field from ``struct ntb_dev_info``.
+  Applications must now query the required memory window base-address
+  alignment with the new ``rte_pmd_ntb_get_mem_align()`` function
+  instead of relying on this field.
+
 
 ABI Changes
 -----------
diff --git a/drivers/raw/ntb/ntb.c b/drivers/raw/ntb/ntb.c
index 09cf325c9a..0e0cdf38e3 100644
--- a/drivers/raw/ntb/ntb.c
+++ b/drivers/raw/ntb/ntb.c
@@ -18,6 +18,7 @@
 #include <rte_memcpy.h>
 #include <rte_rawdev.h>
 #include <rte_rawdev_pmd.h>
+#include <eal_export.h>
 
 #include "ntb_hw_intel.h"
 #include "rte_pmd_ntb.h"
@@ -40,103 +41,12 @@ static struct rte_rawdev_xstats_name ntb_xstats_names[] = {
 };
 #define NTB_XSTATS_NUM RTE_DIM(ntb_xstats_names)
 
-static inline void
-ntb_link_cleanup(struct rte_rawdev *dev)
-{
-	struct ntb_hw *hw = dev->dev_private;
-	int status, i;
-
-	/* Clean spad registers. */
-	for (i = 0; i < hw->spad_cnt; i++) {
-		status = (*hw->ntb_ops->spad_write)(dev, i, 0, 0);
-		if (status)
-			NTB_LOG(ERR, "Failed to clean local spad.");
-	}
-
-	/* Clear mw so that peer cannot access local memory.*/
-	for (i = 0; i < hw->used_mw_num; i++) {
-		status = (*hw->ntb_ops->mw_set_trans)(dev, i, 0, 0);
-		if (status)
-			NTB_LOG(ERR, "Failed to clean mw.");
-	}
-}
-
-static inline int
-ntb_handshake_work(const struct rte_rawdev *dev)
-{
-	struct ntb_hw *hw = dev->dev_private;
-	uint32_t val;
-	int ret, i;
-
-	/* Tell peer the mw info of local side. */
-	ret = (*hw->ntb_ops->spad_write)(dev, SPAD_NUM_MWS, 1, hw->mw_cnt);
-	if (ret < 0)
-		return ret;
-	for (i = 0; i < hw->mw_cnt; i++) {
-		NTB_LOG(INFO, "Local %u mw size: 0x%"PRIx64"", i,
-				hw->mw_size[i]);
-		val = hw->mw_size[i] >> 32;
-		ret = (*hw->ntb_ops->spad_write)(dev, SPAD_MW0_SZ_H + 2 * i,
-						 1, val);
-		if (ret < 0)
-			return ret;
-		val = hw->mw_size[i];
-		ret = (*hw->ntb_ops->spad_write)(dev, SPAD_MW0_SZ_L + 2 * i,
-						 1, val);
-		if (ret < 0)
-			return ret;
-	}
-
-	/* Tell peer about the queue info and map memory to the peer. */
-	ret = (*hw->ntb_ops->spad_write)(dev, SPAD_Q_SZ, 1, hw->queue_size);
-	if (ret < 0)
-		return ret;
-	ret = (*hw->ntb_ops->spad_write)(dev, SPAD_NUM_QPS, 1,
-					 hw->queue_pairs);
-	if (ret < 0)
-		return ret;
-	ret = (*hw->ntb_ops->spad_write)(dev, SPAD_USED_MWS, 1,
-					 hw->used_mw_num);
-	if (ret < 0)
-		return ret;
-	for (i = 0; i < hw->used_mw_num; i++) {
-		val = (uint64_t)(size_t)(hw->mz[i]->addr) >> 32;
-		ret = (*hw->ntb_ops->spad_write)(dev, SPAD_MW0_BA_H + 2 * i,
-						 1, val);
-		if (ret < 0)
-			return ret;
-		val = (uint64_t)(size_t)(hw->mz[i]->addr);
-		ret = (*hw->ntb_ops->spad_write)(dev, SPAD_MW0_BA_L + 2 * i,
-						 1, val);
-		if (ret < 0)
-			return ret;
-	}
-
-	for (i = 0; i < hw->used_mw_num; i++) {
-		ret = (*hw->ntb_ops->mw_set_trans)(dev, i, hw->mz[i]->iova,
-						   hw->mz[i]->len);
-		if (ret < 0)
-			return ret;
-	}
-
-	/* Ring doorbell 0 to tell peer the device is ready. */
-	ret = (*hw->ntb_ops->peer_db_set)(dev, 0);
-	if (ret < 0)
-		return ret;
-
-	return 0;
-}
-
 static void
 ntb_dev_intr_handler(void *param)
 {
 	struct rte_rawdev *dev = (struct rte_rawdev *)param;
 	struct ntb_hw *hw = dev->dev_private;
-	uint32_t val_h, val_l;
-	uint64_t peer_mw_size;
-	uint64_t db_bits = 0;
-	uint8_t peer_mw_cnt;
-	int i = 0;
+	uint64_t db_bits;
 
 	db_bits = (*hw->ntb_ops->db_read)(dev);
 	if (!db_bits)
@@ -155,36 +65,19 @@ ntb_dev_intr_handler(void *param)
 		if (hw->peer_dev_up)
 			return;
 
-		/* Check if mw setting on the peer is the same as local. */
-		peer_mw_cnt = (*hw->ntb_ops->spad_read)(dev, SPAD_NUM_MWS, 0);
-		if (peer_mw_cnt != hw->mw_cnt) {
-			NTB_LOG(ERR, "Both mw cnt must be the same.");
+		/* Check that the peer memory-window layout matches local. */
+		if ((*hw->ntb_ops->peer_mw_validate)(dev) < 0)
 			return;
-		}
-
-		for (i = 0; i < hw->mw_cnt; i++) {
-			val_h = (*hw->ntb_ops->spad_read)
-				(dev, SPAD_MW0_SZ_H + 2 * i, 0);
-			val_l = (*hw->ntb_ops->spad_read)
-				(dev, SPAD_MW0_SZ_L + 2 * i, 0);
-			peer_mw_size = ((uint64_t)val_h << 32) | val_l;
-			NTB_LOG(DEBUG, "Peer %u mw size: 0x%"PRIx64"", i,
-					peer_mw_size);
-			if (peer_mw_size != hw->mw_size[i]) {
-				NTB_LOG(ERR, "Mw config must be the same.");
-				return;
-			}
-		}
 
 		hw->peer_dev_up = 1;
 
 		/**
-		 * Handshake with peer. Spad_write & mw_set_trans only works
-		 * when both devices are up. So write spad again when db is
-		 * received. And set db again for the later device who may miss
-		 * the 1st db.
+		 * Handshake with peer. Scratchpad and translation programming
+		 * only takes effect when both devices are up, so run the
+		 * handshake again on the doorbell and ring the peer so the
+		 * device that came up second does not miss the first doorbell.
 		 */
-		if (ntb_handshake_work(dev) < 0) {
+		if ((*hw->ntb_ops->dev_handshake)(dev) < 0) {
 			NTB_LOG(ERR, "Handshake work failed.");
 			return;
 		}
@@ -202,7 +95,7 @@ ntb_dev_intr_handler(void *param)
 		(*hw->ntb_ops->db_clear)(dev, 2);
 
 		/* Peer device will be down, So clean local side too. */
-		ntb_link_cleanup(dev);
+		(*hw->ntb_ops->link_cleanup)(dev);
 
 		hw->peer_dev_up = 0;
 		/* Response peer's dev_stop request. */
@@ -220,6 +113,10 @@ ntb_dev_intr_handler(void *param)
 
 	/* Clear other received doorbells. */
 	(*hw->ntb_ops->db_clear)(dev, db_bits);
+
+	/* Let the vendor process any hardware-specific events. */
+	if (hw->ntb_ops->handle_hw_event != NULL)
+		(*hw->ntb_ops->handle_hw_event)(dev);
 }
 
 static int
@@ -708,6 +605,11 @@ ntb_dequeue_bufs(struct rte_rawdev *dev,
 	for (nb_rx = 0; nb_rx < count; nb_rx++) {
 		i = 0;
 		while (true) {
+			if (unlikely(nb_mbufs >= rxq->nb_rx_desc)) {
+				NTB_LOG(ERR, "Malformed rx stream (no EOP); "
+					"aborting to avoid desc overflow.");
+				goto end_of_rx;
+			}
 			rx_item = rxq->rx_used_ring + rxq->last_used;
 			rxm_t = sw_ring[rxq->last_used].mbuf;
 			rxm_t->data_len = rx_item->len;
@@ -796,13 +698,6 @@ ntb_dev_info_get(struct rte_rawdev *dev, rte_rawdev_obj_t dev_info,
 	info->mw_cnt = hw->mw_cnt;
 	info->mw_size = hw->mw_size;
 
-	/**
-	 * Intel hardware requires that mapped memory base address should be
-	 * aligned with EMBARSZ and needs continuous memzone.
-	 */
-	info->mw_size_align = (uint8_t)(hw->pci_dev->id.vendor_id ==
-					NTB_INTEL_VENDOR_ID);
-
 	if (!hw->queue_size || !hw->queue_pairs) {
 		NTB_LOG(ERR, "No queue size and queue num assigned.");
 		return -EAGAIN;
@@ -817,6 +712,26 @@ ntb_dev_info_get(struct rte_rawdev *dev, rte_rawdev_obj_t dev_info,
 	return 0;
 }
 
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_pmd_ntb_get_mem_align, 26.11)
+uint64_t
+rte_pmd_ntb_get_mem_align(uint16_t dev_id, uint32_t mw_id, uint64_t mw_len)
+{
+	struct rte_rawdev *dev;
+	struct ntb_hw *hw;
+
+	if (dev_id >= RTE_RAWDEV_MAX_DEVS)
+		return 0;
+	dev = rte_rawdev_pmd_get_dev(dev_id);
+	if (dev->dev_private == NULL)
+		return 0;
+
+	hw = dev->dev_private;
+	if (mw_id >= hw->mw_cnt)
+		return RTE_CACHE_LINE_SIZE;
+
+	return (*hw->ntb_ops->mem_align)(dev, mw_id, mw_len);
+}
+
 static int
 ntb_dev_configure(const struct rte_rawdev *dev, rte_rawdev_obj_t config,
 		size_t config_size)
@@ -845,7 +760,7 @@ ntb_dev_configure(const struct rte_rawdev *dev, rte_rawdev_obj_t config,
 					 sizeof(uint64_t), 0);
 
 	/* Start handshake with the peer. */
-	ret = ntb_handshake_work(dev);
+	ret = (*hw->ntb_ops->dev_handshake)(dev);
 	if (ret < 0) {
 		rte_free(hw->rx_queues);
 		rte_free(hw->tx_queues);
@@ -861,8 +776,6 @@ static int
 ntb_dev_start(struct rte_rawdev *dev)
 {
 	struct ntb_hw *hw = dev->dev_private;
-	uint32_t peer_base_l, peer_val;
-	uint64_t peer_base_h;
 	uint32_t i;
 	int ret;
 
@@ -891,31 +804,10 @@ ntb_dev_start(struct rte_rawdev *dev)
 		goto err_q_init;
 	}
 
-	peer_val = (*hw->ntb_ops->spad_read)(dev, SPAD_Q_SZ, 0);
-	if (peer_val != hw->queue_size) {
-		NTB_LOG(ERR, "Inconsistent queue size! (local: %u peer: %u)",
-			hw->queue_size, peer_val);
-		ret = -EINVAL;
-		goto err_up;
-	}
-
-	peer_val = (*hw->ntb_ops->spad_read)(dev, SPAD_NUM_QPS, 0);
-	if (peer_val != hw->queue_pairs) {
-		NTB_LOG(ERR, "Inconsistent number of queues! (local: %u peer:"
-			" %u)", hw->queue_pairs, peer_val);
-		ret = -EINVAL;
+	/* Read and validate the peer configuration. */
+	ret = (*hw->ntb_ops->read_peer_config)(dev);
+	if (ret < 0)
 		goto err_up;
-	}
-
-	hw->peer_used_mws = (*hw->ntb_ops->spad_read)(dev, SPAD_USED_MWS, 0);
-
-	for (i = 0; i < hw->peer_used_mws; i++) {
-		peer_base_h = (*hw->ntb_ops->spad_read)(dev,
-				SPAD_MW0_BA_H + 2 * i, 0);
-		peer_base_l = (*hw->ntb_ops->spad_read)(dev,
-				SPAD_MW0_BA_L + 2 * i, 0);
-		hw->peer_mw_base[i] = (peer_base_h << 32) + peer_base_l;
-	}
 
 	dev->started = 1;
 
@@ -942,7 +834,7 @@ ntb_dev_stop(struct rte_rawdev *dev)
 	if (!hw->peer_dev_up)
 		goto clean;
 
-	ntb_link_cleanup(dev);
+	(*hw->ntb_ops->link_cleanup)(dev);
 
 	/* Notify the peer that device will be down. */
 	status = (*hw->ntb_ops->peer_db_set)(dev, 1);
@@ -1324,7 +1216,6 @@ ntb_vendor_ops_check(const struct ntb_hw *hw)
 } while (0)
 	NTB_OPS_REQ(ntb_dev_init);
 	NTB_OPS_REQ(get_peer_mw_addr);
-	NTB_OPS_REQ(mw_set_trans);
 	NTB_OPS_REQ(ioremap);
 	NTB_OPS_REQ(get_link_status);
 	NTB_OPS_REQ(set_link);
@@ -1335,6 +1226,11 @@ ntb_vendor_ops_check(const struct ntb_hw *hw)
 	NTB_OPS_REQ(db_set_mask);
 	NTB_OPS_REQ(peer_db_set);
 	NTB_OPS_REQ(vector_bind);
+	NTB_OPS_REQ(dev_handshake);
+	NTB_OPS_REQ(peer_mw_validate);
+	NTB_OPS_REQ(read_peer_config);
+	NTB_OPS_REQ(link_cleanup);
+	NTB_OPS_REQ(mem_align);
 #undef NTB_OPS_REQ
 
 	return 0;
diff --git a/drivers/raw/ntb/ntb.h b/drivers/raw/ntb/ntb.h
index 8c7a2230f9..546677a694 100644
--- a/drivers/raw/ntb/ntb.h
+++ b/drivers/raw/ntb/ntb.h
@@ -88,7 +88,6 @@ enum ntb_spad_idx {
  * NTB device operations
  * @ntb_dev_init: Init ntb dev.
  * @get_peer_mw_addr: To get the addr of peer mw[mw_idx].
- * @mw_set_trans: Set translation of internal memory that remote can access.
  * @ioremap: Translate the remote host address to bar address.
  * @get_link_status: get link status, link speed and link width.
  * @set_link: Set local side up/down.
@@ -100,12 +99,21 @@ enum ntb_spad_idx {
  * for those db bits.
  * @peer_db_set: Set doorbell bit to generate peer interrupt for that bit.
  * @vector_bind: Bind vector source [intr] to msix vector [msix].
+ * @dev_handshake: Advertise the local configuration to the peer, program
+ * the memory-window translations and signal readiness.
+ * @peer_mw_validate: Validate that the peer's advertised memory-window
+ * layout matches the local one.
+ * @read_peer_config: Read and validate the peer configuration at start.
+ * @link_cleanup: Tear down the local scratchpad/translation state so the
+ * peer can no longer reach local memory.
+ * @mem_align: Base-address alignment required for a memory window memzone
+ * of a given length.
+ * @handle_hw_event: Optional. Process vendor-specific hardware events from
+ * the common interrupt handler. May be NULL.
  */
 struct ntb_dev_ops {
 	int (*ntb_dev_init)(const struct rte_rawdev *dev);
 	void *(*get_peer_mw_addr)(const struct rte_rawdev *dev, int mw_idx);
-	int (*mw_set_trans)(const struct rte_rawdev *dev, int mw_idx,
-			    uint64_t addr, uint64_t size);
 	void *(*ioremap)(const struct rte_rawdev *dev, uint64_t addr);
 	int (*get_link_status)(const struct rte_rawdev *dev);
 	int (*set_link)(const struct rte_rawdev *dev, bool up);
@@ -119,6 +127,13 @@ struct ntb_dev_ops {
 	int (*peer_db_set)(const struct rte_rawdev *dev, uint8_t db_bit);
 	int (*vector_bind)(const struct rte_rawdev *dev, uint8_t intr,
 			   uint8_t msix);
+	int (*dev_handshake)(const struct rte_rawdev *dev);
+	int (*peer_mw_validate)(const struct rte_rawdev *dev);
+	int (*read_peer_config)(const struct rte_rawdev *dev);
+	void (*link_cleanup)(const struct rte_rawdev *dev);
+	uint64_t (*mem_align)(const struct rte_rawdev *dev, uint32_t mw_id,
+			      uint64_t mw_len);
+	void (*handle_hw_event)(const struct rte_rawdev *dev);
 };
 
 struct ntb_desc {
diff --git a/drivers/raw/ntb/ntb_hw_intel.c b/drivers/raw/ntb/ntb_hw_intel.c
index 956f411ea3..ed449255dd 100644
--- a/drivers/raw/ntb/ntb_hw_intel.c
+++ b/drivers/raw/ntb/ntb_hw_intel.c
@@ -11,6 +11,7 @@
 #include <bus_pci_driver.h>
 #include <rte_rawdev.h>
 #include <rte_rawdev_pmd.h>
+#include <rte_memzone.h>
 
 #include "ntb.h"
 #include "ntb_hw_intel.h"
@@ -612,11 +613,168 @@ intel_ntb_vector_bind(const struct rte_rawdev *dev, uint8_t intr, uint8_t msix)
 	return 0;
 }
 
+/* Advertise the local configuration to the peer and program the memory
+ * windows. Scratchpad writes and translations only take effect once both
+ * sides are up, so this is also re-run from the ready doorbell.
+ */
+static int
+intel_dev_handshake(const struct rte_rawdev *dev)
+{
+	struct ntb_hw *hw = dev->dev_private;
+	uint32_t val;
+	int ret, i;
+
+	/* Tell peer the mw info of local side. */
+	ret = intel_ntb_spad_write(dev, SPAD_NUM_MWS, 1, hw->mw_cnt);
+	if (ret < 0)
+		return ret;
+	for (i = 0; i < hw->mw_cnt; i++) {
+		NTB_LOG(INFO, "Local %u mw size: 0x%"PRIx64"", i,
+				hw->mw_size[i]);
+		val = hw->mw_size[i] >> 32;
+		ret = intel_ntb_spad_write(dev, SPAD_MW0_SZ_H + 2 * i, 1, val);
+		if (ret < 0)
+			return ret;
+		val = hw->mw_size[i];
+		ret = intel_ntb_spad_write(dev, SPAD_MW0_SZ_L + 2 * i, 1, val);
+		if (ret < 0)
+			return ret;
+	}
+
+	/* Tell peer about the queue info and map memory to the peer. */
+	ret = intel_ntb_spad_write(dev, SPAD_Q_SZ, 1, hw->queue_size);
+	if (ret < 0)
+		return ret;
+	ret = intel_ntb_spad_write(dev, SPAD_NUM_QPS, 1, hw->queue_pairs);
+	if (ret < 0)
+		return ret;
+	ret = intel_ntb_spad_write(dev, SPAD_USED_MWS, 1, hw->used_mw_num);
+	if (ret < 0)
+		return ret;
+	for (i = 0; i < hw->used_mw_num; i++) {
+		val = (uint64_t)(size_t)(hw->mz[i]->addr) >> 32;
+		ret = intel_ntb_spad_write(dev, SPAD_MW0_BA_H + 2 * i, 1, val);
+		if (ret < 0)
+			return ret;
+		val = (uint64_t)(size_t)(hw->mz[i]->addr);
+		ret = intel_ntb_spad_write(dev, SPAD_MW0_BA_L + 2 * i, 1, val);
+		if (ret < 0)
+			return ret;
+	}
+
+	for (i = 0; i < hw->used_mw_num; i++) {
+		ret = intel_ntb_mw_set_trans(dev, i, hw->mz[i]->iova,
+					     hw->mz[i]->len);
+		if (ret < 0)
+			return ret;
+	}
+
+	/* Ring doorbell 0 to tell peer the device is ready. */
+	return intel_ntb_peer_db_set(dev, 0);
+}
+
+/* Check that the peer advertises the same memory-window layout as local. */
+static int
+intel_peer_mw_validate(const struct rte_rawdev *dev)
+{
+	struct ntb_hw *hw = dev->dev_private;
+	uint64_t peer_mw_size;
+	uint32_t val_h, val_l;
+	uint8_t peer_mw_cnt;
+	int i;
+
+	peer_mw_cnt = intel_ntb_spad_read(dev, SPAD_NUM_MWS, 0);
+	if (peer_mw_cnt != hw->mw_cnt) {
+		NTB_LOG(ERR, "Both mw cnt must be the same.");
+		return -EINVAL;
+	}
+
+	for (i = 0; i < hw->mw_cnt; i++) {
+		val_h = intel_ntb_spad_read(dev, SPAD_MW0_SZ_H + 2 * i, 0);
+		val_l = intel_ntb_spad_read(dev, SPAD_MW0_SZ_L + 2 * i, 0);
+		peer_mw_size = ((uint64_t)val_h << 32) | val_l;
+		NTB_LOG(DEBUG, "Peer %u mw size: 0x%"PRIx64"", i, peer_mw_size);
+		if (peer_mw_size != hw->mw_size[i]) {
+			NTB_LOG(ERR, "Mw config must be the same.");
+			return -EINVAL;
+		}
+	}
+
+	return 0;
+}
+
+/* Read and validate the peer configuration recorded in the scratchpad. */
+static int
+intel_read_peer_config(const struct rte_rawdev *dev)
+{
+	struct ntb_hw *hw = dev->dev_private;
+	uint32_t peer_base_l, peer_val;
+	uint64_t peer_base_h;
+	int i;
+
+	peer_val = intel_ntb_spad_read(dev, SPAD_Q_SZ, 0);
+	if (peer_val != hw->queue_size) {
+		NTB_LOG(ERR, "Inconsistent queue size! (local: %u peer: %u)",
+			hw->queue_size, peer_val);
+		return -EINVAL;
+	}
+
+	peer_val = intel_ntb_spad_read(dev, SPAD_NUM_QPS, 0);
+	if (peer_val != hw->queue_pairs) {
+		NTB_LOG(ERR, "Inconsistent number of queues! (local: %u peer:"
+			" %u)", hw->queue_pairs, peer_val);
+		return -EINVAL;
+	}
+
+	hw->peer_used_mws = intel_ntb_spad_read(dev, SPAD_USED_MWS, 0);
+
+	for (i = 0; i < hw->peer_used_mws; i++) {
+		peer_base_h = intel_ntb_spad_read(dev, SPAD_MW0_BA_H + 2 * i, 0);
+		peer_base_l = intel_ntb_spad_read(dev, SPAD_MW0_BA_L + 2 * i, 0);
+		hw->peer_mw_base[i] = (peer_base_h << 32) + peer_base_l;
+	}
+
+	return 0;
+}
+
+/* Tear down the local scratchpad and translations so the peer can no
+ * longer reach local memory.
+ */
+static void
+intel_link_cleanup(const struct rte_rawdev *dev)
+{
+	struct ntb_hw *hw = dev->dev_private;
+	int status, i;
+
+	/* Clean spad registers. */
+	for (i = 0; i < hw->spad_cnt; i++) {
+		status = intel_ntb_spad_write(dev, i, 0, 0);
+		if (status)
+			NTB_LOG(ERR, "Failed to clean local spad.");
+	}
+
+	/* Clear mw so that peer cannot access local memory. */
+	for (i = 0; i < hw->used_mw_num; i++) {
+		status = intel_ntb_mw_set_trans(dev, i, 0, 0);
+		if (status)
+			NTB_LOG(ERR, "Failed to clean mw.");
+	}
+}
+
+static uint64_t
+intel_ntb_get_mem_align(const struct rte_rawdev *dev, uint32_t mw_id,
+			uint64_t mw_len __rte_unused)
+{
+	struct ntb_hw *hw = dev->dev_private;
+
+	/* The memzone base must be aligned to the memory window size. */
+	return hw->mw_size[mw_id];
+}
+
 /* operations for primary side of local ntb */
 const struct ntb_dev_ops intel_ntb_ops = {
 	.ntb_dev_init       = intel_ntb_dev_init,
 	.get_peer_mw_addr   = intel_ntb_get_peer_mw_addr,
-	.mw_set_trans       = intel_ntb_mw_set_trans,
 	.ioremap            = intel_ntb_ioremap,
 	.get_link_status    = intel_ntb_get_link_status,
 	.set_link           = intel_ntb_set_link,
@@ -627,4 +785,9 @@ const struct ntb_dev_ops intel_ntb_ops = {
 	.db_set_mask        = intel_ntb_db_set_mask,
 	.peer_db_set        = intel_ntb_peer_db_set,
 	.vector_bind        = intel_ntb_vector_bind,
+	.dev_handshake      = intel_dev_handshake,
+	.peer_mw_validate   = intel_peer_mw_validate,
+	.read_peer_config   = intel_read_peer_config,
+	.link_cleanup       = intel_link_cleanup,
+	.mem_align          = intel_ntb_get_mem_align,
 };
diff --git a/drivers/raw/ntb/rte_pmd_ntb.h b/drivers/raw/ntb/rte_pmd_ntb.h
index 76da3be026..73e4628b28 100644
--- a/drivers/raw/ntb/rte_pmd_ntb.h
+++ b/drivers/raw/ntb/rte_pmd_ntb.h
@@ -7,6 +7,8 @@
 
 #include <stdint.h>
 
+#include <rte_compat.h>
+
 /* App needs to set/get these attrs */
 #define NTB_QUEUE_SZ_NAME           "queue_size"
 #define NTB_QUEUE_NUM_NAME          "queue_num"
@@ -23,8 +25,6 @@
 
 struct ntb_dev_info {
 	uint32_t ntb_hdr_size;
-	/**< memzone needs to be mw size align or not. */
-	uint8_t mw_size_align;
 	uint8_t mw_cnt;
 	uint64_t *mw_size;
 };
@@ -42,4 +42,28 @@ struct ntb_queue_conf {
 	struct rte_mempool *rx_mp;
 };
 
+/**
+ * @warning
+ * @b EXPERIMENTAL: this API may change without prior notice.
+ *
+ * Get the base-address alignment a memory window memzone must be reserved
+ * with. Some NTB hardware constrains the address a memory window can be
+ * translated to (for example, hardware that forms the peer target as
+ * (base | offset) needs the base aligned to a power of two >= the window
+ * length). Applications should reserve the memzone for memory window
+ * @p mw_id, of length @p mw_len, with at least the returned alignment.
+ *
+ * @param dev_id
+ *   The identifier of the raw device.
+ * @param mw_id
+ *   The memory window index.
+ * @param mw_len
+ *   The length, in bytes, of the memzone to be reserved.
+ * @return
+ *   The required base-address alignment in bytes, or 0 on error.
+ */
+__rte_experimental
+uint64_t
+rte_pmd_ntb_get_mem_align(uint16_t dev_id, uint32_t mw_id, uint64_t mw_len);
+
 #endif /* _RTE_PMD_NTB_H_ */
diff --git a/examples/ntb/ntb_fwd.c b/examples/ntb/ntb_fwd.c
index bbacce97db..9168f299c6 100644
--- a/examples/ntb/ntb_fwd.c
+++ b/examples/ntb/ntb_fwd.c
@@ -1150,8 +1150,6 @@ ntb_mbuf_pool_create(uint16_t mbuf_seg_size, uint32_t nb_mbuf,
 		if (!left_sz)
 			break;
 		snprintf(mz_name, sizeof(mz_name), "ntb_mw_%d", mz_id);
-		align = ntb_info.mw_size_align ? ntb_info.mw_size[mz_id] :
-			RTE_CACHE_LINE_SIZE;
 		/* Reserve ntb header space on memzone 0. */
 		max_mz_len = mz_id ? ntb_info.mw_size[mz_id] :
 			     ntb_info.mw_size[mz_id] - ntb_info.ntb_hdr_size;
@@ -1159,6 +1157,10 @@ ntb_mbuf_pool_create(uint16_t mbuf_seg_size, uint32_t nb_mbuf,
 			(max_mz_len / total_elt_sz * total_elt_sz);
 		if (!mz_len)
 			continue;
+		/* Let the driver report the base-address alignment its
+		 * hardware needs for a memory window of this length.
+		 */
+		align = rte_pmd_ntb_get_mem_align(dev_id, mz_id, mz_len);
 		mz = rte_memzone_reserve_aligned(mz_name, mz_len, socket_id,
 					RTE_MEMZONE_IOVA_CONTIG, align);
 		if (mz == NULL) {
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 7+ messages in thread

* [PATCH v4 3/3] raw/ntb: add AMD NTB support
  2026-10-26  8:55 [PATCH v4 0/3] raw/ntb: add AMD NTB support Raghavendra Ningoji
  2026-10-26  8:55 ` [PATCH v4 1/3] raw/ntb: validate vendor ops at probe Raghavendra Ningoji
  2026-10-26  8:55 ` [PATCH v4 2/3] raw/ntb: generalize framework for multiple vendors Raghavendra Ningoji
@ 2026-10-26  8:55 ` Raghavendra Ningoji
  2026-10-26 15:26 ` [PATCH v4 0/3] " David Marchand
  3 siblings, 0 replies; 7+ messages in thread
From: Raghavendra Ningoji @ 2026-10-26  8:55 UTC (permalink / raw)
  To: dev
  Cc: jingjing.wu, thomas, selwin.sebastian, bhagyada.modali,
	bruce.richardson, david.marchand, sachin.saxena, hemant.agrawal,
	Raghavendra Ningoji

Add support for the NTB endpoints integrated in AMD EPYC Embedded
"Turin", "Genoa" and "Siena" processors to the raw/ntb driver.

The AMD NTB uses a primary/secondary topology: one endpoint enumerates
as the primary (device ID 0x14c0) and the other as the secondary
(device ID 0x14c3). The hardware exposes two memory windows (BAR23 and
BAR45), 16 doorbells and a single shared 16-register scratchpad bank.
The scratchpad bank is split into two disjoint 8-register sets, one per
side, so the driver uses a packed handshake layout that fits in 8
registers, plugged in through the framework's dev_handshake,
peer_mw_validate, read_peer_config and link_cleanup ops. Vendor-specific
hardware (SMU) events are handled through the optional handle_hw_event
op, and a memory-window alignment helper through the mem_align op.

The AMD NTB uses an outbound translation window: a write to a BARxx
memory window offset is forwarded to (xlat_base | offset) in the peer's
memory rather than (xlat_base + offset). For that to be correct the
translation base must be aligned to a power of two >= the window length
so that no offset bit collides with a set bit in the base; the XLAT
register also requires at least 4K alignment. The mem_align op reports
this requirement through rte_pmd_ntb_get_mem_align(), and
amd_ntb_mw_set_trans() rejects a misaligned base.

On the secondary side the device's own PCIe link status does not
reflect the true inter-host link, so the link speed and width are read
from the upstream switch port via sysfs.

Update the documentation, release notes and MAINTAINERS accordingly.

Signed-off-by: Raghavendra Ningoji <raghavendra.ningoji@amd.com>
---
 MAINTAINERS                            |   2 +
 doc/guides/rawdevs/ntb.rst             |  31 +-
 doc/guides/rel_notes/release_26_11.rst |   8 +
 drivers/raw/ntb/meson.build            |   7 +-
 drivers/raw/ntb/ntb.c                  |   7 +
 drivers/raw/ntb/ntb.h                  |   3 +
 drivers/raw/ntb/ntb_hw_amd.c           | 658 +++++++++++++++++++++++++
 drivers/raw/ntb/ntb_hw_amd.h           | 111 +++++
 usertools/dpdk-devbind.py              |   1 +
 9 files changed, 824 insertions(+), 4 deletions(-)
 create mode 100644 drivers/raw/ntb/ntb_hw_amd.c
 create mode 100644 drivers/raw/ntb/ntb_hw_amd.h

diff --git a/MAINTAINERS b/MAINTAINERS
index 1720cff62e..cc86b35b29 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1616,6 +1616,8 @@ F: doc/guides/rawdevs/cnxk_rvu_lf.rst
 
 NTB
 M: Jingjing Wu <jingjing.wu@intel.com>
+M: Bhagyada Modali <bhagyada.modali@amd.com>
+M: Raghavendra Ningoji <raghavendra.ningoji@amd.com>
 F: drivers/raw/ntb/
 F: doc/guides/rawdevs/ntb.rst
 F: examples/ntb/
diff --git a/doc/guides/rawdevs/ntb.rst b/doc/guides/rawdevs/ntb.rst
index 3721b4880e..db371f5728 100644
--- a/doc/guides/rawdevs/ntb.rst
+++ b/doc/guides/rawdevs/ntb.rst
@@ -37,10 +37,36 @@ then reboot.
 - Set ``PCIe PLL SSC (Spread Spectrum Clocking)`` as ``Disabled``, on both hosts.
   This is a hardware requirement when using Re-timer Cards.
 
+AMD EPYC Embedded NTB
+---------------------
+
+The driver also supports the NTB endpoints integrated in AMD EPYC Embedded
+"Turin", "Genoa" and "Siena" processors. These use a primary/secondary
+topology rather than the Intel back-to-back topology: one endpoint is
+enumerated as the primary (device ID ``0x14c0``) and the other as the
+secondary (device ID ``0x14c3``). The BIOS on both systems performs NTB link
+training; no additional NTB-specific BIOS options are required beyond enabling
+the NTB endpoints.
+
+The AMD NTB hardware exposes two memory windows (BAR23 and BAR45), 16
+doorbells and a single shared 16-register scratchpad bank. The scratchpad
+bank is split into two disjoint 8-register sets, one owned by each side, so
+the driver uses a packed handshake layout that fits within 8 registers.
+
+The AMD NTB uses an outbound translation window: a write to a memory window
+offset is forwarded to ``(base | offset)`` in the peer's memory rather than
+``(base + offset)``. The translation base must therefore be aligned to a
+power of two greater than or equal to the window length so that no offset
+bit collides with a set bit in the base. Different NTB hardware imposes
+different alignment constraints, so applications should not assume any
+particular value: reserve each memory window memzone with the alignment
+returned by ``rte_pmd_ntb_get_mem_align()`` for that window and length. The
+``ntb_fwd`` example shows this usage.
+
 Device Setup
 ------------
 
-The Intel NTB devices need to be bound to a DPDK-supported kernel driver
+The NTB devices need to be bound to a DPDK-supported kernel driver
 to use, i.e. igb_uio, vfio. The ``dpdk-devbind.py`` script can be used to
 show devices status and to bind them to a suitable kernel driver. They will
 appear under the category of "Misc (rawdev) devices".
@@ -151,8 +177,9 @@ like the following:
 Limitation
 ----------
 
-This PMD is only supported on Intel Xeon Platforms:
+This PMD is supported on:
 
+- AMD EPYC Embedded "Turin", "Genoa" and "Siena" processors.
 - 4th Generation Intel® Xeon® Scalable Processors.
 - 3rd Generation Intel® Xeon® Scalable Processors.
 - 2nd Generation Intel® Xeon® Scalable Processors.
diff --git a/doc/guides/rel_notes/release_26_11.rst b/doc/guides/rel_notes/release_26_11.rst
index 30f2f3009f..8a6977db33 100644
--- a/doc/guides/rel_notes/release_26_11.rst
+++ b/doc/guides/rel_notes/release_26_11.rst
@@ -264,6 +264,14 @@ New Features
   Added ``dpdk-validate-bpf`` tool to pre-validate eBPF programs
   for compatibility with the ``lib/bpf`` execution context.
 
+* **Added AMD NTB support to the NTB rawdev driver.**
+
+  Added support for the NTB endpoints integrated in AMD EPYC Embedded
+  "Turin", "Genoa" and "Siena" processors to the ``raw/ntb`` driver.
+  The NTB rawdev framework was generalized to support multiple vendors,
+  with AMD-specific hardware access, a primary/secondary topology and a
+  packed scratchpad handshake.
+
 
 Removed Items
 -------------
diff --git a/drivers/raw/ntb/meson.build b/drivers/raw/ntb/meson.build
index 9096f2b25a..24a46836aa 100644
--- a/drivers/raw/ntb/meson.build
+++ b/drivers/raw/ntb/meson.build
@@ -2,6 +2,9 @@
 # Copyright(c) 2019 Intel Corporation.
 
 deps += ['rawdev', 'mbuf', 'mempool', 'pci', 'bus_pci']
-sources = files('ntb.c',
-                'ntb_hw_intel.c')
+sources = files(
+        'ntb.c',
+        'ntb_hw_amd.c',
+        'ntb_hw_intel.c',
+)
 headers = files('rte_pmd_ntb.h')
diff --git a/drivers/raw/ntb/ntb.c b/drivers/raw/ntb/ntb.c
index 0e0cdf38e3..ac6abf566b 100644
--- a/drivers/raw/ntb/ntb.c
+++ b/drivers/raw/ntb/ntb.c
@@ -20,11 +20,14 @@
 #include <rte_rawdev_pmd.h>
 #include <eal_export.h>
 
+#include "ntb_hw_amd.h"
 #include "ntb_hw_intel.h"
 #include "rte_pmd_ntb.h"
 #include "ntb.h"
 
 static const struct rte_pci_id pci_id_ntb_map[] = {
+	{ RTE_PCI_DEVICE(NTB_AMD_VENDOR_ID, NTB_AMD_DEV_ID_PRI) },
+	{ RTE_PCI_DEVICE(NTB_AMD_VENDOR_ID, NTB_AMD_DEV_ID_SEC) },
 	{ RTE_PCI_DEVICE(NTB_INTEL_VENDOR_ID, NTB_INTEL_DEV_ID_B2B_SKX) },
 	{ RTE_PCI_DEVICE(NTB_INTEL_VENDOR_ID, NTB_INTEL_DEV_ID_B2B_ICX) },
 	{ .vendor_id = 0, /* sentinel */ },
@@ -1250,6 +1253,10 @@ ntb_init_hw(struct rte_rawdev *dev, struct rte_pci_device *pci_dev)
 	hw->link_width = NTB_WIDTH_NONE;
 
 	switch (pci_dev->id.device_id) {
+	case NTB_AMD_DEV_ID_PRI:
+	case NTB_AMD_DEV_ID_SEC:
+		hw->ntb_ops = &amd_ntb_ops;
+		break;
 	case NTB_INTEL_DEV_ID_B2B_SKX:
 	case NTB_INTEL_DEV_ID_B2B_ICX:
 		hw->ntb_ops = &intel_ntb_ops;
diff --git a/drivers/raw/ntb/ntb.h b/drivers/raw/ntb/ntb.h
index 546677a694..f6bfc6515e 100644
--- a/drivers/raw/ntb/ntb.h
+++ b/drivers/raw/ntb/ntb.h
@@ -42,6 +42,9 @@ enum ntb_topo {
 	NTB_TOPO_NONE = 0,
 	NTB_TOPO_B2B_USD,
 	NTB_TOPO_B2B_DSD,
+	/* Primary/secondary topology (e.g. AMD NTB). */
+	NTB_TOPO_PRI,
+	NTB_TOPO_SEC,
 };
 
 enum ntb_link {
diff --git a/drivers/raw/ntb/ntb_hw_amd.c b/drivers/raw/ntb/ntb_hw_amd.c
new file mode 100644
index 0000000000..3f83f2e15c
--- /dev/null
+++ b/drivers/raw/ntb/ntb_hw_amd.c
@@ -0,0 +1,658 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2026 Advanced Micro Devices, Inc.
+ */
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <limits.h>
+
+#include <rte_common.h>
+#include <rte_io.h>
+#include <rte_eal.h>
+#include <rte_pci.h>
+#include <bus_pci_driver.h>
+#include <rte_rawdev.h>
+#include <rte_rawdev_pmd.h>
+#include <rte_malloc.h>
+#include <rte_memzone.h>
+
+#include "ntb.h"
+#include "ntb_hw_amd.h"
+
+static enum amd_ntb_bar amd_ntb_bar[] = {
+	AMD_NTB_BAR23,
+	AMD_NTB_BAR45,
+};
+
+static int
+amd_ntb_dev_init(const struct rte_rawdev *dev)
+{
+	struct ntb_hw *ntb = dev->dev_private;
+	uint32_t sideinfo;
+	int i, bar;
+
+	if (ntb == NULL) {
+		NTB_LOG(ERR, "Invalid device.");
+		return -EINVAL;
+	}
+
+	ntb->hw_addr = (char *)ntb->pci_dev->mem_resource[0].addr;
+
+	/* Bit 0 of the link status register carries the topology. */
+	sideinfo = rte_read32((char *)ntb->hw_addr + AMD_SIDEINFO_OFFSET);
+	if (sideinfo & AMD_SIDE_MASK)
+		ntb->topo = NTB_TOPO_SEC;
+	else
+		ntb->topo = NTB_TOPO_PRI;
+
+	NTB_LOG(INFO, "Device topology: %s (sideinfo 0x%" PRIx32 ")",
+		ntb->topo == NTB_TOPO_SEC ? "secondary" : "primary", sideinfo);
+
+	ntb->mw_cnt = AMD_MW_COUNT;
+	ntb->db_cnt = AMD_DB_COUNT;
+	/* The 16-register scratchpad bank is split into two halves, so only
+	 * AMD_SPAD_PER_SIDE registers are usable per side.
+	 */
+	ntb->spad_cnt = AMD_SPAD_PER_SIDE;
+
+	ntb->mw_size = rte_zmalloc("ntb_mw_size",
+				   ntb->mw_cnt * sizeof(uint64_t), 0);
+	if (ntb->mw_size == NULL) {
+		NTB_LOG(ERR, "Cannot allocate memory for mw size.");
+		return -ENOMEM;
+	}
+	for (i = 0; i < ntb->mw_cnt; i++) {
+		bar = amd_ntb_bar[i];
+		ntb->mw_size[i] = ntb->pci_dev->mem_resource[bar].len;
+		NTB_LOG(INFO, "mw[%d] bar%d size 0x%" PRIx64, i, bar,
+			ntb->mw_size[i]);
+	}
+
+	/* Reserve the last 2 scratchpad registers for application use. */
+	for (i = 0; i < NTB_SPAD_USER_MAX_NUM; i++)
+		ntb->spad_user_list[i] = ntb->spad_cnt;
+	ntb->spad_user_list[0] = ntb->spad_cnt - 2;
+	ntb->spad_user_list[1] = ntb->spad_cnt - 1;
+
+	return 0;
+}
+
+static void *
+amd_ntb_get_peer_mw_addr(const struct rte_rawdev *dev, int mw_idx)
+{
+	struct ntb_hw *ntb = dev->dev_private;
+
+	if (mw_idx < 0 || mw_idx >= ntb->mw_cnt) {
+		NTB_LOG(ERR, "Invalid memory window index (0 - %u).",
+			ntb->mw_cnt - 1);
+		return NULL;
+	}
+
+	return ntb->pci_dev->mem_resource[amd_ntb_bar[mw_idx]].addr;
+}
+
+static int
+amd_ntb_mw_set_trans(const struct rte_rawdev *dev, int mw_idx,
+		     uint64_t addr, uint64_t size)
+{
+	struct ntb_hw *ntb = dev->dev_private;
+	void *peer_mmio = AMD_PEER_MMIO(ntb);
+	uint32_t xlat_off, limit_off;
+	uint64_t mw_size, reg_val;
+	uint8_t bar;
+
+	if (mw_idx < 0 || mw_idx >= ntb->mw_cnt) {
+		NTB_LOG(ERR, "Invalid memory window index (0 - %u).",
+			ntb->mw_cnt - 1);
+		return -EINVAL;
+	}
+
+	bar = amd_ntb_bar[mw_idx];
+	mw_size = ntb->pci_dev->mem_resource[bar].len;
+	if (size > mw_size) {
+		NTB_LOG(ERR, "Set translation size 0x%" PRIx64 " exceeds mw "
+			"size 0x%" PRIx64, size, mw_size);
+		return -EINVAL;
+	}
+
+	/* Outbound translation forms the target as (base | offset), so a
+	 * misaligned base aliases writes (see mem_align op). Reject it early.
+	 */
+	if (addr & (rte_align64pow2(size) - 1)) {
+		NTB_LOG(ERR, "mw%d translation base 0x%" PRIx64 " is not "
+			"aligned to a power of two >= size 0x%" PRIx64
+			"; window writes would alias.", mw_idx, addr, size);
+		return -EINVAL;
+	}
+
+	/* Program the peer's outbound translation window (S* registers) so
+	 * that peer writes into its BAR land in our local memory at 'addr'.
+	 */
+	if (mw_idx == 0) {
+		xlat_off = AMD_BAR23_XLAT_OFFSET;
+		limit_off = AMD_BAR23_LIMIT_OFFSET;
+	} else {
+		xlat_off = AMD_BAR45_XLAT_OFFSET;
+		limit_off = AMD_BAR45_LIMIT_OFFSET;
+	}
+
+	rte_write64(addr, (char *)peer_mmio + xlat_off);
+	reg_val = rte_read64((char *)peer_mmio + xlat_off);
+	if (reg_val != addr) {
+		NTB_LOG(ERR, "Failed to set mw%d translation.", mw_idx);
+		rte_write64(0, (char *)peer_mmio + xlat_off);
+		return -EIO;
+	}
+
+	rte_write64(size, (char *)peer_mmio + limit_off);
+	reg_val = rte_read64((char *)peer_mmio + limit_off);
+	if (reg_val != size) {
+		NTB_LOG(ERR, "Failed to set mw%d limit.", mw_idx);
+		rte_write64(0, (char *)peer_mmio + xlat_off);
+		rte_write64(0, (char *)peer_mmio + limit_off);
+		return -EIO;
+	}
+
+	return 0;
+}
+
+static void *
+amd_ntb_ioremap(const struct rte_rawdev *dev, uint64_t addr)
+{
+	struct ntb_hw *ntb = dev->dev_private;
+	void *mapped = NULL;
+	void *base;
+	int i;
+
+	for (i = 0; i < ntb->peer_used_mws; i++) {
+		if (addr >= ntb->peer_mw_base[i] &&
+		    addr <= ntb->peer_mw_base[i] + ntb->mw_size[i]) {
+			base = amd_ntb_get_peer_mw_addr(dev, i);
+			mapped = (void *)(size_t)(addr - ntb->peer_mw_base[i] +
+						  (size_t)base);
+			break;
+		}
+	}
+
+	return mapped;
+}
+
+/* Read link speed/width from the device's PCIe capability link status. */
+static int
+amd_ntb_read_pcie_link_status(struct ntb_hw *ntb, uint16_t *link_status)
+{
+	struct rte_pci_device *pci_dev = ntb->pci_dev;
+	uint8_t pos, cap_id, next;
+	uint16_t status;
+	int ret;
+
+	ret = rte_pci_read_config(pci_dev, &status, sizeof(status),
+				  RTE_PCI_STATUS);
+	if (ret != sizeof(status))
+		return -EIO;
+	if (!(status & RTE_PCI_STATUS_CAP_LIST))
+		return -ENOTSUP;
+
+	ret = rte_pci_read_config(pci_dev, &pos, sizeof(pos),
+				  RTE_PCI_CAPABILITY_LIST);
+	if (ret != sizeof(pos))
+		return -EIO;
+
+	while (pos) {
+		ret = rte_pci_read_config(pci_dev, &cap_id, sizeof(cap_id),
+					  pos);
+		if (ret != sizeof(cap_id))
+			return -EIO;
+		ret = rte_pci_read_config(pci_dev, &next, sizeof(next),
+					  pos + 1);
+		if (ret != sizeof(next))
+			return -EIO;
+		if (cap_id == RTE_PCI_CAP_ID_EXP) {
+			ret = rte_pci_read_config(pci_dev, link_status,
+						  sizeof(*link_status),
+						  pos + RTE_PCI_EXP_LNKSTA);
+			if (ret != sizeof(*link_status))
+				return -EIO;
+			return 0;
+		}
+		pos = next;
+	}
+
+	return -ENOTSUP;
+}
+
+/* Read the PCIe link status (LNKSTA) from an arbitrary device's config space
+ * exposed via sysfs. Used to query bridges that are not bound to this driver
+ * (e.g. the upstream switch above a secondary-side NTB), which rte_pci_*
+ * cannot access directly.
+ */
+static int
+amd_ntb_read_lnksta_sysfs(const char *config_path, uint16_t *link_status)
+{
+	uint8_t pos, cap_id, next;
+	uint16_t status;
+	int fd, ret = -EIO;
+
+	fd = open(config_path, O_RDONLY);
+	if (fd < 0)
+		return -errno;
+
+	if (pread(fd, &status, sizeof(status), RTE_PCI_STATUS) !=
+	    sizeof(status))
+		goto out;
+	if (!(status & RTE_PCI_STATUS_CAP_LIST))
+		goto out;
+
+	if (pread(fd, &pos, sizeof(pos), RTE_PCI_CAPABILITY_LIST) !=
+	    sizeof(pos))
+		goto out;
+
+	while (pos) {
+		if (pread(fd, &cap_id, sizeof(cap_id), pos) != sizeof(cap_id))
+			goto out;
+		if (pread(fd, &next, sizeof(next), pos + 1) != sizeof(next))
+			goto out;
+		if (cap_id == RTE_PCI_CAP_ID_EXP) {
+			if (pread(fd, link_status, sizeof(*link_status),
+				  pos + RTE_PCI_EXP_LNKSTA) ==
+			    sizeof(*link_status))
+				ret = 0;
+			goto out;
+		}
+		pos = next;
+	}
+out:
+	close(fd);
+	return ret;
+}
+
+/* On the secondary side the NTB device's own PCIe link status does not reflect
+ * the inter-host link. Mirror the Linux amd_ntb behaviour by walking up two
+ * bridge levels (device -> downstream switch -> upstream switch) and reading
+ * the upstream switch port's link status instead.
+ */
+static int
+amd_ntb_read_upstream_link_status(struct ntb_hw *ntb, uint16_t *link_status)
+{
+	char path[PATH_MAX];
+	char real[PATH_MAX];
+	char *p;
+	int i;
+
+	snprintf(path, sizeof(path),
+		 "/sys/bus/pci/devices/%04x:%02x:%02x.%x",
+		 ntb->pci_dev->addr.domain, ntb->pci_dev->addr.bus,
+		 ntb->pci_dev->addr.devid, ntb->pci_dev->addr.function);
+
+	if (realpath(path, real) == NULL)
+		return -errno;
+
+	/* Strip two trailing path components to reach the upstream switch. */
+	for (i = 0; i < 2; i++) {
+		p = strrchr(real, '/');
+		if (p == NULL || p == real)
+			return -ENOENT;
+		*p = '\0';
+	}
+
+	snprintf(path, sizeof(path), "%s/config", real);
+	return amd_ntb_read_lnksta_sysfs(path, link_status);
+}
+
+static int
+amd_ntb_get_link_status(const struct rte_rawdev *dev)
+{
+	struct ntb_hw *ntb = dev->dev_private;
+	uint16_t link_status = 0;
+	uint32_t sideinfo;
+	int ret;
+
+	/* The link is usable once the peer has set its SIDE_READY bit. */
+	sideinfo = rte_read32((char *)AMD_PEER_MMIO(ntb) + AMD_SIDEINFO_OFFSET);
+	ntb->link_status = !!(sideinfo & AMD_SIDE_READY);
+
+	if (!ntb->link_status) {
+		ntb->link_speed = NTB_SPEED_NONE;
+		ntb->link_width = NTB_WIDTH_NONE;
+		return 0;
+	}
+
+	/* The primary reads its own PCIe link status; the secondary must read
+	 * the upstream switch port above it. If the upstream read fails, fall
+	 * back to the local device so speed/width is still best-effort.
+	 */
+	if (ntb->topo == NTB_TOPO_SEC) {
+		ret = amd_ntb_read_upstream_link_status(ntb, &link_status);
+		if (ret != 0)
+			ret = amd_ntb_read_pcie_link_status(ntb, &link_status);
+	} else {
+		ret = amd_ntb_read_pcie_link_status(ntb, &link_status);
+	}
+
+	if (ret == 0) {
+		ntb->link_speed = AMD_LNK_STA_SPEED(link_status);
+		ntb->link_width = AMD_LNK_STA_WIDTH(link_status);
+	} else {
+		NTB_LOG(WARNING, "Failed to read PCIe link status (%d).", ret);
+		ntb->link_speed = NTB_SPEED_NONE;
+		ntb->link_width = NTB_WIDTH_NONE;
+	}
+
+	return 0;
+}
+
+static int
+amd_ntb_set_link(const struct rte_rawdev *dev, bool up)
+{
+	struct ntb_hw *ntb = dev->dev_private;
+	void *mmio = ntb->hw_addr;
+	uint32_t reg;
+
+	reg = rte_read32((char *)mmio + AMD_SIDEINFO_OFFSET);
+	if (up) {
+		if (!(reg & AMD_SIDE_READY)) {
+			reg |= AMD_SIDE_READY;
+			rte_write32(reg, (char *)mmio + AMD_SIDEINFO_OFFSET);
+		}
+		reg = rte_read32((char *)mmio + AMD_CNTL_OFFSET);
+		reg |= (AMD_PMM_REG_CTL | AMD_SMM_REG_CTL);
+		rte_write32(reg, (char *)mmio + AMD_CNTL_OFFSET);
+	} else {
+		if (reg & AMD_SIDE_READY) {
+			reg &= ~AMD_SIDE_READY;
+			rte_write32(reg, (char *)mmio + AMD_SIDEINFO_OFFSET);
+		}
+		reg = rte_read32((char *)mmio + AMD_CNTL_OFFSET);
+		reg &= ~(AMD_PMM_REG_CTL | AMD_SMM_REG_CTL);
+		rte_write32(reg, (char *)mmio + AMD_CNTL_OFFSET);
+	}
+
+	return 0;
+}
+
+static uint32_t
+amd_ntb_spad_read(const struct rte_rawdev *dev, int spad, bool peer)
+{
+	struct ntb_hw *ntb = dev->dev_private;
+	uint32_t offset;
+
+	if (spad < 0 || spad >= ntb->spad_cnt) {
+		NTB_LOG(ERR, "Invalid scratchpad index.");
+		return 0;
+	}
+
+	offset = AMD_SPAD_BASE(ntb, peer);
+
+	return rte_read32((char *)ntb->hw_addr + AMD_SPAD_OFFSET + offset +
+			  (spad << 2));
+}
+
+static int
+amd_ntb_spad_write(const struct rte_rawdev *dev, int spad,
+		   bool peer, uint32_t spad_v)
+{
+	struct ntb_hw *ntb = dev->dev_private;
+	uint32_t offset;
+
+	if (spad < 0 || spad >= ntb->spad_cnt) {
+		NTB_LOG(ERR, "Invalid scratchpad index.");
+		return -EINVAL;
+	}
+
+	offset = AMD_SPAD_BASE(ntb, peer);
+
+	rte_write32(spad_v, (char *)ntb->hw_addr + AMD_SPAD_OFFSET + offset +
+		    (spad << 2));
+
+	return 0;
+}
+
+static uint64_t
+amd_ntb_db_read(const struct rte_rawdev *dev)
+{
+	struct ntb_hw *ntb = dev->dev_private;
+
+	return (uint64_t)rte_read16((char *)ntb->hw_addr +
+				    AMD_DBSTAT_OFFSET);
+}
+
+static int
+amd_ntb_db_clear(const struct rte_rawdev *dev, uint64_t db_bits)
+{
+	struct ntb_hw *ntb = dev->dev_private;
+
+	rte_write16((uint16_t)db_bits, (char *)ntb->hw_addr +
+		    AMD_DBSTAT_OFFSET);
+
+	return 0;
+}
+
+static int
+amd_ntb_db_set_mask(const struct rte_rawdev *dev, uint64_t db_mask)
+{
+	struct ntb_hw *ntb = dev->dev_private;
+
+	if (db_mask & ~ntb->db_valid_mask)
+		return -EINVAL;
+
+	ntb->db_mask |= db_mask;
+	rte_write16((uint16_t)ntb->db_mask, (char *)ntb->hw_addr +
+		    AMD_DBMASK_OFFSET);
+
+	return 0;
+}
+
+static int
+amd_ntb_peer_db_set(const struct rte_rawdev *dev, uint8_t db_idx)
+{
+	struct ntb_hw *ntb = dev->dev_private;
+
+	if (((uint64_t)1 << db_idx) & ~ntb->db_valid_mask) {
+		NTB_LOG(ERR, "Invalid doorbell.");
+		return -EINVAL;
+	}
+
+	rte_write16((uint16_t)1 << db_idx, (char *)ntb->hw_addr +
+		    AMD_DBREQ_OFFSET);
+
+	return 0;
+}
+
+static int
+amd_ntb_vector_bind(const struct rte_rawdev *dev __rte_unused,
+		    uint8_t intr __rte_unused, uint8_t msix __rte_unused)
+{
+	/* Each doorbell/event maps to its MSI-X vector by default. */
+	return 0;
+}
+
+/* Handshake: advertises the local configuration to the peer using the
+ * packed 8-register scratchpad layout, programs the peer's outbound
+ * translation windows and rings doorbell 0 to signal readiness.
+ */
+static int
+amd_dev_handshake(const struct rte_rawdev *dev)
+{
+	struct ntb_hw *ntb = dev->dev_private;
+	uint32_t info;
+	uint64_t base;
+	int i, ret;
+
+	info = (ntb->mw_cnt & 0xff) |
+	       ((uint32_t)(ntb->queue_pairs & 0xff) << 8) |
+	       ((uint32_t)(ntb->used_mw_num & 0xff) << 16);
+	ret = amd_ntb_spad_write(dev, AMD_SPAD_CNT_INFO, 1, info);
+	if (ret < 0)
+		return ret;
+
+	ret = amd_ntb_spad_write(dev, AMD_SPAD_QUEUE_SZ, 1, ntb->queue_size);
+	if (ret < 0)
+		return ret;
+
+	for (i = 0; i < ntb->used_mw_num; i++) {
+		/* Advertise the memzone virtual base (used by ioremap on the
+		 * peer) and program the translation with the IOVA.
+		 */
+		base = (uint64_t)(size_t)ntb->mz[i]->addr;
+		ret = amd_ntb_spad_write(dev, AMD_SPAD_MW0_BA_L + 2 * i, 1,
+					 (uint32_t)base);
+		if (ret < 0)
+			return ret;
+		ret = amd_ntb_spad_write(dev, AMD_SPAD_MW0_BA_H + 2 * i, 1,
+					 (uint32_t)(base >> 32));
+		if (ret < 0)
+			return ret;
+	}
+
+	for (i = 0; i < ntb->used_mw_num; i++) {
+		ret = amd_ntb_mw_set_trans(dev, i, ntb->mz[i]->iova,
+					   ntb->mz[i]->len);
+		if (ret < 0)
+			return ret;
+	}
+
+	/* Ring doorbell 0 to tell the peer the device is ready. */
+	return amd_ntb_peer_db_set(dev, 0);
+}
+
+/* Peer-config read at device start. Validates the peer's queue
+ * configuration and records the peer memory-window base addresses.
+ */
+static int
+amd_read_peer_config(const struct rte_rawdev *dev)
+{
+	struct ntb_hw *ntb = dev->dev_private;
+	uint32_t info, peer_qps, peer_qsz, lo, hi;
+	int i;
+
+	info = amd_ntb_spad_read(dev, AMD_SPAD_CNT_INFO, 0);
+	peer_qps = (info >> 8) & 0xff;
+	if (peer_qps != ntb->queue_pairs) {
+		NTB_LOG(ERR, "Inconsistent number of queues! (local: %u peer: %u)",
+			ntb->queue_pairs, peer_qps);
+		return -EINVAL;
+	}
+
+	peer_qsz = amd_ntb_spad_read(dev, AMD_SPAD_QUEUE_SZ, 0);
+	if (peer_qsz != ntb->queue_size) {
+		NTB_LOG(ERR, "Inconsistent queue size! (local: %u peer: %u)",
+			ntb->queue_size, peer_qsz);
+		return -EINVAL;
+	}
+
+	ntb->peer_used_mws = (info >> 16) & 0xff;
+	for (i = 0; i < ntb->peer_used_mws; i++) {
+		lo = amd_ntb_spad_read(dev, AMD_SPAD_MW0_BA_L + 2 * i, 0);
+		hi = amd_ntb_spad_read(dev, AMD_SPAD_MW0_BA_H + 2 * i, 0);
+		ntb->peer_mw_base[i] = ((uint64_t)hi << 32) | lo;
+	}
+
+	return 0;
+}
+
+/* Check that the peer advertises the same number of memory windows. */
+static int
+amd_peer_mw_validate(const struct rte_rawdev *dev)
+{
+	struct ntb_hw *ntb = dev->dev_private;
+	uint32_t info, peer_mw_cnt;
+
+	info = amd_ntb_spad_read(dev, AMD_SPAD_CNT_INFO, 0);
+	peer_mw_cnt = info & 0xff;
+	if (peer_mw_cnt != ntb->mw_cnt) {
+		NTB_LOG(ERR, "Peer mw cnt %u != local mw cnt %u.",
+			peer_mw_cnt, ntb->mw_cnt);
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+/* Tear down the local scratchpad and translations so the peer can no
+ * longer reach local memory.
+ */
+static void
+amd_link_cleanup(const struct rte_rawdev *dev)
+{
+	struct ntb_hw *ntb = dev->dev_private;
+	int i;
+
+	for (i = 0; i < ntb->spad_cnt; i++)
+		amd_ntb_spad_write(dev, i, 0, 0);
+
+	for (i = 0; i < ntb->used_mw_num; i++)
+		amd_ntb_mw_set_trans(dev, i, 0, 0);
+}
+
+/* Handle link/power-management events and acknowledge the SMU. Invoked from
+ * the common interrupt handler when no inter-device doorbell is pending.
+ */
+static void
+amd_handle_hw_event(const struct rte_rawdev *dev)
+{
+	struct ntb_hw *ntb = dev->dev_private;
+	uint32_t event, ack;
+
+	event = rte_read32((char *)ntb->hw_addr + AMD_INTSTAT_OFFSET);
+	event &= AMD_EVENT_INTMASK;
+	if (event == 0)
+		return;
+
+	switch (event) {
+	case AMD_PEER_FLUSH_EVENT:
+		NTB_LOG(INFO, "Peer flush event.");
+		break;
+	case AMD_PEER_D0_EVENT:
+		ack = rte_read32((char *)ntb->hw_addr + AMD_PMESTAT_OFFSET);
+		if (ack & 0x1)
+			NTB_LOG(INFO, "D0 wakeup completed for NTB.");
+		/* fall through to ack the SMU */
+		/* Falls through. */
+	case AMD_PEER_RESET_EVENT:
+	case AMD_LINK_DOWN_EVENT:
+	case AMD_PEER_D3_EVENT:
+	case AMD_PEER_PMETO_EVENT:
+	case AMD_LINK_UP_EVENT:
+		ack = rte_read32((char *)ntb->hw_addr + AMD_SMUACK_OFFSET);
+		ack |= event;
+		rte_write32(ack, (char *)ntb->hw_addr + AMD_SMUACK_OFFSET);
+		break;
+	default:
+		NTB_LOG(ERR, "Unknown interrupt event 0x%" PRIx32, event);
+		break;
+	}
+}
+
+static uint64_t
+amd_ntb_get_mem_align(const struct rte_rawdev *dev __rte_unused,
+		      uint32_t mw_id __rte_unused, uint64_t mw_len)
+{
+	/* Base must be a power of two >= the window length, and >= 4K. */
+	return RTE_MAX((uint64_t)RTE_PGSIZE_4K, rte_align64pow2(mw_len));
+}
+
+const struct ntb_dev_ops amd_ntb_ops = {
+	.ntb_dev_init		= amd_ntb_dev_init,
+	.get_peer_mw_addr	= amd_ntb_get_peer_mw_addr,
+	.ioremap		= amd_ntb_ioremap,
+	.get_link_status	= amd_ntb_get_link_status,
+	.set_link		= amd_ntb_set_link,
+	.spad_read		= amd_ntb_spad_read,
+	.spad_write		= amd_ntb_spad_write,
+	.db_read		= amd_ntb_db_read,
+	.db_clear		= amd_ntb_db_clear,
+	.db_set_mask		= amd_ntb_db_set_mask,
+	.peer_db_set		= amd_ntb_peer_db_set,
+	.vector_bind		= amd_ntb_vector_bind,
+	.dev_handshake		= amd_dev_handshake,
+	.peer_mw_validate	= amd_peer_mw_validate,
+	.read_peer_config	= amd_read_peer_config,
+	.link_cleanup		= amd_link_cleanup,
+	.mem_align		= amd_ntb_get_mem_align,
+	.handle_hw_event	= amd_handle_hw_event,
+};
diff --git a/drivers/raw/ntb/ntb_hw_amd.h b/drivers/raw/ntb/ntb_hw_amd.h
new file mode 100644
index 0000000000..48f0db1ad4
--- /dev/null
+++ b/drivers/raw/ntb/ntb_hw_amd.h
@@ -0,0 +1,111 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2026 Advanced Micro Devices, Inc.
+ */
+
+#ifndef _NTB_HW_AMD_H_
+#define _NTB_HW_AMD_H_
+
+#include <stdint.h>
+
+/* NTB vendor and device IDs (EPYC Embedded Turin/Genoa/Siena). */
+#define NTB_AMD_VENDOR_ID		0x1022
+#define NTB_AMD_DEV_ID_PRI		0x14c0	/* Primary NTB endpoint. */
+#define NTB_AMD_DEV_ID_SEC		0x14c3	/* Secondary NTB endpoint. */
+
+/* Device data (spec Table 12). */
+#define AMD_MW_COUNT			2
+#define AMD_DB_COUNT			16
+#define AMD_SPAD_COUNT			16
+#define AMD_MSIX_VECTOR_COUNT		24
+
+/* Peer register window: peer register = primary offset + 0x400. */
+#define AMD_PEER_OFFSET			0x400
+
+/* Peer register window base, derived from the local BAR0 mapping. */
+#define AMD_PEER_MMIO(ntb)		RTE_PTR_ADD((ntb)->hw_addr, AMD_PEER_OFFSET)
+
+/* Primary-side MMIO register offsets (BAR0), spec Table 1. */
+#define AMD_CNTL_OFFSET			0x200	/* Link Control. */
+#define AMD_SPADMUTEX_OFFSET		0x20C	/* Scratchpad mutex. */
+#define AMD_SPAD_OFFSET			0x210	/* Scratchpad registers. */
+#define AMD_SIDEINFO_OFFSET		0x408	/* Link Status (PSIDE_INFO). */
+#define AMD_BAR23_LIMIT_OFFSET		0x418	/* MW BAR23 size. */
+#define AMD_BAR45_LIMIT_OFFSET		0x420	/* MW BAR45 size. */
+#define AMD_BAR23_XLAT_OFFSET		0x438	/* MW BAR23 translation. */
+#define AMD_BAR45_XLAT_OFFSET		0x440	/* MW BAR45 translation. */
+#define AMD_DBFM_OFFSET			0x450	/* Doorbell flush mode. */
+#define AMD_DBREQ_OFFSET		0x454	/* Doorbell request. */
+#define AMD_DBMASK_OFFSET		0x45C	/* Doorbell mask. */
+#define AMD_DBSTAT_OFFSET		0x460	/* Doorbell status. */
+#define AMD_INTMASK_OFFSET		0x470	/* Interrupt mask. */
+#define AMD_INTSTAT_OFFSET		0x474	/* Interrupt status. */
+#define AMD_PMESTAT_OFFSET		0x480	/* PME status. */
+#define AMD_SMUACK_OFFSET		0x4A0	/* SMU control (PSMU_ACK). */
+
+/* Link Status Register (0x408) bits, spec Table 5. */
+#define AMD_SIDE_MASK			(1 << 0) /* 0: primary, 1: secondary. */
+#define AMD_SIDE_READY			(1 << 1) /* Side link ready. */
+
+/* Link Control Register (0x200) bits, spec Table 2. */
+#define AMD_SMM_REG_CTL			(1 << 20)
+#define AMD_PMM_REG_CTL			(1 << 21)
+
+/* Interrupt Status/Mask event bits, spec Table 7. */
+#define AMD_PEER_FLUSH_EVENT		(1 << 0)
+#define AMD_PEER_RESET_EVENT		(1 << 1)
+#define AMD_PEER_D3_EVENT		(1 << 2)
+#define AMD_PEER_PMETO_EVENT		(1 << 3)
+#define AMD_PEER_D0_EVENT		(1 << 4)
+#define AMD_LINK_UP_EVENT		(1 << 5)
+#define AMD_LINK_DOWN_EVENT		(1 << 6)
+#define AMD_EVENT_INTMASK		(AMD_PEER_FLUSH_EVENT | \
+					 AMD_PEER_RESET_EVENT | \
+					 AMD_PEER_D3_EVENT | \
+					 AMD_PEER_PMETO_EVENT | \
+					 AMD_PEER_D0_EVENT | \
+					 AMD_LINK_UP_EVENT | \
+					 AMD_LINK_DOWN_EVENT)
+
+/* PCIe link status decoding (link speed/width). */
+#define AMD_LNK_STA_SPEED_MASK		0x000f
+#define AMD_LNK_STA_WIDTH_MASK		0x03f0
+#define AMD_LNK_STA_SPEED(x)		((x) & AMD_LNK_STA_SPEED_MASK)
+#define AMD_LNK_STA_WIDTH(x)		(((x) & AMD_LNK_STA_WIDTH_MASK) >> 4)
+
+/* The 16-register scratchpad bank is shared between both sides (there is no
+ * separate peer-scratchpad window). It is split into two disjoint 8-register
+ * sets so each side owns one half (offset 0 for one side, 0x20 for the other)
+ * and no mutex is required. Because only 8 registers are available per side,
+ * the driver uses its own packed handshake layout instead of the built-in
+ * protocol.
+ */
+#define AMD_SPAD_SET_OFFSET		0x20
+#define AMD_SPAD_PER_SIDE		(AMD_SPAD_COUNT >> 1)
+
+/* Byte offset of the scratchpad set owned by the local side (@p peer false)
+ * or the peer side (@p peer true). The primary owns offset 0, the secondary
+ * owns offset AMD_SPAD_SET_OFFSET.
+ */
+#define AMD_SPAD_BASE(ntb, peer) \
+	((((ntb)->topo == NTB_TOPO_PRI) == !!(peer)) ? AMD_SPAD_SET_OFFSET : 0)
+
+/* Packed scratchpad handshake layout (indices within an 8-register set).
+ * Indices 6 and 7 are reserved for application user scratchpads.
+ */
+enum amd_spad_idx {
+	AMD_SPAD_CNT_INFO = 0,	/* num_mws | num_qps<<8 | used_mws<<16 */
+	AMD_SPAD_QUEUE_SZ,	/* queue_size */
+	AMD_SPAD_MW0_BA_L,	/* mw0 base address, low 32 bits */
+	AMD_SPAD_MW0_BA_H,	/* mw0 base address, high 32 bits */
+	AMD_SPAD_MW1_BA_L,	/* mw1 base address, low 32 bits */
+	AMD_SPAD_MW1_BA_H,	/* mw1 base address, high 32 bits */
+};
+
+enum amd_ntb_bar {
+	AMD_NTB_BAR23 = 2,
+	AMD_NTB_BAR45 = 4,
+};
+
+extern const struct ntb_dev_ops amd_ntb_ops;
+
+#endif /* _NTB_HW_AMD_H_ */
diff --git a/usertools/dpdk-devbind.py b/usertools/dpdk-devbind.py
index 78d841474f..15b19acb27 100755
--- a/usertools/dpdk-devbind.py
+++ b/usertools/dpdk-devbind.py
@@ -87,6 +87,7 @@
         'name': 'Misc (rawdev)',
         'match': [
             {'Class': '01', 'Vendor': '1af4', 'Device': '1001,1042'},  # VirtIO block
+            {'Class': '06', 'Vendor': '1022', 'Device': '14c0,14c3'},  # AMD NTB
             {'Class': '06', 'Vendor': '8086', 'Device': '201c'},  # Intel NTB Skylake
             {'Class': '06', 'Vendor': '8086', 'Device': '347e'},  # Intel NTB Ice Lake
             {'Class': '08', 'Vendor': '177d', 'Device': 'a059,a060'},  # Marvell CNXK BPHY CGX
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 7+ messages in thread

* Re: [PATCH v4 0/3] raw/ntb: add AMD NTB support
  2026-10-26  8:55 [PATCH v4 0/3] raw/ntb: add AMD NTB support Raghavendra Ningoji
                   ` (2 preceding siblings ...)
  2026-10-26  8:55 ` [PATCH v4 3/3] raw/ntb: add AMD NTB support Raghavendra Ningoji
@ 2026-10-26 15:26 ` David Marchand
  2026-10-26 17:00   ` Stephen Hemminger
  3 siblings, 1 reply; 7+ messages in thread
From: David Marchand @ 2026-10-26 15:26 UTC (permalink / raw)
  To: Raghavendra Ningoji
  Cc: dev, jingjing.wu, thomas, selwin.sebastian, bhagyada.modali,
	bruce.richardson, sachin.saxena, hemant.agrawal

Hello,

On Mon, 26 Oct 2026 at 09:55, Raghavendra Ningoji
<raghavendra.ningoji@amd.com> wrote:
>
> This series adds support for the Non-Transparent Bridge (NTB) endpoints
> integrated in AMD EPYC Embedded "Turin", "Genoa" and "Siena" processors
> to the raw/ntb driver.
>
> The common NTB code validates the vendor ops once at probe time, and the
> vendor abstraction is built around a small set of mandatory ops instead
> of optional hooks with built-in fallbacks:
>
> - Patch 1 is a preparation commit that enforces the vendor ops once, at
>   probe time, and removes the per-call NULL checks that are scattered
>   throughout the common code.
>
> - Patch 2 generalizes the framework for multiple vendors. The link
>   bring-up/tear-down is now expressed through mandatory ops
>   (dev_handshake, peer_mw_validate, read_peer_config, link_cleanup); the
>   former built-in scratchpad logic moved into the Intel driver. The
>   mw_set_trans op is gone (it is now a vendor-internal concern of the
>   handshake), and there is a single common interrupt handler with just
>   one optional op (handle_hw_event) for vendor-specific hardware events.
>
> - Patch 3 adds the AMD hardware driver, its documentation, release note
>   and MAINTAINERS entry.
>
> A few AMD-specific hardware details worth noting for reviewers:
>
> - Primary/secondary topology. One endpoint enumerates as the primary
>   (device ID 0x14c0) and the other as the secondary (0x14c3), rather
>   than the Intel back-to-back model.
>
> - Packed scratchpad handshake. The hardware exposes a single shared
>   16-register scratchpad bank split into two disjoint 8-register sets
>   (one per side), so the driver uses a packed handshake layout that
>   fits in 8 registers, plugged in through the dev_handshake,
>   peer_mw_validate and read_peer_config ops.
>
> - Outbound translation window. AMD NTB uses an outbound translation
>   window: a write to a BARxx memory-window offset is forwarded to
>   (xlat_base | offset) in the peer's memory rather than
>   (xlat_base + offset). The translation base must therefore be aligned
>   to a power of two >= the window length so that no offset bit collides
>   with a set bit in the base. This is reported to applications through
>   the rte_pmd_ntb_get_mem_align() API and honoured by the ntb example
>   when reserving the memory window memzone.
>
> - Secondary-side link status. On the secondary side the device's own
>   PCIe link status does not reflect the true inter-host link, so the
>   speed and width are read from the upstream switch port via sysfs.
>
> Tested on a two-node setup (primary + secondary EPYC Embedded hosts)
> with the dpdk-ntb example: the link comes up at the expected speed and
> width on both sides, and file transfers are byte-correct end to end for
> both small and large files.
>
> Changes since v3:
>
> - Simplified the probe-time vendor ops check (patch 1) to a plain
>   per-member macro instead of an offsetof() table.
>
> - Removed the mw_size_align field from struct ntb_dev_info entirely
>   (patch 2): applications must now use rte_pmd_ntb_get_mem_align(). This
>   is documented in the API changes section of the release notes.
>
> - Removed the unused pmd_private field from struct ntb_hw and the whole
>   struct amd_ntb_hw (patch 3): self_mmio is just ntb->hw_addr, peer_mmio
>   becomes AMD_PEER_MMIO(ntb), the scratchpad offsets become an
>   AMD_SPAD_BASE(ntb, peer) macro, and int_mask/peer_status/ctl_status
>   were unused.
>
> - Sorted AMD alphabetically before Intel in meson.build, the ntb_hw
>   header include, the PCI id mapping table and usertools/dpdk-devbind.py.
>
> - Moved the primary/secondary topology definitions (NTB_TOPO_PRI/SEC)
>   from patch 2 to patch 3 where they are first used.
>
> - Updated the Limitation section of doc/guides/rawdevs/ntb.rst to list
>   the supported AMD processors.

- "external" build was broken for the example as spotted by the CI.
  https://github.com/ovsrobot/dpdk/actions/runs/40296962846/job/121485246438
  This is a known limitation of the .pc provided by DPDK (which Robin
recently tried to fix).
  I tested and squashed a simple fix:
https://github.com/david-marchand/dpdk/commit/16b0d741ebf39b22ebb696c07f2ed44ad383bfc9

- Some commitlogs were not updated after the v4 changes, fixed.

- Some strange indentation issues were still there, fixed.

With all those fixes applied, I pushed this driver update.
Thanks.


-- 
David Marchand


^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH v4 0/3] raw/ntb: add AMD NTB support
  2026-10-26 15:26 ` [PATCH v4 0/3] " David Marchand
@ 2026-10-26 17:00   ` Stephen Hemminger
  2026-10-27  9:08     ` David Marchand
  0 siblings, 1 reply; 7+ messages in thread
From: Stephen Hemminger @ 2026-10-26 17:00 UTC (permalink / raw)
  To: David Marchand
  Cc: Raghavendra Ningoji, dev, jingjing.wu, thomas, selwin.sebastian,
	bhagyada.modali, bruce.richardson, sachin.saxena, hemant.agrawal

On Mon, 26 Oct 2026 11:26:01 -0400
David Marchand <david.marchand@redhat.com> wrote:

> >
> > The common NTB code validates the vendor ops once at probe time, and the
> > vendor abstraction is built around a small set of mandatory ops instead
> > of optional hooks with built-in fallbacks:
> >
> > - Patch 1 is a preparation commit that enforces the vendor ops once, at
> >   probe time, and removes the per-call NULL checks that are scattered
> >   throughout the common code.
> >
> > - Patch 2 generalizes the framework for multiple vendors. The link
> >   bring-up/tear-down is now expressed through mandatory ops
> >   (dev_handshake, peer_mw_validate, read_peer_config, link_cleanup); the
> >   former built-in scratchpad logic moved into the Intel driver. The
> >   mw_set_trans op is gone (it is now a vendor-internal concern of the
> >   handshake), and there is a single common interrupt handler with just
> >   one optional op (handle_hw_event) for vendor-specific hardware events.
> >
> > - Patch 3 adds the AMD hardware driver, its documentation, release note
> >   and MAINTAINERS entry.
> >
> > A few AMD-specific hardware details worth noting for reviewers:
> >
> > - Primary/secondary topology. One endpoint enumerates as the primary
> >   (device ID 0x14c0) and the other as the secondary (0x14c3), rather
> >   than the Intel back-to-back model.
> >
> > - Packed scratchpad handshake. The hardware exposes a single shared
> >   16-register scratchpad bank split into two disjoint 8-register sets
> >   (one per side), so the driver uses a packed handshake layout that
> >   fits in 8 registers, plugged in through the dev_handshake,
> >   peer_mw_validate and read_peer_config ops.
> >
> > - Outbound translation window. AMD NTB uses an outbound translation
> >   window: a write to a BARxx memory-window offset is forwarded to
> >   (xlat_base | offset) in the peer's memory rather than
> >   (xlat_base + offset). The translation base must therefore be aligned
> >   to a power of two >= the window length so that no offset bit collides
> >   with a set bit in the base. This is reported to applications through
> >   the rte_pmd_ntb_get_mem_align() API and honoured by the ntb example
> >   when reserving the memory window memzone.
> >
> > - Secondary-side link status. On the secondary side the device's own
> >   PCIe link status does not reflect the true inter-host link, so the
> >   speed and width are read from the upstream switch port via sysfs.
> >
> > Tested on a two-node setup (primary + secondary EPYC Embedded hosts)
> > with the dpdk-ntb example: the link comes up at the expected speed and
> > width on both sides, and file transfers are byte-correct end to end for
> > both small and large files.
> >
> > Changes since v3:
> >
> > - Simplified the probe-time vendor ops check (patch 1) to a plain
> >   per-member macro instead of an offsetof() table.
> >
> > - Removed the mw_size_align field from struct ntb_dev_info entirely
> >   (patch 2): applications must now use rte_pmd_ntb_get_mem_align(). This
> >   is documented in the API changes section of the release notes.
> >
> > - Removed the unused pmd_private field from struct ntb_hw and the whole
> >   struct amd_ntb_hw (patch 3): self_mmio is just ntb->hw_addr, peer_mmio
> >   becomes AMD_PEER_MMIO(ntb), the scratchpad offsets become an
> >   AMD_SPAD_BASE(ntb, peer) macro, and int_mask/peer_status/ctl_status
> >   were unused.
> >
> > - Sorted AMD alphabetically before Intel in meson.build, the ntb_hw
> >   header include, the PCI id mapping table and usertools/dpdk-devbind.py.
> >
> > - Moved the primary/secondary topology definitions (NTB_TOPO_PRI/SEC)
> >   from patch 2 to patch 3 where they are first used.
> >
> > - Updated the Limitation section of doc/guides/rawdevs/ntb.rst to list
> >   the supported AMD processors.  
> 
> - "external" build was broken for the example as spotted by the CI.
>   https://github.com/ovsrobot/dpdk/actions/runs/40296962846/job/121485246438
>   This is a known limitation of the .pc provided by DPDK (which Robin
> recently tried to fix).
>   I tested and squashed a simple fix:
> https://github.com/david-marchand/dpdk/commit/16b0d741ebf39b22ebb696c07f2ed44ad383bfc9
> 
> - Some commitlogs were not updated after the v4 changes, fixed.
> 
> - Some strange indentation issues were still there, fixed.
> 
> With all those fixes applied, I pushed this driver update.
> Thanks.


I think merge was premature. More detailed AI review reports:

Subject: Re: [PATCH 0/4] raw/ntb: AMD NTB support

Post-merge review of the raw/ntb series (1/4 - 3/4) against current
main. Findings below are for follow-up fixes.

Series summary
--------------
The ops-table refactor in 1/4 and 2/4 is reasonable. The main problems
are a type-confusion hole in the new public API, an unbounded
peer-supplied loop count that writes past peer_mw_base[], and AMD event
handling that cannot actually run as wired.


[PATCH 2/4] raw/ntb: generalize framework for multiple vendors
--------------------------------------------------------------

Error: rte_pmd_ntb_get_mem_align() trusts any rawdev id.

  It checks only dev_id < RTE_RAWDEV_MAX_DEVS and dev_private != NULL,
  then casts dev_private to struct ntb_hw. Passing the id of an
  unattached slot or of a non-NTB rawdev (dpaa2, ifpga, cnxk...) calls
  through a garbage ntb_ops pointer. Check the device is valid and is
  ours:

	if (!rte_rawdev_pmd_is_valid_dev(dev_id))
		return 0;
	dev = rte_rawdev_pmd_get_dev(dev_id);
	if (dev->dev_ops != &ntb_ops)
		return 0;

  Also, an out of range mw_id returns RTE_CACHE_LINE_SIZE, which
  contradicts the doxygen ("0 on error"). Return 0.

Warning: Rx EOP guard leaks the partial chain and desyncs the ring.

  On the new "Malformed rx stream" path the descriptors already walked
  have advanced last_used/last_avail and been chained under `first`,
  but nb_rx is not incremented, so the chain is never returned or
  freed. If nb_rx == 0 the refilled descriptors are not published
  either (avail_cnt not written) while rxq->last_avail has moved.
  The mbuf-alloc failure path has the same bug already; the guard
  copies it. Free the partial chain and only advance ring indexes for
  completed packets.

  The guard also bounds on nb_rx_desc, not on the descriptors the peer
  actually published (used_cnt - last_used), so it still reads entries
  the peer has not written. This is a fix for code in released
  versions; it should be its own patch with Fixes: and
  Cc: stable@dpdk.org, not folded into the refactor.

Info: removing mw_size_align changes the layout of the installed
  struct ntb_dev_info; note it under ABI Changes as well.

Info: examples/ntb/Makefile adds -lrte_raw_ntb to LDFLAGS, which also
  hits the static target. Other examples (l2fwd-crypto,
  vm_power_manager) use LDFLAGS_SHARED.


[PATCH 3/4] raw/ntb: support AMD NTB
------------------------------------

Error: peer_used_mws from the scratchpad overflows peer_mw_base[].

  amd_ntb_read_peer_config():

	ntb->peer_used_mws = (info >> 16) & 0xff;
	for (i = 0; i < ntb->peer_used_mws; i++)
		...
		ntb->peer_mw_base[i] = ...

  peer_mw_base is rte_zmalloc'd with mw_cnt (2) entries. A peer
  advertising more than 2 (bug, version skew, or just garbage in an
  uninitialised scratchpad) writes past the allocation. Intel has the
  same pattern; fix both:

	if (peer_used_mws > ntb->mw_cnt) {
		NTB_LOG(ERR, "Peer used mws %u > mw cnt %u",
			peer_used_mws, ntb->mw_cnt);
		return -EINVAL;
	}

Warning: hardware events are never delivered.

  - AMD_INTMASK_OFFSET is defined but never written, so event
    delivery depends on the reset default.
  - ntb_init_hw() enables db_cnt (16) eventfds; the comment in
    amd_ntb_vector_bind() says events have their own MSI-X vectors
    (AMD_MSIX_VECTOR_COUNT is 24), and those are never enabled.
  - handle_hw_event is only called at the bottom of
    ntb_dev_intr_handler(), after doorbells 0/1/2 have already
    returned early, so it runs only on a stray doorbell.

  As written amd_ntb_handle_hw_event() is effectively dead code.
  Either wire up the event vector and INTMASK, or drop it.

Warning: doorbell interrupts with vfio-pci MSI-X.

  amd_ntb_vector_bind() is a no-op and doorbells map 1:1 to MSI-X
  vectors. The callback is registered on the device intr handle only,
  so with vfio-pci only vector 0 (DB0) reaches ntb_dev_intr_handler().
  DB1 (peer going down) and DB2 are lost, so the stop handshake in
  ntb_dev_stop() never completes. Was this tested with vfio-pci, or
  only igb_uio? If uio only, say so in ntb.rst.

Warning: amd_ntb_handle_hw_event() switches on a bitmask.

  INTSTAT can have several bits set at once; any combination falls to
  default and is logged as unknown and never acked to the SMU. Loop
  over the set bits, or ack all of (event & AMD_EVENT_INTMASK) after
  handling D0. Also drop the duplicated fallthrough comment; one
  /* fallthrough */ is enough.

Warning: ioremap range check is off by one.

	addr <= ntb->peer_mw_base[i] + ntb->mw_size[i]

  accepts one past the end of the window. Should be <. Copied from
  intel_ntb_ioremap(), which needs the same fix.

Warning: ntb->mw_size is never freed.

  Allocated in amd_ntb_dev_init(), not freed on later ntb_init_hw()
  failure or in ntb_destroy(). Same leak exists on the Intel side;
  free it in common code at release.

Info: unused defines in ntb_hw_amd.h: AMD_SPADMUTEX_OFFSET,
  AMD_DBFM_OFFSET, AMD_INTMASK_OFFSET, AMD_MSIX_VECTOR_COUNT.

Info: amd_ntb_mw_set_trans() checks IOVA alignment, but
  rte_memzone_reserve_aligned() aligns the VA. With IOVA as PA and a
  window larger than the hugepage size the IOVA is not guaranteed to
  be aligned, and configure fails with -EINVAL. Worth a line in
  ntb.rst that large windows need IOVA as VA (or 1G pages).

Info: amd_ntb_peer_mw_validate() only compares the window count; the
  Intel version also checks sizes. Mismatched BAR sizes between hosts
  go undetected.

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH v4 0/3] raw/ntb: add AMD NTB support
  2026-10-26 17:00   ` Stephen Hemminger
@ 2026-10-27  9:08     ` David Marchand
  0 siblings, 0 replies; 7+ messages in thread
From: David Marchand @ 2026-10-27  9:08 UTC (permalink / raw)
  To: Raghavendra Ningoji
  Cc: dev, jingjing.wu, thomas, selwin.sebastian, bhagyada.modali,
	bruce.richardson, sachin.saxena, hemant.agrawal,
	Stephen Hemminger

Hello,

On Mon, 26 Oct 2026 at 18:00, Stephen Hemminger
<stephen@networkplumber.org> wrote:
> Subject: Re: [PATCH 0/4] raw/ntb: AMD NTB support
>
> Post-merge review of the raw/ntb series (1/4 - 3/4) against current
> main. Findings below are for follow-up fixes.
>
> Series summary
> --------------
> The ops-table refactor in 1/4 and 2/4 is reasonable. The main problems
> are a type-confusion hole in the new public API, an unbounded
> peer-supplied loop count that writes past peer_mw_base[], and AMD event
> handling that cannot actually run as wired.
>
>
> [PATCH 2/4] raw/ntb: generalize framework for multiple vendors
> --------------------------------------------------------------
>
> Error: rte_pmd_ntb_get_mem_align() trusts any rawdev id.
>
>   It checks only dev_id < RTE_RAWDEV_MAX_DEVS and dev_private != NULL,
>   then casts dev_private to struct ntb_hw. Passing the id of an
>   unattached slot or of a non-NTB rawdev (dpaa2, ifpga, cnxk...) calls
>   through a garbage ntb_ops pointer. Check the device is valid and is
>   ours:
>
>         if (!rte_rawdev_pmd_is_valid_dev(dev_id))
>                 return 0;
>         dev = rte_rawdev_pmd_get_dev(dev_id);
>         if (dev->dev_ops != &ntb_ops)
>                 return 0;
>
>   Also, an out of range mw_id returns RTE_CACHE_LINE_SIZE, which
>   contradicts the doxygen ("0 on error"). Return 0.
>
> Warning: Rx EOP guard leaks the partial chain and desyncs the ring.
>
>   On the new "Malformed rx stream" path the descriptors already walked
>   have advanced last_used/last_avail and been chained under `first`,
>   but nb_rx is not incremented, so the chain is never returned or
>   freed. If nb_rx == 0 the refilled descriptors are not published
>   either (avail_cnt not written) while rxq->last_avail has moved.
>   The mbuf-alloc failure path has the same bug already; the guard
>   copies it. Free the partial chain and only advance ring indexes for
>   completed packets.
>
>   The guard also bounds on nb_rx_desc, not on the descriptors the peer
>   actually published (used_cnt - last_used), so it still reads entries
>   the peer has not written. This is a fix for code in released
>   versions; it should be its own patch with Fixes: and
>   Cc: stable@dpdk.org, not folded into the refactor.
>
> Info: removing mw_size_align changes the layout of the installed
>   struct ntb_dev_info; note it under ABI Changes as well.
>
> Info: examples/ntb/Makefile adds -lrte_raw_ntb to LDFLAGS, which also
>   hits the static target. Other examples (l2fwd-crypto,
>   vm_power_manager) use LDFLAGS_SHARED.
>
>
> [PATCH 3/4] raw/ntb: support AMD NTB
> ------------------------------------
>
> Error: peer_used_mws from the scratchpad overflows peer_mw_base[].
>
>   amd_ntb_read_peer_config():
>
>         ntb->peer_used_mws = (info >> 16) & 0xff;
>         for (i = 0; i < ntb->peer_used_mws; i++)
>                 ...
>                 ntb->peer_mw_base[i] = ...
>
>   peer_mw_base is rte_zmalloc'd with mw_cnt (2) entries. A peer
>   advertising more than 2 (bug, version skew, or just garbage in an
>   uninitialised scratchpad) writes past the allocation. Intel has the
>   same pattern; fix both:
>
>         if (peer_used_mws > ntb->mw_cnt) {
>                 NTB_LOG(ERR, "Peer used mws %u > mw cnt %u",
>                         peer_used_mws, ntb->mw_cnt);
>                 return -EINVAL;
>         }
>
> Warning: hardware events are never delivered.
>
>   - AMD_INTMASK_OFFSET is defined but never written, so event
>     delivery depends on the reset default.
>   - ntb_init_hw() enables db_cnt (16) eventfds; the comment in
>     amd_ntb_vector_bind() says events have their own MSI-X vectors
>     (AMD_MSIX_VECTOR_COUNT is 24), and those are never enabled.
>   - handle_hw_event is only called at the bottom of
>     ntb_dev_intr_handler(), after doorbells 0/1/2 have already
>     returned early, so it runs only on a stray doorbell.
>
>   As written amd_ntb_handle_hw_event() is effectively dead code.
>   Either wire up the event vector and INTMASK, or drop it.
>
> Warning: doorbell interrupts with vfio-pci MSI-X.
>
>   amd_ntb_vector_bind() is a no-op and doorbells map 1:1 to MSI-X
>   vectors. The callback is registered on the device intr handle only,
>   so with vfio-pci only vector 0 (DB0) reaches ntb_dev_intr_handler().
>   DB1 (peer going down) and DB2 are lost, so the stop handshake in
>   ntb_dev_stop() never completes. Was this tested with vfio-pci, or
>   only igb_uio? If uio only, say so in ntb.rst.
>
> Warning: amd_ntb_handle_hw_event() switches on a bitmask.
>
>   INTSTAT can have several bits set at once; any combination falls to
>   default and is logged as unknown and never acked to the SMU. Loop
>   over the set bits, or ack all of (event & AMD_EVENT_INTMASK) after
>   handling D0. Also drop the duplicated fallthrough comment; one
>   /* fallthrough */ is enough.
>
> Warning: ioremap range check is off by one.
>
>         addr <= ntb->peer_mw_base[i] + ntb->mw_size[i]
>
>   accepts one past the end of the window. Should be <. Copied from
>   intel_ntb_ioremap(), which needs the same fix.
>
> Warning: ntb->mw_size is never freed.
>
>   Allocated in amd_ntb_dev_init(), not freed on later ntb_init_hw()
>   failure or in ntb_destroy(). Same leak exists on the Intel side;
>   free it in common code at release.
>
> Info: unused defines in ntb_hw_amd.h: AMD_SPADMUTEX_OFFSET,
>   AMD_DBFM_OFFSET, AMD_INTMASK_OFFSET, AMD_MSIX_VECTOR_COUNT.
>
> Info: amd_ntb_mw_set_trans() checks IOVA alignment, but
>   rte_memzone_reserve_aligned() aligns the VA. With IOVA as PA and a
>   window larger than the hugepage size the IOVA is not guaranteed to
>   be aligned, and configure fails with -EINVAL. Worth a line in
>   ntb.rst that large windows need IOVA as VA (or 1G pages).
>
> Info: amd_ntb_peer_mw_validate() only compares the window count; the
>   Intel version also checks sizes. Mismatched BAR sizes between hosts
>   go undetected.

Please look into those warnings and send fixes if confirmed.

Thanks.


-- 
David Marchand


^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2026-10-27  9:09 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-26  8:55 [PATCH v4 0/3] raw/ntb: add AMD NTB support Raghavendra Ningoji
2026-10-26  8:55 ` [PATCH v4 1/3] raw/ntb: validate vendor ops at probe Raghavendra Ningoji
2026-10-26  8:55 ` [PATCH v4 2/3] raw/ntb: generalize framework for multiple vendors Raghavendra Ningoji
2026-10-26  8:55 ` [PATCH v4 3/3] raw/ntb: add AMD NTB support Raghavendra Ningoji
2026-10-26 15:26 ` [PATCH v4 0/3] " David Marchand
2026-10-26 17:00   ` Stephen Hemminger
2026-10-27  9:08     ` David Marchand

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.