[PATCH 1/6] spi: spi-geni-qcom: Add transfer_one_message() support

From: Jyothi Kumar Seerapu

Date: Fri Nov 06 2026 - 10:07:10 EST


The GENI SPI driver currently relies on the SPI core's default
transfer_one_message() implementation. Adding Quad SPI (QSPI) support
requires driver-managed message sequencing. In particular, GENI QSPI
reads require combining a command/address TX phase and an RX data
phase into a single hardware command.

Add a driver-specific transfer_one_message() implementation, retaining
spi_geni_transfer_one() as an internal helper for programming individual
transfers. Handle completion, transfer delays, errors and DMA buffer
synchronization at the message level.

The custom message handler must also provide the GPIO chip-select
sequencing and delays previously handled by the SPI core. Track the
selected GPIO and its timing and polarity for device switching.
Invalidate the cached selection during setup and preserve the software
inactive delay for native chip-selects.

Signed-off-by: Jyothi Kumar Seerapu <jyothi.seerapu@xxxxxxxxxxxxxxxx>
---
drivers/spi/spi-geni-qcom.c | 247 +++++++++++++++++++++++++++++++++++++++-----
1 file changed, 224 insertions(+), 23 deletions(-)

diff --git a/drivers/spi/spi-geni-qcom.c b/drivers/spi/spi-geni-qcom.c
index 6c57f8309a3b..25c03096ee23 100644
--- a/drivers/spi/spi-geni-qcom.c
+++ b/drivers/spi/spi-geni-qcom.c
@@ -6,6 +6,7 @@
#define CREATE_TRACE_POINTS
#include <trace/events/qcom_geni_spi.h>

+#include <linux/acpi.h>
#include <linux/clk.h>
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
@@ -13,6 +14,7 @@
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/log2.h>
+#include <linux/math64.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/pm_opp.h>
@@ -106,10 +108,14 @@ struct spi_geni_master {
struct completion abort_done;
struct completion tx_reset_done;
struct completion rx_reset_done;
+ struct completion xfer_done;
unsigned int oversampling;
spinlock_t lock;
int irq;
- bool cs_flag;
+ struct gpio_desc *cs_gpiod;
+ struct spi_delay cs_hold;
+ struct spi_delay cs_inactive;
+ bool cs_inactive_level;
bool abort_failed;
struct dma_chan *tx;
struct dma_chan *rx;
@@ -400,23 +406,23 @@ spi_gsi_callback_result(void *cb, const struct dmaengine_result *result)
struct spi_controller *spi = cb;
struct spi_geni_master *mas = spi_controller_get_devdata(spi);

- spi->cur_msg->status = -EIO;
+ /*
+ * Record only failures. msg->status is finalized when the
+ * entire SPI message completes.
+ */
if (result->result != DMA_TRANS_NOERROR) {
dev_err(&spi->dev, "DMA txn failed: %d\n", result->result);
trace_geni_se_regs(&mas->se);
- spi_finalize_current_transfer(spi);
- return;
- }
-
- if (!result->residue) {
- spi->cur_msg->status = 0;
+ spi->cur_msg->status = -EIO;
+ } else if (!result->residue) {
dev_dbg(&spi->dev, "DMA txn completed\n");
} else {
dev_err(&spi->dev, "DMA xfer has pending: %d\n", result->residue);
trace_geni_se_regs(&mas->se);
+ spi->cur_msg->status = -EIO;
}

- spi_finalize_current_transfer(spi);
+ complete(&mas->xfer_done);
}

static int setup_gsi_xfer(struct spi_transfer *xfer, struct spi_geni_master *mas,
@@ -511,7 +517,7 @@ static int setup_gsi_xfer(struct spi_transfer *xfer, struct spi_geni_master *mas
dma_async_issue_pending(mas->rx);

dma_async_issue_pending(mas->tx);
- return 1;
+ return 0;
}

static u32 get_xfer_len_in_words(struct spi_transfer *xfer,
@@ -926,7 +932,6 @@ static int spi_geni_transfer_one(struct spi_controller *spi,
struct spi_transfer *xfer)
{
struct spi_geni_master *mas = spi_controller_get_devdata(spi);
- int ret;

if (spi_geni_is_abort_still_pending(mas))
return -EBUSY;
@@ -935,16 +940,206 @@ static int spi_geni_transfer_one(struct spi_controller *spi,
if (!xfer->len)
return 0;

- if (mas->cur_xfer_mode == GENI_SE_FIFO || mas->cur_xfer_mode == GENI_SE_DMA) {
- ret = setup_se_xfer(xfer, mas, slv->mode, spi);
- /* SPI framework expects +ve ret code to wait for transfer complete */
- if (!ret)
- ret = 1;
- return ret;
- }
+ if (mas->cur_xfer_mode == GENI_SE_FIFO || mas->cur_xfer_mode == GENI_SE_DMA)
+ return setup_se_xfer(xfer, mas, slv->mode, spi);
+
return setup_gsi_xfer(xfer, mas, slv, spi);
}

+static int spi_geni_setup(struct spi_device *slv)
+{
+ struct spi_geni_master *mas = spi_controller_get_devdata(slv->controller);
+
+ /* The core deasserts CS after setup, possibly with a new polarity. */
+ if (!(slv->mode & SPI_NO_CS) && mas->cs_gpiod == spi_get_csgpiod(slv, 0))
+ mas->cs_gpiod = NULL;
+
+ return 0;
+}
+
+static void spi_geni_set_cs(struct spi_device *spi_slv, bool enable)
+{
+ struct gpio_desc *cs_gpiod = spi_get_csgpiod(spi_slv, 0);
+ struct spi_geni_master *mas = spi_controller_get_devdata(spi_slv->controller);
+ bool level = true;
+
+ if (spi_slv->mode & SPI_NO_CS)
+ cs_gpiod = NULL;
+
+ /* Release a previously selected GPIO before switching devices. */
+ if (mas->cs_gpiod && (!enable || mas->cs_gpiod != cs_gpiod)) {
+ spi_delay_exec(&mas->cs_hold, NULL);
+ gpiod_set_value_cansleep(mas->cs_gpiod, mas->cs_inactive_level);
+ spi_delay_exec(&mas->cs_inactive, NULL);
+ mas->cs_gpiod = NULL;
+ }
+
+ /* Preserve the software inactive delay for hardware-controlled CS. */
+ if (!cs_gpiod && !enable && !(spi_slv->mode & SPI_NO_CS))
+ spi_delay_exec(&spi_slv->cs_inactive, NULL);
+
+ if (!enable || !cs_gpiod || mas->cs_gpiod == cs_gpiod)
+ return;
+
+ /* DT polarity is handled by gpiolib; ACPI uses SPI_CS_HIGH. */
+ if (is_acpi_device_node(dev_fwnode(&spi_slv->dev)) &&
+ !(spi_slv->mode & SPI_CS_HIGH))
+ level = false;
+
+ gpiod_set_value_cansleep(cs_gpiod, level);
+ spi_delay_exec(&spi_slv->cs_setup, NULL);
+
+ /* GPIOs belong to the controller; do not retain a spi_device pointer. */
+ mas->cs_gpiod = cs_gpiod;
+ mas->cs_hold = spi_slv->cs_hold;
+ mas->cs_inactive = spi_slv->cs_inactive;
+ mas->cs_inactive_level = !level;
+}
+
+static void spi_geni_dma_sync(struct spi_controller *spi,
+ struct spi_transfer *xfer, bool for_device)
+{
+ /* The core maps buffers with DMA_ATTR_SKIP_CPU_SYNC. */
+ if (for_device) {
+ if (xfer->tx_sg_mapped)
+ dma_sync_sgtable_for_device(spi->cur_tx_dma_dev,
+ &xfer->tx_sg, DMA_TO_DEVICE);
+ if (xfer->rx_sg_mapped)
+ dma_sync_sgtable_for_device(spi->cur_rx_dma_dev,
+ &xfer->rx_sg, DMA_FROM_DEVICE);
+ } else {
+ if (xfer->rx_sg_mapped)
+ dma_sync_sgtable_for_cpu(spi->cur_rx_dma_dev,
+ &xfer->rx_sg, DMA_FROM_DEVICE);
+ if (xfer->tx_sg_mapped)
+ dma_sync_sgtable_for_cpu(spi->cur_tx_dma_dev,
+ &xfer->tx_sg, DMA_TO_DEVICE);
+ }
+}
+
+static int spi_geni_wait_for_xfer(struct spi_controller *spi,
+ const struct spi_transfer *xfer)
+{
+ struct spi_geni_master *mas = spi_controller_get_devdata(spi);
+ u64 bits, ms;
+
+ if (spi_controller_is_target(spi)) {
+ /* A target cannot predict when its peer will supply clocks. */
+ if (wait_for_completion_interruptible(&mas->xfer_done)) {
+ dev_dbg(mas->dev, "SPI transfer interrupted\n");
+ return -EINTR;
+ }
+ return 0;
+ }
+
+ bits = (u64)xfer->len * BITS_PER_BYTE;
+ ms = div_u64(bits * MSEC_PER_SEC, xfer->speed_hz ?: 100000);
+ ms = min_t(u64, ms * 2 + 200, UINT_MAX);
+
+ if (!wait_for_completion_timeout(&mas->xfer_done, msecs_to_jiffies(ms))) {
+ dev_err(mas->dev, "Transfer timed out\n");
+ return -ETIMEDOUT;
+ }
+
+ return 0;
+}
+
+static int spi_geni_transfer_one_message(struct spi_controller *spi,
+ struct spi_message *msg)
+{
+ struct spi_geni_master *mas = spi_controller_get_devdata(spi);
+ struct spi_device *spi_slv = msg->spi;
+ struct spi_transfer *xfer, *next_xfer;
+ bool keep_cs = false;
+ bool dma_synced = false;
+ bool started = false;
+ int ret = 0;
+
+ xfer = list_first_entry(&msg->transfers, struct spi_transfer, transfer_list);
+ spi_geni_set_cs(spi_slv, !xfer->cs_off);
+
+ list_for_each_entry(xfer, &msg->transfers, transfer_list) {
+ if ((xfer->tx_buf || xfer->rx_buf) && xfer->len) {
+ reinit_completion(&mas->xfer_done);
+
+ /* Synchronize buffers only for DMA; the SE FIFO path uses CPU access. */
+ dma_synced = mas->cur_xfer_mode == GENI_GPI_DMA ||
+ ((xfer->tx_sg.nents || xfer->rx_sg.nents) &&
+ xfer->tx_sg.nents <= 1 && xfer->rx_sg.nents <= 1);
+ if (dma_synced)
+ spi_geni_dma_sync(spi, xfer, true);
+
+ ret = spi_geni_transfer_one(spi, spi_slv, xfer);
+ if (ret < 0) {
+ dev_err(mas->dev, "Transfer failed: %d\n", ret);
+ goto out_err;
+ }
+ started = true;
+
+ ret = spi_geni_wait_for_xfer(spi, xfer);
+ if (ret)
+ goto out_err;
+
+ /* DMA errors and target aborts are reported through msg->status. */
+ if (msg->status != -EINPROGRESS) {
+ ret = msg->status;
+ goto out_err;
+ }
+ if (dma_synced) {
+ spi_geni_dma_sync(spi, xfer, false);
+ dma_synced = false;
+ }
+ } else if (xfer->len) {
+ dev_err(&spi_slv->dev,
+ "Bufferless transfer has length %u\n", xfer->len);
+ ret = -EINVAL;
+ goto out_err;
+ }
+
+ msg->actual_length += xfer->len;
+ ret = spi_transfer_delay_exec(xfer);
+ if (ret)
+ goto out_err;
+
+ next_xfer = list_is_last(&xfer->transfer_list, &msg->transfers) ?
+ NULL : list_next_entry(xfer, transfer_list);
+ if (!next_xfer) {
+ keep_cs = xfer->cs_change;
+ } else if (xfer->cs_change) {
+ if (!xfer->cs_off)
+ spi_geni_set_cs(spi_slv, false);
+
+ spi_transfer_cs_change_delay_exec(msg, xfer);
+
+ if (!next_xfer->cs_off)
+ spi_geni_set_cs(spi_slv, true);
+ } else if (xfer->cs_off != next_xfer->cs_off) {
+ spi_geni_set_cs(spi_slv, xfer->cs_off);
+ }
+ }
+
+out_err:
+ if (!ret && msg->status != -EINPROGRESS)
+ ret = msg->status;
+
+ if (ret && started)
+ spi_geni_handle_err(spi, msg);
+
+ /* Return buffer ownership only after DMA has completed or been stopped. */
+ if (dma_synced)
+ spi_geni_dma_sync(spi, xfer, false);
+
+ if (ret || !keep_cs)
+ spi_geni_set_cs(spi_slv, false);
+
+ if (msg->status == -EINPROGRESS)
+ msg->status = ret;
+
+ spi_finalize_current_message(spi);
+
+ return ret;
+}
+
static irqreturn_t geni_spi_isr(int irq, void *data)
{
struct spi_controller *spi = data;
@@ -979,8 +1174,6 @@ static irqreturn_t geni_spi_isr(int irq, void *data)

if (m_irq & M_CMD_DONE_EN) {
if (mas->cur_xfer) {
- spi_finalize_current_transfer(spi);
- mas->cur_xfer = NULL;
/*
* If this happens, then a CMD_DONE came before all the
* Tx buffer bytes were sent out. This is unusual, log
@@ -1005,6 +1198,9 @@ static irqreturn_t geni_spi_isr(int irq, void *data)
mas->rx_rem_bytes, mas->cur_bits_per_word);
trace_geni_se_regs(se);
}
+
+ complete(&mas->xfer_done);
+ mas->cur_xfer = NULL;
} else {
complete(&mas->cs_done);
}
@@ -1025,7 +1221,7 @@ static irqreturn_t geni_spi_isr(int irq, void *data)
if (dma_rx_status & RX_RESET_DONE)
complete(&mas->rx_reset_done);
if (!mas->tx_rem_bytes && !mas->rx_rem_bytes && xfer) {
- spi_finalize_current_transfer(spi);
+ complete(&mas->xfer_done);
mas->cur_xfer = NULL;
}
}
@@ -1057,11 +1253,14 @@ static irqreturn_t geni_spi_isr(int irq, void *data)

static int spi_geni_target_abort(struct spi_controller *spi)
{
+ struct spi_geni_master *mas = spi_controller_get_devdata(spi);
+
if (!spi->cur_msg)
return 0;

+ spi->cur_msg->status = -ECANCELED;
handle_se_timeout(spi);
- spi_finalize_current_transfer(spi);
+ complete(&mas->xfer_done);

return 0;
}
@@ -1116,8 +1315,9 @@ static int spi_geni_probe(struct platform_device *pdev)
spi->num_chipselect = 4;
spi->max_speed_hz = 50000000;
spi->max_dma_len = 0xffff0; /* 24 bits for tx/rx dma length */
+ spi->setup = spi_geni_setup;
spi->prepare_message = spi_geni_prepare_message;
- spi->transfer_one = spi_geni_transfer_one;
+ spi->transfer_one_message = spi_geni_transfer_one_message;
spi->can_dma = geni_can_dma;
spi->dma_map_dev = dev->parent;
spi->auto_runtime_pm = true;
@@ -1129,6 +1329,7 @@ static int spi_geni_probe(struct platform_device *pdev)
init_completion(&mas->abort_done);
init_completion(&mas->tx_reset_done);
init_completion(&mas->rx_reset_done);
+ init_completion(&mas->xfer_done);
spin_lock_init(&mas->lock);

if (spi->target)

--
2.34.1