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), selected per SPI bus via the
optional "qcom,se-dma-allowed" device tree property.

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]>
---
 .../spi/qcom,geni-spi.txt                     |  44 ++
 drivers/spi/Kconfig                           |   9 +
 drivers/spi/Makefile                          |   1 +
 drivers/spi/qcom_geni_spi.c                   | 735 ++++++++++++++++++
 include/soc/qcom/geni-se.h                    |  32 +
 5 files changed, 821 insertions(+)
 create mode 100644 doc/device-tree-bindings/spi/qcom,geni-spi.txt
 create mode 100644 drivers/spi/qcom_geni_spi.c

diff --git a/doc/device-tree-bindings/spi/qcom,geni-spi.txt 
b/doc/device-tree-bindings/spi/qcom,geni-spi.txt
new file mode 100644
index 00000000..020de31a
--- /dev/null
+++ b/doc/device-tree-bindings/spi/qcom,geni-spi.txt
@@ -0,0 +1,44 @@
+Qualcomm GENI SPI controller
+
+The GENI SPI controller is the SPI-protocol personality of a Qualcomm
+GENI/QUPv3 Serial Engine (SE). Each SE sits inside a QUPv3 wrapper
+(compatible "qcom,geni-se-qup") and needs Serial Engine firmware for the
+SPI protocol loaded into it before use; see the qcom,geni-se-qup binding
+for the firmware-loading mechanism.
+
+Required properties:
+- compatible   : must be "qcom,geni-spi"
+- reg          : base address and size of the SE registers
+
+Optional properties:
+- qcom,se-dma-allowed : if present, use the SE's own DMA engine (SE-DMA)
+                       for transfers instead of the CPU-driven FIFO path.
+
+SPI slave nodes are added as children as per the generic SPI bindings
+in spi-bus.txt (e.g. reg = <cs>, spi-max-frequency, spi-tx/rx-bus-width).
+
+Example:
+
+       qupv3_0: geni-se-qup@ac0000 {
+               compatible = "qcom,geni-se-qup";
+               reg = <0x00ac0000 0x6000>;
+               #address-cells = <1>;
+               #size-cells = <1>;
+               ranges;
+
+               spi0: spi@a80000 {
+                       compatible = "qcom,geni-spi";
+                       reg = <0x00a80000 0x4000>;
+                       clocks = <&gcc GCC_QUPV3_WRAP0_S0_CLK>;
+                       clock-names = "se";
+                       qcom,se-dma-allowed;
+                       #address-cells = <1>;
+                       #size-cells = <0>;
+
+                       flash@0 {
+                               compatible = "jedec,spi-nor";
+                               reg = <0>;
+                               spi-max-frequency = <48000000>;
+                       };
+               };
+       };
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..adeec493
--- /dev/null
+++ b/drivers/spi/qcom_geni_spi.c
@@ -0,0 +1,735 @@
+// 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 dma_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 */
+       if (priv->dma_capable) {
+               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->dma_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);
+
+       if (priv->dma_capable) {
+               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;
+       }
+
+       /* DMA capability is controlled by DT, not read back from hardware */
+       priv->dma_capable = dev_read_bool(dev, "qcom,se-dma-allowed");
+
+       if (!priv->dma_capable &&
+           (readl(priv->base + GENI_IF_DISABLE_RO) & FIFO_IF_DISABLE)) {
+               dev_err(dev, "Neither FIFO nor DMA mode usable\n");
+               clk_disable(&priv->se);
+               return -ENXIO;
+       }
+
+       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
 
-- 
2.34.1

Reply via email to