// SPDX-License-Identifier: GPL-2.0+ 
 | 
/* 
 | 
 * Copyright (C) 2016 NextThing Co 
 | 
 * Copyright (C) 2016-2019 Bootlin 
 | 
 * 
 | 
 * Author: Maxime Ripard <maxime.ripard@bootlin.com> 
 | 
 */ 
 | 
  
 | 
#include <linux/clk.h> 
 | 
#include <linux/dma-mapping.h> 
 | 
#include <linux/interrupt.h> 
 | 
#include <linux/module.h> 
 | 
#include <linux/mutex.h> 
 | 
#include <linux/of.h> 
 | 
#include <linux/of_device.h> 
 | 
#include <linux/of_graph.h> 
 | 
#include <linux/platform_device.h> 
 | 
#include <linux/pm_runtime.h> 
 | 
#include <linux/reset.h> 
 | 
#include <linux/videodev2.h> 
 | 
  
 | 
#include <media/v4l2-dev.h> 
 | 
#include <media/v4l2-device.h> 
 | 
#include <media/v4l2-fwnode.h> 
 | 
#include <media/v4l2-ioctl.h> 
 | 
#include <media/v4l2-mediabus.h> 
 | 
  
 | 
#include <media/videobuf2-core.h> 
 | 
#include <media/videobuf2-dma-contig.h> 
 | 
  
 | 
#include "sun4i_csi.h" 
 | 
  
 | 
struct sun4i_csi_traits { 
 | 
    unsigned int channels; 
 | 
    unsigned int max_width; 
 | 
    bool has_isp; 
 | 
}; 
 | 
  
 | 
static const struct media_entity_operations sun4i_csi_video_entity_ops = { 
 | 
    .link_validate = v4l2_subdev_link_validate, 
 | 
}; 
 | 
  
 | 
static int sun4i_csi_notify_bound(struct v4l2_async_notifier *notifier, 
 | 
                  struct v4l2_subdev *subdev, 
 | 
                  struct v4l2_async_subdev *asd) 
 | 
{ 
 | 
    struct sun4i_csi *csi = container_of(notifier, struct sun4i_csi, 
 | 
                         notifier); 
 | 
  
 | 
    csi->src_subdev = subdev; 
 | 
    csi->src_pad = media_entity_get_fwnode_pad(&subdev->entity, 
 | 
                           subdev->fwnode, 
 | 
                           MEDIA_PAD_FL_SOURCE); 
 | 
    if (csi->src_pad < 0) { 
 | 
        dev_err(csi->dev, "Couldn't find output pad for subdev %s\n", 
 | 
            subdev->name); 
 | 
        return csi->src_pad; 
 | 
    } 
 | 
  
 | 
    dev_dbg(csi->dev, "Bound %s pad: %d\n", subdev->name, csi->src_pad); 
 | 
    return 0; 
 | 
} 
 | 
  
 | 
static int sun4i_csi_notify_complete(struct v4l2_async_notifier *notifier) 
 | 
{ 
 | 
    struct sun4i_csi *csi = container_of(notifier, struct sun4i_csi, 
 | 
                         notifier); 
 | 
    struct v4l2_subdev *subdev = &csi->subdev; 
 | 
    struct video_device *vdev = &csi->vdev; 
 | 
    int ret; 
 | 
  
 | 
    ret = v4l2_device_register_subdev(&csi->v4l, subdev); 
 | 
    if (ret < 0) 
 | 
        return ret; 
 | 
  
 | 
    ret = sun4i_csi_v4l2_register(csi); 
 | 
    if (ret < 0) 
 | 
        return ret; 
 | 
  
 | 
    ret = media_device_register(&csi->mdev); 
 | 
    if (ret) 
 | 
        return ret; 
 | 
  
 | 
    /* Create link from subdev to main device */ 
 | 
    ret = media_create_pad_link(&subdev->entity, CSI_SUBDEV_SOURCE, 
 | 
                    &vdev->entity, 0, 
 | 
                    MEDIA_LNK_FL_ENABLED | 
 | 
                    MEDIA_LNK_FL_IMMUTABLE); 
 | 
    if (ret) 
 | 
        goto err_clean_media; 
 | 
  
 | 
    ret = media_create_pad_link(&csi->src_subdev->entity, csi->src_pad, 
 | 
                    &subdev->entity, CSI_SUBDEV_SINK, 
 | 
                    MEDIA_LNK_FL_ENABLED | 
 | 
                    MEDIA_LNK_FL_IMMUTABLE); 
 | 
    if (ret) 
 | 
        goto err_clean_media; 
 | 
  
 | 
    ret = v4l2_device_register_subdev_nodes(&csi->v4l); 
 | 
    if (ret < 0) 
 | 
        goto err_clean_media; 
 | 
  
 | 
    return 0; 
 | 
  
 | 
err_clean_media: 
 | 
    media_device_unregister(&csi->mdev); 
 | 
  
 | 
    return ret; 
 | 
} 
 | 
  
 | 
static const struct v4l2_async_notifier_operations sun4i_csi_notify_ops = { 
 | 
    .bound        = sun4i_csi_notify_bound, 
 | 
    .complete    = sun4i_csi_notify_complete, 
 | 
}; 
 | 
  
 | 
static int sun4i_csi_notifier_init(struct sun4i_csi *csi) 
 | 
{ 
 | 
    struct v4l2_fwnode_endpoint vep = { 
 | 
        .bus_type = V4L2_MBUS_PARALLEL, 
 | 
    }; 
 | 
    struct v4l2_async_subdev *asd; 
 | 
    struct fwnode_handle *ep; 
 | 
    int ret; 
 | 
  
 | 
    v4l2_async_notifier_init(&csi->notifier); 
 | 
  
 | 
    ep = fwnode_graph_get_endpoint_by_id(dev_fwnode(csi->dev), 0, 0, 
 | 
                         FWNODE_GRAPH_ENDPOINT_NEXT); 
 | 
    if (!ep) 
 | 
        return -EINVAL; 
 | 
  
 | 
    ret = v4l2_fwnode_endpoint_parse(ep, &vep); 
 | 
    if (ret) 
 | 
        goto out; 
 | 
  
 | 
    csi->bus = vep.bus.parallel; 
 | 
  
 | 
    asd = v4l2_async_notifier_add_fwnode_remote_subdev(&csi->notifier, 
 | 
                               ep, sizeof(*asd)); 
 | 
    if (IS_ERR(asd)) { 
 | 
        ret = PTR_ERR(asd); 
 | 
        goto out; 
 | 
    } 
 | 
  
 | 
    csi->notifier.ops = &sun4i_csi_notify_ops; 
 | 
  
 | 
out: 
 | 
    fwnode_handle_put(ep); 
 | 
    return ret; 
 | 
} 
 | 
  
 | 
static int sun4i_csi_probe(struct platform_device *pdev) 
 | 
{ 
 | 
    struct v4l2_subdev *subdev; 
 | 
    struct video_device *vdev; 
 | 
    struct sun4i_csi *csi; 
 | 
    struct resource *res; 
 | 
    int ret; 
 | 
    int irq; 
 | 
  
 | 
    csi = devm_kzalloc(&pdev->dev, sizeof(*csi), GFP_KERNEL); 
 | 
    if (!csi) 
 | 
        return -ENOMEM; 
 | 
    platform_set_drvdata(pdev, csi); 
 | 
    csi->dev = &pdev->dev; 
 | 
    subdev = &csi->subdev; 
 | 
    vdev = &csi->vdev; 
 | 
  
 | 
    csi->traits = of_device_get_match_data(&pdev->dev); 
 | 
    if (!csi->traits) 
 | 
        return -EINVAL; 
 | 
  
 | 
    /* 
 | 
     * On Allwinner SoCs, some high memory bandwidth devices do DMA 
 | 
     * directly over the memory bus (called MBUS), instead of the 
 | 
     * system bus. The memory bus has a different addressing scheme 
 | 
     * without the DRAM starting offset. 
 | 
     * 
 | 
     * In some cases this can be described by an interconnect in 
 | 
     * the device tree. In other cases where the hardware is not 
 | 
     * fully understood and the interconnect is left out of the 
 | 
     * device tree, fall back to a default offset. 
 | 
     */ 
 | 
    if (of_find_property(csi->dev->of_node, "interconnects", NULL)) { 
 | 
        ret = of_dma_configure(csi->dev, csi->dev->of_node, true); 
 | 
        if (ret) 
 | 
            return ret; 
 | 
    } else { 
 | 
        /* 
 | 
         * XXX(hch): this has no business in a driver and needs to move 
 | 
         * to the device tree. 
 | 
         */ 
 | 
#ifdef PHYS_PFN_OFFSET 
 | 
        ret = dma_direct_set_offset(csi->dev, PHYS_OFFSET, 0, SZ_4G); 
 | 
        if (ret) 
 | 
            return ret; 
 | 
#endif 
 | 
    } 
 | 
  
 | 
    csi->mdev.dev = csi->dev; 
 | 
    strscpy(csi->mdev.model, "Allwinner Video Capture Device", 
 | 
        sizeof(csi->mdev.model)); 
 | 
    csi->mdev.hw_revision = 0; 
 | 
    snprintf(csi->mdev.bus_info, sizeof(csi->mdev.bus_info), "platform:%s", 
 | 
         dev_name(csi->dev)); 
 | 
    media_device_init(&csi->mdev); 
 | 
    csi->v4l.mdev = &csi->mdev; 
 | 
  
 | 
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0); 
 | 
    csi->regs = devm_ioremap_resource(&pdev->dev, res); 
 | 
    if (IS_ERR(csi->regs)) 
 | 
        return PTR_ERR(csi->regs); 
 | 
  
 | 
    irq = platform_get_irq(pdev, 0); 
 | 
    if (irq < 0) 
 | 
        return irq; 
 | 
  
 | 
    csi->bus_clk = devm_clk_get(&pdev->dev, "bus"); 
 | 
    if (IS_ERR(csi->bus_clk)) { 
 | 
        dev_err(&pdev->dev, "Couldn't get our bus clock\n"); 
 | 
        return PTR_ERR(csi->bus_clk); 
 | 
    } 
 | 
  
 | 
    if (csi->traits->has_isp) { 
 | 
        csi->isp_clk = devm_clk_get(&pdev->dev, "isp"); 
 | 
        if (IS_ERR(csi->isp_clk)) { 
 | 
            dev_err(&pdev->dev, "Couldn't get our ISP clock\n"); 
 | 
            return PTR_ERR(csi->isp_clk); 
 | 
        } 
 | 
    } 
 | 
  
 | 
    csi->ram_clk = devm_clk_get(&pdev->dev, "ram"); 
 | 
    if (IS_ERR(csi->ram_clk)) { 
 | 
        dev_err(&pdev->dev, "Couldn't get our ram clock\n"); 
 | 
        return PTR_ERR(csi->ram_clk); 
 | 
    } 
 | 
  
 | 
    csi->rst = devm_reset_control_get(&pdev->dev, NULL); 
 | 
    if (IS_ERR(csi->rst)) { 
 | 
        dev_err(&pdev->dev, "Couldn't get our reset line\n"); 
 | 
        return PTR_ERR(csi->rst); 
 | 
    } 
 | 
  
 | 
    /* Initialize subdev */ 
 | 
    v4l2_subdev_init(subdev, &sun4i_csi_subdev_ops); 
 | 
    subdev->flags = V4L2_SUBDEV_FL_HAS_DEVNODE | V4L2_SUBDEV_FL_HAS_EVENTS; 
 | 
    subdev->entity.function = MEDIA_ENT_F_VID_IF_BRIDGE; 
 | 
    subdev->owner = THIS_MODULE; 
 | 
    snprintf(subdev->name, sizeof(subdev->name), "sun4i-csi-0"); 
 | 
    v4l2_set_subdevdata(subdev, csi); 
 | 
  
 | 
    csi->subdev_pads[CSI_SUBDEV_SINK].flags = 
 | 
        MEDIA_PAD_FL_SINK | MEDIA_PAD_FL_MUST_CONNECT; 
 | 
    csi->subdev_pads[CSI_SUBDEV_SOURCE].flags = MEDIA_PAD_FL_SOURCE; 
 | 
    ret = media_entity_pads_init(&subdev->entity, CSI_SUBDEV_PADS, 
 | 
                     csi->subdev_pads); 
 | 
    if (ret < 0) 
 | 
        return ret; 
 | 
  
 | 
    csi->vdev_pad.flags = MEDIA_PAD_FL_SINK | MEDIA_PAD_FL_MUST_CONNECT; 
 | 
    vdev->entity.ops = &sun4i_csi_video_entity_ops; 
 | 
    ret = media_entity_pads_init(&vdev->entity, 1, &csi->vdev_pad); 
 | 
    if (ret < 0) 
 | 
        return ret; 
 | 
  
 | 
    ret = sun4i_csi_dma_register(csi, irq); 
 | 
    if (ret) 
 | 
        goto err_clean_pad; 
 | 
  
 | 
    ret = sun4i_csi_notifier_init(csi); 
 | 
    if (ret) 
 | 
        goto err_unregister_media; 
 | 
  
 | 
    ret = v4l2_async_notifier_register(&csi->v4l, &csi->notifier); 
 | 
    if (ret) { 
 | 
        dev_err(csi->dev, "Couldn't register our notifier.\n"); 
 | 
        goto err_unregister_media; 
 | 
    } 
 | 
  
 | 
    pm_runtime_enable(&pdev->dev); 
 | 
  
 | 
    return 0; 
 | 
  
 | 
err_unregister_media: 
 | 
    media_device_unregister(&csi->mdev); 
 | 
    sun4i_csi_dma_unregister(csi); 
 | 
  
 | 
err_clean_pad: 
 | 
    media_device_cleanup(&csi->mdev); 
 | 
  
 | 
    return ret; 
 | 
} 
 | 
  
 | 
static int sun4i_csi_remove(struct platform_device *pdev) 
 | 
{ 
 | 
    struct sun4i_csi *csi = platform_get_drvdata(pdev); 
 | 
  
 | 
    v4l2_async_notifier_unregister(&csi->notifier); 
 | 
    v4l2_async_notifier_cleanup(&csi->notifier); 
 | 
    vb2_video_unregister_device(&csi->vdev); 
 | 
    media_device_unregister(&csi->mdev); 
 | 
    sun4i_csi_dma_unregister(csi); 
 | 
    media_device_cleanup(&csi->mdev); 
 | 
  
 | 
    return 0; 
 | 
} 
 | 
  
 | 
static const struct sun4i_csi_traits sun4i_a10_csi1_traits = { 
 | 
    .channels = 1, 
 | 
    .max_width = 24, 
 | 
    .has_isp = false, 
 | 
}; 
 | 
  
 | 
static const struct sun4i_csi_traits sun7i_a20_csi0_traits = { 
 | 
    .channels = 4, 
 | 
    .max_width = 16, 
 | 
    .has_isp = true, 
 | 
}; 
 | 
  
 | 
static const struct of_device_id sun4i_csi_of_match[] = { 
 | 
    { .compatible = "allwinner,sun4i-a10-csi1", .data = &sun4i_a10_csi1_traits }, 
 | 
    { .compatible = "allwinner,sun7i-a20-csi0", .data = &sun7i_a20_csi0_traits }, 
 | 
    { /* Sentinel */ } 
 | 
}; 
 | 
MODULE_DEVICE_TABLE(of, sun4i_csi_of_match); 
 | 
  
 | 
static int __maybe_unused sun4i_csi_runtime_resume(struct device *dev) 
 | 
{ 
 | 
    struct sun4i_csi *csi = dev_get_drvdata(dev); 
 | 
  
 | 
    reset_control_deassert(csi->rst); 
 | 
    clk_prepare_enable(csi->bus_clk); 
 | 
    clk_prepare_enable(csi->ram_clk); 
 | 
    clk_set_rate(csi->isp_clk, 80000000); 
 | 
    clk_prepare_enable(csi->isp_clk); 
 | 
  
 | 
    writel(1, csi->regs + CSI_EN_REG); 
 | 
  
 | 
    return 0; 
 | 
} 
 | 
  
 | 
static int __maybe_unused sun4i_csi_runtime_suspend(struct device *dev) 
 | 
{ 
 | 
    struct sun4i_csi *csi = dev_get_drvdata(dev); 
 | 
  
 | 
    clk_disable_unprepare(csi->isp_clk); 
 | 
    clk_disable_unprepare(csi->ram_clk); 
 | 
    clk_disable_unprepare(csi->bus_clk); 
 | 
  
 | 
    reset_control_assert(csi->rst); 
 | 
  
 | 
    return 0; 
 | 
} 
 | 
  
 | 
static const struct dev_pm_ops sun4i_csi_pm_ops = { 
 | 
    SET_RUNTIME_PM_OPS(sun4i_csi_runtime_suspend, 
 | 
               sun4i_csi_runtime_resume, 
 | 
               NULL) 
 | 
}; 
 | 
  
 | 
static struct platform_driver sun4i_csi_driver = { 
 | 
    .probe    = sun4i_csi_probe, 
 | 
    .remove    = sun4i_csi_remove, 
 | 
    .driver    = { 
 | 
        .name        = "sun4i-csi", 
 | 
        .of_match_table    = sun4i_csi_of_match, 
 | 
        .pm        = &sun4i_csi_pm_ops, 
 | 
    }, 
 | 
}; 
 | 
module_platform_driver(sun4i_csi_driver); 
 | 
  
 | 
MODULE_DESCRIPTION("Allwinner A10 Camera Sensor Interface driver"); 
 | 
MODULE_AUTHOR("Maxime Ripard <mripard@kernel.org>"); 
 | 
MODULE_LICENSE("GPL"); 
 |