root/drivers/media/platform/amlogic/c3/isp/c3-isp-dev.c
// SPDX-License-Identifier: (GPL-2.0-only OR MIT)
/*
 * Copyright (C) 2024 Amlogic, Inc. All rights reserved
 */

#include <linux/clk.h>
#include <linux/device.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>

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

#include "c3-isp-common.h"
#include "c3-isp-regs.h"

u32 c3_isp_read(struct c3_isp_device *isp, u32 reg)
{
        return readl(isp->base + reg);
}

void c3_isp_write(struct c3_isp_device *isp, u32 reg, u32 val)
{
        writel(val, isp->base + reg);
}

void c3_isp_update_bits(struct c3_isp_device *isp, u32 reg, u32 mask, u32 val)
{
        u32 orig, tmp;

        orig = c3_isp_read(isp, reg);

        tmp = orig & ~mask;
        tmp |= val & mask;

        if (tmp != orig)
                c3_isp_write(isp, reg, tmp);
}

/* PM runtime suspend */
static int c3_isp_runtime_suspend(struct device *dev)
{
        struct c3_isp_device *isp = dev_get_drvdata(dev);

        clk_bulk_disable_unprepare(isp->info->clock_num, isp->clks);

        return 0;
}

/* PM runtime resume */
static int c3_isp_runtime_resume(struct device *dev)
{
        struct c3_isp_device *isp = dev_get_drvdata(dev);

        return clk_bulk_prepare_enable(isp->info->clock_num, isp->clks);
}

static const struct dev_pm_ops c3_isp_pm_ops = {
        SYSTEM_SLEEP_PM_OPS(pm_runtime_force_suspend,
                            pm_runtime_force_resume)
        RUNTIME_PM_OPS(c3_isp_runtime_suspend,
                       c3_isp_runtime_resume, NULL)
};

/* IRQ handling */
static irqreturn_t c3_isp_irq_handler(int irq, void *dev)
{
        struct c3_isp_device *isp = dev;
        u32 status;

        /* Get irq status and clear irq status */
        status = c3_isp_read(isp, ISP_TOP_RO_IRQ_STAT);
        c3_isp_write(isp, ISP_TOP_IRQ_CLR, status);

        if (status & ISP_TOP_RO_IRQ_STAT_FRM_END_MASK) {
                c3_isp_stats_isr(isp);
                c3_isp_params_isr(isp);
                c3_isp_captures_isr(isp);
                isp->frm_sequence++;
        }

        if (status & ISP_TOP_RO_IRQ_STAT_FRM_RST_MASK)
                c3_isp_core_queue_sof(isp);

        return IRQ_HANDLED;
}

/* Subdev notifier register */
static int c3_isp_notify_bound(struct v4l2_async_notifier *notifier,
                               struct v4l2_subdev *sd,
                               struct v4l2_async_connection *asc)
{
        struct c3_isp_device *isp =
                container_of(notifier, struct c3_isp_device, notifier);
        struct media_pad *sink =
                &isp->core.sd.entity.pads[C3_ISP_CORE_PAD_SINK_VIDEO];

        return v4l2_create_fwnode_links_to_pad(sd, sink, MEDIA_LNK_FL_ENABLED |
                                               MEDIA_LNK_FL_IMMUTABLE);
}

static int c3_isp_notify_complete(struct v4l2_async_notifier *notifier)
{
        struct c3_isp_device *isp =
                container_of(notifier, struct c3_isp_device, notifier);

        return v4l2_device_register_subdev_nodes(&isp->v4l2_dev);
}

static const struct v4l2_async_notifier_operations c3_isp_notify_ops = {
        .bound = c3_isp_notify_bound,
        .complete = c3_isp_notify_complete,
};

static int c3_isp_async_nf_register(struct c3_isp_device *isp)
{
        struct v4l2_async_connection *asc;
        struct fwnode_handle *ep;
        int ret;

        v4l2_async_nf_init(&isp->notifier, &isp->v4l2_dev);

        ep = fwnode_graph_get_endpoint_by_id(dev_fwnode(isp->dev), 0, 0,
                                             FWNODE_GRAPH_ENDPOINT_NEXT);
        if (!ep)
                return -ENOTCONN;

        asc = v4l2_async_nf_add_fwnode_remote(&isp->notifier, ep,
                                              struct v4l2_async_connection);
        fwnode_handle_put(ep);

        if (IS_ERR(asc))
                return PTR_ERR(asc);

        isp->notifier.ops = &c3_isp_notify_ops;
        ret = v4l2_async_nf_register(&isp->notifier);
        if (ret)
                v4l2_async_nf_cleanup(&isp->notifier);

        return ret;
}

