Hi Balaji Selvanathan

On 8/25/2026 3:23 PM, Balaji Selvanathan wrote:
Hi Vandhiadevan,

On 8/17/2026 9:41 PM, Vandhiadevan Karunamoorthy via U-Boot wrote:
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.karunamoor...@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
License should be "SPDX-License-Identifier: GPL-2.0-only"
+/*
+ * 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")
We can drop this i guess "

Based on the Linux GENI SPI driver

"
Agreed. I'll update the SPDX tag to |GPL-2.0-only| and remove the Linux driver/commit references from the file header.

+ */
+
+#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));

If rx/tx isn't itself cache-line aligned, the outward-rounded range covers bytes outside [rx, rx+len) belonging to unrelated data, and invalidating them discards any live CPU-dirtied data sharing those cache lines. Maybe we can use bounce buffer for the DMA path? Same for the invalidate_dcache_range call in qcom_geni_spi_dma_wait_rx.

Good point. I'll switch to a bounce buffer for unaligned DMA RX buffers and review the cache maintenance handling in |qcom_geni_spi_dma_wait_rx()| as well.


+
+    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);

qcom_geni_spi_fifo_xfer() (SPI_ERR status) and qcom_geni_spi_dma_wait_tx/rx() can all return -EIO on genuine hardware errors, but only -ETIMEDOUT triggers abort. On an -EIO failure the GENI command is left active and CS potentially still asserted; the next transfer issues a new command on top of a still-active one. So, should be if (ret)?. Separately, qcom_geni_spi_abort()'s own return value is discarded at the call site, so a failed abort is invisible to the caller.

Thanks,

Balaji

Agreed. I'll update the error handling to abort on any transfer failure, not just |-ETIMEDOUT|, and handle abort failures appropriately.

Thanks,

VK

+
+    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]>

Reply via email to