All of lore.kernel.org
 help / color / mirror / Atom feed
From: "Jonathan Neuschäfer" <j.neuschaefer@gmx.net>
To: linux-spi@vger.kernel.org, openbmc@lists.ozlabs.org
Cc: "Rob Herring" <robh+dt@kernel.org>,
	"Krzysztof Kozlowski" <krzysztof.kozlowski+dt@linaro.org>,
	"Jonathan Neuschäfer" <j.neuschaefer@gmx.net>,
	devicetree@vger.kernel.org, linux-kernel@vger.kernel.org,
	"Mark Brown" <broonie@kernel.org>
Subject: [PATCH v2 2/3] spi: wpcm-fiu: Add driver for Nuvoton WPCM450 Flash Interface Unit (FIU)
Date: Thu, 24 Nov 2022 20:13:59 +0100	[thread overview]
Message-ID: <20221124191400.287918-3-j.neuschaefer@gmx.net> (raw)
In-Reply-To: <20221124191400.287918-1-j.neuschaefer@gmx.net>

The Flash Interface Unit (FIU) is the SPI flash controller in the
Nuvoton WPCM450 BMC SoC. It supports four chip selects, and direct
(memory-mapped) access to 16 MiB per chip. Larger flash chips can be
accessed by software-defined SPI transfers.

The FIU in newer NPCM7xx SoCs is not compatible with the WPCM450 FIU.

Signed-off-by: Jonathan Neuschäfer <j.neuschaefer@gmx.net>
---

v2:
- Fix a few nits from the kernel test robot

v1:
- https://lore.kernel.org/lkml/20221105185911.1547847-8-j.neuschaefer@gmx.net/
---
 drivers/spi/Kconfig        |  11 +
 drivers/spi/Makefile       |   1 +
 drivers/spi/spi-wpcm-fiu.c | 444 +++++++++++++++++++++++++++++++++++++
 3 files changed, 456 insertions(+)
 create mode 100644 drivers/spi/spi-wpcm-fiu.c

diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig
index d1bb62f7368b7..ee5f9e61cc280 100644
--- a/drivers/spi/Kconfig
+++ b/drivers/spi/Kconfig
@@ -635,6 +635,17 @@ config SPI_MTK_SNFI
 	  is implemented as a SPI-MEM controller with pipelined ECC
 	  capcability.

+config SPI_WPCM_FIU
+	tristate "Nuvoton WPCM450 Flash Interface Unit"
+	depends on ARCH_NPCM || COMPILE_TEST
+	select REGMAP
+	help
+	  This enables support got the Flash Interface Unit SPI controller
+	  present in the Nuvoton WPCM450 SoC.
+
+	  This driver does not support generic SPI. The implementation only
+	  supports the spi-mem interface.
+
 config SPI_NPCM_FIU
 	tristate "Nuvoton NPCM FLASH Interface Unit"
 	depends on ARCH_NPCM || COMPILE_TEST
diff --git a/drivers/spi/Makefile b/drivers/spi/Makefile
index 4b34e855c8412..e30196d0a4cf9 100644
--- a/drivers/spi/Makefile
+++ b/drivers/spi/Makefile
@@ -83,6 +83,7 @@ obj-$(CONFIG_SPI_MTK_NOR)		+= spi-mtk-nor.o
 obj-$(CONFIG_SPI_MTK_SNFI)		+= spi-mtk-snfi.o
 obj-$(CONFIG_SPI_MXIC)			+= spi-mxic.o
 obj-$(CONFIG_SPI_MXS)			+= spi-mxs.o
+obj-$(CONFIG_SPI_WPCM_FIU)		+= spi-wpcm-fiu.o
 obj-$(CONFIG_SPI_NPCM_FIU)		+= spi-npcm-fiu.o
 obj-$(CONFIG_SPI_NPCM_PSPI)		+= spi-npcm-pspi.o
 obj-$(CONFIG_SPI_NXP_FLEXSPI)		+= spi-nxp-fspi.o