static void c3_isp_async_nf_unregister(struct c3_isp_device *isp)
{
        v4l2_async_nf_unregister(&isp->notifier);
        v4l2_async_nf_cleanup(&isp->notifier);
}

static int c3_isp_media_register(struct c3_isp_device *isp)
{
        struct media_device *media_dev = &isp->media_dev;
        struct v4l2_device *v4l2_dev = &isp->v4l2_dev;
        int ret;

        /* Initialize media device */
        strscpy(media_dev->model, C3_ISP_DRIVER_NAME, sizeof(media_dev->model));
        media_dev->dev = isp->dev;

        media_device_init(media_dev);

        /* Initialize v4l2 device */
        v4l2_dev->mdev = media_dev;
        strscpy(v4l2_dev->name, C3_ISP_DRIVER_NAME, sizeof(v4l2_dev->name));

        ret = v4l2_device_register(isp->dev, v4l2_dev);
        if (ret)
                goto err_media_dev_cleanup;

        ret = media_device_register(&isp->media_dev);
        if (ret) {
                dev_err(isp->dev, "Failed to register media device: %d\n", ret);
                goto err_unreg_v4l2_dev;
        }

        return 0;

err_unreg_v4l2_dev:
        v4l2_device_unregister(&isp->v4l2_dev);
err_media_dev_cleanup:
        media_device_cleanup(media_dev);
        return ret;
}

static void c3_isp_media_unregister(struct c3_isp_device *isp)
{
        media_device_unregister(&isp->media_dev);
        v4l2_device_unregister(&isp->v4l2_dev);
        media_device_cleanup(&isp->media_dev);
}

static void c3_isp_remove_links(struct c3_isp_device *isp)
{
        unsigned int i;

        media_entity_remove_links(&isp->core.sd.entity);

        for (i = 0; i < C3_ISP_NUM_RSZ; i++)
                media_entity_remove_links(&isp->resizers[i].sd.entity);

        for (i = 0; i < C3_ISP_NUM_CAP_DEVS; i++)
                media_entity_remove_links(&isp->caps[i].vdev.entity);
}

static int c3_isp_create_links(struct c3_isp_device *isp)
{
        unsigned int i;
        int ret;

        for (i = 0; i < C3_ISP_NUM_RSZ; i++) {
                ret = media_create_pad_link(&isp->resizers[i].sd.entity,
                                            C3_ISP_RSZ_PAD_SOURCE,
                                            &isp->caps[i].vdev.entity, 0,
                                            MEDIA_LNK_FL_ENABLED |
                                            MEDIA_LNK_FL_IMMUTABLE);
                if (ret) {
                        dev_err(isp->dev,
                                "Failed to link rsz %u and cap %u\n", i, i);
                        goto err_remove_links;
                }

                ret = media_create_pad_link(&isp->core.sd.entity,
                                            C3_ISP_CORE_PAD_SOURCE_VIDEO_0 + i,
                                            &isp->resizers[i].sd.entity,
                                            C3_ISP_RSZ_PAD_SINK,
                                            MEDIA_LNK_FL_ENABLED);
                if (ret) {
                        dev_err(isp->dev,
                                "Failed to link core and rsz %u\n", i);
                        goto err_remove_links;
                }
        }

        ret = media_create_pad_link(&isp->core.sd.entity,
                                    C3_ISP_CORE_PAD_SOURCE_STATS,
                                    &isp->stats.vdev.entity,
                                    0, MEDIA_LNK_FL_ENABLED);
        if (ret) {
                dev_err(isp->dev, "Failed to link core and stats\n");
                goto err_remove_links;
        }

        ret = media_create_pad_link(&isp->params.vdev.entity, 0,
                                    &isp->core.sd.entity,
                                    C3_ISP_CORE_PAD_SINK_PARAMS,
                                    MEDIA_LNK_FL_ENABLED);
        if (ret) {
                dev_err(isp->dev, "Failed to link params and core\n");
                goto err_remove_links;
        }

        return 0;

err_remove_links:
        c3_isp_remove_links(isp);
        return ret;
}

