The implementation is transparent for the SPI devices and doesn't require their modifications. It is based on a virtual SPI device (spi-daisy_chain) and defines two required device tree properties ('spi-daisy-chain-len' and 'spi-daisy-chain-noop') and one optional ('spi-daisy-chain-bits_per_word'). It has been tested on hardware with a chain of three ltc2694 devices (kernel v4.19).
Signed-off-by: Adrian Fiergolski <adrian.fiergol...@fastree3d.com> --- Changes in v2: - declare spi_daisy_chain_clean as static Reported-by: kernel test robot <l...@intel.com> --- drivers/spi/Kconfig | 8 + drivers/spi/Makefile | 1 + drivers/spi/spi-daisy_chain.c | 428 ++++++++++++++++++++++++++++ drivers/spi/spi.c | 65 ++++- include/linux/spi/spi-daisy_chain.h | 32 +++ include/linux/spi/spi.h | 17 +- 6 files changed, 541 insertions(+), 10 deletions(-) create mode 100644 drivers/spi/spi-daisy_chain.c create mode 100644 include/linux/spi/spi-daisy_chain.h diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig index 8f1f8fca79e3..822c4b4bdd5c 100644 --- a/drivers/spi/Kconfig +++ b/drivers/spi/Kconfig @@ -55,6 +55,14 @@ config SPI_MEM This extension is meant to simplify interaction with SPI memories by providing a high-level interface to send memory-like commands. +config SPI_DAISY_CHAIN + bool "SPI daisy chain support" + depends on OF + help + This enables support for the SPI daisy chains. + This extension provides a virtual SPI daisy chain device which + links together physical SPI devices on a common SPI daisy chain. + comment "SPI Master Controller Drivers" config SPI_ALTERA diff --git a/drivers/spi/Makefile b/drivers/spi/Makefile index d2e41d3d464a..dbcb98480f51 100644 --- a/drivers/spi/Makefile +++ b/drivers/spi/Makefile @@ -9,6 +9,7 @@ ccflags-$(CONFIG_SPI_DEBUG) := -DDEBUG # config declarations into driver model code obj-$(CONFIG_SPI_MASTER) += spi.o obj-$(CONFIG_SPI_MEM) += spi-mem.o +obj-$(CONFIG_SPI_DAISY_CHAIN) += spi-daisy_chain.o obj-$(CONFIG_SPI_MUX) += spi-mux.o obj-$(CONFIG_SPI_SPIDEV) += spidev.o obj-$(CONFIG_SPI_LOOPBACK_TEST) += spi-loopback-test.o diff --git a/drivers/spi/spi-daisy_chain.c b/drivers/spi/spi-daisy_chain.c new file mode 100644 index 000000000000..f187502b1861 --- /dev/null +++ b/drivers/spi/spi-daisy_chain.c @@ -0,0 +1,428 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/* + * A driver handling the SPI daisy chaines. + * + * Copyright (C) 2020 Fastree3D + * Adrian Fiergolski <adrian.fiergol...@fastree3d.com> + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/of.h> +#include <linux/spi/spi.h> + +#include <linux/spi/spi-daisy_chain.h> + +/** + * spi_daisy_chain_message_pre - generate SPI daisy chain message + * @spi: device with which data will be exchanged + * @message: describes the data transfers + * + * The call checks if the SPI device is part of daisy chain. If so, it + * generates a message containg no-operation codes + * of unaddressed device of a given daisy chain. + **/ + +int spi_daisy_chain_message_pre(struct spi_device *spi, + struct spi_message *message) +{ + struct spi_transfer *tr, *ntr; + struct spi_daisy_chain_device *spi_chain_dev; + int rc; + + //the device is not part of a daisy-chain + if (spi->daisy_chain_devs == NULL) + return 0; + + if (message->is_dma_mapped) { + dev_err(&spi->dev, + "DMA mapped transfer is not support when on daisy chain"); + return -EINVAL; + } + + if (!list_is_singular(&message->transfers)) { + dev_err(&spi->dev, + "Mutliple transfer segments are not support when on daisy chain"); + return -EINVAL; + } + + message->daisy_chain_transfers = message->transfers; + INIT_LIST_HEAD(&message->transfers); + + list_for_each_entry(spi_chain_dev, spi->daisy_chain_devs, devices) { + if (spi_chain_dev->spi == spi) { + tr = list_first_entry(&message->daisy_chain_transfers, + struct spi_transfer, + transfer_list); + + //check if mode is not being changed + if (tr->tx_nbits) + switch (spi->mode & + (SPI_TX_DUAL | SPI_TX_QUAD)) { + case 0: + if (!(tr->tx_nbits & SPI_NBITS_SINGLE)) + goto err_tx_mode; + break; + + case SPI_TX_DUAL: + if (!(tr->tx_nbits & SPI_NBITS_DUAL)) + goto err_tx_mode; + break; + + case SPI_TX_QUAD: + if (!(tr->tx_nbits & SPI_NBITS_QUAD)) + goto err_tx_mode; + break; + + default: + goto err_tx_mode; + } + + if (tr->rx_nbits) + switch (spi->mode & + (SPI_RX_DUAL | SPI_RX_QUAD)) { + case 0: + if (!(tr->rx_nbits & SPI_NBITS_SINGLE)) + goto err_rx_mode; + break; + + case SPI_RX_DUAL: + if (tr->rx_nbits & SPI_NBITS_DUAL) + goto err_rx_mode; + break; + case SPI_RX_QUAD: + if (tr->rx_nbits & SPI_NBITS_QUAD) + goto err_rx_mode; + break; + default: + goto err_rx_mode; + } + + //check if frequency is not being changed + if (tr->speed_hz && tr->speed_hz != spi->max_speed_hz) { + dev_err(&spi->dev, + "Change of SPI frequency not supported when on daisy chain"); + return -EINVAL; + } + + //daisy chain operations has a regular length + if (tr->len == spi_chain_dev->no_operation.len) { + tr->bits_per_word = spi_chain_dev->no_operation + .bits_per_word; + tr->cs_change = 0; + + list_add_tail(&tr->transfer_list, + &message->transfers); + } + //daisy chain operation has different than regular length + else { + if (tr->len > spi_chain_dev->no_operation.len) { + dev_err(&spi->dev, + "Transmission not supported"); + return -EINVAL; + } + + ntr = kzalloc(sizeof(*ntr), GFP_KERNEL); + + if (!ntr) + return -ENOMEM; + + message->daisy_chain_new_transfer = ntr; + + ntr->len = spi_chain_dev->no_operation.len; + ntr->bits_per_word = spi_chain_dev->no_operation + .bits_per_word; + + //copy tx buffer + if (tr->tx_buf) { + ntr->tx_buf = + kmalloc(ntr->len, GFP_KERNEL); + if (!ntr->tx_buf) { + rc = -ENOMEM; + goto err_out; + } + + //The daisy-chain padding is assumed to be right-justified, + //so unused tx bits are transferred first + memcpy((void *)((char *)ntr->tx_buf + + ntr->len - tr->len), + tr->tx_buf, tr->len); + } + + //allocate rx buffer + if (tr->rx_buf) { + ntr->rx_buf = + kmalloc(ntr->len, GFP_KERNEL); + if (!ntr->rx_buf) { + rc = -ENOMEM; + goto err_out; + } + } + + list_add_tail(&ntr->transfer_list, + &message->transfers); + } + } else + list_add_tail( + &spi_chain_dev->no_operation.transfer_list, + &message->transfers); + } + + return 0; + +err_out: + kfree(ntr->tx_buf); + kfree(ntr->rx_buf); + kfree(ntr); + return rc; + +err_tx_mode: + dev_err(&spi->dev, "Unsupported tx mode on daisy chain"); + return -EINVAL; + +err_rx_mode: + dev_err(&spi->dev, "Unsupported rx mode on daisy chain"); + return -EINVAL; +} +EXPORT_SYMBOL_GPL(spi_daisy_chain_message_pre); + +/** + * spi_daisy_chain_message_post - generate SPI daisy chain message + * @spi: device with which data will be exchanged + * @message: describes the data transfers + * + * The call checks if the SPI device is part of daisy chain. If so, it + * removes no-operation codes of unaddressd device of a given chain. + **/ + +void spi_daisy_chain_message_post(struct spi_device *spi, + struct spi_message *message) +{ + struct spi_transfer *tr; + + //the device is not part of a daisy-chain + if (spi->daisy_chain_devs == NULL) + return; + + if (message->daisy_chain_new_transfer) { + tr = list_first_entry(&message->daisy_chain_transfers, + struct spi_transfer, transfer_list); + if (tr->rx_buf) + //The daisy-chain padding is assumed to be right-justified, + //so unused rx bits were received first and can be skipped + memcpy((void *)tr->rx_buf, + (char *)message->daisy_chain_new_transfer->rx_buf + + message->daisy_chain_new_transfer->len - + tr->len, + tr->len); + + kfree(message->daisy_chain_new_transfer->tx_buf); + kfree(message->daisy_chain_new_transfer->rx_buf); + kfree(message->daisy_chain_new_transfer); + } + + list_del(&message->transfers); + + message->transfers = message->daisy_chain_transfers; +} +EXPORT_SYMBOL_GPL(spi_daisy_chain_message_post); + +static void spi_daisy_chain_clean(struct list_head *daisy_chain_devs) +{ + struct spi_device *spi_dev; + struct spi_daisy_chain_device *spi_chain_dev; + + list_for_each_entry(spi_chain_dev, daisy_chain_devs, devices) { + spi_dev = spi_chain_dev->spi; + spi_dev_put(spi_dev); + kfree(spi_chain_dev->no_operation.tx_buf); + kfree(spi_chain_dev); + } + list_del(daisy_chain_devs); + kfree(daisy_chain_devs); +} + +static int spi_daisy_chain_driver_probe(struct spi_device *spi) +{ + struct device_node *nc; + struct spi_device *spi_dev; + struct spi_daisy_chain_device *spi_chain_dev; + struct spi_transfer *no_operation; + int w_size; + int rc; + + //Initialise the SPI daisy-chain queue + spi->daisy_chain_devs = + kzalloc(sizeof(*spi->daisy_chain_devs), GFP_KERNEL); + if (!spi->daisy_chain_devs) + return -ENOMEM; + + INIT_LIST_HEAD(spi->daisy_chain_devs); + + for_each_available_child_of_node(spi->dev.of_node, nc) { + if (of_node_test_and_set_flag(nc, OF_POPULATED)) + continue; + + spi_chain_dev = kzalloc(sizeof(*spi_chain_dev), GFP_KERNEL); + if (!spi_chain_dev) { + rc = -ENOMEM; + goto err_out; + } + + //////////////////////////////////////////////////////////// + //extract daisy-chain no-operation command from devicetree + //////////////////////////////////////////////////////////// + no_operation = &spi_chain_dev->no_operation; + if (of_property_read_u8(nc, "spi-daisy-chain-bits_per_word", + &no_operation->bits_per_word)) { + no_operation->bits_per_word = 8; + } + + if (no_operation->bits_per_word > 32) { + dev_err(&spi->dev, + "device %pOF: 'spi-daisy-chain-bits_per_word' property can't by higher than 32", + nc); + rc = -EINVAL; + goto err_out_spi_chain; + } + + if (of_property_read_u32(nc, "spi-daisy-chain-len", + &no_operation->len)) { + dev_err(&spi->dev, + "device %pOF doesn't define 'spi-daisy-chain-len' property", + nc); + rc = -EINVAL; + goto err_out_spi_chain; + } + + // SPI transfer length should be multiple of SPI word size + // where SPI word size should be power-of-two multiple + if (no_operation->bits_per_word <= 8) + w_size = 1; + else if (no_operation->bits_per_word <= 16) + w_size = 2; + else + w_size = 4; + + /* No partial transfers accepted */ + if (no_operation->len % w_size) { + rc = -EINVAL; + dev_err(&spi->dev, + "no partial transfers accepted (propeties 'spi-daisy-chain-len' and spi-daisy-chain-bits_per_word of device %pOF", + nc); + rc = -EINVAL; + goto err_out_spi_chain; + } + + no_operation->tx_buf = kmalloc(no_operation->len, GFP_KERNEL); + if (!no_operation->tx_buf) { + rc = -ENOMEM; + goto err_out_spi_chain; + } + + if (of_property_read_u8_array(nc, "spi-daisy-chain-noop", + (void *)no_operation->tx_buf, + no_operation->len)) { + dev_err(&spi->dev, + "device %pOF doesn't define 'spi-daisy-chain-noop' property", + nc); + rc = -EINVAL; + goto err_out_tx_buf; + } + + //////////////////////////// + //allocate a new SPI device + //////////////////////////// + spi_dev = spi_alloc_device(spi->controller); + if (!spi_dev) { + dev_err(&spi->dev, "spi_device alloc error for %pOF\n", + nc); + rc = -ENOMEM; + goto err_out_tx_buf; + } + spi_chain_dev->spi = spi_dev; + spi_dev->daisy_chain_devs = spi->daisy_chain_devs; + + //select device driver + rc = of_modalias_node(nc, spi_dev->modalias, + sizeof(spi_dev->modalias)); + if (rc < 0) { + dev_err(&spi->dev, "cannot find modalias for %pOF\n", + nc); + goto err_out_spi_dev; + } + + //store a pointer to the node in the device structure + of_node_get(nc); + spi_dev->dev.of_node = nc; + + //add the SPI device to the chain + list_add_tail(&spi_chain_dev->devices, spi->daisy_chain_devs); + } + + ////////////////////// + //add all SPI devices + ////////////////////// + list_for_each_entry(spi_chain_dev, spi->daisy_chain_devs, devices) { + spi_dev = spi_chain_dev->spi; + + //All devices on the chain share settings of the daisy-chain node + //The individual settings of the SPI nodes are ignored + spi_dev->mode = spi->mode; + spi_dev->chip_select = spi->chip_select; + spi_dev->max_speed_hz = spi->max_speed_hz; + + //Register the new device + rc = spi_add_device(spi_dev); + if (rc) { + dev_err(&spi->dev, + "spi_device register error on daisy chain %pOF\n", + spi_dev->dev.of_node); + of_node_put(nc); + goto err_out; + } + } + + return 0; + +err_out_spi_dev: + spi_dev_put(spi_chain_dev->spi); +err_out_tx_buf: + kfree(spi_chain_dev->no_operation.tx_buf); +err_out_spi_chain: + kfree(spi_chain_dev); +err_out: + spi_daisy_chain_clean(spi->daisy_chain_devs); + return rc; +} + +static int spi_daisy_chain_driver_remove(struct spi_device *spi) +{ + spi_daisy_chain_clean(spi->daisy_chain_devs); + return 0; +} + +static const struct of_device_id spi_daisy_chain_of_match[] = { + { + .compatible = "spi,daisy_chain", + }, + {} +}; +MODULE_DEVICE_TABLE(of, spi_daisy_chain_of_match); + +static struct spi_driver spi_daisy_chain_driver = { + .probe = spi_daisy_chain_driver_probe, + .remove = spi_daisy_chain_driver_remove, + .driver = { + .name = "daisy_chain", + .of_match_table = spi_daisy_chain_of_match, + }, +}; +module_spi_driver(spi_daisy_chain_driver); + +MODULE_AUTHOR("Adrian Fiergolski <adrian.fiergol...@fastree3d.com>"); +MODULE_DESCRIPTION("Driver handling SPI daisy chain"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("spi:daisy_chain"); diff --git a/drivers/spi/spi.c b/drivers/spi/spi.c index 8158e281f354..96c8ce572e2f 100644 --- a/drivers/spi/spi.c +++ b/drivers/spi/spi.c @@ -18,6 +18,7 @@ #include <linux/mod_devicetable.h> #include <linux/spi/spi.h> #include <linux/spi/spi-mem.h> +#include <linux/spi/spi-daisy_chain.h> #include <linux/of_gpio.h> #include <linux/gpio/consumer.h> #include <linux/pm_runtime.h> @@ -522,14 +523,34 @@ EXPORT_SYMBOL_GPL(spi_alloc_device); static void spi_dev_set_name(struct spi_device *spi) { struct acpi_device *adev = ACPI_COMPANION(&spi->dev); +#ifdef CONFIG_SPI_DAISY_CHAIN + struct spi_daisy_chain_device *spi_chain_dev; + int spi_dev_index; +#endif if (adev) { dev_set_name(&spi->dev, "spi-%s", acpi_dev_name(adev)); return; } - dev_set_name(&spi->dev, "%s.%u", dev_name(&spi->controller->dev), - spi->chip_select); +#ifdef CONFIG_SPI_DAISY_CHAIN + if (spi->daisy_chain_devs != NULL) { + spi_dev_index = 0; + list_for_each_entry(spi_chain_dev, spi->daisy_chain_devs, + devices){ + if (spi_chain_dev->spi == spi) + break; + spi_dev_index++; + } + + dev_set_name(&spi->dev, "%s.%u.%u", + dev_name(&spi->controller->dev), spi->chip_select, + spi_dev_index); + + } else +#endif + dev_set_name(&spi->dev, "%s.%u", + dev_name(&spi->controller->dev), spi->chip_select); } static int spi_dev_check(struct device *dev, void *data) @@ -573,15 +594,27 @@ int spi_add_device(struct spi_device *spi) * chipselect **BEFORE** we call setup(), else we'll trash * its configuration. Lock against concurrent add() calls. */ - mutex_lock(&spi_add_lock); +#ifdef CONFIG_SPI_DAISY_CHAIN + /* Do not lock the controller when registering the daisy_chain driver + * as the last one wouldn't be able to register its subnodes + */ + if (strcmp((const char *)spi->dev.of_node->name, "daisy_chain") == 0) +#endif + mutex_lock(&spi_add_lock); - status = bus_for_each_dev(&spi_bus_type, NULL, spi, spi_dev_check); - if (status) { - dev_err(dev, "chipselect %d already in use\n", +#ifdef CONFIG_SPI_DAISY_CHAIN + if (spi->daisy_chain_devs == NULL) { +#endif + status = bus_for_each_dev(&spi_bus_type, NULL, spi, + spi_dev_check); + if (status) { + dev_err(dev, "chipselect %d already in use\n", spi->chip_select); - goto done; + goto done; + } +#ifdef CONFIG_SPI_DAISY_CHAIN } - +#endif /* Descriptors take precedence */ if (ctlr->cs_gpiods) spi->cs_gpiod = ctlr->cs_gpiods[spi->chip_select]; @@ -608,7 +641,10 @@ int spi_add_device(struct spi_device *spi) dev_dbg(dev, "registered child %s\n", dev_name(&spi->dev)); done: - mutex_unlock(&spi_add_lock); +#ifdef CONFIG_SPI_DAISY_CHAIN + if (strcmp((const char *)spi->dev.of_node->name, "daisy_chain") == 0) +#endif + mutex_unlock(&spi_add_lock); return status; } EXPORT_SYMBOL_GPL(spi_add_device); @@ -3694,6 +3730,12 @@ static int __spi_sync(struct spi_device *spi, struct spi_message *message) SPI_STATISTICS_INCREMENT_FIELD(&ctlr->statistics, spi_sync); SPI_STATISTICS_INCREMENT_FIELD(&spi->statistics, spi_sync); +#ifdef CONFIG_SPI_DAISY_CHAIN + status = spi_daisy_chain_message_pre(spi, message); + if (status < 0) + return status; +#endif + /* If we're not using the legacy transfer method then we will * try to transfer in the calling context so special case. * This code would be less tricky if we could remove the @@ -3727,6 +3769,11 @@ static int __spi_sync(struct spi_device *spi, struct spi_message *message) status = message->status; } message->context = NULL; + +#ifdef CONFIG_SPI_DAISY_CHAIN + spi_daisy_chain_message_post(spi, message); +#endif + return status; } diff --git a/include/linux/spi/spi-daisy_chain.h b/include/linux/spi/spi-daisy_chain.h new file mode 100644 index 000000000000..8967292863b7 --- /dev/null +++ b/include/linux/spi/spi-daisy_chain.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later + * + * A driver handling the SPI daisy chaines. + * + * Copyright (C) 2020 Fastree3D + * Adrian Fiergolski <adrian.fiergol...@fastree3d.com> + */ + +#ifndef __LINUX_SPI_DAISY_CHAIN_H +#define __LINUX_SPI_DAISY_CHAIN_H + +#include <linux/mutex.h> +#include <linux/types.h> + +/** + * struct spi_daisy_chain - interface to a SPI device on a daisy chain + * @spi: SPI device on the daisy chain + * @no_operation: no-opeartion SPI message for the given device + * @devices: list of SPI devices sharing the given daisy chain + **/ +struct spi_daisy_chain_device { + struct spi_device *spi; + struct spi_transfer no_operation; + struct list_head devices; +}; + +extern int spi_daisy_chain_message_pre(struct spi_device *spi, + struct spi_message *message); +extern void spi_daisy_chain_message_post(struct spi_device *spi, + struct spi_message *message); + +#endif /* __LINUX_SPI_DAISY_CHAIN_H */ diff --git a/include/linux/spi/spi.h b/include/linux/spi/spi.h index aac57b5b7c21..bdc6973606dd 100644 --- a/include/linux/spi/spi.h +++ b/include/linux/spi/spi.h @@ -111,6 +111,9 @@ extern int spi_delay_exec(struct spi_delay *_delay, struct spi_transfer *xfer); * @dev: Driver model representation of the device. * @controller: SPI controller used with the device. * @master: Copy of controller, for backwards compatibility. + * @daisy_chain_devs: list of all SPI devices on the daisy chain + * used by the given SPI device. + * Handled by the SPI daisy chain driver. * @max_speed_hz: Maximum clock rate to be used with this chip * (on this board); may be changed by the device's driver. * The spi_transfer.speed_hz can override this for each transfer. @@ -160,6 +163,9 @@ struct spi_device { struct device dev; struct spi_controller *controller; struct spi_controller *master; /* compatibility layer */ +#ifdef CONFIG_SPI_DAISY_CHAIN + struct list_head *daisy_chain_devs; +#endif u32 max_speed_hz; u8 chip_select; u8 bits_per_word; @@ -945,6 +951,11 @@ struct spi_transfer { /** * struct spi_message - one multi-segment SPI transaction * @transfers: list of transfer segments in this transaction + * @daisy_chain_transfers: head of the original transfers queue. + * Handled by the SPI daisy chain driver. + * @daisy_chain_new_transfer: pointer to an extra SPI transfer, + * in case it had to be created. + * Handled by the SPI daisy chain driver. * @spi: SPI device to which the transaction is queued * @is_dma_mapped: if true, the caller provided both dma and cpu virtual * addresses for each transfer buffer @@ -975,7 +986,11 @@ struct spi_transfer { struct spi_message { struct list_head transfers; - struct spi_device *spi; +#ifdef CONFIG_SPI_DAISY_CHAIN + struct list_head daisy_chain_transfers; + struct spi_transfer *daisy_chain_new_transfer; +#endif + struct spi_device *spi; unsigned is_dma_mapped:1; -- 2.27.0