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