root/drivers/spi/spi-offload.c
// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (C) 2024 Analog Devices Inc.
 * Copyright (C) 2024 BayLibre, SAS
 */

/*
 * SPI Offloading support.
 *
 * Some SPI controllers support offloading of SPI transfers. Essentially, this
 * is the ability for a SPI controller to perform SPI transfers with minimal
 * or even no CPU intervention, e.g. via a specialized SPI controller with a
 * hardware trigger or via a conventional SPI controller using a non-Linux MCU
 * processor core to offload the work.
 */

#define DEFAULT_SYMBOL_NAMESPACE "SPI_OFFLOAD"

#include <linux/cleanup.h>
#include <linux/device.h>
#include <linux/dmaengine.h>
#include <linux/export.h>
#include <linux/kref.h>
#include <linux/list.h>
#include <linux/mutex.h>
#include <linux/of.h>
#include <linux/property.h>
#include <linux/spi/offload/consumer.h>
#include <linux/spi/offload/provider.h>
#include <linux/spi/offload/types.h>
#include <linux/spi/spi.h>
#include <linux/types.h>

struct spi_controller_and_offload {
        struct spi_controller *controller;
        struct spi_offload *offload;
};

struct spi_offload_trigger {
        struct list_head list;
        struct kref ref;
        struct fwnode_handle *fwnode;
        /* synchronizes calling ops and driver registration */
        struct mutex lock;
        /*
         * If the provider goes away while the consumer still has a reference,
         * ops and priv will be set to NULL and all calls will fail with -ENODEV.
         */
        const struct spi_offload_trigger_ops *ops;
        void *priv;
};

static LIST_HEAD(spi_offload_triggers);
static DEFINE_MUTEX(spi_offload_triggers_lock);

/**
 * devm_spi_offload_alloc() - Allocate offload instance
 * @dev: Device for devm purposes and assigned to &struct spi_offload.provider_dev
 * @priv_size: Size of private data to allocate
 *
 * Offload providers should use this to allocate offload instances.
 *
 * Return: Pointer to new offload instance or error on failure.
 */
struct spi_offload *devm_spi_offload_alloc(struct device *dev,
                                           size_t priv_size)
{
        struct spi_offload *offload;
        void *priv;

        offload = devm_kzalloc(dev, sizeof(*offload), GFP_KERNEL);
        if (!offload)
                return ERR_PTR(-ENOMEM);

        priv = devm_kzalloc(dev, priv_size, GFP_KERNEL);
        if (!priv)
                return ERR_PTR(-ENOMEM);

        offload->provider_dev = dev;
        offload->priv = priv;

        return offload;
}
EXPORT_SYMBOL_GPL(devm_spi_offload_alloc);

static void spi_offload_put(void *data)
{
        struct spi_controller_and_offload *resource = data;

        resource->controller->put_offload(resource->offload);
        kfree(resource);
}

/**
 * devm_spi_offload_get() - Get an offload instance
 * @dev: Device for devm purposes
 * @spi: SPI device to use for the transfers
 * @config: Offload configuration
 *
 * Peripheral drivers call this function to get an offload instance that meets
 * the requirements specified in @config. If no suitable offload instance is
 * available, -ENODEV is returned.
 *
 * Return: Offload instance or error on failure.
 */
struct spi_offload *devm_spi_offload_get(struct device *dev,
                                         struct spi_device *spi,
                                         const struct spi_offload_config *config)
{
        struct spi_controller_and_offload *resource;
        struct spi_offload *offload;
        int ret;

        if (!spi || !config)
                return ERR_PTR(-EINVAL);

        if (!spi->controller->get_offload)
                return ERR_PTR(-ENODEV);

        resource = kzalloc_obj(*resource);
        if (!resource)
                return ERR_PTR(-ENOMEM);

        offload = spi->controller->get_offload(spi, config);
        if (IS_ERR(offload)) {
                kfree(resource);
                return offload;
        }

        resource->controller = spi->controller;
        resource->offload = offload;

        ret = devm_add_action_or_reset(dev, spi_offload_put, resource);
        if (ret)
                return ERR_PTR(ret);

        return offload;
}
EXPORT_SYMBOL_GPL(devm_spi_offload_get);

static void spi_offload_trigger_free(struct kref *ref)
{
        struct spi_offload_trigger *trigger =
                container_of(ref, struct spi_offload_trigger, ref);

        mutex_destroy(&trigger->lock);
        fwnode_handle_put(trigger->fwnode);
        kfree(trigger);
}

static void spi_offload_trigger_put(void *data)
{
        struct spi_offload_trigger *trigger = data;

        scoped_guard(mutex, &trigger->lock)
                if (trigger->ops && trigger->ops->release)
                        trigger->ops->release(trigger);

        kref_put(&trigger->ref, spi_offload_trigger_free);
}

