Commit | Line | Data |
---|---|---|
68803ad4 PZ |
1 | /* |
2 | * video stream multiplexer controlled via mux control | |
3 | * | |
4 | * Copyright (C) 2013 Pengutronix, Sascha Hauer <kernel@pengutronix.de> | |
5 | * Copyright (C) 2016-2017 Pengutronix, Philipp Zabel <kernel@pengutronix.de> | |
6 | * | |
7 | * This program is free software; you can redistribute it and/or | |
8 | * modify it under the terms of the GNU General Public License | |
9 | * as published by the Free Software Foundation; either version 2 | |
10 | * of the License, or (at your option) any later version. | |
11 | * This program is distributed in the hope that it will be useful, | |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 | * GNU General Public License for more details. | |
15 | */ | |
16 | ||
17 | #include <linux/err.h> | |
18 | #include <linux/module.h> | |
19 | #include <linux/mutex.h> | |
435945e0 | 20 | #include <linux/mux/consumer.h> |
68803ad4 PZ |
21 | #include <linux/of.h> |
22 | #include <linux/of_graph.h> | |
23 | #include <linux/platform_device.h> | |
c5afc789 | 24 | #include <linux/slab.h> |
68803ad4 PZ |
25 | #include <media/v4l2-async.h> |
26 | #include <media/v4l2-device.h> | |
c5afc789 | 27 | #include <media/v4l2-fwnode.h> |
68803ad4 PZ |
28 | #include <media/v4l2-subdev.h> |
29 | ||
30 | struct video_mux { | |
31 | struct v4l2_subdev subdev; | |
32 | struct media_pad *pads; | |
33 | struct v4l2_mbus_framefmt *format_mbus; | |
435945e0 | 34 | struct mux_control *mux; |
68803ad4 PZ |
35 | struct mutex lock; |
36 | int active; | |
37 | }; | |
38 | ||
efe1958e PZ |
39 | static const struct v4l2_mbus_framefmt video_mux_format_mbus_default = { |
40 | .width = 1, | |
41 | .height = 1, | |
42 | .code = MEDIA_BUS_FMT_Y8_1X8, | |
43 | .field = V4L2_FIELD_NONE, | |
44 | }; | |
45 | ||
68803ad4 PZ |
46 | static inline struct video_mux *v4l2_subdev_to_video_mux(struct v4l2_subdev *sd) |
47 | { | |
48 | return container_of(sd, struct video_mux, subdev); | |
49 | } | |
50 | ||
51 | static int video_mux_link_setup(struct media_entity *entity, | |
52 | const struct media_pad *local, | |
53 | const struct media_pad *remote, u32 flags) | |
54 | { | |
55 | struct v4l2_subdev *sd = media_entity_to_v4l2_subdev(entity); | |
56 | struct video_mux *vmux = v4l2_subdev_to_video_mux(sd); | |
fd32d534 | 57 | u16 source_pad = entity->num_pads - 1; |
68803ad4 PZ |
58 | int ret = 0; |
59 | ||
60 | /* | |
61 | * The mux state is determined by the enabled sink pad link. | |
62 | * Enabling or disabling the source pad link has no effect. | |
63 | */ | |
64 | if (local->flags & MEDIA_PAD_FL_SOURCE) | |
65 | return 0; | |
66 | ||
67 | dev_dbg(sd->dev, "link setup '%s':%d->'%s':%d[%d]", | |
68 | remote->entity->name, remote->index, local->entity->name, | |
69 | local->index, flags & MEDIA_LNK_FL_ENABLED); | |
70 | ||
71 | mutex_lock(&vmux->lock); | |
72 | ||
73 | if (flags & MEDIA_LNK_FL_ENABLED) { | |
74 | if (vmux->active == local->index) | |
75 | goto out; | |
76 | ||
77 | if (vmux->active >= 0) { | |
78 | ret = -EBUSY; | |
79 | goto out; | |
80 | } | |
81 | ||
82 | dev_dbg(sd->dev, "setting %d active\n", local->index); | |
435945e0 | 83 | ret = mux_control_try_select(vmux->mux, local->index); |
68803ad4 PZ |
84 | if (ret < 0) |
85 | goto out; | |
86 | vmux->active = local->index; | |
fd32d534 CL |
87 | |
88 | /* Propagate the active format to the source */ | |
89 | vmux->format_mbus[source_pad] = vmux->format_mbus[vmux->active]; | |
68803ad4 PZ |
90 | } else { |
91 | if (vmux->active != local->index) | |
92 | goto out; | |
93 | ||
94 | dev_dbg(sd->dev, "going inactive\n"); | |
435945e0 | 95 | mux_control_deselect(vmux->mux); |
68803ad4 PZ |
96 | vmux->active = -1; |
97 | } | |
98 | ||
99 | out: | |
100 | mutex_unlock(&vmux->lock); | |
101 | return ret; | |
102 | } | |
103 | ||
104 | static const struct media_entity_operations video_mux_ops = { | |
105 | .link_setup = video_mux_link_setup, | |
106 | .link_validate = v4l2_subdev_link_validate, | |
107 | }; | |
108 | ||
109 | static int video_mux_s_stream(struct v4l2_subdev *sd, int enable) | |
110 | { | |
111 | struct video_mux *vmux = v4l2_subdev_to_video_mux(sd); | |
112 | struct v4l2_subdev *upstream_sd; | |
113 | struct media_pad *pad; | |
114 | ||
115 | if (vmux->active == -1) { | |
116 | dev_err(sd->dev, "Can not start streaming on inactive mux\n"); | |
117 | return -EINVAL; | |
118 | } | |
119 | ||
120 | pad = media_entity_remote_pad(&sd->entity.pads[vmux->active]); | |
121 | if (!pad) { | |
122 | dev_err(sd->dev, "Failed to find remote source pad\n"); | |
123 | return -ENOLINK; | |
124 | } | |
125 | ||
126 | if (!is_media_entity_v4l2_subdev(pad->entity)) { | |
127 | dev_err(sd->dev, "Upstream entity is not a v4l2 subdev\n"); | |
128 | return -ENODEV; | |
129 | } | |
130 | ||
131 | upstream_sd = media_entity_to_v4l2_subdev(pad->entity); | |
132 | ||
133 | return v4l2_subdev_call(upstream_sd, video, s_stream, enable); | |
134 | } | |
135 | ||
136 | static const struct v4l2_subdev_video_ops video_mux_subdev_video_ops = { | |
137 | .s_stream = video_mux_s_stream, | |
138 | }; | |
139 | ||
140 | static struct v4l2_mbus_framefmt * | |
141 | __video_mux_get_pad_format(struct v4l2_subdev *sd, | |
142 | struct v4l2_subdev_pad_config *cfg, | |
143 | unsigned int pad, u32 which) | |
144 | { | |
145 | struct video_mux *vmux = v4l2_subdev_to_video_mux(sd); | |
146 | ||
147 | switch (which) { | |
148 | case V4L2_SUBDEV_FORMAT_TRY: | |
149 | return v4l2_subdev_get_try_format(sd, cfg, pad); | |
150 | case V4L2_SUBDEV_FORMAT_ACTIVE: | |
151 | return &vmux->format_mbus[pad]; | |
152 | default: | |
153 | return NULL; | |
154 | } | |
155 | } | |
156 | ||
157 | static int video_mux_get_format(struct v4l2_subdev *sd, | |
158 | struct v4l2_subdev_pad_config *cfg, | |
159 | struct v4l2_subdev_format *sdformat) | |
160 | { | |
161 | struct video_mux *vmux = v4l2_subdev_to_video_mux(sd); | |
162 | ||
163 | mutex_lock(&vmux->lock); | |
164 | ||
165 | sdformat->format = *__video_mux_get_pad_format(sd, cfg, sdformat->pad, | |
166 | sdformat->which); | |
167 | ||
168 | mutex_unlock(&vmux->lock); | |
169 | ||
170 | return 0; | |
171 | } | |
172 | ||
173 | static int video_mux_set_format(struct v4l2_subdev *sd, | |
174 | struct v4l2_subdev_pad_config *cfg, | |
175 | struct v4l2_subdev_format *sdformat) | |
176 | { | |
177 | struct video_mux *vmux = v4l2_subdev_to_video_mux(sd); | |
fd32d534 | 178 | struct v4l2_mbus_framefmt *mbusformat, *source_mbusformat; |
68803ad4 | 179 | struct media_pad *pad = &vmux->pads[sdformat->pad]; |
fd32d534 | 180 | u16 source_pad = sd->entity.num_pads - 1; |
68803ad4 PZ |
181 | |
182 | mbusformat = __video_mux_get_pad_format(sd, cfg, sdformat->pad, | |
183 | sdformat->which); | |
184 | if (!mbusformat) | |
185 | return -EINVAL; | |
186 | ||
fd32d534 CL |
187 | source_mbusformat = __video_mux_get_pad_format(sd, cfg, source_pad, |
188 | sdformat->which); | |
189 | if (!source_mbusformat) | |
190 | return -EINVAL; | |
191 | ||
efe1958e PZ |
192 | /* No size limitations except V4L2 compliance requirements */ |
193 | v4l_bound_align_image(&sdformat->format.width, 1, 65536, 0, | |
194 | &sdformat->format.height, 1, 65536, 0, 0); | |
195 | ||
196 | /* All formats except LVDS and vendor specific formats are acceptable */ | |
197 | switch (sdformat->format.code) { | |
198 | case MEDIA_BUS_FMT_RGB444_1X12: | |
199 | case MEDIA_BUS_FMT_RGB444_2X8_PADHI_BE: | |
200 | case MEDIA_BUS_FMT_RGB444_2X8_PADHI_LE: | |
201 | case MEDIA_BUS_FMT_RGB555_2X8_PADHI_BE: | |
202 | case MEDIA_BUS_FMT_RGB555_2X8_PADHI_LE: | |
203 | case MEDIA_BUS_FMT_RGB565_1X16: | |
204 | case MEDIA_BUS_FMT_BGR565_2X8_BE: | |
205 | case MEDIA_BUS_FMT_BGR565_2X8_LE: | |
206 | case MEDIA_BUS_FMT_RGB565_2X8_BE: | |
207 | case MEDIA_BUS_FMT_RGB565_2X8_LE: | |
208 | case MEDIA_BUS_FMT_RGB666_1X18: | |
209 | case MEDIA_BUS_FMT_RBG888_1X24: | |
210 | case MEDIA_BUS_FMT_RGB666_1X24_CPADHI: | |
211 | case MEDIA_BUS_FMT_BGR888_1X24: | |
212 | case MEDIA_BUS_FMT_GBR888_1X24: | |
213 | case MEDIA_BUS_FMT_RGB888_1X24: | |
214 | case MEDIA_BUS_FMT_RGB888_2X12_BE: | |
215 | case MEDIA_BUS_FMT_RGB888_2X12_LE: | |
216 | case MEDIA_BUS_FMT_ARGB8888_1X32: | |
217 | case MEDIA_BUS_FMT_RGB888_1X32_PADHI: | |
218 | case MEDIA_BUS_FMT_RGB101010_1X30: | |
219 | case MEDIA_BUS_FMT_RGB121212_1X36: | |
220 | case MEDIA_BUS_FMT_RGB161616_1X48: | |
221 | case MEDIA_BUS_FMT_Y8_1X8: | |
222 | case MEDIA_BUS_FMT_UV8_1X8: | |
223 | case MEDIA_BUS_FMT_UYVY8_1_5X8: | |
224 | case MEDIA_BUS_FMT_VYUY8_1_5X8: | |
225 | case MEDIA_BUS_FMT_YUYV8_1_5X8: | |
226 | case MEDIA_BUS_FMT_YVYU8_1_5X8: | |
227 | case MEDIA_BUS_FMT_UYVY8_2X8: | |
228 | case MEDIA_BUS_FMT_VYUY8_2X8: | |
229 | case MEDIA_BUS_FMT_YUYV8_2X8: | |
230 | case MEDIA_BUS_FMT_YVYU8_2X8: | |
231 | case MEDIA_BUS_FMT_Y10_1X10: | |
232 | case MEDIA_BUS_FMT_UYVY10_2X10: | |
233 | case MEDIA_BUS_FMT_VYUY10_2X10: | |
234 | case MEDIA_BUS_FMT_YUYV10_2X10: | |
235 | case MEDIA_BUS_FMT_YVYU10_2X10: | |
236 | case MEDIA_BUS_FMT_Y12_1X12: | |
237 | case MEDIA_BUS_FMT_UYVY12_2X12: | |
238 | case MEDIA_BUS_FMT_VYUY12_2X12: | |
239 | case MEDIA_BUS_FMT_YUYV12_2X12: | |
240 | case MEDIA_BUS_FMT_YVYU12_2X12: | |
241 | case MEDIA_BUS_FMT_UYVY8_1X16: | |
242 | case MEDIA_BUS_FMT_VYUY8_1X16: | |
243 | case MEDIA_BUS_FMT_YUYV8_1X16: | |
244 | case MEDIA_BUS_FMT_YVYU8_1X16: | |
245 | case MEDIA_BUS_FMT_YDYUYDYV8_1X16: | |
246 | case MEDIA_BUS_FMT_UYVY10_1X20: | |
247 | case MEDIA_BUS_FMT_VYUY10_1X20: | |
248 | case MEDIA_BUS_FMT_YUYV10_1X20: | |
249 | case MEDIA_BUS_FMT_YVYU10_1X20: | |
250 | case MEDIA_BUS_FMT_VUY8_1X24: | |
251 | case MEDIA_BUS_FMT_YUV8_1X24: | |
252 | case MEDIA_BUS_FMT_UYYVYY8_0_5X24: | |
253 | case MEDIA_BUS_FMT_UYVY12_1X24: | |
254 | case MEDIA_BUS_FMT_VYUY12_1X24: | |
255 | case MEDIA_BUS_FMT_YUYV12_1X24: | |
256 | case MEDIA_BUS_FMT_YVYU12_1X24: | |
257 | case MEDIA_BUS_FMT_YUV10_1X30: | |
258 | case MEDIA_BUS_FMT_UYYVYY10_0_5X30: | |
259 | case MEDIA_BUS_FMT_AYUV8_1X32: | |
260 | case MEDIA_BUS_FMT_UYYVYY12_0_5X36: | |
261 | case MEDIA_BUS_FMT_YUV12_1X36: | |
262 | case MEDIA_BUS_FMT_YUV16_1X48: | |
263 | case MEDIA_BUS_FMT_UYYVYY16_0_5X48: | |
264 | case MEDIA_BUS_FMT_JPEG_1X8: | |
265 | case MEDIA_BUS_FMT_AHSV8888_1X32: | |
7637c261 RMS |
266 | case MEDIA_BUS_FMT_SBGGR8_1X8: |
267 | case MEDIA_BUS_FMT_SGBRG8_1X8: | |
268 | case MEDIA_BUS_FMT_SGRBG8_1X8: | |
269 | case MEDIA_BUS_FMT_SRGGB8_1X8: | |
270 | case MEDIA_BUS_FMT_SBGGR10_1X10: | |
271 | case MEDIA_BUS_FMT_SGBRG10_1X10: | |
272 | case MEDIA_BUS_FMT_SGRBG10_1X10: | |
273 | case MEDIA_BUS_FMT_SRGGB10_1X10: | |
274 | case MEDIA_BUS_FMT_SBGGR12_1X12: | |
275 | case MEDIA_BUS_FMT_SGBRG12_1X12: | |
276 | case MEDIA_BUS_FMT_SGRBG12_1X12: | |
277 | case MEDIA_BUS_FMT_SRGGB12_1X12: | |
278 | case MEDIA_BUS_FMT_SBGGR14_1X14: | |
279 | case MEDIA_BUS_FMT_SGBRG14_1X14: | |
280 | case MEDIA_BUS_FMT_SGRBG14_1X14: | |
281 | case MEDIA_BUS_FMT_SRGGB14_1X14: | |
282 | case MEDIA_BUS_FMT_SBGGR16_1X16: | |
283 | case MEDIA_BUS_FMT_SGBRG16_1X16: | |
284 | case MEDIA_BUS_FMT_SGRBG16_1X16: | |
285 | case MEDIA_BUS_FMT_SRGGB16_1X16: | |
efe1958e PZ |
286 | break; |
287 | default: | |
288 | sdformat->format.code = MEDIA_BUS_FMT_Y8_1X8; | |
289 | break; | |
290 | } | |
291 | if (sdformat->format.field == V4L2_FIELD_ANY) | |
292 | sdformat->format.field = V4L2_FIELD_NONE; | |
293 | ||
68803ad4 PZ |
294 | mutex_lock(&vmux->lock); |
295 | ||
296 | /* Source pad mirrors active sink pad, no limitations on sink pads */ | |
297 | if ((pad->flags & MEDIA_PAD_FL_SOURCE) && vmux->active >= 0) | |
298 | sdformat->format = vmux->format_mbus[vmux->active]; | |
299 | ||
300 | *mbusformat = sdformat->format; | |
301 | ||
fd32d534 CL |
302 | /* Propagate the format from an active sink to source */ |
303 | if ((pad->flags & MEDIA_PAD_FL_SINK) && (pad->index == vmux->active)) | |
304 | *source_mbusformat = sdformat->format; | |
305 | ||
68803ad4 PZ |
306 | mutex_unlock(&vmux->lock); |
307 | ||
308 | return 0; | |
309 | } | |
310 | ||
efe1958e PZ |
311 | static int video_mux_init_cfg(struct v4l2_subdev *sd, |
312 | struct v4l2_subdev_pad_config *cfg) | |
313 | { | |
314 | struct video_mux *vmux = v4l2_subdev_to_video_mux(sd); | |
315 | struct v4l2_mbus_framefmt *mbusformat; | |
316 | unsigned int i; | |
317 | ||
318 | mutex_lock(&vmux->lock); | |
319 | ||
320 | for (i = 0; i < sd->entity.num_pads; i++) { | |
321 | mbusformat = v4l2_subdev_get_try_format(sd, cfg, i); | |
322 | *mbusformat = video_mux_format_mbus_default; | |
323 | } | |
324 | ||
325 | mutex_unlock(&vmux->lock); | |
326 | ||
327 | return 0; | |
328 | } | |
329 | ||
68803ad4 | 330 | static const struct v4l2_subdev_pad_ops video_mux_pad_ops = { |
efe1958e | 331 | .init_cfg = video_mux_init_cfg, |
68803ad4 PZ |
332 | .get_fmt = video_mux_get_format, |
333 | .set_fmt = video_mux_set_format, | |
334 | }; | |
335 | ||
336 | static const struct v4l2_subdev_ops video_mux_subdev_ops = { | |
337 | .pad = &video_mux_pad_ops, | |
338 | .video = &video_mux_subdev_video_ops, | |
339 | }; | |
340 | ||
c5afc789 SL |
341 | static int video_mux_parse_endpoint(struct device *dev, |
342 | struct v4l2_fwnode_endpoint *vep, | |
343 | struct v4l2_async_subdev *asd) | |
344 | { | |
345 | /* | |
346 | * it's not an error if remote is missing on a video-mux | |
347 | * input port, return -ENOTCONN to skip this endpoint with | |
348 | * no error. | |
349 | */ | |
350 | return fwnode_device_is_available(asd->match.fwnode) ? 0 : -ENOTCONN; | |
351 | } | |
352 | ||
353 | static int video_mux_async_register(struct video_mux *vmux, | |
354 | unsigned int num_input_pads) | |
355 | { | |
356 | unsigned int i, *ports; | |
357 | int ret; | |
358 | ||
359 | ports = kcalloc(num_input_pads, sizeof(*ports), GFP_KERNEL); | |
360 | if (!ports) | |
361 | return -ENOMEM; | |
362 | for (i = 0; i < num_input_pads; i++) | |
363 | ports[i] = i; | |
364 | ||
365 | ret = v4l2_async_register_fwnode_subdev( | |
366 | &vmux->subdev, sizeof(struct v4l2_async_subdev), | |
367 | ports, num_input_pads, video_mux_parse_endpoint); | |
368 | ||
369 | kfree(ports); | |
370 | return ret; | |
371 | } | |
372 | ||
68803ad4 PZ |
373 | static int video_mux_probe(struct platform_device *pdev) |
374 | { | |
375 | struct device_node *np = pdev->dev.of_node; | |
376 | struct device *dev = &pdev->dev; | |
377 | struct device_node *ep; | |
378 | struct video_mux *vmux; | |
379 | unsigned int num_pads = 0; | |
efe1958e | 380 | unsigned int i; |
68803ad4 | 381 | int ret; |
68803ad4 PZ |
382 | |
383 | vmux = devm_kzalloc(dev, sizeof(*vmux), GFP_KERNEL); | |
384 | if (!vmux) | |
385 | return -ENOMEM; | |
386 | ||
387 | platform_set_drvdata(pdev, vmux); | |
388 | ||
389 | v4l2_subdev_init(&vmux->subdev, &video_mux_subdev_ops); | |
f764e6d6 | 390 | snprintf(vmux->subdev.name, sizeof(vmux->subdev.name), "%pOFn", np); |
68803ad4 PZ |
391 | vmux->subdev.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE; |
392 | vmux->subdev.dev = dev; | |
393 | ||
394 | /* | |
395 | * The largest numbered port is the output port. It determines | |
396 | * total number of pads. | |
397 | */ | |
398 | for_each_endpoint_of_node(np, ep) { | |
399 | struct of_endpoint endpoint; | |
400 | ||
401 | of_graph_parse_endpoint(ep, &endpoint); | |
402 | num_pads = max(num_pads, endpoint.port + 1); | |
403 | } | |
404 | ||
405 | if (num_pads < 2) { | |
406 | dev_err(dev, "Not enough ports %d\n", num_pads); | |
407 | return -EINVAL; | |
408 | } | |
409 | ||
435945e0 PZ |
410 | vmux->mux = devm_mux_control_get(dev, NULL); |
411 | if (IS_ERR(vmux->mux)) { | |
412 | ret = PTR_ERR(vmux->mux); | |
68803ad4 PZ |
413 | if (ret != -EPROBE_DEFER) |
414 | dev_err(dev, "Failed to get mux: %d\n", ret); | |
415 | return ret; | |
416 | } | |
417 | ||
418 | mutex_init(&vmux->lock); | |
419 | vmux->active = -1; | |
420 | vmux->pads = devm_kcalloc(dev, num_pads, sizeof(*vmux->pads), | |
421 | GFP_KERNEL); | |
aeb0d0f5 KL |
422 | if (!vmux->pads) |
423 | return -ENOMEM; | |
424 | ||
68803ad4 PZ |
425 | vmux->format_mbus = devm_kcalloc(dev, num_pads, |
426 | sizeof(*vmux->format_mbus), | |
427 | GFP_KERNEL); | |
aeb0d0f5 KL |
428 | if (!vmux->format_mbus) |
429 | return -ENOMEM; | |
68803ad4 | 430 | |
efe1958e PZ |
431 | for (i = 0; i < num_pads; i++) { |
432 | vmux->pads[i].flags = (i < num_pads - 1) ? MEDIA_PAD_FL_SINK | |
433 | : MEDIA_PAD_FL_SOURCE; | |
434 | vmux->format_mbus[i] = video_mux_format_mbus_default; | |
435 | } | |
68803ad4 PZ |
436 | |
437 | vmux->subdev.entity.function = MEDIA_ENT_F_VID_MUX; | |
438 | ret = media_entity_pads_init(&vmux->subdev.entity, num_pads, | |
439 | vmux->pads); | |
440 | if (ret < 0) | |
441 | return ret; | |
442 | ||
443 | vmux->subdev.entity.ops = &video_mux_ops; | |
444 | ||
c5afc789 | 445 | return video_mux_async_register(vmux, num_pads - 1); |
68803ad4 PZ |
446 | } |
447 | ||
448 | static int video_mux_remove(struct platform_device *pdev) | |
449 | { | |
450 | struct video_mux *vmux = platform_get_drvdata(pdev); | |
451 | struct v4l2_subdev *sd = &vmux->subdev; | |
452 | ||
453 | v4l2_async_unregister_subdev(sd); | |
454 | media_entity_cleanup(&sd->entity); | |
455 | ||
456 | return 0; | |
457 | } | |
458 | ||
459 | static const struct of_device_id video_mux_dt_ids[] = { | |
460 | { .compatible = "video-mux", }, | |
461 | { /* sentinel */ } | |
462 | }; | |
463 | MODULE_DEVICE_TABLE(of, video_mux_dt_ids); | |
464 | ||
465 | static struct platform_driver video_mux_driver = { | |
466 | .probe = video_mux_probe, | |
467 | .remove = video_mux_remove, | |
468 | .driver = { | |
469 | .of_match_table = video_mux_dt_ids, | |
470 | .name = "video-mux", | |
471 | }, | |
472 | }; | |
473 | ||
474 | module_platform_driver(video_mux_driver); | |
475 | ||
476 | MODULE_DESCRIPTION("video stream multiplexer"); | |
477 | MODULE_AUTHOR("Sascha Hauer, Pengutronix"); | |
478 | MODULE_AUTHOR("Philipp Zabel, Pengutronix"); | |
479 | MODULE_LICENSE("GPL"); |