| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * i.MX8 ISI - Input crossbar switch |
| * |
| * Copyright (c) 2022 Laurent Pinchart <laurent.pinchart@ideasonboard.com> |
| */ |
| |
| #include <linux/device.h> |
| #include <linux/errno.h> |
| #include <linux/kernel.h> |
| #include <linux/minmax.h> |
| #include <linux/regmap.h> |
| #include <linux/slab.h> |
| #include <linux/string.h> |
| #include <linux/types.h> |
| |
| #include <media/media-entity.h> |
| #include <media/v4l2-subdev.h> |
| |
| #include "imx8-isi-core.h" |
| |
| static inline struct mxc_isi_crossbar *to_isi_crossbar(struct v4l2_subdev *sd) |
| { |
| return container_of(sd, struct mxc_isi_crossbar, sd); |
| } |
| |
| static int mxc_isi_crossbar_gasket_enable(struct mxc_isi_crossbar *xbar, |
| struct v4l2_subdev_state *state, |
| struct v4l2_subdev *remote_sd, |
| u32 remote_pad, unsigned int port) |
| { |
| struct mxc_isi_dev *isi = xbar->isi; |
| const struct mxc_gasket_ops *gasket_ops = isi->pdata->gasket_ops; |
| const struct v4l2_mbus_framefmt *fmt; |
| struct v4l2_mbus_frame_desc fd; |
| int ret; |
| |
| if (!gasket_ops) |
| return 0; |
| |
| /* |
| * Configure and enable the gasket with the frame size and CSI-2 data |
| * type. For YUV422 8-bit, enable dual component mode unconditionally, |
| * to match the configuration of the CSIS. |
| */ |
| |
| ret = v4l2_subdev_call(remote_sd, pad, get_frame_desc, remote_pad, &fd); |
| if (ret) { |
| dev_err(isi->dev, |
| "failed to get frame descriptor from '%s':%u: %d\n", |
| remote_sd->name, remote_pad, ret); |
| return ret; |
| } |
| |
| if (fd.num_entries != 1) { |
| dev_err(isi->dev, "invalid frame descriptor for '%s':%u\n", |
| remote_sd->name, remote_pad); |
| return -EINVAL; |
| } |
| |
| fmt = v4l2_subdev_state_get_stream_format(state, port, 0); |
| if (!fmt) |
| return -EINVAL; |
| |
| gasket_ops->enable(isi, &fd, fmt, port); |
| return 0; |
| } |
| |
| static void mxc_isi_crossbar_gasket_disable(struct mxc_isi_crossbar *xbar, |
| unsigned int port) |
| { |
| struct mxc_isi_dev *isi = xbar->isi; |
| const struct mxc_gasket_ops *gasket_ops = isi->pdata->gasket_ops; |
| |
| if (!gasket_ops) |
| return; |
| |
| gasket_ops->disable(isi, port); |
| } |
| |
| /* ----------------------------------------------------------------------------- |
| * V4L2 subdev operations |
| */ |
| |
| static const struct v4l2_mbus_framefmt mxc_isi_crossbar_default_format = { |
| .code = MXC_ISI_DEF_MBUS_CODE_SINK, |
| .width = MXC_ISI_DEF_WIDTH, |
| .height = MXC_ISI_DEF_HEIGHT, |
| .field = V4L2_FIELD_NONE, |
| .colorspace = MXC_ISI_DEF_COLOR_SPACE, |
| .ycbcr_enc = MXC_ISI_DEF_YCBCR_ENC, |
| .quantization = MXC_ISI_DEF_QUANTIZATION, |
| .xfer_func = MXC_ISI_DEF_XFER_FUNC, |
| }; |
| |
| static int __mxc_isi_crossbar_set_routing(struct v4l2_subdev *sd, |
| struct v4l2_subdev_state *state, |
| struct v4l2_subdev_krouting *routing) |
| { |
| struct mxc_isi_crossbar *xbar = to_isi_crossbar(sd); |
| struct v4l2_subdev_route *route; |
| int ret; |
| |
| ret = v4l2_subdev_routing_validate(sd, routing, |
| V4L2_SUBDEV_ROUTING_NO_N_TO_1); |
| if (ret) |
| return ret; |
| |
| /* The memory input can be routed to the first pipeline only. */ |
| for_each_active_route(&state->routing, route) { |
| if (route->sink_pad == xbar->num_sinks - 1 && |
| route->source_pad != xbar->num_sinks) { |
| dev_dbg(xbar->isi->dev, |
| "invalid route from memory input (%u) to pipe %u\n", |
| route->sink_pad, |
| route->source_pad - xbar->num_sinks); |
| return -EINVAL; |
| } |
| } |
| |
| return v4l2_subdev_set_routing_with_fmt(sd, state, routing, |
| &mxc_isi_crossbar_default_format); |
| } |
| |
| static struct v4l2_subdev * |
| mxc_isi_crossbar_xlate_streams(struct mxc_isi_crossbar *xbar, |
| struct v4l2_subdev_state *state, |
| u32 source_pad, u64 source_streams, |
| u32 *__sink_pad, u64 *__sink_streams, |
| u32 *remote_pad) |
| { |
| struct v4l2_subdev_route *route; |
| struct v4l2_subdev *sd; |
| struct media_pad *pad; |
| u64 sink_streams = 0; |
| int sink_pad = -1; |
| |
| /* |
| * Translate the source pad and streams to the sink side. The routing |
| * validation forbids stream merging, so all matching entries in the |
| * routing table are guaranteed to have the same sink pad. |
| * |
| * TODO: This is likely worth a helper function, it could perhaps be |
| * supported by v4l2_subdev_state_xlate_streams() with pad1 set to -1. |
| */ |
| for_each_active_route(&state->routing, route) { |
| if (route->source_pad != source_pad || |
| !(source_streams & BIT(route->source_stream))) |
| continue; |
| |
| sink_streams |= BIT(route->sink_stream); |
| sink_pad = route->sink_pad; |
| } |
| |
| if (sink_pad < 0) { |
| dev_dbg(xbar->isi->dev, |
| "no stream connected to pipeline %u\n", |
| source_pad - xbar->num_sinks); |
| return ERR_PTR(-EPIPE); |
| } |
| |
| pad = media_pad_remote_pad_first(&xbar->pads[sink_pad]); |
| sd = media_entity_to_v4l2_subdev(pad->entity); |
| |
| if (!sd) { |
| dev_dbg(xbar->isi->dev, |
| "no entity connected to crossbar input %u\n", |
| sink_pad); |
| return ERR_PTR(-EPIPE); |
| } |
| |
| *__sink_pad = sink_pad; |
| *__sink_streams = sink_streams; |
| *remote_pad = pad->index; |
| |
| return sd; |
| } |
| |
| static int mxc_isi_crossbar_init_cfg(struct v4l2_subdev *sd, |
| struct v4l2_subdev_state *state) |
| { |
| struct mxc_isi_crossbar *xbar = to_isi_crossbar(sd); |
| struct v4l2_subdev_krouting routing = { }; |
| struct v4l2_subdev_route *routes; |
| unsigned int i; |
| int ret; |
| |
| /* |
| * Create a 1:1 mapping between pixel link inputs and outputs to |
| * pipelines by default. |
| */ |
| routes = kcalloc(xbar->num_sources, sizeof(*routes), GFP_KERNEL); |
| if (!routes) |
| return -ENOMEM; |
| |
| for (i = 0; i < xbar->num_sources; ++i) { |
| struct v4l2_subdev_route *route = &routes[i]; |
| |
| route->sink_pad = i; |
| route->source_pad = i + xbar->num_sinks; |
| route->flags = V4L2_SUBDEV_ROUTE_FL_ACTIVE; |
| } |
| |
| routing.num_routes = xbar->num_sources; |
| routing.routes = routes; |
| |
| ret = __mxc_isi_crossbar_set_routing(sd, state, &routing); |
| |
| kfree(routes); |
| |
| return ret; |
| } |
| |
| static int mxc_isi_crossbar_enum_mbus_code(struct v4l2_subdev *sd, |
| struct v4l2_subdev_state *state, |
| struct v4l2_subdev_mbus_code_enum *code) |
| { |
| struct mxc_isi_crossbar *xbar = to_isi_crossbar(sd); |
| const struct mxc_isi_bus_format_info *info; |
| |
| if (code->pad >= xbar->num_sinks) { |
| const struct v4l2_mbus_framefmt *format; |
| |
| /* |
| * The media bus code on source pads is identical to the |
| * connected sink pad. |
| */ |
| if (code->index > 0) |
| return -EINVAL; |
| |
| format = v4l2_subdev_state_get_opposite_stream_format(state, |
| code->pad, |
| code->stream); |
| if (!format) |
| return -EINVAL; |
| |
| code->code = format->code; |
| |
| return 0; |
| } |
| |
| info = mxc_isi_bus_format_by_index(code->index, MXC_ISI_PIPE_PAD_SINK); |
| if (!info) |
| return -EINVAL; |
| |
| code->code = info->mbus_code; |
| |
| return 0; |
| } |
| |
| static int mxc_isi_crossbar_set_fmt(struct v4l2_subdev *sd, |
| struct v4l2_subdev_state *state, |
| struct v4l2_subdev_format *fmt) |
| { |
| struct mxc_isi_crossbar *xbar = to_isi_crossbar(sd); |
| struct v4l2_mbus_framefmt *sink_fmt; |
| struct v4l2_subdev_route *route; |
| |
| if (fmt->which == V4L2_SUBDEV_FORMAT_ACTIVE && |
| media_pad_is_streaming(&xbar->pads[fmt->pad])) |
| return -EBUSY; |
| |
| /* |
| * The source pad format is always identical to the sink pad format and |
| * can't be modified. |
| */ |
| if (fmt->pad >= xbar->num_sinks) |
| return v4l2_subdev_get_fmt(sd, state, fmt); |
| |
| /* Validate the requested format. */ |
| if (!mxc_isi_bus_format_by_code(fmt->format.code, MXC_ISI_PIPE_PAD_SINK)) |
| fmt->format.code = MXC_ISI_DEF_MBUS_CODE_SINK; |
| |
| fmt->format.width = clamp_t(unsigned int, fmt->format.width, |
| MXC_ISI_MIN_WIDTH, MXC_ISI_MAX_WIDTH_CHAINED); |
| fmt->format.height = clamp_t(unsigned int, fmt->format.height, |
| MXC_ISI_MIN_HEIGHT, MXC_ISI_MAX_HEIGHT); |
| fmt->format.field = V4L2_FIELD_NONE; |
| |
| /* |
| * Set the format on the sink stream and propagate it to the source |
| * streams. |
| */ |
| sink_fmt = v4l2_subdev_state_get_stream_format(state, fmt->pad, |
| fmt->stream); |
| if (!sink_fmt) |
| return -EINVAL; |
| |
| *sink_fmt = fmt->format; |
| |
| /* TODO: A format propagation helper would be useful. */ |
| for_each_active_route(&state->routing, route) { |
| struct v4l2_mbus_framefmt *source_fmt; |
| |
| if (route->sink_pad != fmt->pad || |
| route->sink_stream != fmt->stream) |
| continue; |
| |
| source_fmt = v4l2_subdev_state_get_stream_format(state, route->source_pad, |
| route->source_stream); |
| if (!source_fmt) |
| return -EINVAL; |
| |
| *source_fmt = fmt->format; |
| } |
| |
| return 0; |
| } |
| |
| static int mxc_isi_crossbar_set_routing(struct v4l2_subdev *sd, |
| struct v4l2_subdev_state *state, |
| enum v4l2_subdev_format_whence which, |
| struct v4l2_subdev_krouting *routing) |
| { |
| if (which == V4L2_SUBDEV_FORMAT_ACTIVE && |
| media_entity_is_streaming(&sd->entity)) |
| return -EBUSY; |
| |
| return __mxc_isi_crossbar_set_routing(sd, state, routing); |
| } |
| |
| static int mxc_isi_crossbar_enable_streams(struct v4l2_subdev *sd, |
| struct v4l2_subdev_state *state, |
| u32 pad, u64 streams_mask) |
| { |
| struct mxc_isi_crossbar *xbar = to_isi_crossbar(sd); |
| struct v4l2_subdev *remote_sd; |
| struct mxc_isi_input *input; |
| u64 sink_streams; |
| u32 sink_pad; |
| u32 remote_pad; |
| int ret; |
| |
| remote_sd = mxc_isi_crossbar_xlate_streams(xbar, state, pad, streams_mask, |
| &sink_pad, &sink_streams, |
| &remote_pad); |
| if (IS_ERR(remote_sd)) |
| return PTR_ERR(remote_sd); |
| |
| input = &xbar->inputs[sink_pad]; |
| |
| /* |
| * TODO: Track per-stream enable counts to support multiplexed |
| * streams. |
| */ |
| if (!input->enable_count) { |
| ret = mxc_isi_crossbar_gasket_enable(xbar, state, remote_sd, |
| remote_pad, sink_pad); |
| if (ret) |
| return ret; |
| |
| ret = v4l2_subdev_enable_streams(remote_sd, remote_pad, |
| sink_streams); |
| if (ret) { |
| dev_err(xbar->isi->dev, |
| "failed to %s streams 0x%llx on '%s':%u: %d\n", |
| "enable", sink_streams, remote_sd->name, |
| remote_pad, ret); |
| mxc_isi_crossbar_gasket_disable(xbar, sink_pad); |
| return ret; |
| } |
| } |
| |
| input->enable_count++; |
| |
| return 0; |
| } |
| |
| static int mxc_isi_crossbar_disable_streams(struct v4l2_subdev *sd, |
| struct v4l2_subdev_state *state, |
| u32 pad, u64 streams_mask) |
| { |
| struct mxc_isi_crossbar *xbar = to_isi_crossbar(sd); |
| struct v4l2_subdev *remote_sd; |
| struct mxc_isi_input *input; |
| u64 sink_streams; |
| u32 sink_pad; |
| u32 remote_pad; |
| int ret = 0; |
| |
| remote_sd = mxc_isi_crossbar_xlate_streams(xbar, state, pad, streams_mask, |
| &sink_pad, &sink_streams, |
| &remote_pad); |
| if (IS_ERR(remote_sd)) |
| return PTR_ERR(remote_sd); |
| |
| input = &xbar->inputs[sink_pad]; |
| |
| input->enable_count--; |
| |
| if (!input->enable_count) { |
| ret = v4l2_subdev_disable_streams(remote_sd, remote_pad, |
| sink_streams); |
| if (ret) |
| dev_err(xbar->isi->dev, |
| "failed to %s streams 0x%llx on '%s':%u: %d\n", |
| "disable", sink_streams, remote_sd->name, |
| remote_pad, ret); |
| |
| mxc_isi_crossbar_gasket_disable(xbar, sink_pad); |
| } |
| |
| return ret; |
| } |
| |
| static const struct v4l2_subdev_pad_ops mxc_isi_crossbar_subdev_pad_ops = { |
| .init_cfg = mxc_isi_crossbar_init_cfg, |
| .enum_mbus_code = mxc_isi_crossbar_enum_mbus_code, |
| .get_fmt = v4l2_subdev_get_fmt, |
| .set_fmt = mxc_isi_crossbar_set_fmt, |
| .set_routing = mxc_isi_crossbar_set_routing, |
| .enable_streams = mxc_isi_crossbar_enable_streams, |
| .disable_streams = mxc_isi_crossbar_disable_streams, |
| }; |
| |
| static const struct v4l2_subdev_ops mxc_isi_crossbar_subdev_ops = { |
| .pad = &mxc_isi_crossbar_subdev_pad_ops, |
| }; |
| |
| static const struct media_entity_operations mxc_isi_cross_entity_ops = { |
| .get_fwnode_pad = v4l2_subdev_get_fwnode_pad_1_to_1, |
| .link_validate = v4l2_subdev_link_validate, |
| .has_pad_interdep = v4l2_subdev_has_pad_interdep, |
| }; |
| |
| /* ----------------------------------------------------------------------------- |
| * Init & cleanup |
| */ |
| |
| int mxc_isi_crossbar_init(struct mxc_isi_dev *isi) |
| { |
| struct mxc_isi_crossbar *xbar = &isi->crossbar; |
| struct v4l2_subdev *sd = &xbar->sd; |
| unsigned int num_pads; |
| unsigned int i; |
| int ret; |
| |
| xbar->isi = isi; |
| |
| v4l2_subdev_init(sd, &mxc_isi_crossbar_subdev_ops); |
| sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE | V4L2_SUBDEV_FL_STREAMS; |
| strscpy(sd->name, "crossbar", sizeof(sd->name)); |
| sd->dev = isi->dev; |
| |
| sd->entity.function = MEDIA_ENT_F_VID_MUX; |
| sd->entity.ops = &mxc_isi_cross_entity_ops; |
| |
| /* |
| * The subdev has one sink and one source per port, plus one sink for |
| * the memory input. |
| */ |
| xbar->num_sinks = isi->pdata->num_ports + 1; |
| xbar->num_sources = isi->pdata->num_ports; |
| num_pads = xbar->num_sinks + xbar->num_sources; |
| |
| xbar->pads = kcalloc(num_pads, sizeof(*xbar->pads), GFP_KERNEL); |
| if (!xbar->pads) |
| return -ENOMEM; |
| |
| xbar->inputs = kcalloc(xbar->num_sinks, sizeof(*xbar->inputs), |
| GFP_KERNEL); |
| if (!xbar->inputs) { |
| ret = -ENOMEM; |
| goto err_free; |
| } |
| |
| for (i = 0; i < xbar->num_sinks; ++i) |
| xbar->pads[i].flags = MEDIA_PAD_FL_SINK; |
| for (i = 0; i < xbar->num_sources; ++i) |
| xbar->pads[i + xbar->num_sinks].flags = MEDIA_PAD_FL_SOURCE; |
| |
| ret = media_entity_pads_init(&sd->entity, num_pads, xbar->pads); |
| if (ret) |
| goto err_free; |
| |
| ret = v4l2_subdev_init_finalize(sd); |
| if (ret < 0) |
| goto err_entity; |
| |
| return 0; |
| |
| err_entity: |
| media_entity_cleanup(&sd->entity); |
| err_free: |
| kfree(xbar->pads); |
| kfree(xbar->inputs); |
| |
| return ret; |
| } |
| |
| void mxc_isi_crossbar_cleanup(struct mxc_isi_crossbar *xbar) |
| { |
| media_entity_cleanup(&xbar->sd.entity); |
| kfree(xbar->pads); |
| kfree(xbar->inputs); |
| } |
| |
| int mxc_isi_crossbar_register(struct mxc_isi_crossbar *xbar) |
| { |
| return v4l2_device_register_subdev(&xbar->isi->v4l2_dev, &xbar->sd); |
| } |
| |
| void mxc_isi_crossbar_unregister(struct mxc_isi_crossbar *xbar) |
| { |
| } |