From patchwork Tue Nov 27 17:54:37 2018 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Anatolij Gustschin X-Patchwork-Id: 10701137 Return-Path: Received: from mail.wl.linuxfoundation.org (pdx-wl-mail.web.codeaurora.org [172.30.200.125]) by pdx-korg-patchwork-2.web.codeaurora.org (Postfix) with ESMTP id 6E11913BB for ; Tue, 27 Nov 2018 17:54:52 +0000 (UTC) Received: from mail.wl.linuxfoundation.org (localhost [127.0.0.1]) by mail.wl.linuxfoundation.org (Postfix) with ESMTP id 5D0692B6C3 for ; Tue, 27 Nov 2018 17:54:52 +0000 (UTC) Received: by mail.wl.linuxfoundation.org (Postfix, from userid 486) id 507CE2B6CC; Tue, 27 Nov 2018 17:54:52 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on pdx-wl-mail.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-7.9 required=2.0 tests=BAYES_00,MAILING_LIST_MULTI, RCVD_IN_DNSWL_HI autolearn=ham version=3.3.1 Received: from vger.kernel.org (vger.kernel.org [209.132.180.67]) by mail.wl.linuxfoundation.org (Postfix) with ESMTP id 255122B6C3 for ; Tue, 27 Nov 2018 17:54:51 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1732075AbeK1Exb (ORCPT ); Tue, 27 Nov 2018 23:53:31 -0500 Received: from mail-out.m-online.net ([212.18.0.9]:39439 "EHLO mail-out.m-online.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1732074AbeK1Exa (ORCPT ); Tue, 27 Nov 2018 23:53:30 -0500 Received: from frontend01.mail.m-online.net (unknown [192.168.8.182]) by mail-out.m-online.net (Postfix) with ESMTP id 434BJ60CHGz1qvgd; Tue, 27 Nov 2018 18:54:46 +0100 (CET) Received: from localhost (dynscan1.mnet-online.de [192.168.6.70]) by mail.m-online.net (Postfix) with ESMTP id 434BJ601fDz1qqkB; Tue, 27 Nov 2018 18:54:46 +0100 (CET) X-Virus-Scanned: amavisd-new at mnet-online.de Received: from mail.mnet-online.de ([192.168.8.182]) by localhost (dynscan1.mail.m-online.net [192.168.6.70]) (amavisd-new, port 10024) with ESMTP id f27OlRTbt9bZ; Tue, 27 Nov 2018 18:54:44 +0100 (CET) X-Auth-Info: FyCzfr3QHLavkVQ78dLCwJHO5RkbQB43tpLMTBtJFwc= Received: from crub.agik.hopto.org (p508DE362.dip0.t-ipconnect.de [80.141.227.98]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mail.mnet-online.de (Postfix) with ESMTPSA; Tue, 27 Nov 2018 18:54:44 +0100 (CET) From: Anatolij Gustschin To: linux-usb@vger.kernel.org, linux-spi@vger.kernel.org, linux-fpga@vger.kernel.org, linux-kernel@vger.kernel.org Cc: gregkh@linuxfoundation.org, broonie@kernel.org, atull@kernel.org, mdf@kernel.org Subject: [PATCH v3 2/3] spi: add FTDI MPSSE SPI controller driver Date: Tue, 27 Nov 2018 18:54:37 +0100 Message-Id: <20181127175438.16116-3-agust@denx.de> X-Mailer: git-send-email 2.17.1 In-Reply-To: <20181127175438.16116-1-agust@denx.de> References: <20181127175438.16116-1-agust@denx.de> Sender: linux-fpga-owner@vger.kernel.org Precedence: bulk List-ID: X-Mailing-List: linux-fpga@vger.kernel.org X-Virus-Scanned: ClamAV using ClamSMTP Add SPI bus controller driver for FTDI MPSSE mode. This driver is supposed to be used together with the FT232H interface driver for FPGA configuration in drivers/usb/misc/ft232h-intf.c which adds an mpsse spi platform device describing USB SPI bus with attached SPI slave devices. Signed-off-by: Anatolij Gustschin --- drivers/spi/Kconfig | 7 + drivers/spi/Makefile | 1 + drivers/spi/spi-ftdi-mpsse.c | 651 +++++++++++++++++++++++++++++++++++ 3 files changed, 659 insertions(+) create mode 100644 drivers/spi/spi-ftdi-mpsse.c diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig index 7d3a5c94727e..2cb24e28c485 100644 --- a/drivers/spi/Kconfig +++ b/drivers/spi/Kconfig @@ -259,6 +259,13 @@ config SPI_FSL_LPSPI help This enables Freescale i.MX LPSPI controllers in master mode. +config SPI_FTDI_MPSSE + tristate "FTDI MPSSE SPI controller" + depends on USB_FT232H_INTF || COMPILE_TEST + help + FT232H supports SPI in MPSSE mode. This driver provides MPSSE + SPI controller in master mode. + config SPI_GPIO tristate "GPIO-based bitbanging SPI Master" depends on GPIOLIB || COMPILE_TEST diff --git a/drivers/spi/Makefile b/drivers/spi/Makefile index 3575205c5c27..065240a02b31 100644 --- a/drivers/spi/Makefile +++ b/drivers/spi/Makefile @@ -45,6 +45,7 @@ obj-$(CONFIG_SPI_FSL_LIB) += spi-fsl-lib.o obj-$(CONFIG_SPI_FSL_ESPI) += spi-fsl-espi.o obj-$(CONFIG_SPI_FSL_LPSPI) += spi-fsl-lpspi.o obj-$(CONFIG_SPI_FSL_SPI) += spi-fsl-spi.o +obj-$(CONFIG_SPI_FTDI_MPSSE) += spi-ftdi-mpsse.o obj-$(CONFIG_SPI_GPIO) += spi-gpio.o obj-$(CONFIG_SPI_IMG_SPFI) += spi-img-spfi.o obj-$(CONFIG_SPI_IMX) += spi-imx.o diff --git a/drivers/spi/spi-ftdi-mpsse.c b/drivers/spi/spi-ftdi-mpsse.c new file mode 100644 index 000000000000..ae8442eb4052 --- /dev/null +++ b/drivers/spi/spi-ftdi-mpsse.c @@ -0,0 +1,651 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// FTDI FT232H MPSSE SPI controller driver +// +// Copyright (C) 2017 - 2018 DENX Software Engineering +// Anatolij Gustschin +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum gpiol { + MPSSE_SK = BIT(0), + MPSSE_DO = BIT(1), + MPSSE_DI = BIT(2), + MPSSE_CS = BIT(3), +}; + +struct ftdi_spi { + struct platform_device *pdev; + struct usb_interface *intf; + struct spi_controller *master; + const struct ft232h_intf_ops *iops; + struct gpiod_lookup_table *lookup[FTDI_MPSSE_GPIOS]; + struct gpio_desc **cs_gpios; + + u8 txrx_cmd; + u8 rx_cmd; + u8 tx_cmd; + u8 xfer_buf[SZ_64K]; + u16 last_mode; +}; + +static void ftdi_spi_set_cs(struct spi_device *spi, bool enable) +{ + struct ftdi_spi *priv = spi_controller_get_devdata(spi->master); + u16 cs = spi->chip_select; + + dev_dbg(&priv->pdev->dev, "%s: CS %u, cs mode %d, val %d\n", + __func__, cs, (spi->mode & SPI_CS_HIGH), enable); + + gpiod_set_raw_value_cansleep(priv->cs_gpios[cs], enable); +} + +static inline u8 ftdi_spi_txrx_byte_cmd(struct spi_device *spi) +{ + u8 mode = spi->mode & (SPI_CPOL | SPI_CPHA); + u8 cmd; + + if (spi->mode & SPI_LSB_FIRST) { + switch (mode) { + case SPI_MODE_0: + case SPI_MODE_1: + cmd = TXF_RXR_BYTES_LSB; + break; + case SPI_MODE_2: + case SPI_MODE_3: + cmd = TXR_RXF_BYTES_LSB; + break; + } + } else { + switch (mode) { + case SPI_MODE_0: + case SPI_MODE_1: + cmd = TXF_RXR_BYTES_MSB; + break; + case SPI_MODE_2: + case SPI_MODE_3: + cmd = TXR_RXF_BYTES_MSB; + break; + } + } + return cmd; +} + +static inline int ftdi_spi_loopback_cfg(struct ftdi_spi *priv, int on) +{ + int ret; + + priv->xfer_buf[0] = on ? LOOPBACK_ON : LOOPBACK_OFF; + + ret = priv->iops->write_data(priv->intf, priv->xfer_buf, 1); + if (ret < 0) + dev_warn(&priv->pdev->dev, "loopback %d failed\n", on); + return ret; +} + +static int ftdi_spi_tx_rx(struct ftdi_spi *priv, struct spi_device *spi, + struct spi_transfer *t) +{ + const struct ft232h_intf_ops *ops = priv->iops; + struct device *dev = &priv->pdev->dev; + void *rx_offs; + const void *tx_offs; + size_t remaining, stride; + size_t rx_stride; + int ret, tout = 10; + const u8 *tx_data = t->tx_buf; + u8 *rx_data = t->rx_buf; + + ops->lock(priv->intf); + + if (spi->mode & SPI_LOOP) { + ret = ftdi_spi_loopback_cfg(priv, 1); + if (ret < 0) + goto err; + } + + remaining = t->len; + rx_offs = rx_data; + tx_offs = tx_data; + + while (remaining) { + stride = min_t(size_t, remaining, SZ_512 - 3); + + priv->xfer_buf[0] = priv->txrx_cmd; + priv->xfer_buf[1] = stride - 1; + priv->xfer_buf[2] = (stride - 1) >> 8; + memcpy(&priv->xfer_buf[3], tx_offs, stride); + print_hex_dump_debug("WR: ", DUMP_PREFIX_OFFSET, 16, 1, + priv->xfer_buf, stride + 3, 1); + + ret = ops->write_data(priv->intf, priv->xfer_buf, stride + 3); + if (ret < 0) { + dev_err(dev, "%s: xfer failed %d\n", __func__, ret); + goto fail; + } + dev_dbg(dev, "%s: WR %zu byte(s), TXRX CMD 0x%02x\n", + __func__, stride, priv->txrx_cmd); + + rx_stride = min_t(size_t, stride, SZ_512); + + ret = ops->read_data(priv->intf, priv->xfer_buf, rx_stride); + while (ret == 0) { + /* If no data yet, wait and repeat */ + usleep_range(5000, 5100); + ret = ops->read_data(priv->intf, priv->xfer_buf, + rx_stride); + dev_dbg(dev, "Waiting data ready, read: %d\n", ret); + if (!--tout) { + dev_err(dev, "Read timeout\n"); + ret = -ETIMEDOUT; + goto fail; + } + } + + if (ret < 0) + goto fail; + + print_hex_dump_debug("RD: ", DUMP_PREFIX_OFFSET, 16, 1, + priv->xfer_buf, rx_stride, 1); + memcpy(rx_offs, priv->xfer_buf, ret); + rx_offs += ret; + + remaining -= stride; + tx_offs += stride; + dev_dbg(dev, "%s: WR remains %zu\n", __func__, remaining); + } + + ret = 0; + +fail: + if (spi->mode & SPI_LOOP) + ftdi_spi_loopback_cfg(priv, 0); + +err: + ops->unlock(priv->intf); + return ret; +} + +static int ftdi_spi_push_buf(struct ftdi_spi *priv, const void *buf, size_t len) +{ + size_t bytesleft = len; + int ret; + + do { + ret = priv->iops->write_data(priv->intf, buf, bytesleft); + if (ret < 0) + return ret; + + buf += ret; + bytesleft -= ret; + } while (bytesleft); + + return len; +} + +static int ftdi_spi_tx(struct ftdi_spi *priv, struct spi_transfer *xfer) +{ + const void *tx_offs; + size_t remaining, stride; + int ret; + + priv->iops->lock(priv->intf); + + tx_offs = xfer->tx_buf; + remaining = xfer->len; + + do { + stride = min_t(size_t, remaining, sizeof(priv->xfer_buf) - 3); + + priv->xfer_buf[0] = priv->tx_cmd; + priv->xfer_buf[1] = stride - 1; + priv->xfer_buf[2] = (stride - 1) >> 8; + + memcpy(&priv->xfer_buf[3], tx_offs, stride); + + ret = ftdi_spi_push_buf(priv, priv->xfer_buf, stride + 3); + if (ret < 0) { + dev_dbg(&priv->pdev->dev, "%s: tx failed %d\n", + __func__, ret); + goto err; + } + dev_dbg(&priv->pdev->dev, "%s: %zu byte(s) done\n", + __func__, stride); + remaining -= stride; + tx_offs += stride; + } while (remaining); + + ret = 0; +err: + priv->iops->unlock(priv->intf); + return ret; +} + +static int ftdi_spi_rx(struct ftdi_spi *priv, struct spi_transfer *xfer) +{ + const struct ft232h_intf_ops *ops = priv->iops; + struct device *dev = &priv->pdev->dev; + size_t remaining, stride; + int ret, tout = 10; + void *rx_offs; + + dev_dbg(dev, "%s: CMD 0x%02x, len %u\n", + __func__, priv->rx_cmd, xfer->len); + + priv->xfer_buf[0] = priv->rx_cmd; + priv->xfer_buf[1] = xfer->len - 1; + priv->xfer_buf[2] = (xfer->len - 1) >> 8; + + ops->lock(priv->intf); + + ret = ops->write_data(priv->intf, priv->xfer_buf, 3); + if (ret < 0) + goto err; + + remaining = xfer->len; + rx_offs = xfer->rx_buf; + + do { + stride = min_t(size_t, remaining, SZ_512); + + ret = ops->read_data(priv->intf, priv->xfer_buf, stride); + if (ret < 0) + goto err; + + if (!ret) { + dev_dbg(dev, "Waiting for data (read: %02X), tout %d\n", + ret, tout); + if (--tout) + continue; + + dev_dbg(dev, "read timeout...\n"); + ret = -ETIMEDOUT; + goto err; + } + + memcpy(rx_offs, priv->xfer_buf, ret); + + dev_dbg(dev, "%s: %d byte(s)\n", __func__, ret); + rx_offs += ret; + remaining -= ret; + } while (remaining); + + ret = 0; +err: + ops->unlock(priv->intf); + return ret; +} + +static int ftdi_spi_transfer_one(struct spi_controller *ctlr, + struct spi_device *spi, + struct spi_transfer *xfer) +{ + struct ftdi_spi *priv = spi_controller_get_devdata(ctlr); + struct device *dev = &priv->pdev->dev; + int ret = 0; + + if (!xfer->len) + return 0; + + if (priv->last_mode != spi->mode) { + u8 spi_mode = spi->mode & (SPI_CPOL | SPI_CPHA); + u8 pins = 0; + + dev_dbg(dev, "%s: MODE 0x%x\n", __func__, spi->mode); + + if (spi->mode & SPI_LSB_FIRST) { + switch (spi_mode) { + case SPI_MODE_0: + case SPI_MODE_3: + priv->tx_cmd = TX_BYTES_FE_LSB; + priv->rx_cmd = RX_BYTES_RE_LSB; + break; + case SPI_MODE_1: + case SPI_MODE_2: + priv->tx_cmd = TX_BYTES_RE_LSB; + priv->rx_cmd = RX_BYTES_FE_LSB; + break; + } + } else { + switch (spi_mode) { + case SPI_MODE_0: + case SPI_MODE_3: + priv->tx_cmd = TX_BYTES_FE_MSB; + priv->rx_cmd = RX_BYTES_RE_MSB; + break; + case SPI_MODE_1: + case SPI_MODE_2: + priv->tx_cmd = TX_BYTES_RE_MSB; + priv->rx_cmd = RX_BYTES_FE_MSB; + break; + } + } + + priv->txrx_cmd = ftdi_spi_txrx_byte_cmd(spi); + + switch (spi_mode) { + case SPI_MODE_2: + case SPI_MODE_3: + pins |= MPSSE_SK; + break; + } + + ret = priv->iops->cfg_bus_pins(priv->intf, + MPSSE_SK | MPSSE_DO, pins); + if (ret < 0) { + dev_err(dev, "IO cfg failed: %d\n", ret); + return ret; + } + priv->last_mode = spi->mode; + } + + dev_dbg(dev, "%s: mode 0x%x, CMD RX/TX 0x%x/0x%x\n", + __func__, spi->mode, priv->rx_cmd, priv->tx_cmd); + + if (xfer->tx_buf && xfer->rx_buf) + ret = ftdi_spi_tx_rx(priv, spi, xfer); + else if (xfer->tx_buf) + ret = ftdi_spi_tx(priv, xfer); + else if (xfer->rx_buf) + ret = ftdi_spi_rx(priv, xfer); + + dev_dbg(dev, "%s: xfer ret %d\n", __func__, ret); + + spi_finalize_current_transfer(ctlr); + return ret; +} + +static int ftdi_mpsse_init(struct ftdi_spi *priv) +{ + struct platform_device *pdev = priv->pdev; + int ret; + + dev_dbg(&pdev->dev, "MPSSE init\n"); + + /* Setup and send off the Hi-Speed specific commands for the FTx232H */ + priv->xfer_buf[0] = DIS_DIV_5; /* Use 60MHz master clock */ + priv->xfer_buf[1] = DIS_ADAPTIVE; /* Turn off adaptive clocking */ + priv->xfer_buf[2] = DIS_3_PHASE; /* Disable three-phase clocking */ + + priv->iops->lock(priv->intf); + + ret = priv->iops->write_data(priv->intf, priv->xfer_buf, 3); + if (ret < 0) { + dev_err(&pdev->dev, "Clk cfg failed: %d\n", ret); + priv->iops->unlock(priv->intf); + return ret; + } + + priv->xfer_buf[0] = TCK_DIVISOR; + priv->xfer_buf[1] = div_value(60000000); + priv->xfer_buf[2] = div_value(60000000) >> 8; + dev_dbg(&pdev->dev, "TCK_DIVISOR: 0x%04x 0x%04x\n", + priv->xfer_buf[1], priv->xfer_buf[2]); + + ret = priv->iops->write_data(priv->intf, priv->xfer_buf, 3); + if (ret < 0) { + dev_err(&pdev->dev, "Clk cfg failed: %d\n", ret); + priv->iops->unlock(priv->intf); + return ret; + } + + priv->iops->unlock(priv->intf); + + ret = priv->iops->cfg_bus_pins(priv->intf, MPSSE_SK | MPSSE_DO, 0); + if (ret < 0) { + dev_err(&pdev->dev, "Can't init SPI bus pins: %d\n", ret); + return ret; + } + + return 0; +} + +static int ftdi_spi_init_io(struct spi_controller *master, unsigned int dev_idx) +{ + struct ftdi_spi *priv = spi_controller_get_devdata(master); + struct platform_device *pdev = priv->pdev; + const struct mpsse_spi_platform_data *pd; + const struct mpsse_spi_dev_data *data; + struct gpiod_lookup_table *lookup; + size_t lookup_size, size; + char *label; + unsigned int i; + u16 cs; + + pd = pdev->dev.platform_data; + + data = pd->spi_info[dev_idx].platform_data; + if (!data || data->magic != FTDI_MPSSE_IO_DESC_MAGIC) + return 0; + + size = data->desc_len + 1; + + lookup_size = sizeof(*lookup) + size * sizeof(struct gpiod_lookup); + lookup = devm_kzalloc(&pdev->dev, lookup_size, GFP_KERNEL); + if (!lookup) + return -ENOMEM; + + cs = pd->spi_info[dev_idx].chip_select; + + lookup->dev_id = devm_kasprintf(&pdev->dev, GFP_KERNEL, "spi%d.%d", + master->bus_num, cs); + if (!lookup->dev_id) { + devm_kfree(&pdev->dev, lookup); + return -ENOMEM; + } + dev_dbg(&pdev->dev, "LOOKUP ID '%s'\n", lookup->dev_id); + + label = devm_kasprintf(&pdev->dev, GFP_KERNEL, "ftdi-mpsse-gpio.%d", + pdev->id); + if (!label) { + devm_kfree(&pdev->dev, (void *)lookup->dev_id); + devm_kfree(&pdev->dev, lookup); + return -ENOMEM; + } + + for (i = 0; i < data->desc_len; i++) { + dev_dbg(&pdev->dev, "con_id: '%s' idx: %d, flags: 0x%x\n", + data->desc[i].con_id, data->desc[i].idx, + data->desc[i].flags); + lookup->table[i].chip_label = label; + lookup->table[i].chip_hwnum = data->desc[i].idx; + lookup->table[i].idx = 0; + lookup->table[i].con_id = data->desc[i].con_id; + lookup->table[i].flags = data->desc[i].flags; + } + + priv->lookup[cs] = lookup; + gpiod_add_lookup_table(lookup); + return 0; +} + +static int ftdi_spi_probe(struct platform_device *pdev) +{ + const struct mpsse_spi_platform_data *pd; + struct device *dev = &pdev->dev; + struct spi_controller *master; + struct ftdi_spi *priv; + struct gpio_desc *desc; + u16 num_cs, max_cs = 0; + unsigned int i; + int ret; + + pd = dev->platform_data; + if (!pd) { + dev_err(dev, "Missing platform data.\n"); + return -EINVAL; + } + + if (!pd->ops || + !pd->ops->read_data || !pd->ops->write_data || + !pd->ops->lock || !pd->ops->unlock || + !pd->ops->set_bitmode || !pd->ops->set_baudrate || + !pd->ops->disable_bitbang || !pd->ops->cfg_bus_pins) + return -EINVAL; + + if (pd->spi_info_len > FTDI_MPSSE_GPIOS) + return -EINVAL; + + /* Find max. slave chipselect number */ + num_cs = pd->spi_info_len; + for (i = 0; i < num_cs; i++) { + if (max_cs < pd->spi_info[i].chip_select) + max_cs = pd->spi_info[i].chip_select; + } + + if (max_cs > FTDI_MPSSE_GPIOS - 1) { + dev_err(dev, "Invalid max CS in platform data: %u\n", max_cs); + return -EINVAL; + } + dev_dbg(dev, "CS count %u, max CS %u\n", num_cs, max_cs); + max_cs += 1; /* including CS0 */ + + master = spi_alloc_master(&pdev->dev, sizeof(*priv)); + if (!master) + return -ENOMEM; + + platform_set_drvdata(pdev, master); + + priv = spi_controller_get_devdata(master); + priv->master = master; + priv->pdev = pdev; + priv->intf = to_usb_interface(dev->parent); + priv->iops = pd->ops; + + master->bus_num = -1; + master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_LOOP | + SPI_CS_HIGH | SPI_LSB_FIRST; + master->num_chipselect = max_cs; + master->min_speed_hz = 450; + master->max_speed_hz = 30000000; + master->bits_per_word_mask = SPI_BPW_MASK(8); + master->set_cs = ftdi_spi_set_cs; + master->transfer_one = ftdi_spi_transfer_one; + master->auto_runtime_pm = false; + + priv->cs_gpios = devm_kcalloc(&master->dev, max_cs, sizeof(desc), + GFP_KERNEL); + if (!priv->cs_gpios) { + spi_controller_put(master); + return -ENOMEM; + } + + for (i = 0; i < num_cs; i++) { + unsigned int idx = pd->spi_info[i].chip_select; + + dev_dbg(&pdev->dev, "CS num: %u\n", idx); + desc = devm_gpiod_get_index(&priv->pdev->dev, "spi-cs", + i, GPIOD_OUT_LOW); + if (IS_ERR(desc)) { + ret = PTR_ERR(desc); + dev_err(&pdev->dev, "CS %u gpiod err: %d\n", i, ret); + continue; + } + priv->cs_gpios[idx] = desc; + } + + ret = spi_register_controller(master); + if (ret < 0) { + dev_err(&pdev->dev, "Failed to register spi master\n"); + spi_controller_put(master); + return ret; + } + + ret = priv->iops->set_bitmode(priv->intf, 0x00, BITMODE_MPSSE); + if (ret < 0) { + dev_err(&pdev->dev, "Failed to set MPSSE mode\n"); + goto err; + } + + priv->last_mode = 0xffff; + + ret = ftdi_mpsse_init(priv); + if (ret < 0) { + dev_err(&pdev->dev, "MPSSE init failed\n"); + goto err; + } + + for (i = 0; i < pd->spi_info_len; i++) { + struct spi_device *sdev; + u16 cs; + + dev_dbg(&pdev->dev, "slave: '%s', CS: %u\n", + pd->spi_info[i].modalias, pd->spi_info[i].chip_select); + + ret = ftdi_spi_init_io(master, i); + if (ret < 0) { + dev_warn(&pdev->dev, "Can't add slave IO: %d\n", ret); + continue; + } + sdev = spi_new_device(master, &pd->spi_info[i]); + if (!sdev) { + cs = pd->spi_info[i].chip_select; + dev_warn(&pdev->dev, "Can't add slave '%s', CS %u\n", + pd->spi_info[i].modalias, cs); + if (priv->lookup[cs]) { + gpiod_remove_lookup_table(priv->lookup[cs]); + priv->lookup[cs] = NULL; + } + } + } + + return 0; +err: + platform_set_drvdata(pdev, NULL); + spi_unregister_controller(master); + return ret; +} + +static int ftdi_spi_slave_release(struct device *dev, void *data) +{ + struct spi_device *spi = to_spi_device(dev); + struct ftdi_spi *priv = data; + u16 cs = spi->chip_select; + + dev_dbg(dev, "%s: remove CS %u\n", __func__, cs); + spi_unregister_device(to_spi_device(dev)); + + if (priv->lookup[cs]) + gpiod_remove_lookup_table(priv->lookup[cs]); + return 0; +} + +static int ftdi_spi_remove(struct platform_device *pdev) +{ + struct spi_controller *master; + struct ftdi_spi *priv; + + master = platform_get_drvdata(pdev); + priv = spi_controller_get_devdata(master); + + device_for_each_child(&master->dev, priv, ftdi_spi_slave_release); + + spi_unregister_controller(master); + return 0; +} + +static struct platform_driver ftdi_spi_driver = { + .driver.name = "ftdi-mpsse-spi", + .probe = ftdi_spi_probe, + .remove = ftdi_spi_remove, +}; +module_platform_driver(ftdi_spi_driver); + +MODULE_ALIAS("platform:ftdi-mpsse-spi"); +MODULE_AUTHOR("Anatolij Gustschin