root/drivers/media/platform/nxp/imx8-isi/imx8-isi-core.c
// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright 2019-2020 NXP
 */

#include <linux/bits.h>
#include <linux/clk.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/mfd/syscon.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/pm.h>
#include <linux/pm_runtime.h>
#include <linux/property.h>
#include <linux/slab.h>
#include <linux/string.h>
#include <linux/types.h>

#include <media/media-device.h>
#include <media/v4l2-async.h>
#include <media/v4l2-device.h>
#include <media/v4l2-mc.h>

#include "imx8-isi-core.h"

/* -----------------------------------------------------------------------------
 * V4L2 async subdevs
 */

struct mxc_isi_async_subdev {
        struct v4l2_async_connection asd;
        unsigned int port;
};

static inline struct mxc_isi_async_subdev *
asd_to_mxc_isi_async_subdev(struct v4l2_async_connection *asd)
{
        return container_of(asd, struct mxc_isi_async_subdev, asd);
};

static inline struct mxc_isi_dev *
notifier_to_mxc_isi_dev(struct v4l2_async_notifier *n)
{
        return container_of(n, struct mxc_isi_dev, notifier);
};

static int mxc_isi_async_notifier_bound(struct v4l2_async_notifier *notifier,
                                        struct v4l2_subdev *sd,
                                        struct v4l2_async_connection *asc)
{
        const unsigned int link_flags = MEDIA_LNK_FL_IMMUTABLE
                                      | MEDIA_LNK_FL_ENABLED;
        struct mxc_isi_dev *isi = notifier_to_mxc_isi_dev(notifier);
        struct mxc_isi_async_subdev *masd = asd_to_mxc_isi_async_subdev(asc);
        struct media_pad *pad = &isi->crossbar.pads[masd->port];
        struct device_link *link;

        dev_dbg(isi->dev, "Bound subdev %s to crossbar input %u\n", sd->name,
                masd->port);

        /*
         * Enforce suspend/resume ordering between the source (supplier) and
         * the ISI (consumer). The source will be suspended before and resume
         * after the ISI.
         */
        link = device_link_add(isi->dev, sd->dev, DL_FLAG_STATELESS);
        if (!link) {
                dev_err(isi->dev,
                        "Failed to create device link to source %s\n", sd->name);
                return -EINVAL;
        }

        return v4l2_create_fwnode_links_to_pad(sd, pad, link_flags);
}

static int mxc_isi_async_notifier_complete(struct v4l2_async_notifier *notifier)
{
        struct mxc_isi_dev *isi = notifier_to_mxc_isi_dev(notifier);
        int ret;

        dev_dbg(isi->dev, "All subdevs bound\n");

        ret = v4l2_device_register_subdev_nodes(&isi->v4l2_dev);
        if (ret < 0) {
                dev_err(isi->dev,
                        "Failed to register subdev nodes: %d\n", ret);
                return ret;
        }

        return media_device_register(&isi->media_dev);
}

static const struct v4l2_async_notifier_operations mxc_isi_async_notifier_ops = {
        .bound = mxc_isi_async_notifier_bound,
        .complete = mxc_isi_async_notifier_complete,
};

static int mxc_isi_pipe_register(struct mxc_isi_pipe *pipe)
{
        int ret;

        ret = v4l2_device_register_subdev(&pipe->isi->v4l2_dev, &pipe->sd);
        if (ret < 0)
                return ret;

        return mxc_isi_video_register(pipe, &pipe->isi->v4l2_dev);
}

static void mxc_isi_pipe_unregister(struct mxc_isi_pipe *pipe)
{
        mxc_isi_video_unregister(pipe);
}

