mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Adrian Ambrozewicz <aambrozewicz@nvidia.com>
To: Mark Brown <broonie@kernel.org>
Cc: Donald Shannon <donalds@nvidia.com>,
	Nathan Chancellor <nathan@kernel.org>,
	Nick Desaulniers <ndesaulniers@google.com>,
	Bill Wendling <morbo@google.com>,
	Justin Stitt <justinstitt@google.com>,
	linux-kernel@vger.kernel.org, linux-spi@vger.kernel.org,
	linux-usb@vger.kernel.org, llvm@lists.linux.dev,
	Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Subject: [PATCH 09/11] spi: Add NVIDIA LSTP SPI driver
Date: Mon, 12 Oct 2026 20:36:07 +0300	[thread overview]
Message-ID: <20261012-lstp-usb-adapter-v1-9-aambrozewicz@nvidia.com> (raw)
In-Reply-To: <20261012-lstp-usb-adapter-v1-0-aambrozewicz@nvidia.com>

Add a driver for the SPI controller channels of NVIDIA LSTP USB
adapters. Each channel is registered as an SPI host controller; devices
behind it can be described by device tree or ACPI, or created from
userspace through the controller's new_device attribute. The firmware
reports the chip selects and the bus speed.

Signed-off-by: Adrian Ambrozewicz <aambrozewicz@nvidia.com>
---
 MAINTAINERS            |   1 +
 drivers/spi/Kconfig    |  13 ++
 drivers/spi/Makefile   |   1 +
 drivers/spi/spi-lstp.c | 316 +++++++++++++++++++++++++++++++++++++++++
 4 files changed, 331 insertions(+)
 create mode 100644 drivers/spi/spi-lstp.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 082c4b3a0eb6..081e3f32c840 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -19580,6 +19580,7 @@ F:	Documentation/firmware-guide/acpi/lstp.rst
 F:	drivers/gpio/gpio-lstp.c
 F:	drivers/i2c/busses/i2c-lstp.c
 F:	drivers/leds/leds-lstp.c
+F:	drivers/spi/spi-lstp.c
 F:	drivers/usb/misc/lstp.c
 F:	include/linux/usb/lstp.h
 
diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig
index a0f37e2df716..e76abaac825c 100644
--- a/drivers/spi/Kconfig
+++ b/drivers/spi/Kconfig
@@ -694,6 +694,19 @@ config SPI_LJCA
 	  This driver can also be built as a module. If so, the module
 	  will be called spi-ljca.
 
+config SPI_LSTP
+	tristate "NVIDIA LSTP SPI support"
+	depends on USB_LSTP
+	default USB_LSTP
+	help
+	  Say Y or M here to support the SPI channels of NVIDIA LSTP USB
+	  adapters. Each channel is registered as an SPI host controller,
+	  and the devices behind it can be described by device tree, ACPI
+	  or created through spidev.
+
+	  This driver can also be built as a module. If so, the module
+	  will be called spi-lstp.
+
 config SPI_MA35D1_QSPI
 	tristate "Nuvoton MA35D1 QSPI controller"
 	depends on ARCH_MA35 || COMPILE_TEST
diff --git a/drivers/spi/Makefile b/drivers/spi/Makefile
index a18814fbde93..c0b1ea6491dd 100644
--- a/drivers/spi/Makefile
+++ b/drivers/spi/Makefile
@@ -86,6 +86,7 @@ obj-$(CONFIG_SPI_LOONGSON_CORE)		+= spi-loongson-core.o
 obj-$(CONFIG_SPI_LOONGSON_PCI)		+= spi-loongson-pci.o
 obj-$(CONFIG_SPI_LOONGSON_PLATFORM)	+= spi-loongson-plat.o
 obj-$(CONFIG_SPI_LP8841_RTC)		+= spi-lp8841-rtc.o
+obj-$(CONFIG_SPI_LSTP)			+= spi-lstp.o
 obj-$(CONFIG_SPI_MA35D1_QSPI)		+= spi-ma35d1-qspi.o
 obj-$(CONFIG_SPI_MESON_SPICC)		+= spi-meson-spicc.o
 obj-$(CONFIG_SPI_MESON_SPIFC)		+= spi-meson-spifc.o