diff --git a/drivers/spi/spi-wpcm-fiu.c b/drivers/spi/spi-wpcm-fiu.c
new file mode 100644
index 0000000000000..e525fe074f883
--- /dev/null
+++ b/drivers/spi/spi-wpcm-fiu.c
@@ -0,0 +1,444 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (C) 2022 Jonathan Neuschäfer
+
+#include <linux/clk.h>
+#include <linux/mfd/syscon.h>
+#include <linux/module.h>
+#include <linux/of_address.h>
+#include <linux/of_device.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/spi/spi-mem.h>
+
+#define FIU_CFG		0x00
+#define FIU_BURST_BFG	0x01
+#define FIU_RESP_CFG	0x02
+#define FIU_CFBB_PROT	0x03
+#define FIU_FWIN1_LOW	0x04
+#define FIU_FWIN1_HIGH	0x06
+#define FIU_FWIN2_LOW	0x08
+#define FIU_FWIN2_HIGH	0x0a
+#define FIU_FWIN3_LOW	0x0c
+#define FIU_FWIN3_HIGH	0x0e
+#define FIU_PROT_LOCK	0x10
+#define FIU_PROT_CLEAR	0x11
+#define FIU_SPI_FL_CFG	0x14
+#define FIU_UMA_CODE	0x16
+#define FIU_UMA_AB0	0x17
+#define FIU_UMA_AB1	0x18
+#define FIU_UMA_AB2	0x19
+#define FIU_UMA_DB0	0x1a
+#define FIU_UMA_DB1	0x1b
+#define FIU_UMA_DB2	0x1c
+#define FIU_UMA_DB3	0x1d
+#define FIU_UMA_CTS	0x1e
+#define FIU_UMA_ECTS	0x1f
+
+#define FIU_BURST_CFG_R16	3
+
+#define FIU_UMA_CTS_D_SIZE(x)	(x)
+#define FIU_UMA_CTS_A_SIZE	BIT(3)
+#define FIU_UMA_CTS_WR		BIT(4)
+#define FIU_UMA_CTS_CS(x)	((x) << 5)
+#define FIU_UMA_CTS_EXEC_DONE	BIT(7)
+
+#define SHM_FLASH_SIZE	0x02
+#define SHM_FLASH_SIZE_STALL_HOST BIT(6)
+
+/*
+ * I observed a typical wait time of 16 iterations for a UMA transfer to
+ * finish, so this should be a safe limit.
+ */
+#define UMA_WAIT_ITERATIONS 100
+
+struct wpcm_fiu_spi {
+	struct device *dev;
+	struct clk *clk;
+	void __iomem *regs;
+	struct regmap *shm_regmap;
+};
+
+static void wpcm_fiu_set_opcode(struct wpcm_fiu_spi *fiu, u8 opcode)
+{
+	writeb(opcode, fiu->regs + FIU_UMA_CODE);
+}
+
+static void wpcm_fiu_set_addr(struct wpcm_fiu_spi *fiu, u32 addr)
+{
+	writeb((addr >>  0) & 0xff, fiu->regs + FIU_UMA_AB0);
+	writeb((addr >>  8) & 0xff, fiu->regs + FIU_UMA_AB1);
+	writeb((addr >> 16) & 0xff, fiu->regs + FIU_UMA_AB2);
+}
+
+static void wpcm_fiu_set_data(struct wpcm_fiu_spi *fiu, const u8 *data, unsigned int nbytes)
+{
+	int i;
+
+	for (i = 0; i < nbytes; i++)
+		writeb(data[i], fiu->regs + FIU_UMA_DB0 + i);
+}
+
+static void wpcm_fiu_get_data(struct wpcm_fiu_spi *fiu, u8 *data, unsigned int nbytes)
+{
+	int i;
+
+	for (i = 0; i < nbytes; i++)
+		data[i] = readb(fiu->regs + FIU_UMA_DB0 + i);
+}
+
+/*
+ * Perform a UMA (User Mode Access) operation, i.e. a software-controlled SPI transfer.
+ */
+static int wpcm_fiu_do_uma(struct wpcm_fiu_spi *fiu, unsigned int cs,
+			   bool use_addr, bool write, int data_bytes)
+{
+	int i = 0;
+	u8 cts = FIU_UMA_CTS_EXEC_DONE | FIU_UMA_CTS_CS(cs);
+
+	if (use_addr)
+		cts |= FIU_UMA_CTS_A_SIZE;
+	if (write)
+		cts |= FIU_UMA_CTS_WR;
+	cts |= FIU_UMA_CTS_D_SIZE(data_bytes);
+
+	writeb(cts, fiu->regs + FIU_UMA_CTS);
+
+	for (i = 0; i < UMA_WAIT_ITERATIONS; i++)
+		if (!(readb(fiu->regs + FIU_UMA_CTS) & FIU_UMA_CTS_EXEC_DONE))
+			return 0;
+
+	dev_info(fiu->dev, "UMA transfer has not finished in %d iterations\n", UMA_WAIT_ITERATIONS);
+	return -EIO;
+}
+
+static void wpcm_fiu_ects_assert(struct wpcm_fiu_spi *fiu, unsigned int cs)
+{
+	u8 ects = readb(fiu->regs + FIU_UMA_ECTS);
+
+	ects &= ~BIT(cs);
+	writeb(ects, fiu->regs + FIU_UMA_ECTS);
+}
+
+static void wpcm_fiu_ects_deassert(struct wpcm_fiu_spi *fiu, unsigned int cs)
+{
+	u8 ects = readb(fiu->regs + FIU_UMA_ECTS);
+
+	ects |= BIT(cs);
+	writeb(ects, fiu->regs + FIU_UMA_ECTS);
+}
+
+struct wpcm_fiu_op_shape {
+	bool (*match)(const struct spi_mem_op *op);
+	int (*exec)(struct spi_mem *mem, const struct spi_mem_op *op);
+};
+
+static bool wpcm_fiu_normal_match(const struct spi_mem_op *op)
+{
+	// Opcode 0x0b (FAST READ) is treated differently in hardware
+	if (op->cmd.opcode == 0x0b)
+		return false;
+
+	return (op->addr.nbytes == 0 || op->addr.nbytes == 3) &&
+	       op->dummy.nbytes == 0 && op->data.nbytes <= 4;
+}
+
+static int wpcm_fiu_normal_exec(struct spi_mem *mem, const struct spi_mem_op *op)
+{
+	struct wpcm_fiu_spi *fiu = spi_controller_get_devdata(mem->spi->controller);
+	int ret;
+
+	wpcm_fiu_set_opcode(fiu, op->cmd.opcode);
+	wpcm_fiu_set_addr(fiu, op->addr.val);
+	if (op->data.dir == SPI_MEM_DATA_OUT)
+		wpcm_fiu_set_data(fiu, op->data.buf.out, op->data.nbytes);
+
+	ret = wpcm_fiu_do_uma(fiu, mem->spi->chip_select, op->addr.nbytes == 3,
+			      op->data.dir == SPI_MEM_DATA_OUT, op->data.nbytes);
+
+	if (op->data.dir == SPI_MEM_DATA_IN)
+		wpcm_fiu_get_data(fiu, op->data.buf.in, op->data.nbytes);
+
+	return ret;
+}
+
+static bool wpcm_fiu_fast_read_match(const struct spi_mem_op *op)
+{
+	return op->cmd.opcode == 0x0b && op->addr.nbytes == 3 &&
+	       op->dummy.nbytes == 1 &&
+	       op->data.nbytes >= 1 && op->data.nbytes <= 4 &&
+	       op->data.dir == SPI_MEM_DATA_IN;
+}
+
+static int wpcm_fiu_fast_read_exec(struct spi_mem *mem, const struct spi_mem_op *op)
+{
+	return -EINVAL;
+}
+
+/*
+ * 4-byte addressing.
+ *
+ * Flash view:  [ C  A  A  A   A     D  D  D  D]
+ * bytes:        13 aa bb cc  dd -> 5a a5 f0 0f
+ * FIU's view:  [ C  A  A  A][ C     D  D  D  D]
+ * FIU mode:    [ read/write][      read       ]
+ */
+static bool wpcm_fiu_4ba_match(const struct spi_mem_op *op)
+{
+	return op->addr.nbytes == 4 && op->dummy.nbytes == 0 && op->data.nbytes <= 4;
+}
+
+static int wpcm_fiu_4ba_exec(struct spi_mem *mem, const struct spi_mem_op *op)
+{
+	struct wpcm_fiu_spi *fiu = spi_controller_get_devdata(mem->spi->controller);
+	int cs = mem->spi->chip_select;
+
+	wpcm_fiu_ects_assert(fiu, cs);
+
+	wpcm_fiu_set_opcode(fiu, op->cmd.opcode);
+	wpcm_fiu_set_addr(fiu, op->addr.val >> 8);
+	wpcm_fiu_do_uma(fiu, cs, true, false, 0);
+
+	wpcm_fiu_set_opcode(fiu, op->addr.val & 0xff);
+	wpcm_fiu_set_addr(fiu, 0);
+	if (op->data.dir == SPI_MEM_DATA_OUT)
+		wpcm_fiu_set_data(fiu, op->data.buf.out, op->data.nbytes);
+	wpcm_fiu_do_uma(fiu, cs, false, op->data.dir == SPI_MEM_DATA_OUT, op->data.nbytes);
+
+	wpcm_fiu_ects_deassert(fiu, cs);
+
+	if (op->data.dir == SPI_MEM_DATA_IN)
+		wpcm_fiu_get_data(fiu, op->data.buf.in, op->data.nbytes);
+
+	return 0;
+}
+
+/*
+ * RDID (Read Identification) needs special handling because Linux expects to
+ * be able to read 6 ID bytes and FIU can only read up to 4 at once.
+ *
+ * We're lucky in this case, because executing the RDID instruction twice will
+ * result in the same result.
+ *
+ * What we do is as follows (C: write command/opcode byte, D: read data byte,
+ * A: write address byte):
+ *
+ *  1. C D D D
+ *  2. C A A A D D D
+ */
+static bool wpcm_fiu_rdid_match(const struct spi_mem_op *op)
+{
+	return op->cmd.opcode == 0x9f && op->addr.nbytes == 0 &&
+	       op->dummy.nbytes == 0 && op->data.nbytes == 6 &&
+	       op->data.dir == SPI_MEM_DATA_IN;
+}
+
+static int wpcm_fiu_rdid_exec(struct spi_mem *mem, const struct spi_mem_op *op)
+{
+	struct wpcm_fiu_spi *fiu = spi_controller_get_devdata(mem->spi->controller);
+	int cs = mem->spi->chip_select;
+
+	/* First transfer */
+	wpcm_fiu_set_opcode(fiu, op->cmd.opcode);
+	wpcm_fiu_set_addr(fiu, 0);
+	wpcm_fiu_do_uma(fiu, cs, false, false, 3);
+	wpcm_fiu_get_data(fiu, op->data.buf.in, 3);
+
+	/* Second transfer */
+	wpcm_fiu_set_opcode(fiu, op->cmd.opcode);
+	wpcm_fiu_set_addr(fiu, 0);
+	wpcm_fiu_do_uma(fiu, cs, true, false, 3);
+	wpcm_fiu_get_data(fiu, op->data.buf.in + 3, 3);
+
+	return 0;
+}
+
+/*
+ * With some dummy bytes.
+ *
+ *  C A A A  X*  X D D D D
+ * [C A A A  D*][C D D D D]
+ */
+static bool wpcm_fiu_dummy_match(const struct spi_mem_op *op)
+{
+	// Opcode 0x0b (FAST READ) is treated differently in hardware
+	if (op->cmd.opcode == 0x0b)
+		return false;
+
+	return (op->addr.nbytes == 0 || op->addr.nbytes == 3) &&
+	       op->dummy.nbytes >= 1 && op->dummy.nbytes <= 5 &&
+	       op->data.nbytes <= 4;
+}
+
+static int wpcm_fiu_dummy_exec(struct spi_mem *mem, const struct spi_mem_op *op)
+{
+	struct wpcm_fiu_spi *fiu = spi_controller_get_devdata(mem->spi->controller);
+	int cs = mem->spi->chip_select;
+
+	wpcm_fiu_ects_assert(fiu, cs);
+
+	/* First transfer */
+	wpcm_fiu_set_opcode(fiu, op->cmd.opcode);
+	wpcm_fiu_set_addr(fiu, op->addr.val);
+	wpcm_fiu_do_uma(fiu, cs, op->addr.nbytes != 0, true, op->dummy.nbytes - 1);
+
+	/* Second transfer */
+	wpcm_fiu_set_opcode(fiu, 0);
+	wpcm_fiu_set_addr(fiu, 0);
+	wpcm_fiu_do_uma(fiu, cs, false, false, op->data.nbytes);
+	wpcm_fiu_get_data(fiu, op->data.buf.in, op->data.nbytes);
+
+	wpcm_fiu_ects_deassert(fiu, cs);
+
+	return 0;
+}
+
+static const struct wpcm_fiu_op_shape wpcm_fiu_op_shapes[] = {
+	{ .match = wpcm_fiu_normal_match, .exec = wpcm_fiu_normal_exec },
+	{ .match = wpcm_fiu_fast_read_match, .exec = wpcm_fiu_fast_read_exec },
+	{ .match = wpcm_fiu_4ba_match, .exec = wpcm_fiu_4ba_exec },
+	{ .match = wpcm_fiu_rdid_match, .exec = wpcm_fiu_rdid_exec },
+	{ .match = wpcm_fiu_dummy_match, .exec = wpcm_fiu_dummy_exec },
+};
+
+static const struct wpcm_fiu_op_shape *wpcm_fiu_find_op_shape(const struct spi_mem_op *op)
+{
+	size_t i;
+
+	for (i = 0; i < ARRAY_SIZE(wpcm_fiu_op_shapes); i++) {
+		const struct wpcm_fiu_op_shape *shape = &wpcm_fiu_op_shapes[i];
+
+		if (shape->match(op))
+			return shape;
+	}
+
+	return NULL;
+}
+
+static bool wpcm_fiu_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.dtr || op->addr.dtr || op->dummy.dtr || op->data.dtr)
+		return false;
+
+	if (op->cmd.buswidth > 1 || op->addr.buswidth > 1 ||
+	    op->dummy.buswidth > 1 || op->data.buswidth > 1)
+		return false;
+
+	return wpcm_fiu_find_op_shape(op) != NULL;
+}
+
+/*
+ * In order to ensure the integrity of SPI transfers performed via UMA,
+ * temporarily disable (stall) memory accesses coming from the host CPU.
+ */
+static void wpcm_fiu_stall_host(struct wpcm_fiu_spi *fiu, bool stall)
+{
+	if (fiu->shm_regmap) {
+		int res = regmap_update_bits(fiu->shm_regmap, SHM_FLASH_SIZE,
+					     SHM_FLASH_SIZE_STALL_HOST,
+					     stall ? SHM_FLASH_SIZE_STALL_HOST : 0);
+		if (res)
+			dev_warn(fiu->dev, "Failed to (un)stall host memory accesses: %d\n", res);
+	}
+}
+
+static int wpcm_fiu_exec_op(struct spi_mem *mem, const struct spi_mem_op *op)
+{
+	struct wpcm_fiu_spi *fiu = spi_controller_get_devdata(mem->spi->controller);
+	const struct wpcm_fiu_op_shape *shape = wpcm_fiu_find_op_shape(op);
+
+	wpcm_fiu_stall_host(fiu, true);
+
+	if (shape)
+		return shape->exec(mem, op);
+
+	wpcm_fiu_stall_host(fiu, false);
+
+	return -ENOTSUPP;
+}
+
+static int wpcm_fiu_adjust_op_size(struct spi_mem *mem, struct spi_mem_op *op)
+{
+	if (op->data.nbytes > 4)
+		op->data.nbytes = 4;
+
+	return 0;
+}
+
+static const struct spi_controller_mem_ops wpcm_fiu_mem_ops = {
+	.adjust_op_size = wpcm_fiu_adjust_op_size,
+	.supports_op = wpcm_fiu_supports_op,
+	.exec_op = wpcm_fiu_exec_op,
+};
+
+static void wpcm_fiu_hw_init(struct wpcm_fiu_spi *fiu)
+{
+	/* Deassert all manually asserted chip selects */
+	writeb(0x0f, fiu->regs + FIU_UMA_ECTS);
+}
+
+static int wpcm_fiu_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct spi_controller *ctrl;
+	struct wpcm_fiu_spi *fiu;
+	struct resource *res;
+
+	ctrl = devm_spi_alloc_master(dev, sizeof(*fiu));
+	if (!ctrl)
+		return -ENOMEM;
+
+	fiu = spi_controller_get_devdata(ctrl);
+	fiu->dev = dev;
+
+	res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "control");
+	fiu->regs = devm_ioremap_resource(dev, res);
+	if (IS_ERR(fiu->regs)) {
+		dev_err(dev, "Failed to map registers\n");
+		return PTR_ERR(fiu->regs);
+	}
+
+	fiu->clk = devm_clk_get_enabled(dev, NULL);
+	if (IS_ERR(fiu->clk))
+		return PTR_ERR(fiu->clk);
+
+	fiu->shm_regmap = syscon_regmap_lookup_by_phandle_optional(dev->of_node, "nuvoton,shm");
+
+	wpcm_fiu_hw_init(fiu);
+
+	ctrl->bus_num = -1;
+	ctrl->mem_ops = &wpcm_fiu_mem_ops;
+	ctrl->num_chipselect = 4;
+	ctrl->dev.of_node = dev->of_node;
+
+	/*
+	 * The FIU doesn't include a clock divider, the clock is entirely
+	 * determined by the AHB3 bus clock.
+	 */
+	ctrl->min_speed_hz = clk_get_rate(fiu->clk);
+	ctrl->max_speed_hz = clk_get_rate(fiu->clk);
+
+	return devm_spi_register_controller(dev, ctrl);
+}
+
+static const struct of_device_id wpcm_fiu_dt_ids[] = {
+	{ .compatible = "nuvoton,wpcm450-fiu", },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, wpcm_fiu_dt_ids);
+
+static struct platform_driver wpcm_fiu_driver = {
+	.driver = {
+		.name	= "wpcm450-fiu",
+		.bus	= &platform_bus_type,
+		.of_match_table = wpcm_fiu_dt_ids,
+	},
+	.probe      = wpcm_fiu_probe,
+};
+module_platform_driver(wpcm_fiu_driver);
+
+MODULE_DESCRIPTION("Nuvoton WPCM450 FIU SPI controller driver");
+MODULE_AUTHOR("Jonathan Neuschäfer <j.neuschaefer@gmx.net>");
+MODULE_LICENSE("GPL");
--
2.35.1