static int mxc_isi_v4l2_init(struct mxc_isi_dev *isi)
{
        struct fwnode_handle *node = dev_fwnode(isi->dev);
        struct media_device *media_dev = &isi->media_dev;
        struct v4l2_device *v4l2_dev = &isi->v4l2_dev;
        unsigned int i;
        int ret;

        /* Initialize the media device. */
        strscpy(media_dev->model, "FSL Capture Media Device",
                sizeof(media_dev->model));
        media_dev->dev = isi->dev;

        media_device_init(media_dev);

        /* Initialize and register the V4L2 device. */
        v4l2_dev->mdev = media_dev;
        strscpy(v4l2_dev->name, "mx8-img-md", sizeof(v4l2_dev->name));

        ret = v4l2_device_register(isi->dev, v4l2_dev);
        if (ret < 0) {
                dev_err(isi->dev,
                        "Failed to register V4L2 device: %d\n", ret);
                goto err_media;
        }

        /* Register the crossbar switch subdev. */
        ret = mxc_isi_crossbar_register(&isi->crossbar);
        if (ret < 0) {
                dev_err(isi->dev, "Failed to register crossbar: %d\n", ret);
                goto err_v4l2;
        }

        /* Register the pipeline subdevs and link them to the crossbar switch. */
        for (i = 0; i < isi->pdata->num_channels; ++i) {
                struct mxc_isi_pipe *pipe = &isi->pipes[i];

                ret = mxc_isi_pipe_register(pipe);
                if (ret < 0) {
                        dev_err(isi->dev, "Failed to register pipe%u: %d\n", i,
                                ret);
                        goto err_v4l2;
                }

                ret = media_create_pad_link(&isi->crossbar.sd.entity,
                                            isi->crossbar.num_sinks + i,
                                            &pipe->sd.entity,
                                            MXC_ISI_PIPE_PAD_SINK,
                                            MEDIA_LNK_FL_IMMUTABLE |
                                            MEDIA_LNK_FL_ENABLED);
                if (ret < 0)
                        goto err_v4l2;
        }

        /* Register the M2M device. */
        ret = mxc_isi_m2m_register(isi, v4l2_dev);
        if (ret < 0) {
                dev_err(isi->dev, "Failed to register M2M device: %d\n", ret);
                goto err_v4l2;
        }

        /* Initialize, fill and register the async notifier. */
        v4l2_async_nf_init(&isi->notifier, v4l2_dev);
        isi->notifier.ops = &mxc_isi_async_notifier_ops;

        for (i = 0; i < isi->pdata->num_ports; ++i) {
                struct mxc_isi_async_subdev *masd;
                struct fwnode_handle *ep;

                ep = fwnode_graph_get_endpoint_by_id(node, i, 0,
                                                     FWNODE_GRAPH_ENDPOINT_NEXT);

                if (!ep)
                        continue;

                masd = v4l2_async_nf_add_fwnode_remote(&isi->notifier, ep,
                                                       struct mxc_isi_async_subdev);
                fwnode_handle_put(ep);

                if (IS_ERR(masd)) {
                        ret = PTR_ERR(masd);
                        goto err_m2m;
                }

                masd->port = i;
        }

        ret = v4l2_async_nf_register(&isi->notifier);
        if (ret < 0) {
                dev_err(isi->dev,
                        "Failed to register async notifier: %d\n", ret);
                goto err_m2m;
        }

        return 0;

err_m2m:
        mxc_isi_m2m_unregister(isi);
        v4l2_async_nf_cleanup(&isi->notifier);
err_v4l2:
        v4l2_device_unregister(v4l2_dev);
err_media:
        media_device_cleanup(media_dev);
        return ret;
}

static void mxc_isi_v4l2_cleanup(struct mxc_isi_dev *isi)
{
        unsigned int i;

        v4l2_async_nf_unregister(&isi->notifier);
        v4l2_async_nf_cleanup(&isi->notifier);

        v4l2_device_unregister(&isi->v4l2_dev);
        media_device_unregister(&isi->media_dev);

        mxc_isi_m2m_unregister(isi);

        for (i = 0; i < isi->pdata->num_channels; ++i)
                mxc_isi_pipe_unregister(&isi->pipes[i]);

        mxc_isi_crossbar_unregister(&isi->crossbar);

        media_device_cleanup(&isi->media_dev);
}

/* -----------------------------------------------------------------------------
 * Device information
 */

/* Panic will assert when the buffers are 50% full */