static int c3_isp_videos_register(struct c3_isp_device *isp)
{
        int ret;

        ret = c3_isp_captures_register(isp);
        if (ret)
                return ret;

        ret = c3_isp_stats_register(isp);
        if (ret)
                goto err_captures_unregister;

        ret = c3_isp_params_register(isp);
        if (ret)
                goto err_stats_unregister;

        ret = c3_isp_create_links(isp);
        if (ret)
                goto err_params_unregister;

        return 0;

err_params_unregister:
        c3_isp_params_unregister(isp);
err_stats_unregister:
        c3_isp_stats_unregister(isp);
err_captures_unregister:
        c3_isp_captures_unregister(isp);
        return ret;
}

static void c3_isp_videos_unregister(struct c3_isp_device *isp)
{
        c3_isp_remove_links(isp);
        c3_isp_params_unregister(isp);
        c3_isp_stats_unregister(isp);
        c3_isp_captures_unregister(isp);
}

static int c3_isp_get_clocks(struct c3_isp_device *isp)
{
        const struct c3_isp_info *info = isp->info;

        for (unsigned int i = 0; i < info->clock_num; i++)
                isp->clks[i].id = info->clocks[i];

        return devm_clk_bulk_get(isp->dev, info->clock_num, isp->clks);
}

static int c3_isp_probe(struct platform_device *pdev)
{
        struct device *dev = &pdev->dev;
        struct c3_isp_device *isp;
        int irq;
        int ret;

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

        isp->info = of_device_get_match_data(dev);
        isp->dev = dev;

        isp->base = devm_platform_ioremap_resource_byname(pdev, "isp");
        if (IS_ERR(isp->base))
                return dev_err_probe(dev, PTR_ERR(isp->base),
                                     "Failed to ioremap resource\n");

        irq = platform_get_irq(pdev, 0);
        if (irq < 0)
                return irq;

        ret = c3_isp_get_clocks(isp);
        if (ret)
                return dev_err_probe(dev, ret, "Failed to get clocks\n");

        platform_set_drvdata(pdev, isp);

        pm_runtime_enable(dev);

        ret = c3_isp_media_register(isp);
        if (ret)
                goto err_runtime_disable;

        ret = c3_isp_core_register(isp);
        if (ret)
                goto err_v4l2_unregister;

        ret = c3_isp_resizers_register(isp);
        if (ret)
                goto err_core_unregister;

        ret = c3_isp_async_nf_register(isp);
        if (ret)
                goto err_resizers_unregister;

        ret = devm_request_irq(dev, irq,
                               c3_isp_irq_handler, IRQF_SHARED,
                               dev_driver_string(dev), isp);
        if (ret)
                goto err_nf_unregister;

        ret = c3_isp_videos_register(isp);
        if (ret)
                goto err_nf_unregister;

        return 0;

err_nf_unregister:
        c3_isp_async_nf_unregister(isp);
err_resizers_unregister:
        c3_isp_resizers_unregister(isp);
err_core_unregister:
        c3_isp_core_unregister(isp);
err_v4l2_unregister:
        c3_isp_media_unregister(isp);
err_runtime_disable:
        pm_runtime_disable(dev);
        return ret;
};

static void c3_isp_remove(struct platform_device *pdev)
{
        struct c3_isp_device *isp = platform_get_drvdata(pdev);

        c3_isp_videos_unregister(isp);
        c3_isp_async_nf_unregister(isp);
        c3_isp_core_unregister(isp);
        c3_isp_resizers_unregister(isp);
        c3_isp_media_unregister(isp);
        pm_runtime_disable(isp->dev);
};

static const struct c3_isp_info isp_info = {
        .clocks = {"vapb", "isp0"},
        .clock_num = 2
};

static const struct of_device_id c3_isp_of_match[] = {
        { .compatible = "amlogic,c3-isp",
          .data = &isp_info },
        { },
};
MODULE_DEVICE_TABLE(of, c3_isp_of_match);

static struct platform_driver c3_isp_driver = {
        .probe = c3_isp_probe,
        .remove = c3_isp_remove,
        .driver = {
                .name = "c3-isp",
                .of_match_table = c3_isp_of_match,
                .pm = pm_ptr(&c3_isp_pm_ops),
        },
};

module_platform_driver(c3_isp_driver);

MODULE_AUTHOR("Keke Li <keke.li@amlogic.com>");
MODULE_DESCRIPTION("Amlogic C3 ISP pipeline");
MODULE_LICENSE("GPL");