WARNING: multiple messages have this Message-ID (diff)
From: "Jonathan Neuschäfer" <j.neuschaefer@gmx.net>
To: linux-spi@vger.kernel.org, openbmc@lists.ozlabs.org
Cc: devicetree@vger.kernel.org,
	"Jonathan Neuschäfer" <j.neuschaefer@gmx.net>,
	"Rob Herring" <robh+dt@kernel.org>,
	linux-kernel@vger.kernel.org, "Mark Brown" <broonie@kernel.org>,
	"Krzysztof Kozlowski" <krzysztof.kozlowski+dt@linaro.org>
Subject: [PATCH v2 2/3] spi: wpcm-fiu: Add driver for Nuvoton WPCM450 Flash Interface Unit (FIU)
Date: Thu, 24 Nov 2022 20:13:59 +0100	[thread overview]
Message-ID: <20221124191400.287918-3-j.neuschaefer@gmx.net> (raw)
In-Reply-To: <20221124191400.287918-1-j.neuschaefer@gmx.net>

The Flash Interface Unit (FIU) is the SPI flash controller in the
Nuvoton WPCM450 BMC SoC. It supports four chip selects, and direct
(memory-mapped) access to 16 MiB per chip. Larger flash chips can be
accessed by software-defined SPI transfers.