/* For i.MX8MN ISI IER version */
static const struct mxc_isi_ier_reg mxc_imx8_isi_ier_v1 = {
        .oflw_y_buf_en = { .mask = BIT(19) },
        .oflw_u_buf_en = { .mask = BIT(21) },
        .oflw_v_buf_en = { .mask = BIT(23) },

        .panic_y_buf_en = { .mask = BIT(20) },
        .panic_u_buf_en = { .mask = BIT(22) },
        .panic_v_buf_en = { .mask = BIT(24) },
};

/* For i.MX8QXP C0 and i.MX8MP ISI IER version */
static const struct mxc_isi_ier_reg mxc_imx8_isi_ier_v2 = {
        .oflw_y_buf_en = { .mask = BIT(18) },
        .oflw_u_buf_en = { .mask = BIT(20) },
        .oflw_v_buf_en = { .mask = BIT(22) },

        .panic_y_buf_en = { .mask = BIT(19) },
        .panic_u_buf_en = { .mask = BIT(21) },
        .panic_v_buf_en = { .mask = BIT(23) },
};

/* For i.MX8QM ISI IER version */
static const struct mxc_isi_ier_reg mxc_imx8_isi_ier_qm = {
        .oflw_y_buf_en = { .mask = BIT(16) },
        .oflw_u_buf_en = { .mask = BIT(19) },
        .oflw_v_buf_en = { .mask = BIT(22) },

        .excs_oflw_y_buf_en = { .mask = BIT(17) },
        .excs_oflw_u_buf_en = { .mask = BIT(20) },
        .excs_oflw_v_buf_en = { .mask = BIT(23) },

        .panic_y_buf_en = { .mask = BIT(18) },
        .panic_u_buf_en = { .mask = BIT(21) },
        .panic_v_buf_en = { .mask = BIT(24) },
};

/* Panic will assert when the buffers are 50% full */
static const struct mxc_isi_set_thd mxc_imx8_isi_thd_v1 = {
        .panic_set_thd_y = { .mask = 0x0000f, .offset = 0,  .threshold = 0x7 },
        .panic_set_thd_u = { .mask = 0x00f00, .offset = 8,  .threshold = 0x7 },
        .panic_set_thd_v = { .mask = 0xf0000, .offset = 16, .threshold = 0x7 },
};

static const struct mxc_isi_plat_data mxc_imx8mn_data = {
        .model                  = MXC_ISI_IMX8MN,
        .num_ports              = 1,
        .num_channels           = 1,
        .reg_offset             = 0,
        .ier_reg                = &mxc_imx8_isi_ier_v1,
        .set_thd                = &mxc_imx8_isi_thd_v1,
        .buf_active_reverse     = false,
        .gasket_ops             = &mxc_imx8_gasket_ops,
        .has_36bit_dma          = false,
};

static const struct mxc_isi_plat_data mxc_imx8mp_data = {
        .model                  = MXC_ISI_IMX8MP,
        .num_ports              = 2,
        .num_channels           = 2,
        .reg_offset             = 0x2000,
        .ier_reg                = &mxc_imx8_isi_ier_v2,
        .set_thd                = &mxc_imx8_isi_thd_v1,
        .buf_active_reverse     = true,
        .gasket_ops             = &mxc_imx8_gasket_ops,
        .has_36bit_dma          = true,
};

static const struct mxc_isi_plat_data mxc_imx8qm_data = {
        .model                  = MXC_ISI_IMX8QM,
        .num_ports              = 5,
        .num_channels           = 8,
        .reg_offset             = 0x10000,
        .ier_reg                = &mxc_imx8_isi_ier_qm,
        .set_thd                = &mxc_imx8_isi_thd_v1,
        .buf_active_reverse     = true,
        .has_36bit_dma          = false,
};

static const struct mxc_isi_plat_data mxc_imx8qxp_data = {
        .model                  = MXC_ISI_IMX8QXP,
        .num_ports              = 5,
        .num_channels           = 6,
        .reg_offset             = 0x10000,
        .ier_reg                = &mxc_imx8_isi_ier_v2,
        .set_thd                = &mxc_imx8_isi_thd_v1,
        .buf_active_reverse     = true,
        .has_36bit_dma          = false,
};

