aboutsummaryrefslogtreecommitdiff
path: root/sys/dev/qcom_qup
diff options
context:
space:
mode:
authorAdrian Chadd <adrian@FreeBSD.org>2021-12-27 23:27:29 +0000
committerAdrian Chadd <adrian@FreeBSD.org>2021-12-27 23:27:29 +0000
commitd27ba3088424e53eabc0b0186ed122ec43119501 (patch)
tree037cd599e5507e4ee26443d1ae49d162d0ef0c6f /sys/dev/qcom_qup
parent989453da0589b8dc5c1948fd81f986a37ea385eb (diff)
Diffstat (limited to 'sys/dev/qcom_qup')
-rw-r--r--sys/dev/qcom_qup/qcom_qup_reg.h115
-rw-r--r--sys/dev/qcom_qup/qcom_spi.c911
-rw-r--r--sys/dev/qcom_qup/qcom_spi_debug.h49
-rw-r--r--sys/dev/qcom_qup/qcom_spi_hw.c985
-rw-r--r--sys/dev/qcom_qup/qcom_spi_reg.h76
-rw-r--r--sys/dev/qcom_qup/qcom_spi_var.h162
6 files changed, 2298 insertions, 0 deletions
diff --git a/sys/dev/qcom_qup/qcom_qup_reg.h b/sys/dev/qcom_qup/qcom_qup_reg.h
new file mode 100644
index 000000000000..18931e560959
--- /dev/null
+++ b/sys/dev/qcom_qup/qcom_qup_reg.h
@@ -0,0 +1,115 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * Copyright (c) 2021 Adrian Chadd <adrian@FreeBSD.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * $FreeBSD$
+ */
+
+#ifndef __QCOM_QUP_REG_H__
+#define __QCOM_QUP_REG_H__
+
+#define QUP_CONFIG 0x0000
+#define QUP_CONFIG_N 0x001f
+#define QUP_CONFIG_SPI_MODE (1U << 8)
+#define QUP_CONFIG_MINI_CORE_I2C_MASTER (2U << 8)
+#define QUP_CONFIG_MINI_CORE_I2C_SLAVE (3U << 8)
+
+#define QUP_CONFIG_NO_OUTPUT (1U << 6)
+#define QUP_CONFIG_NO_INPUT (1U << 7)
+#define QUP_CONFIG_APP_CLK_ON_EN (1 << 12)
+#define QUP_CONFIG_CLOCK_AUTO_GATE (1U << 13)
+
+#define QUP_STATE 0x0004
+#define QUP_STATE_VALID (1U << 2)
+#define QUP_STATE_RESET 0
+#define QUP_STATE_RUN 1
+#define QUP_STATE_PAUSE 3
+#define QUP_STATE_MASK 3
+#define QUP_STATE_CLEAR 2
+
+#define QUP_IO_M_MODES 0x0008
+#define QUP_IO_M_OUTPUT_BLOCK_SIZE_MASK 0x3
+#define QUP_IO_M_OUTPUT_BLOCK_SIZE_SHIFT 0
+
+#define QUP_IO_M_OUTPUT_FIFO_SIZE_MASK 0x7
+#define QUP_IO_M_OUTPUT_FIFO_SIZE_SHIFT 2
+
+#define QUP_IO_M_INPUT_BLOCK_SIZE_MASK 0x3
+#define QUP_IO_M_INPUT_BLOCK_SIZE_SHIFT 5
+
+#define QUP_IO_M_INPUT_FIFO_SIZE_MASK 0x7
+#define QUP_IO_M_INPUT_FIFO_SIZE_SHIFT 7
+
+#define QUP_IO_M_PACK_EN (1U << 15)
+#define QUP_IO_M_UNPACK_EN (1U << 14)
+#define QUP_IO_M_INPUT_MODE_SHIFT 12
+#define QUP_IO_M_OUTPUT_MODE_SHIFT 10
+#define QUP_IO_M_INPUT_MODE_MASK 0x3
+#define QUP_IO_M_OUTPUT_MODE_MASK 0x3
+
+#define QUP_IO_M_MODE_FIFO 0
+#define QUP_IO_M_MODE_BLOCK 1
+#define QUP_IO_M_MODE_DMOV 2
+#define QUP_IO_M_MODE_BAM 3
+
+#define QUP_SW_RESET 0x000c
+
+#define QUP_OPERATIONAL 0x0018
+#define QUP_OP_IN_BLOCK_READ_REQ (1U << 13)
+#define QUP_OP_OUT_BLOCK_WRITE_REQ (1U << 12)
+#define QUP_OP_MAX_INPUT_DONE_FLAG (1U << 11)
+#define QUP_OP_MAX_OUTPUT_DONE_FLAG (1U << 10)
+#define QUP_OP_IN_SERVICE_FLAG (1U << 9)
+#define QUP_OP_OUT_SERVICE_FLAG (1U << 8)
+#define QUP_OP_IN_FIFO_FULL (1U << 7)
+#define QUP_OP_OUT_FIFO_FULL (1U << 6)
+#define QUP_OP_IN_FIFO_NOT_EMPTY (1U << 5)
+#define QUP_OP_OUT_FIFO_NOT_EMPTY (1U << 4)
+
+#define QUP_ERROR_FLAGS 0x001c
+#define QUP_ERROR_FLAGS_EN 0x0020
+#define QUP_ERROR_OUTPUT_OVER_RUN (1U << 5)
+#define QUP_ERROR_INPUT_UNDER_RUN (1U << 4)
+#define QUP_ERROR_OUTPUT_UNDER_RUN (1U << 3)
+#define QUP_ERROR_INPUT_OVER_RUN (1U << 2)
+
+#define QUP_OPERATIONAL_MASK 0x0028
+
+#define QUP_HW_VERSION 0x0030
+#define QUP_HW_VERSION_2_1_1 0x20010001
+
+#define QUP_MX_OUTPUT_CNT 0x0100
+#define QUP_MX_OUTPUT_CNT_CURRENT 0x0104
+#define QUP_OUTPUT_FIFO 0x0110
+#define QUP_MX_WRITE_CNT 0x0150
+#define QUP_MX_WRITE_CNT_CURRENT 0x0154
+#define QUP_MX_INPUT_CNT 0x0200
+#define QUP_MX_INPUT_CNT_CURRENT 0x0204
+#define QUP_MX_READ_CNT 0x0208
+#define QUP_MX_READ_CNT_CURRENT 0x020c
+#define QUP_INPUT_FIFO 0x0218
+
+#endif /* __QCOM_QUP_REG_H__ */
+
diff --git a/sys/dev/qcom_qup/qcom_spi.c b/sys/dev/qcom_qup/qcom_spi.c
new file mode 100644
index 000000000000..e16a03db46dc
--- /dev/null
+++ b/sys/dev/qcom_qup/qcom_spi.c
@@ -0,0 +1,911 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * Copyright (c) 2021, Adrian Chadd <adrian@FreeBSD.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice unmodified, this list of conditions, and the following
+ * disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/cdefs.h>
+__FBSDID("$FreeBSD$");
+
+#include <sys/param.h>
+#include <sys/systm.h>
+
+#include <sys/bus.h>
+#include <sys/interrupt.h>
+#include <sys/malloc.h>
+#include <sys/lock.h>
+#include <sys/mutex.h>
+#include <sys/kernel.h>
+#include <sys/module.h>
+#include <sys/rman.h>
+#include <sys/gpio.h>
+
+#include <vm/vm.h>
+#include <vm/pmap.h>
+#include <vm/vm_extern.h>
+
+#include <machine/bus.h>
+#include <machine/cpu.h>
+
+#include <dev/fdt/fdt_common.h>
+#include <dev/fdt/fdt_pinctrl.h>
+
+#include <dev/gpio/gpiobusvar.h>
+#include <dev/ofw/ofw_bus.h>
+#include <dev/ofw/ofw_bus_subr.h>
+
+#include <dev/extres/clk/clk.h>
+#include <dev/extres/hwreset/hwreset.h>
+
+#include <dev/spibus/spi.h>
+#include <dev/spibus/spibusvar.h>
+#include "spibus_if.h"
+
+#include <dev/qcom_qup/qcom_spi_var.h>
+#include <dev/qcom_qup/qcom_qup_reg.h>
+#include <dev/qcom_qup/qcom_spi_reg.h>
+#include <dev/qcom_qup/qcom_spi_debug.h>
+
+static struct ofw_compat_data compat_data[] = {
+ { "qcom,spi-qup-v1.1.1", QCOM_SPI_HW_QPI_V1_1 },
+ { "qcom,spi-qup-v2.1.1", QCOM_SPI_HW_QPI_V2_1 },
+ { "qcom,spi-qup-v2.2.1", QCOM_SPI_HW_QPI_V2_2 },
+ { NULL, 0 }
+};
+
+/*
+ * Flip the CS GPIO line either active or inactive.
+ *
+ * Actually listen to the CS polarity.
+ */
+static void
+qcom_spi_set_chipsel(struct qcom_spi_softc *sc, int cs, bool active)
+{
+ bool pinactive;
+ bool invert = !! (cs & SPIBUS_CS_HIGH);
+
+ cs = cs & ~SPIBUS_CS_HIGH;
+
+ if (sc->cs_pins[cs] == NULL) {
+ device_printf(sc->sc_dev,
+ "%s: cs=%u, active=%u, invert=%u, no gpio?\n",
+ __func__, cs, active, invert);
+ return;
+ }
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_CHIPSELECT,
+ "%s: cs=%u active=%u\n", __func__, cs, active);
+
+ /*
+ * Default rule here is CS is active low.
+ */
+ if (active)
+ pinactive = false;
+ else
+ pinactive = true;
+
+ /*
+ * Invert the CS line if required.
+ */
+ if (invert)
+ pinactive = !! pinactive;
+
+ gpio_pin_set_active(sc->cs_pins[cs], pinactive);
+ gpio_pin_is_active(sc->cs_pins[cs], &pinactive);
+}
+
+static void
+qcom_spi_intr(void *arg)
+{
+ struct qcom_spi_softc *sc = arg;
+ int ret;
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_INTR, "%s: called\n", __func__);
+
+
+ QCOM_SPI_LOCK(sc);
+ ret = qcom_spi_hw_interrupt_handle(sc);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: failed to read intr status\n");
+ goto done;
+ }
+
+ /*
+ * Handle spurious interrupts outside of an actual
+ * transfer.
+ */
+ if (sc->transfer.active == false) {
+ device_printf(sc->sc_dev,
+ "ERROR: spurious interrupt\n");
+ qcom_spi_hw_ack_opmode(sc);
+ goto done;
+ }
+
+ /* Now, handle interrupts */
+ if (sc->intr.error) {
+ sc->intr.error = false;
+ device_printf(sc->sc_dev, "ERROR: intr\n");
+ }
+
+ if (sc->intr.do_rx) {
+ sc->intr.do_rx = false;
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_INTR,
+ "%s: PIO_READ\n", __func__);
+ if (sc->state.transfer_mode == QUP_IO_M_MODE_FIFO)
+ ret = qcom_spi_hw_read_pio_fifo(sc);
+ else
+ ret = qcom_spi_hw_read_pio_block(sc);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: qcom_spi_hw_read failed (%u)\n", ret);
+ goto done;
+ }
+ }
+ if (sc->intr.do_tx) {
+ sc->intr.do_tx = false;
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_INTR,
+ "%s: PIO_WRITE\n", __func__);
+ /*
+ * For FIFO operations we do not do a write here, we did
+ * it at the beginning of the transfer.
+ *
+ * For BLOCK operations yes, we call the routine.
+ */
+
+ if (sc->state.transfer_mode == QUP_IO_M_MODE_FIFO)
+ ret = qcom_spi_hw_ack_write_pio_fifo(sc);
+ else
+ ret = qcom_spi_hw_write_pio_block(sc);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: qcom_spi_hw_write failed (%u)\n", ret);
+ goto done;
+ }
+ }
+
+ /*
+ * Do this last. We may actually have completed the
+ * transfer in the PIO receive path above and it will
+ * set the done flag here.
+ */
+ if (sc->intr.done) {
+ sc->intr.done = false;
+ sc->transfer.done = true;
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_INTR,
+ "%s: transfer done\n", __func__);
+ wakeup(sc);
+ }
+
+done:
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_INTR,
+ "%s: done\n", __func__);
+ QCOM_SPI_UNLOCK(sc);
+}
+
+static int
+qcom_spi_probe(device_t dev)
+{
+
+ if (!ofw_bus_status_okay(dev))
+ return (ENXIO);
+
+ if (!ofw_bus_search_compatible(dev, compat_data)->ocd_data)
+ return (ENXIO);
+
+ device_set_desc(dev, "Qualcomm SPI Interface");
+ return (BUS_PROBE_DEFAULT);
+}
+
+/*
+ * Allocate GPIOs if provided in the SPI controller block.
+ *
+ * Some devices will use GPIO lines for chip select.
+ * It's also quite annoying because some devices will want to use
+ * the hardware provided CS gating for say, the first chipselect block,
+ * and then use GPIOs for the later ones.
+ *
+ * So here we just assume for now that SPI index 0 uses the hardware
+ * lines, and >0 use GPIO lines. Revisit this if better hardware
+ * shows up.
+ *
+ * And finally, iterating over the cs-gpios list to allocate GPIOs
+ * doesn't actually tell us what the polarity is. For that we need
+ * to actually iterate over the list of child nodes and check what
+ * their properties are (and look for "spi-cs-high".)
+ */
+static void
+qcom_spi_attach_gpios(struct qcom_spi_softc *sc)
+{
+ phandle_t node;
+ int idx, err;
+
+ /* Allocate gpio pins for configured chip selects. */
+ node = ofw_bus_get_node(sc->sc_dev);
+ for (idx = 0; idx < nitems(sc->cs_pins); idx++) {
+ err = gpio_pin_get_by_ofw_propidx(sc->sc_dev, node,
+ "cs-gpios", idx, &sc->cs_pins[idx]);
+ if (err == 0) {
+ err = gpio_pin_setflags(sc->cs_pins[idx],
+ GPIO_PIN_OUTPUT);
+ if (err != 0) {
+ device_printf(sc->sc_dev,
+ "error configuring gpio for"
+ " cs %u (%d)\n", idx, err);
+ }
+ /*
+ * We can't set this HIGH right now because
+ * we don't know if it needs to be set to
+ * high for inactive or low for inactive
+ * based on the child SPI device flags.
+ */
+#if 0
+ gpio_pin_set_active(sc->cs_pins[idx], 1);
+ gpio_pin_is_active(sc->cs_pins[idx], &tmp);
+#endif
+ } else {
+ device_printf(sc->sc_dev,
+ "cannot configure gpio for chip select %u\n", idx);
+ sc->cs_pins[idx] = NULL;
+ }
+ }
+}
+
+static void
+qcom_spi_sysctl_attach(struct qcom_spi_softc *sc)
+{
+ struct sysctl_ctx_list *ctx = device_get_sysctl_ctx(sc->sc_dev);
+ struct sysctl_oid *tree = device_get_sysctl_tree(sc->sc_dev);
+
+ SYSCTL_ADD_UINT(ctx, SYSCTL_CHILDREN(tree), OID_AUTO,
+ "debug", CTLFLAG_RW, &sc->sc_debug, 0,
+ "control debugging printfs");
+}
+
+static int
+qcom_spi_attach(device_t dev)
+{
+ struct qcom_spi_softc *sc = device_get_softc(dev);
+ int rid, ret, i, val;
+
+ sc->sc_dev = dev;
+
+ /*
+ * Hardware version is stored in the ofw_compat_data table.
+ */
+ sc->hw_version =
+ ofw_bus_search_compatible(dev, compat_data)->ocd_data;
+
+ mtx_init(&sc->sc_mtx, device_get_nameunit(dev), NULL, MTX_DEF);
+
+ rid = 0;
+ sc->sc_mem_res = bus_alloc_resource_any(dev, SYS_RES_MEMORY, &rid,
+ RF_ACTIVE);
+ if (!sc->sc_mem_res) {
+ device_printf(dev, "ERROR: Could not map memory\n");
+ ret = ENXIO;
+ goto error;
+ }
+
+ rid = 0;
+ sc->sc_irq_res = bus_alloc_resource_any(dev, SYS_RES_IRQ, &rid,
+ RF_ACTIVE | RF_SHAREABLE);
+ if (!sc->sc_irq_res) {
+ device_printf(dev, "ERROR: Could not map interrupt\n");
+ ret = ENXIO;
+ goto error;
+ }
+
+ ret = bus_setup_intr(dev, sc->sc_irq_res,
+ INTR_TYPE_MISC | INTR_MPSAFE,
+ NULL, qcom_spi_intr, sc, &sc->sc_irq_h);
+ if (ret != 0) {
+ device_printf(dev, "ERROR: could not configure interrupt "
+ "(%d)\n",
+ ret);
+ goto error;
+ }
+
+ qcom_spi_attach_gpios(sc);
+
+ ret = clk_get_by_ofw_name(dev, 0, "core", &sc->clk_core);
+ if (ret != 0) {
+ device_printf(dev, "ERROR: could not get %s clock (%d)\n",
+ "core", ret);
+ goto error;
+ }
+ ret = clk_get_by_ofw_name(dev, 0, "iface", &sc->clk_iface);
+ if (ret != 0) {
+ device_printf(dev, "ERROR: could not get %s clock (%d)\n",
+ "iface", ret);
+ goto error;
+ }
+
+ /* Bring up initial clocks if they're off */
+ ret = clk_enable(sc->clk_core);
+ if (ret != 0) {
+ device_printf(dev, "ERROR: couldn't enable core clock (%u)\n",
+ ret);
+ goto error;
+ }
+ ret = clk_enable(sc->clk_iface);
+ if (ret != 0) {
+ device_printf(dev, "ERROR: couldn't enable iface clock (%u)\n",
+ ret);
+ goto error;
+ }
+
+ /*
+ * Read optional spi-max-frequency
+ */
+ if (OF_getencprop(ofw_bus_get_node(dev), "spi-max-frequency",
+ &val, sizeof(val)) > 0)
+ sc->config.max_frequency = val;
+ else
+ sc->config.max_frequency = SPI_MAX_RATE;
+
+ /*
+ * Read optional cs-select
+ */
+ if (OF_getencprop(ofw_bus_get_node(dev), "cs-select",
+ &val, sizeof(val)) > 0)
+ sc->config.cs_select = val;
+ else
+ sc->config.cs_select = 0;
+
+ /*
+ * Read optional num-cs
+ */
+ if (OF_getencprop(ofw_bus_get_node(dev), "num-cs",
+ &val, sizeof(val)) > 0)
+ sc->config.num_cs = val;
+ else
+ sc->config.num_cs = SPI_NUM_CHIPSELECTS;
+
+ ret = fdt_pinctrl_configure_by_name(dev, "default");
+ if (ret != 0) {
+ device_printf(dev,
+ "ERROR: could not configure default pinmux\n");
+ goto error;
+ }
+
+ ret = qcom_spi_hw_read_controller_transfer_sizes(sc);
+ if (ret != 0) {
+ device_printf(dev, "ERROR: Could not read transfer config\n");
+ goto error;
+ }
+
+
+ device_printf(dev, "BLOCK: input=%u bytes, output=%u bytes\n",
+ sc->config.input_block_size,
+ sc->config.output_block_size);
+ device_printf(dev, "FIFO: input=%u bytes, output=%u bytes\n",
+ sc->config.input_fifo_size,
+ sc->config.output_fifo_size);
+
+ /* QUP config */
+ QCOM_SPI_LOCK(sc);
+ ret = qcom_spi_hw_qup_init_locked(sc);
+ if (ret != 0) {
+ device_printf(dev, "ERROR: QUP init failed (%d)\n", ret);
+ QCOM_SPI_UNLOCK(sc);
+ goto error;
+ }
+
+ /* Initial SPI config */
+ ret = qcom_spi_hw_spi_init_locked(sc);
+ if (ret != 0) {
+ device_printf(dev, "ERROR: SPI init failed (%d)\n", ret);
+ QCOM_SPI_UNLOCK(sc);
+ goto error;
+ }
+ QCOM_SPI_UNLOCK(sc);
+
+ sc->spibus = device_add_child(dev, "spibus", -1);
+
+ /* We're done, so shut down the interface clock for now */
+ device_printf(dev, "DONE: shutting down interface clock for now\n");
+ clk_disable(sc->clk_iface);
+
+ /* Register for debug sysctl */
+ qcom_spi_sysctl_attach(sc);
+
+ return (bus_generic_attach(dev));
+error:
+ if (sc->sc_irq_h)
+ bus_teardown_intr(dev, sc->sc_irq_res, sc->sc_irq_h);
+ if (sc->sc_mem_res)
+ bus_release_resource(dev, SYS_RES_MEMORY, 0, sc->sc_mem_res);
+ if (sc->sc_irq_res)
+ bus_release_resource(dev, SYS_RES_IRQ, 0, sc->sc_irq_res);
+ if (sc->clk_core) {
+ clk_disable(sc->clk_core);
+ clk_release(sc->clk_core);
+ }
+ if (sc->clk_iface) {
+ clk_disable(sc->clk_iface);
+ clk_release(sc->clk_iface);
+ }
+ for (i = 0; i < CS_MAX; i++) {
+ if (sc->cs_pins[i] != NULL)
+ gpio_pin_release(sc->cs_pins[i]);
+ }
+ mtx_destroy(&sc->sc_mtx);
+ return (ret);
+}
+
+/*
+ * Do a PIO transfer.
+ *
+ * Note that right now the TX/RX lens need to match, I'm not doing
+ * dummy reads / dummy writes as required if they're not the same
+ * size. The QUP hardware supports doing multi-phase transactions
+ * where the FIFO isn't engaged for transmit or receive, but it's
+ * not yet being done here.
+ */
+static int
+qcom_spi_transfer_pio_block(struct qcom_spi_softc *sc, int mode,
+ char *tx_buf, int tx_len, char *rx_buf, int rx_len)
+{
+ int ret = 0;
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_TRANSFER, "%s: start\n",
+ __func__);
+
+ if (rx_len != tx_len) {
+ device_printf(sc->sc_dev,
+ "ERROR: tx/rx len doesn't match (%d/%d)\n",
+ tx_len, rx_len);
+ return (ENXIO);
+ }
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ /*
+ * Make initial choices for transfer configuration.
+ */
+ ret = qcom_spi_hw_setup_transfer_selection(sc, tx_len);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: failed to setup transfer selection (%d)\n",
+ ret);
+ return (ret);
+ }
+
+ /* Now set suitable buffer/lengths */
+ sc->transfer.tx_buf = tx_buf;
+ sc->transfer.tx_len = tx_len;
+ sc->transfer.rx_buf = rx_buf;
+ sc->transfer.rx_len = rx_len;
+ sc->transfer.done = false;
+ sc->transfer.active = false;
+
+ /*
+ * Loop until the full transfer set is done.
+ *
+ * qcom_spi_hw_setup_current_transfer() will take care of
+ * setting a maximum transfer size for the hardware and choose
+ * a suitable operating mode.
+ */
+ while (sc->transfer.tx_offset < sc->transfer.tx_len) {
+ /*
+ * Set transfer to false early; this covers
+ * it also finishing a sub-transfer and we're
+ * about the put the block into RESET state before
+ * starting a new transfer.
+ */
+ sc->transfer.active = false;
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_TRANSFER,
+ "%s: tx=%d of %d bytes, rx=%d of %d bytes\n",
+ __func__,
+ sc->transfer.tx_offset, sc->transfer.tx_len,
+ sc->transfer.rx_offset, sc->transfer.rx_len);
+
+ /*
+ * Set state to RESET before doing anything.
+ *
+ * Otherwise the second sub-transfer that we queue up
+ * will generate interrupts immediately when we start
+ * configuring it here and it'll start underflowing.
+ */
+ ret = qcom_spi_hw_qup_set_state_locked(sc, QUP_STATE_RESET);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: can't transition to RESET (%u)\n", ret);
+ goto done;
+ }
+ /* blank interrupt state; we'll do a RESET below */
+ bzero(&sc->intr, sizeof(sc->intr));
+ sc->transfer.done = false;
+
+ /*
+ * Configure what the transfer configuration for this
+ * sub-transfer will be.
+ */
+ ret = qcom_spi_hw_setup_current_transfer(sc);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: failed to setup sub transfer (%d)\n",
+ ret);
+ goto done;
+ }
+
+ /*
+ * For now since we're configuring up PIO, we only setup
+ * the PIO transfer size.
+ */
+ ret = qcom_spi_hw_setup_pio_transfer_cnt(sc);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: qcom_spi_hw_setup_pio_transfer_cnt failed"
+ " (%u)\n", ret);
+ goto done;
+ }
+
+#if 0
+ /*
+ * This is what we'd do to setup the block transfer sizes.
+ */
+ ret = qcom_spi_hw_setup_block_transfer_cnt(sc);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: qcom_spi_hw_setup_block_transfer_cnt failed"
+ " (%u)\n", ret);
+ goto done;
+ }
+#endif
+
+ ret = qcom_spi_hw_setup_io_modes(sc);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: qcom_spi_hw_setup_io_modes failed"
+ " (%u)\n", ret);
+ goto done;
+ }
+
+ ret = qcom_spi_hw_setup_spi_io_clock_polarity(sc,
+ !! (mode & SPIBUS_MODE_CPOL));
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: qcom_spi_hw_setup_spi_io_clock_polarity"
+ " failed (%u)\n", ret);
+ goto done;
+ }
+
+ ret = qcom_spi_hw_setup_spi_config(sc, sc->state.frequency,
+ !! (mode & SPIBUS_MODE_CPHA));
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: qcom_spi_hw_setup_spi_config failed"
+ " (%u)\n", ret);
+ goto done;
+ }
+
+ ret = qcom_spi_hw_setup_qup_config(sc, !! (tx_len > 0),
+ !! (rx_len > 0));
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: qcom_spi_hw_setup_qup_config failed"
+ " (%u)\n", ret);
+ goto done;
+ }
+
+ ret = qcom_spi_hw_setup_operational_mask(sc);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: qcom_spi_hw_setup_operational_mask failed"
+ " (%u)\n", ret);
+ goto done;
+ }
+
+ /*
+ * Setup is done; reset the controller and start the PIO
+ * write.
+ */
+
+ /*
+ * Set state to RUN; we may start getting interrupts that
+ * are valid and we need to handle.
+ */
+ sc->transfer.active = true;
+ ret = qcom_spi_hw_qup_set_state_locked(sc, QUP_STATE_RUN);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: can't transition to RUN (%u)\n", ret);
+ goto done;
+ }
+
+ /*
+ * Set state to PAUSE
+ */
+ ret = qcom_spi_hw_qup_set_state_locked(sc, QUP_STATE_PAUSE);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: can't transition to PAUSE (%u)\n", ret);
+ goto done;
+ }
+
+ /*
+ * If FIFO mode, write data now. Else, we'll get an
+ * interrupt when it's time to populate more data
+ * in BLOCK mode.
+ */
+ if (sc->state.transfer_mode == QUP_IO_M_MODE_FIFO)
+ ret = qcom_spi_hw_write_pio_fifo(sc);
+ else
+ ret = qcom_spi_hw_write_pio_block(sc);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: qcom_spi_hw_write failed (%u)\n", ret);
+ goto done;
+ }
+
+ /*
+ * Set state to RUN
+ */
+ ret = qcom_spi_hw_qup_set_state_locked(sc, QUP_STATE_RUN);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: can't transition to RUN (%u)\n", ret);
+ goto done;
+ }
+
+ /*
+ * Wait for an interrupt notification (which will
+ * continue to drive the state machine for this
+ * sub-transfer) or timeout.
+ */
+ ret = 0;
+ while (ret == 0 && sc->transfer.done == false) {
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_TRANSFER,
+ "%s: waiting\n", __func__);
+ ret = msleep(sc, &sc->sc_mtx, 0, "qcom_spi", 0);
+ }
+ }
+done:
+ /*
+ * Complete; put controller into reset.
+ *
+ * Don't worry about return value here; if we errored out above then
+ * we want to communicate that value to the caller.
+ */
+ (void) qcom_spi_hw_qup_set_state_locked(sc, QUP_STATE_RESET);
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_TRANSFER,
+ "%s: completed\n", __func__);
+
+ /*
+ * Blank the transfer state so we don't use an old transfer
+ * state in a subsequent interrupt.
+ */
+ (void) qcom_spi_hw_complete_transfer(sc);
+ sc->transfer.active = false;
+
+ return (ret);
+}
+
+static int
+qcom_spi_transfer(device_t dev, device_t child, struct spi_command *cmd)
+{
+ struct qcom_spi_softc *sc = device_get_softc(dev);
+ uint32_t cs_val, mode_val, clock_val;
+ uint32_t ret = 0;
+
+ spibus_get_cs(child, &cs_val);
+ spibus_get_clock(child, &clock_val);
+ spibus_get_mode(child, &mode_val);
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_TRANSFER,
+ "%s: called; child cs=0x%08x, clock=%u, mode=0x%08x, "
+ "cmd=%u/%u bytes; data=%u/%u bytes\n",
+ __func__,
+ cs_val,
+ clock_val,
+ mode_val,
+ cmd->tx_cmd_sz, cmd->rx_cmd_sz,
+ cmd->tx_data_sz, cmd->rx_data_sz);
+
+ QCOM_SPI_LOCK(sc);
+
+ /*
+ * wait until the controller isn't busy
+ */
+ while (sc->sc_busy == true)
+ mtx_sleep(sc, &sc->sc_mtx, 0, "qcom_spi_wait", 0);
+
+ /*
+ * it's ours now!
+ */
+ sc->sc_busy = true;
+
+ sc->state.cs_high = !! (cs_val & SPIBUS_CS_HIGH);
+ sc->state.frequency = clock_val;
+
+ /*
+ * We can't set the clock frequency and enable it
+ * with the driver lock held, as the SPI lock is non-sleepable
+ * and the clock framework is sleepable.
+ *
+ * No other transaction is going on right now, so we can
+ * unlock here and do the clock related work.
+ */
+ QCOM_SPI_UNLOCK(sc);
+
+ /*
+ * Set the clock frequency
+ */
+ ret = clk_set_freq(sc->clk_iface, sc->state.frequency, 0);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: failed to set frequency to %u\n",
+ sc->state.frequency);
+ goto done2;
+ }
+ clk_enable(sc->clk_iface);
+
+ QCOM_SPI_LOCK(sc);
+
+ /*
+ * Set state to RESET
+ */
+ ret = qcom_spi_hw_qup_set_state_locked(sc, QUP_STATE_RESET);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: can't transition to RESET (%u)\n", ret);
+ goto done;
+ }
+
+ /* Assert hardware CS if set, else GPIO */
+ if (sc->cs_pins[cs_val & ~SPIBUS_CS_HIGH] == NULL)
+ qcom_spi_hw_spi_cs_force(sc, cs_val & SPIBUS_CS_HIGH, true);
+ else
+ qcom_spi_set_chipsel(sc, cs_val & ~SPIBUS_CS_HIGH, true);
+
+ /*
+ * cmd buffer transfer
+ */
+ ret = qcom_spi_transfer_pio_block(sc, mode_val, cmd->tx_cmd,
+ cmd->tx_cmd_sz, cmd->rx_cmd, cmd->rx_cmd_sz);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: failed to transfer cmd payload (%u)\n", ret);
+ goto done;
+ }
+
+ /*
+ * data buffer transfer
+ */
+ if (cmd->tx_data_sz > 0) {
+ ret = qcom_spi_transfer_pio_block(sc, mode_val, cmd->tx_data,
+ cmd->tx_data_sz, cmd->rx_data, cmd->rx_data_sz);
+ if (ret != 0) {
+ device_printf(sc->sc_dev,
+ "ERROR: failed to transfer data payload (%u)\n",
+ ret);
+ goto done;
+ }
+ }
+
+done:
+ /* De-assert GPIO/CS */
+ if (sc->cs_pins[cs_val & ~SPIBUS_CS_HIGH] == NULL)
+ qcom_spi_hw_spi_cs_force(sc, cs_val & ~SPIBUS_CS_HIGH, false);
+ else
+ qcom_spi_set_chipsel(sc, cs_val & ~SPIBUS_CS_HIGH, false);
+
+ /*
+ * Similarly to when we enabled the clock, we can't hold it here
+ * across a clk API as that's a sleep lock and we're non-sleepable.
+ * So instead we unlock/relock here, but we still hold the busy flag.
+ */
+
+ QCOM_SPI_UNLOCK(sc);
+ clk_disable(sc->clk_iface);
+ QCOM_SPI_LOCK(sc);
+done2:
+ /*
+ * We're done; so mark the bus as not busy and wakeup
+ * the next caller.
+ */
+ sc->sc_busy = false;
+ wakeup_one(sc);
+ QCOM_SPI_UNLOCK(sc);
+ return (ret);
+}
+
+static int
+qcom_spi_detach(device_t dev)
+{
+ struct qcom_spi_softc *sc = device_get_softc(dev);
+ int i;
+
+ bus_generic_detach(sc->sc_dev);
+ if (sc->spibus != NULL)
+ device_delete_child(dev, sc->spibus);
+
+ if (sc->sc_irq_h)
+ bus_teardown_intr(dev, sc->sc_irq_res, sc->sc_irq_h);
+
+ if (sc->clk_iface) {
+ clk_disable(sc->clk_iface);
+ clk_release(sc->clk_iface);
+ }
+ if (sc->clk_core) {
+ clk_disable(sc->clk_core);
+ clk_release(sc->clk_core);
+ }
+
+ for (i = 0; i < CS_MAX; i++) {
+ if (sc->cs_pins[i] != NULL)
+ gpio_pin_release(sc->cs_pins[i]);
+ }
+
+ if (sc->sc_mem_res)
+ bus_release_resource(dev, SYS_RES_MEMORY, 0, sc->sc_mem_res);
+ if (sc->sc_irq_res)
+ bus_release_resource(dev, SYS_RES_IRQ, 0, sc->sc_irq_res);
+
+ mtx_destroy(&sc->sc_mtx);
+
+ return (0);
+}
+
+static phandle_t
+qcom_spi_get_node(device_t bus, device_t dev)
+{
+
+ return ofw_bus_get_node(bus);
+}
+
+
+static device_method_t qcom_spi_methods[] = {
+ /* Device interface */
+ DEVMETHOD(device_probe, qcom_spi_probe),
+ DEVMETHOD(device_attach, qcom_spi_attach),
+ DEVMETHOD(device_detach, qcom_spi_detach),
+ /* TODO: suspend */
+ /* TODO: resume */
+
+ DEVMETHOD(spibus_transfer, qcom_spi_transfer),
+
+ /* ofw_bus_if */
+ DEVMETHOD(ofw_bus_get_node, qcom_spi_get_node),
+
+ DEVMETHOD_END
+};
+
+static driver_t qcom_spi_driver = {
+ "qcom_spi",
+ qcom_spi_methods,
+ sizeof(struct qcom_spi_softc),
+};
+
+static devclass_t qcom_spi_devclass;
+
+DRIVER_MODULE(qcom_spi, simplebus, qcom_spi_driver, qcom_spi_devclass, 0, 0);
+DRIVER_MODULE(ofw_spibus, qcom_spi, ofw_spibus_driver, ofw_spibus_devclass,
+ 0, 0);
+MODULE_DEPEND(qcom_spi, ofw_spibus, 1, 1, 1);
+SIMPLEBUS_PNP_INFO(compat_data);
diff --git a/sys/dev/qcom_qup/qcom_spi_debug.h b/sys/dev/qcom_qup/qcom_spi_debug.h
new file mode 100644
index 000000000000..9225bb4d42ee
--- /dev/null
+++ b/sys/dev/qcom_qup/qcom_spi_debug.h
@@ -0,0 +1,49 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * Copyright (c) 2021 Adrian Chadd <adrian@FreeBSD.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * $FreeBSD$
+ */
+
+#ifndef __QCOM_SPI_DEBUG_H__
+#define __QCOM_SPI_DEBUG_H__
+
+#define QCOM_SPI_DEBUG_TRANSFER 0x00000001
+#define QCOM_SPI_DEBUG_HW_TRANSFER_SETUP 0x00000002
+#define QCOM_SPI_DEBUG_HW_TX_FIFO 0x00000004
+#define QCOM_SPI_DEBUG_HW_RX_FIFO 0x00000008
+#define QCOM_SPI_DEBUG_INTR 0x00000010
+#define QCOM_SPI_DEBUG_CHIPSELECT 0x00000020
+#define QCOM_SPI_DEBUG_HW_CHIPSELECT 0x00000080
+#define QCOM_SPI_DEBUG_HW_STATE_CHANGE 0x00000100
+#define QCOM_SPI_DEBUG_HW_INTR 0x00000200
+
+#define QCOM_SPI_DPRINTF(sc, flags, ...) \
+ do { \
+ if ((sc)->sc_debug & flags) \
+ device_printf((sc)->sc_dev, __VA_ARGS__); \
+ } while (0)
+
+#endif /* __QCOM_SPI_DEBUG_H__ */
diff --git a/sys/dev/qcom_qup/qcom_spi_hw.c b/sys/dev/qcom_qup/qcom_spi_hw.c
new file mode 100644
index 000000000000..1d08b10e5cd4
--- /dev/null
+++ b/sys/dev/qcom_qup/qcom_spi_hw.c
@@ -0,0 +1,985 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * Copyright (c) 2021, Adrian Chadd <adrian@FreeBSD.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice unmodified, this list of conditions, and the following
+ * disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/cdefs.h>
+__FBSDID("$FreeBSD$");
+
+#include <sys/param.h>
+#include <sys/systm.h>
+
+#include <sys/bus.h>
+#include <sys/interrupt.h>
+#include <sys/malloc.h>
+#include <sys/lock.h>
+#include <sys/mutex.h>
+#include <sys/kernel.h>
+#include <sys/module.h>
+#include <sys/rman.h>
+
+#include <vm/vm.h>
+#include <vm/pmap.h>
+#include <vm/vm_extern.h>
+
+#include <machine/bus.h>
+#include <machine/cpu.h>
+
+#include <dev/gpio/gpiobusvar.h>
+#include <dev/ofw/ofw_bus.h>
+#include <dev/ofw/ofw_bus_subr.h>
+
+#include <dev/extres/clk/clk.h>
+#include <dev/extres/hwreset/hwreset.h>
+
+#include <dev/spibus/spi.h>
+#include <dev/spibus/spibusvar.h>
+#include "spibus_if.h"
+
+#include <dev/qcom_qup/qcom_spi_var.h>
+#include <dev/qcom_qup/qcom_spi_reg.h>
+#include <dev/qcom_qup/qcom_qup_reg.h>
+#include <dev/qcom_qup/qcom_spi_debug.h>
+
+int
+qcom_spi_hw_read_controller_transfer_sizes(struct qcom_spi_softc *sc)
+{
+ uint32_t reg, val;
+
+ reg = QCOM_SPI_READ_4(sc, QUP_IO_M_MODES);
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_HW_TRANSFER_SETUP,
+ "%s: QUP_IO_M_MODES=0x%08x\n", __func__, reg);
+
+ /* Input block size */
+ val = (reg >> QUP_IO_M_INPUT_BLOCK_SIZE_SHIFT)
+ & QUP_IO_M_INPUT_BLOCK_SIZE_MASK;
+ if (val == 0)
+ sc->config.input_block_size = 4;
+ else
+ sc->config.input_block_size = val * 16;
+
+ /* Output block size */
+ val = (reg >> QUP_IO_M_OUTPUT_BLOCK_SIZE_SHIFT)
+ & QUP_IO_M_OUTPUT_BLOCK_SIZE_MASK;
+ if (val == 0)
+ sc->config.output_block_size = 4;
+ else
+ sc->config.output_block_size = val * 16;
+
+ /* Input FIFO size */
+ val = (reg >> QUP_IO_M_INPUT_FIFO_SIZE_SHIFT)
+ & QUP_IO_M_INPUT_FIFO_SIZE_MASK;
+ sc->config.input_fifo_size =
+ sc->config.input_block_size * (2 << val);
+
+ /* Output FIFO size */
+ val = (reg >> QUP_IO_M_OUTPUT_FIFO_SIZE_SHIFT)
+ & QUP_IO_M_OUTPUT_FIFO_SIZE_MASK;
+ sc->config.output_fifo_size =
+ sc->config.output_block_size * (2 << val);
+
+ return (0);
+}
+
+static bool
+qcom_spi_hw_qup_is_state_valid_locked(struct qcom_spi_softc *sc)
+{
+ uint32_t reg;
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ reg = QCOM_SPI_READ_4(sc, QUP_STATE);
+ QCOM_SPI_BARRIER_READ(sc);
+
+ return !! (reg & QUP_STATE_VALID);
+}
+
+static int
+qcom_spi_hw_qup_wait_state_valid_locked(struct qcom_spi_softc *sc)
+{
+ int i;
+
+ for (i = 0; i < 10; i++) {
+ if (qcom_spi_hw_qup_is_state_valid_locked(sc))
+ break;
+ }
+ if (i >= 10) {
+ device_printf(sc->sc_dev,
+ "ERROR: timeout waiting for valid state\n");
+ return (ENXIO);
+ }
+ return (0);
+}
+
+static bool
+qcom_spi_hw_is_opmode_dma_locked(struct qcom_spi_softc *sc)
+{
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ if (sc->state.transfer_mode == QUP_IO_M_MODE_DMOV)
+ return (true);
+ if (sc->state.transfer_mode == QUP_IO_M_MODE_BAM)
+ return (true);
+ return (false);
+}
+
+int
+qcom_spi_hw_qup_set_state_locked(struct qcom_spi_softc *sc, uint32_t state)
+{
+ uint32_t cur_state;
+ int ret;
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ /* Wait until the state becomes valid */
+ ret = qcom_spi_hw_qup_wait_state_valid_locked(sc);
+ if (ret != 0) {
+ return (ret);
+ }
+
+ cur_state = QCOM_SPI_READ_4(sc, QUP_STATE);
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_HW_STATE_CHANGE,
+ "%s: target state=%d, cur_state=0x%08x\n",
+ __func__, state, cur_state);
+
+ /*
+ * According to the QUP specification, when going
+ * from PAUSE to RESET, two writes are required.
+ */
+ if ((state == QUP_STATE_RESET)
+ && ((cur_state & QUP_STATE_MASK) == QUP_STATE_PAUSE)) {
+ QCOM_SPI_WRITE_4(sc, QUP_STATE, QUP_STATE_CLEAR);
+ QCOM_SPI_BARRIER_WRITE(sc);
+ QCOM_SPI_WRITE_4(sc, QUP_STATE, QUP_STATE_CLEAR);
+ QCOM_SPI_BARRIER_WRITE(sc);
+ } else {
+ cur_state &= ~QUP_STATE_MASK;
+ cur_state |= state;
+ QCOM_SPI_WRITE_4(sc, QUP_STATE, cur_state);
+ QCOM_SPI_BARRIER_WRITE(sc);
+ }
+
+ /* Wait until the state becomes valid */
+ ret = qcom_spi_hw_qup_wait_state_valid_locked(sc);
+ if (ret != 0) {
+ return (ret);
+ }
+
+ cur_state = QCOM_SPI_READ_4(sc, QUP_STATE);
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_HW_STATE_CHANGE,
+ "%s: FINISH: target state=%d, cur_state=0x%08x\n",
+ __func__, state, cur_state);
+
+ return (0);
+}
+
+/*
+ * Do initial QUP setup.
+ *
+ * This is initially for the SPI driver; it would be interesting to see how
+ * much of this is the same with the I2C/HSUART paths.
+ */
+int
+qcom_spi_hw_qup_init_locked(struct qcom_spi_softc *sc)
+{
+ int ret;
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ /* Full hardware reset */
+ (void) qcom_spi_hw_do_full_reset(sc);
+
+ ret = qcom_spi_hw_qup_set_state_locked(sc, QUP_STATE_RESET);
+ if (ret != 0) {
+ device_printf(sc->sc_dev, "ERROR: %s: couldn't reset\n",
+ __func__);
+ goto error;
+ }
+
+ QCOM_SPI_WRITE_4(sc, QUP_OPERATIONAL, 0);
+ QCOM_SPI_WRITE_4(sc, QUP_IO_M_MODES, 0);
+ /* Note: no QUP_OPERATIONAL_MASK in QUP v1 */
+ if (! QCOM_SPI_QUP_VERSION_V1(sc))
+ QCOM_SPI_WRITE_4(sc, QUP_OPERATIONAL_MASK, 0);
+
+ /* Explicitly disable input overrun in QUP v1 */
+ if (QCOM_SPI_QUP_VERSION_V1(sc))
+ QCOM_SPI_WRITE_4(sc, QUP_ERROR_FLAGS_EN,
+ QUP_ERROR_OUTPUT_OVER_RUN
+ | QUP_ERROR_INPUT_UNDER_RUN
+ | QUP_ERROR_OUTPUT_UNDER_RUN);
+ QCOM_SPI_BARRIER_WRITE(sc);
+
+ return (0);
+error:
+ return (ret);
+}
+
+/*
+ * Do initial SPI setup.
+ */
+int
+qcom_spi_hw_spi_init_locked(struct qcom_spi_softc *sc)
+{
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ /* Initial SPI error flags */
+ QCOM_SPI_WRITE_4(sc, SPI_ERROR_FLAGS_EN,
+ QUP_ERROR_INPUT_UNDER_RUN
+ | QUP_ERROR_OUTPUT_UNDER_RUN);
+ QCOM_SPI_BARRIER_WRITE(sc);
+
+ /* Initial SPI config */
+ QCOM_SPI_WRITE_4(sc, SPI_CONFIG, 0);
+ QCOM_SPI_BARRIER_WRITE(sc);
+
+ /* Initial CS/tri-state io control config */
+ QCOM_SPI_WRITE_4(sc, SPI_IO_CONTROL,
+ SPI_IO_C_NO_TRI_STATE
+ | SPI_IO_C_CS_SELECT(sc->config.cs_select));
+ QCOM_SPI_BARRIER_WRITE(sc);
+
+ return (0);
+}
+
+/*
+ * Force the currently selected device CS line to be active
+ * or inactive.
+ *
+ * This forces it to be active or inactive rather than letting
+ * the SPI transfer machine do its thing. If you want to be able
+ * break up a big transaction into a handful of smaller ones,
+ * without toggling /CS_n for that device, then you need it forced.
+ * (If you toggle the /CS_n to the device to inactive then active,
+ * NOR/NAND devices tend to stop a block transfer.)
+ */
+int
+qcom_spi_hw_spi_cs_force(struct qcom_spi_softc *sc, int cs, bool enable)
+{
+ uint32_t reg;
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_HW_CHIPSELECT,
+ "%s: called, enable=%u\n",
+ __func__, enable);
+
+ reg = QCOM_SPI_READ_4(sc, SPI_IO_CONTROL);
+ if (enable)
+ reg |= SPI_IO_C_FORCE_CS;
+ else
+ reg &= ~SPI_IO_C_FORCE_CS;
+ reg &= ~SPI_IO_C_CS_SELECT_MASK;
+ reg |= SPI_IO_C_CS_SELECT(cs);
+ QCOM_SPI_WRITE_4(sc, SPI_IO_CONTROL, reg);
+ QCOM_SPI_BARRIER_WRITE(sc);
+
+ return (0);
+}
+
+/*
+ * ACK/store current interrupt flag state.
+ */
+int
+qcom_spi_hw_interrupt_handle(struct qcom_spi_softc *sc)
+{
+ uint32_t qup_error, spi_error, op_flags;
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ /* Get QUP/SPI state */
+ qup_error = QCOM_SPI_READ_4(sc, QUP_ERROR_FLAGS);
+ spi_error = QCOM_SPI_READ_4(sc, SPI_ERROR_FLAGS);
+ op_flags = QCOM_SPI_READ_4(sc, QUP_OPERATIONAL);
+
+ /* ACK state */
+ QCOM_SPI_WRITE_4(sc, QUP_ERROR_FLAGS, qup_error);
+ QCOM_SPI_WRITE_4(sc, SPI_ERROR_FLAGS, spi_error);
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_HW_INTR,
+ "%s: called; qup=0x%08x, spi=0x%08x, op=0x%08x\n",
+ __func__,
+ qup_error,
+ spi_error,
+ op_flags);
+
+ /* handle error flags */
+ if (qup_error != 0) {
+ device_printf(sc->sc_dev, "ERROR: (QUP) mask=0x%08x\n",
+ qup_error);
+ sc->intr.error = true;
+ }
+ if (spi_error != 0) {
+ device_printf(sc->sc_dev, "ERROR: (SPI) mask=0x%08x\n",
+ spi_error);
+ sc->intr.error = true;
+ }
+
+ /* handle operational state */
+ if (qcom_spi_hw_is_opmode_dma_locked(sc)) {
+ /* ACK interrupts now */
+ QCOM_SPI_WRITE_4(sc, QUP_OPERATIONAL, op_flags);
+ if ((op_flags & QUP_OP_IN_SERVICE_FLAG)
+ && (op_flags & QUP_OP_MAX_INPUT_DONE_FLAG))
+ sc->intr.rx_dma_done = true;
+ if ((op_flags & QUP_OP_OUT_SERVICE_FLAG)
+ && (op_flags & QUP_OP_MAX_OUTPUT_DONE_FLAG))
+ sc->intr.tx_dma_done = true;
+ } else {
+ /* FIFO/Block */
+ if (op_flags & QUP_OP_IN_SERVICE_FLAG)
+ sc->intr.do_rx = true;
+ if (op_flags & QUP_OP_OUT_SERVICE_FLAG)
+ sc->intr.do_tx = true;
+ }
+
+ /* Check if we've finished transfers */
+ if (op_flags & QUP_OP_MAX_INPUT_DONE_FLAG)
+ sc->intr.done = true;
+ if (sc->intr.error)
+ sc->intr.done = true;
+
+ return (0);
+}
+
+/*
+ * Make initial transfer selections based on the transfer sizes
+ * and alignment.
+ *
+ * For now this'll just default to FIFO until that works, and then
+ * will grow to include BLOCK / DMA as appropriate.
+ */
+int
+qcom_spi_hw_setup_transfer_selection(struct qcom_spi_softc *sc, uint32_t len)
+{
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ /*
+ * For now only support doing a single FIFO transfer.
+ * The main PIO transfer routine loop will break it up for us.
+ */
+ sc->state.transfer_mode = QUP_IO_M_MODE_FIFO;
+ sc->transfer.tx_offset = 0;
+ sc->transfer.rx_offset = 0;
+ sc->transfer.tx_len = 0;
+ sc->transfer.rx_len = 0;
+ sc->transfer.tx_buf = NULL;
+ sc->transfer.rx_buf = NULL;
+
+ /*
+ * If we're sending a DWORD multiple sized block (like IO buffers)
+ * then we can totally just use the DWORD size transfers.
+ *
+ * This is really only valid for PIO/block modes; I'm not yet
+ * sure what we should do for DMA modes.
+ */
+ if (len > 0 && len % 4 == 0)
+ sc->state.transfer_word_size = 4;
+ else
+ sc->state.transfer_word_size = 1;
+
+ return (0);
+}
+
+/*
+ * Blank the transfer state after a full transfer is completed.
+ */
+int
+qcom_spi_hw_complete_transfer(struct qcom_spi_softc *sc)
+{
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ sc->state.transfer_mode = QUP_IO_M_MODE_FIFO;
+ sc->transfer.tx_offset = 0;
+ sc->transfer.rx_offset = 0;
+ sc->transfer.tx_len = 0;
+ sc->transfer.rx_len = 0;
+ sc->transfer.tx_buf = NULL;
+ sc->transfer.rx_buf = NULL;
+ sc->state.transfer_word_size = 0;
+ return (0);
+}
+
+/*
+ * Configure up the transfer selection for the current transfer.
+ *
+ * This calculates how many words we can transfer in the current
+ * transfer and what's left to transfer.
+ */
+int
+qcom_spi_hw_setup_current_transfer(struct qcom_spi_softc *sc)
+{
+ uint32_t bytes_left;
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ /*
+ * XXX For now, base this on the TX side buffer size, not both.
+ * Later on we'll want to configure it based on the MAX of
+ * either and just eat up the dummy values in the PIO
+ * routines. (For DMA it's .. more annoyingly complicated
+ * if the transfer sizes are not symmetrical.)
+ */
+ bytes_left = sc->transfer.tx_len - sc->transfer.tx_offset;
+
+ if (sc->state.transfer_mode == QUP_IO_M_MODE_FIFO) {
+ /*
+ * For FIFO transfers the num_words limit depends upon
+ * the word size, FIFO size and how many bytes are left.
+ * It definitely will be under SPI_MAX_XFER so don't
+ * worry about that here.
+ */
+ sc->transfer.num_words = bytes_left / sc->state.transfer_word_size;
+ sc->transfer.num_words = MIN(sc->transfer.num_words,
+ sc->config.input_fifo_size / sizeof(uint32_t));
+ } else if (sc->state.transfer_mode == QUP_IO_M_MODE_BLOCK) {
+ /*
+ * For BLOCK transfers the logic will be a little different.
+ * Instead of it being based on the maximum input_fifo_size,
+ * it'll be broken down into the 'words per block" size but
+ * our maximum transfer size will ACTUALLY be capped by
+ * SPI_MAX_XFER (65536-64 bytes.) Each transfer
+ * will end up being in multiples of a block until the
+ * last transfer.
+ */
+ sc->transfer.num_words = bytes_left / sc->state.transfer_word_size;
+ sc->transfer.num_words = MIN(sc->transfer.num_words,
+ SPI_MAX_XFER);
+ }
+
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_HW_TRANSFER_SETUP,
+ "%s: transfer.tx_len=%u,"
+ "transfer.tx_offset=%u,"
+ " transfer_word_size=%u,"
+ " bytes_left=%u, num_words=%u, fifo_word_max=%u\n",
+ __func__,
+ sc->transfer.tx_len,
+ sc->transfer.tx_offset,
+ sc->state.transfer_word_size,
+ bytes_left,
+ sc->transfer.num_words,
+ sc->config.input_fifo_size / sizeof(uint32_t));
+
+ return (0);
+}
+
+/*
+ * Setup the PIO FIFO transfer count.
+ *
+ * Note that we get a /single/ TX/RX phase up to these num_words
+ * transfers.
+ */
+int
+qcom_spi_hw_setup_pio_transfer_cnt(struct qcom_spi_softc *sc)
+{
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ QCOM_SPI_WRITE_4(sc, QUP_MX_READ_CNT, sc->transfer.num_words);
+ QCOM_SPI_WRITE_4(sc, QUP_MX_WRITE_CNT, sc->transfer.num_words);
+ QCOM_SPI_WRITE_4(sc, QUP_MX_INPUT_CNT, 0);
+ QCOM_SPI_WRITE_4(sc, QUP_MX_OUTPUT_CNT, 0);
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_HW_TRANSFER_SETUP,
+ "%s: num_words=%u\n", __func__,
+ sc->transfer.num_words);
+
+ QCOM_SPI_BARRIER_WRITE(sc);
+
+ return (0);
+}
+
+/*
+ * Setup the PIO BLOCK transfer count.
+ *
+ * This sets up the total transfer size, in TX/RX FIFO block size
+ * chunks. We will get multiple notifications when a block sized
+ * chunk of data is avaliable or required.
+ */
+int
+qcom_spi_hw_setup_block_transfer_cnt(struct qcom_spi_softc *sc)
+{
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ QCOM_SPI_WRITE_4(sc, QUP_MX_READ_CNT, 0);
+ QCOM_SPI_WRITE_4(sc, QUP_MX_WRITE_CNT, 0);
+ QCOM_SPI_WRITE_4(sc, QUP_MX_INPUT_CNT, sc->transfer.num_words);
+ QCOM_SPI_WRITE_4(sc, QUP_MX_OUTPUT_CNT, sc->transfer.num_words);
+ QCOM_SPI_BARRIER_WRITE(sc);
+
+ return (0);
+}
+
+int
+qcom_spi_hw_setup_io_modes(struct qcom_spi_softc *sc)
+{
+ uint32_t reg;
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ reg = QCOM_SPI_READ_4(sc, QUP_IO_M_MODES);
+
+ reg &= ~((QUP_IO_M_INPUT_MODE_MASK << QUP_IO_M_INPUT_MODE_SHIFT)
+ | (QUP_IO_M_OUTPUT_MODE_MASK << QUP_IO_M_OUTPUT_MODE_SHIFT));
+
+ /*
+ * If it's being done using DMA then the hardware will
+ * need to pack and unpack the byte stream into the word/dword
+ * stream being expected by the SPI/QUP micro engine.
+ *
+ * For PIO modes we're doing the pack/unpack in software,
+ * see the pio/block transfer routines.
+ */
+ if (qcom_spi_hw_is_opmode_dma_locked(sc))
+ reg |= (QUP_IO_M_PACK_EN | QUP_IO_M_UNPACK_EN);
+ else
+ reg &= ~(QUP_IO_M_PACK_EN | QUP_IO_M_UNPACK_EN);
+
+ /* Transfer mode */
+ reg |= ((sc->state.transfer_mode & QUP_IO_M_INPUT_MODE_MASK)
+ << QUP_IO_M_INPUT_MODE_SHIFT);
+ reg |= ((sc->state.transfer_mode & QUP_IO_M_OUTPUT_MODE_MASK)
+ << QUP_IO_M_OUTPUT_MODE_SHIFT);
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_HW_TRANSFER_SETUP,
+ "%s: QUP_IO_M_MODES=0x%08x\n", __func__, reg);
+
+ QCOM_SPI_WRITE_4(sc, QUP_IO_M_MODES, reg);
+ QCOM_SPI_BARRIER_WRITE(sc);
+
+ return (0);
+}
+
+int
+qcom_spi_hw_setup_spi_io_clock_polarity(struct qcom_spi_softc *sc,
+ bool cpol)
+{
+ uint32_t reg;
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ reg = QCOM_SPI_READ_4(sc, SPI_IO_CONTROL);
+
+ if (cpol)
+ reg |= SPI_IO_C_CLK_IDLE_HIGH;
+ else
+ reg &= ~SPI_IO_C_CLK_IDLE_HIGH;
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_HW_TRANSFER_SETUP,
+ "%s: SPI_IO_CONTROL=0x%08x\n", __func__, reg);
+
+ QCOM_SPI_WRITE_4(sc, SPI_IO_CONTROL, reg);
+ QCOM_SPI_BARRIER_WRITE(sc);
+
+ return (0);
+}
+
+int
+qcom_spi_hw_setup_spi_config(struct qcom_spi_softc *sc, uint32_t clock_val,
+ bool cpha)
+{
+ uint32_t reg;
+
+ /*
+ * For now we don't have a way to configure loopback SPI for testing,
+ * or the clock/transfer phase. When we do then here's where we
+ * would put that.
+ */
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ reg = QCOM_SPI_READ_4(sc, SPI_CONFIG);
+ reg &= ~SPI_CONFIG_LOOPBACK;
+
+ if (cpha)
+ reg &= ~SPI_CONFIG_INPUT_FIRST;
+ else
+ reg |= SPI_CONFIG_INPUT_FIRST;
+
+ /*
+ * If the frequency is above SPI_HS_MIN_RATE then enable high speed.
+ * This apparently improves stability.
+ *
+ * Note - don't do this if SPI loopback is enabled!
+ */
+ if (clock_val >= SPI_HS_MIN_RATE)
+ reg |= SPI_CONFIG_HS_MODE;
+ else
+ reg &= ~SPI_CONFIG_HS_MODE;
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_HW_TRANSFER_SETUP,
+ "%s: SPI_CONFIG=0x%08x\n", __func__, reg);
+
+ QCOM_SPI_WRITE_4(sc, SPI_CONFIG, reg);
+ QCOM_SPI_BARRIER_WRITE(sc);
+
+ return (0);
+}
+
+int
+qcom_spi_hw_setup_qup_config(struct qcom_spi_softc *sc, bool is_tx, bool is_rx)
+{
+ uint32_t reg;
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ reg = QCOM_SPI_READ_4(sc, QUP_CONFIG);
+ reg &= ~(QUP_CONFIG_NO_INPUT | QUP_CONFIG_NO_OUTPUT | QUP_CONFIG_N);
+
+ /* SPI mode */
+ reg |= QUP_CONFIG_SPI_MODE;
+
+ /* bitmask for number of bits per word being used in each FIFO slot */
+ reg |= ((sc->state.transfer_word_size * 8) - 1) & QUP_CONFIG_N;
+
+ /*
+ * When doing DMA we need to configure whether we are shifting
+ * data in, out, and/or both. For PIO/block modes it must stay
+ * unset.
+ */
+ if (qcom_spi_hw_is_opmode_dma_locked(sc)) {
+ if (is_rx == false)
+ reg |= QUP_CONFIG_NO_INPUT;
+ if (is_tx == false)
+ reg |= QUP_CONFIG_NO_OUTPUT;
+ }
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_HW_TRANSFER_SETUP,
+ "%s: QUP_CONFIG=0x%08x\n", __func__, reg);
+
+ QCOM_SPI_WRITE_4(sc, QUP_CONFIG, reg);
+ QCOM_SPI_BARRIER_WRITE(sc);
+
+ return (0);
+}
+
+int
+qcom_spi_hw_setup_operational_mask(struct qcom_spi_softc *sc)
+{
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ if (QCOM_SPI_QUP_VERSION_V1(sc)) {
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_HW_TRANSFER_SETUP,
+ "%s: skipping, qupv1\n", __func__);
+ return (0);
+ }
+
+ if (qcom_spi_hw_is_opmode_dma_locked(sc))
+ QCOM_SPI_WRITE_4(sc, QUP_OPERATIONAL_MASK,
+ QUP_OP_IN_SERVICE_FLAG | QUP_OP_OUT_SERVICE_FLAG);
+ else
+ QCOM_SPI_WRITE_4(sc, QUP_OPERATIONAL_MASK, 0);
+
+ QCOM_SPI_BARRIER_WRITE(sc);
+
+ return (0);
+}
+
+/*
+ * ACK that we already have serviced the output FIFO.
+ */
+int
+qcom_spi_hw_ack_write_pio_fifo(struct qcom_spi_softc *sc)
+{
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+ QCOM_SPI_WRITE_4(sc, QUP_OPERATIONAL, QUP_OP_OUT_SERVICE_FLAG);
+ QCOM_SPI_BARRIER_WRITE(sc);
+ return (0);
+}
+
+int
+qcom_spi_hw_ack_opmode(struct qcom_spi_softc *sc)
+{
+ uint32_t reg;
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ QCOM_SPI_BARRIER_READ(sc);
+ reg = QCOM_SPI_READ_4(sc, QUP_OPERATIONAL);
+ QCOM_SPI_WRITE_4(sc, QUP_OPERATIONAL, QUP_OP_OUT_SERVICE_FLAG);
+ QCOM_SPI_BARRIER_WRITE(sc);
+ return (0);
+
+}
+
+/*
+ * Read the value from the TX buffer into the given 32 bit DWORD,
+ * pre-shifting it into the place requested.
+ *
+ * Returns true if there was a byte available, false otherwise.
+ */
+static bool
+qcom_spi_hw_write_from_tx_buf(struct qcom_spi_softc *sc, int shift,
+ uint32_t *val)
+{
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ if (sc->transfer.tx_buf == NULL)
+ return false;
+
+ if (sc->transfer.tx_offset < sc->transfer.tx_len) {
+ *val |= (sc->transfer.tx_buf[sc->transfer.tx_offset] & 0xff)
+ << shift;
+ sc->transfer.tx_offset++;
+ return true;
+ }
+
+ return false;
+}
+
+int
+qcom_spi_hw_write_pio_fifo(struct qcom_spi_softc *sc)
+{
+ uint32_t i;
+ int num_bytes = 0;
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ QCOM_SPI_WRITE_4(sc, QUP_OPERATIONAL, QUP_OP_OUT_SERVICE_FLAG);
+ QCOM_SPI_BARRIER_WRITE(sc);
+
+ /*
+ * Loop over the transfer num_words, do complain if we are full.
+ */
+ for (i = 0; i < sc->transfer.num_words; i++) {
+ uint32_t reg;
+
+ /* Break if FIFO is full */
+ if ((QCOM_SPI_READ_4(sc, QUP_OPERATIONAL)
+ & QUP_OP_OUT_FIFO_FULL) != 0) {
+ device_printf(sc->sc_dev, "%s: FIFO full\n", __func__);
+ break;
+ }
+
+ /*
+ * Handle 1, 2, 4 byte transfer packing rules.
+ *
+ * Unlike read, where the shifting is done towards the MSB
+ * for us by default, we have to do it ourselves for transmit.
+ * There's a bit that one can set to do the preshifting
+ * (and u-boot uses it!) but I'll stick with what Linux is
+ * doing to make it easier for future maintenance.
+ *
+ * The format is the same as 4 byte RX - 0xaabbccdd;
+ * the byte ordering on the wire being aa, bb, cc, dd.
+ */
+ reg = 0;
+ if (sc->state.transfer_word_size == 1) {
+ if (qcom_spi_hw_write_from_tx_buf(sc, 24, &reg))
+ num_bytes++;
+ } else if (sc->state.transfer_word_size == 2) {
+ if (qcom_spi_hw_write_from_tx_buf(sc, 24, &reg))
+ num_bytes++;
+ if (qcom_spi_hw_write_from_tx_buf(sc, 16, &reg))
+ num_bytes++;
+ } else if (sc->state.transfer_word_size == 4) {
+ if (qcom_spi_hw_write_from_tx_buf(sc, 24, &reg))
+ num_bytes++;
+ if (qcom_spi_hw_write_from_tx_buf(sc, 16, &reg))
+ num_bytes++;
+ if (qcom_spi_hw_write_from_tx_buf(sc, 8, &reg))
+ num_bytes++;
+ if (qcom_spi_hw_write_from_tx_buf(sc, 0, &reg))
+ num_bytes++;
+ }
+
+ /*
+ * always shift out something in case we need phantom
+ * writes to finish things up whilst we read a reply
+ * payload.
+ */
+ QCOM_SPI_WRITE_4(sc, QUP_OUTPUT_FIFO, reg);
+ QCOM_SPI_BARRIER_WRITE(sc);
+ }
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_HW_TX_FIFO,
+ "%s: wrote %d bytes (%d fifo slots)\n",
+ __func__, num_bytes, sc->transfer.num_words);
+
+ return (0);
+}
+
+int
+qcom_spi_hw_write_pio_block(struct qcom_spi_softc *sc)
+{
+ /* Not yet implemented */
+ return (ENXIO);
+}
+
+/*
+ * Read data into the the RX buffer and increment the RX offset.
+ *
+ * Return true if the byte was saved into the RX buffer, else
+ * return false.
+ */
+static bool
+qcom_spi_hw_read_into_rx_buf(struct qcom_spi_softc *sc, uint8_t val)
+{
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ if (sc->transfer.rx_buf == NULL)
+ return false;
+
+ /* Make sure we aren't overflowing the receive buffer */
+ if (sc->transfer.rx_offset < sc->transfer.rx_len) {
+ sc->transfer.rx_buf[sc->transfer.rx_offset] = val;
+ sc->transfer.rx_offset++;
+ return true;
+ }
+ return false;
+}
+
+/*
+ * Read "n_words" transfers, and push those bytes into the receive buffer.
+ * Make sure we have enough space, and make sure we don't overflow the
+ * read buffer size too!
+ */
+int
+qcom_spi_hw_read_pio_fifo(struct qcom_spi_softc *sc)
+{
+ uint32_t i;
+ uint32_t reg;
+ int num_bytes = 0;
+
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ QCOM_SPI_WRITE_4(sc, QUP_OPERATIONAL, QUP_OP_IN_SERVICE_FLAG);
+ QCOM_SPI_BARRIER_WRITE(sc);
+
+ for (i = 0; i < sc->transfer.num_words; i++) {
+ /* Break if FIFO is empty */
+ QCOM_SPI_BARRIER_READ(sc);
+ reg = QCOM_SPI_READ_4(sc, QUP_OPERATIONAL);
+ if ((reg & QUP_OP_IN_FIFO_NOT_EMPTY) == 0) {
+ device_printf(sc->sc_dev, "%s: FIFO empty\n", __func__);
+ break;
+ }
+
+ /*
+ * Always read num_words up to FIFO being non-empty; that way
+ * if we have mis-matching TX/RX buffer sizes for some reason
+ * we will read the needed phantom bytes.
+ */
+ reg = QCOM_SPI_READ_4(sc, QUP_INPUT_FIFO);
+
+ /*
+ * Unpack the receive buffer based on whether we are
+ * doing 1, 2, or 4 byte transfer words.
+ */
+ if (sc->state.transfer_word_size == 1) {
+ if (qcom_spi_hw_read_into_rx_buf(sc, reg & 0xff))
+ num_bytes++;
+ } else if (sc->state.transfer_word_size == 2) {
+ if (qcom_spi_hw_read_into_rx_buf(sc, (reg >> 8) & 0xff))
+ num_bytes++;
+ if (qcom_spi_hw_read_into_rx_buf(sc, reg & 0xff))
+ num_bytes++;
+ } else if (sc->state.transfer_word_size == 4) {
+ if (qcom_spi_hw_read_into_rx_buf(sc, (reg >> 24) & 0xff))
+ num_bytes++;
+ if (qcom_spi_hw_read_into_rx_buf(sc, (reg >> 16) & 0xff))
+ num_bytes++;
+ if (qcom_spi_hw_read_into_rx_buf(sc, (reg >> 8) & 0xff))
+ num_bytes++;
+ if (qcom_spi_hw_read_into_rx_buf(sc, reg & 0xff))
+ num_bytes++;
+ }
+ }
+
+ QCOM_SPI_DPRINTF(sc, QCOM_SPI_DEBUG_HW_TX_FIFO,
+ "%s: read %d bytes (%d transfer words)\n",
+ __func__, num_bytes, sc->transfer.num_words);
+
+#if 0
+ /*
+ * This is a no-op for FIFO mode, it's only a thing for BLOCK
+ * transfers.
+ */
+ QCOM_SPI_BARRIER_READ(sc);
+ reg = QCOM_SPI_READ_4(sc, QUP_OPERATIONAL);
+ if (reg & QUP_OP_MAX_INPUT_DONE_FLAG) {
+ device_printf(sc->sc_dev, "%s: read complete (DONE)\n" ,
+ __func__);
+ sc->intr.done = true;
+ }
+#endif
+
+#if 0
+ /*
+ * And see if we've finished the transfer and won't be getting
+ * any more. Then treat it as done as well.
+ *
+ * In FIFO only mode we don't get a completion interrupt;
+ * we get an interrupt when the FIFO has enough data present.
+ */
+ if ((sc->state.transfer_mode == QUP_IO_M_MODE_FIFO)
+ && (sc->transfer.rx_offset >= sc->transfer.rx_len)) {
+ device_printf(sc->sc_dev, "%s: read complete (rxlen)\n",
+ __func__);
+ sc->intr.done = true;
+ }
+#endif
+
+ /*
+ * For FIFO transfers we get a /single/ result that complete
+ * the FIFO transfer. We won't get any subsequent transfers;
+ * we'll need to schedule a new FIFO transfer.
+ */
+ sc->intr.done = true;
+
+ return (0);
+}
+
+int
+qcom_spi_hw_read_pio_block(struct qcom_spi_softc *sc)
+{
+
+ /* Not yet implemented */
+ return (ENXIO);
+}
+
+int
+qcom_spi_hw_do_full_reset(struct qcom_spi_softc *sc)
+{
+ QCOM_SPI_ASSERT_LOCKED(sc);
+
+ QCOM_SPI_WRITE_4(sc, QUP_SW_RESET, 1);
+ QCOM_SPI_BARRIER_WRITE(sc);
+ DELAY(100);
+
+ return (0);
+}
diff --git a/sys/dev/qcom_qup/qcom_spi_reg.h b/sys/dev/qcom_qup/qcom_spi_reg.h
new file mode 100644
index 000000000000..d23cbc3c8efb
--- /dev/null
+++ b/sys/dev/qcom_qup/qcom_spi_reg.h
@@ -0,0 +1,76 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * Copyright (c) 2021 Adrian Chadd <adrian@FreeBSD.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * $FreeBSD$
+ */
+
+#ifndef __QCOM_SPI_REG_H__
+#define __QCOM_SPI_REG_H__
+
+#define SPI_CONFIG 0x0300
+#define SPI_CONFIG_HS_MODE (1U << 10)
+#define SPI_CONFIG_INPUT_FIRST (1U << 9)
+#define SPI_CONFIG_LOOPBACK (1U << 8)
+
+#define SPI_IO_CONTROL 0x0304
+#define SPI_IO_C_FORCE_CS (1U << 11)
+#define SPI_IO_C_CLK_IDLE_HIGH (1U << 10)
+#define SPI_IO_C_MX_CS_MODE (1U << 8)
+#define SPI_IO_C_CS_N_POLARITY_0 (1U << 4)
+#define SPI_IO_C_CS_SELECT(x) (((x) & 3) << 2)
+#define SPI_IO_C_CS_SELECT_MASK 0x000c
+#define SPI_IO_C_TRISTATE_CS (1U << 1)
+#define SPI_IO_C_NO_TRI_STATE (1U << 0)
+
+#define SPI_ERROR_FLAGS 0x0308
+#define SPI_ERROR_FLAGS_EN 0x030c
+#define SPI_ERROR_CLK_OVER_RUN (1U << 1)
+#define SPI_ERROR_CLK_UNDER_RUN (1U << 0)
+
+/*
+ * Strictly this isn't true; some controllers have
+ * less CS lines exposed via GPIO/pinmux.
+ */
+#define SPI_NUM_CHIPSELECTS 4
+
+/*
+ * The maximum single SPI transaction done in any mode.
+ * Ie, if you have a PIO/DMA transaction larger than
+ * this then it must be split up into SPI_MAX_XFER
+ * sub-transactions in the transfer loop.
+ */
+#define SPI_MAX_XFER (65536 - 64)
+
+/*
+ * Any frequency at or above 26MHz is considered "high"
+ * and will have some different parameters configured.
+ */
+#define SPI_HS_MIN_RATE 26000000
+
+#define SPI_MAX_RATE 50000000
+
+#endif /* __QCOM_SPI_REG_H__ */
+
diff --git a/sys/dev/qcom_qup/qcom_spi_var.h b/sys/dev/qcom_qup/qcom_spi_var.h
new file mode 100644
index 000000000000..322de1e87a57
--- /dev/null
+++ b/sys/dev/qcom_qup/qcom_spi_var.h
@@ -0,0 +1,162 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * Copyright (c) 2021 Adrian Chadd <adrian@FreeBSD.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * $FreeBSD$
+ */
+
+#ifndef __QCOM_SPI_VAR_H__
+#define __QCOM_SPI_VAR_H__
+
+typedef enum {
+ QCOM_SPI_HW_QPI_V1_1 = 1,
+ QCOM_SPI_HW_QPI_V2_1 = 2,
+ QCOM_SPI_HW_QPI_V2_2 = 3,
+} qcom_spi_hw_version_t;
+
+#define CS_MAX 4
+
+struct qcom_spi_softc {
+ device_t sc_dev;
+ device_t spibus;
+
+ uint32_t sc_debug;
+
+ struct resource *sc_mem_res;
+ struct resource *sc_irq_res;
+ void *sc_irq_h;
+
+ struct mtx sc_mtx;
+ bool sc_busy; /* an SPI transfer (cmd+data)
+ * is active */
+
+ qcom_spi_hw_version_t hw_version;
+
+ clk_t clk_core; /* QUP/SPI core */
+ clk_t clk_iface; /* SPI interface */
+
+ /* For GPIO chip selects .. */
+ gpio_pin_t cs_pins[CS_MAX];
+
+ struct {
+ /*
+ * FIFO size / block size in bytes.
+ *
+ * The FIFO slots are DWORD sized, not byte sized.
+ * So if the transfer size is set to 8 bits per
+ * word (which is what we'll support initially)
+ * the effective available FIFO is
+ * fifo_size / sizeof(uint32_t).
+ */
+ uint32_t input_block_size;
+ uint32_t output_block_size;
+ uint32_t input_fifo_size;
+ uint32_t output_fifo_size;
+
+ uint32_t cs_select;
+ uint32_t num_cs;
+ uint32_t max_frequency;
+ } config;
+
+ struct {
+ uint32_t transfer_mode; /* QUP_IO_M_MODE_* */
+ uint32_t transfer_word_size; /* how many bytes in a transfer word */
+ uint32_t frequency;
+ bool cs_high; /* true if CS is high for active */
+ } state;
+
+ struct {
+ bool tx_dma_done;
+ bool rx_dma_done;
+ bool done;
+ bool do_tx;
+ bool do_rx;
+ bool error;
+ } intr;
+
+ struct {
+ bool active; /* a (sub) transfer is active */
+ uint32_t num_words; /* number of word_size words to transfer */
+ const char *tx_buf;
+ int tx_len;
+ int tx_offset;
+ char *rx_buf;
+ int rx_len;
+ int rx_offset;
+ bool done;
+ } transfer;
+};
+
+#define QCOM_SPI_QUP_VERSION_V1(sc) \
+ ((sc)->hw_version == QCOM_SPI_HW_QPI_V1_1)
+
+#define QCOM_SPI_LOCK(sc) mtx_lock(&(sc)->sc_mtx)
+#define QCOM_SPI_UNLOCK(sc) mtx_unlock(&(sc)->sc_mtx)
+#define QCOM_SPI_ASSERT_LOCKED(sc) mtx_assert(&(sc)->sc_mtx, MA_OWNED)
+#define QCOM_SPI_READ_4(sc, reg) bus_read_4((sc)->sc_mem_res, (reg))
+#define QCOM_SPI_WRITE_4(sc, reg, val) bus_write_4((sc)->sc_mem_res, \
+ (reg), (val))
+
+/* XXX TODO: the region size should be in the tag or softc */
+#define QCOM_SPI_BARRIER_WRITE(sc) bus_barrier((sc)->sc_mem_res, \
+ 0, 0x600, BUS_SPACE_BARRIER_WRITE)
+#define QCOM_SPI_BARRIER_READ(sc) bus_barrier((sc)->sc_mem_res, \
+ 0, 0x600, BUS_SPACE_BARRIER_READ)
+#define QCOM_SPI_BARRIER_RW(sc) bus_barrier((sc)->sc_mem_res, \
+ 0, 0x600, BUS_SPACE_BARRIER_READ | BUS_SPACE_BARRIER_WRITE)
+
+extern int qcom_spi_hw_read_controller_transfer_sizes(
+ struct qcom_spi_softc *sc);
+extern int qcom_spi_hw_qup_set_state_locked(struct qcom_spi_softc *sc,
+ uint32_t state);
+extern int qcom_spi_hw_qup_init_locked(struct qcom_spi_softc *sc);
+extern int qcom_spi_hw_spi_init_locked(struct qcom_spi_softc *sc);
+extern int qcom_spi_hw_spi_cs_force(struct qcom_spi_softc *sc, int cs,
+ bool enable);
+extern int qcom_spi_hw_interrupt_handle(struct qcom_spi_softc *sc);
+extern int qcom_spi_hw_setup_transfer_selection(struct qcom_spi_softc *sc,
+ uint32_t len);
+extern int qcom_spi_hw_complete_transfer(struct qcom_spi_softc *sc);
+
+extern int qcom_spi_hw_setup_current_transfer(struct qcom_spi_softc *sc);
+extern int qcom_spi_hw_setup_pio_transfer_cnt(struct qcom_spi_softc *sc);
+extern int qcom_spi_hw_setup_block_transfer_cnt(struct qcom_spi_softc *sc);
+extern int qcom_spi_hw_setup_io_modes(struct qcom_spi_softc *sc);
+extern int qcom_spi_hw_setup_spi_io_clock_polarity(
+ struct qcom_spi_softc *sc, bool cpol);
+extern int qcom_spi_hw_setup_spi_config(struct qcom_spi_softc *sc,
+ uint32_t clock_val, bool cpha);
+extern int qcom_spi_hw_setup_qup_config(struct qcom_spi_softc *sc,
+ bool is_tx, bool is_rx);
+extern int qcom_spi_hw_setup_operational_mask(struct qcom_spi_softc *sc);
+extern int qcom_spi_hw_ack_write_pio_fifo(struct qcom_spi_softc *sc);
+extern int qcom_spi_hw_ack_opmode(struct qcom_spi_softc *sc);
+extern int qcom_spi_hw_write_pio_fifo(struct qcom_spi_softc *sc);
+extern int qcom_spi_hw_write_pio_block(struct qcom_spi_softc *sc);
+extern int qcom_spi_hw_read_pio_fifo(struct qcom_spi_softc *sc);
+extern int qcom_spi_hw_read_pio_block(struct qcom_spi_softc *sc);
+extern int qcom_spi_hw_do_full_reset(struct qcom_spi_softc *sc);
+
+#endif /* __QCOM_SPI_VAR_H__ */