The FIU in newer NPCM7xx SoCs is not compatible with the WPCM450 FIU.

Signed-off-by: Jonathan Neuschäfer <j.neuschaefer@gmx.net>
---

v2:
- Fix a few nits from the kernel test robot

v1:
- https://lore.kernel.org/lkml/20221105185911.1547847-8-j.neuschaefer@gmx.net/
---
 drivers/spi/Kconfig        |  11 +
 drivers/spi/Makefile       |   1 +
 drivers/spi/spi-wpcm-fiu.c | 444 +++++++++++++++++++++++++++++++++++++
 3 files changed, 456 insertions(+)
 create mode 100644 drivers/spi/spi-wpcm-fiu.c

diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig
index d1bb62f7368b7..ee5f9e61cc280 100644
--- a/drivers/spi/Kconfig
+++ b/drivers/spi/Kconfig
@@ -635,6 +635,17 @@ config SPI_MTK_SNFI
 	  is implemented as a SPI-MEM controller with pipelined ECC
 	  capcability.

+config SPI_WPCM_FIU
+	tristate "Nuvoton WPCM450 Flash Interface Unit"
+	depends on ARCH_NPCM || COMPILE_TEST
+	select REGMAP
+	help
+	  This enables support got the Flash Interface Unit SPI controller
+	  present in the Nuvoton WPCM450 SoC.
+
+	  This driver does not support generic SPI. The implementation only
+	  supports the spi-mem interface.
+
 config SPI_NPCM_FIU
 	tristate "Nuvoton NPCM FLASH Interface Unit"
 	depends on ARCH_NPCM || COMPILE_TEST
