diff mbox series

[08/10] spi: atcspi200_spi: Add spi-mem support for the Metanoia ATCSPI200 variant

Message ID 20261002-mt2824-v1-8-bc44cf0e0289@bootlin.com
State Under Review
Delegated to: Tim Ouyang
Headers show
Series riscv: Add support for Metanoia MT2824 SoC and MT5824 EVB | expand

Commit Message

Benoît Monin Oct. 2, 2026, 1:02 p.m. UTC
The Metanoia variant of the ATCSPI200 core adds hardware command and
address phases along with the dual and quad I/O data formats. Expose
them through the spi-mem operations, which is required to use SPI NOR
flash devices at full speed.

The Metanoia variant is detected at probe time with
device_is_compatible() and gets the mem_ops assigned, following the
same pattern as the Octeon driver. The Andes instances keep the
generic spi-mem fallback built on top of the .xfer operation,
unchanged.

Signed-off-by: Benoît Monin <benoit.monin@bootlin.com>
---
 drivers/spi/atcspi200_spi.c | 176 ++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 168 insertions(+), 8 deletions(-)
diff mbox series

Patch

diff --git a/drivers/spi/atcspi200_spi.c b/drivers/spi/atcspi200_spi.c
index 72b612c6560..a229b196de1 100644
--- a/drivers/spi/atcspi200_spi.c
+++ b/drivers/spi/atcspi200_spi.c
@@ -10,6 +10,7 @@ 
 #include <log.h>
 #include <malloc.h>
 #include <spi.h>
+#include <spi-mem.h>
 #include <asm/global_data.h>
 #include <asm/io.h>
 #include <dm.h>