diff --git a/drivers/spi/spi-lstp.c b/drivers/spi/spi-lstp.c
new file mode 100644
index 000000000000..7aa908e6c398
--- /dev/null
+++ b/drivers/spi/spi-lstp.c
@@ -0,0 +1,316 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * SPI driver for LSTP USB interface.
+ *
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES.  All rights reserved.
+ */
+
+#include <linux/spi/spi.h>
+#include <linux/acpi.h>
+#include <linux/err.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/property.h>
+
+#include <linux/usb/lstp.h>
+
+enum lstp_spi_cmd {
+	LSTP_SPI_CMD_READ = 0x01,
+	LSTP_SPI_CMD_WRITE = 0x02,
+	LSTP_SPI_CMD_WRITE_READ = 0x03,
+	LSTP_SPI_CMD_POSTED_WRITE = 0x04
+};
+
+/* clang-format off */
+enum lstp_spi_cmd_cs_toggle {
+	LSTP_SPI_CMD_CS_ASSERT = 0x20,
+	LSTP_SPI_CMD_CS_DEASSERT = 0x10
+};
+
+enum lstp_spi_cmd_cs_sel {
+	LSTP_SPI_CMD_CS0 = 0x00,
+	LSTP_SPI_CMD_CS1 = 0x40
+};
+
+static const u8 lstp_spi_cs_map[] = {
+	LSTP_SPI_CMD_CS0,
+	LSTP_SPI_CMD_CS1
+}; /* clang-format on */
+
+struct lstp_spi_read_req {
+	__le32 len;
+} __packed;
+
+struct lstp_spi_config {
+	u8 num_devices;
+	__le32 speed_hz;
+} __packed;
+
+struct lstp_spi_priv {
+	struct spi_controller *ctrl;
+	u32 current_speed_hz;
+};
+
+/* A transfer without data still sends a zero-length WRITE when @cs_flags moves CS. */
+static int lstp_spi_do_transfer(struct lstp_channel *ch, struct spi_transfer *xfer, u8 cs_bits,
+				u8 cs_flags)
+{
+	struct lstp_spi_priv *priv = ch->priv;
+
+	if (xfer->word_delay.value)
+		dev_warn_once(ch->dev, "%s: ch_%d: word_delay not supported, ignoring\n", __func__,
+			      ch->ch_id);
+
+	xfer->error = SPI_TRANS_FAIL_NO_START;
+	xfer->effective_speed_hz = priv->current_speed_hz;
+
+	if (xfer->len && !xfer->tx_buf && !xfer->rx_buf) {
+		dev_err_ratelimited(ch->dev, "%s: ch_%d: Transfer of %u bytes has no buffer\n",
+				    __func__, ch->ch_id, xfer->len);
+		return -EINVAL;
+	}
+	if (xfer->tx_buf && xfer->len > ch->max_tx_payload) {
+		dev_err_ratelimited(ch->dev,
+				    "%s: ch_%d: TX transfer length %u exceeds capacity %zu\n",
+				    __func__, ch->ch_id, xfer->len, ch->max_tx_payload);
+		return -EINVAL;
+	}
+	if (xfer->rx_buf && xfer->len > ch->max_rx_payload) {
+		dev_err_ratelimited(ch->dev,
+				    "%s: ch_%d: RX transfer length %u exceeds capacity %zu\n",
+				    __func__, ch->ch_id, xfer->len, ch->max_rx_payload);
+		return -EINVAL;
+	}
+
+	int ret;
+
+	if (xfer->tx_buf && xfer->rx_buf) {
+		u8 cmd = LSTP_SPI_CMD_WRITE_READ | cs_bits | cs_flags;
+
+		ret = lstp_request(ch, cmd, xfer->tx_buf, xfer->len, xfer->rx_buf, xfer->len, NULL,
+				   NULL);
+	} else if (xfer->tx_buf) {
+		u8 cmd = LSTP_SPI_CMD_WRITE | cs_bits | cs_flags;
+
+		ret = lstp_request(ch, cmd, xfer->tx_buf, xfer->len, NULL, 0, NULL, NULL);
+	} else if (xfer->rx_buf) {
+		struct lstp_spi_read_req req = { .len = cpu_to_le32(xfer->len) };
+		u8 cmd = LSTP_SPI_CMD_READ | cs_bits | cs_flags;
+
+		ret = lstp_request(ch, cmd, &req, sizeof(req), xfer->rx_buf, xfer->len, NULL, NULL);
+	} else if (cs_flags) {
+		u8 cmd = LSTP_SPI_CMD_WRITE | cs_bits | cs_flags;
+
+		ret = lstp_request(ch, cmd, NULL, 0, NULL, 0, NULL, NULL);
+	} else {
+		/* No data and no CS change; only the caller's delay is left to do. */
+		ret = 0;
+	}
+
+	if (ret) {
+		if (ret != -ENODEV)
+			dev_err_ratelimited(ch->dev, "%s: ch_%d: Transfer failed (%pe)\n", __func__,
+					    ch->ch_id, ERR_PTR(ret));
+		xfer->error = SPI_TRANS_FAIL_IO;
+		return ret;
+	}
+
+	xfer->error = 0;
+	return 0;
+}
+
+/*
+ * CS edges ride in each transfer's command byte rather than in a set_cs callback, which would
+ * cost a USB round trip per edge.
+ */
+static int lstp_spi_transfer_one_message(struct spi_controller *ctrl, struct spi_message *mesg)
+{
+	struct lstp_channel *ch = spi_controller_get_devdata(ctrl);
+	struct spi_transfer *xfer;
+	u8 chip_select = spi_get_chipselect(mesg->spi, 0);
+	int ret = 0;
+	bool cs_active = false;
+
+	if (chip_select >= ctrl->num_chipselect) {
+		dev_err_ratelimited(ch->dev, "%s: ch_%d: Invalid chip select %u (max %u)\n",
+				    __func__, ch->ch_id, chip_select, ctrl->num_chipselect - 1);
+		ret = -EINVAL;
+		goto finalize;
+	}
+
+	u8 cs_bits = lstp_spi_cs_map[chip_select];
+
+	list_for_each_entry(xfer, &mesg->transfers, transfer_list) {
+		u8 cs_flags = 0;
+		bool is_last = list_is_last(&xfer->transfer_list, &mesg->transfers);
+		bool no_data = !xfer->tx_buf && !xfer->rx_buf;
+
+		if (!cs_active) {
+			cs_flags |= LSTP_SPI_CMD_CS_ASSERT;
+			cs_active = true;
+		}
+
+		if ((is_last && !xfer->cs_change) || (!is_last && xfer->cs_change)) {
+			cs_flags |= LSTP_SPI_CMD_CS_DEASSERT;
+			cs_active = false;
+		}
+
+		/* A data transfer fuses delay and CS edge into one command; CS-only waits first. */
+		if (no_data)
+			spi_transfer_delay_exec(xfer);
+
+		ret = lstp_spi_do_transfer(ch, xfer, cs_bits, cs_flags);
+		if (ret)
+			break;
+
+		mesg->actual_length += xfer->len;
+
+		if (!is_last && xfer->cs_change)
+			spi_transfer_cs_change_delay_exec(mesg, xfer);
+
+		if (!no_data)
+			spi_transfer_delay_exec(xfer);
+	}
+
+finalize:
+	mesg->status = ret;
+	spi_finalize_current_message(ctrl);
+	return ret;
+}
+
+/* spi_setup() path: firmware owns CS, so cs_setup/cs_hold/cs_inactive are only warned about. */
+static int lstp_spi_set_cs_timing(struct spi_device *spi)
+{
+	struct lstp_channel *ch = spi_controller_get_devdata(spi->controller);
+
+	if (spi->cs_setup.value || spi->cs_hold.value || spi->cs_inactive.value)
+		dev_warn_ratelimited(ch->dev,
+				     "%s: ch_%d: CS timing delays not supported, ignoring\n",
+				     __func__, ch->ch_id);
+	return 0;
+}
+
+static size_t lstp_spi_max_xfer_size(struct spi_device *spi)
+{
+	struct lstp_channel *ch = spi_controller_get_devdata(spi->controller);
+
+	return min(ch->max_tx_payload, ch->max_rx_payload);
+}
+
+/*
+ * Device tree lists peripherals as children of the channel node, so an empty node is an
+ * incomplete description. ACPI resolves SpiSerialBus() resources from anywhere in the namespace,
+ * and the SPI core enumerates nothing from a software node.
+ */
+static void lstp_spi_enumerate(struct lstp_channel *ch, struct spi_controller *ctrl)
+{
+	if (is_of_node(ch->fwnode)) {
+		unsigned int child_count = 0;
+		struct fwnode_handle *child;
+
+		fwnode_for_each_available_child_node(ch->fwnode, child)
+			child_count++;
+
+		if (child_count == 0) {
+			dev_warn(ch->dev,
+				 "%s: ch_%d: Firmware node present but defines no SPI devices\n",
+				 __func__, ch->ch_id);
+		} else {
+			dev_info(ch->dev, "%s: ch_%d: Firmware node present with %u SPI devices\n",
+				 __func__, ch->ch_id, child_count);
+		}
+	} else if (is_acpi_node(ch->fwnode)) {
+		dev_info(ch->dev,
+			 "%s: ch_%d: ACPI describes this channel; its peripherals resolve from the namespace\n",
+			 __func__, ch->ch_id);
+	} else if (ch->fwnode) {
+		dev_warn(ch->dev,
+			 "%s: ch_%d: Software node describes this channel, so no SPI devices are enumerated for it\n",
+			 __func__, ch->ch_id);
+	}
+}
+
+static int lstp_spi_probe(struct auxiliary_device *auxdev, const struct auxiliary_device_id *id)
+{
+	struct lstp_channel *ch = lstp_channel_from_auxdev(auxdev);
+	const struct lstp_channel_init_info *info = dev_get_platdata(&auxdev->dev);
+
+	if (!info)
+		return -ENODEV;
+
+	if (info->config_len < sizeof(struct lstp_spi_config)) {
+		dev_err(ch->dev, "%s: ch_%d: SPI config too short: %zu < %zu\n", __func__,
+			ch->ch_id, info->config_len, sizeof(struct lstp_spi_config));
+		return -EINVAL;
+	}
+
+	const struct lstp_spi_config *config = info->config;
+	struct lstp_spi_priv *priv = devm_kzalloc(ch->dev, sizeof(*priv), GFP_KERNEL);
+
+	if (!priv)
+		return -ENOMEM;
+
+	struct spi_controller *ctrl = devm_spi_alloc_host(ch->dev, 0);
+
+	if (!ctrl)
+		return -ENOMEM;
+
+	u8 num_devices = config->num_devices;
+	u32 speed_hz = le32_to_cpu(config->speed_hz);
+
+	if (num_devices == 0 || num_devices > ARRAY_SIZE(lstp_spi_cs_map)) {
+		dev_err(ch->dev, "%s: ch_%d: Invalid num_devices %u (max %zu)\n", __func__,
+			ch->ch_id, num_devices, ARRAY_SIZE(lstp_spi_cs_map));
+		return -EINVAL;
+	}
+
+	if (speed_hz == 0) {
+		dev_err(ch->dev, "%s: ch_%d: Device returned SPI speed of %u Hz\n", __func__,
+			ch->ch_id, speed_hz);
+		return -EINVAL;
+	}
+
+	spi_controller_set_devdata(ctrl, ch);
+	ctrl->bus_num = -1;
+	ctrl->num_chipselect = num_devices;
+	ctrl->transfer_one_message = lstp_spi_transfer_one_message;
+	ctrl->max_transfer_size = lstp_spi_max_xfer_size;
+	ctrl->set_cs_timing = lstp_spi_set_cs_timing;
+
+	device_set_node(&ctrl->dev, ch->fwnode);
+
+	priv->ctrl = ctrl;
+	priv->current_speed_hz = speed_hz;
+	/* Registration probes peripherals, which may transfer at once; transfers read @ch->priv. */
+	ch->priv = priv;
+
+	int ret = devm_spi_register_controller(ch->dev, ctrl);
+
+	if (ret) {
+		dev_err(ch->dev, "%s: ch_%d: Could not register SPI controller (%pe)\n", __func__,
+			ch->ch_id, ERR_PTR(ret));
+		return ret;
+	}
+
+	lstp_spi_enumerate(ch, ctrl);
+
+	dev_info(ch->dev, "%s: ch_%d: Started as %s (devices=%u, speed=%u Hz)\n", __func__,
+		 ch->ch_id, ch->display_name, ctrl->num_chipselect, priv->current_speed_hz);
+	return 0;
+}
+
+static const struct auxiliary_device_id lstp_spi_id_table[] = { { .name = "lstp.spi" }, {} };
+MODULE_DEVICE_TABLE(auxiliary, lstp_spi_id_table);
+
+static struct lstp_auxiliary_driver lstp_spi_drv = {
+	.adrv = {
+		.name = "spi",
+		.probe = lstp_spi_probe,
+		.id_table = lstp_spi_id_table,
+	},
+};
+module_lstp_auxiliary_driver(lstp_spi_drv);
+
+MODULE_IMPORT_NS("LSTP");
+MODULE_DESCRIPTION("LSTP SPI driver");
+MODULE_LICENSE("GPL");
-- 
2.43.0


  parent reply	other threads:[~2026-10-12 17:37 UTC|newest]

