From 71f6390dd2b12584e470f2ba1ef8f7b6d9643e25 Mon Sep 17 00:00:00 2001 From: Vandhiadevan Karunamoorthy Date: Tue, 15 Sep 2026 10:52:42 +0530 Subject: [PATCH 01/13] misc: qcom_geni: fix RX_RFR_WATERMARK to use RX FIFO depth, not width load_se_firmware() derives the RX_RFR_WATERMARK value from QUPV3_SE_HW_PARAM_1 using RX_FIFO_WIDTH_BIT/RX_FIFO_WIDTH_MASK, which decode the RX FIFO element width (bits per FIFO word), not its depth (number of entries). The watermark register expects a depth-based threshold, so this produces an incorrect watermark value and, on QUP HW versions >= 3.10 where the depth field widened to 8 bits for 256-byte-deep FIFOs, an inconsistent one depending on core revision. This mirrors geni_i2c_get_tx_fifo_depth() in drivers/i2c/geni_i2c.c, which already reads SE_HW_PARAM_0 with the HW-version-gated TX_FIFO_DEPTH_MSK/TX_FIFO_DEPTH_MSK_256_BYTES masks for the same reason on the TX side. The same depth-vs-width distinction, and the HW-version gating for QUP HW >= 3.10, was introduced upstream in Linux by commit fe8aa1ba0783 ("soc: qcom: geni-se: Update Tx and Rx fifo depth based on QUP HW version"). The Linux driver this file's firmware-loading sequence was ported from, added by commit d4bf06592ad6 ("soc: qcom: geni-se: Add support to load QUP SE Firmware via Linux subsystem"), computes the RX_RFR_WATERMARK value via geni_se_get_rx_fifo_depth() which applies that same depth mask -- confirming this is a porting bug rather than an intentional difference. Add geni_se_fifo_depth_mask() to the shared include/soc/qcom/geni-se.h to pick the correct depth mask for a given QUP HW version. Signed-off-by: Vandhiadevan Karunamoorthy --- drivers/misc/qcom_geni.c | 9 +++++++-- include/soc/qcom/geni-se.h | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/drivers/misc/qcom_geni.c b/drivers/misc/qcom_geni.c index 40044e5d48a6..039a23e8d66d 100644 --- a/drivers/misc/qcom_geni.c +++ b/drivers/misc/qcom_geni.c @@ -176,6 +176,7 @@ static int load_se_firmware(struct qup_se_rsc *rsc, bool elf, void *info) const u32 *fw_val_arr, *cfg_val_arr; const u8 *cfg_idx_arr; u32 i, reg_value, mask, ramn_cnt; + u32 hw_version, rx_fifo_depth_mask; int ret; if (elf) { @@ -251,8 +252,12 @@ static int load_se_firmware(struct qup_se_rsc *rsc, bool elf, void *info) rsc->base + GENI_CFG_REG0 + (cfg_idx_arr[i] * sizeof(u32))); /* Configure condition for assertion of RX_RFR_WATERMARK condition. */ - reg_value = readl_relaxed(rsc->base + QUPV3_SE_HW_PARAM_1); - mask = (reg_value >> RX_FIFO_WIDTH_BIT) & RX_FIFO_WIDTH_MASK; + hw_version = readl_relaxed(rsc->wrapper_base + QUP_HW_VER_REG); + rx_fifo_depth_mask = geni_se_fifo_depth_mask(hw_version, RX_FIFO_DEPTH_MSK_256_BYTES, + RX_FIFO_DEPTH_MSK); + + reg_value = readl_relaxed(rsc->base + SE_HW_PARAM_1); + mask = (reg_value & rx_fifo_depth_mask) >> RX_FIFO_DEPTH_SHFT; writel_relaxed(mask - 2, rsc->base + GENI_RX_RFR_WATERMARK_REG); /* Let hardware control CGC */ diff --git a/include/soc/qcom/geni-se.h b/include/soc/qcom/geni-se.h index 3063b37010de..ae70c2ece8cd 100644 --- a/include/soc/qcom/geni-se.h +++ b/include/soc/qcom/geni-se.h @@ -282,6 +282,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 From 7573efb3096620b579e6b31757c2386e36aff9cf Mon Sep 17 00:00:00 2001 From: Vandhiadevan Karunamoorthy Date: Tue, 15 Sep 2026 10:56:43 +0530 Subject: [PATCH 02/13] spi: Add Qualcomm GENI QUPv3 SPI controller driver 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 SE-DMA register offsets 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. The driver's TX fifo depth calculation reuses geni_se_fifo_depth_mask(), added to the same header by the preceding fix. Signed-off-by: Vandhiadevan Karunamoorthy --- drivers/spi/Kconfig | 9 + drivers/spi/Makefile | 1 + drivers/spi/qcom_geni_spi.c | 779 ++++++++++++++++++++++++++++++++++++ include/soc/qcom/geni-se.h | 8 + 4 files changed, 797 insertions(+) create mode 100644 drivers/spi/qcom_geni_spi.c diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig index 007ad5e77334..691b317a9fe0 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 13d9c5dce80b..5271e20ebd8c 100644 --- a/drivers/spi/Makefile +++ b/drivers/spi/Makefile @@ -67,6 +67,7 @@ obj-$(CONFIG_OCTEON_SPI) += octeon_spi.o obj-$(CONFIG_OMAP3_SPI) += omap3_spi.o obj-$(CONFIG_PIC32_SPI) += pic32_spi.o obj-$(CONFIG_PL022_SPI) += pl022_spi.o +obj-$(CONFIG_SPI_GENI_QCOM) += qcom_geni_spi.o obj-$(CONFIG_SPI_QUP) += spi-qup.o obj-$(CONFIG_SPI_MXIC) += spi-mxic.o obj-$(CONFIG_RENESAS_RPC_SPI) += renesas_rpc_spi.o diff --git a/drivers/spi/qcom_geni_spi.c b/drivers/spi/qcom_geni_spi.c new file mode 100644 index 000000000000..7d114fed60b3 --- /dev/null +++ b/drivers/spi/qcom_geni_spi.c @@ -0,0 +1,779 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Qualcomm GENI QUPv3 SPI controller driver + * + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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 udevice *dev, 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) { + dev_err(dev, "DMA TX RESET failed\n"); + 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) { + dev_err(dev, "DMA RX RESET failed\n"); + 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 + +/* RX DMA requires a cache-line-aligned start and length to invalidate safely */ +static bool qcom_geni_spi_rx_needs_bounce(const void *rx, unsigned int len) +{ + return !IS_ALIGNED((ulong)rx, ARCH_DMA_MINALIGN) || + !IS_ALIGNED(len, ARCH_DMA_MINALIGN); +} + +static int qcom_geni_spi_xfer_chunk(struct udevice *dev, struct qcom_geni_spi_priv *priv, + unsigned int len, const void *dout, void *din, + bool use_dma, bool last_chunk, ulong timeout_ms) +{ + 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 (!last_chunk) + 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, 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); + } + + return ret; +} + +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; + u8 edge_buf[2][ARCH_DMA_MINALIGN] __aligned(ARCH_DMA_MINALIGN); + unsigned int head_len = 0, tail_len = 0, mid_len = len, done = 0; + int ret = 0; + + if (din && use_dma && qcom_geni_spi_rx_needs_bounce(din, len)) { + head_len = min_t(unsigned int, + ALIGN((ulong)din, ARCH_DMA_MINALIGN) - (ulong)din, + len); + tail_len = (len - head_len) % ARCH_DMA_MINALIGN; + mid_len = len - head_len - tail_len; + } + + if (head_len) { + ret = qcom_geni_spi_xfer_chunk(dev, priv, head_len, dout, edge_buf[0], + use_dma, xfer_end && !mid_len && !tail_len, + timeout_ms); + if (!ret) + memcpy(din, edge_buf[0], head_len); + done += head_len; + } + + if (!ret && mid_len) { + ret = qcom_geni_spi_xfer_chunk(dev, priv, mid_len, + dout ? (const u8 *)dout + done : NULL, + din ? (u8 *)din + done : NULL, + use_dma, xfer_end && !tail_len, timeout_ms); + done += mid_len; + } + + if (!ret && tail_len) { + ret = qcom_geni_spi_xfer_chunk(dev, priv, tail_len, + dout ? (const u8 *)dout + done : NULL, + edge_buf[1], use_dma, xfer_end, timeout_ms); + if (!ret) + memcpy((u8 *)din + done, edge_buf[1], tail_len); + } + + 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_dbg(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 ae70c2ece8cd..d76365b3b9cf 100644 --- a/include/soc/qcom/geni-se.h +++ b/include/soc/qcom/geni-se.h @@ -64,10 +64,18 @@ enum geni_se_protocol_type { #define SE_GENI_RX_WATERMARK_REG 0x810 #define SE_GENI_RX_RFR_WATERMARK_REG 0x814 #define SE_GENI_IOS 0x908 +#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_TX_IRQ_STAT 0xc40 #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_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_DMA_RX_IRQ_STAT 0xd40 #define SE_DMA_RX_IRQ_CLR 0xd44 #define SE_DMA_RX_IRQ_EN_SET 0xd4c From 18f684410290eb2ce069dddc7939ee887c2055b9 Mon Sep 17 00:00:00 2001 From: Vandhiadevan Karunamoorthy Date: Tue, 25 Aug 2026 11:39:09 +0530 Subject: [PATCH 03/13] mtd: mtdblock: fix lba not being set on MTD block devices mtd_blk_probe() never initializes bdesc->lba, so every MTD block device reports a block count of zero regardless of the underlying MTD device's actual size. This makes MTD block devices unusable for partition table (GPT/MBR) scanning, since partition code relies on lba to know the device's extent. Set lba from the MTD device's size and the block descriptor's blksz, mirroring how other block drivers populate this field during probe. Signed-off-by: Vandhiadevan Karunamoorthy --- drivers/mtd/mtdblock.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/mtd/mtdblock.c b/drivers/mtd/mtdblock.c index 66a79b8c56ae..27ba08304607 100644 --- a/drivers/mtd/mtdblock.c +++ b/drivers/mtd/mtdblock.c @@ -211,6 +211,8 @@ static int mtd_blk_probe(struct udevice *dev) pr_warn("MTD device '%s' is NAND, please use UBI devices instead\n", mtd->name); + bdesc->lba = lldiv(mtd->size, bdesc->blksz); + return 0; } From 264ecc71cc1a0092892320738816d99c6903d9b9 Mon Sep 17 00:00:00 2001 From: Vandhiadevan Karunamoorthy Date: Thu, 17 Sep 2026 11:47:53 +0530 Subject: [PATCH 04/13] mtd: spi: sf_probe: bind SPI-NOR into MTD block subsystem Bind the SPI-NOR jedec_spi_nor device into the MTD block subsystem from a new jedec_spi_nor_bind() .bind() hook, matching the spinand_bind()/mmc_bind() precedent of creating blk children once at bind time rather than on every probe. Set the block descriptor's logical block size, log2blksz, and lba from a new CONFIG_MTD_BLOCK_SIZE Kconfig value (default 4 KB) instead of a DT property, matching the sector size GPT images are generated with. No changes to mtd_bind()'s signature or behavior for existing callers. .bind() runs exactly once per device lifetime, so the mtdblock child is created unconditionally there. Probe-time only looks the child up via blk_find_from_parent() to refresh the mtd_info pointer and block geometry (blksz, log2blksz, lba, part_type), which must still happen on every probe since flash->mtd is only valid after spi_nor_scan() succeeds. If the lookup fails, that is a real error (the child failed to bind), reported via dev_err() and unwound through the existing err_read_id path, which is a generic post-spi_claim_bus() cleanup and not specific to ID-read failures. Wrap the block in a preprocessor '#if CONFIG_IS_ENABLED(MTD_BLOCK)' guard instead of a runtime check, since CONFIG_MTD_BLOCK_SIZE is only emitted to autoconf.h when MTD_BLOCK is enabled; referencing it unconditionally fails to build with MTD_BLOCK disabled. Signed-off-by: Vandhiadevan Karunamoorthy --- drivers/mtd/Kconfig | 9 +++++++++ drivers/mtd/spi/sf_probe.c | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/drivers/mtd/Kconfig b/drivers/mtd/Kconfig index 38d6dd142ddc..0c33c9c7fd1a 100644 --- a/drivers/mtd/Kconfig +++ b/drivers/mtd/Kconfig @@ -39,6 +39,15 @@ config MTD_BLOCK Enable support for block device access to MTD devices using blk_ops abstraction. +config MTD_BLOCK_SIZE + hex "MTD logical block size" + depends on MTD_BLOCK + default 0x1000 + help + Logical block size exposed by mtdblock/ubiblock devices to the + block layer. This is the sector size used for GPT partitioning, + not the underlying flash erase size. + config SYS_MTDPARTS_RUNTIME bool "Allow MTDPARTS to be configured at runtime" depends on !COMPILE_TEST diff --git a/drivers/mtd/spi/sf_probe.c b/drivers/mtd/spi/sf_probe.c index dee03662d45d..1540dfdb6171 100644 --- a/drivers/mtd/spi/sf_probe.c +++ b/drivers/mtd/spi/sf_probe.c @@ -9,12 +9,15 @@ #include #include +#include #include #include #include +#include #include #include #include +#include #include "sf_internal.h" @@ -100,6 +103,28 @@ static int spi_flash_probe_slave(struct spi_flash *flash) if (ret) goto err_read_id; +#if CONFIG_IS_ENABLED(MTD_BLOCK) + if (CONFIG_IS_ENABLED(DM_SPI_FLASH)) { + struct mtd_info **plat = dev_get_plat(flash->dev); + struct udevice *bdev; + struct blk_desc *bdesc; + + /* The mtdblock child is created by jedec_spi_nor_bind(). */ + ret = blk_find_from_parent(flash->dev, &bdev); + if (ret) { + dev_err(flash->dev, "mtdblock child not found: %d\n", ret); + goto err_read_id; + } + + bdesc = dev_get_uclass_plat(bdev); + *plat = &flash->mtd; + bdesc->blksz = CONFIG_MTD_BLOCK_SIZE; + bdesc->log2blksz = LOG2(bdesc->blksz); + bdesc->lba = lldiv(flash->mtd.size, bdesc->blksz); + bdesc->part_type = PART_TYPE_UNKNOWN; + } +#endif + if (CONFIG_IS_ENABLED(SPI_DIRMAP)) { ret = spi_nor_create_read_dirmap(flash); if (ret) @@ -253,13 +278,23 @@ static const struct udevice_id spi_flash_std_ids[] = { { } }; +static int jedec_spi_nor_bind(struct udevice *dev) +{ + if (!CONFIG_IS_ENABLED(MTD_BLOCK)) + return 0; + + return mtd_bind(dev, dev_get_plat(dev)); +} + U_BOOT_DRIVER(jedec_spi_nor) = { .name = "jedec_spi_nor", .id = UCLASS_SPI_FLASH, .of_match = spi_flash_std_ids, + .bind = jedec_spi_nor_bind, .probe = spi_flash_std_probe, .remove = spi_flash_std_remove, .priv_auto = sizeof(struct spi_nor), + .plat_auto = sizeof(struct mtd_info *), .ops = &spi_flash_std_ops, .flags = DM_FLAG_OS_PREPARE, }; From 633d03e344299b09044d86dabe058efc684e86a4 Mon Sep 17 00:00:00 2001 From: Vandhiadevan Karunamoorthy Date: Sun, 13 Sep 2026 16:56:50 +0530 Subject: [PATCH 05/13] blk: uclass: dispatch UCLASS_MTD lookups by block descriptor For uclass_id == UCLASS_MTD, resolve blk_get_devnum_by_uclass_idname() via blk_get_devnum_by_uclass_id(UCLASS_MTD, devnum), matching on the block descriptor's own uclass_id rather than its parent device's uclass. SPI-NOR backed mtdblock/ubiblock devices are parented by a UCLASS_SPI_FLASH device, so the generic parent-uclass matching loop never finds them; SPI-NAND and parallel NOR are unaffected since their MTD block devices are parented directly by a UCLASS_MTD device. --- drivers/block/blk-uclass.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/block/blk-uclass.c b/drivers/block/blk-uclass.c index 73c24fd91763..37f39817bc38 100644 --- a/drivers/block/blk-uclass.c +++ b/drivers/block/blk-uclass.c @@ -128,6 +128,9 @@ struct blk_desc *blk_get_devnum_by_uclass_idname(const char *uclass_idname, int return NULL; } + if (uclass_id == UCLASS_MTD) + return blk_get_devnum_by_uclass_id(UCLASS_MTD, devnum); + ret = uclass_get(UCLASS_BLK, &uc); if (ret) return NULL; From 0016d5a6e857e925a16229a03fba6912e7ad5329 Mon Sep 17 00:00:00 2001 From: Vandhiadevan Karunamoorthy Date: Tue, 25 Aug 2026 16:14:11 +0530 Subject: [PATCH 06/13] fastboot: block: add 'oem set-block-target' command and runtime override Add fastboot_block_set_target() so an OEM command can point the generic block fastboot backend at a different interface/device at runtime, without requiring a rebuild. Falls back to the existing build-time CONFIG_FASTBOOT_FLASH_BLOCK_INTERFACE_NAME/ CONFIG_FASTBOOT_FLASH_BLOCK_DEVICE_ID defaults when no override is set. Expose this to fastboot clients via a new "oem set-block-target [device]" command, gated behind CONFIG_FASTBOOT_CMD_OEM_SET_BLOCK_TARGET (depends on CONFIG_FASTBOOT_FLASH_BLOCK). This is needed to target storage devices, such as SPI-NOR exposed through the MTD block uclass, that aren't known until runtime or that coexist with another default block interface on the same board. Signed-off-by: Vandhiadevan Karunamoorthy --- drivers/fastboot/Kconfig | 11 +++++ drivers/fastboot/fb_block.c | 75 ++++++++++++++++++++++++++++++----- drivers/fastboot/fb_command.c | 37 +++++++++++++++++ include/fastboot.h | 1 + include/fb_block.h | 9 +++++ 5 files changed, 122 insertions(+), 11 deletions(-) diff --git a/drivers/fastboot/Kconfig b/drivers/fastboot/Kconfig index a4539844d898..c7e0509eaad0 100644 --- a/drivers/fastboot/Kconfig +++ b/drivers/fastboot/Kconfig @@ -225,6 +225,17 @@ config FASTBOOT_FLASH_BLOCK_DEVICE_ID Device identifiers are numbered starting from 0 and the most common case is to use the first controller on the system. +config FASTBOOT_CMD_OEM_SET_BLOCK_TARGET + bool "Enable the 'oem set-block-target' command" + depends on FASTBOOT_FLASH_BLOCK + help + Add support for the "oem set-block-target" command from a client. + This allows switching the block interface (e.g. scsi, mtd) and + device number used by the fastboot "flash"/"erase" commands at + runtime, instead of relying solely on the + CONFIG_FASTBOOT_FLASH_BLOCK_INTERFACE_NAME / + CONFIG_FASTBOOT_FLASH_BLOCK_DEVICE_ID build-time defaults. + config FASTBOOT_GPT_NAME string "Target name for updating GPT" depends on (FASTBOOT_FLASH_MMC || FASTBOOT_FLASH_BLOCK) && EFI_PARTITION diff --git a/drivers/fastboot/fb_block.c b/drivers/fastboot/fb_block.c index a8c74e35d0d1..d3e5f6ed1f3f 100644 --- a/drivers/fastboot/fb_block.c +++ b/drivers/fastboot/fb_block.c @@ -125,6 +125,66 @@ static lbaint_t fb_block_sparse_reserve(struct sparse_storage *info, return blkcnt; } +/** + * block_interface_override - runtime-set block interface name, empty if unset + */ +static char block_interface_override[16]; +/** + * block_device_override - runtime-set block device number, -1 if unset + */ +static int block_device_override = -1; + +/** + * fb_block_interface() - Resolve the block interface name to use + * + * Returns the runtime override set via fastboot_block_set_target() if one + * is active, otherwise falls back to the build-time config default. + */ +static const char *fb_block_interface(void) +{ + if (!IS_ENABLED(CONFIG_FASTBOOT_FLASH_BLOCK)) + return NULL; + + if (block_interface_override[0]) + return block_interface_override; + + return config_opt_enabled(CONFIG_FASTBOOT_FLASH_BLOCK, + CONFIG_FASTBOOT_FLASH_BLOCK_INTERFACE_NAME, NULL); +} + +/** + * fb_block_device() - Resolve the block device number to use + * + * Returns the runtime override set via fastboot_block_set_target() if one + * is active, otherwise falls back to the build-time config default. + */ +static int fb_block_device(void) +{ + if (!IS_ENABLED(CONFIG_FASTBOOT_FLASH_BLOCK)) + return -1; + + if (block_device_override >= 0) + return block_device_override; + + return config_opt_enabled(CONFIG_FASTBOOT_FLASH_BLOCK, + CONFIG_FASTBOOT_FLASH_BLOCK_DEVICE_ID, -1); +} + +int fastboot_block_set_target(const char *interface, int device) +{ + if (interface) { + if (strlen(interface) >= sizeof(block_interface_override)) + return -EINVAL; + strlcpy(block_interface_override, interface, + sizeof(block_interface_override)); + } + + if (device >= 0) + block_device_override = device; + + return 0; +} + /** * parse_device_partition() - Parse and validate device:partition format * @part_name: Input string in format "N:partition" or "partition" @@ -140,8 +200,7 @@ static int parse_device_partition(const char *part_name, int *device, { const char *colon_pos; - *device = config_opt_enabled(CONFIG_FASTBOOT_FLASH_BLOCK, - CONFIG_FASTBOOT_FLASH_BLOCK_DEVICE_ID, -1); + *device = fb_block_device(); /* Check for colon in partition name */ colon_pos = strchr(part_name, ':'); @@ -190,9 +249,7 @@ int fastboot_block_get_part_info(const char *part_name, char *response) { int ret; - const char *interface = config_opt_enabled(CONFIG_FASTBOOT_FLASH_BLOCK, - CONFIG_FASTBOOT_FLASH_BLOCK_INTERFACE_NAME, - NULL); + const char *interface = fb_block_interface(); int device; const char *partition_name; @@ -385,9 +442,7 @@ void fastboot_block_flash_write(const char *part_name, void *download_buffer, #if CONFIG_IS_ENABLED(EFI_PARTITION) if (is_partition_table_name(part_name, CONFIG_FASTBOOT_GPT_NAME)) { int device; - const char *interface = config_opt_enabled(CONFIG_FASTBOOT_FLASH_BLOCK, - CONFIG_FASTBOOT_FLASH_BLOCK_INTERFACE_NAME, - NULL); + const char *interface = fb_block_interface(); parse_device_partition(part_name, &device, NULL); fastboot_flash_gpt_partition_table(interface, device, @@ -399,9 +454,7 @@ void fastboot_block_flash_write(const char *part_name, void *download_buffer, #if CONFIG_IS_ENABLED(DOS_PARTITION) if (is_partition_table_name(part_name, CONFIG_FASTBOOT_MBR_NAME)) { int device; - const char *interface = config_opt_enabled(CONFIG_FASTBOOT_FLASH_BLOCK, - CONFIG_FASTBOOT_FLASH_BLOCK_INTERFACE_NAME, - NULL); + const char *interface = fb_block_interface(); parse_device_partition(part_name, &device, NULL); fastboot_flash_mbr_partition_table(interface, device, diff --git a/drivers/fastboot/fb_command.c b/drivers/fastboot/fb_command.c index 18d86988f4c9..bba009a387c8 100644 --- a/drivers/fastboot/fb_command.c +++ b/drivers/fastboot/fb_command.c @@ -45,6 +45,7 @@ static void oem_partconf(char *, char *); static void oem_bootbus(char *, char *); static void oem_console(char *, char *); static void oem_board(char *, char *); +static void oem_set_block_target(char *, char *); static void run_ucmd(char *, char *); static void run_acmd(char *, char *); @@ -120,6 +121,11 @@ static const struct { .command = "oem board", .dispatch = CONFIG_IS_ENABLED(FASTBOOT_OEM_BOARD, (oem_board), (NULL)) }, + [FASTBOOT_COMMAND_OEM_SET_BLOCK_TARGET] = { + .command = "oem set-block-target", + .dispatch = CONFIG_IS_ENABLED(FASTBOOT_CMD_OEM_SET_BLOCK_TARGET, + (oem_set_block_target), (NULL)) + }, [FASTBOOT_COMMAND_UCMD] = { .command = "UCmd", .dispatch = CONFIG_IS_ENABLED(FASTBOOT_UUU_SUPPORT, (run_ucmd), (NULL)) @@ -589,3 +595,34 @@ static void __maybe_unused oem_board(char *cmd_parameter, char *response) { fastboot_oem_board(cmd_parameter, (void *)fastboot_buf_addr, image_size, response); } + +/** + * oem_set_block_target() - Execute the OEM set-block-target command + * + * @cmd_parameter: Pointer to " [device]" command parameter + * @response: Pointer to fastboot response buffer + * + * Switches the block interface (e.g. "scsi", "mtd") and, optionally, + * device number used by the fastboot block backend at runtime. + */ +static void __maybe_unused oem_set_block_target(char *cmd_parameter, char *response) +{ + int device = -1; + char *space; + + if (!cmd_parameter) { + fastboot_fail("Expected command parameter", response); + return; + } + + space = strchr(cmd_parameter, ' '); + if (space) { + *space = '\0'; + device = simple_strtoul(space + 1, NULL, 10); + } + + if (fastboot_block_set_target(cmd_parameter, device)) + fastboot_fail("invalid interface name", response); + else + fastboot_okay(NULL, response); +} diff --git a/include/fastboot.h b/include/fastboot.h index b106d6177493..13dcdf919bc9 100644 --- a/include/fastboot.h +++ b/include/fastboot.h @@ -51,6 +51,7 @@ enum { FASTBOOT_COMMAND_OEM_RUN, FASTBOOT_COMMAND_OEM_CONSOLE, FASTBOOT_COMMAND_OEM_BOARD, + FASTBOOT_COMMAND_OEM_SET_BLOCK_TARGET, FASTBOOT_COMMAND_ACMD, FASTBOOT_COMMAND_UCMD, FASTBOOT_COMMAND_COUNT diff --git a/include/fb_block.h b/include/fb_block.h index 189c708e2f0e..fd981d95316e 100644 --- a/include/fb_block.h +++ b/include/fb_block.h @@ -102,4 +102,13 @@ void fastboot_block_write_sparse_image(struct blk_desc *dev_desc, struct disk_pa void fastboot_block_flash_write(const char *part_name, void *download_buffer, u32 download_bytes, char *response); +/** + * fastboot_block_set_target() - Override block interface/device at runtime + * + * @interface: New interface name (e.g. "scsi", "mtd"), or NULL to leave unchanged + * @device: New device number, or -1 to leave unchanged + * Return: 0 on success, -EINVAL if interface name too long + */ +int fastboot_block_set_target(const char *interface, int device); + #endif // _FB_BLOCK_H_ From 74d436c82844712ed7d40555cca6d02c2e0c6897 Mon Sep 17 00:00:00 2001 From: Vandhiadevan Karunamoorthy Date: Tue, 25 Aug 2026 16:16:39 +0530 Subject: [PATCH 07/13] fastboot: spi: add 'oem spi-nor-init' command Add an "oem spi-nor-init" fastboot command that removes the default SPI flash device and reprobes it, mirroring the remove+re-probe pattern already used by fastboot_spi_flash_probe() in fb_spi_flash.c. This lets the driver's own probe() perform any bring-up it needs (e.g. loading firmware). Some SoCs load SPI-NOR firmware from storage in a single bulk pass at boot, before a bare reprobe of the SPI flash device is enough to pick it up (e.g. if the firmware source wasn't available yet at that point). To let such platforms redo that one-time, driver-external step here without hardcoding any vendor-specific call into generic fastboot code, add a weak fastboot_oem_spi_nor_reinit() hook that is called between the removal and the reprobe. The default implementation is a no-op; SoC-specific code may override it. This is useful on boards where the SPI flash device is not usable until fastboot has flashed something it depends on, such as firmware or configuration data. It is intended to be used together with "oem set-block-target" once the device has been brought up. Signed-off-by: Vandhiadevan Karunamoorthy --- drivers/fastboot/Kconfig | 11 +++++++ drivers/fastboot/fb_command.c | 57 +++++++++++++++++++++++++++++++++++ include/fastboot.h | 14 +++++++++ 3 files changed, 82 insertions(+) diff --git a/drivers/fastboot/Kconfig b/drivers/fastboot/Kconfig index c7e0509eaad0..0d07d0602a3b 100644 --- a/drivers/fastboot/Kconfig +++ b/drivers/fastboot/Kconfig @@ -236,6 +236,17 @@ config FASTBOOT_CMD_OEM_SET_BLOCK_TARGET CONFIG_FASTBOOT_FLASH_BLOCK_INTERFACE_NAME / CONFIG_FASTBOOT_FLASH_BLOCK_DEVICE_ID build-time defaults. +config FASTBOOT_CMD_OEM_SPI_NOR_INIT + bool "Enable the 'oem spi-nor-init' command" + depends on DM_SPI_FLASH + help + Add support for the "oem spi-nor-init" command from a client. This + removes and reprobes the default SPI-NOR device, letting its + driver's probe() handle any vendor-specific bring-up (e.g. firmware + load) it needs. Use together with "oem set-block-target" to bring up + SPI-NOR via fastboot on boards where SPI-NOR is not usable until + after fastboot has flashed something it depends on. + config FASTBOOT_GPT_NAME string "Target name for updating GPT" depends on (FASTBOOT_FLASH_MMC || FASTBOOT_FLASH_BLOCK) && EFI_PARTITION diff --git a/drivers/fastboot/fb_command.c b/drivers/fastboot/fb_command.c index bba009a387c8..4bc5fd7b5ee9 100644 --- a/drivers/fastboot/fb_command.c +++ b/drivers/fastboot/fb_command.c @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -13,8 +14,10 @@ #include #include #include +#include #include #include +#include #include /** @@ -46,6 +49,7 @@ static void oem_bootbus(char *, char *); static void oem_console(char *, char *); static void oem_board(char *, char *); static void oem_set_block_target(char *, char *); +static void oem_spi_nor_init(char *, char *); static void run_ucmd(char *, char *); static void run_acmd(char *, char *); @@ -126,6 +130,11 @@ static const struct { .dispatch = CONFIG_IS_ENABLED(FASTBOOT_CMD_OEM_SET_BLOCK_TARGET, (oem_set_block_target), (NULL)) }, + [FASTBOOT_COMMAND_OEM_SPI_NOR_INIT] = { + .command = "oem spi-nor-init", + .dispatch = CONFIG_IS_ENABLED(FASTBOOT_CMD_OEM_SPI_NOR_INIT, + (oem_spi_nor_init), (NULL)) + }, [FASTBOOT_COMMAND_UCMD] = { .command = "UCmd", .dispatch = CONFIG_IS_ENABLED(FASTBOOT_UUU_SUPPORT, (run_ucmd), (NULL)) @@ -626,3 +635,51 @@ static void __maybe_unused oem_set_block_target(char *cmd_parameter, char *respo else fastboot_okay(NULL, response); } + +/** + * fastboot_oem_spi_nor_reinit() - Vendor hook for SPI-NOR re-bring-up. + * + * This is a default weak implementation, which may be overridden in + * SoC-specific code. It runs after the default SPI-NOR device has been + * removed and before it is reprobed, and can be used to redo any one-time, + * driver-external bring-up (e.g. reloading firmware from storage) that the + * device's own probe() cannot repeat on its own. + * + * Return: 0 on success, or a negative error code. + */ +int __weak fastboot_oem_spi_nor_reinit(void) +{ + return 0; +} + +/** + * oem_spi_nor_init() - Execute the OEM spi-nor-init command + * + * @cmd_parameter: Pointer to command parameter + * @response: Pointer to fastboot response buffer + * + * Removes the default SPI-NOR device and reprobes it, so the driver's + * probe() can perform any bring-up it needs. Useful when SPI-NOR is not + * usable until after fastboot has flashed something it depends on. + */ +static void __maybe_unused oem_spi_nor_init(char *cmd_parameter, char *response) +{ + const int bus = config_opt_enabled(CONFIG_DM_SPI_FLASH, CONFIG_SF_DEFAULT_BUS, 0); + const int cs = config_opt_enabled(CONFIG_DM_SPI_FLASH, CONFIG_SF_DEFAULT_CS, 0); + struct udevice *bus_dev, *new; + int ret; + + ret = fastboot_oem_spi_nor_reinit(); + if (ret) { + fastboot_fail("SPI-NOR vendor reinit failed", response); + return; + } + + ret = spi_get_bus_and_cs(bus, cs, &bus_dev, (struct spi_slave **)&new); + if (ret) { + fastboot_fail("failed to probe SPI-NOR", response); + return; + } + + fastboot_okay(NULL, response); +} diff --git a/include/fastboot.h b/include/fastboot.h index 13dcdf919bc9..f6496b3bf99d 100644 --- a/include/fastboot.h +++ b/include/fastboot.h @@ -52,6 +52,7 @@ enum { FASTBOOT_COMMAND_OEM_CONSOLE, FASTBOOT_COMMAND_OEM_BOARD, FASTBOOT_COMMAND_OEM_SET_BLOCK_TARGET, + FASTBOOT_COMMAND_OEM_SPI_NOR_INIT, FASTBOOT_COMMAND_ACMD, FASTBOOT_COMMAND_UCMD, FASTBOOT_COMMAND_COUNT @@ -106,6 +107,19 @@ void fastboot_okay(const char *reason, char *response); */ int fastboot_set_reboot_flag(enum fastboot_reboot_reason reason); +/** + * fastboot_oem_spi_nor_reinit() - Vendor hook for SPI-NOR re-bring-up + * + * Called by the "oem spi-nor-init" command after removing the default + * SPI-NOR device and before reprobing it. The default implementation is a + * no-op; SoC-specific code may override it to redo any one-time bring-up + * (e.g. reloading firmware from storage) that the device's own probe() + * cannot repeat on its own. + * + * Return: 0 on success, or a negative error code. + */ +int fastboot_oem_spi_nor_reinit(void); + /** * fastboot_set_progress_callback() - set progress callback * From 569f39a6b18ff70e01582cd34621f12b5a1f5686 Mon Sep 17 00:00:00 2001 From: Vandhiadevan Karunamoorthy Date: Tue, 25 Aug 2026 16:16:40 +0530 Subject: [PATCH 08/13] misc: qcom_geni: implement fastboot_oem_spi_nor_reinit() qcom_geni_fw_initialise() runs once, at EVT_LAST_STAGE_INIT, and reads GENI SE firmware from a dedicated storage partition into memory for its children to pick up during their own probe(). If that partition does not exist yet at that point (e.g. first boot, before fastboot has flashed it), it finds nothing to load and does not retry later. A later bare SPI-NOR reprobe cannot recover from that on its own, since the SPI driver's probe() only re-reads firmware already cached by qcom_geni_fw_initialise(); it does not re-read storage itself. Override the fastboot_oem_spi_nor_reinit() hook to re-run qcom_geni_fw_initialise(), so that "fastboot oem spi-nor-init" can pick up firmware that has been flashed since boot before reprobing the SPI-NOR device. Signed-off-by: Vandhiadevan Karunamoorthy --- drivers/misc/qcom_geni.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/misc/qcom_geni.c b/drivers/misc/qcom_geni.c index 039a23e8d66d..91af2a6751d3 100644 --- a/drivers/misc/qcom_geni.c +++ b/drivers/misc/qcom_geni.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -643,6 +644,11 @@ int qcom_geni_fw_probe(struct udevice *dev) EVENT_SPY_SIMPLE(EVT_LAST_STAGE_INIT, qcom_geni_fw_initialise); #endif +int fastboot_oem_spi_nor_reinit(void) +{ + return qcom_geni_fw_initialise(); +} + static const struct udevice_id geni_ids[] = { { .compatible = "qcom,geni-se-qup" }, {} From 2e81d960163821c99df3863443a3ae4b1ad5c007 Mon Sep 17 00:00:00 2001 From: Vandhiadevan Karunamoorthy Date: Wed, 26 Aug 2026 13:15:39 +0530 Subject: [PATCH 09/13] clk: qcom: sa8775p: Add QUPv3 wrapper 3 SPI clock support Add the clock RCG, gate bit definitions, and frequency table needed to enable and configure the SE0 (SPI) serial engine clock on QUPv3 wrapper 3, GCC_QUPV3_WRAP3_S0_CLK. This is required to bring up the GENI SPI controller on QUPv3 wrapper 3, e.g. for SPI-NOR flash attached to that wrapper. Also add the wrapper's M-AHB/S-AHB clock gates and the QSPI/AGGRE-NOC gates that must be enabled alongside the SE0 clock. Signed-off-by: Vandhiadevan Karunamoorthy --- drivers/clk/qcom/clock-sa8775p.c | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/drivers/clk/qcom/clock-sa8775p.c b/drivers/clk/qcom/clock-sa8775p.c index eafa8baf0218..a352689375be 100644 --- a/drivers/clk/qcom/clock-sa8775p.c +++ b/drivers/clk/qcom/clock-sa8775p.c @@ -25,6 +25,7 @@ #define UFS_PHY_UNIPRO_CORE_CLK_CMD_RCGR 0x8308c #define SDCC1_APPS_CLK_CMD_RCGR 0x20014 +#define QUPV3_WRAP3_S0_CLK_CMD_RCGR 0xc4154 #define APCS_GPLL9_STATUS 0x9000 #define APCS_GPLLX_ENA_REG 0x4b028 @@ -53,6 +54,26 @@ #define GCC_QUPV3_WRAP2_S6_CLK_ENA_BIT BIT(29) #define GCC_QUPV3_WRAP3_S0_CLK_ENA_BIT BIT(25) +#define GCC_QUPV3_WRAP_3_M_AHB_ENA_BIT BIT(27) +#define GCC_QUPV3_WRAP_3_S_AHB_ENA_BIT BIT(20) +#define GCC_QUPV3_WRAP3_QSPI_ENA_BIT BIT(26) +#define GCC_AGGRE_NOC_QUPV3_AXI_ENA_BIT BIT(28) + +/* QUP Wrapper 3 SE0 (SPI) clock frequency table */ +static const struct freq_tbl ftbl_gcc_qupv3_wrap3_s0_clk_src[] = { + F(7372800, CFG_CLK_SRC_GPLL0_EVEN, 1, 384, 15625), + F(14745600, CFG_CLK_SRC_GPLL0_EVEN, 1, 768, 15625), + F(19200000, CFG_CLK_SRC_CXO, 1, 0, 0), + F(29491200, CFG_CLK_SRC_GPLL0_EVEN, 1, 1536, 15625), + F(32000000, CFG_CLK_SRC_GPLL0_EVEN, 1, 8, 75), + F(48000000, CFG_CLK_SRC_GPLL0, 2, 4, 25), + F(64000000, CFG_CLK_SRC_GPLL0_EVEN, 1, 16, 75), + F(75000000, CFG_CLK_SRC_GPLL0_EVEN, 4, 0, 0), + F(80000000, CFG_CLK_SRC_GPLL0_EVEN, 1, 4, 15), + F(96000000, CFG_CLK_SRC_GPLL0_EVEN, 1, 8, 25), + F(100000000, CFG_CLK_SRC_GPLL0, 6, 0, 0), + { } +}; /* UFS AXI clock frequency table */ static const struct freq_tbl ftbl_gcc_ufs_phy_axi_clk_src[] = { @@ -147,6 +168,12 @@ static ulong sa8775p_set_rate(struct clk *clk, ulong rate) clk_rcg_set_rate_mnd(priv->base, SDCC1_APPS_CLK_CMD_RCGR, freq->pre_div, freq->m, freq->n, freq->src, 8); return freq->freq; + case GCC_QUPV3_WRAP3_S0_CLK: + freq = qcom_find_freq(ftbl_gcc_qupv3_wrap3_s0_clk_src, rate); + clk_rcg_set_rate_mnd(priv->base, QUPV3_WRAP3_S0_CLK_CMD_RCGR, + freq->pre_div, freq->m, freq->n, + freq->src, 16); + return freq->freq; default: return 0; } @@ -189,7 +216,11 @@ static const struct gate_clk sa8775p_clks[] = { GATE_CLK(GCC_QUPV3_WRAP2_S6_CLK, 0x4b018, GCC_QUPV3_WRAP2_S6_CLK_ENA_BIT), /* QUP Wrapper 3 clocks */ + GATE_CLK(GCC_QUPV3_WRAP_3_M_AHB_CLK, 0x4b000, GCC_QUPV3_WRAP_3_M_AHB_ENA_BIT), + GATE_CLK(GCC_QUPV3_WRAP_3_S_AHB_CLK, 0x4b000, GCC_QUPV3_WRAP_3_S_AHB_ENA_BIT), GATE_CLK(GCC_QUPV3_WRAP3_S0_CLK, 0x4b000, GCC_QUPV3_WRAP3_S0_CLK_ENA_BIT), + GATE_CLK(GCC_QUPV3_WRAP3_QSPI_CLK, 0x4b000, GCC_QUPV3_WRAP3_QSPI_ENA_BIT), + GATE_CLK(GCC_AGGRE_NOC_QUPV3_AXI_CLK, 0x4b000, GCC_AGGRE_NOC_QUPV3_AXI_ENA_BIT), /* UFS PHY clocks */ GATE_CLK_POLLED(GCC_UFS_PHY_AXI_CLK, 0x83018, BIT(0), 0x83018), @@ -229,6 +260,10 @@ static int sa8775p_enable(struct clk *clk) qcom_gate_clk_en(priv, GCC_USB3_PRIM_PHY_AUX_CLK); qcom_gate_clk_en(priv, GCC_USB3_PRIM_PHY_COM_AUX_CLK); break; + case GCC_QUPV3_WRAP3_S0_CLK: + qcom_gate_clk_en(priv, GCC_QUPV3_WRAP3_QSPI_CLK); + qcom_gate_clk_en(priv, GCC_AGGRE_NOC_QUPV3_AXI_CLK); + break; } return qcom_gate_clk_en(priv, clk->id); From 7eaa8b9f0e290858c09ec77b112f42528f353311 Mon Sep 17 00:00:00 2001 From: Vandhiadevan Karunamoorthy Date: Wed, 26 Aug 2026 13:22:39 +0530 Subject: [PATCH 10/13] arm: dts: lemans-evk: Enable SPI-NOR flash support Wire up the QUPv3 wrapper 3 / SPI21 GENI SPI controller and its clock/pinctrl dependencies so the SPI-NOR flash on the LeMans EVK can be probed in U-Boot proper. Mark GCC, RPMH clock controller, Apps RSC, TLMM and QUPv3 wrapper 3 as bootph-all so they remain bound prior to relocation, add the SPI21 pinctrl state, set the SE0 clock to 48 MHz via assigned-clocks on GCC, and drop the power-domain/DMA properties on SPI21 that are not usable prior to relocation. Add a jedec,spi-nor flash child node and an spi0 alias for the SPI-NOR flash. Signed-off-by: Vandhiadevan Karunamoorthy --- arch/arm/dts/lemans-evk-u-boot.dtsi | 32 ++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/arch/arm/dts/lemans-evk-u-boot.dtsi b/arch/arm/dts/lemans-evk-u-boot.dtsi index fd4eaa2fec81..5f05a3226054 100644 --- a/arch/arm/dts/lemans-evk-u-boot.dtsi +++ b/arch/arm/dts/lemans-evk-u-boot.dtsi @@ -36,7 +36,7 @@ }; &gcc { - bootph-all; /* Enable GCC for SPL */ + bootph-all; }; /* RPMH clock controller - required by GCC */ @@ -56,6 +56,13 @@ &tlmm { bootph-all; /* Make TLMM GPIO controller available in SPL */ + + qup_spi21_default: qup-spi21-state { + pins = "gpio13", "gpio14", "gpio15", "gpio16"; + function = "sail_top"; + drive-strength = <2>; + bias-disable; + }; }; /* Enter Qualcomm EDL/download mode via "reset -edl". */ @@ -64,3 +71,26 @@ mode-edl = <0x80000000 0x00000001>; }; }; + +&{/aliases} { + spi0 = &spi21; +}; + +&qupv3_id_3 { + bootph-all; + status = "okay"; +}; + +&spi21 { + assigned-clocks = <&gcc GCC_QUPV3_WRAP3_S0_CLK>; + assigned-clock-rates = <48000000>; + status = "okay"; + + flash@0 { + compatible = "jedec,spi-nor"; + reg = <0>; + spi-max-frequency = <48000000>; + spi-tx-bus-width = <1>; + spi-rx-bus-width = <1>; + }; +}; From eaf151be8e6479496aa82094aa764ed36df885ac Mon Sep 17 00:00:00 2001 From: Vandhiadevan Karunamoorthy Date: Tue, 1 Sep 2026 12:02:06 +0530 Subject: [PATCH 11/13] configs: qcom_lemans_snagboot: enable SPI-NOR block-based fastboot Turn on the GENI/QUPv3 wrapper-3 SPI-NOR stack and the fastboot runtime block-target override that this branch already implements, using the existing qcom_lemans_snagboot_defconfig rather than adding a new one: - CONFIG_OF_UPSTREAM plus CONFIG_QCOM_COMMAND_DB for RPMH clock resource lookups - CONFIG_IOMMU / CONFIG_QCOM_HYP_SMMU so the QUP3 GENI SE-DMA stream is routed through the hypervisor-backed SMMU instead of silently faulting - DM_SPI / SPI_GENI_QCOM, DM_SPI_FLASH / SPI_FLASH_MTD and the MTD block layer for SPI-NOR on QUPv3 wrapper 3, SE0 - the broader SPI-NOR vendor set from drivers/mtd/spi/Kconfig (Atmel, Dosilicon, EON, GigaDevice, ISSI, Macronix, Puya, Silicon Kaiser, Spansion, STMicro, SST, Winbond, XMC, XTX, ZBIT), instead of Macronix alone - FASTBOOT_CMD_OEM_SET_BLOCK_TARGET / FASTBOOT_CMD_OEM_SPI_NOR_INIT so fastboot can switch its block backend from the snagboot default ("scsi") to "mtd" at runtime, once SPI-NOR has probed Validated with a full build for lemans-evk (qcom_lemans_snagboot_defconfig), confirming qcom_geni_spi.o, sf_probe.o, mtdblock.o and fb_block.o all compile and link into u-boot.bin. Signed-off-by: Vandhiadevan Karunamoorthy --- configs/qcom_lemans_snagboot_defconfig | 52 +++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/configs/qcom_lemans_snagboot_defconfig b/configs/qcom_lemans_snagboot_defconfig index d6906c1e5012..4c7cbdad8773 100644 --- a/configs/qcom_lemans_snagboot_defconfig +++ b/configs/qcom_lemans_snagboot_defconfig @@ -15,4 +15,54 @@ CONFIG_FASTBOOT_BUF_ADDR=0xdb300000 CONFIG_DEFAULT_DEVICE_TREE="qcom/lemans-evk" # Timer frequency for Lemans platform -CONFIG_COUNTER_FREQUENCY=19200000 \ No newline at end of file +CONFIG_COUNTER_FREQUENCY=19200000 + +# Use upstream DTS +CONFIG_OF_UPSTREAM=y + +# CMD_DB required for RPMH clock resource lookups (used by SMMU/RPMH setup below) +CONFIG_QCOM_COMMAND_DB=y + +# SMMU: the QUP3 GENI SE-DMA stream needs the Qualcomm hypervisor-backed SMMU +# enabled or DMA silently faults instead of landing in DRAM. +CONFIG_IOMMU=y +CONFIG_QCOM_HYP_SMMU=y + +# SPI-NOR on QUPV3 wrapper 3, SE0 +CONFIG_SPI=y +CONFIG_DM_SPI=y +CONFIG_SPI_GENI_QCOM=y +CONFIG_MTD=y +CONFIG_DM_MTD=y +CONFIG_CMD_MTD=y +CONFIG_MTD_BLOCK=y +CONFIG_DM_SPI_FLASH=y +CONFIG_SPI_FLASH_MTD=y +CONFIG_SPI_FLASH_SFDP_SUPPORT=y +CONFIG_SF_DEFAULT_BUS=0 +CONFIG_SF_DEFAULT_CS=0 +CONFIG_SF_DEFAULT_SPEED=48000000 +CONFIG_SF_DEFAULT_MODE=0 +CONFIG_CMD_SF_TEST=y + +# SPI-NOR flash vendors +CONFIG_SPI_FLASH_ATMEL=y +CONFIG_SPI_FLASH_DOSILICON=y +CONFIG_SPI_FLASH_EON=y +CONFIG_SPI_FLASH_GIGADEVICE=y +CONFIG_SPI_FLASH_ISSI=y +CONFIG_SPI_FLASH_MACRONIX=y +CONFIG_SPI_FLASH_PUYA=y +CONFIG_SPI_FLASH_SILICONKAISER=y +CONFIG_SPI_FLASH_SPANSION=y +CONFIG_SPI_FLASH_STMICRO=y +CONFIG_SPI_FLASH_SST=y +CONFIG_SPI_FLASH_WINBOND=y +CONFIG_SPI_FLASH_XMC=y +CONFIG_SPI_FLASH_XTX=y +CONFIG_SPI_FLASH_ZBIT=y + +# Fastboot: switch the block backend from the snagboot default ("scsi") to +# "mtd" at runtime, once SPI-NOR has been probed +CONFIG_FASTBOOT_CMD_OEM_SET_BLOCK_TARGET=y +CONFIG_FASTBOOT_CMD_OEM_SPI_NOR_INIT=y From ab00fc95415d97a33e2f184c1073326e5d22a772 Mon Sep 17 00:00:00 2001 From: Vandhiadevan Karunamoorthy Date: Tue, 1 Sep 2026 22:30:15 +0530 Subject: [PATCH 12/13] misc: qcom_geni: fix NULL pointer dereference in qcom_geni_load_firmware() qcom_geni_load_firmware() unconditionally dereferences the firmware blob pointer cached in the GENI wrapper's private data (dev_get_priv(dev->parent)) without checking it for NULL. qcom_geni_fw_initialise() only populates that private data when it successfully locates and reads the dedicated QUP firmware partition at EVT_LAST_STAGE_INIT. If no such partition exists yet - e.g. a board with no MMC/UFS storage and an unprovisioned SPI-NOR, before any image has been flashed - it prints "QUP firmware partition not found" and returns without ever setting the wrapper's private data, leaving it NULL. Any later probe of a GENI peripheral (SPI/I2C/UART) whose firmware is not yet loaded on that hardware instance then calls qcom_geni_load_firmware(), which dereferences the NULL pointer and crashes with a synchronous abort/data abort. Add a NULL check and fail gracefully with -ENOENT, matching how the SPI caller already handles a nonzero return from this function. Signed-off-by: Vandhiadevan Karunamoorthy --- drivers/misc/qcom_geni.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/drivers/misc/qcom_geni.c b/drivers/misc/qcom_geni.c index 91af2a6751d3..a0a7b7c943ba 100644 --- a/drivers/misc/qcom_geni.c +++ b/drivers/misc/qcom_geni.c @@ -427,6 +427,10 @@ int qcom_geni_load_firmware(phys_addr_t qup_base, /* The firmware blob is the private data of the GENI wrapper (parent) */ fw = dev_get_priv(dev->parent); + if (!fw) { + dev_err(dev, "QUP firmware not available\n"); + return -ENOENT; + } if (IS_ELF(*(Elf32_Ehdr *)fw)) { ret = read_elf(&rsc, fw, &hdr); From 2dc4dbb4b15094ff675ab4f34af19f98499583e7 Mon Sep 17 00:00:00 2001 From: Vandhiadevan Karunamoorthy Date: Wed, 2 Sep 2026 09:31:54 +0530 Subject: [PATCH 13/13] disk: part_efi: force partition re-detection after GPT write write_mbr_and_gpt_partitions() calls part_init() after writing the GPT to re-detect the partition table, but part_init() takes a fast path when desc->part_type is already cached: it only re-validates the existing driver's test() instead of re-scanning all partition drivers. On SPI-NOR/MTD block devices, part_type is cached as PART_TYPE_MTD from the initial probe, since the MTD partition driver's test() unconditionally matches. When fastboot subsequently writes a GPT to SPI-NOR (e.g. via 'gpt write'), the post-write part_init() call keeps reusing the stale MTD type instead of detecting EFI, so 'part list mtd 0' reports "Partition Type: MTD" with no partitions until an unrelated full reprobe (e.g. 'sf probe') resets part_type and forces a re-scan. Reset part_type to PART_TYPE_UNKNOWN before the post-write part_init() call so it always performs a full re-scan and correctly detects EFI immediately. Signed-off-by: Vandhiadevan Karunamoorthy --- disk/part_efi.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/disk/part_efi.c b/disk/part_efi.c index d8b17ec2e91a..d1266165fa15 100644 --- a/disk/part_efi.c +++ b/disk/part_efi.c @@ -975,6 +975,9 @@ int write_mbr_and_gpt_partitions(struct blk_desc *desc, void *buf) return 1; } + if (desc->part_type == PART_TYPE_MTD) + desc->part_type = PART_TYPE_UNKNOWN; + /* Update the partition table entries*/ part_init(desc);