diff options
author | Maxime Ripard <maxime.ripard@bootlin.com> | 2018-05-04 10:08:08 -0400 |
---|---|---|
committer | Mauro Carvalho Chehab <mchehab+samsung@kernel.org> | 2018-05-16 11:12:21 -0400 |
commit | 1fc3b37f34f69ee3fd61ca624fc005fb0bfe3984 (patch) | |
tree | 914f28f12cb832bc193b473ff09ac8c7460c2b6a /drivers/media/platform/cadence/cdns-csi2rx.c | |
parent | 3290aa63ee8088b74a1e4d6ec639272a47c8e9d7 (diff) |
media: v4l: cadence: Add Cadence MIPI-CSI2 RX driver
The Cadence CSI-2 RX Controller is an hardware block meant to be used as a
bridge between a CSI-2 bus and pixel grabbers.
It supports operating with internal or external D-PHY, with up to 4 lanes,
or without any D-PHY. The current code only supports the latter case.
It also support dynamic mapping of the CSI-2 virtual channels to the
associated pixel grabbers, but that isn't allowed at the moment either.
Acked-by: Benoit Parrot <bparrot@ti.com>
Reviewed-by: Niklas Söderlund <niklas.soderlund+renesas@ragnatech.se>
Signed-off-by: Maxime Ripard <maxime.ripard@bootlin.com>
Signed-off-by: Sakari Ailus <sakari.ailus@linux.intel.com>
Signed-off-by: Mauro Carvalho Chehab <mchehab+samsung@kernel.org>
Diffstat (limited to 'drivers/media/platform/cadence/cdns-csi2rx.c')
-rw-r--r-- | drivers/media/platform/cadence/cdns-csi2rx.c | 498 |
1 files changed, 498 insertions, 0 deletions
diff --git a/drivers/media/platform/cadence/cdns-csi2rx.c b/drivers/media/platform/cadence/cdns-csi2rx.c new file mode 100644 index 000000000000..fe612ec1f99f --- /dev/null +++ b/drivers/media/platform/cadence/cdns-csi2rx.c | |||
@@ -0,0 +1,498 @@ | |||
1 | // SPDX-License-Identifier: GPL-2.0+ | ||
2 | /* | ||
3 | * Driver for Cadence MIPI-CSI2 RX Controller v1.3 | ||
4 | * | ||
5 | * Copyright (C) 2017 Cadence Design Systems Inc. | ||
6 | */ | ||
7 | |||
8 | #include <linux/clk.h> | ||
9 | #include <linux/delay.h> | ||
10 | #include <linux/io.h> | ||
11 | #include <linux/module.h> | ||
12 | #include <linux/of.h> | ||
13 | #include <linux/of_graph.h> | ||
14 | #include <linux/phy/phy.h> | ||
15 | #include <linux/platform_device.h> | ||
16 | |||
17 | #include <media/v4l2-ctrls.h> | ||
18 | #include <media/v4l2-device.h> | ||
19 | #include <media/v4l2-fwnode.h> | ||
20 | #include <media/v4l2-subdev.h> | ||
21 | |||
22 | #define CSI2RX_DEVICE_CFG_REG 0x000 | ||
23 | |||
24 | #define CSI2RX_SOFT_RESET_REG 0x004 | ||
25 | #define CSI2RX_SOFT_RESET_PROTOCOL BIT(1) | ||
26 | #define CSI2RX_SOFT_RESET_FRONT BIT(0) | ||
27 | |||
28 | #define CSI2RX_STATIC_CFG_REG 0x008 | ||
29 | #define CSI2RX_STATIC_CFG_DLANE_MAP(llane, plane) ((plane) << (16 + (llane) * 4)) | ||
30 | #define CSI2RX_STATIC_CFG_LANES_MASK GENMASK(11, 8) | ||
31 | |||
32 | #define CSI2RX_STREAM_BASE(n) (((n) + 1) * 0x100) | ||
33 | |||
34 | #define CSI2RX_STREAM_CTRL_REG(n) (CSI2RX_STREAM_BASE(n) + 0x000) | ||
35 | #define CSI2RX_STREAM_CTRL_START BIT(0) | ||
36 | |||
37 | #define CSI2RX_STREAM_DATA_CFG_REG(n) (CSI2RX_STREAM_BASE(n) + 0x008) | ||
38 | #define CSI2RX_STREAM_DATA_CFG_EN_VC_SELECT BIT(31) | ||
39 | #define CSI2RX_STREAM_DATA_CFG_VC_SELECT(n) BIT((n) + 16) | ||
40 | |||
41 | #define CSI2RX_STREAM_CFG_REG(n) (CSI2RX_STREAM_BASE(n) + 0x00c) | ||
42 | #define CSI2RX_STREAM_CFG_FIFO_MODE_LARGE_BUF (1 << 8) | ||
43 | |||
44 | #define CSI2RX_LANES_MAX 4 | ||
45 | #define CSI2RX_STREAMS_MAX 4 | ||
46 | |||
47 | enum csi2rx_pads { | ||
48 | CSI2RX_PAD_SINK, | ||
49 | CSI2RX_PAD_SOURCE_STREAM0, | ||
50 | CSI2RX_PAD_SOURCE_STREAM1, | ||
51 | CSI2RX_PAD_SOURCE_STREAM2, | ||
52 | CSI2RX_PAD_SOURCE_STREAM3, | ||
53 | CSI2RX_PAD_MAX, | ||
54 | }; | ||
55 | |||
56 | struct csi2rx_priv { | ||
57 | struct device *dev; | ||
58 | unsigned int count; | ||
59 | |||
60 | /* | ||
61 | * Used to prevent race conditions between multiple, | ||
62 | * concurrent calls to start and stop. | ||
63 | */ | ||
64 | struct mutex lock; | ||
65 | |||
66 | void __iomem *base; | ||
67 | struct clk *sys_clk; | ||
68 | struct clk *p_clk; | ||
69 | struct clk *pixel_clk[CSI2RX_STREAMS_MAX]; | ||
70 | struct phy *dphy; | ||
71 | |||
72 | u8 lanes[CSI2RX_LANES_MAX]; | ||
73 | u8 num_lanes; | ||
74 | u8 max_lanes; | ||
75 | u8 max_streams; | ||
76 | bool has_internal_dphy; | ||
77 | |||
78 | struct v4l2_subdev subdev; | ||
79 | struct v4l2_async_notifier notifier; | ||
80 | struct media_pad pads[CSI2RX_PAD_MAX]; | ||
81 | |||
82 | /* Remote source */ | ||
83 | struct v4l2_async_subdev asd; | ||
84 | struct v4l2_subdev *source_subdev; | ||
85 | int source_pad; | ||
86 | }; | ||
87 | |||
88 | static inline | ||
89 | struct csi2rx_priv *v4l2_subdev_to_csi2rx(struct v4l2_subdev *subdev) | ||
90 | { | ||
91 | return container_of(subdev, struct csi2rx_priv, subdev); | ||
92 | } | ||
93 | |||
94 | static void csi2rx_reset(struct csi2rx_priv *csi2rx) | ||
95 | { | ||
96 | writel(CSI2RX_SOFT_RESET_PROTOCOL | CSI2RX_SOFT_RESET_FRONT, | ||
97 | csi2rx->base + CSI2RX_SOFT_RESET_REG); | ||
98 | |||
99 | udelay(10); | ||
100 | |||
101 | writel(0, csi2rx->base + CSI2RX_SOFT_RESET_REG); | ||
102 | } | ||
103 | |||
104 | static int csi2rx_start(struct csi2rx_priv *csi2rx) | ||
105 | { | ||
106 | unsigned int i; | ||
107 | unsigned long lanes_used = 0; | ||
108 | u32 reg; | ||
109 | int ret; | ||
110 | |||
111 | ret = clk_prepare_enable(csi2rx->p_clk); | ||
112 | if (ret) | ||
113 | return ret; | ||
114 | |||
115 | csi2rx_reset(csi2rx); | ||
116 | |||
117 | reg = csi2rx->num_lanes << 8; | ||
118 | for (i = 0; i < csi2rx->num_lanes; i++) { | ||
119 | reg |= CSI2RX_STATIC_CFG_DLANE_MAP(i, csi2rx->lanes[i]); | ||
120 | set_bit(csi2rx->lanes[i], &lanes_used); | ||
121 | } | ||
122 | |||
123 | /* | ||
124 | * Even the unused lanes need to be mapped. In order to avoid | ||
125 | * to map twice to the same physical lane, keep the lanes used | ||
126 | * in the previous loop, and only map unused physical lanes to | ||
127 | * the rest of our logical lanes. | ||
128 | */ | ||
129 | for (i = csi2rx->num_lanes; i < csi2rx->max_lanes; i++) { | ||
130 | unsigned int idx = find_first_zero_bit(&lanes_used, | ||
131 | sizeof(lanes_used)); | ||
132 | set_bit(idx, &lanes_used); | ||
133 | reg |= CSI2RX_STATIC_CFG_DLANE_MAP(i, i + 1); | ||
134 | } | ||
135 | |||
136 | writel(reg, csi2rx->base + CSI2RX_STATIC_CFG_REG); | ||
137 | |||
138 | ret = v4l2_subdev_call(csi2rx->source_subdev, video, s_stream, true); | ||
139 | if (ret) | ||
140 | goto err_disable_pclk; | ||
141 | |||
142 | /* | ||
143 | * Create a static mapping between the CSI virtual channels | ||
144 | * and the output stream. | ||
145 | * | ||
146 | * This should be enhanced, but v4l2 lacks the support for | ||
147 | * changing that mapping dynamically. | ||
148 | * | ||
149 | * We also cannot enable and disable independent streams here, | ||
150 | * hence the reference counting. | ||
151 | */ | ||
152 | for (i = 0; i < csi2rx->max_streams; i++) { | ||
153 | ret = clk_prepare_enable(csi2rx->pixel_clk[i]); | ||
154 | if (ret) | ||
155 | goto err_disable_pixclk; | ||
156 | |||
157 | writel(CSI2RX_STREAM_CFG_FIFO_MODE_LARGE_BUF, | ||
158 | csi2rx->base + CSI2RX_STREAM_CFG_REG(i)); | ||
159 | |||
160 | writel(CSI2RX_STREAM_DATA_CFG_EN_VC_SELECT | | ||
161 | CSI2RX_STREAM_DATA_CFG_VC_SELECT(i), | ||
162 | csi2rx->base + CSI2RX_STREAM_DATA_CFG_REG(i)); | ||
163 | |||
164 | writel(CSI2RX_STREAM_CTRL_START, | ||
165 | csi2rx->base + CSI2RX_STREAM_CTRL_REG(i)); | ||
166 | } | ||
167 | |||
168 | ret = clk_prepare_enable(csi2rx->sys_clk); | ||
169 | if (ret) | ||
170 | goto err_disable_pixclk; | ||
171 | |||
172 | clk_disable_unprepare(csi2rx->p_clk); | ||
173 | |||
174 | return 0; | ||
175 | |||
176 | err_disable_pixclk: | ||
177 | for (; i >= 0; i--) | ||
178 | clk_disable_unprepare(csi2rx->pixel_clk[i]); | ||
179 | |||
180 | err_disable_pclk: | ||
181 | clk_disable_unprepare(csi2rx->p_clk); | ||
182 | |||
183 | return ret; | ||
184 | } | ||
185 | |||
186 | static void csi2rx_stop(struct csi2rx_priv *csi2rx) | ||
187 | { | ||
188 | unsigned int i; | ||
189 | |||
190 | clk_prepare_enable(csi2rx->p_clk); | ||
191 | clk_disable_unprepare(csi2rx->sys_clk); | ||
192 | |||
193 | for (i = 0; i < csi2rx->max_streams; i++) { | ||
194 | writel(0, csi2rx->base + CSI2RX_STREAM_CTRL_REG(i)); | ||
195 | |||
196 | clk_disable_unprepare(csi2rx->pixel_clk[i]); | ||
197 | } | ||
198 | |||
199 | clk_disable_unprepare(csi2rx->p_clk); | ||
200 | |||
201 | if (v4l2_subdev_call(csi2rx->source_subdev, video, s_stream, false)) | ||
202 | dev_warn(csi2rx->dev, "Couldn't disable our subdev\n"); | ||
203 | } | ||
204 | |||
205 | static int csi2rx_s_stream(struct v4l2_subdev *subdev, int enable) | ||
206 | { | ||
207 | struct csi2rx_priv *csi2rx = v4l2_subdev_to_csi2rx(subdev); | ||
208 | int ret = 0; | ||
209 | |||
210 | mutex_lock(&csi2rx->lock); | ||
211 | |||
212 | if (enable) { | ||
213 | /* | ||
214 | * If we're not the first users, there's no need to | ||
215 | * enable the whole controller. | ||
216 | */ | ||
217 | if (!csi2rx->count) { | ||
218 | ret = csi2rx_start(csi2rx); | ||
219 | if (ret) | ||
220 | goto out; | ||
221 | } | ||
222 | |||
223 | csi2rx->count++; | ||
224 | } else { | ||
225 | csi2rx->count--; | ||
226 | |||
227 | /* | ||
228 | * Let the last user turn off the lights. | ||
229 | */ | ||
230 | if (!csi2rx->count) | ||
231 | csi2rx_stop(csi2rx); | ||
232 | } | ||
233 | |||
234 | out: | ||
235 | mutex_unlock(&csi2rx->lock); | ||
236 | return ret; | ||
237 | } | ||
238 | |||
239 | static const struct v4l2_subdev_video_ops csi2rx_video_ops = { | ||
240 | .s_stream = csi2rx_s_stream, | ||
241 | }; | ||
242 | |||
243 | static const struct v4l2_subdev_ops csi2rx_subdev_ops = { | ||
244 | .video = &csi2rx_video_ops, | ||
245 | }; | ||
246 | |||
247 | static int csi2rx_async_bound(struct v4l2_async_notifier *notifier, | ||
248 | struct v4l2_subdev *s_subdev, | ||
249 | struct v4l2_async_subdev *asd) | ||
250 | { | ||
251 | struct v4l2_subdev *subdev = notifier->sd; | ||
252 | struct csi2rx_priv *csi2rx = v4l2_subdev_to_csi2rx(subdev); | ||
253 | |||
254 | csi2rx->source_pad = media_entity_get_fwnode_pad(&s_subdev->entity, | ||
255 | s_subdev->fwnode, | ||
256 | MEDIA_PAD_FL_SOURCE); | ||
257 | if (csi2rx->source_pad < 0) { | ||
258 | dev_err(csi2rx->dev, "Couldn't find output pad for subdev %s\n", | ||
259 | s_subdev->name); | ||
260 | return csi2rx->source_pad; | ||
261 | } | ||
262 | |||
263 | csi2rx->source_subdev = s_subdev; | ||
264 | |||
265 | dev_dbg(csi2rx->dev, "Bound %s pad: %d\n", s_subdev->name, | ||
266 | csi2rx->source_pad); | ||
267 | |||
268 | return media_create_pad_link(&csi2rx->source_subdev->entity, | ||
269 | csi2rx->source_pad, | ||
270 | &csi2rx->subdev.entity, 0, | ||
271 | MEDIA_LNK_FL_ENABLED | | ||
272 | MEDIA_LNK_FL_IMMUTABLE); | ||
273 | } | ||
274 | |||
275 | static const struct v4l2_async_notifier_operations csi2rx_notifier_ops = { | ||
276 | .bound = csi2rx_async_bound, | ||
277 | }; | ||
278 | |||
279 | static int csi2rx_get_resources(struct csi2rx_priv *csi2rx, | ||
280 | struct platform_device *pdev) | ||
281 | { | ||
282 | struct resource *res; | ||
283 | unsigned char i; | ||
284 | u32 dev_cfg; | ||
285 | |||
286 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); | ||
287 | csi2rx->base = devm_ioremap_resource(&pdev->dev, res); | ||
288 | if (IS_ERR(csi2rx->base)) | ||
289 | return PTR_ERR(csi2rx->base); | ||
290 | |||
291 | csi2rx->sys_clk = devm_clk_get(&pdev->dev, "sys_clk"); | ||
292 | if (IS_ERR(csi2rx->sys_clk)) { | ||
293 | dev_err(&pdev->dev, "Couldn't get sys clock\n"); | ||
294 | return PTR_ERR(csi2rx->sys_clk); | ||
295 | } | ||
296 | |||
297 | csi2rx->p_clk = devm_clk_get(&pdev->dev, "p_clk"); | ||
298 | if (IS_ERR(csi2rx->p_clk)) { | ||
299 | dev_err(&pdev->dev, "Couldn't get P clock\n"); | ||
300 | return PTR_ERR(csi2rx->p_clk); | ||
301 | } | ||
302 | |||
303 | csi2rx->dphy = devm_phy_optional_get(&pdev->dev, "dphy"); | ||
304 | if (IS_ERR(csi2rx->dphy)) { | ||
305 | dev_err(&pdev->dev, "Couldn't get external D-PHY\n"); | ||
306 | return PTR_ERR(csi2rx->dphy); | ||
307 | } | ||
308 | |||
309 | /* | ||
310 | * FIXME: Once we'll have external D-PHY support, the check | ||
311 | * will need to be removed. | ||
312 | */ | ||
313 | if (csi2rx->dphy) { | ||
314 | dev_err(&pdev->dev, "External D-PHY not supported yet\n"); | ||
315 | return -EINVAL; | ||
316 | } | ||
317 | |||
318 | clk_prepare_enable(csi2rx->p_clk); | ||
319 | dev_cfg = readl(csi2rx->base + CSI2RX_DEVICE_CFG_REG); | ||
320 | clk_disable_unprepare(csi2rx->p_clk); | ||
321 | |||
322 | csi2rx->max_lanes = dev_cfg & 7; | ||
323 | if (csi2rx->max_lanes > CSI2RX_LANES_MAX) { | ||
324 | dev_err(&pdev->dev, "Invalid number of lanes: %u\n", | ||
325 | csi2rx->max_lanes); | ||
326 | return -EINVAL; | ||
327 | } | ||
328 | |||
329 | csi2rx->max_streams = (dev_cfg >> 4) & 7; | ||
330 | if (csi2rx->max_streams > CSI2RX_STREAMS_MAX) { | ||
331 | dev_err(&pdev->dev, "Invalid number of streams: %u\n", | ||
332 | csi2rx->max_streams); | ||
333 | return -EINVAL; | ||
334 | } | ||
335 | |||
336 | csi2rx->has_internal_dphy = dev_cfg & BIT(3) ? true : false; | ||
337 | |||
338 | /* | ||
339 | * FIXME: Once we'll have internal D-PHY support, the check | ||
340 | * will need to be removed. | ||
341 | */ | ||
342 | if (csi2rx->has_internal_dphy) { | ||
343 | dev_err(&pdev->dev, "Internal D-PHY not supported yet\n"); | ||
344 | return -EINVAL; | ||
345 | } | ||
346 | |||
347 | for (i = 0; i < csi2rx->max_streams; i++) { | ||
348 | char clk_name[16]; | ||
349 | |||
350 | snprintf(clk_name, sizeof(clk_name), "pixel_if%u_clk", i); | ||
351 | csi2rx->pixel_clk[i] = devm_clk_get(&pdev->dev, clk_name); | ||
352 | if (IS_ERR(csi2rx->pixel_clk[i])) { | ||
353 | dev_err(&pdev->dev, "Couldn't get clock %s\n", clk_name); | ||
354 | return PTR_ERR(csi2rx->pixel_clk[i]); | ||
355 | } | ||
356 | } | ||
357 | |||
358 | return 0; | ||
359 | } | ||
360 | |||
361 | static int csi2rx_parse_dt(struct csi2rx_priv *csi2rx) | ||
362 | { | ||
363 | struct v4l2_fwnode_endpoint v4l2_ep; | ||
364 | struct fwnode_handle *fwh; | ||
365 | struct device_node *ep; | ||
366 | int ret; | ||
367 | |||
368 | ep = of_graph_get_endpoint_by_regs(csi2rx->dev->of_node, 0, 0); | ||
369 | if (!ep) | ||
370 | return -EINVAL; | ||
371 | |||
372 | fwh = of_fwnode_handle(ep); | ||
373 | ret = v4l2_fwnode_endpoint_parse(fwh, &v4l2_ep); | ||
374 | if (ret) { | ||
375 | dev_err(csi2rx->dev, "Could not parse v4l2 endpoint\n"); | ||
376 | of_node_put(ep); | ||
377 | return ret; | ||
378 | } | ||
379 | |||
380 | if (v4l2_ep.bus_type != V4L2_MBUS_CSI2) { | ||
381 | dev_err(csi2rx->dev, "Unsupported media bus type: 0x%x\n", | ||
382 | v4l2_ep.bus_type); | ||
383 | of_node_put(ep); | ||
384 | return -EINVAL; | ||
385 | } | ||
386 | |||
387 | memcpy(csi2rx->lanes, v4l2_ep.bus.mipi_csi2.data_lanes, | ||
388 | sizeof(csi2rx->lanes)); | ||
389 | csi2rx->num_lanes = v4l2_ep.bus.mipi_csi2.num_data_lanes; | ||
390 | if (csi2rx->num_lanes > csi2rx->max_lanes) { | ||
391 | dev_err(csi2rx->dev, "Unsupported number of data-lanes: %d\n", | ||
392 | csi2rx->num_lanes); | ||
393 | of_node_put(ep); | ||
394 | return -EINVAL; | ||
395 | } | ||
396 | |||
397 | csi2rx->asd.match.fwnode = fwnode_graph_get_remote_port_parent(fwh); | ||
398 | csi2rx->asd.match_type = V4L2_ASYNC_MATCH_FWNODE; | ||
399 | of_node_put(ep); | ||
400 | |||
401 | csi2rx->notifier.subdevs = devm_kzalloc(csi2rx->dev, | ||
402 | sizeof(*csi2rx->notifier.subdevs), | ||
403 | GFP_KERNEL); | ||
404 | if (!csi2rx->notifier.subdevs) | ||
405 | return -ENOMEM; | ||
406 | |||
407 | csi2rx->notifier.subdevs[0] = &csi2rx->asd; | ||
408 | csi2rx->notifier.num_subdevs = 1; | ||
409 | csi2rx->notifier.ops = &csi2rx_notifier_ops; | ||
410 | |||
411 | return v4l2_async_subdev_notifier_register(&csi2rx->subdev, | ||
412 | &csi2rx->notifier); | ||
413 | } | ||
414 | |||
415 | static int csi2rx_probe(struct platform_device *pdev) | ||
416 | { | ||
417 | struct csi2rx_priv *csi2rx; | ||
418 | unsigned int i; | ||
419 | int ret; | ||
420 | |||
421 | csi2rx = kzalloc(sizeof(*csi2rx), GFP_KERNEL); | ||
422 | if (!csi2rx) | ||
423 | return -ENOMEM; | ||
424 | platform_set_drvdata(pdev, csi2rx); | ||
425 | csi2rx->dev = &pdev->dev; | ||
426 | mutex_init(&csi2rx->lock); | ||
427 | |||
428 | ret = csi2rx_get_resources(csi2rx, pdev); | ||
429 | if (ret) | ||
430 | goto err_free_priv; | ||
431 | |||
432 | ret = csi2rx_parse_dt(csi2rx); | ||
433 | if (ret) | ||
434 | goto err_free_priv; | ||
435 | |||
436 | csi2rx->subdev.owner = THIS_MODULE; | ||
437 | csi2rx->subdev.dev = &pdev->dev; | ||
438 | v4l2_subdev_init(&csi2rx->subdev, &csi2rx_subdev_ops); | ||
439 | v4l2_set_subdevdata(&csi2rx->subdev, &pdev->dev); | ||
440 | snprintf(csi2rx->subdev.name, V4L2_SUBDEV_NAME_SIZE, "%s.%s", | ||
441 | KBUILD_MODNAME, dev_name(&pdev->dev)); | ||
442 | |||
443 | /* Create our media pads */ | ||
444 | csi2rx->subdev.entity.function = MEDIA_ENT_F_VID_IF_BRIDGE; | ||
445 | csi2rx->pads[CSI2RX_PAD_SINK].flags = MEDIA_PAD_FL_SINK; | ||
446 | for (i = CSI2RX_PAD_SOURCE_STREAM0; i < CSI2RX_PAD_MAX; i++) | ||
447 | csi2rx->pads[i].flags = MEDIA_PAD_FL_SOURCE; | ||
448 | |||
449 | ret = media_entity_pads_init(&csi2rx->subdev.entity, CSI2RX_PAD_MAX, | ||
450 | csi2rx->pads); | ||
451 | if (ret) | ||
452 | goto err_free_priv; | ||
453 | |||
454 | ret = v4l2_async_register_subdev(&csi2rx->subdev); | ||
455 | if (ret < 0) | ||
456 | goto err_free_priv; | ||
457 | |||
458 | dev_info(&pdev->dev, | ||
459 | "Probed CSI2RX with %u/%u lanes, %u streams, %s D-PHY\n", | ||
460 | csi2rx->num_lanes, csi2rx->max_lanes, csi2rx->max_streams, | ||
461 | csi2rx->has_internal_dphy ? "internal" : "no"); | ||
462 | |||
463 | return 0; | ||
464 | |||
465 | err_free_priv: | ||
466 | kfree(csi2rx); | ||
467 | return ret; | ||
468 | } | ||
469 | |||
470 | static int csi2rx_remove(struct platform_device *pdev) | ||
471 | { | ||
472 | struct csi2rx_priv *csi2rx = platform_get_drvdata(pdev); | ||
473 | |||
474 | v4l2_async_unregister_subdev(&csi2rx->subdev); | ||
475 | kfree(csi2rx); | ||
476 | |||
477 | return 0; | ||
478 | } | ||
479 | |||
480 | static const struct of_device_id csi2rx_of_table[] = { | ||
481 | { .compatible = "cdns,csi2rx" }, | ||
482 | { }, | ||
483 | }; | ||
484 | MODULE_DEVICE_TABLE(of, csi2rx_of_table); | ||
485 | |||
486 | static struct platform_driver csi2rx_driver = { | ||
487 | .probe = csi2rx_probe, | ||
488 | .remove = csi2rx_remove, | ||
489 | |||
490 | .driver = { | ||
491 | .name = "cdns-csi2rx", | ||
492 | .of_match_table = csi2rx_of_table, | ||
493 | }, | ||
494 | }; | ||
495 | module_platform_driver(csi2rx_driver); | ||
496 | MODULE_AUTHOR("Maxime Ripard <maxime.ripard@bootlin.com>"); | ||
497 | MODULE_DESCRIPTION("Cadence CSI2-RX controller"); | ||
498 | MODULE_LICENSE("GPL"); | ||