static const struct mxc_isi_plat_data mxc_imx8ulp_data = {
        .model                  = MXC_ISI_IMX8ULP,
        .num_ports              = 1,
        .num_channels           = 1,
        .reg_offset             = 0x0,
        .ier_reg                = &mxc_imx8_isi_ier_v2,
        .set_thd                = &mxc_imx8_isi_thd_v1,
        .buf_active_reverse     = true,
        .has_36bit_dma          = false,
};

static const struct mxc_isi_plat_data mxc_imx91_data = {
        .model                  = MXC_ISI_IMX91,
        .num_ports              = 1,
        .num_channels           = 1,
        .reg_offset             = 0,
        .ier_reg                = &mxc_imx8_isi_ier_v2,
        .set_thd                = &mxc_imx8_isi_thd_v1,
        .buf_active_reverse     = true,
        .has_36bit_dma          = false,
};

static const struct mxc_isi_plat_data mxc_imx93_data = {
        .model                  = MXC_ISI_IMX93,
        .num_ports              = 1,
        .num_channels           = 1,
        .reg_offset             = 0,
        .ier_reg                = &mxc_imx8_isi_ier_v2,
        .set_thd                = &mxc_imx8_isi_thd_v1,
        .buf_active_reverse     = true,
        .gasket_ops             = &mxc_imx93_gasket_ops,
        .has_36bit_dma          = false,
};

/* -----------------------------------------------------------------------------
 * Power management
 */

static int mxc_isi_pm_suspend(struct device *dev)
{
        struct mxc_isi_dev *isi = dev_get_drvdata(dev);
        unsigned int i;

        for (i = 0; i < isi->pdata->num_channels; ++i) {
                struct mxc_isi_pipe *pipe = &isi->pipes[i];

                mxc_isi_video_suspend(pipe);
        }

        mxc_isi_m2m_suspend(&isi->m2m);

        return pm_runtime_force_suspend(dev);
}

static int mxc_isi_pm_resume(struct device *dev)
{
        struct mxc_isi_dev *isi = dev_get_drvdata(dev);
        unsigned int i;
        int err = 0;
        int ret;

        ret = pm_runtime_force_resume(dev);
        if (ret < 0)
                return ret;

        for (i = 0; i < isi->pdata->num_channels; ++i) {
                struct mxc_isi_pipe *pipe = &isi->pipes[i];

                ret = mxc_isi_video_resume(pipe);
                if (ret) {
                        dev_err(dev, "Failed to resume pipeline %u (%d)\n", i,
                                ret);
                        /*
                         * Record the last error as it's as meaningful as any,
                         * and continue resuming the other pipelines.
                         */
                        err = ret;
                }
        }

        ret = mxc_isi_m2m_resume(&isi->m2m);
        if (ret) {
                dev_err(dev, "Failed to resume ISI (%d) for m2m\n", ret);
                err = ret;
        }

        return err;
}

static int mxc_isi_runtime_suspend(struct device *dev)
{
        struct mxc_isi_dev *isi = dev_get_drvdata(dev);

        clk_bulk_disable_unprepare(isi->num_clks, isi->clks);

        return 0;
}

static int mxc_isi_runtime_resume(struct device *dev)
{
        struct mxc_isi_dev *isi = dev_get_drvdata(dev);
        int ret;

        ret = clk_bulk_prepare_enable(isi->num_clks, isi->clks);
        if (ret) {
                dev_err(dev, "Failed to enable clocks (%d)\n", ret);
                return ret;
        }

        return 0;
}

static const struct dev_pm_ops mxc_isi_pm_ops = {
        SYSTEM_SLEEP_PM_OPS(mxc_isi_pm_suspend, mxc_isi_pm_resume)
        RUNTIME_PM_OPS(mxc_isi_runtime_suspend, mxc_isi_runtime_resume, NULL)
};

/* -----------------------------------------------------------------------------
 * Probe, remove & driver
 */