Thread overview: 42+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-12 17:35 [PATCH 00/11] Add NVIDIA LSTP USB adapter drivers Adrian Ambrozewicz
2026-10-12 17:35 ` [PATCH 01/11] dt-bindings: gpio: Add NVIDIA LSTP GPIO Adrian Ambrozewicz
2026-10-13  6:09   ` Krzysztof Kozlowski
2026-10-16  8:44     ` Adrian Ambrozewicz
2026-10-12 17:36 ` [PATCH 02/11] dt-bindings: i2c: Add NVIDIA LSTP I2C Adrian Ambrozewicz
2026-10-13  6:11   ` Krzysztof Kozlowski
2026-10-12 17:36 ` [PATCH 03/11] dt-bindings: spi: Add NVIDIA LSTP SPI Adrian Ambrozewicz
2026-10-13  6:11   ` Krzysztof Kozlowski
2026-10-12 17:36 ` [PATCH 04/11] dt-bindings: usb: Add NVIDIA LSTP USB adapter Adrian Ambrozewicz
2026-10-13  6:13   ` Krzysztof Kozlowski
2026-10-13  6:17   ` Krzysztof Kozlowski
2026-10-22 10:40     ` Adrian Ambrozewicz
2026-10-12 17:36 ` [PATCH 05/11] usb: misc: Add NVIDIA LSTP USB adapter driver Adrian Ambrozewicz
2026-10-12 17:36 ` [PATCH 06/11] gpio: Add NVIDIA LSTP GPIO driver Adrian Ambrozewicz
2026-10-13 23:25   ` Linus Walleij
2026-10-16  8:19     ` Adrian Ambrozewicz
2026-10-16 21:14       ` Linus Walleij
2026-10-20 13:53         ` Adrian Ambrozewicz
2026-10-12 17:36 ` [PATCH 07/11] leds: Add NVIDIA LSTP LED driver Adrian Ambrozewicz
2026-10-12 17:36 ` [PATCH 08/11] i2c: Add NVIDIA LSTP I2C driver Adrian Ambrozewicz
2026-10-12 17:36 ` Adrian Ambrozewicz [this message]
2026-10-12 17:36 ` [PATCH 10/11] tty: Add NVIDIA LSTP UART driver Adrian Ambrozewicz
2026-10-13  5:49   ` Jiri Slaby
2026-10-16  8:19     ` Adrian Ambrozewicz
2026-10-15  1:25   ` kernel test robot
2026-10-16  7:43   ` kernel test robot
2026-10-12 17:36 ` [PATCH 11/11] ipmi: Add NVIDIA LSTP IPMI BMC driver Adrian Ambrozewicz
2026-10-12 21:10   ` Corey Minyard
2026-10-16  8:19     ` Adrian Ambrozewicz
2026-10-16 12:26       ` Corey Minyard
2026-10-20 13:53         ` Adrian Ambrozewicz
2026-10-13 11:37 ` [PATCH 00/11] Add NVIDIA LSTP USB adapter drivers Greg Kroah-Hartman
2026-10-16  8:44   ` Adrian Ambrozewicz
2026-10-22 10:40   ` Adrian Ambrozewicz
2026-10-22 13:29     ` Greg Kroah-Hartman
2026-10-13 11:39 ` Greg Kroah-Hartman
2026-10-14  3:38   ` Jeremy Kerr
2026-10-14  4:53     ` Greg Kroah-Hartman
2026-10-16  8:19       ` Adrian Ambrozewicz
2026-10-30  4:06     ` Jeremy Kerr
2026-10-30  5:32       ` Greg Kroah-Hartman
2026-11-02  5:55         ` Jeremy Kerr

Reply instructions:

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

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

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

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

  git send-email \
    --in-reply-to=20261012-lstp-usb-adapter-v1-9-aambrozewicz@nvidia.com \
    --to=aambrozewicz@nvidia.com \
    --cc=broonie@kernel.org \
    --cc=donalds@nvidia.com \
    --cc=gregkh@linuxfoundation.org \
    --cc=justinstitt@google.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-spi@vger.kernel.org \
    --cc=linux-usb@vger.kernel.org \
    --cc=llvm@lists.linux.dev \
    --cc=morbo@google.com \
    --cc=nathan@kernel.org \
    --cc=ndesaulniers@google.com \
    /path/to/YOUR_REPLY

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

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

all inboxes | Powered by JetHome®