diff --git a/drivers/spi/Makefile b/drivers/spi/Makefile
index 4b34e855c8412..e30196d0a4cf9 100644
--- a/drivers/spi/Makefile
+++ b/drivers/spi/Makefile
@@ -83,6 +83,7 @@ obj-$(CONFIG_SPI_MTK_NOR)		+= spi-mtk-nor.o
 obj-$(CONFIG_SPI_MTK_SNFI)		+= spi-mtk-snfi.o
 obj-$(CONFIG_SPI_MXIC)			+= spi-mxic.o
 obj-$(CONFIG_SPI_MXS)			+= spi-mxs.o
+obj-$(CONFIG_SPI_WPCM_FIU)		+= spi-wpcm-fiu.o
 obj-$(CONFIG_SPI_NPCM_FIU)		+= spi-npcm-fiu.o
 obj-$(CONFIG_SPI_NPCM_PSPI)		+= spi-npcm-pspi.o
 obj-$(CONFIG_SPI_NXP_FLEXSPI)		+= spi-nxp-fspi.o
diff --git a/drivers/spi/spi-wpcm-fiu.c b/drivers/spi/spi-wpcm-fiu.c
new file mode 100644
index 0000000000000..e525fe074f883
--- /dev/null
+++ b/drivers/spi/spi-wpcm-fiu.c
@@ -0,0 +1,444 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (C) 2022 Jonathan Neuschäfer
+
+#include <linux/clk.h>
+#include <linux/mfd/syscon.h>
+#include <linux/module.h>
+#include <linux/of_address.h>
+#include <linux/of_device.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/spi/spi-mem.h>
+
+#define FIU_CFG		0x00
+#define FIU_BURST_BFG	0x01
+#define FIU_RESP_CFG	0x02
+#define FIU_CFBB_PROT	0x03
+#define FIU_FWIN1_LOW	0x04
+#define FIU_FWIN1_HIGH	0x06
+#define FIU_FWIN2_LOW	0x08
+#define FIU_FWIN2_HIGH	0x0a
+#define FIU_FWIN3_LOW	0x0c
+#define FIU_FWIN3_HIGH	0x0e
+#define FIU_PROT_LOCK	0x10
+#define FIU_PROT_CLEAR	0x11
+#define FIU_SPI_FL_CFG	0x14
+#define FIU_UMA_CODE	0x16
+#define FIU_UMA_AB0	0x17
+#define FIU_UMA_AB1	0x18
+#define FIU_UMA_AB2	0x19
+#define FIU_UMA_DB0	0x1a
+#define FIU_UMA_DB1	0x1b
+#define FIU_UMA_DB2	0x1c
+#define FIU_UMA_DB3	0x1d
+#define FIU_UMA_CTS	0x1e
+#define FIU_UMA_ECTS	0x1f
+
+#define FIU_BURST_CFG_R16	3
+
+#define FIU_UMA_CTS_D_SIZE(x)	(x)
+#define FIU_UMA_CTS_A_SIZE	BIT(3)
+#define FIU_UMA_CTS_WR		BIT(4)
+#define FIU_UMA_CTS_CS(x)	((x) << 5)
+#define FIU_UMA_CTS_EXEC_DONE	BIT(7)
+
+#define SHM_FLASH_SIZE	0x02
+#define SHM_FLASH_SIZE_STALL_HOST BIT(6)
+
+/*
+ * I observed a typical wait time of 16 iterations for a UMA transfer to
+ * finish, so this should be a safe limit.
+ */
+#define UMA_WAIT_ITERATIONS 100
+
+struct wpcm_fiu_spi {
+	struct device *dev;
+	struct clk *clk;
+	void __iomem *regs;
+	struct regmap *shm_regmap;
+};
+
+static void wpcm_fiu_set_opcode(struct wpcm_fiu_spi *fiu, u8 opcode)
+{
+	writeb(opcode, fiu->regs + FIU_UMA_CODE);
+}
+
+static void wpcm_fiu_set_addr(struct wpcm_fiu_spi *fiu, u32 addr)
+{
+	writeb((addr >>  0) & 0xff, fiu->regs + FIU_UMA_AB0);
+	writeb((addr >>  8) & 0xff, fiu->regs + FIU_UMA_AB1);
+	writeb((addr >> 16) & 0xff, fiu->regs + FIU_UMA_AB2);
+}
+
+static void wpcm_fiu_set_data(struct wpcm_fiu_spi *fiu, const u8 *data, unsigned int nbytes)
+{
+	int i;
+
+	for (i = 0; i < nbytes; i++)
+		writeb(data[i], fiu->regs + FIU_UMA_DB0 + i);
+}
+
+static void wpcm_fiu_get_data(struct wpcm_fiu_spi *fiu, u8 *data, unsigned int nbytes)
+{
+	int i;
+
+	for (i = 0; i < nbytes; i++)
+		data[i] = readb(fiu->regs + FIU_UMA_DB0 + i);
+}
+
+/*
+ * Perform a UMA (User Mode Access) operation, i.e. a software-controlled SPI transfer.
+ */
+static int wpcm_fiu_do_uma(struct wpcm_fiu_spi *fiu, unsigned int cs,
+			   bool use_addr, bool write, int data_bytes)
+{
+	int i = 0;
+	u8 cts = FIU_UMA_CTS_EXEC_DONE | FIU_UMA_CTS_CS(cs);
+
+	if (use_addr)
+		cts |= FIU_UMA_CTS_A_SIZE;
+	if (write)
+		cts |= FIU_UMA_CTS_WR;
+	cts |= FIU_UMA_CTS_D_SIZE(data_bytes);
+
+	writeb(cts, fiu->regs + FIU_UMA_CTS);
+
+	for (i = 0; i < UMA_WAIT_ITERATIONS; i++)
+		if (!(readb(fiu->regs + FIU_UMA_CTS) & FIU_UMA_CTS_EXEC_DONE))
+			return 0;
+
+	dev_info(fiu->dev, "UMA transfer has not finished in %d iterations\n", UMA_WAIT_ITERATIONS);
+	return -EIO;
+}
+
+static void wpcm_fiu_ects_assert(struct wpcm_fiu_spi *fiu, unsigned int cs)
+{
+	u8 ects = readb(fiu->regs + FIU_UMA_ECTS);
+
+	ects &= ~BIT(cs);
+	writeb(ects, fiu->regs + FIU_UMA_ECTS);
+}
+
+static void wpcm_fiu_ects_deassert(struct wpcm_fiu_spi *fiu, unsigned int cs)
+{
+	u8 ects = readb(fiu->regs + FIU_UMA_ECTS);
+
+	ects |= BIT(cs);
+	writeb(ects, fiu->regs + FIU_UMA_ECTS);
+}
+
+struct wpcm_fiu_op_shape {
+	bool (*match)(const struct spi_mem_op *op);
+	int (*exec)(struct spi_mem *mem, const struct spi_mem_op *op);
+};
+
+static bool wpcm_fiu_normal_match(const struct spi_mem_op *op)
+{
+	// Opcode 0x0b (FAST READ) is treated differently in hardware
+	if (op->cmd.opcode == 0x0b)
+		return false;
+
+	return (op->addr.nbytes == 0 || op->addr.nbytes == 3) &&
+	       op->dummy.nbytes == 0 && op->data.nbytes <= 4;
+}
+
+static int wpcm_fiu_normal_exec(struct spi_mem *mem, const struct spi_mem_op *op)
+{
+	struct wpcm_fiu_spi *fiu = spi_controller_get_devdata(mem->spi->controller);
+	int ret;
+
+	wpcm_fiu_set_opcode(fiu, op->cmd.opcode);
+	wpcm_fiu_set_addr(fiu, op->addr.val);
+	if (op->data.dir == SPI_MEM_DATA_OUT)
+		wpcm_fiu_set_data(fiu, op->data.buf.out, op->data.nbytes);
+
+	ret = wpcm_fiu_do_uma(fiu, mem->spi->chip_select, op->addr.nbytes == 3,
+			      op->data.dir == SPI_MEM_DATA_OUT, op->data.nbytes);
+
+	if (op->data.dir == SPI_MEM_DATA_IN)
+		wpcm_fiu_get_data(fiu, op->data.buf.in, op->data.nbytes);
+
+	return ret;
+}
+
+static bool wpcm_fiu_fast_read_match(const struct spi_mem_op *op)
+{
+	return op->cmd.opcode == 0x0b && op->addr.nbytes == 3 &&
+	       op->dummy.nbytes == 1 &&
+	       op->data.nbytes >= 1 && op->data.nbytes <= 4 &&
+	       op->data.dir == SPI_MEM_DATA_IN;
+}
+
+static int wpcm_fiu_fast_read_exec(struct spi_mem *mem, const struct spi_mem_op *op)
+{
+	return -EINVAL;
+}
+
+/*
+ * 4-byte addressing.
+ *
+ * Flash view:  [ C  A  A  A   A     D  D  D  D]
+ * bytes:        13 aa bb cc  dd -> 5a a5 f0 0f
+ * FIU's view:  [ C  A  A  A][ C     D  D  D  D]
+ * FIU mode:    [ read/write][      read       ]
+ */
+static bool wpcm_fiu_4ba_match(const struct spi_mem_op *op)
+{
+	return op->addr.nbytes == 4 && op->dummy.nbytes == 0 && op->data.nbytes <= 4;
+}
+
+static int wpcm_fiu_4ba_exec(struct spi_mem *mem, const struct spi_mem_op *op)
+{
+	struct wpcm_fiu_spi *fiu = spi_controller_get_devdata(mem->spi->controller);
+	int cs = mem->spi->chip_select;
+
+	wpcm_fiu_ects_assert(fiu, cs);
+
+	wpcm_fiu_set_opcode(fiu, op->cmd.opcode);
+	wpcm_fiu_set_addr(fiu, op->addr.val >> 8);
+	wpcm_fiu_do_uma(fiu, cs, true, false, 0);
+
+	wpcm_fiu_set_opcode(fiu, op->addr.val & 0xff);
+	wpcm_fiu_set_addr(fiu, 0);
+	if (op->data.dir == SPI_MEM_DATA_OUT)
+		wpcm_fiu_set_data(fiu, op->data.buf.out, op->data.nbytes);
+	wpcm_fiu_do_uma(fiu, cs, false, op->data.dir == SPI_MEM_DATA_OUT, op->data.nbytes);
+
+	wpcm_fiu_ects_deassert(fiu, cs);
+
+	if (op->data.dir == SPI_MEM_DATA_IN)
+		wpcm_fiu_get_data(fiu, op->data.buf.in, op->data.nbytes);
+
+	return 0;
+}
+
+/*
+ * RDID (Read Identification) needs special handling because Linux expects to
+ * be able to read 6 ID bytes and FIU can only read up to 4 at once.
+ *
+ * We're lucky in this case, because executing the RDID instruction twice will
+ * result in the same result.
+ *
+ * What we do is as follows (C: write command/opcode byte, D: read data byte,
+ * A: write address byte):
+ *
+ *  1. C D D D
+ *  2. C A A A D D D
+ */
+static bool wpcm_fiu_rdid_match(const struct spi_mem_op *op)
+{
+	return op->cmd.opcode == 0x9f && op->addr.nbytes == 0 &&
+	       op->dummy.nbytes == 0 && op->data.nbytes == 6 &&
+	       op->data.dir == SPI_MEM_DATA_IN;
+}
+
+static int wpcm_fiu_rdid_exec(struct spi_mem *mem, const struct spi_mem_op *op)
+{
+	struct wpcm_fiu_spi *fiu = spi_controller_get_devdata(mem->spi->controller);
+	int cs = mem->spi->chip_select;
+
+	/* First transfer */
+	wpcm_fiu_set_opcode(fiu, op->cmd.opcode);
+	wpcm_fiu_set_addr(fiu, 0);
+	wpcm_fiu_do_uma(fiu, cs, false, false, 3);
+	wpcm_fiu_get_data(fiu, op->data.buf.in, 3);
+
+	/* Second transfer */
+	wpcm_fiu_set_opcode(fiu, op->cmd.opcode);
+	wpcm_fiu_set_addr(fiu, 0);
+	wpcm_fiu_do_uma(fiu, cs, true, false, 3);
+	wpcm_fiu_get_data(fiu, op->data.buf.in + 3, 3);
+
+	return 0;
+}
+
+/*
+ * With some dummy bytes.
+ *
+ *  C A A A  X*  X D D D D
+ * [C A A A  D*][C D D D D]
+ */
+static bool wpcm_fiu_dummy_match(const struct spi_mem_op *op)
+{
+	// Opcode 0x0b (FAST READ) is treated differently in hardware
+	if (op->cmd.opcode == 0x0b)
+		return false;
+
+	return (op->addr.nbytes == 0 || op->addr.nbytes == 3) &&
+	       op->dummy.nbytes >= 1 && op->dummy.nbytes <= 5 &&
+	       op->data.nbytes <= 4;
+}
+
+static int wpcm_fiu_dummy_exec(struct spi_mem *mem, const struct spi_mem_op *op)
+{
+	struct wpcm_fiu_spi *fiu = spi_controller_get_devdata(mem->spi->controller);
+	int cs = mem->spi->chip_select;
+
+	wpcm_fiu_ects_assert(fiu, cs);
+
+	/* First transfer */
+	wpcm_fiu_set_opcode(fiu, op->cmd.opcode);
+	wpcm_fiu_set_addr(fiu, op->addr.val);
+	wpcm_fiu_do_uma(fiu, cs, op->addr.nbytes != 0, true, op->dummy.nbytes - 1);
+
+	/* Second transfer */
+	wpcm_fiu_set_opcode(fiu, 0);
+	wpcm_fiu_set_addr(fiu, 0);
+	wpcm_fiu_do_uma(fiu, cs, false, false, op->data.nbytes);
+	wpcm_fiu_get_data(fiu, op->data.buf.in, op->data.nbytes);
+
+	wpcm_fiu_ects_deassert(fiu, cs);
+
+	return 0;
+}
+
+static const struct wpcm_fiu_op_shape wpcm_fiu_op_shapes[] = {
+	{ .match = wpcm_fiu_normal_match, .exec = wpcm_fiu_normal_exec },
+	{ .match = wpcm_fiu_fast_read_match, .exec = wpcm_fiu_fast_read_exec },
+	{ .match = wpcm_fiu_4ba_match, .exec = wpcm_fiu_4ba_exec },
+	{ .match = wpcm_fiu_rdid_match, .exec = wpcm_fiu_rdid_exec },
+	{ .match = wpcm_fiu_dummy_match, .exec = wpcm_fiu_dummy_exec },
+};
+
+static const struct wpcm_fiu_op_shape *wpcm_fiu_find_op_shape(const struct spi_mem_op *op)
+{
+	size_t i;
+
+	for (i = 0; i < ARRAY_SIZE(wpcm_fiu_op_shapes); i++) {
+		const struct wpcm_fiu_op_shape *shape = &wpcm_fiu_op_shapes[i];
+
+		if (shape->match(op))
+			return shape;
+	}
+
+	return NULL;
+}
+
+static bool wpcm_fiu_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.dtr || op->addr.dtr || op->dummy.dtr || op->data.dtr)
+		return false;
+
+	if (op->cmd.buswidth > 1 || op->addr.buswidth > 1 ||
+	    op->dummy.buswidth > 1 || op->data.buswidth > 1)
+		return false;
+
+	return wpcm_fiu_find_op_shape(op) != NULL;
+}
+
+/*
+ * In order to ensure the integrity of SPI transfers performed via UMA,
+ * temporarily disable (stall) memory accesses coming from the host CPU.
+ */
+static void wpcm_fiu_stall_host(struct wpcm_fiu_spi *fiu, bool stall)
+{
+	if (fiu->shm_regmap) {
+		int res = regmap_update_bits(fiu->shm_regmap, SHM_FLASH_SIZE,
+					     SHM_FLASH_SIZE_STALL_HOST,
+					     stall ? SHM_FLASH_SIZE_STALL_HOST : 0);
+		if (res)
+			dev_warn(fiu->dev, "Failed to (un)stall host memory accesses: %d\n", res);
+	}
+}
+
+static int wpcm_fiu_exec_op(struct spi_mem *mem, const struct spi_mem_op *op)
+{
+	struct wpcm_fiu_spi *fiu = spi_controller_get_devdata(mem->spi->controller);
+	const struct wpcm_fiu_op_shape *shape = wpcm_fiu_find_op_shape(op);
+
+	wpcm_fiu_stall_host(fiu, true);
+
+	if (shape)
+		return shape->exec(mem, op);
+
+	wpcm_fiu_stall_host(fiu, false);
+
+	return -ENOTSUPP;
+}
+
+static int wpcm_fiu_adjust_op_size(struct spi_mem *mem, struct spi_mem_op *op)
+{
+	if (op->data.nbytes > 4)
+		op->data.nbytes = 4;
+
+	return 0;
+}
+
+static const struct spi_controller_mem_ops wpcm_fiu_mem_ops = {
+	.adjust_op_size = wpcm_fiu_adjust_op_size,
+	.supports_op = wpcm_fiu_supports_op,
+	.exec_op = wpcm_fiu_exec_op,
+};
+
+static void wpcm_fiu_hw_init(struct wpcm_fiu_spi *fiu)
+{
+	/* Deassert all manually asserted chip selects */
+	writeb(0x0f, fiu->regs + FIU_UMA_ECTS);
+}
+
+static int wpcm_fiu_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct spi_controller *ctrl;
+	struct wpcm_fiu_spi *fiu;
+	struct resource *res;
+
+	ctrl = devm_spi_alloc_master(dev, sizeof(*fiu));
+	if (!ctrl)
+		return -ENOMEM;
+
+	fiu = spi_controller_get_devdata(ctrl);
+	fiu->dev = dev;
+
+	res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "control");
+	fiu->regs = devm_ioremap_resource(dev, res);
+	if (IS_ERR(fiu->regs)) {
+		dev_err(dev, "Failed to map registers\n");
+		return PTR_ERR(fiu->regs);
+	}
+
+	fiu->clk = devm_clk_get_enabled(dev, NULL);
+	if (IS_ERR(fiu->clk))
+		return PTR_ERR(fiu->clk);
+
+	fiu->shm_regmap = syscon_regmap_lookup_by_phandle_optional(dev->of_node, "nuvoton,shm");
+
+	wpcm_fiu_hw_init(fiu);
+
+	ctrl->bus_num = -1;
+	ctrl->mem_ops = &wpcm_fiu_mem_ops;
+	ctrl->num_chipselect = 4;
+	ctrl->dev.of_node = dev->of_node;
+
+	/*
+	 * The FIU doesn't include a clock divider, the clock is entirely
+	 * determined by the AHB3 bus clock.
+	 */
+	ctrl->min_speed_hz = clk_get_rate(fiu->clk);
+	ctrl->max_speed_hz = clk_get_rate(fiu->clk);
+
+	return devm_spi_register_controller(dev, ctrl);
+}
+
+static const struct of_device_id wpcm_fiu_dt_ids[] = {
+	{ .compatible = "nuvoton,wpcm450-fiu", },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, wpcm_fiu_dt_ids);
+
+static struct platform_driver wpcm_fiu_driver = {
+	.driver = {
+		.name	= "wpcm450-fiu",
+		.bus	= &platform_bus_type,
+		.of_match_table = wpcm_fiu_dt_ids,
+	},
+	.probe      = wpcm_fiu_probe,
+};
+module_platform_driver(wpcm_fiu_driver);
+
+MODULE_DESCRIPTION("Nuvoton WPCM450 FIU SPI controller driver");
+MODULE_AUTHOR("Jonathan Neuschäfer <j.neuschaefer@gmx.net>");
+MODULE_LICENSE("GPL");
--
2.35.1


  parent reply	other threads:[~2022-11-24 19:14 UTC|newest]