static int mxc_isi_probe(struct platform_device *pdev)
{
        struct device *dev = &pdev->dev;
        struct mxc_isi_dev *isi;
        unsigned int dma_size;
        unsigned int i;
        int ret = 0;

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

        isi->dev = dev;
        platform_set_drvdata(pdev, isi);

        isi->pdata = of_device_get_match_data(dev);

        isi->pipes = kzalloc_objs(isi->pipes[0], isi->pdata->num_channels);
        if (!isi->pipes)
                return -ENOMEM;

        isi->num_clks = devm_clk_bulk_get_all(dev, &isi->clks);
        if (isi->num_clks < 0)
                return dev_err_probe(dev, isi->num_clks, "Failed to get clocks\n");

        isi->regs = devm_platform_ioremap_resource(pdev, 0);
        if (IS_ERR(isi->regs))
                return dev_err_probe(dev, PTR_ERR(isi->regs),
                                     "Failed to get ISI register map\n");

        if (isi->pdata->gasket_ops) {
                isi->gasket = syscon_regmap_lookup_by_phandle(dev->of_node,
                                                              "fsl,blk-ctrl");
                if (IS_ERR(isi->gasket))
                        return dev_err_probe(dev, PTR_ERR(isi->gasket),
                                             "failed to get gasket\n");
        }

        dma_size = isi->pdata->has_36bit_dma ? 36 : 32;
        dma_set_mask_and_coherent(dev, DMA_BIT_MASK(dma_size));

        ret = devm_pm_runtime_enable(dev);
        if (ret)
                return ret;

        ret = mxc_isi_crossbar_init(isi);
        if (ret)
                return dev_err_probe(dev, ret,
                                     "Failed to initialize crossbar\n");

        for (i = 0; i < isi->pdata->num_channels; ++i) {
                ret = mxc_isi_pipe_init(isi, i);
                if (ret < 0) {
                        dev_err(dev, "Failed to initialize pipe%u: %d\n", i,
                                ret);
                        goto err_xbar;
                }
        }

        ret = mxc_isi_v4l2_init(isi);
        if (ret < 0) {
                dev_err(dev, "Failed to initialize V4L2: %d\n", ret);
                goto err_xbar;
        }

        mxc_isi_debug_init(isi);

        return 0;

err_xbar:
        mxc_isi_crossbar_cleanup(&isi->crossbar);

        return ret;
}

static void mxc_isi_remove(struct platform_device *pdev)
{
        struct mxc_isi_dev *isi = platform_get_drvdata(pdev);
        unsigned int i;

        mxc_isi_debug_cleanup(isi);

        for (i = 0; i < isi->pdata->num_channels; ++i) {
                struct mxc_isi_pipe *pipe = &isi->pipes[i];

                mxc_isi_pipe_cleanup(pipe);
        }

        mxc_isi_crossbar_cleanup(&isi->crossbar);
        mxc_isi_v4l2_cleanup(isi);
}

static const struct of_device_id mxc_isi_of_match[] = {
        { .compatible = "fsl,imx8mn-isi", .data = &mxc_imx8mn_data },
        { .compatible = "fsl,imx8mp-isi", .data = &mxc_imx8mp_data },
        { .compatible = "fsl,imx8qm-isi", .data = &mxc_imx8qm_data },
        { .compatible = "fsl,imx8qxp-isi", .data = &mxc_imx8qxp_data },
        { .compatible = "fsl,imx8ulp-isi", .data = &mxc_imx8ulp_data },
        { .compatible = "fsl,imx91-isi", .data = &mxc_imx91_data },
        { .compatible = "fsl,imx93-isi", .data = &mxc_imx93_data },
        { /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, mxc_isi_of_match);

static struct platform_driver mxc_isi_driver = {
        .probe          = mxc_isi_probe,
        .remove         = mxc_isi_remove,
        .driver = {
                .of_match_table = mxc_isi_of_match,
                .name           = MXC_ISI_DRIVER_NAME,
                .pm             = pm_ptr(&mxc_isi_pm_ops),
        }
};
module_platform_driver(mxc_isi_driver);

MODULE_AUTHOR("Freescale Semiconductor, Inc.");
MODULE_DESCRIPTION("IMX8 Image Sensing Interface driver");
MODULE_LICENSE("GPL");