From d27ba3088424e53eabc0b0186ed122ec43119501 Mon Sep 17 00:00:00 2001 From: Adrian Chadd Date: Mon, 27 Dec 2021 15:27:29 -0800 Subject: [PATCH] qcom_qup: add initial v1/v2 QUP SPI driver The Qualcomm Universal Peripherals Engine (QUP) is a unified SPI and I2C peripheral that ships with a variety of Qualcomm SoCs. It supports three transfer modes - single PIO, block PIO and DMA. This driver only supports the single PIO mode, which is enough to bootstrap the rest of the SPI NAND/NOR support and means I can do things like read the Wifi calibration data from NOR. It has some hardware support code for the other transfer modes as well as some support for split transfers (ie, transfers with no read or write phase), but I haven't yet implemented those. This driver is based on four sources - the linux driver, the u-boot driver, some initial work done for APQ8064 by mmel@, and the APQ8064 Technical Reference Manual which is surprisingly free and open to read. The linux and u-boot drivers approach a variety of things completely differently, from how PIO is done, the hardware support for re-ordering bytes in a transfer word and how the CS lines are used. Tested: * IPQ4018, SPI to NAND/NOR flash, PIO only --- sys/arm/conf/std.qca | 5 + sys/arm/qualcomm/std.ipq4018 | 3 + sys/dev/qcom_qup/qcom_qup_reg.h | 115 ++++ sys/dev/qcom_qup/qcom_spi.c | 911 +++++++++++++++++++++++++++ sys/dev/qcom_qup/qcom_spi_debug.h | 49 ++ sys/dev/qcom_qup/qcom_spi_hw.c | 985 ++++++++++++++++++++++++++++++ sys/dev/qcom_qup/qcom_spi_reg.h | 76 +++ sys/dev/qcom_qup/qcom_spi_var.h | 162 +++++ 8 files changed, 2306 insertions(+) create mode 100644 sys/dev/qcom_qup/qcom_qup_reg.h create mode 100644 sys/dev/qcom_qup/qcom_spi.c create mode 100644 sys/dev/qcom_qup/qcom_spi_debug.h create mode 100644 sys/dev/qcom_qup/qcom_spi_hw.c create mode 100644 sys/dev/qcom_qup/qcom_spi_reg.h create mode 100644 sys/dev/qcom_qup/qcom_spi_var.h diff --git a/sys/arm/conf/std.qca b/sys/arm/conf/std.qca index a78e27074ea..7caae2f645c 100644 --- a/sys/arm/conf/std.qca +++ b/sys/arm/conf/std.qca @@ -24,6 +24,11 @@ device syscon # Random device qcom_rnd +# SPI +device spibus +device qcom_qup_spi +device mx25l + # interrupt controller device gic diff --git a/sys/arm/qualcomm/std.ipq4018 b/sys/arm/qualcomm/std.ipq4018 index 8a9dba579e4..2187e3f1751 100644 --- a/sys/arm/qualcomm/std.ipq4018 +++ b/sys/arm/qualcomm/std.ipq4018 @@ -5,6 +5,9 @@ arm/qualcomm/qcom_cpu_kpssv2.c optional smp dev/qcom_rnd/qcom_rnd.c optional qcom_rnd +dev/qcom_qup/qcom_spi.c optional qcom_qup_spi +dev/qcom_qup/qcom_spi_hw.c optional qcom_qup_spi + dev/qcom_gcc/qcom_gcc_ipq4018.c optional qcom_gcc_ipq4018 dev/qcom_gcc/qcom_gcc_ipq4018_reset.c optional qcom_gcc_ipq4018 dev/qcom_gcc/qcom_gcc_ipq4018_clock.c optional qcom_gcc_ipq4018 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 00000000000..18931e56095 --- /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 + * + * 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 00000000000..e16a03db46d --- /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 + * + * 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 +__FBSDID("$FreeBSD$"); + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include "spibus_if.h" + +#include +#include +#include +#include + +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 00000000000..9225bb4d42e --- /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 + * + * 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 00000000000..1d08b10e5cd --- /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 + * + * 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 +__FBSDID("$FreeBSD$"); + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include "spibus_if.h" + +#include +#include +#include +#include + +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, ®)) + num_bytes++; + } else if (sc->state.transfer_word_size == 2) { + if (qcom_spi_hw_write_from_tx_buf(sc, 24, ®)) + num_bytes++; + if (qcom_spi_hw_write_from_tx_buf(sc, 16, ®)) + num_bytes++; + } else if (sc->state.transfer_word_size == 4) { + if (qcom_spi_hw_write_from_tx_buf(sc, 24, ®)) + num_bytes++; + if (qcom_spi_hw_write_from_tx_buf(sc, 16, ®)) + num_bytes++; + if (qcom_spi_hw_write_from_tx_buf(sc, 8, ®)) + num_bytes++; + if (qcom_spi_hw_write_from_tx_buf(sc, 0, ®)) + 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 00000000000..d23cbc3c8ef --- /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 + * + * 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 00000000000..322de1e87a5 --- /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 + * + * 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__ */