Thread overview: 30+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-11-24 19:13 [PATCH v2 0/3] Nuvoton WPCM450 FIU SPI flash controller Jonathan Neuschäfer
2022-11-24 19:13 ` Jonathan Neuschäfer
2022-11-24 19:13 ` [PATCH v2 1/3] dt-bindings: spi: Add Nuvoton WPCM450 Flash Interface Unit (FIU) Jonathan Neuschäfer
2022-11-24 19:13   ` Jonathan Neuschäfer
2022-11-25  8:33   ` Krzysztof Kozlowski
2022-11-25 13:13   ` Mark Brown
2022-11-25 13:13     ` Mark Brown
2022-11-25 14:33     ` Jonathan Neuschäfer
2022-11-25 14:33       ` Jonathan Neuschäfer
2022-11-26 22:25   ` Rob Herring
2022-11-26 22:25     ` Rob Herring
2022-11-28 11:05     ` Conor Dooley
2022-11-28 11:05       ` Conor Dooley
2022-11-28 13:58       ` Jonathan Neuschäfer
2022-11-28 13:58         ` Jonathan Neuschäfer
2022-11-28 14:09         ` Conor Dooley
2022-11-28 14:09           ` Conor Dooley
2022-11-28 17:57           ` Mark Brown
2022-11-28 17:57             ` Mark Brown
2022-11-24 19:13 ` Jonathan Neuschäfer [this message]
2022-11-24 19:13   ` [PATCH v2 2/3] spi: wpcm-fiu: Add driver for " Jonathan Neuschäfer
2022-11-25 13:04   ` Mark Brown
2022-11-25 13:04     ` Mark Brown
2022-11-25 16:33     ` Jonathan Neuschäfer
2022-11-25 16:33       ` Jonathan Neuschäfer
2022-11-25 16:48       ` Mark Brown
2022-11-25 16:48         ` Mark Brown
2022-11-24 19:14 ` [PATCH v2 3/3] spi: wpcm-fiu: Add direct map support Jonathan Neuschäfer
2022-11-24 19:14   ` Jonathan Neuschäfer
2022-11-25 14:59 ` [PATCH v2 0/3] Nuvoton WPCM450 FIU SPI flash controller Mark Brown

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20221124191400.287918-3-j.neuschaefer@gmx.net \
    --to=j.neuschaefer@gmx.net \
    --cc=broonie@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=krzysztof.kozlowski+dt@linaro.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-spi@vger.kernel.org \
    --cc=openbmc@lists.ozlabs.org \
    --cc=robh+dt@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.