mirror of
git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2025-08-21 06:50:25 +00:00

The C3 ISP supports multi-camera and multi-exposure HDR, integrates advanced imaging technologies for optimal quality, and drives the core pipeline to transform raw sensor data into high-fidelity images through demosaicing, color correction, and tone mapping operations. Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com> Signed-off-by: Keke Li <keke.li@amlogic.com> Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com> Signed-off-by: Hans Verkuil <hverkuil@xs4all.nl> [hverkuil: drop unnecessary vb2_ops_wait_prepare/finish callbacks]
421 lines
9.6 KiB
C
421 lines
9.6 KiB
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");
|