[PATCH v2] spi: Add Qualcomm GENI QUPv3 SPI controller driver

Vandhiadevan Karunamoorthy via U-Boot <[email protected]>
Newsgroups gmane.comp.boot-loaders.u-boot.general,gmane.comp.boot-loaders.u-boot
Message-ID <[email protected]>
Add a DM_SPI driver for the SPI-protocol personality of the Qualcomm
GENI Serial Engine, found inside a QUPv3 wrapper on Qualcomm SoCs
such as SDM845, SM8250 and SA8775P. The Serial Engine is shared
across UART/I2C/SPI protocols and needs firmware for the desired
protocol loaded into it before use, via the existing
qcom_geni_load_firmware() helper.

The driver supports both the CPU-driven FIFO transfer path and the
Serial Engine's own DMA engine (SE-DMA). Whether FIFO mode is usable
is read back from hardware (GENI_IF_DISABLE_RO); SE-DMA is always
available. The driver picks FIFO for small transfers when FIFO is
available, and SE-DMA otherwise, matching the mode-selection logic
of the equivalent Linux driver.

Based on the Linux GENI SPI driver and shared GENI SE helper code,
notably:
 - commit 561de45f72bd ("spi: spi-geni-qcom: Add SPI driver support
   for GENI based QUP"), for the base FIFO-mode driver structure.
 - commit e5f0dfa78ac7 ("spi: spi-geni-qcom: Add support for SE DMA
   mode"), for the SE-DMA transfer path.
 - commit fe8aa1ba0783 ("soc: qcom: geni-se: Update Tx and Rx fifo
   depth based on QUP HW version"), for the fifo-depth helper.

Add the register offsets and the fifo-depth helper needed by the new
driver to the shared include/soc/qcom/geni-se.h, and wire up the new
driver's Kconfig entry and Makefile rule.

Signed-off-by: Vandhiadevan Karunamoorthy <[email protected]>
---
Changes in v2:
- Drop cover letter
- Drop DT property
- Drop plain-text binding doc
- Link to v1:
https://lore.kernel.org/u-boot/20260814104520.3384723-2-vandhiadevan.karunamoorthy@oss.qualcomm.com/
---
 drivers/spi/Kconfig         |   9 +
 drivers/spi/Makefile        |   1 +
 drivers/spi/qcom_geni_spi.c | 728 ++++++++++++++++++++++++++++++++++++++++++++
 include/soc/qcom/geni-se.h  |  32 ++
 4 files changed, 770 insertions(+)

diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig
index cfbedd64..99870085 100644
--- a/drivers/spi/Kconfig
+++ b/drivers/spi/Kconfig
@@ -463,6 +463,15 @@ config SPI_QUP
 	  mode supports up to 50MHz, up to four chip selects, programmable
 	  data path from 4 bits to 32 bits and numerous protocol variants.
 
+config SPI_GENI_QCOM
+	bool "Qualcomm GENI SPI controller"
+	depends on DM_SPI && QCOM_GENI
+	help
+	  Enable support for the SPI Serial Engine of the Qualcomm Generic
+	  Interface (GENI) based Qualcomm Universal Peripheral (QUP) wrapper.
+	  Used to access SPI devices such as SPI-NOR flash on Qualcomm SoCs
+	  that have a GENI/QUPv3 wrapper, e.g. SDM845, SM8250, SA8775P.
+
 config RENESAS_RPC_SPI
 	bool "Renesas RPC SPI driver"
 	depends on RCAR_64 || RZA1
diff --git a/drivers/spi/Makefile b/drivers/spi/Makefile
index 13d9c5dc..c1ade77c 100644
--- a/drivers/spi/Makefile
+++ b/drivers/spi/Makefile
@@ -68,6 +68,7 @@ obj-$(CONFIG_OMAP3_SPI) += omap3_spi.o
 obj-$(CONFIG_PIC32_SPI) += pic32_spi.o
 obj-$(CONFIG_PL022_SPI) += pl022_spi.o
 obj-$(CONFIG_SPI_QUP) += spi-qup.o
+obj-$(CONFIG_SPI_GENI_QCOM) += qcom_geni_spi.o
 obj-$(CONFIG_SPI_MXIC) += spi-mxic.o
 obj-$(CONFIG_RENESAS_RPC_SPI) += renesas_rpc_spi.o
 obj-$(CONFIG_ROCKCHIP_SFC) += rockchip_sfc.o
diff --git a/drivers/spi/qcom_geni_spi.c b/drivers/spi/qcom_geni_spi.c
new file mode 100644
index 00000000..0b693e77
--- /dev/null
+++ b/drivers/spi/qcom_geni_spi.c
@@ -0,0 +1,728 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Qualcomm GENI QUPv3 SPI controller driver
+ *
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ *
+ * Based on the Linux GENI SPI driver (drivers/spi/spi-geni-qcom.c) and the
+ * shared GENI SE helper (drivers/soc/qcom/qcom-geni-se.c), notably:
+ *  - Linux commit 561de45f72bd ("spi: spi-geni-qcom: Add SPI driver support
+ *    for GENI based QUP")
+ *  - Linux commit e5f0dfa78ac7 ("spi: spi-geni-qcom: Add support for SE DMA
+ *    mode")
+ *  - Linux commit fe8aa1ba0783 ("soc: qcom: geni-se: Update Tx and Rx fifo
+ *    depth based on QUP HW version")
+ */
+
+#include <log.h>
+#include <dm.h>
+#include <dm/device.h>
+#include <dm/read.h>
+#include <dm/device_compat.h>
+#include <linux/delay.h>
+#include <linux/errno.h>
+#include <linux/err.h>
+#include <linux/bitops.h>
+#include <linux/kernel.h>
+#include <asm/cache.h>
+#include <asm/io.h>
+#include <cpu_func.h>
+#include <spi.h>
+#include <clk.h>
+#include <time.h>
+#include <soc/qcom/geni-se.h>
+#include <soc/qcom/qup-fw-load.h>
+
+/* SPI-protocol-specific SE registers, on top of the generic ones in geni-se.h */
+#define SE_SPI_CPHA			0x224
+#define SE_SPI_LOOPBACK		0x22c
+#define SE_SPI_CPOL			0x230
+#define SE_SPI_DEMUX_OUTPUT_INV	0x24c
+#define SE_SPI_DEMUX_SEL		0x250
+#define SE_SPI_TRANS_CFG		0x25c
+#define SE_SPI_DELAYS_COUNTERS		0x278
+#define SE_SPI_WORD_LEN		0x268
+#define SE_SPI_TX_TRANS_LEN		0x26c
+#define SE_SPI_RX_TRANS_LEN		0x270
+
+#define CPHA				BIT(0)
+
+#define LOOPBACK_ENABLE			0x1
+#define LOOPBACK_MSK			GENMASK(1, 0)
+
+#define CPOL				BIT(2)
+
+/* SE_SPI_TRANS_CFG */
+#define CS_TOGGLE			BIT(1)
+
+#define WORD_LEN_MSK			GENMASK(9, 0)
+#define SPI_WORD_LEN_BITS		8
+#define MIN_WORD_LEN			4
+
+#define SPI_TX_ONLY			1
+#define SPI_RX_ONLY			2
+#define SPI_TX_RX			7
+#define FRAGMENTATION			BIT(2)
+
+#define SPI_ERR	(M_CMD_OVERRUN_EN | M_ILLEGAL_CMD_EN | M_CMD_FAILURE_EN | \
+		  M_RX_FIFO_RD_ERR_EN | M_RX_FIFO_WR_ERR_EN | \
+		  M_TX_FIFO_RD_ERR_EN | M_TX_FIFO_WR_ERR_EN)
+
+#define SPI_XFER_TIMEOUT_MS		250
+
+/* SPI-NOR reads/writes are page/sector sized; skip DMA setup below this */
+#define SPI_DMA_MIN_XFER_BYTES		64
+
+struct qcom_geni_spi_priv {
+	phys_addr_t wrapper;
+	phys_addr_t base;
+	struct clk se;
+	u32 tx_wm;
+	u32 tx_fifo_depth;
+	u32 bpw;
+	u32 bytes_per_fifo_word;
+	u32 oversampling;
+	bool fifo_capable;
+};
+
+/* Bytes packed into each 32-bit FIFO word, based on word length */
+static unsigned int qcom_geni_spi_bytes_per_fifo_word(unsigned int bpw)
+{
+	if (bpw <= 8)
+		return 4;
+	else if (bpw <= 16)
+		return 2;
+	return 1;
+}
+
+/* Scale the wait budget with transfer size instead of a flat constant */
+static ulong qcom_geni_spi_xfer_timeout_ms(struct qcom_geni_spi_priv *priv,
+					   unsigned int len)
+{
+	ulong rate = clk_get_rate(&priv->se);
+	ulong ms;
+
+	if (IS_ERR_VALUE(rate) || !rate)
+		return SPI_XFER_TIMEOUT_MS + len / 1000;
+
+	/* len is in bytes; add generous margin for controller/DMA overhead */
+	ms = DIV_ROUND_UP((u64)len * 8 * 1000, rate) * 4;
+
+	return max_t(ulong, ms, SPI_XFER_TIMEOUT_MS);
+}
+
+#define NUM_PACKING_VECTORS		4
+#define PACKING_START_SHIFT		5
+#define PACKING_DIR_SHIFT		4
+#define PACKING_LEN_SHIFT		1
+#define PACKING_STOP_BIT		BIT(0)
+#define PACKING_VECTOR_SHIFT		10
+
+/* Configure how the SE packs/unpacks "bpw"-bit words into 32-bit FIFO entries */
+static void qcom_geni_spi_config_packing(struct qcom_geni_spi_priv *priv, int bpw,
+					 bool msb_to_lsb)
+{
+	u32 cfg0, cfg1, cfg[NUM_PACKING_VECTORS] = {0};
+	int len, temp_bpw = bpw;
+	int idx_start = msb_to_lsb ? bpw - 1 : 0;
+	int idx = idx_start;
+	int idx_delta = msb_to_lsb ? -BITS_PER_BYTE : BITS_PER_BYTE;
+	int i, iter, pack_words;
+	unsigned int ceil_bpw;
+
+	if (bpw <= 8)
+		pack_words = 4;
+	else if (bpw <= 16)
+		pack_words = 2;
+	else
+		pack_words = 1;
+
+	ceil_bpw = (bpw & (BITS_PER_BYTE - 1)) ?
+		((bpw & ~(BITS_PER_BYTE - 1)) + BITS_PER_BYTE) : bpw;
+
+	iter = (ceil_bpw * pack_words) >> 3;
+	if (iter <= 0 || iter > NUM_PACKING_VECTORS)
+		return;
+
+	for (i = 0; i < iter; i++) {
+		len = min_t(int, temp_bpw, BITS_PER_BYTE) - 1;
+		cfg[i] = idx << PACKING_START_SHIFT;
+		cfg[i] |= msb_to_lsb << PACKING_DIR_SHIFT;
+		cfg[i] |= len << PACKING_LEN_SHIFT;
+
+		if (temp_bpw <= BITS_PER_BYTE) {
+			idx = ((i + 1) * BITS_PER_BYTE) + idx_start;
+			temp_bpw = bpw;
+		} else {
+			idx = idx + idx_delta;
+			temp_bpw = temp_bpw - BITS_PER_BYTE;
+		}
+	}
+	cfg[iter - 1] |= PACKING_STOP_BIT;
+	cfg0 = cfg[0] | (cfg[1] << PACKING_VECTOR_SHIFT);
+	cfg1 = cfg[2] | (cfg[3] << PACKING_VECTOR_SHIFT);
+
+	writel(cfg0, priv->base + SE_GENI_TX_PACKING_CFG0);
+	writel(cfg1, priv->base + SE_GENI_TX_PACKING_CFG1);
+	writel(cfg0, priv->base + SE_GENI_RX_PACKING_CFG0);
+	writel(cfg1, priv->base + SE_GENI_RX_PACKING_CFG1);
+
+	writel(bpw / 16, priv->base + SE_GENI_BYTE_GRAN);
+}
+
+static int qcom_geni_spi_fifo_xfer(struct qcom_geni_spi_priv *priv, const u8 *tx,
+				   u8 *rx, unsigned int len, ulong timeout_ms)
+{
+	ulong start = get_timer(0);
+	unsigned int tx_cur = 0, rx_cur = 0;
+
+	while (get_timer(start) < timeout_ms) {
+		u32 status = readl(priv->base + SE_GENI_M_IRQ_STATUS);
+		unsigned int i;
+
+		if (status & SPI_ERR) {
+			writel(status, priv->base + SE_GENI_M_IRQ_CLEAR);
+			if (tx)
+				writel(0, priv->base + SE_GENI_TX_WATERMARK_REG);
+			return -EIO;
+		}
+
+		if (tx && (status & M_TX_FIFO_WATERMARK_EN)) {
+			for (i = 0; i < priv->tx_wm && tx_cur < len; i++) {
+				u32 word = 0;
+				unsigned int p;
+
+				for (p = 0; p < priv->bytes_per_fifo_word && tx_cur < len; p++)
+					word |= tx[tx_cur++] << (p * 8);
+
+				writel(word, priv->base + SE_GENI_TX_FIFOn);
+			}
+
+			if (tx_cur == len)
+				writel(0, priv->base + SE_GENI_TX_WATERMARK_REG);
+		}
+
+		if (status & (M_RX_FIFO_WATERMARK_EN | M_RX_FIFO_LAST_EN)) {
+			u32 rxstatus = readl(priv->base + SE_GENI_RX_FIFO_STATUS);
+			u32 rxcnt = rxstatus & RX_FIFO_WC_MSK;
+
+			if (rx) {
+				for (i = 0; rx_cur < len && i < rxcnt; i++) {
+					u32 word = readl(priv->base + SE_GENI_RX_FIFOn);
+					unsigned int p;
+
+					for (p = 0; p < priv->bytes_per_fifo_word &&
+					     rx_cur < len; p++) {
+						rx[rx_cur++] = word & 0xff;
+						word >>= 8;
+					}
+				}
+			} else {
+				for (i = 0; i < rxcnt; i++)
+					readl(priv->base + SE_GENI_RX_FIFOn);
+			}
+		}
+
+		writel(status, priv->base + SE_GENI_M_IRQ_CLEAR);
+
+		if (status & M_CMD_DONE_EN) {
+			/* Drain any residual RX words after CMD_DONE */
+			if (rx && rx_cur < len) {
+				u32 rxstatus = readl(priv->base + SE_GENI_RX_FIFO_STATUS);
+				u32 rxcnt = rxstatus & RX_FIFO_WC_MSK;
+				unsigned int i;
+
+				for (i = 0; rx_cur < len && i < rxcnt; i++) {
+					u32 word = readl(priv->base + SE_GENI_RX_FIFOn);
+					unsigned int p;
+
+					for (p = 0; p < priv->bytes_per_fifo_word &&
+					     rx_cur < len; p++) {
+						rx[rx_cur++] = word & 0xff;
+						word >>= 8;
+					}
+				}
+			}
+			return 0;
+		}
+	}
+
+	return -ETIMEDOUT;
+}
+
+static int qcom_geni_spi_abort(struct qcom_geni_spi_priv *priv)
+{
+	ulong start = get_timer(0);
+	u32 status;
+
+	writel(M_GENI_CMD_ABORT, priv->base + SE_GENI_M_CMD_CTRL_REG);
+
+	do {
+		status = readl(priv->base + SE_GENI_M_IRQ_STATUS);
+		if (get_timer(start) > SPI_XFER_TIMEOUT_MS)
+			return -ETIMEDOUT;
+	} while (!(status & M_CMD_ABORT_EN));
+
+	writel(status, priv->base + SE_GENI_M_IRQ_CLEAR);
+
+	/* Reset TX/RX DMA FSMs so the next transfer starts clean */
+	start = get_timer(0);
+	writel(1, priv->base + SE_DMA_TX_FSM_RST);
+	do {
+		status = readl(priv->base + SE_DMA_TX_IRQ_STAT);
+		if (get_timer(start) > SPI_XFER_TIMEOUT_MS)
+			break;
+	} while (!(status & TX_RESET_DONE));
+	writel(status, priv->base + SE_DMA_TX_IRQ_CLR);
+
+	start = get_timer(0);
+	writel(1, priv->base + SE_DMA_RX_FSM_RST);
+	do {
+		status = readl(priv->base + SE_DMA_RX_IRQ_STAT);
+		if (get_timer(start) > SPI_XFER_TIMEOUT_MS)
+			break;
+	} while (!(status & RX_RESET_DONE));
+	writel(status, priv->base + SE_DMA_RX_IRQ_CLR);
+
+	return 0;
+}
+
+/* SE-DMA: the SE moves data directly to/from a physical buffer */
+static void qcom_geni_spi_dma_tx_start(struct qcom_geni_spi_priv *priv, const u8 *tx,
+				       unsigned int len)
+{
+	phys_addr_t buf = (phys_addr_t)(uintptr_t)tx;
+
+	flush_dcache_range(ALIGN_DOWN((ulong)tx, ARCH_DMA_MINALIGN),
+			   ALIGN((ulong)tx + len, ARCH_DMA_MINALIGN));
+
+	writel(lower_32_bits(buf), priv->base + SE_DMA_TX_PTR_L);
+	writel(upper_32_bits(buf), priv->base + SE_DMA_TX_PTR_H);
+	writel(GENI_SE_DMA_EOT_BUF, priv->base + SE_DMA_TX_ATTR);
+	writel(len, priv->base + SE_DMA_TX_LEN);
+}
+
+static void qcom_geni_spi_dma_rx_start(struct qcom_geni_spi_priv *priv, u8 *rx,
+				       unsigned int len)
+{
+	phys_addr_t buf = (phys_addr_t)(uintptr_t)rx;
+
+	/* Discard stale dirty cache lines before the DMA write lands */
+	invalidate_dcache_range(ALIGN_DOWN((ulong)rx, ARCH_DMA_MINALIGN),
+				ALIGN((ulong)rx + len, ARCH_DMA_MINALIGN));
+
+	writel(lower_32_bits(buf), priv->base + SE_DMA_RX_PTR_L);
+	writel(upper_32_bits(buf), priv->base + SE_DMA_RX_PTR_H);
+	writel(0, priv->base + SE_DMA_RX_ATTR);
+	writel(len, priv->base + SE_DMA_RX_LEN);
+}
+
+static int qcom_geni_spi_dma_wait_tx(struct qcom_geni_spi_priv *priv, ulong timeout_ms)
+{
+	ulong start = get_timer(0);
+	u32 status;
+
+	while (get_timer(start) < timeout_ms) {
+		status = readl(priv->base + SE_DMA_TX_IRQ_STAT);
+		if (!status) {
+			udelay(1);
+			continue;
+		}
+
+		writel(status, priv->base + SE_DMA_TX_IRQ_CLR);
+
+		if (status & TX_SBE)
+			return -EIO;
+		if (status & TX_DMA_DONE)
+			return 0;
+	}
+
+	return -ETIMEDOUT;
+}
+
+static int qcom_geni_spi_dma_wait_rx(struct qcom_geni_spi_priv *priv, u8 *rx,
+				     unsigned int len, ulong timeout_ms)
+{
+	ulong start = get_timer(0);
+	u32 status;
+
+	while (get_timer(start) < timeout_ms) {
+		status = readl(priv->base + SE_DMA_RX_IRQ_STAT);
+		if (!status) {
+			udelay(1);
+			continue;
+		}
+
+		writel(status, priv->base + SE_DMA_RX_IRQ_CLR);
+
+		if (status & RX_SBE)
+			return -EIO;
+		if (status & RX_DMA_DONE) {
+			/* Force the AXI write to retire before trusting the buffer */
+			readl(priv->base + SE_DMA_RX_LEN);
+			invalidate_dcache_range(ALIGN_DOWN((ulong)rx, ARCH_DMA_MINALIGN),
+						ALIGN((ulong)rx + len, ARCH_DMA_MINALIGN));
+			return 0;
+		}
+	}
+
+	return -ETIMEDOUT;
+}
+
+static int qcom_geni_spi_dma_xfer_wait(struct qcom_geni_spi_priv *priv, const u8 *tx,
+				       u8 *rx, unsigned int len, ulong timeout_ms)
+{
+	int ret;
+
+	if (tx) {
+		ret = qcom_geni_spi_dma_wait_tx(priv, timeout_ms);
+		if (ret)
+			return ret;
+	}
+
+	if (rx) {
+		ret = qcom_geni_spi_dma_wait_rx(priv, rx, len, timeout_ms);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+/* TX/RX_TRANS_LEN and DMA_TX/RX_LEN are 24-bit HW fields; split large transfers */
+#define SPI_GENI_MAX_XFER_BYTES		0xFFFFFF
+
+static int qcom_geni_spi_xfer_once(struct udevice *dev, unsigned int len,
+				   const void *dout, void *din, bool xfer_end)
+{
+	struct udevice *bus = dev_get_parent(dev);
+	struct qcom_geni_spi_priv *priv = dev_get_priv(bus);
+	ulong timeout_ms = qcom_geni_spi_xfer_timeout_ms(priv, len);
+	u32 cmd, m_param = 0;
+	int ret;
+
+	writel(dout ? len : 0, priv->base + SE_SPI_TX_TRANS_LEN);
+	writel(din ? len : 0, priv->base + SE_SPI_RX_TRANS_LEN);
+
+	/* SPI_TX_ONLY | SPI_RX_ONLY is not a valid opcode, use SPI_TX_RX instead */
+	if (dout && din)
+		cmd = SPI_TX_RX;
+	else if (din)
+		cmd = SPI_RX_ONLY;
+	else if (dout)
+		cmd = SPI_TX_ONLY;
+	else
+		cmd = 0;
+
+	if (!xfer_end)
+		m_param |= FRAGMENTATION;
+
+	if (!priv->fifo_capable || len >= SPI_DMA_MIN_XFER_BYTES) {
+		setbits_le32(priv->base + SE_GENI_DMA_MODE_EN, GENI_DMA_MODE_EN);
+
+		/* DMA: issue M_CMD0 first, then arm DMA descriptors */
+		writel((cmd << M_OPCODE_SHFT) | (m_param & M_PARAMS_MSK),
+		       priv->base + SE_GENI_M_CMD0);
+		if (din)
+			qcom_geni_spi_dma_rx_start(priv, din, len);
+		if (dout)
+			qcom_geni_spi_dma_tx_start(priv, dout, len);
+
+		ret = qcom_geni_spi_dma_xfer_wait(priv, dout, din, len, timeout_ms);
+
+		/* Clear M_CMD_DONE so status doesn't accumulate across transfers */
+		writel(readl(priv->base + SE_GENI_M_IRQ_STATUS),
+		       priv->base + SE_GENI_M_IRQ_CLEAR);
+	} else {
+		clrbits_le32(priv->base + SE_GENI_DMA_MODE_EN, GENI_DMA_MODE_EN);
+
+		/*
+		 * Set watermarks before M_CMD0. Only set TX_WATERMARK when
+		 * we have TX data, since SPI_RX_ONLY doesn't use the TX FIFO.
+		 */
+		if (dout)
+			writel(1, priv->base + SE_GENI_TX_WATERMARK_REG);
+		/* RX_WATERMARK=0: SE fires RX_FIFO_LAST at end of transfer */
+		if (din)
+			writel(0, priv->base + SE_GENI_RX_WATERMARK_REG);
+
+		writel((cmd << M_OPCODE_SHFT) | (m_param & M_PARAMS_MSK),
+		       priv->base + SE_GENI_M_CMD0);
+
+		ret = qcom_geni_spi_fifo_xfer(priv, dout, din, len, timeout_ms);
+	}
+
+	if (ret == -ETIMEDOUT)
+		qcom_geni_spi_abort(priv);
+
+	return ret;
+}
+
+static int qcom_geni_spi_xfer(struct udevice *dev, unsigned int bitlen,
+			      const void *dout, void *din, unsigned long flags)
+{
+	struct udevice *bus = dev_get_parent(dev);
+	struct qcom_geni_spi_priv *priv = dev_get_priv(bus);
+	unsigned int len = DIV_ROUND_UP(bitlen, 8);
+	unsigned int done = 0;
+	int ret;
+
+	if (flags & SPI_XFER_BEGIN)
+		writel(0xffffffff, priv->base + SE_GENI_M_IRQ_CLEAR);
+
+	while (done < len) {
+		unsigned int chunk = min_t(unsigned int, len - done,
+					    SPI_GENI_MAX_XFER_BYTES);
+		bool xfer_end = (flags & SPI_XFER_END) && (done + chunk == len);
+
+		ret = qcom_geni_spi_xfer_once(dev,
+					      chunk,
+					  dout ? (const u8 *)dout + done : NULL,
+					  din ? (u8 *)din + done : NULL,
+					  xfer_end);
+		if (ret)
+			return ret;
+
+		done += chunk;
+	}
+
+	return 0;
+}
+
+static int qcom_geni_spi_set_speed(struct udevice *bus, uint speed)
+{
+	struct qcom_geni_spi_priv *priv = dev_get_priv(bus);
+	ulong parent_rate;
+	u32 div;
+
+	if (!speed)
+		return -EINVAL;
+
+	parent_rate = clk_get_rate(&priv->se);
+	if (IS_ERR_VALUE(parent_rate) || !parent_rate)
+		div = priv->oversampling;
+	else
+		div = DIV_ROUND_UP(parent_rate, priv->oversampling * speed);
+
+	div = clamp_t(u32, div, 1, CLK_DIV_MSK >> CLK_DIV_SHFT);
+
+	writel(0, priv->base + SE_GENI_CLK_SEL);
+	writel((div << CLK_DIV_SHFT) | SER_CLK_EN, priv->base + GENI_SER_M_CLK_CFG);
+
+	return 0;
+}
+
+static int qcom_geni_spi_set_mode(struct udevice *bus, uint mode)
+{
+	struct qcom_geni_spi_priv *priv = dev_get_priv(bus);
+	u32 val;
+
+	val = readl(priv->base + SE_SPI_LOOPBACK);
+	val &= ~LOOPBACK_MSK;
+	if (mode & SPI_LOOP)
+		val |= LOOPBACK_ENABLE;
+	writel(val, priv->base + SE_SPI_LOOPBACK);
+
+	val = readl(priv->base + SE_SPI_CPHA);
+	if (mode & SPI_CPHA)
+		val |= CPHA;
+	else
+		val &= ~CPHA;
+	writel(val, priv->base + SE_SPI_CPHA);
+
+	val = readl(priv->base + SE_SPI_CPOL);
+	if (mode & SPI_CPOL)
+		val |= CPOL;
+	else
+		val &= ~CPOL;
+	writel(val, priv->base + SE_SPI_CPOL);
+
+	return 0;
+}
+
+static int qcom_geni_spi_claim_bus(struct udevice *dev)
+{
+	struct udevice *bus = dev_get_parent(dev);
+	struct qcom_geni_spi_priv *priv = dev_get_priv(bus);
+	struct dm_spi_slave_plat *slave_plat = dev_get_parent_plat(dev);
+	unsigned int cs = slave_plat->cs[0];
+	unsigned int bpw = slave_plat->wordlen ? slave_plat->wordlen : SPI_WORD_LEN_BITS;
+
+	priv->bpw = bpw;
+	priv->bytes_per_fifo_word = qcom_geni_spi_bytes_per_fifo_word(bpw);
+
+	writel((bpw - MIN_WORD_LEN) & WORD_LEN_MSK, priv->base + SE_SPI_WORD_LEN);
+	writel(cs, priv->base + SE_SPI_DEMUX_SEL);
+	writel(slave_plat->mode & SPI_CS_HIGH ? BIT(cs) : 0,
+	       priv->base + SE_SPI_DEMUX_OUTPUT_INV);
+
+	qcom_geni_spi_config_packing(priv, bpw, true);
+
+	writel(0xffffffff, priv->base + SE_GENI_M_IRQ_CLEAR);
+
+	return 0;
+}
+
+static int qcom_geni_spi_release_bus(struct udevice *dev)
+{
+	return 0;
+}
+
+static u32 qcom_geni_spi_get_tx_fifo_depth(struct qcom_geni_spi_priv *priv)
+{
+	u32 val, hw_version, depth_mask;
+
+	hw_version = readl(priv->wrapper + QUP_HW_VER_REG);
+	depth_mask = geni_se_fifo_depth_mask(hw_version, TX_FIFO_DEPTH_MSK_256_BYTES,
+					     TX_FIFO_DEPTH_MSK);
+
+	val = readl(priv->base + SE_HW_PARAM_0);
+
+	return (val & depth_mask) >> TX_FIFO_DEPTH_SHFT;
+}
+
+/* QUP v1.0 undersamples the SPI clock and needs 2x the requested bit rate */
+static u32 qcom_geni_spi_get_oversampling(struct qcom_geni_spi_priv *priv)
+{
+	u32 hw_version = readl(priv->wrapper + QUP_HW_VER_REG);
+	u32 hw_major = GENI_SE_VERSION_MAJOR(hw_version);
+	u32 hw_minor = GENI_SE_VERSION_MINOR(hw_version);
+
+	if (hw_major == 1 && hw_minor == 0)
+		return 2;
+
+	return 1;
+}
+
+static void qcom_geni_spi_hw_init(struct qcom_geni_spi_priv *priv)
+{
+	u32 val;
+
+	writel(0xffffffff, priv->base + SE_GENI_M_IRQ_CLEAR);
+
+	val = readl(priv->base + GENI_CGC_CTRL);
+	val |= DEFAULT_CGC_EN;
+	writel(val, priv->base + GENI_CGC_CTRL);
+
+	writel(DEFAULT_IO_OUTPUT_CTRL_MSK, priv->base + GENI_OUTPUT_CTRL);
+	writel(FORCE_DEFAULT, priv->base + GENI_FORCE_DEFAULT_REG);
+
+	val = readl(priv->base + SE_IRQ_EN);
+	val |= GENI_M_IRQ_EN;
+	writel(val, priv->base + SE_IRQ_EN);
+
+	writel(priv->tx_wm, priv->base + SE_GENI_TX_WATERMARK_REG);
+
+	val = readl(priv->base + SE_GENI_M_IRQ_EN);
+	val |= M_COMMON_GENI_M_IRQ_EN | M_CMD_DONE_EN | SPI_ERR;
+	writel(val, priv->base + SE_GENI_M_IRQ_EN);
+
+	writel(0xffffffff, priv->base + SE_DMA_TX_IRQ_CLR);
+	writel(0xffffffff, priv->base + SE_DMA_RX_IRQ_CLR);
+	writel(TX_DMA_DONE | TX_SBE, priv->base + SE_DMA_TX_IRQ_EN_SET);
+	writel(RX_DMA_DONE | RX_SBE, priv->base + SE_DMA_RX_IRQ_EN_SET);
+
+	/* We always control CS manually, don't let the SE auto-toggle it */
+	val = readl(priv->base + SE_SPI_TRANS_CFG);
+	val &= ~CS_TOGGLE;
+	writel(val, priv->base + SE_SPI_TRANS_CFG);
+}
+
+static int qcom_geni_spi_probe(struct udevice *dev)
+{
+	struct qcom_geni_spi_priv *priv = dev_get_priv(dev);
+	u32 proto;
+	int ret;
+
+	priv->wrapper = dev_read_addr(dev->parent);
+	if (priv->wrapper == FDT_ADDR_T_NONE)
+		return -EINVAL;
+
+	priv->base = dev_read_addr(dev);
+	if (priv->base == FDT_ADDR_T_NONE)
+		return -EINVAL;
+
+	ret = clk_get_by_name(dev, "se", &priv->se);
+	if (ret) {
+		dev_err(dev, "clk_get_by_name(se) failed: %d\n", ret);
+		return ret;
+	}
+
+	ret = clk_enable(&priv->se);
+	if (ret) {
+		dev_err(dev, "clk_enable(se) failed: %d\n", ret);
+		return ret;
+	}
+
+	proto = readl(priv->base + GENI_FW_REVISION_RO);
+	proto &= FW_REV_PROTOCOL_MSK;
+	proto >>= FW_REV_PROTOCOL_SHFT;
+
+	if (proto == GENI_SE_INVALID_PROTO) {
+		dev_info(dev, "firmware not loaded, loading now\n");
+		ret = qcom_geni_load_firmware(priv->base, dev);
+		if (ret) {
+			dev_err(dev, "firmware load failed: %d\n", ret);
+			clk_disable(&priv->se);
+			return ret;
+		}
+		proto = readl(priv->base + GENI_FW_REVISION_RO);
+		proto &= FW_REV_PROTOCOL_MSK;
+		proto >>= FW_REV_PROTOCOL_SHFT;
+		dev_info(dev, "firmware loaded, proto=0x%x\n", proto);
+	} else {
+		dev_info(dev, "firmware already loaded, proto=0x%x\n", proto);
+	}
+
+	if (proto != GENI_SE_SPI) {
+		dev_err(dev, "Invalid proto %d\n", proto);
+		clk_disable(&priv->se);
+		return -ENXIO;
+	}
+
+	/*
+	 * SE-DMA is an inherent capability of the GENI SE core (mirrors
+	 * upstream Linux's spi-geni-qcom.c, which never gates SE-DMA by DT
+	 * or hardware version). Only FIFO availability needs checking here.
+	 */
+	priv->fifo_capable = !(readl(priv->base + GENI_IF_DISABLE_RO) & FIFO_IF_DISABLE);
+
+	priv->tx_fifo_depth = qcom_geni_spi_get_tx_fifo_depth(priv);
+	if (!priv->tx_fifo_depth) {
+		dev_err(dev, "Invalid TX FIFO depth\n");
+		clk_disable(&priv->se);
+		return -ENXIO;
+	}
+	priv->tx_wm = priv->tx_fifo_depth - 1;
+	priv->oversampling = qcom_geni_spi_get_oversampling(priv);
+
+	qcom_geni_spi_hw_init(priv);
+
+	return 0;
+}
+
+static const struct dm_spi_ops qcom_geni_spi_ops = {
+	.claim_bus	= qcom_geni_spi_claim_bus,
+	.release_bus	= qcom_geni_spi_release_bus,
+	.xfer		= qcom_geni_spi_xfer,
+	.set_speed	= qcom_geni_spi_set_speed,
+	.set_mode	= qcom_geni_spi_set_mode,
+	/*
+	 * cs_info is not needed, since we require all chip selects to be
+	 * in the device tree explicitly
+	 */
+};
+
+static const struct udevice_id qcom_geni_spi_ids[] = {
+	{ .compatible = "qcom,geni-spi" },
+	{ }
+};
+
+U_BOOT_DRIVER(qcom_geni_spi) = {
+	.name	= "qcom_geni_spi",
+	.id	= UCLASS_SPI,
+	.of_match = qcom_geni_spi_ids,
+	.probe	= qcom_geni_spi_probe,
+	.priv_auto = sizeof(struct qcom_geni_spi_priv),
+	.ops	= &qcom_geni_spi_ops,
+};
diff --git a/include/soc/qcom/geni-se.h b/include/soc/qcom/geni-se.h
index fc9a8e82..79516c1f 100644
--- a/include/soc/qcom/geni-se.h
+++ b/include/soc/qcom/geni-se.h
@@ -68,11 +68,19 @@ enum geni_se_protocol_type {
 #define SE_DMA_TX_IRQ_CLR		0xc44
 #define SE_DMA_TX_IRQ_EN_SET		0xc4c
 #define SE_DMA_TX_FSM_RST		0xc58
+#define SE_DMA_TX_PTR_L			0xc30
+#define SE_DMA_TX_PTR_H			0xc34
+#define SE_DMA_TX_ATTR			0xc38
+#define SE_DMA_TX_LEN			0xc3c
 #define SE_DMA_RX_IRQ_STAT		0xd40
 #define SE_DMA_RX_IRQ_CLR		0xd44
 #define SE_DMA_RX_IRQ_EN_SET		0xd4c
 #define SE_DMA_RX_LEN_IN		0xd54
 #define SE_DMA_RX_FSM_RST		0xd58
+#define SE_DMA_RX_PTR_L			0xd30
+#define SE_DMA_RX_PTR_H			0xd34
+#define SE_DMA_RX_ATTR			0xd38
+#define SE_DMA_RX_LEN			0xd3c
 #define SE_GSI_EVENT_EN			0xe18
 #define SE_IRQ_EN			0xe1c
 #define SE_HW_PARAM_0			0xe24
@@ -277,6 +285,30 @@ enum geni_se_protocol_type {
 #define GENI_SE_VERSION_MINOR(ver) ((ver & HW_VER_MINOR_MASK) >> HW_VER_MINOR_SHFT)
 #define GENI_SE_VERSION_STEP(ver) (ver & HW_VER_STEP_MASK)
 
+/*
+ * geni_se_fifo_depth_mask() - Pick the TX/RX SE_HW_PARAM_x fifo depth mask
+ * for a given QUP_HW_VER_REG value.
+ * @hw_version: value read from QUP_HW_VER_REG
+ * @depth_mask_256: mask to use on HW that supports 256-byte-deep fifos
+ *                   (QUP HW version >= 3.10, 8-bit depth field)
+ * @depth_mask: mask to use on older HW (6-bit depth field)
+ *
+ * QUP HW version >= 3.10 widened the fifo depth field in SE_HW_PARAM_0
+ * (TX) and SE_HW_PARAM_1 (RX) from 6 bits to 8 bits; both fields are
+ * gated by the same major/minor check.
+ */
+static inline u32 geni_se_fifo_depth_mask(u32 hw_version, u32 depth_mask_256,
+					  u32 depth_mask)
+{
+	u32 hw_major = GENI_SE_VERSION_MAJOR(hw_version);
+	u32 hw_minor = GENI_SE_VERSION_MINOR(hw_version);
+
+	if ((hw_major == 3 && hw_minor >= 10) || hw_major > 3)
+		return depth_mask_256;
+
+	return depth_mask;
+}
+
 /* QUP SE VERSION value for major number 2 and minor number 5 */
 #define QUP_SE_VERSION_2_5                  0x20050000
 

---
base-commit: ece349ade2973e220f524ce59e59711cc919263f
change-id: 20260817-geni-spi-v2-b562e4bddc08

Best regards,
--  
Vandhiadevan Karunamoorthy <[email protected]>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.