Hi Casey Connolly,

On 8/28/2026 10:12 PM, Casey Connolly wrote:

On 28/08/2026 06:39, Vandhiadevan Karunamoorthy 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.

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 v3:
ah I reviewed your v2, still most of that applies I think

+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);
+       bool use_dma = !priv->fifo_capable || len >= SPI_DMA_MIN_XFER_BYTES;
+       void *rx_buf = din;
+       void *rx_bounce = NULL;
+       u32 cmd, m_param = 0;
+       int ret;
+
+       if (din && use_dma && qcom_geni_spi_rx_needs_bounce(din, len)) {
+               rx_bounce = malloc_cache_aligned(len);
Would it be better to allocate this to some nice size in probe()? You
can use LMB to allocate like 4mb (I suspect that would also make it
easier to adapt to whatever mechanism we end up with for uncached memory
in the future). Probably also good to free it up in .remove() as well.

If there is some potential concern with the size being too small (like
dumping a 32mb EEPROM in one go or something?) then eh I guess this is
fine as-is, malloc/realloc might be preferable but it's not a big deal.

Thanks for the review and suggestions.

Your comment led me to revisit the bounce-buffer design. While considering a probe-time allocation, I found that some Qualcomm U-Boot configurations have limited memory budgets, making large permanent buffers less attractive. I also wanted to avoid using LMB here, since it allocates memory outside the U-Boot-managed region and that memory may contain valuable crash/debug data on some platforms.

The updated implementation only bounces the unaligned head/tail portions through a small fixed-size aligned buffer, while DMAing the aligned middle portion directly into the caller's buffer. This eliminates transfer-sized allocations entirely and keeps memory usage constant regardless of transfer size.

I'll post this approach in the next revision.

Regards,

VK

+               if (!rx_bounce)
+                       return -ENOMEM;
+               rx_buf = rx_bounce;
+       }
+
+       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 (use_dma) {
+               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, rx_buf, len);
+               if (dout)
+                       qcom_geni_spi_dma_tx_start(priv, dout, len);
+
+               ret = qcom_geni_spi_dma_xfer_wait(priv, dout, rx_buf, 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 (rx_bounce) {
+               if (!ret)
+                       memcpy(din, rx_bounce, len);
+               free(rx_bounce);
+       }
+
+       if (ret) {
+               int abort_ret = qcom_geni_spi_abort(dev, priv);
+
+               if (abort_ret)
+                       dev_err(dev, "abort failed: %d\n", abort_ret);
+       }
+
+       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");
dev_dbg()

Thanks,> +           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 fc9a8e82cd8..79516c1f846 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: 20260826-geni-spi-v2-review-20f0279ee7c0

Best regards,
--
Vandhiadevan Karunamoorthy <[email protected]>

Reply via email to