| // SPDX-License-Identifier: GPL-2.0+ |
| /* |
| * Driver for Cadence MIPI-CSI2 TX Controller |
| * |
| * Copyright (C) 2017-2018 Cadence Design Systems Inc. |
| */ |
| |
| #include <linux/clk.h> |
| #include <linux/delay.h> |
| #include <linux/io.h> |
| #include <linux/module.h> |
| #include <linux/mutex.h> |
| #include <linux/of.h> |
| #include <linux/of_graph.h> |
| #include <linux/platform_device.h> |
| #include <linux/slab.h> |
| |
| #include <media/v4l2-ctrls.h> |
| #include <media/v4l2-device.h> |
| #include <media/v4l2-fwnode.h> |
| #include <media/v4l2-subdev.h> |
| |
| #define CSI2TX_DEVICE_CONFIG_REG 0x00 |
| #define CSI2TX_DEVICE_CONFIG_STREAMS_MASK GENMASK(6, 4) |
| #define CSI2TX_DEVICE_CONFIG_HAS_DPHY BIT(3) |
| #define CSI2TX_DEVICE_CONFIG_LANES_MASK GENMASK(2, 0) |
| |
| #define CSI2TX_CONFIG_REG 0x20 |
| #define CSI2TX_CONFIG_CFG_REQ BIT(2) |
| #define CSI2TX_CONFIG_SRST_REQ BIT(1) |
| |
| #define CSI2TX_DPHY_CFG_REG 0x28 |
| #define CSI2TX_DPHY_CFG_CLK_RESET BIT(16) |
| #define CSI2TX_DPHY_CFG_LANE_RESET(n) BIT((n) + 12) |
| #define CSI2TX_DPHY_CFG_MODE_MASK GENMASK(9, 8) |
| #define CSI2TX_DPHY_CFG_MODE_LPDT (2 << 8) |
| #define CSI2TX_DPHY_CFG_MODE_HS (1 << 8) |
| #define CSI2TX_DPHY_CFG_MODE_ULPS (0 << 8) |
| #define CSI2TX_DPHY_CFG_CLK_ENABLE BIT(4) |
| #define CSI2TX_DPHY_CFG_LANE_ENABLE(n) BIT(n) |
| |
| #define CSI2TX_DPHY_CLK_WAKEUP_REG 0x2c |
| #define CSI2TX_DPHY_CLK_WAKEUP_ULPS_CYCLES(n) ((n) & 0xffff) |
| |
| #define CSI2TX_DT_CFG_REG(n) (0x80 + (n) * 8) |
| #define CSI2TX_DT_CFG_DT(n) (((n) & 0x3f) << 2) |
| |
| #define CSI2TX_DT_FORMAT_REG(n) (0x84 + (n) * 8) |
| #define CSI2TX_DT_FORMAT_BYTES_PER_LINE(n) (((n) & 0xffff) << 16) |
| #define CSI2TX_DT_FORMAT_MAX_LINE_NUM(n) ((n) & 0xffff) |
| |
| #define CSI2TX_STREAM_IF_CFG_REG(n) (0x100 + (n) * 4) |
| #define CSI2TX_STREAM_IF_CFG_FILL_LEVEL(n) ((n) & 0x1f) |
| |
| #define CSI2TX_LANES_MAX 4 |
| #define CSI2TX_STREAMS_MAX 4 |
| |
| enum csi2tx_pads { |
| CSI2TX_PAD_SOURCE, |
| CSI2TX_PAD_SINK_STREAM0, |
| CSI2TX_PAD_SINK_STREAM1, |
| CSI2TX_PAD_SINK_STREAM2, |
| CSI2TX_PAD_SINK_STREAM3, |
| CSI2TX_PAD_MAX, |
| }; |
| |
| struct csi2tx_fmt { |
| u32 mbus; |
| u32 dt; |
| u32 bpp; |
| }; |
| |
| struct csi2tx_priv { |
| struct device *dev; |
| unsigned int count; |
| |
| /* |
| * Used to prevent race conditions between multiple, |
| * concurrent calls to start and stop. |
| */ |
| struct mutex lock; |
| |
| void __iomem *base; |
| |
| struct clk *esc_clk; |
| struct clk *p_clk; |
| struct clk *pixel_clk[CSI2TX_STREAMS_MAX]; |
| |
| struct v4l2_subdev subdev; |
| struct media_pad pads[CSI2TX_PAD_MAX]; |
| struct v4l2_mbus_framefmt pad_fmts[CSI2TX_PAD_MAX]; |
| |
| bool has_internal_dphy; |
| u8 lanes[CSI2TX_LANES_MAX]; |
| unsigned int num_lanes; |
| unsigned int max_lanes; |
| unsigned int max_streams; |
| }; |
| |
| static const struct csi2tx_fmt csi2tx_formats[] = { |
| { |
| .mbus = MEDIA_BUS_FMT_UYVY8_1X16, |
| .bpp = 2, |
| .dt = 0x1e, |
| }, |
| { |
| .mbus = MEDIA_BUS_FMT_RGB888_1X24, |
| .bpp = 3, |
| .dt = 0x24, |
| }, |
| }; |
| |
| static const struct v4l2_mbus_framefmt fmt_default = { |
| .width = 1280, |
| .height = 720, |
| .code = MEDIA_BUS_FMT_RGB888_1X24, |
| .field = V4L2_FIELD_NONE, |
| .colorspace = V4L2_COLORSPACE_DEFAULT, |
| }; |
| |
| static inline |
| struct csi2tx_priv *v4l2_subdev_to_csi2tx(struct v4l2_subdev *subdev) |
| { |
| return container_of(subdev, struct csi2tx_priv, subdev); |
| } |
| |
| static const struct csi2tx_fmt *csi2tx_get_fmt_from_mbus(u32 mbus) |
| { |
| unsigned int i; |
| |
| for (i = 0; i < ARRAY_SIZE(csi2tx_formats); i++) |
| if (csi2tx_formats[i].mbus == mbus) |
| return &csi2tx_formats[i]; |
| |
| return NULL; |
| } |
| |
| static int csi2tx_enum_mbus_code(struct v4l2_subdev *subdev, |
| struct v4l2_subdev_pad_config *cfg, |
| struct v4l2_subdev_mbus_code_enum *code) |
| { |
| if (code->pad || code->index >= ARRAY_SIZE(csi2tx_formats)) |
| return -EINVAL; |
| |
| code->code = csi2tx_formats[code->index].mbus; |
| |
| return 0; |
| } |
| |
| static struct v4l2_mbus_framefmt * |
| __csi2tx_get_pad_format(struct v4l2_subdev *subdev, |
| struct v4l2_subdev_pad_config *cfg, |
| struct v4l2_subdev_format *fmt) |
| { |
| struct csi2tx_priv *csi2tx = v4l2_subdev_to_csi2tx(subdev); |
| |
| if (fmt->which == V4L2_SUBDEV_FORMAT_TRY) |
| return v4l2_subdev_get_try_format(subdev, cfg, |
| fmt->pad); |
| |
| return &csi2tx->pad_fmts[fmt->pad]; |
| } |
| |
| static int csi2tx_get_pad_format(struct v4l2_subdev *subdev, |
| struct v4l2_subdev_pad_config *cfg, |
| struct v4l2_subdev_format *fmt) |
| { |
| const struct v4l2_mbus_framefmt *format; |
| |
| /* Multiplexed pad? */ |
| if (fmt->pad == CSI2TX_PAD_SOURCE) |
| return -EINVAL; |
| |
| format = __csi2tx_get_pad_format(subdev, cfg, fmt); |
| if (!format) |
| return -EINVAL; |
| |
| fmt->format = *format; |
| |
| return 0; |
| } |
| |
| static int csi2tx_set_pad_format(struct v4l2_subdev *subdev, |
| struct v4l2_subdev_pad_config *cfg, |
| struct v4l2_subdev_format *fmt) |
| { |
| const struct v4l2_mbus_framefmt *src_format = &fmt->format; |
| struct v4l2_mbus_framefmt *dst_format; |
| |
| /* Multiplexed pad? */ |
| if (fmt->pad == CSI2TX_PAD_SOURCE) |
| return -EINVAL; |
| |
| if (!csi2tx_get_fmt_from_mbus(fmt->format.code)) |
| src_format = &fmt_default; |
| |
| dst_format = __csi2tx_get_pad_format(subdev, cfg, fmt); |
| if (!dst_format) |
| return -EINVAL; |
| |
| *dst_format = *src_format; |
| |
| return 0; |
| } |
| |
| static const struct v4l2_subdev_pad_ops csi2tx_pad_ops = { |
| .enum_mbus_code = csi2tx_enum_mbus_code, |
| .get_fmt = csi2tx_get_pad_format, |
| .set_fmt = csi2tx_set_pad_format, |
| }; |
| |
| static void csi2tx_reset(struct csi2tx_priv *csi2tx) |
| { |
| writel(CSI2TX_CONFIG_SRST_REQ, csi2tx->base + CSI2TX_CONFIG_REG); |
| |
| udelay(10); |
| } |
| |
| static int csi2tx_start(struct csi2tx_priv *csi2tx) |
| { |
| struct media_entity *entity = &csi2tx->subdev.entity; |
| struct media_link *link; |
| unsigned int i; |
| u32 reg; |
| |
| csi2tx_reset(csi2tx); |
| |
| writel(CSI2TX_CONFIG_CFG_REQ, csi2tx->base + CSI2TX_CONFIG_REG); |
| |
| udelay(10); |
| |
| /* Configure our PPI interface with the D-PHY */ |
| writel(CSI2TX_DPHY_CLK_WAKEUP_ULPS_CYCLES(32), |
| csi2tx->base + CSI2TX_DPHY_CLK_WAKEUP_REG); |
| |
| /* Put our lanes (clock and data) out of reset */ |
| reg = CSI2TX_DPHY_CFG_CLK_RESET | CSI2TX_DPHY_CFG_MODE_LPDT; |
| for (i = 0; i < csi2tx->num_lanes; i++) |
| reg |= CSI2TX_DPHY_CFG_LANE_RESET(csi2tx->lanes[i]); |
| writel(reg, csi2tx->base + CSI2TX_DPHY_CFG_REG); |
| |
| udelay(10); |
| |
| /* Enable our (clock and data) lanes */ |
| reg |= CSI2TX_DPHY_CFG_CLK_ENABLE; |
| for (i = 0; i < csi2tx->num_lanes; i++) |
| reg |= CSI2TX_DPHY_CFG_LANE_ENABLE(csi2tx->lanes[i]); |
| writel(reg, csi2tx->base + CSI2TX_DPHY_CFG_REG); |
| |
| udelay(10); |
| |
| /* Switch to HS mode */ |
| reg &= ~CSI2TX_DPHY_CFG_MODE_MASK; |
| writel(reg | CSI2TX_DPHY_CFG_MODE_HS, |
| csi2tx->base + CSI2TX_DPHY_CFG_REG); |
| |
| udelay(10); |
| |
| /* |
| * Create a static mapping between the CSI virtual channels |
| * and the input streams. |
| * |
| * This should be enhanced, but v4l2 lacks the support for |
| * changing that mapping dynamically at the moment. |
| * |
| * We're protected from the userspace setting up links at the |
| * same time by the upper layer having called |
| * media_pipeline_start(). |
| */ |
| list_for_each_entry(link, &entity->links, list) { |
| struct v4l2_mbus_framefmt *mfmt; |
| const struct csi2tx_fmt *fmt; |
| unsigned int stream; |
| int pad_idx = -1; |
| |
| /* Only consider our enabled input pads */ |
| for (i = CSI2TX_PAD_SINK_STREAM0; i < CSI2TX_PAD_MAX; i++) { |
| struct media_pad *pad = &csi2tx->pads[i]; |
| |
| if ((pad == link->sink) && |
| (link->flags & MEDIA_LNK_FL_ENABLED)) { |
| pad_idx = i; |
| break; |
| } |
| } |
| |
| if (pad_idx < 0) |
| continue; |
| |
| mfmt = &csi2tx->pad_fmts[pad_idx]; |
| fmt = csi2tx_get_fmt_from_mbus(mfmt->code); |
| if (!fmt) |
| continue; |
| |
| stream = pad_idx - CSI2TX_PAD_SINK_STREAM0; |
| |
| /* |
| * We use the stream ID there, but it's wrong. |
| * |
| * A stream could very well send a data type that is |
| * not equal to its stream ID. We need to find a |
| * proper way to address it. |
| */ |
| writel(CSI2TX_DT_CFG_DT(fmt->dt), |
| csi2tx->base + CSI2TX_DT_CFG_REG(stream)); |
| |
| writel(CSI2TX_DT_FORMAT_BYTES_PER_LINE(mfmt->width * fmt->bpp) | |
| CSI2TX_DT_FORMAT_MAX_LINE_NUM(mfmt->height + 1), |
| csi2tx->base + CSI2TX_DT_FORMAT_REG(stream)); |
| |
| /* |
| * TODO: This needs to be calculated based on the |
| * output CSI2 clock rate. |
| */ |
| writel(CSI2TX_STREAM_IF_CFG_FILL_LEVEL(4), |
| csi2tx->base + CSI2TX_STREAM_IF_CFG_REG(stream)); |
| } |
| |
| /* Disable the configuration mode */ |
| writel(0, csi2tx->base + CSI2TX_CONFIG_REG); |
| |
| return 0; |
| } |
| |
| static void csi2tx_stop(struct csi2tx_priv *csi2tx) |
| { |
| writel(CSI2TX_CONFIG_CFG_REQ | CSI2TX_CONFIG_SRST_REQ, |
| csi2tx->base + CSI2TX_CONFIG_REG); |
| } |
| |
| static int csi2tx_s_stream(struct v4l2_subdev *subdev, int enable) |
| { |
| struct csi2tx_priv *csi2tx = v4l2_subdev_to_csi2tx(subdev); |
| int ret = 0; |
| |
| mutex_lock(&csi2tx->lock); |
| |
| if (enable) { |
| /* |
| * If we're not the first users, there's no need to |
| * enable the whole controller. |
| */ |
| if (!csi2tx->count) { |
| ret = csi2tx_start(csi2tx); |
| if (ret) |
| goto out; |
| } |
| |
| csi2tx->count++; |
| } else { |
| csi2tx->count--; |
| |
| /* |
| * Let the last user turn off the lights. |
| */ |
| if (!csi2tx->count) |
| csi2tx_stop(csi2tx); |
| } |
| |
| out: |
| mutex_unlock(&csi2tx->lock); |
| return ret; |
| } |
| |
| static const struct v4l2_subdev_video_ops csi2tx_video_ops = { |
| .s_stream = csi2tx_s_stream, |
| }; |
| |
| static const struct v4l2_subdev_ops csi2tx_subdev_ops = { |
| .pad = &csi2tx_pad_ops, |
| .video = &csi2tx_video_ops, |
| }; |
| |
| static int csi2tx_get_resources(struct csi2tx_priv *csi2tx, |
| struct platform_device *pdev) |
| { |
| struct resource *res; |
| unsigned int i; |
| u32 dev_cfg; |
| |
| res = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| csi2tx->base = devm_ioremap_resource(&pdev->dev, res); |
| if (IS_ERR(csi2tx->base)) |
| return PTR_ERR(csi2tx->base); |
| |
| csi2tx->p_clk = devm_clk_get(&pdev->dev, "p_clk"); |
| if (IS_ERR(csi2tx->p_clk)) { |
| dev_err(&pdev->dev, "Couldn't get p_clk\n"); |
| return PTR_ERR(csi2tx->p_clk); |
| } |
| |
| csi2tx->esc_clk = devm_clk_get(&pdev->dev, "esc_clk"); |
| if (IS_ERR(csi2tx->esc_clk)) { |
| dev_err(&pdev->dev, "Couldn't get the esc_clk\n"); |
| return PTR_ERR(csi2tx->esc_clk); |
| } |
| |
| clk_prepare_enable(csi2tx->p_clk); |
| dev_cfg = readl(csi2tx->base + CSI2TX_DEVICE_CONFIG_REG); |
| clk_disable_unprepare(csi2tx->p_clk); |
| |
| csi2tx->max_lanes = dev_cfg & CSI2TX_DEVICE_CONFIG_LANES_MASK; |
| if (csi2tx->max_lanes > CSI2TX_LANES_MAX) { |
| dev_err(&pdev->dev, "Invalid number of lanes: %u\n", |
| csi2tx->max_lanes); |
| return -EINVAL; |
| } |
| |
| csi2tx->max_streams = (dev_cfg & CSI2TX_DEVICE_CONFIG_STREAMS_MASK) >> 4; |
| if (csi2tx->max_streams > CSI2TX_STREAMS_MAX) { |
| dev_err(&pdev->dev, "Invalid number of streams: %u\n", |
| csi2tx->max_streams); |
| return -EINVAL; |
| } |
| |
| csi2tx->has_internal_dphy = !!(dev_cfg & CSI2TX_DEVICE_CONFIG_HAS_DPHY); |
| |
| for (i = 0; i < csi2tx->max_streams; i++) { |
| char clk_name[16]; |
| |
| snprintf(clk_name, sizeof(clk_name), "pixel_if%u_clk", i); |
| csi2tx->pixel_clk[i] = devm_clk_get(&pdev->dev, clk_name); |
| if (IS_ERR(csi2tx->pixel_clk[i])) { |
| dev_err(&pdev->dev, "Couldn't get clock %s\n", |
| clk_name); |
| return PTR_ERR(csi2tx->pixel_clk[i]); |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int csi2tx_check_lanes(struct csi2tx_priv *csi2tx) |
| { |
| struct v4l2_fwnode_endpoint v4l2_ep; |
| struct device_node *ep; |
| int ret; |
| |
| ep = of_graph_get_endpoint_by_regs(csi2tx->dev->of_node, 0, 0); |
| if (!ep) |
| return -EINVAL; |
| |
| ret = v4l2_fwnode_endpoint_parse(of_fwnode_handle(ep), &v4l2_ep); |
| if (ret) { |
| dev_err(csi2tx->dev, "Could not parse v4l2 endpoint\n"); |
| goto out; |
| } |
| |
| if (v4l2_ep.bus_type != V4L2_MBUS_CSI2) { |
| dev_err(csi2tx->dev, "Unsupported media bus type: 0x%x\n", |
| v4l2_ep.bus_type); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| csi2tx->num_lanes = v4l2_ep.bus.mipi_csi2.num_data_lanes; |
| if (csi2tx->num_lanes > csi2tx->max_lanes) { |
| dev_err(csi2tx->dev, |
| "Current configuration uses more lanes than supported\n"); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| memcpy(csi2tx->lanes, v4l2_ep.bus.mipi_csi2.data_lanes, |
| sizeof(csi2tx->lanes)); |
| |
| out: |
| of_node_put(ep); |
| return ret; |
| } |
| |
| static int csi2tx_probe(struct platform_device *pdev) |
| { |
| struct csi2tx_priv *csi2tx; |
| unsigned int i; |
| int ret; |
| |
| csi2tx = kzalloc(sizeof(*csi2tx), GFP_KERNEL); |
| if (!csi2tx) |
| return -ENOMEM; |
| platform_set_drvdata(pdev, csi2tx); |
| mutex_init(&csi2tx->lock); |
| csi2tx->dev = &pdev->dev; |
| |
| ret = csi2tx_get_resources(csi2tx, pdev); |
| if (ret) |
| goto err_free_priv; |
| |
| v4l2_subdev_init(&csi2tx->subdev, &csi2tx_subdev_ops); |
| csi2tx->subdev.owner = THIS_MODULE; |
| csi2tx->subdev.dev = &pdev->dev; |
| csi2tx->subdev.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE; |
| snprintf(csi2tx->subdev.name, V4L2_SUBDEV_NAME_SIZE, "%s.%s", |
| KBUILD_MODNAME, dev_name(&pdev->dev)); |
| |
| ret = csi2tx_check_lanes(csi2tx); |
| if (ret) |
| goto err_free_priv; |
| |
| /* Create our media pads */ |
| csi2tx->subdev.entity.function = MEDIA_ENT_F_VID_IF_BRIDGE; |
| csi2tx->pads[CSI2TX_PAD_SOURCE].flags = MEDIA_PAD_FL_SOURCE; |
| for (i = CSI2TX_PAD_SINK_STREAM0; i < CSI2TX_PAD_MAX; i++) |
| csi2tx->pads[i].flags = MEDIA_PAD_FL_SINK; |
| |
| /* |
| * Only the input pads are considered to have a format at the |
| * moment. The CSI link can multiplex various streams with |
| * different formats, and we can't expose this in v4l2 right |
| * now. |
| */ |
| for (i = CSI2TX_PAD_SINK_STREAM0; i < CSI2TX_PAD_MAX; i++) |
| csi2tx->pad_fmts[i] = fmt_default; |
| |
| ret = media_entity_pads_init(&csi2tx->subdev.entity, CSI2TX_PAD_MAX, |
| csi2tx->pads); |
| if (ret) |
| goto err_free_priv; |
| |
| ret = v4l2_async_register_subdev(&csi2tx->subdev); |
| if (ret < 0) |
| goto err_free_priv; |
| |
| dev_info(&pdev->dev, |
| "Probed CSI2TX with %u/%u lanes, %u streams, %s D-PHY\n", |
| csi2tx->num_lanes, csi2tx->max_lanes, csi2tx->max_streams, |
| csi2tx->has_internal_dphy ? "internal" : "no"); |
| |
| return 0; |
| |
| err_free_priv: |
| kfree(csi2tx); |
| return ret; |
| } |
| |
| static int csi2tx_remove(struct platform_device *pdev) |
| { |
| struct csi2tx_priv *csi2tx = platform_get_drvdata(pdev); |
| |
| v4l2_async_unregister_subdev(&csi2tx->subdev); |
| kfree(csi2tx); |
| |
| return 0; |
| } |
| |
| static const struct of_device_id csi2tx_of_table[] = { |
| { .compatible = "cdns,csi2tx" }, |
| { }, |
| }; |
| MODULE_DEVICE_TABLE(of, csi2tx_of_table); |
| |
| static struct platform_driver csi2tx_driver = { |
| .probe = csi2tx_probe, |
| .remove = csi2tx_remove, |
| |
| .driver = { |
| .name = "cdns-csi2tx", |
| .of_match_table = csi2tx_of_table, |
| }, |
| }; |
| module_platform_driver(csi2tx_driver); |
| MODULE_AUTHOR("Maxime Ripard <maxime.ripard@bootlin.com>"); |
| MODULE_DESCRIPTION("Cadence CSI2-TX controller"); |
| MODULE_LICENSE("GPL"); |