diff options
| author | Adrian Chadd <adrian@FreeBSD.org> | 2021-12-27 23:27:29 +0000 |
|---|---|---|
| committer | Adrian Chadd <adrian@FreeBSD.org> | 2021-12-27 23:27:29 +0000 |
| commit | d27ba3088424e53eabc0b0186ed122ec43119501 (patch) | |
| tree | 037cd599e5507e4ee26443d1ae49d162d0ef0c6f /sys/dev/qcom_qup | |
| parent | 989453da0589b8dc5c1948fd81f986a37ea385eb (diff) | |
Diffstat (limited to 'sys/dev/qcom_qup')
| -rw-r--r-- | sys/dev/qcom_qup/qcom_qup_reg.h | 115 | ||||
| -rw-r--r-- | sys/dev/qcom_qup/qcom_spi.c | 911 | ||||
| -rw-r--r-- | sys/dev/qcom_qup/qcom_spi_debug.h | 49 | ||||
| -rw-r--r-- | sys/dev/qcom_qup/qcom_spi_hw.c | 985 | ||||
| -rw-r--r-- | sys/dev/qcom_qup/qcom_spi_reg.h | 76 | ||||
| -rw-r--r-- | sys/dev/qcom_qup/qcom_spi_var.h | 162 |
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, ®)) + 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 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__ */ |