@@ -30,6 +31,7 @@  struct atcspi200_spi_regs {
 	u32	reserve1[3];
 	u32	format;		/* 0x10 */
 #define DATA_LENGTH(x)	((x-1)<<8)
+#define ADDR_LENGTH(x)	((x-1)<<16)
 	u32	pio;
 	u32	reserve2[2];
 	u32	tctrl;		/* 0x20 */
@@ -49,6 +51,10 @@  struct atcspi200_spi_regs {
 #define WCNT_MASK	(0x1FF<<WCNT_OFFSET)
 #define RCNT_OFFSET	0
 #define RCNT_MASK	(0x1FF<<RCNT_OFFSET)
+#define CMD_EN		(1<<30)
+#define ADDR_EN		(1<<29)
+#define DUAL_QUAD(x)	((x)<<22)
+#define DUMMY_CNT(x)	(((x)-1)<<9)
 	u32	cmd;
 	u32	addr;
 	u32	data;
@@ -364,6 +370,164 @@  static int atcspi200_spi_get_clk(struct udevice *bus)
 	return 0;
 }
 
+/*
+ * The dummy phase is generated in units of 8 bits: one unit takes
+ * (8 / data buswidth) SCLK cycles. The unit count is programmed in the
+ * 2-bit DummyCnt field of the TransCtrl register.
+ */
+static unsigned int atcspi200_mem_dummy_units(const struct spi_mem_op *op)
+{
+	unsigned int cycles;
+
+	cycles = op->dummy.nbytes * 8 /
+		 max_t(unsigned int, op->dummy.buswidth, 1U);
+
+	return cycles * op->data.buswidth / 8;
+}
+
+static int atcspi200_mem_adjust_op_size(struct spi_slave *slave,
+					struct spi_mem_op *op)
+{
+	op->data.nbytes = min(op->data.nbytes, (unsigned int)MAX_TRANSFER_LEN);
+
+	return 0;
+}
+
+static bool atcspi200_mem_supports_op(struct spi_slave *slave,
+				      const struct spi_mem_op *op)
+{
+	unsigned int cycles, units;
+
+	if (!spi_mem_default_supports_op(slave, op))
+		return false;
+
+	/* the address phase is always transferred on a single line */
+	if (op->addr.nbytes > 4 || (op->addr.nbytes && op->addr.buswidth != 1))
+		return false;
+
+	/* a dummy phase without data cannot be expressed by the controller */
+	if (op->dummy.nbytes && !op->data.nbytes)
+		return false;
+
+	if (!op->data.nbytes)
+		return true;
+
+	if (op->data.buswidth != 1 && op->data.buswidth != 2 &&
+	    op->data.buswidth != 4)
+		return false;
+
+	/* the write and read counters are 9-bit fields */
+	if (op->data.nbytes > MAX_TRANSFER_LEN)
+		return false;
+
+	/* reject dummy cycles which cannot be represented by DummyCnt */
+	cycles = op->dummy.nbytes * 8 /
+		 max_t(unsigned int, op->dummy.buswidth, 1U);
+	if (cycles * op->data.buswidth % 8)
+		return false;
+
+	units = cycles * op->data.buswidth / 8;
+
+	return units <= 4;
+}
+
+static int atcspi200_mem_exec_op(struct spi_slave *slave,
+				 const struct spi_mem_op *op)
+{
+	struct udevice *bus = slave->dev->parent;
+	struct nds_spi_slave *ns = dev_get_priv(bus);
+	unsigned int format, tc = 0, units;
+	unsigned int len = op->data.nbytes;
+	const u8 *dout = op->data.buf.out;
+	u8 *din = op->data.buf.in;
+	int timeout = SPI_TIMEOUT;
+
+	/* reprogram the address length for this operation */
+	format = in_le32(&ns->regs->format) & ~(0x3 << 16);
+	if (op->addr.nbytes)
+		format |= ADDR_LENGTH(op->addr.nbytes);
+	out_le32(&ns->regs->format, format);
+
+	if (op->cmd.nbytes)
+		tc |= CMD_EN;
+	if (op->addr.nbytes) {
+		tc |= ADDR_EN;
+		out_le32(&ns->regs->addr, op->addr.val);
+	}
+
+	if (op->data.nbytes) {
+		if (op->data.buswidth == 2)
+			tc |= DUAL_QUAD(1);
+		else if (op->data.buswidth == 4)
+			tc |= DUAL_QUAD(2);
+
+		if (op->data.dir == SPI_MEM_DATA_IN) {
+			units = atcspi200_mem_dummy_units(op);
+			tc |= units ? TRAMODE_DR : TRAMODE_RO;
+			if (units)
+				tc |= DUMMY_CNT(units);
+			tc |= (op->data.nbytes - 1) & 0x1FF;
+		} else {
+			tc |= TRAMODE_WO;
+			tc |= ((op->data.nbytes - 1) & 0x1FF) << WCNT_OFFSET;
+		}
+	} else {
+		tc |= TRAMODE_NONE;
+	}
+
+	out_le32(&ns->regs->tctrl, tc);
+	out_le32(&ns->regs->cmd, op->cmd.opcode);
+
+	while (len) {
+		unsigned int event = in_le32(&ns->regs->status);
+
+		if (op->data.dir == SPI_MEM_DATA_OUT) {
+			if (!(event & TXFFL)) {
+				out_le32(&ns->regs->data, *dout++);
+				len--;
+			}
+		} else {
+			if (event & RXFVE_MASK) {
+				*din++ = in_le32(&ns->regs->data);
+				len--;
+			}
+		}
+
+		if (!timeout--)
+			return -ETIMEDOUT;
+	}
+
+	while (in_le32(&ns->regs->status) & SPIBSY) {
+		if (!timeout--)
+			return -ETIMEDOUT;
+	}
+
+	/* restore the timing register like the legacy path does */
+	out_le32(&ns->regs->timing, ns->mtiming);
+
+	return 0;
+}
+
+/*
+ * The Metanoia variant of the ATCSPI200 core adds the command and address
+ * phases along with the dual and quad I/O data formats, exposed through the
+ * spi-mem operations assigned at probe time below. The other instances keep
+ * using the generic spi-mem fallback on top of .xfer.
+ */
+static struct dm_spi_ops atcspi200_spi_ops = {
+	.claim_bus	= atcspi200_spi_claim_bus,
+	.release_bus	= atcspi200_spi_release_bus,
+	.xfer		= atcspi200_spi_xfer,
+	.set_speed	= atcspi200_spi_set_speed,
+	.set_mode	= atcspi200_spi_set_mode,
+};
+
+static const struct spi_controller_mem_ops atcspi200_mem_ops = {
+	.adjust_op_size = atcspi200_mem_adjust_op_size,
+	.supports_op = atcspi200_mem_supports_op,
+	.exec_op = atcspi200_mem_exec_op,
+};
+
 static int atcspi200_spi_probe(struct udevice *bus)
 {
 	struct nds_spi_slave *ns = dev_get_priv(bus);
@@ -373,6 +537,9 @@  static int atcspi200_spi_probe(struct udevice *bus)
 	ns->mtiming = ns->regs->timing;
 	atcspi200_spi_get_clk(bus);
 
+	if (device_is_compatible(bus, "metanoia,mt2824-spi"))
+		atcspi200_spi_ops.mem_ops = &atcspi200_mem_ops;
+
 	return 0;
 }
 
@@ -394,16 +561,9 @@  static int atcspi200_ofdata_to_platadata(struct udevice *bus)
 	return 0;
 }
 
-static const struct dm_spi_ops atcspi200_spi_ops = {
-	.claim_bus	= atcspi200_spi_claim_bus,
-	.release_bus	= atcspi200_spi_release_bus,
-	.xfer		= atcspi200_spi_xfer,
-	.set_speed	= atcspi200_spi_set_speed,
-	.set_mode	= atcspi200_spi_set_mode,
-};
-
 static const struct udevice_id atcspi200_spi_ids[] = {
 	{ .compatible = "andestech,atcspi200" },
+	{ .compatible = "metanoia,mt2824-spi" },
 	{ }
 };