* [PATCH v3 0/2] spi: ma35d1-qspi: Add Nuvoton MA35D1 QSPI controller
@ 2026-06-11 9:12 Chi-Wen Weng
2026-06-11 9:12 ` [PATCH v3 1/2] dt-bindings: spi: nuvoton,ma35d1-qspi: Add Nuvoton MA35D1 QSPI Chi-Wen Weng
2026-06-11 9:12 ` [PATCH v3 2/2] spi: ma35d1-qspi: Add Nuvoton MA35D1 QSPI controller support Chi-Wen Weng
0 siblings, 2 replies; 4+ messages in thread
From: Chi-Wen Weng @ 2026-06-11 9:12 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt
Cc: linux-arm-kernel, linux-spi, devicetree, linux-kernel, cwweng,
cwweng.linux
From: Chi-Wen Weng <cwweng@nuvoton.com>
Add devicetree binding and SPI controller driver support for the
Nuvoton MA35D1 Quad SPI controller.
The MA35D1 QSPI controller supports SPI memory devices such as SPI NOR
and SPI NAND flashes in single, dual and quad I/O modes. This initial
driver implements a conservative PIO-based transfer path and spi-mem
operation support.
Changes in v3:
- Fixed DCO mismatch by using a consistent author and Signed-off-by
address.
- Added spi_controller_mem_caps and set per_op_freq for spi-mem
operations.
- Added SPI_CS_HIGH to controller mode_bits.
- Added a short delay after requesting FIFO reset before polling reset
completion.
- Documented that the MA35D1 QSPI controller pushes one RX FIFO entry for
each TX word in single, dual-output and quad-output modes, so TX-only
transfers intentionally drain and discard RX data.
- Added missing linux/delay.h include for udelay().
- Cleaned up unused register bit definitions.
Changes in v2:
- Updated patch subject lines to match SPI subsystem style.
- Added commit message to the dt-bindings patch.
- Added ARCH_MA35 || COMPILE_TEST dependency to Kconfig.
- Expanded Kconfig help text.
- Converted the driver file header to // comments.
- Added reset control handling to the driver.
- Added resets property to the binding.
- Added num-cs constraint to the binding.
- Dropped the flash child node from the binding example.
- Used op->max_freq for spi-mem operations.
- Split low-level CS register handling from the SPI core .set_cs()
callback.
- Handled SPI_CS_HIGH explicitly for the spi-mem direct CS path.
- Fixed spi-mem opcode transfer to use a u8 buffer.
- Limited spi-mem command opcode length to one byte.
- Forced spi-mem operations to 8-bit word size.
- Avoided driving bidirectional data pins during dummy cycles.
- Drained RX FIFO during TX-only transfers.
- Rejected invalid chip-select numbers instead of mapping them to SS1.
- Rejected unsupported dual/quad full-duplex generic SPI transfers.
- Fixed checkpatch style issues.
Chi-Wen Weng (2):
dt-bindings: spi: nuvoton,ma35d1-qspi: Add Nuvoton MA35D1 QSPI
spi: ma35d1-qspi: Add Nuvoton MA35D1 QSPI controller support
.../bindings/spi/nuvoton,ma35d1-qspi.yaml | 62 ++
drivers/spi/Kconfig | 10 +
drivers/spi/Makefile | 1 +
drivers/spi/spi-ma35d1-qspi.c | 633 ++++++++++++++++++
4 files changed, 706 insertions(+)
create mode 100644 Documentation/devicetree/bindings/spi/nuvoton,ma35d1-qspi.yaml
create mode 100644 drivers/spi/spi-ma35d1-qspi.c
--
2.25.1
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v3 1/2] dt-bindings: spi: nuvoton,ma35d1-qspi: Add Nuvoton MA35D1 QSPI
2026-06-11 9:12 [PATCH v3 0/2] spi: ma35d1-qspi: Add Nuvoton MA35D1 QSPI controller Chi-Wen Weng
@ 2026-06-11 9:12 ` Chi-Wen Weng
2026-06-11 9:12 ` [PATCH v3 2/2] spi: ma35d1-qspi: Add Nuvoton MA35D1 QSPI controller support Chi-Wen Weng
1 sibling, 0 replies; 4+ messages in thread
From: Chi-Wen Weng @ 2026-06-11 9:12 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt
Cc: linux-arm-kernel, linux-spi, devicetree, linux-kernel, cwweng,
cwweng.linux
From: Chi-Wen Weng <cwweng@nuvoton.com>
Add a devicetree binding for the Quad SPI controller found in
Nuvoton MA35D1 SoCs.
The controller supports SPI memory devices such as SPI NOR and SPI NAND
flashes. It has one register range, one clock input and one reset line,
and supports up to two chip selects.
Signed-off-by: Chi-Wen Weng <cwweng@nuvoton.com>
---
.../bindings/spi/nuvoton,ma35d1-qspi.yaml | 62 +++++++++++++++++++
1 file changed, 62 insertions(+)
create mode 100644 Documentation/devicetree/bindings/spi/nuvoton,ma35d1-qspi.yaml
diff --git a/Documentation/devicetree/bindings/spi/nuvoton,ma35d1-qspi.yaml b/Documentation/devicetree/bindings/spi/nuvoton,ma35d1-qspi.yaml
new file mode 100644
index 000000000000..d3b36e612eb0
--- /dev/null
+++ b/Documentation/devicetree/bindings/spi/nuvoton,ma35d1-qspi.yaml
@@ -0,0 +1,62 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/spi/nuvoton,ma35d1-qspi.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Nuvoton MA35D1 Quad SPI Controller
+
+maintainers:
+ - Chi-Wen Weng <cwweng@nuvoton.com>
+
+allOf:
+ - $ref: /schemas/spi/spi-controller.yaml#
+
+properties:
+ compatible:
+ const: nuvoton,ma35d1-qspi
+
+ reg:
+ maxItems: 1
+
+ interrupts:
+ maxItems: 1
+
+ clocks:
+ maxItems: 1
+
+ resets:
+ maxItems: 1
+
+ num-cs:
+ maximum: 2
+
+required:
+ - compatible
+ - reg
+ - clocks
+ - resets
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+ #include <dt-bindings/clock/nuvoton,ma35d1-clk.h>
+ #include <dt-bindings/reset/nuvoton,ma35d1-reset.h>
+
+ soc {
+ #address-cells = <2>;
+ #size-cells = <2>;
+
+ spi@40680000 {
+ compatible = "nuvoton,ma35d1-qspi";
+ reg = <0 0x40680000 0 0x100>;
+ interrupts = <GIC_SPI 57 IRQ_TYPE_LEVEL_HIGH>;
+ clocks = <&clk QSPI0_GATE>;
+ resets = <&sys MA35D1_RESET_QSPI0>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+ };
+ };
+
--
2.25.1
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH v3 2/2] spi: ma35d1-qspi: Add Nuvoton MA35D1 QSPI controller support
2026-06-11 9:12 [PATCH v3 0/2] spi: ma35d1-qspi: Add Nuvoton MA35D1 QSPI controller Chi-Wen Weng
2026-06-11 9:12 ` [PATCH v3 1/2] dt-bindings: spi: nuvoton,ma35d1-qspi: Add Nuvoton MA35D1 QSPI Chi-Wen Weng
@ 2026-06-11 9:12 ` Chi-Wen Weng
2026-06-11 9:27 ` sashiko-bot
1 sibling, 1 reply; 4+ messages in thread
From: Chi-Wen Weng @ 2026-06-11 9:12 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt
Cc: linux-arm-kernel, linux-spi, devicetree, linux-kernel, cwweng,
cwweng.linux
From: Chi-Wen Weng <cwweng@nuvoton.com>
Add SPI controller driver support for the Nuvoton MA35D1 Quad SPI
controller.
The controller supports standard SPI transfers and spi-mem operations
for SPI memory devices such as SPI NOR and SPI NAND flashes. The driver
supports single, dual and quad I/O modes and uses a conservative
word-by-word PIO transfer path for the initial upstream version.
The driver also handles controller reset, clock divider programming,
chip-select control, transfer mode setup and spi-mem command/address/
dummy/data phases.
Signed-off-by: Chi-Wen Weng <cwweng@nuvoton.com>
---
drivers/spi/Kconfig | 10 +
drivers/spi/Makefile | 1 +
drivers/spi/spi-ma35d1-qspi.c | 633 ++++++++++++++++++++++++++++++++++
3 files changed, 644 insertions(+)
create mode 100644 drivers/spi/spi-ma35d1-qspi.c
diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig
index b563f49e2197..8b8297ee3f2e 100644
--- a/drivers/spi/Kconfig
+++ b/drivers/spi/Kconfig
@@ -694,6 +694,16 @@ config SPI_LJCA
This driver can also be built as a module. If so, the module
will be called spi-ljca.
+config SPI_MA35D1_QSPI
+ tristate "Nuvoton MA35D1 QSPI controller"
+ depends on ARCH_MA35 || COMPILE_TEST
+ help
+ This enables support for the Quad SPI controller found in
+ Nuvoton MA35D1 SoCs.
+
+ The controller supports SPI memory devices such as SPI NOR and
+ SPI NAND flashes in single, dual and quad I/O modes.
+
config SPI_MESON_SPICC
tristate "Amlogic Meson SPICC controller"
depends on COMMON_CLK
diff --git a/drivers/spi/Makefile b/drivers/spi/Makefile
index 9d36190a9884..c5bb0efd108c 100644
--- a/drivers/spi/Makefile
+++ b/drivers/spi/Makefile
@@ -85,6 +85,7 @@ obj-$(CONFIG_SPI_LOONGSON_CORE) += spi-loongson-core.o
obj-$(CONFIG_SPI_LOONGSON_PCI) += spi-loongson-pci.o
obj-$(CONFIG_SPI_LOONGSON_PLATFORM) += spi-loongson-plat.o
obj-$(CONFIG_SPI_LP8841_RTC) += spi-lp8841-rtc.o
+obj-$(CONFIG_SPI_MA35D1_QSPI) += spi-ma35d1-qspi.o
obj-$(CONFIG_SPI_MESON_SPICC) += spi-meson-spicc.o
obj-$(CONFIG_SPI_MESON_SPIFC) += spi-meson-spifc.o
obj-$(CONFIG_SPI_MICROCHIP_CORE_QSPI) += spi-microchip-core-qspi.o
diff --git a/drivers/spi/spi-ma35d1-qspi.c b/drivers/spi/spi-ma35d1-qspi.c
new file mode 100644
index 000000000000..d3e687b6a06d
--- /dev/null
+++ b/drivers/spi/spi-ma35d1-qspi.c
@@ -0,0 +1,633 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+//
+// Nuvoton MA35D1 QSPI controller driver
+//
+// Copyright (c) 2026 Nuvoton Technology Corp.
+// Author: Chi-Wen Weng <cwweng@nuvoton.com>
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/reset.h>
+#include <linux/spi/spi.h>
+#include <linux/spi/spi-mem.h>
+
+/* Register offset definitions */
+#define NUVOTON_QSPI_CTL_OFFSET 0x00 /* Control Register, RW */
+#define NUVOTON_QSPI_CLKDIV_OFFSET 0x04 /* Clock Divider Register, RW */
+#define NUVOTON_QSPI_SSCTL_OFFSET 0x08 /* Slave Select Register, RW */
+#define NUVOTON_QSPI_FIFOCTL_OFFSET 0x10 /* FIFO Control Register, RW */
+#define NUVOTON_QSPI_STATUS_OFFSET 0x14 /* Status Register, RW */
+#define NUVOTON_QSPI_TX_OFFSET 0x20 /* Data Transmit Register, WO */
+#define NUVOTON_QSPI_RX_OFFSET 0x30 /* Data Receive Register, RO */
+
+/* QSPI Control Register bit masks */
+#define NUVOTON_QSPI_CTL_QUADIOEN_MASK BIT(22) /* Quad I/O Mode Enable */
+#define NUVOTON_QSPI_CTL_DUALIOEN_MASK BIT(21) /* Dual I/O Mode Enable */
+#define NUVOTON_QSPI_CTL_DATDIR_MASK BIT(20) /* Data Port Direction Control */
+#define NUVOTON_QSPI_CTL_REORDER_MASK BIT(19) /* Byte Reorder Function Enable */
+#define NUVOTON_QSPI_CTL_LSB_MASK BIT(13) /* Send LSB First */
+#define NUVOTON_QSPI_CTL_DWIDTH_MASK GENMASK(12, 8) /* Data Width */
+#define NUVOTON_QSPI_CTL_SUSPITV_MASK GENMASK(7, 4) /* Suspend Interval */
+#define NUVOTON_QSPI_CTL_CLKPOL_MASK BIT(3) /* Clock Polarity */
+#define NUVOTON_QSPI_CTL_TXNEG_MASK BIT(2) /* Transmit on Negative Edge */
+#define NUVOTON_QSPI_CTL_RXNEG_MASK BIT(1) /* Receive on Negative Edge */
+#define NUVOTON_QSPI_CTL_SPIEN_MASK BIT(0) /* QSPI Transfer Control Enable */
+
+/* QSPI Clock Divider Register bit masks */
+#define NUVOTON_QSPI_CLKDIV_MASK GENMASK(8, 0) /* Clock Divider */
+
+/* QSPI Slave Select Control Register bit masks */
+#define NUVOTON_QSPI_SSCTL_SS1_MASK BIT(1) /* Slave Selection 1 Control */
+#define NUVOTON_QSPI_SSCTL_SS0_MASK BIT(0) /* Slave Selection 0 Control */
+
+/* QSPI FIFO Control Register bit masks */
+#define NUVOTON_QSPI_FIFOCTL_TXRST_MASK BIT(1) /* Transmit Reset */
+#define NUVOTON_QSPI_FIFOCTL_RXRST_MASK BIT(0) /* Receive Reset */
+
+/* QSPI Status Register bit masks */
+#define NUVOTON_QSPI_STATUS_TXRXRST_MASK BIT(23) /* TX or RX Reset Status */
+#define NUVOTON_QSPI_STATUS_TXFULL_MASK BIT(17) /* Transmit FIFO Full */
+#define NUVOTON_QSPI_STATUS_SPIENSTS_MASK BIT(15) /* QSPI Enable Status */
+#define NUVOTON_QSPI_STATUS_RXEMPTY_MASK BIT(8) /* Receive FIFO Empty */
+#define NUVOTON_QSPI_STATUS_BUSY_MASK BIT(0) /* Busy Status */
+
+#define NUVOTON_QSPI_DEFAULT_NUM_CS 2
+#define NUVOTON_QSPI_DEFAULT_BPW 8
+#define NUVOTON_QSPI_TIMEOUT_US 10000
+
+struct nuvoton_qspi {
+ void __iomem *regs;
+ struct clk *clk;
+ struct device *dev;
+ u32 speed_hz;
+ u8 bits_per_word;
+};
+
+static u32 nuvoton_qspi_read(struct nuvoton_qspi *qspi, u32 reg)
+{
+ return readl(qspi->regs + reg);
+}
+
+static void nuvoton_qspi_write(struct nuvoton_qspi *qspi, u32 val, u32 reg)
+{
+ writel(val, qspi->regs + reg);
+}
+
+static void nuvoton_qspi_update_bits(struct nuvoton_qspi *qspi, u32 reg,
+ u32 mask, u32 val)
+{
+ u32 tmp;
+
+ tmp = nuvoton_qspi_read(qspi, reg);
+ tmp &= ~mask;
+ tmp |= val & mask;
+ nuvoton_qspi_write(qspi, tmp, reg);
+}
+
+static int nuvoton_qspi_wait_ready(struct nuvoton_qspi *qspi)
+{
+ u32 val;
+
+ return readl_poll_timeout(qspi->regs + NUVOTON_QSPI_STATUS_OFFSET,
+ val,
+ !(val & NUVOTON_QSPI_STATUS_BUSY_MASK),
+ 0, NUVOTON_QSPI_TIMEOUT_US);
+}
+
+static int nuvoton_qspi_reset_fifo(struct nuvoton_qspi *qspi)
+{
+ u32 val;
+
+ val = nuvoton_qspi_read(qspi, NUVOTON_QSPI_FIFOCTL_OFFSET);
+ val |= NUVOTON_QSPI_FIFOCTL_TXRST_MASK |
+ NUVOTON_QSPI_FIFOCTL_RXRST_MASK;
+ nuvoton_qspi_write(qspi, val, NUVOTON_QSPI_FIFOCTL_OFFSET);
+
+ /*
+ * Give the controller a short time to latch the FIFO reset request
+ * before polling the reset status bit.
+ */
+ udelay(1);
+
+ return readl_poll_timeout_atomic(qspi->regs + NUVOTON_QSPI_STATUS_OFFSET,
+ val,
+ !(val & NUVOTON_QSPI_STATUS_TXRXRST_MASK),
+ 1, NUVOTON_QSPI_TIMEOUT_US);
+}
+
+static int nuvoton_qspi_set_speed(struct nuvoton_qspi *qspi, u32 speed_hz)
+{
+ unsigned long clk_rate;
+ u32 div;
+
+ if (!speed_hz)
+ return -EINVAL;
+
+ if (qspi->speed_hz == speed_hz)
+ return 0;
+
+ clk_rate = clk_get_rate(qspi->clk);
+ if (!clk_rate) {
+ dev_err(qspi->dev, "failed to get clock rate\n");
+ return -EINVAL;
+ }
+
+ div = DIV_ROUND_UP(clk_rate, speed_hz) - 1;
+ if (div > FIELD_MAX(NUVOTON_QSPI_CLKDIV_MASK)) {
+ dev_err(qspi->dev, "unsupported SPI clock %u Hz\n", speed_hz);
+ return -EINVAL;
+ }
+
+ nuvoton_qspi_write(qspi, FIELD_PREP(NUVOTON_QSPI_CLKDIV_MASK, div),
+ NUVOTON_QSPI_CLKDIV_OFFSET);
+ qspi->speed_hz = speed_hz;
+
+ return 0;
+}
+
+static int nuvoton_qspi_set_bits_per_word(struct nuvoton_qspi *qspi, u8 bpw)
+{
+ u32 val;
+
+ if (bpw != 8 && bpw != 16 && bpw != 32)
+ return -EINVAL;
+
+ if (bpw == 32)
+ val = NUVOTON_QSPI_CTL_REORDER_MASK;
+ else
+ val = FIELD_PREP(NUVOTON_QSPI_CTL_DWIDTH_MASK, bpw);
+
+ nuvoton_qspi_update_bits(qspi, NUVOTON_QSPI_CTL_OFFSET,
+ NUVOTON_QSPI_CTL_DWIDTH_MASK |
+ NUVOTON_QSPI_CTL_REORDER_MASK, val);
+ qspi->bits_per_word = bpw;
+
+ return 0;
+}
+
+static int nuvoton_qspi_setup_transfer(struct spi_device *spi,
+ u32 speed_hz, u8 bpw)
+{
+ struct nuvoton_qspi *qspi = spi_controller_get_devdata(spi->controller);
+ u32 mode = spi->mode & SPI_MODE_X_MASK;
+ u32 ctl = 0;
+ int ret;
+
+ if (!speed_hz)
+ speed_hz = spi->max_speed_hz;
+
+ if (!bpw)
+ bpw = NUVOTON_QSPI_DEFAULT_BPW;
+
+ ret = nuvoton_qspi_set_speed(qspi, speed_hz);
+ if (ret)
+ return ret;
+
+ ret = nuvoton_qspi_set_bits_per_word(qspi, bpw);
+ if (ret)
+ return ret;
+
+ if (mode == SPI_MODE_0 || mode == SPI_MODE_3)
+ ctl |= NUVOTON_QSPI_CTL_TXNEG_MASK;
+ else
+ ctl |= NUVOTON_QSPI_CTL_RXNEG_MASK;
+
+ if (spi->mode & SPI_CPOL)
+ ctl |= NUVOTON_QSPI_CTL_CLKPOL_MASK;
+
+ if (spi->mode & SPI_LSB_FIRST)
+ ctl |= NUVOTON_QSPI_CTL_LSB_MASK;
+
+ nuvoton_qspi_update_bits(qspi, NUVOTON_QSPI_CTL_OFFSET,
+ NUVOTON_QSPI_CTL_TXNEG_MASK |
+ NUVOTON_QSPI_CTL_RXNEG_MASK |
+ NUVOTON_QSPI_CTL_CLKPOL_MASK |
+ NUVOTON_QSPI_CTL_LSB_MASK, ctl);
+
+ return 0;
+}
+
+static void nuvoton_qspi_set_bus_width(struct nuvoton_qspi *qspi,
+ unsigned int buswidth,
+ enum spi_mem_data_dir dir)
+{
+ u32 ctl = 0;
+
+ if (buswidth == 4)
+ ctl |= NUVOTON_QSPI_CTL_QUADIOEN_MASK;
+ else if (buswidth == 2)
+ ctl |= NUVOTON_QSPI_CTL_DUALIOEN_MASK;
+
+ if (buswidth > 1 && dir == SPI_MEM_DATA_OUT)
+ ctl |= NUVOTON_QSPI_CTL_DATDIR_MASK;
+
+ nuvoton_qspi_update_bits(qspi, NUVOTON_QSPI_CTL_OFFSET,
+ NUVOTON_QSPI_CTL_QUADIOEN_MASK |
+ NUVOTON_QSPI_CTL_DUALIOEN_MASK |
+ NUVOTON_QSPI_CTL_DATDIR_MASK, ctl);
+}
+
+static u32 nuvoton_qspi_tx_word(const void *txbuf, unsigned int idx, u8 bpw)
+{
+ if (!txbuf)
+ return 0;
+
+ if (bpw <= 8)
+ return ((const u8 *)txbuf)[idx];
+ if (bpw <= 16)
+ return ((const u16 *)txbuf)[idx];
+
+ return ((const u32 *)txbuf)[idx];
+}
+
+static void nuvoton_qspi_rx_word(void *rxbuf, unsigned int idx, u32 val, u8 bpw)
+{
+ if (!rxbuf)
+ return;
+
+ if (bpw <= 8)
+ ((u8 *)rxbuf)[idx] = val;
+ else if (bpw <= 16)
+ ((u16 *)rxbuf)[idx] = val;
+ else
+ ((u32 *)rxbuf)[idx] = val;
+}
+
+static int nuvoton_qspi_wait_tx_not_full(struct nuvoton_qspi *qspi)
+{
+ u32 val;
+
+ return readl_poll_timeout_atomic(qspi->regs + NUVOTON_QSPI_STATUS_OFFSET,
+ val,
+ !(val & NUVOTON_QSPI_STATUS_TXFULL_MASK),
+ 0, NUVOTON_QSPI_TIMEOUT_US);
+}
+
+static int nuvoton_qspi_wait_rx_not_empty(struct nuvoton_qspi *qspi)
+{
+ u32 val;
+
+ return readl_poll_timeout_atomic(qspi->regs + NUVOTON_QSPI_STATUS_OFFSET,
+ val,
+ !(val & NUVOTON_QSPI_STATUS_RXEMPTY_MASK),
+ 0, NUVOTON_QSPI_TIMEOUT_US);
+}
+
+static int nuvoton_qspi_txrx(struct nuvoton_qspi *qspi, const void *txbuf,
+ void *rxbuf, unsigned int len)
+{
+ unsigned int bytes_per_word = DIV_ROUND_UP(qspi->bits_per_word, 8);
+ unsigned int words;
+ u32 val;
+ int ret;
+ int i;
+
+ if (!len)
+ return 0;
+
+ if (len % bytes_per_word)
+ return -EINVAL;
+
+ words = len / bytes_per_word;
+
+ ret = nuvoton_qspi_reset_fifo(qspi);
+ if (ret) {
+ dev_err(qspi->dev, "FIFO reset timed out\n");
+ return ret;
+ }
+
+ /*
+ * Use conservative word-by-word PIO access. This keeps the initial driver
+ * simple and avoids relying on FIFO threshold interrupts or DMA support.
+ *
+ * The MA35D1 QSPI controller pushes one RX FIFO entry for each TX word in
+ * single, dual-output and quad-output modes. Drain RX after every TX word
+ * and discard the value for TX-only transfers to avoid RX FIFO overflow.
+ */
+ for (i = 0; i < words; i++) {
+ ret = nuvoton_qspi_wait_tx_not_full(qspi);
+ if (ret) {
+ dev_err(qspi->dev, "TX FIFO full timeout\n");
+ return ret;
+ }
+
+ nuvoton_qspi_write(qspi, nuvoton_qspi_tx_word(txbuf, i,
+ qspi->bits_per_word),
+ NUVOTON_QSPI_TX_OFFSET);
+
+ ret = nuvoton_qspi_wait_rx_not_empty(qspi);
+ if (ret) {
+ dev_err(qspi->dev, "RX FIFO empty timeout\n");
+ return ret;
+ }
+
+ val = nuvoton_qspi_read(qspi, NUVOTON_QSPI_RX_OFFSET);
+ if (rxbuf)
+ nuvoton_qspi_rx_word(rxbuf, i, val, qspi->bits_per_word);
+ }
+
+ ret = nuvoton_qspi_wait_ready(qspi);
+ if (ret)
+ dev_err(qspi->dev, "controller busy timeout\n");
+
+ return ret;
+}
+
+static int nuvoton_qspi_hw_init(struct nuvoton_qspi *qspi)
+{
+ u32 val;
+ int ret;
+
+ ret = nuvoton_qspi_set_bits_per_word(qspi, NUVOTON_QSPI_DEFAULT_BPW);
+ if (ret)
+ return ret;
+
+ nuvoton_qspi_update_bits(qspi, NUVOTON_QSPI_CTL_OFFSET,
+ NUVOTON_QSPI_CTL_SUSPITV_MASK |
+ NUVOTON_QSPI_CTL_TXNEG_MASK |
+ NUVOTON_QSPI_CTL_RXNEG_MASK |
+ NUVOTON_QSPI_CTL_CLKPOL_MASK |
+ NUVOTON_QSPI_CTL_LSB_MASK,
+ NUVOTON_QSPI_CTL_TXNEG_MASK);
+
+ val = nuvoton_qspi_read(qspi, NUVOTON_QSPI_CTL_OFFSET);
+ nuvoton_qspi_write(qspi, val | NUVOTON_QSPI_CTL_SPIEN_MASK,
+ NUVOTON_QSPI_CTL_OFFSET);
+
+ ret = readl_poll_timeout(qspi->regs + NUVOTON_QSPI_STATUS_OFFSET, val,
+ (val & NUVOTON_QSPI_STATUS_SPIENSTS_MASK),
+ 1, NUVOTON_QSPI_TIMEOUT_US);
+ if (ret) {
+ dev_err(qspi->dev, "failed to enable controller\n");
+ return ret;
+ }
+
+ ret = nuvoton_qspi_reset_fifo(qspi);
+ if (ret)
+ dev_err(qspi->dev, "FIFO reset timed out\n");
+
+ return ret;
+}
+
+static bool nuvoton_qspi_mem_supports_op(struct spi_mem *mem,
+ const struct spi_mem_op *op)
+{
+ if (!spi_mem_default_supports_op(mem, op))
+ return false;
+
+ if (op->cmd.buswidth > 4 || op->addr.buswidth > 4 ||
+ op->dummy.buswidth > 4 || op->data.buswidth > 4)
+ return false;
+
+ if (op->cmd.nbytes != 1)
+ return false;
+
+ if (op->addr.nbytes > 4)
+ return false;
+
+ return true;
+}
+
+static void nuvoton_qspi_set_cs_level(struct nuvoton_qspi *qspi,
+ unsigned int cs, bool assert)
+{
+ u32 mask;
+ u32 val;
+
+ switch (cs) {
+ case 0:
+ mask = NUVOTON_QSPI_SSCTL_SS0_MASK;
+ break;
+ case 1:
+ mask = NUVOTON_QSPI_SSCTL_SS1_MASK;
+ break;
+ default:
+ dev_warn(qspi->dev, "invalid chip select %u\n", cs);
+ return;
+ }
+
+ val = nuvoton_qspi_read(qspi, NUVOTON_QSPI_SSCTL_OFFSET);
+
+ if (assert)
+ val |= mask;
+ else
+ val &= ~mask;
+
+ nuvoton_qspi_write(qspi, val, NUVOTON_QSPI_SSCTL_OFFSET);
+}
+
+static void nuvoton_qspi_set_cs(struct spi_device *spi, bool enable)
+{
+ struct nuvoton_qspi *qspi = spi_controller_get_devdata(spi->controller);
+
+ nuvoton_qspi_set_cs_level(qspi, spi_get_chipselect(spi, 0), enable);
+}
+
+static void nuvoton_qspi_mem_set_cs(struct spi_device *spi, bool enable)
+{
+ struct nuvoton_qspi *qspi = spi_controller_get_devdata(spi->controller);
+ bool assert = enable;
+
+ if (spi->mode & SPI_CS_HIGH)
+ assert = !assert;
+
+ nuvoton_qspi_set_cs_level(qspi, spi_get_chipselect(spi, 0), assert);
+}
+
+static int nuvoton_qspi_mem_exec_op(struct spi_mem *mem,
+ const struct spi_mem_op *op)
+{
+ struct spi_device *spi = mem->spi;
+ struct nuvoton_qspi *qspi = spi_controller_get_devdata(spi->controller);
+ u8 opcode = op->cmd.opcode;
+ u8 addr[4];
+ int ret;
+ int i;
+
+ ret = nuvoton_qspi_setup_transfer(spi, op->max_freq, NUVOTON_QSPI_DEFAULT_BPW);
+ if (ret)
+ return ret;
+
+ nuvoton_qspi_mem_set_cs(spi, true);
+
+ nuvoton_qspi_set_bus_width(qspi, op->cmd.buswidth, SPI_MEM_DATA_OUT);
+ ret = nuvoton_qspi_txrx(qspi, &opcode, NULL, 1);
+ if (ret)
+ goto out_deassert_cs;
+
+ if (op->addr.nbytes) {
+ for (i = 0; i < op->addr.nbytes; i++)
+ addr[i] = op->addr.val >> (8 * (op->addr.nbytes - i - 1));
+
+ nuvoton_qspi_set_bus_width(qspi, op->addr.buswidth,
+ SPI_MEM_DATA_OUT);
+ ret = nuvoton_qspi_txrx(qspi, addr, NULL, op->addr.nbytes);
+ if (ret)
+ goto out_deassert_cs;
+ }
+
+ if (op->dummy.nbytes) {
+ nuvoton_qspi_set_bus_width(qspi, op->dummy.buswidth,
+ SPI_MEM_DATA_IN);
+ ret = nuvoton_qspi_txrx(qspi, NULL, NULL, op->dummy.nbytes);
+ if (ret)
+ goto out_deassert_cs;
+ }
+
+ if (op->data.nbytes) {
+ nuvoton_qspi_set_bus_width(qspi, op->data.buswidth,
+ op->data.dir);
+ ret = nuvoton_qspi_txrx(qspi,
+ op->data.dir == SPI_MEM_DATA_OUT ?
+ op->data.buf.out : NULL,
+ op->data.dir == SPI_MEM_DATA_IN ?
+ op->data.buf.in : NULL,
+ op->data.nbytes);
+ }
+
+out_deassert_cs:
+ nuvoton_qspi_set_bus_width(qspi, 1, SPI_MEM_DATA_IN);
+ nuvoton_qspi_mem_set_cs(spi, false);
+
+ return ret;
+}
+
+static const struct spi_controller_mem_ops nuvoton_qspi_mem_ops = {
+ .supports_op = nuvoton_qspi_mem_supports_op,
+ .exec_op = nuvoton_qspi_mem_exec_op,
+};
+
+static const struct spi_controller_mem_caps nuvoton_qspi_mem_caps = {
+ .per_op_freq = true,
+};
+
+static int nuvoton_qspi_transfer_one(struct spi_controller *ctlr,
+ struct spi_device *spi,
+ struct spi_transfer *xfer)
+{
+ struct nuvoton_qspi *qspi = spi_controller_get_devdata(ctlr);
+ enum spi_mem_data_dir dir = SPI_MEM_DATA_IN;
+ unsigned int buswidth = 1;
+ int ret;
+
+ ret = nuvoton_qspi_setup_transfer(spi, xfer->speed_hz, xfer->bits_per_word);
+ if (ret)
+ return ret;
+
+ if (xfer->tx_buf && xfer->rx_buf) {
+ if (xfer->tx_nbits != SPI_NBITS_SINGLE ||
+ xfer->rx_nbits != SPI_NBITS_SINGLE)
+ return -EOPNOTSUPP;
+ }
+
+ if (xfer->tx_buf) {
+ dir = SPI_MEM_DATA_OUT;
+ if (xfer->tx_nbits == SPI_NBITS_QUAD)
+ buswidth = 4;
+ else if (xfer->tx_nbits == SPI_NBITS_DUAL)
+ buswidth = 2;
+ } else if (xfer->rx_buf) {
+ if (xfer->rx_nbits == SPI_NBITS_QUAD)
+ buswidth = 4;
+ else if (xfer->rx_nbits == SPI_NBITS_DUAL)
+ buswidth = 2;
+ }
+
+ nuvoton_qspi_set_bus_width(qspi, buswidth, dir);
+ ret = nuvoton_qspi_txrx(qspi, xfer->tx_buf, xfer->rx_buf, xfer->len);
+ nuvoton_qspi_set_bus_width(qspi, 1, SPI_MEM_DATA_IN);
+
+ return ret;
+}
+
+static int nuvoton_qspi_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct spi_controller *ctlr;
+ struct nuvoton_qspi *qspi;
+ struct reset_control *rst;
+ int ret;
+
+ ctlr = devm_spi_alloc_host(dev, sizeof(*qspi));
+ if (!ctlr)
+ return -ENOMEM;
+
+ platform_set_drvdata(pdev, ctlr);
+
+ qspi = spi_controller_get_devdata(ctlr);
+ qspi->dev = dev;
+ qspi->bits_per_word = NUVOTON_QSPI_DEFAULT_BPW;
+
+ qspi->regs = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(qspi->regs))
+ return PTR_ERR(qspi->regs);
+
+ rst = devm_reset_control_get_exclusive(dev, NULL);
+ if (IS_ERR(rst))
+ return dev_err_probe(dev, PTR_ERR(rst),
+ "failed to get reset\n");
+
+ qspi->clk = devm_clk_get_enabled(dev, NULL);
+ if (IS_ERR(qspi->clk))
+ return dev_err_probe(dev, PTR_ERR(qspi->clk),
+ "failed to get and enable clock\n");
+
+ ret = reset_control_assert(rst);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to assert reset\n");
+
+ udelay(2);
+
+ ret = reset_control_deassert(rst);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to deassert reset\n");
+
+ ctlr->num_chipselect = NUVOTON_QSPI_DEFAULT_NUM_CS;
+ ctlr->mem_ops = &nuvoton_qspi_mem_ops;
+ ctlr->mem_caps = &nuvoton_qspi_mem_caps;
+ ctlr->set_cs = nuvoton_qspi_set_cs;
+ ctlr->transfer_one = nuvoton_qspi_transfer_one;
+ ctlr->bits_per_word_mask = SPI_BPW_MASK(8) | SPI_BPW_MASK(16) |
+ SPI_BPW_MASK(32);
+ ctlr->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH | SPI_LSB_FIRST |
+ SPI_RX_DUAL | SPI_TX_DUAL |
+ SPI_RX_QUAD | SPI_TX_QUAD;
+ ctlr->dev.of_node = dev->of_node;
+
+ ret = nuvoton_qspi_hw_init(qspi);
+ if (ret)
+ return ret;
+
+ ret = devm_spi_register_controller(dev, ctlr);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to register spi controller\n");
+
+ return 0;
+}
+
+static const struct of_device_id nuvoton_qspi_of_match[] = {
+ { .compatible = "nuvoton,ma35d1-qspi" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, nuvoton_qspi_of_match);
+
+static struct platform_driver nuvoton_qspi_driver = {
+ .driver = {
+ .name = "ma35d1-qspi",
+ .of_match_table = nuvoton_qspi_of_match,
+ },
+ .probe = nuvoton_qspi_probe,
+};
+module_platform_driver(nuvoton_qspi_driver);
+
+MODULE_DESCRIPTION("Nuvoton MA35D1 QSPI controller driver");
+MODULE_AUTHOR("Chi-Wen Weng <cwweng@nuvoton.com>");
+MODULE_LICENSE("GPL");
--
2.25.1
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH v3 2/2] spi: ma35d1-qspi: Add Nuvoton MA35D1 QSPI controller support
2026-06-11 9:12 ` [PATCH v3 2/2] spi: ma35d1-qspi: Add Nuvoton MA35D1 QSPI controller support Chi-Wen Weng
@ 2026-06-11 9:27 ` sashiko-bot
0 siblings, 0 replies; 4+ messages in thread
From: sashiko-bot @ 2026-06-11 9:27 UTC (permalink / raw)
To: Chi-Wen Weng; +Cc: robh, conor+dt, devicetree
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Inconsistent chip-select polarity logic between SPI core and spi-mem operations leads to inverted CS levels.
- [High] Data race on the Slave Select Control (SSCTL) register due to concurrent `spi_setup()` and transfers.
- [High] The PIO transfer loop uses atomic polling for bulk data transfers without yielding the CPU, risking soft lockups on large memory operations.
--
commit 870f4cc6d9d6ab3e7f64bb28977c07e0fcfd4027
Author: Chi-Wen Weng <cwweng@nuvoton.com>
spi: ma35d1-qspi: Add Nuvoton MA35D1 QSPI controller support
Add SPI controller driver support for the Nuvoton MA35D1 Quad SPI
controller. The controller supports standard SPI transfers and spi-mem
operations in single, dual, and quad modes.
diff --git a/drivers/spi/spi-ma35d1-qspi.c b/drivers/spi/spi-ma35d1-qspi.c
new file mode 100644
index 0000000000000..d3e687b6a06d7
--- /dev/null
+++ b/drivers/spi/spi-ma35d1-qspi.c
[ ... ]
> +static int nuvoton_qspi_txrx(struct nuvoton_qspi *qspi, const void *txbuf,
> + void *rxbuf, unsigned int len)
> +{
> + unsigned int bytes_per_word = DIV_ROUND_UP(qspi->bits_per_word, 8);
> + unsigned int words;
> + u32 val;
> + int ret;
> + int i;
> +
> + if (!len)
> + return 0;
> +
> + if (len % bytes_per_word)
> + return -EINVAL;
> +
> + words = len / bytes_per_word;
> +
> + ret = nuvoton_qspi_reset_fifo(qspi);
> + if (ret) {
> + dev_err(qspi->dev, "FIFO reset timed out\n");
> + return ret;
> + }
> +
> + /*
> + * Use conservative word-by-word PIO access. This keeps the initial driver
> + * simple and avoids relying on FIFO threshold interrupts or DMA support.
> + *
> + * The MA35D1 QSPI controller pushes one RX FIFO entry for each TX word in
> + * single, dual-output and quad-output modes. Drain RX after every TX word
> + * and discard the value for TX-only transfers to avoid RX FIFO overflow.
> + */
> + for (i = 0; i < words; i++) {
> + ret = nuvoton_qspi_wait_tx_not_full(qspi);
> + if (ret) {
> + dev_err(qspi->dev, "TX FIFO full timeout\n");
> + return ret;
> + }
> +
> + nuvoton_qspi_write(qspi, nuvoton_qspi_tx_word(txbuf, i,
> + qspi->bits_per_word),
> + NUVOTON_QSPI_TX_OFFSET);
> +
> + ret = nuvoton_qspi_wait_rx_not_empty(qspi);
[Severity: High]
Will this loop trigger kernel soft lockups on large memory operations?
The wait helpers nuvoton_qspi_wait_tx_not_full() and
nuvoton_qspi_wait_rx_not_empty() use readl_poll_timeout_atomic(), which
busy-waits without yielding the CPU.
Since the driver does not set max_transfer_size limits, the SPI framework
can pass down arbitrarily large buffers (e.g., several megabytes for firmware
flashes or filesystems).
Does this loop need a cond_resched(), or should the controller set
appropriate transfer size limits to prevent CPU stalls?
> + if (ret) {
> + dev_err(qspi->dev, "RX FIFO empty timeout\n");
> + return ret;
> + }
> +
> + val = nuvoton_qspi_read(qspi, NUVOTON_QSPI_RX_OFFSET);
> + if (rxbuf)
> + nuvoton_qspi_rx_word(rxbuf, i, val, qspi->bits_per_word);
> + }
[ ... ]
> +static void nuvoton_qspi_set_cs_level(struct nuvoton_qspi *qspi,
> + unsigned int cs, bool assert)
> +{
> + u32 mask;
> + u32 val;
> +
> + switch (cs) {
> + case 0:
> + mask = NUVOTON_QSPI_SSCTL_SS0_MASK;
> + break;
> + case 1:
> + mask = NUVOTON_QSPI_SSCTL_SS1_MASK;
> + break;
> + default:
> + dev_warn(qspi->dev, "invalid chip select %u\n", cs);
> + return;
> + }
> +
> + val = nuvoton_qspi_read(qspi, NUVOTON_QSPI_SSCTL_OFFSET);
> +
> + if (assert)
> + val |= mask;
> + else
> + val &= ~mask;
> +
> + nuvoton_qspi_write(qspi, val, NUVOTON_QSPI_SSCTL_OFFSET);
[Severity: High]
Can this unprotected read-modify-write corrupt the active chip-select state?
The SPI core invokes spi_set_cs() directly during spi_setup() without holding
controller-level locks.
If a transfer is active for CS0, and a concurrent setup call occurs for CS1
(such as an ioctl(SPI_IOC_WR_MODE) from spidev), could this read-modify-write
on the shared SSCTL register race and cause silent data corruption or aborted
transfers?
> +}
> +
> +static void nuvoton_qspi_set_cs(struct spi_device *spi, bool enable)
> +{
> + struct nuvoton_qspi *qspi = spi_controller_get_devdata(spi->controller);
> +
> + nuvoton_qspi_set_cs_level(qspi, spi_get_chipselect(spi, 0), enable);
> +}
> +
> +static void nuvoton_qspi_mem_set_cs(struct spi_device *spi, bool enable)
> +{
> + struct nuvoton_qspi *qspi = spi_controller_get_devdata(spi->controller);
> + bool assert = enable;
> +
> + if (spi->mode & SPI_CS_HIGH)
> + assert = !assert;
> +
> + nuvoton_qspi_set_cs_level(qspi, spi_get_chipselect(spi, 0), assert);
[Severity: High]
Does this cause contradictory chip-select polarity between standard SPI
transfers and SPI memory operations?
The SPI framework passes the physical level to drive to ctlr->set_cs(),
where false means drive low and true means drive high. nuvoton_qspi_set_cs()
passes this directly to nuvoton_qspi_set_cs_level().
However, nuvoton_qspi_mem_set_cs() treats its enable parameter as a logical
assertion flag and performs a manual inversion.
For an active-low device, will one of these paths end up driving the
chip-select line with the wrong polarity?
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260611091246.2070485-1-cwweng.linux@gmail.com?part=2
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-06-11 9:27 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-11 9:12 [PATCH v3 0/2] spi: ma35d1-qspi: Add Nuvoton MA35D1 QSPI controller Chi-Wen Weng
2026-06-11 9:12 ` [PATCH v3 1/2] dt-bindings: spi: nuvoton,ma35d1-qspi: Add Nuvoton MA35D1 QSPI Chi-Wen Weng
2026-06-11 9:12 ` [PATCH v3 2/2] spi: ma35d1-qspi: Add Nuvoton MA35D1 QSPI controller support Chi-Wen Weng
2026-06-11 9:27 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox