diff options
author | Yunhong Jiang <yunhong.jiang@intel.com> | 2015-08-04 12:17:53 -0700 |
---|---|---|
committer | Yunhong Jiang <yunhong.jiang@intel.com> | 2015-08-04 15:44:42 -0700 |
commit | 9ca8dbcc65cfc63d6f5ef3312a33184e1d726e00 (patch) | |
tree | 1c9cafbcd35f783a87880a10f85d1a060db1a563 /kernel/drivers/media/platform/exynos4-is/media-dev.c | |
parent | 98260f3884f4a202f9ca5eabed40b1354c489b29 (diff) |
Add the rt linux 4.1.3-rt3 as base
Import the rt linux 4.1.3-rt3 as OPNFV kvm base.
It's from git://git.kernel.org/pub/scm/linux/kernel/git/rt/linux-rt-devel.git linux-4.1.y-rt and
the base is:
commit 0917f823c59692d751951bf5ea699a2d1e2f26a2
Author: Sebastian Andrzej Siewior <bigeasy@linutronix.de>
Date: Sat Jul 25 12:13:34 2015 +0200
Prepare v4.1.3-rt3
Signed-off-by: Sebastian Andrzej Siewior <bigeasy@linutronix.de>
We lose all the git history this way and it's not good. We
should apply another opnfv project repo in future.
Change-Id: I87543d81c9df70d99c5001fbdf646b202c19f423
Signed-off-by: Yunhong Jiang <yunhong.jiang@intel.com>
Diffstat (limited to 'kernel/drivers/media/platform/exynos4-is/media-dev.c')
-rw-r--r-- | kernel/drivers/media/platform/exynos4-is/media-dev.c | 1499 |
1 files changed, 1499 insertions, 0 deletions
diff --git a/kernel/drivers/media/platform/exynos4-is/media-dev.c b/kernel/drivers/media/platform/exynos4-is/media-dev.c new file mode 100644 index 000000000..f315ef946 --- /dev/null +++ b/kernel/drivers/media/platform/exynos4-is/media-dev.c @@ -0,0 +1,1499 @@ +/* + * S5P/EXYNOS4 SoC series camera host interface media device driver + * + * Copyright (C) 2011 - 2013 Samsung Electronics Co., Ltd. + * Author: Sylwester Nawrocki <s.nawrocki@samsung.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 2 of the License, + * or (at your option) any later version. + */ + +#include <linux/bug.h> +#include <linux/clk.h> +#include <linux/clk-provider.h> +#include <linux/device.h> +#include <linux/errno.h> +#include <linux/i2c.h> +#include <linux/kernel.h> +#include <linux/list.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_platform.h> +#include <linux/of_device.h> +#include <linux/of_graph.h> +#include <linux/platform_device.h> +#include <linux/pm_runtime.h> +#include <linux/types.h> +#include <linux/slab.h> +#include <media/v4l2-async.h> +#include <media/v4l2-ctrls.h> +#include <media/v4l2-of.h> +#include <media/media-device.h> +#include <media/exynos-fimc.h> + +#include "media-dev.h" +#include "fimc-core.h" +#include "fimc-is.h" +#include "fimc-lite.h" +#include "mipi-csis.h" + +/* Set up image sensor subdev -> FIMC capture node notifications. */ +static void __setup_sensor_notification(struct fimc_md *fmd, + struct v4l2_subdev *sensor, + struct v4l2_subdev *fimc_sd) +{ + struct fimc_source_info *src_inf; + struct fimc_sensor_info *md_si; + unsigned long flags; + + src_inf = v4l2_get_subdev_hostdata(sensor); + if (!src_inf || WARN_ON(fmd == NULL)) + return; + + md_si = source_to_sensor_info(src_inf); + spin_lock_irqsave(&fmd->slock, flags); + md_si->host = v4l2_get_subdevdata(fimc_sd); + spin_unlock_irqrestore(&fmd->slock, flags); +} + +/** + * fimc_pipeline_prepare - update pipeline information with subdevice pointers + * @me: media entity terminating the pipeline + * + * Caller holds the graph mutex. + */ +static void fimc_pipeline_prepare(struct fimc_pipeline *p, + struct media_entity *me) +{ + struct fimc_md *fmd = entity_to_fimc_mdev(me); + struct v4l2_subdev *sd; + struct v4l2_subdev *sensor = NULL; + int i; + + for (i = 0; i < IDX_MAX; i++) + p->subdevs[i] = NULL; + + while (1) { + struct media_pad *pad = NULL; + + /* Find remote source pad */ + for (i = 0; i < me->num_pads; i++) { + struct media_pad *spad = &me->pads[i]; + if (!(spad->flags & MEDIA_PAD_FL_SINK)) + continue; + pad = media_entity_remote_pad(spad); + if (pad) + break; + } + + if (pad == NULL || + media_entity_type(pad->entity) != MEDIA_ENT_T_V4L2_SUBDEV) + break; + sd = media_entity_to_v4l2_subdev(pad->entity); + + switch (sd->grp_id) { + case GRP_ID_SENSOR: + sensor = sd; + /* fall through */ + case GRP_ID_FIMC_IS_SENSOR: + p->subdevs[IDX_SENSOR] = sd; + break; + case GRP_ID_CSIS: + p->subdevs[IDX_CSIS] = sd; + break; + case GRP_ID_FLITE: + p->subdevs[IDX_FLITE] = sd; + break; + case GRP_ID_FIMC: + p->subdevs[IDX_FIMC] = sd; + break; + case GRP_ID_FIMC_IS: + p->subdevs[IDX_IS_ISP] = sd; + break; + default: + break; + } + me = &sd->entity; + if (me->num_pads == 1) + break; + } + + if (sensor && p->subdevs[IDX_FIMC]) + __setup_sensor_notification(fmd, sensor, p->subdevs[IDX_FIMC]); +} + +/** + * __subdev_set_power - change power state of a single subdev + * @sd: subdevice to change power state for + * @on: 1 to enable power or 0 to disable + * + * Return result of s_power subdev operation or -ENXIO if sd argument + * is NULL. Return 0 if the subdevice does not implement s_power. + */ +static int __subdev_set_power(struct v4l2_subdev *sd, int on) +{ + int *use_count; + int ret; + + if (sd == NULL) + return -ENXIO; + + use_count = &sd->entity.use_count; + if (on && (*use_count)++ > 0) + return 0; + else if (!on && (*use_count == 0 || --(*use_count) > 0)) + return 0; + ret = v4l2_subdev_call(sd, core, s_power, on); + + return ret != -ENOIOCTLCMD ? ret : 0; +} + +/** + * fimc_pipeline_s_power - change power state of all pipeline subdevs + * @fimc: fimc device terminating the pipeline + * @state: true to power on, false to power off + * + * Needs to be called with the graph mutex held. + */ +static int fimc_pipeline_s_power(struct fimc_pipeline *p, bool on) +{ + static const u8 seq[2][IDX_MAX - 1] = { + { IDX_IS_ISP, IDX_SENSOR, IDX_CSIS, IDX_FLITE }, + { IDX_CSIS, IDX_FLITE, IDX_SENSOR, IDX_IS_ISP }, + }; + int i, ret = 0; + + if (p->subdevs[IDX_SENSOR] == NULL) + return -ENXIO; + + for (i = 0; i < IDX_MAX - 1; i++) { + unsigned int idx = seq[on][i]; + + ret = __subdev_set_power(p->subdevs[idx], on); + + + if (ret < 0 && ret != -ENXIO) + goto error; + } + return 0; +error: + for (; i >= 0; i--) { + unsigned int idx = seq[on][i]; + __subdev_set_power(p->subdevs[idx], !on); + } + return ret; +} + +/** + * __fimc_pipeline_open - update the pipeline information, enable power + * of all pipeline subdevs and the sensor clock + * @me: media entity to start graph walk with + * @prepare: true to walk the current pipeline and acquire all subdevs + * + * Called with the graph mutex held. + */ +static int __fimc_pipeline_open(struct exynos_media_pipeline *ep, + struct media_entity *me, bool prepare) +{ + struct fimc_md *fmd = entity_to_fimc_mdev(me); + struct fimc_pipeline *p = to_fimc_pipeline(ep); + struct v4l2_subdev *sd; + int ret; + + if (WARN_ON(p == NULL || me == NULL)) + return -EINVAL; + + if (prepare) + fimc_pipeline_prepare(p, me); + + sd = p->subdevs[IDX_SENSOR]; + if (sd == NULL) + return -EINVAL; + + /* Disable PXLASYNC clock if this pipeline includes FIMC-IS */ + if (!IS_ERR(fmd->wbclk[CLK_IDX_WB_B]) && p->subdevs[IDX_IS_ISP]) { + ret = clk_prepare_enable(fmd->wbclk[CLK_IDX_WB_B]); + if (ret < 0) + return ret; + } + + ret = fimc_pipeline_s_power(p, 1); + if (!ret) + return 0; + + if (!IS_ERR(fmd->wbclk[CLK_IDX_WB_B]) && p->subdevs[IDX_IS_ISP]) + clk_disable_unprepare(fmd->wbclk[CLK_IDX_WB_B]); + + return ret; +} + +/** + * __fimc_pipeline_close - disable the sensor clock and pipeline power + * @fimc: fimc device terminating the pipeline + * + * Disable power of all subdevs and turn the external sensor clock off. + */ +static int __fimc_pipeline_close(struct exynos_media_pipeline *ep) +{ + struct fimc_pipeline *p = to_fimc_pipeline(ep); + struct v4l2_subdev *sd = p ? p->subdevs[IDX_SENSOR] : NULL; + struct fimc_md *fmd; + int ret; + + if (sd == NULL) { + pr_warn("%s(): No sensor subdev\n", __func__); + return 0; + } + + ret = fimc_pipeline_s_power(p, 0); + + fmd = entity_to_fimc_mdev(&sd->entity); + + /* Disable PXLASYNC clock if this pipeline includes FIMC-IS */ + if (!IS_ERR(fmd->wbclk[CLK_IDX_WB_B]) && p->subdevs[IDX_IS_ISP]) + clk_disable_unprepare(fmd->wbclk[CLK_IDX_WB_B]); + + return ret == -ENXIO ? 0 : ret; +} + +/** + * __fimc_pipeline_s_stream - call s_stream() on pipeline subdevs + * @pipeline: video pipeline structure + * @on: passed as the s_stream() callback argument + */ +static int __fimc_pipeline_s_stream(struct exynos_media_pipeline *ep, bool on) +{ + static const u8 seq[2][IDX_MAX] = { + { IDX_FIMC, IDX_SENSOR, IDX_IS_ISP, IDX_CSIS, IDX_FLITE }, + { IDX_CSIS, IDX_FLITE, IDX_FIMC, IDX_SENSOR, IDX_IS_ISP }, + }; + struct fimc_pipeline *p = to_fimc_pipeline(ep); + int i, ret = 0; + + if (p->subdevs[IDX_SENSOR] == NULL) + return -ENODEV; + + for (i = 0; i < IDX_MAX; i++) { + unsigned int idx = seq[on][i]; + + ret = v4l2_subdev_call(p->subdevs[idx], video, s_stream, on); + + if (ret < 0 && ret != -ENOIOCTLCMD && ret != -ENODEV) + goto error; + } + return 0; +error: + for (; i >= 0; i--) { + unsigned int idx = seq[on][i]; + v4l2_subdev_call(p->subdevs[idx], video, s_stream, !on); + } + return ret; +} + +/* Media pipeline operations for the FIMC/FIMC-LITE video device driver */ +static const struct exynos_media_pipeline_ops fimc_pipeline_ops = { + .open = __fimc_pipeline_open, + .close = __fimc_pipeline_close, + .set_stream = __fimc_pipeline_s_stream, +}; + +static struct exynos_media_pipeline *fimc_md_pipeline_create( + struct fimc_md *fmd) +{ + struct fimc_pipeline *p; + + p = kzalloc(sizeof(*p), GFP_KERNEL); + if (!p) + return NULL; + + list_add_tail(&p->list, &fmd->pipelines); + + p->ep.ops = &fimc_pipeline_ops; + return &p->ep; +} + +static void fimc_md_pipelines_free(struct fimc_md *fmd) +{ + while (!list_empty(&fmd->pipelines)) { + struct fimc_pipeline *p; + + p = list_entry(fmd->pipelines.next, typeof(*p), list); + list_del(&p->list); + kfree(p); + } +} + +/* Parse port node and register as a sub-device any sensor specified there. */ +static int fimc_md_parse_port_node(struct fimc_md *fmd, + struct device_node *port, + unsigned int index) +{ + struct fimc_source_info *pd = &fmd->sensor[index].pdata; + struct device_node *rem, *ep, *np; + struct v4l2_of_endpoint endpoint; + + /* Assume here a port node can have only one endpoint node. */ + ep = of_get_next_child(port, NULL); + if (!ep) + return 0; + + v4l2_of_parse_endpoint(ep, &endpoint); + if (WARN_ON(endpoint.base.port == 0) || index >= FIMC_MAX_SENSORS) + return -EINVAL; + + pd->mux_id = (endpoint.base.port - 1) & 0x1; + + rem = of_graph_get_remote_port_parent(ep); + of_node_put(ep); + if (rem == NULL) { + v4l2_info(&fmd->v4l2_dev, "Remote device at %s not found\n", + ep->full_name); + return 0; + } + + if (fimc_input_is_parallel(endpoint.base.port)) { + if (endpoint.bus_type == V4L2_MBUS_PARALLEL) + pd->sensor_bus_type = FIMC_BUS_TYPE_ITU_601; + else + pd->sensor_bus_type = FIMC_BUS_TYPE_ITU_656; + pd->flags = endpoint.bus.parallel.flags; + } else if (fimc_input_is_mipi_csi(endpoint.base.port)) { + /* + * MIPI CSI-2: only input mux selection and + * the sensor's clock frequency is needed. + */ + pd->sensor_bus_type = FIMC_BUS_TYPE_MIPI_CSI2; + } else { + v4l2_err(&fmd->v4l2_dev, "Wrong port id (%u) at node %s\n", + endpoint.base.port, rem->full_name); + } + /* + * For FIMC-IS handled sensors, that are placed under i2c-isp device + * node, FIMC is connected to the FIMC-IS through its ISP Writeback + * input. Sensors are attached to the FIMC-LITE hostdata interface + * directly or through MIPI-CSIS, depending on the external media bus + * used. This needs to be handled in a more reliable way, not by just + * checking parent's node name. + */ + np = of_get_parent(rem); + + if (np && !of_node_cmp(np->name, "i2c-isp")) + pd->fimc_bus_type = FIMC_BUS_TYPE_ISP_WRITEBACK; + else + pd->fimc_bus_type = pd->sensor_bus_type; + + if (WARN_ON(index >= ARRAY_SIZE(fmd->sensor))) + return -EINVAL; + + fmd->sensor[index].asd.match_type = V4L2_ASYNC_MATCH_OF; + fmd->sensor[index].asd.match.of.node = rem; + fmd->async_subdevs[index] = &fmd->sensor[index].asd; + + fmd->num_sensors++; + + of_node_put(rem); + return 0; +} + +/* Register all SoC external sub-devices */ +static int fimc_md_register_sensor_entities(struct fimc_md *fmd) +{ + struct device_node *parent = fmd->pdev->dev.of_node; + struct device_node *node, *ports; + int index = 0; + int ret; + + /* + * Runtime resume one of the FIMC entities to make sure + * the sclk_cam clocks are not globally disabled. + */ + if (!fmd->pmf) + return -ENXIO; + + ret = pm_runtime_get_sync(fmd->pmf); + if (ret < 0) + return ret; + + fmd->num_sensors = 0; + + /* Attach sensors linked to MIPI CSI-2 receivers */ + for_each_available_child_of_node(parent, node) { + struct device_node *port; + + if (of_node_cmp(node->name, "csis")) + continue; + /* The csis node can have only port subnode. */ + port = of_get_next_child(node, NULL); + if (!port) + continue; + + ret = fimc_md_parse_port_node(fmd, port, index); + if (ret < 0) + goto rpm_put; + index++; + } + + /* Attach sensors listed in the parallel-ports node */ + ports = of_get_child_by_name(parent, "parallel-ports"); + if (!ports) + goto rpm_put; + + for_each_child_of_node(ports, node) { + ret = fimc_md_parse_port_node(fmd, node, index); + if (ret < 0) + break; + index++; + } +rpm_put: + pm_runtime_put(fmd->pmf); + return ret; +} + +static int __of_get_csis_id(struct device_node *np) +{ + u32 reg = 0; + + np = of_get_child_by_name(np, "port"); + if (!np) + return -EINVAL; + of_property_read_u32(np, "reg", ®); + return reg - FIMC_INPUT_MIPI_CSI2_0; +} + +/* + * MIPI-CSIS, FIMC and FIMC-LITE platform devices registration. + */ +static int register_fimc_lite_entity(struct fimc_md *fmd, + struct fimc_lite *fimc_lite) +{ + struct v4l2_subdev *sd; + struct exynos_media_pipeline *ep; + int ret; + + if (WARN_ON(fimc_lite->index >= FIMC_LITE_MAX_DEVS || + fmd->fimc_lite[fimc_lite->index])) + return -EBUSY; + + sd = &fimc_lite->subdev; + sd->grp_id = GRP_ID_FLITE; + + ep = fimc_md_pipeline_create(fmd); + if (!ep) + return -ENOMEM; + + v4l2_set_subdev_hostdata(sd, ep); + + ret = v4l2_device_register_subdev(&fmd->v4l2_dev, sd); + if (!ret) + fmd->fimc_lite[fimc_lite->index] = fimc_lite; + else + v4l2_err(&fmd->v4l2_dev, "Failed to register FIMC.LITE%d\n", + fimc_lite->index); + return ret; +} + +static int register_fimc_entity(struct fimc_md *fmd, struct fimc_dev *fimc) +{ + struct v4l2_subdev *sd; + struct exynos_media_pipeline *ep; + int ret; + + if (WARN_ON(fimc->id >= FIMC_MAX_DEVS || fmd->fimc[fimc->id])) + return -EBUSY; + + sd = &fimc->vid_cap.subdev; + sd->grp_id = GRP_ID_FIMC; + + ep = fimc_md_pipeline_create(fmd); + if (!ep) + return -ENOMEM; + + v4l2_set_subdev_hostdata(sd, ep); + + ret = v4l2_device_register_subdev(&fmd->v4l2_dev, sd); + if (!ret) { + if (!fmd->pmf && fimc->pdev) + fmd->pmf = &fimc->pdev->dev; + fmd->fimc[fimc->id] = fimc; + fimc->vid_cap.user_subdev_api = fmd->user_subdev_api; + } else { + v4l2_err(&fmd->v4l2_dev, "Failed to register FIMC.%d (%d)\n", + fimc->id, ret); + } + return ret; +} + +static int register_csis_entity(struct fimc_md *fmd, + struct platform_device *pdev, + struct v4l2_subdev *sd) +{ + struct device_node *node = pdev->dev.of_node; + int id, ret; + + id = node ? __of_get_csis_id(node) : max(0, pdev->id); + + if (WARN_ON(id < 0 || id >= CSIS_MAX_ENTITIES)) + return -ENOENT; + + if (WARN_ON(fmd->csis[id].sd)) + return -EBUSY; + + sd->grp_id = GRP_ID_CSIS; + ret = v4l2_device_register_subdev(&fmd->v4l2_dev, sd); + if (!ret) + fmd->csis[id].sd = sd; + else + v4l2_err(&fmd->v4l2_dev, + "Failed to register MIPI-CSIS.%d (%d)\n", id, ret); + return ret; +} + +static int register_fimc_is_entity(struct fimc_md *fmd, struct fimc_is *is) +{ + struct v4l2_subdev *sd = &is->isp.subdev; + struct exynos_media_pipeline *ep; + int ret; + + /* Allocate pipeline object for the ISP capture video node. */ + ep = fimc_md_pipeline_create(fmd); + if (!ep) + return -ENOMEM; + + v4l2_set_subdev_hostdata(sd, ep); + + ret = v4l2_device_register_subdev(&fmd->v4l2_dev, sd); + if (ret) { + v4l2_err(&fmd->v4l2_dev, + "Failed to register FIMC-ISP (%d)\n", ret); + return ret; + } + + fmd->fimc_is = is; + return 0; +} + +static int fimc_md_register_platform_entity(struct fimc_md *fmd, + struct platform_device *pdev, + int plat_entity) +{ + struct device *dev = &pdev->dev; + int ret = -EPROBE_DEFER; + void *drvdata; + + /* Lock to ensure dev->driver won't change. */ + device_lock(dev); + + if (!dev->driver || !try_module_get(dev->driver->owner)) + goto dev_unlock; + + drvdata = dev_get_drvdata(dev); + /* Some subdev didn't probe successfully id drvdata is NULL */ + if (drvdata) { + switch (plat_entity) { + case IDX_FIMC: + ret = register_fimc_entity(fmd, drvdata); + break; + case IDX_FLITE: + ret = register_fimc_lite_entity(fmd, drvdata); + break; + case IDX_CSIS: + ret = register_csis_entity(fmd, pdev, drvdata); + break; + case IDX_IS_ISP: + ret = register_fimc_is_entity(fmd, drvdata); + break; + default: + ret = -ENODEV; + } + } + + module_put(dev->driver->owner); +dev_unlock: + device_unlock(dev); + if (ret == -EPROBE_DEFER) + dev_info(&fmd->pdev->dev, "deferring %s device registration\n", + dev_name(dev)); + else if (ret < 0) + dev_err(&fmd->pdev->dev, "%s device registration failed (%d)\n", + dev_name(dev), ret); + return ret; +} + +/* Register FIMC, FIMC-LITE and CSIS media entities */ +static int fimc_md_register_platform_entities(struct fimc_md *fmd, + struct device_node *parent) +{ + struct device_node *node; + int ret = 0; + + for_each_available_child_of_node(parent, node) { + struct platform_device *pdev; + int plat_entity = -1; + + pdev = of_find_device_by_node(node); + if (!pdev) + continue; + + /* If driver of any entity isn't ready try all again later. */ + if (!strcmp(node->name, CSIS_OF_NODE_NAME)) + plat_entity = IDX_CSIS; + else if (!strcmp(node->name, FIMC_IS_OF_NODE_NAME)) + plat_entity = IDX_IS_ISP; + else if (!strcmp(node->name, FIMC_LITE_OF_NODE_NAME)) + plat_entity = IDX_FLITE; + else if (!strcmp(node->name, FIMC_OF_NODE_NAME) && + !of_property_read_bool(node, "samsung,lcd-wb")) + plat_entity = IDX_FIMC; + + if (plat_entity >= 0) + ret = fimc_md_register_platform_entity(fmd, pdev, + plat_entity); + put_device(&pdev->dev); + if (ret < 0) + break; + } + + return ret; +} + +static void fimc_md_unregister_entities(struct fimc_md *fmd) +{ + int i; + + for (i = 0; i < FIMC_MAX_DEVS; i++) { + struct fimc_dev *dev = fmd->fimc[i]; + if (dev == NULL) + continue; + v4l2_device_unregister_subdev(&dev->vid_cap.subdev); + dev->vid_cap.ve.pipe = NULL; + fmd->fimc[i] = NULL; + } + for (i = 0; i < FIMC_LITE_MAX_DEVS; i++) { + struct fimc_lite *dev = fmd->fimc_lite[i]; + if (dev == NULL) + continue; + v4l2_device_unregister_subdev(&dev->subdev); + dev->ve.pipe = NULL; + fmd->fimc_lite[i] = NULL; + } + for (i = 0; i < CSIS_MAX_ENTITIES; i++) { + if (fmd->csis[i].sd == NULL) + continue; + v4l2_device_unregister_subdev(fmd->csis[i].sd); + fmd->csis[i].sd = NULL; + } + + if (fmd->fimc_is) + v4l2_device_unregister_subdev(&fmd->fimc_is->isp.subdev); + + v4l2_info(&fmd->v4l2_dev, "Unregistered all entities\n"); +} + +/** + * __fimc_md_create_fimc_links - create links to all FIMC entities + * @fmd: fimc media device + * @source: the source entity to create links to all fimc entities from + * @sensor: sensor subdev linked to FIMC[fimc_id] entity, may be null + * @pad: the source entity pad index + * @link_mask: bitmask of the fimc devices for which link should be enabled + */ +static int __fimc_md_create_fimc_sink_links(struct fimc_md *fmd, + struct media_entity *source, + struct v4l2_subdev *sensor, + int pad, int link_mask) +{ + struct fimc_source_info *si = NULL; + struct media_entity *sink; + unsigned int flags = 0; + int i, ret = 0; + + if (sensor) { + si = v4l2_get_subdev_hostdata(sensor); + /* Skip direct FIMC links in the logical FIMC-IS sensor path */ + if (si && si->fimc_bus_type == FIMC_BUS_TYPE_ISP_WRITEBACK) + ret = 1; + } + + for (i = 0; !ret && i < FIMC_MAX_DEVS; i++) { + if (!fmd->fimc[i]) + continue; + /* + * Some FIMC variants are not fitted with camera capture + * interface. Skip creating a link from sensor for those. + */ + if (!fmd->fimc[i]->variant->has_cam_if) + continue; + + flags = ((1 << i) & link_mask) ? MEDIA_LNK_FL_ENABLED : 0; + + sink = &fmd->fimc[i]->vid_cap.subdev.entity; + ret = media_entity_create_link(source, pad, sink, + FIMC_SD_PAD_SINK_CAM, flags); + if (ret) + return ret; + + /* Notify FIMC capture subdev entity */ + ret = media_entity_call(sink, link_setup, &sink->pads[0], + &source->pads[pad], flags); + if (ret) + break; + + v4l2_info(&fmd->v4l2_dev, "created link [%s] %c> [%s]\n", + source->name, flags ? '=' : '-', sink->name); + } + + for (i = 0; i < FIMC_LITE_MAX_DEVS; i++) { + if (!fmd->fimc_lite[i]) + continue; + + sink = &fmd->fimc_lite[i]->subdev.entity; + ret = media_entity_create_link(source, pad, sink, + FLITE_SD_PAD_SINK, 0); + if (ret) + return ret; + + /* Notify FIMC-LITE subdev entity */ + ret = media_entity_call(sink, link_setup, &sink->pads[0], + &source->pads[pad], 0); + if (ret) + break; + + v4l2_info(&fmd->v4l2_dev, "created link [%s] -> [%s]\n", + source->name, sink->name); + } + return 0; +} + +/* Create links from FIMC-LITE source pads to other entities */ +static int __fimc_md_create_flite_source_links(struct fimc_md *fmd) +{ + struct media_entity *source, *sink; + int i, ret = 0; + + for (i = 0; i < FIMC_LITE_MAX_DEVS; i++) { + struct fimc_lite *fimc = fmd->fimc_lite[i]; + + if (fimc == NULL) + continue; + + source = &fimc->subdev.entity; + sink = &fimc->ve.vdev.entity; + /* FIMC-LITE's subdev and video node */ + ret = media_entity_create_link(source, FLITE_SD_PAD_SOURCE_DMA, + sink, 0, 0); + if (ret) + break; + /* Link from FIMC-LITE to IS-ISP subdev */ + sink = &fmd->fimc_is->isp.subdev.entity; + ret = media_entity_create_link(source, FLITE_SD_PAD_SOURCE_ISP, + sink, 0, 0); + if (ret) + break; + } + + return ret; +} + +/* Create FIMC-IS links */ +static int __fimc_md_create_fimc_is_links(struct fimc_md *fmd) +{ + struct fimc_isp *isp = &fmd->fimc_is->isp; + struct media_entity *source, *sink; + int i, ret; + + source = &isp->subdev.entity; + + for (i = 0; i < FIMC_MAX_DEVS; i++) { + if (fmd->fimc[i] == NULL) + continue; + + /* Link from FIMC-IS-ISP subdev to FIMC */ + sink = &fmd->fimc[i]->vid_cap.subdev.entity; + ret = media_entity_create_link(source, FIMC_ISP_SD_PAD_SRC_FIFO, + sink, FIMC_SD_PAD_SINK_FIFO, 0); + if (ret) + return ret; + } + + /* Link from FIMC-IS-ISP subdev to fimc-is-isp.capture video node */ + sink = &isp->video_capture.ve.vdev.entity; + + /* Skip this link if the fimc-is-isp video node driver isn't built-in */ + if (sink->num_pads == 0) + return 0; + + return media_entity_create_link(source, FIMC_ISP_SD_PAD_SRC_DMA, + sink, 0, 0); +} + +/** + * fimc_md_create_links - create default links between registered entities + * + * Parallel interface sensor entities are connected directly to FIMC capture + * entities. The sensors using MIPI CSIS bus are connected through immutable + * link with CSI receiver entity specified by mux_id. Any registered CSIS + * entity has a link to each registered FIMC capture entity. Enabled links + * are created by default between each subsequent registered sensor and + * subsequent FIMC capture entity. The number of default active links is + * determined by the number of available sensors or FIMC entities, + * whichever is less. + */ +static int fimc_md_create_links(struct fimc_md *fmd) +{ + struct v4l2_subdev *csi_sensors[CSIS_MAX_ENTITIES] = { NULL }; + struct v4l2_subdev *sensor, *csis; + struct fimc_source_info *pdata; + struct media_entity *source, *sink; + int i, pad, fimc_id = 0, ret = 0; + u32 flags, link_mask = 0; + + for (i = 0; i < fmd->num_sensors; i++) { + if (fmd->sensor[i].subdev == NULL) + continue; + + sensor = fmd->sensor[i].subdev; + pdata = v4l2_get_subdev_hostdata(sensor); + if (!pdata) + continue; + + source = NULL; + + switch (pdata->sensor_bus_type) { + case FIMC_BUS_TYPE_MIPI_CSI2: + if (WARN(pdata->mux_id >= CSIS_MAX_ENTITIES, + "Wrong CSI channel id: %d\n", pdata->mux_id)) + return -EINVAL; + + csis = fmd->csis[pdata->mux_id].sd; + if (WARN(csis == NULL, + "MIPI-CSI interface specified " + "but s5p-csis module is not loaded!\n")) + return -EINVAL; + + pad = sensor->entity.num_pads - 1; + ret = media_entity_create_link(&sensor->entity, pad, + &csis->entity, CSIS_PAD_SINK, + MEDIA_LNK_FL_IMMUTABLE | + MEDIA_LNK_FL_ENABLED); + if (ret) + return ret; + + v4l2_info(&fmd->v4l2_dev, "created link [%s] => [%s]\n", + sensor->entity.name, csis->entity.name); + + source = NULL; + csi_sensors[pdata->mux_id] = sensor; + break; + + case FIMC_BUS_TYPE_ITU_601...FIMC_BUS_TYPE_ITU_656: + source = &sensor->entity; + pad = 0; + break; + + default: + v4l2_err(&fmd->v4l2_dev, "Wrong bus_type: %x\n", + pdata->sensor_bus_type); + return -EINVAL; + } + if (source == NULL) + continue; + + link_mask = 1 << fimc_id++; + ret = __fimc_md_create_fimc_sink_links(fmd, source, sensor, + pad, link_mask); + } + + for (i = 0; i < CSIS_MAX_ENTITIES; i++) { + if (fmd->csis[i].sd == NULL) + continue; + + source = &fmd->csis[i].sd->entity; + pad = CSIS_PAD_SOURCE; + sensor = csi_sensors[i]; + + link_mask = 1 << fimc_id++; + ret = __fimc_md_create_fimc_sink_links(fmd, source, sensor, + pad, link_mask); + } + + /* Create immutable links between each FIMC's subdev and video node */ + flags = MEDIA_LNK_FL_IMMUTABLE | MEDIA_LNK_FL_ENABLED; + for (i = 0; i < FIMC_MAX_DEVS; i++) { + if (!fmd->fimc[i]) + continue; + + source = &fmd->fimc[i]->vid_cap.subdev.entity; + sink = &fmd->fimc[i]->vid_cap.ve.vdev.entity; + + ret = media_entity_create_link(source, FIMC_SD_PAD_SOURCE, + sink, 0, flags); + if (ret) + break; + } + + ret = __fimc_md_create_flite_source_links(fmd); + if (ret < 0) + return ret; + + if (fmd->use_isp) + ret = __fimc_md_create_fimc_is_links(fmd); + + return ret; +} + +/* + * The peripheral sensor and CAM_BLK (PIXELASYNCMx) clocks management. + */ +static void fimc_md_put_clocks(struct fimc_md *fmd) +{ + int i = FIMC_MAX_CAMCLKS; + + while (--i >= 0) { + if (IS_ERR(fmd->camclk[i].clock)) + continue; + clk_put(fmd->camclk[i].clock); + fmd->camclk[i].clock = ERR_PTR(-EINVAL); + } + + /* Writeback (PIXELASYNCMx) clocks */ + for (i = 0; i < FIMC_MAX_WBCLKS; i++) { + if (IS_ERR(fmd->wbclk[i])) + continue; + clk_put(fmd->wbclk[i]); + fmd->wbclk[i] = ERR_PTR(-EINVAL); + } +} + +static int fimc_md_get_clocks(struct fimc_md *fmd) +{ + struct device *dev = &fmd->pdev->dev; + char clk_name[32]; + struct clk *clock; + int i, ret = 0; + + for (i = 0; i < FIMC_MAX_CAMCLKS; i++) + fmd->camclk[i].clock = ERR_PTR(-EINVAL); + + for (i = 0; i < FIMC_MAX_CAMCLKS; i++) { + snprintf(clk_name, sizeof(clk_name), "sclk_cam%u", i); + clock = clk_get(dev, clk_name); + + if (IS_ERR(clock)) { + dev_err(dev, "Failed to get clock: %s\n", clk_name); + ret = PTR_ERR(clock); + break; + } + fmd->camclk[i].clock = clock; + } + if (ret) + fimc_md_put_clocks(fmd); + + if (!fmd->use_isp) + return 0; + /* + * For now get only PIXELASYNCM1 clock (Writeback B/ISP), + * leave PIXELASYNCM0 out for the LCD Writeback driver. + */ + fmd->wbclk[CLK_IDX_WB_A] = ERR_PTR(-EINVAL); + + for (i = CLK_IDX_WB_B; i < FIMC_MAX_WBCLKS; i++) { + snprintf(clk_name, sizeof(clk_name), "pxl_async%u", i); + clock = clk_get(dev, clk_name); + if (IS_ERR(clock)) { + v4l2_err(&fmd->v4l2_dev, "Failed to get clock: %s\n", + clk_name); + ret = PTR_ERR(clock); + break; + } + fmd->wbclk[i] = clock; + } + if (ret) + fimc_md_put_clocks(fmd); + + return ret; +} + +static int __fimc_md_modify_pipeline(struct media_entity *entity, bool enable) +{ + struct exynos_video_entity *ve; + struct fimc_pipeline *p; + struct video_device *vdev; + int ret; + + vdev = media_entity_to_video_device(entity); + if (vdev->entity.use_count == 0) + return 0; + + ve = vdev_to_exynos_video_entity(vdev); + p = to_fimc_pipeline(ve->pipe); + /* + * Nothing to do if we are disabling the pipeline, some link + * has been disconnected and p->subdevs array is cleared now. + */ + if (!enable && p->subdevs[IDX_SENSOR] == NULL) + return 0; + + if (enable) + ret = __fimc_pipeline_open(ve->pipe, entity, true); + else + ret = __fimc_pipeline_close(ve->pipe); + + if (ret == 0 && !enable) + memset(p->subdevs, 0, sizeof(p->subdevs)); + + return ret; +} + +/* Locking: called with entity->parent->graph_mutex mutex held. */ +static int __fimc_md_modify_pipelines(struct media_entity *entity, bool enable) +{ + struct media_entity *entity_err = entity; + struct media_entity_graph graph; + int ret; + + /* + * Walk current graph and call the pipeline open/close routine for each + * opened video node that belongs to the graph of entities connected + * through active links. This is needed as we cannot power on/off the + * subdevs in random order. + */ + media_entity_graph_walk_start(&graph, entity); + + while ((entity = media_entity_graph_walk_next(&graph))) { + if (media_entity_type(entity) != MEDIA_ENT_T_DEVNODE) + continue; + + ret = __fimc_md_modify_pipeline(entity, enable); + + if (ret < 0) + goto err; + } + + return 0; + err: + media_entity_graph_walk_start(&graph, entity_err); + + while ((entity_err = media_entity_graph_walk_next(&graph))) { + if (media_entity_type(entity_err) != MEDIA_ENT_T_DEVNODE) + continue; + + __fimc_md_modify_pipeline(entity_err, !enable); + + if (entity_err == entity) + break; + } + + return ret; +} + +static int fimc_md_link_notify(struct media_link *link, unsigned int flags, + unsigned int notification) +{ + struct media_entity *sink = link->sink->entity; + int ret = 0; + + /* Before link disconnection */ + if (notification == MEDIA_DEV_NOTIFY_PRE_LINK_CH) { + if (!(flags & MEDIA_LNK_FL_ENABLED)) + ret = __fimc_md_modify_pipelines(sink, false); +#if 0 + else + /* TODO: Link state change validation */ +#endif + /* After link activation */ + } else if (notification == MEDIA_DEV_NOTIFY_POST_LINK_CH && + (link->flags & MEDIA_LNK_FL_ENABLED)) { + ret = __fimc_md_modify_pipelines(sink, true); + } + + return ret ? -EPIPE : 0; +} + +static ssize_t fimc_md_sysfs_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct platform_device *pdev = to_platform_device(dev); + struct fimc_md *fmd = platform_get_drvdata(pdev); + + if (fmd->user_subdev_api) + return strlcpy(buf, "Sub-device API (sub-dev)\n", PAGE_SIZE); + + return strlcpy(buf, "V4L2 video node only API (vid-dev)\n", PAGE_SIZE); +} + +static ssize_t fimc_md_sysfs_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct platform_device *pdev = to_platform_device(dev); + struct fimc_md *fmd = platform_get_drvdata(pdev); + bool subdev_api; + int i; + + if (!strcmp(buf, "vid-dev\n")) + subdev_api = false; + else if (!strcmp(buf, "sub-dev\n")) + subdev_api = true; + else + return count; + + fmd->user_subdev_api = subdev_api; + for (i = 0; i < FIMC_MAX_DEVS; i++) + if (fmd->fimc[i]) + fmd->fimc[i]->vid_cap.user_subdev_api = subdev_api; + return count; +} +/* + * This device attribute is to select video pipeline configuration method. + * There are following valid values: + * vid-dev - for V4L2 video node API only, subdevice will be configured + * by the host driver. + * sub-dev - for media controller API, subdevs must be configured in user + * space before starting streaming. + */ +static DEVICE_ATTR(subdev_conf_mode, S_IWUSR | S_IRUGO, + fimc_md_sysfs_show, fimc_md_sysfs_store); + +static int fimc_md_get_pinctrl(struct fimc_md *fmd) +{ + struct device *dev = &fmd->pdev->dev; + struct fimc_pinctrl *pctl = &fmd->pinctl; + + pctl->pinctrl = devm_pinctrl_get(dev); + if (IS_ERR(pctl->pinctrl)) + return PTR_ERR(pctl->pinctrl); + + pctl->state_default = pinctrl_lookup_state(pctl->pinctrl, + PINCTRL_STATE_DEFAULT); + if (IS_ERR(pctl->state_default)) + return PTR_ERR(pctl->state_default); + + pctl->state_idle = pinctrl_lookup_state(pctl->pinctrl, + PINCTRL_STATE_IDLE); + return 0; +} + +static int cam_clk_prepare(struct clk_hw *hw) +{ + struct cam_clk *camclk = to_cam_clk(hw); + int ret; + + if (camclk->fmd->pmf == NULL) + return -ENODEV; + + ret = pm_runtime_get_sync(camclk->fmd->pmf); + return ret < 0 ? ret : 0; +} + +static void cam_clk_unprepare(struct clk_hw *hw) +{ + struct cam_clk *camclk = to_cam_clk(hw); + + if (camclk->fmd->pmf == NULL) + return; + + pm_runtime_put_sync(camclk->fmd->pmf); +} + +static const struct clk_ops cam_clk_ops = { + .prepare = cam_clk_prepare, + .unprepare = cam_clk_unprepare, +}; + +static void fimc_md_unregister_clk_provider(struct fimc_md *fmd) +{ + struct cam_clk_provider *cp = &fmd->clk_provider; + unsigned int i; + + if (cp->of_node) + of_clk_del_provider(cp->of_node); + + for (i = 0; i < cp->num_clocks; i++) + clk_unregister(cp->clks[i]); +} + +static int fimc_md_register_clk_provider(struct fimc_md *fmd) +{ + struct cam_clk_provider *cp = &fmd->clk_provider; + struct device *dev = &fmd->pdev->dev; + int i, ret; + + for (i = 0; i < FIMC_MAX_CAMCLKS; i++) { + struct cam_clk *camclk = &cp->camclk[i]; + struct clk_init_data init; + const char *p_name; + + ret = of_property_read_string_index(dev->of_node, + "clock-output-names", i, &init.name); + if (ret < 0) + break; + + p_name = __clk_get_name(fmd->camclk[i].clock); + + /* It's safe since clk_register() will duplicate the string. */ + init.parent_names = &p_name; + init.num_parents = 1; + init.ops = &cam_clk_ops; + init.flags = CLK_SET_RATE_PARENT; + camclk->hw.init = &init; + camclk->fmd = fmd; + + cp->clks[i] = clk_register(NULL, &camclk->hw); + if (IS_ERR(cp->clks[i])) { + dev_err(dev, "failed to register clock: %s (%ld)\n", + init.name, PTR_ERR(cp->clks[i])); + ret = PTR_ERR(cp->clks[i]); + goto err; + } + cp->num_clocks++; + } + + if (cp->num_clocks == 0) { + dev_warn(dev, "clk provider not registered\n"); + return 0; + } + + cp->clk_data.clks = cp->clks; + cp->clk_data.clk_num = cp->num_clocks; + cp->of_node = dev->of_node; + ret = of_clk_add_provider(dev->of_node, of_clk_src_onecell_get, + &cp->clk_data); + if (ret == 0) + return 0; +err: + fimc_md_unregister_clk_provider(fmd); + return ret; +} + +static int subdev_notifier_bound(struct v4l2_async_notifier *notifier, + struct v4l2_subdev *subdev, + struct v4l2_async_subdev *asd) +{ + struct fimc_md *fmd = notifier_to_fimc_md(notifier); + struct fimc_sensor_info *si = NULL; + int i; + + /* Find platform data for this sensor subdev */ + for (i = 0; i < ARRAY_SIZE(fmd->sensor); i++) + if (fmd->sensor[i].asd.match.of.node == subdev->dev->of_node) + si = &fmd->sensor[i]; + + if (si == NULL) + return -EINVAL; + + v4l2_set_subdev_hostdata(subdev, &si->pdata); + + if (si->pdata.fimc_bus_type == FIMC_BUS_TYPE_ISP_WRITEBACK) + subdev->grp_id = GRP_ID_FIMC_IS_SENSOR; + else + subdev->grp_id = GRP_ID_SENSOR; + + si->subdev = subdev; + + v4l2_info(&fmd->v4l2_dev, "Registered sensor subdevice: %s (%d)\n", + subdev->name, fmd->num_sensors); + + fmd->num_sensors++; + + return 0; +} + +static int subdev_notifier_complete(struct v4l2_async_notifier *notifier) +{ + struct fimc_md *fmd = notifier_to_fimc_md(notifier); + int ret; + + mutex_lock(&fmd->media_dev.graph_mutex); + + ret = fimc_md_create_links(fmd); + if (ret < 0) + goto unlock; + + ret = v4l2_device_register_subdev_nodes(&fmd->v4l2_dev); +unlock: + mutex_unlock(&fmd->media_dev.graph_mutex); + return ret; +} + +static int fimc_md_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct v4l2_device *v4l2_dev; + struct fimc_md *fmd; + int ret; + + fmd = devm_kzalloc(dev, sizeof(*fmd), GFP_KERNEL); + if (!fmd) + return -ENOMEM; + + spin_lock_init(&fmd->slock); + INIT_LIST_HEAD(&fmd->pipelines); + fmd->pdev = pdev; + + strlcpy(fmd->media_dev.model, "SAMSUNG S5P FIMC", + sizeof(fmd->media_dev.model)); + fmd->media_dev.link_notify = fimc_md_link_notify; + fmd->media_dev.dev = dev; + + v4l2_dev = &fmd->v4l2_dev; + v4l2_dev->mdev = &fmd->media_dev; + v4l2_dev->notify = fimc_sensor_notify; + strlcpy(v4l2_dev->name, "s5p-fimc-md", sizeof(v4l2_dev->name)); + + fmd->use_isp = fimc_md_is_isp_available(dev->of_node); + fmd->user_subdev_api = true; + + ret = v4l2_device_register(dev, &fmd->v4l2_dev); + if (ret < 0) { + v4l2_err(v4l2_dev, "Failed to register v4l2_device: %d\n", ret); + return ret; + } + + ret = media_device_register(&fmd->media_dev); + if (ret < 0) { + v4l2_err(v4l2_dev, "Failed to register media device: %d\n", ret); + goto err_v4l2_dev; + } + + ret = fimc_md_get_clocks(fmd); + if (ret) + goto err_md; + + ret = fimc_md_get_pinctrl(fmd); + if (ret < 0) { + if (ret != EPROBE_DEFER) + dev_err(dev, "Failed to get pinctrl: %d\n", ret); + goto err_clk; + } + + platform_set_drvdata(pdev, fmd); + + /* Protect the media graph while we're registering entities */ + mutex_lock(&fmd->media_dev.graph_mutex); + + ret = fimc_md_register_platform_entities(fmd, dev->of_node); + if (ret) { + mutex_unlock(&fmd->media_dev.graph_mutex); + goto err_clk; + } + + ret = fimc_md_register_sensor_entities(fmd); + if (ret) { + mutex_unlock(&fmd->media_dev.graph_mutex); + goto err_m_ent; + } + + mutex_unlock(&fmd->media_dev.graph_mutex); + + ret = device_create_file(&pdev->dev, &dev_attr_subdev_conf_mode); + if (ret) + goto err_m_ent; + /* + * FIMC platform devices need to be registered before the sclk_cam + * clocks provider, as one of these devices needs to be activated + * to enable the clock. + */ + ret = fimc_md_register_clk_provider(fmd); + if (ret < 0) { + v4l2_err(v4l2_dev, "clock provider registration failed\n"); + goto err_attr; + } + + if (fmd->num_sensors > 0) { + fmd->subdev_notifier.subdevs = fmd->async_subdevs; + fmd->subdev_notifier.num_subdevs = fmd->num_sensors; + fmd->subdev_notifier.bound = subdev_notifier_bound; + fmd->subdev_notifier.complete = subdev_notifier_complete; + fmd->num_sensors = 0; + + ret = v4l2_async_notifier_register(&fmd->v4l2_dev, + &fmd->subdev_notifier); + if (ret) + goto err_clk_p; + } + + return 0; + +err_clk_p: + fimc_md_unregister_clk_provider(fmd); +err_attr: + device_remove_file(&pdev->dev, &dev_attr_subdev_conf_mode); +err_clk: + fimc_md_put_clocks(fmd); +err_m_ent: + fimc_md_unregister_entities(fmd); +err_md: + media_device_unregister(&fmd->media_dev); +err_v4l2_dev: + v4l2_device_unregister(&fmd->v4l2_dev); + return ret; +} + +static int fimc_md_remove(struct platform_device *pdev) +{ + struct fimc_md *fmd = platform_get_drvdata(pdev); + + if (!fmd) + return 0; + + fimc_md_unregister_clk_provider(fmd); + v4l2_async_notifier_unregister(&fmd->subdev_notifier); + + v4l2_device_unregister(&fmd->v4l2_dev); + device_remove_file(&pdev->dev, &dev_attr_subdev_conf_mode); + fimc_md_unregister_entities(fmd); + fimc_md_pipelines_free(fmd); + media_device_unregister(&fmd->media_dev); + fimc_md_put_clocks(fmd); + + return 0; +} + +static struct platform_device_id fimc_driver_ids[] __always_unused = { + { .name = "s5p-fimc-md" }, + { }, +}; +MODULE_DEVICE_TABLE(platform, fimc_driver_ids); + +static const struct of_device_id fimc_md_of_match[] = { + { .compatible = "samsung,fimc" }, + { }, +}; +MODULE_DEVICE_TABLE(of, fimc_md_of_match); + +static struct platform_driver fimc_md_driver = { + .probe = fimc_md_probe, + .remove = fimc_md_remove, + .driver = { + .of_match_table = of_match_ptr(fimc_md_of_match), + .name = "s5p-fimc-md", + } +}; + +static int __init fimc_md_init(void) +{ + int ret; + + request_module("s5p-csis"); + ret = fimc_register_driver(); + if (ret) + return ret; + + return platform_driver_register(&fimc_md_driver); +} + +static void __exit fimc_md_exit(void) +{ + platform_driver_unregister(&fimc_md_driver); + fimc_unregister_driver(); +} + +module_init(fimc_md_init); +module_exit(fimc_md_exit); + +MODULE_AUTHOR("Sylwester Nawrocki <s.nawrocki@samsung.com>"); +MODULE_DESCRIPTION("S5P FIMC camera host interface/video postprocessor driver"); +MODULE_LICENSE("GPL"); +MODULE_VERSION("2.0.1"); |