static struct spi_offload_trigger
*spi_offload_trigger_get(enum spi_offload_trigger_type type,
                         struct fwnode_reference_args *args)
{
        struct spi_offload_trigger *trigger;
        bool match = false;
        int ret;

        guard(mutex)(&spi_offload_triggers_lock);

        list_for_each_entry(trigger, &spi_offload_triggers, list) {
                if (trigger->fwnode != args->fwnode)
                        continue;

                match = trigger->ops->match(trigger, type, args->args, args->nargs);
                if (match)
                        break;
        }

        if (!match)
                return ERR_PTR(-EPROBE_DEFER);

        guard(mutex)(&trigger->lock);

        if (trigger->ops->request) {
                ret = trigger->ops->request(trigger, type, args->args, args->nargs);
                if (ret)
                        return ERR_PTR(ret);
        }

        kref_get(&trigger->ref);

        return trigger;
}

/**
 * devm_spi_offload_trigger_get() - Get an offload trigger instance
 * @dev: Device for devm purposes.
 * @offload: Offload instance connected to a trigger.
 * @type: Trigger type to get.
 *
 * Return: Offload trigger instance or error on failure.
 */
struct spi_offload_trigger
*devm_spi_offload_trigger_get(struct device *dev,
                              struct spi_offload *offload,
                              enum spi_offload_trigger_type type)
{
        struct spi_offload_trigger *trigger;
        struct fwnode_reference_args args;
        int ret;

        ret = fwnode_property_get_reference_args(dev_fwnode(offload->provider_dev),
                                                 "trigger-sources",
                                                 "#trigger-source-cells", 0, 0,
                                                 &args);
        if (ret)
                return ERR_PTR(ret);

        trigger = spi_offload_trigger_get(type, &args);
        fwnode_handle_put(args.fwnode);
        if (IS_ERR(trigger))
                return trigger;

        ret = devm_add_action_or_reset(dev, spi_offload_trigger_put, trigger);
        if (ret)
                return ERR_PTR(ret);

        return trigger;
}
EXPORT_SYMBOL_GPL(devm_spi_offload_trigger_get);

/**
 * spi_offload_trigger_validate - Validate the requested trigger
 * @trigger: Offload trigger instance
 * @config: Trigger config to validate
 *
 * On success, @config may be modifed to reflect what the hardware can do.
 * For example, the frequency of a periodic trigger may be adjusted to the
 * nearest supported value.
 *
 * Callers will likely need to do additional validation of the modified trigger
 * parameters.
 *
 * Return: 0 on success, negative error code on failure.
 */
int spi_offload_trigger_validate(struct spi_offload_trigger *trigger,
                                 struct spi_offload_trigger_config *config)
{
        guard(mutex)(&trigger->lock);

        if (!trigger->ops)
                return -ENODEV;

        if (!trigger->ops->validate)
                return -EOPNOTSUPP;

        return trigger->ops->validate(trigger, config);
}
EXPORT_SYMBOL_GPL(spi_offload_trigger_validate);

/**
 * spi_offload_trigger_enable - enables trigger for offload
 * @offload: Offload instance
 * @trigger: Offload trigger instance
 * @config: Trigger config to validate
 *
 * There must be a prepared offload instance with the specified ID (i.e.
 * spi_optimize_message() was called with the same offload assigned to the
 * message). This will also reserve the bus for exclusive use by the offload
 * instance until the trigger is disabled. Any other attempts to send a
 * transfer or lock the bus will fail with -EBUSY during this time.
 *
 * Calls must be balanced with spi_offload_trigger_disable().
 *
 * Context: can sleep
 * Return: 0 on success, else a negative error code.
 */
int spi_offload_trigger_enable(struct spi_offload *offload,
                               struct spi_offload_trigger *trigger,
                               struct spi_offload_trigger_config *config)
{
        int ret;

        guard(mutex)(&trigger->lock);

        if (!trigger->ops)
                return -ENODEV;

        if (offload->ops && offload->ops->trigger_enable) {
                ret = offload->ops->trigger_enable(offload);
                if (ret)
                        return ret;
        }

        if (trigger->ops->enable) {
                ret = trigger->ops->enable(trigger, config);
                if (ret) {
                        if (offload->ops && offload->ops->trigger_disable)
                                offload->ops->trigger_disable(offload);
                        return ret;
                }
        }

        return 0;
}
EXPORT_SYMBOL_GPL(spi_offload_trigger_enable);

/**
 * spi_offload_trigger_disable - disables hardware trigger for offload
 * @offload: Offload instance
 * @trigger: Offload trigger instance
 *
 * Disables the hardware trigger for the offload instance with the specified ID
 * and releases the bus for use by other clients.
 *
 * Context: can sleep
 */
void spi_offload_trigger_disable(struct spi_offload *offload,
                                 struct spi_offload_trigger *trigger)
{
        if (offload->ops && offload->ops->trigger_disable)
                offload->ops->trigger_disable(offload);

        guard(mutex)(&trigger->lock);

        if (!trigger->ops)
                return;

        if (trigger->ops->disable)
                trigger->ops->disable(trigger);
}
EXPORT_SYMBOL_GPL(spi_offload_trigger_disable);

static void spi_offload_release_dma_chan(void *chan)
{
        dma_release_channel(chan);
}

/**
 * devm_spi_offload_tx_stream_request_dma_chan - Get the DMA channel info for the TX stream
 * @dev: Device for devm purposes.
 * @offload: Offload instance
 *
 * This is the DMA channel that will provide data to transfers that use the
 * %SPI_OFFLOAD_XFER_TX_STREAM offload flag.
 *
 * Return: Pointer to DMA channel info, or negative error code
 */
struct dma_chan
*devm_spi_offload_tx_stream_request_dma_chan(struct device *dev,
                                             struct spi_offload *offload)
{
        struct dma_chan *chan;
        int ret;

        if (!offload->ops || !offload->ops->tx_stream_request_dma_chan)
                return ERR_PTR(-EOPNOTSUPP);

        chan = offload->ops->tx_stream_request_dma_chan(offload);
        if (IS_ERR(chan))
                return chan;

        ret = devm_add_action_or_reset(dev, spi_offload_release_dma_chan, chan);
        if (ret)
                return ERR_PTR(ret);

        return chan;
}
EXPORT_SYMBOL_GPL(devm_spi_offload_tx_stream_request_dma_chan);

/**
 * devm_spi_offload_rx_stream_request_dma_chan - Get the DMA channel info for the RX stream
 * @dev: Device for devm purposes.
 * @offload: Offload instance
 *
 * This is the DMA channel that will receive data from transfers that use the
 * %SPI_OFFLOAD_XFER_RX_STREAM offload flag.
 *
 * Return: Pointer to DMA channel info, or negative error code
 */
struct dma_chan
*devm_spi_offload_rx_stream_request_dma_chan(struct device *dev,
                                             struct spi_offload *offload)
{
        struct dma_chan *chan;
        int ret;

        if (!offload->ops || !offload->ops->rx_stream_request_dma_chan)
                return ERR_PTR(-EOPNOTSUPP);

        chan = offload->ops->rx_stream_request_dma_chan(offload);
        if (IS_ERR(chan))
                return chan;

        ret = devm_add_action_or_reset(dev, spi_offload_release_dma_chan, chan);
        if (ret)
                return ERR_PTR(ret);

        return chan;
}
EXPORT_SYMBOL_GPL(devm_spi_offload_rx_stream_request_dma_chan);

/* Triggers providers */

static void spi_offload_trigger_unregister(void *data)
{
        struct spi_offload_trigger *trigger = data;

        scoped_guard(mutex, &spi_offload_triggers_lock)
                list_del(&trigger->list);

        scoped_guard(mutex, &trigger->lock) {
                trigger->priv = NULL;
                trigger->ops = NULL;
        }

        kref_put(&trigger->ref, spi_offload_trigger_free);
}

/**
 * devm_spi_offload_trigger_register() - Allocate and register an offload trigger
 * @dev: Device for devm purposes.
 * @info: Provider-specific trigger info.
 *
 * Return: 0 on success, else a negative error code.
 */
int devm_spi_offload_trigger_register(struct device *dev,
                                      struct spi_offload_trigger_info *info)
{
        struct spi_offload_trigger *trigger;

        if (!info->fwnode || !info->ops || !info->ops->match)
                return -EINVAL;

        trigger = kzalloc(sizeof(*trigger), GFP_KERNEL);
        if (!trigger)
                return -ENOMEM;

        kref_init(&trigger->ref);
        mutex_init(&trigger->lock);
        trigger->fwnode = fwnode_handle_get(info->fwnode);
        trigger->ops = info->ops;
        trigger->priv = info->priv;

        scoped_guard(mutex, &spi_offload_triggers_lock)
                list_add_tail(&trigger->list, &spi_offload_triggers);

        return devm_add_action_or_reset(dev, spi_offload_trigger_unregister, trigger);
}
EXPORT_SYMBOL_GPL(devm_spi_offload_trigger_register);

/**
 * spi_offload_trigger_get_priv() - Get the private data for the trigger
 *
 * @trigger: Offload trigger instance.
 *
 * Return: Private data for the trigger.
 */
void *spi_offload_trigger_get_priv(struct spi_offload_trigger *trigger)
{
        return trigger->priv;
}
EXPORT_SYMBOL_GPL(spi_offload_trigger_get_priv);