Commit | Line | Data |
---|---|---|
ca50c197 SA |
1 | /* |
2 | * V4L2 fwnode binding parsing library | |
3 | * | |
4 | * The origins of the V4L2 fwnode library are in V4L2 OF library that | |
5 | * formerly was located in v4l2-of.c. | |
6 | * | |
7 | * Copyright (c) 2016 Intel Corporation. | |
8 | * Author: Sakari Ailus <sakari.ailus@linux.intel.com> | |
9 | * | |
10 | * Copyright (C) 2012 - 2013 Samsung Electronics Co., Ltd. | |
11 | * Author: Sylwester Nawrocki <s.nawrocki@samsung.com> | |
12 | * | |
13 | * Copyright (C) 2012 Renesas Electronics Corp. | |
14 | * Author: Guennadi Liakhovetski <g.liakhovetski@gmx.de> | |
15 | * | |
16 | * This program is free software; you can redistribute it and/or modify | |
17 | * it under the terms of version 2 of the GNU General Public License as | |
18 | * published by the Free Software Foundation. | |
19 | */ | |
20 | #include <linux/acpi.h> | |
21 | #include <linux/kernel.h> | |
9ca46531 | 22 | #include <linux/mm.h> |
ca50c197 SA |
23 | #include <linux/module.h> |
24 | #include <linux/of.h> | |
25 | #include <linux/property.h> | |
26 | #include <linux/slab.h> | |
27 | #include <linux/string.h> | |
28 | #include <linux/types.h> | |
29 | ||
9ca46531 | 30 | #include <media/v4l2-async.h> |
ca50c197 | 31 | #include <media/v4l2-fwnode.h> |
aef69d54 | 32 | #include <media/v4l2-subdev.h> |
ca50c197 | 33 | |
e07a41f9 SA |
34 | enum v4l2_fwnode_bus_type { |
35 | V4L2_FWNODE_BUS_TYPE_GUESS = 0, | |
36 | V4L2_FWNODE_BUS_TYPE_CSI2_CPHY, | |
37 | V4L2_FWNODE_BUS_TYPE_CSI1, | |
38 | V4L2_FWNODE_BUS_TYPE_CCP2, | |
bf63856a SA |
39 | V4L2_FWNODE_BUS_TYPE_CSI2_DPHY, |
40 | V4L2_FWNODE_BUS_TYPE_PARALLEL, | |
41 | V4L2_FWNODE_BUS_TYPE_BT656, | |
e07a41f9 SA |
42 | NR_OF_V4L2_FWNODE_BUS_TYPE, |
43 | }; | |
44 | ||
f3112735 SA |
45 | static int v4l2_fwnode_endpoint_parse_csi2_bus(struct fwnode_handle *fwnode, |
46 | struct v4l2_fwnode_endpoint *vep) | |
ca50c197 SA |
47 | { |
48 | struct v4l2_fwnode_bus_mipi_csi2 *bus = &vep->bus.mipi_csi2; | |
49 | bool have_clk_lane = false; | |
50 | unsigned int flags = 0, lanes_used = 0; | |
51 | unsigned int i; | |
52 | u32 v; | |
53 | int rval; | |
54 | ||
55 | rval = fwnode_property_read_u32_array(fwnode, "data-lanes", NULL, 0); | |
56 | if (rval > 0) { | |
ad3cdf3e | 57 | u32 array[1 + V4L2_FWNODE_CSI2_MAX_DATA_LANES]; |
ca50c197 | 58 | |
ad3cdf3e SA |
59 | bus->num_data_lanes = |
60 | min_t(int, V4L2_FWNODE_CSI2_MAX_DATA_LANES, rval); | |
ca50c197 SA |
61 | |
62 | fwnode_property_read_u32_array(fwnode, "data-lanes", array, | |
63 | bus->num_data_lanes); | |
64 | ||
65 | for (i = 0; i < bus->num_data_lanes; i++) { | |
66 | if (lanes_used & BIT(array[i])) | |
67 | pr_warn("duplicated lane %u in data-lanes\n", | |
68 | array[i]); | |
69 | lanes_used |= BIT(array[i]); | |
70 | ||
71 | bus->data_lanes[i] = array[i]; | |
c8677aaf | 72 | pr_debug("lane %u position %u\n", i, array[i]); |
ca50c197 | 73 | } |
ca50c197 | 74 | |
4ee23621 | 75 | rval = fwnode_property_read_u32_array(fwnode, |
b24f0215 SA |
76 | "lane-polarities", NULL, |
77 | 0); | |
4ee23621 | 78 | if (rval > 0) { |
b24f0215 | 79 | if (rval != 1 + bus->num_data_lanes /* clock+data */) { |
4ee23621 MCC |
80 | pr_warn("invalid number of lane-polarities entries (need %u, got %u)\n", |
81 | 1 + bus->num_data_lanes, rval); | |
82 | return -EINVAL; | |
83 | } | |
ca50c197 | 84 | |
b24f0215 SA |
85 | fwnode_property_read_u32_array(fwnode, |
86 | "lane-polarities", array, | |
87 | 1 + bus->num_data_lanes); | |
ca50c197 | 88 | |
c8677aaf | 89 | for (i = 0; i < 1 + bus->num_data_lanes; i++) { |
4ee23621 | 90 | bus->lane_polarities[i] = array[i]; |
c8677aaf SA |
91 | pr_debug("lane %u polarity %sinverted", |
92 | i, array[i] ? "" : "not "); | |
93 | } | |
94 | } else { | |
95 | pr_debug("no lane polarities defined, assuming not inverted\n"); | |
4ee23621 | 96 | } |
b24f0215 | 97 | |
ca50c197 SA |
98 | } |
99 | ||
100 | if (!fwnode_property_read_u32(fwnode, "clock-lanes", &v)) { | |
101 | if (lanes_used & BIT(v)) | |
102 | pr_warn("duplicated lane %u in clock-lanes\n", v); | |
103 | lanes_used |= BIT(v); | |
104 | ||
105 | bus->clock_lane = v; | |
106 | have_clk_lane = true; | |
c8677aaf | 107 | pr_debug("clock lane position %u\n", v); |
ca50c197 SA |
108 | } |
109 | ||
c8677aaf | 110 | if (fwnode_property_present(fwnode, "clock-noncontinuous")) { |
ca50c197 | 111 | flags |= V4L2_MBUS_CSI2_NONCONTINUOUS_CLOCK; |
c8677aaf | 112 | pr_debug("non-continuous clock\n"); |
d4865326 | 113 | } else { |
ca50c197 | 114 | flags |= V4L2_MBUS_CSI2_CONTINUOUS_CLOCK; |
c8677aaf | 115 | } |
ca50c197 | 116 | |
2835b5b1 SA |
117 | if (lanes_used || have_clk_lane || |
118 | (flags & ~V4L2_MBUS_CSI2_CONTINUOUS_CLOCK)) { | |
119 | bus->flags = flags; | |
120 | vep->bus_type = V4L2_MBUS_CSI2_DPHY; | |
121 | } | |
ca50c197 SA |
122 | |
123 | return 0; | |
124 | } | |
125 | ||
175b18b8 SA |
126 | #define PARALLEL_MBUS_FLAGS (V4L2_MBUS_HSYNC_ACTIVE_HIGH | \ |
127 | V4L2_MBUS_HSYNC_ACTIVE_LOW | \ | |
128 | V4L2_MBUS_VSYNC_ACTIVE_HIGH | \ | |
129 | V4L2_MBUS_VSYNC_ACTIVE_LOW | \ | |
130 | V4L2_MBUS_FIELD_EVEN_HIGH | \ | |
131 | V4L2_MBUS_FIELD_EVEN_LOW) | |
132 | ||
ca50c197 | 133 | static void v4l2_fwnode_endpoint_parse_parallel_bus( |
175b18b8 SA |
134 | struct fwnode_handle *fwnode, struct v4l2_fwnode_endpoint *vep, |
135 | enum v4l2_fwnode_bus_type bus_type) | |
ca50c197 SA |
136 | { |
137 | struct v4l2_fwnode_bus_parallel *bus = &vep->bus.parallel; | |
138 | unsigned int flags = 0; | |
139 | u32 v; | |
140 | ||
c8677aaf | 141 | if (!fwnode_property_read_u32(fwnode, "hsync-active", &v)) { |
ca50c197 SA |
142 | flags |= v ? V4L2_MBUS_HSYNC_ACTIVE_HIGH : |
143 | V4L2_MBUS_HSYNC_ACTIVE_LOW; | |
c8677aaf SA |
144 | pr_debug("hsync-active %s\n", v ? "high" : "low"); |
145 | } | |
ca50c197 | 146 | |
c8677aaf | 147 | if (!fwnode_property_read_u32(fwnode, "vsync-active", &v)) { |
ca50c197 SA |
148 | flags |= v ? V4L2_MBUS_VSYNC_ACTIVE_HIGH : |
149 | V4L2_MBUS_VSYNC_ACTIVE_LOW; | |
c8677aaf SA |
150 | pr_debug("vsync-active %s\n", v ? "high" : "low"); |
151 | } | |
ca50c197 | 152 | |
c8677aaf | 153 | if (!fwnode_property_read_u32(fwnode, "field-even-active", &v)) { |
ca50c197 SA |
154 | flags |= v ? V4L2_MBUS_FIELD_EVEN_HIGH : |
155 | V4L2_MBUS_FIELD_EVEN_LOW; | |
c8677aaf SA |
156 | pr_debug("field-even-active %s\n", v ? "high" : "low"); |
157 | } | |
158 | ||
c8677aaf | 159 | if (!fwnode_property_read_u32(fwnode, "pclk-sample", &v)) { |
ca50c197 SA |
160 | flags |= v ? V4L2_MBUS_PCLK_SAMPLE_RISING : |
161 | V4L2_MBUS_PCLK_SAMPLE_FALLING; | |
c8677aaf SA |
162 | pr_debug("pclk-sample %s\n", v ? "high" : "low"); |
163 | } | |
ca50c197 | 164 | |
c8677aaf | 165 | if (!fwnode_property_read_u32(fwnode, "data-active", &v)) { |
ca50c197 SA |
166 | flags |= v ? V4L2_MBUS_DATA_ACTIVE_HIGH : |
167 | V4L2_MBUS_DATA_ACTIVE_LOW; | |
c8677aaf SA |
168 | pr_debug("data-active %s\n", v ? "high" : "low"); |
169 | } | |
ca50c197 | 170 | |
c8677aaf SA |
171 | if (fwnode_property_present(fwnode, "slave-mode")) { |
172 | pr_debug("slave mode\n"); | |
ca50c197 | 173 | flags |= V4L2_MBUS_SLAVE; |
c8677aaf | 174 | } else { |
ca50c197 | 175 | flags |= V4L2_MBUS_MASTER; |
c8677aaf | 176 | } |
ca50c197 | 177 | |
c8677aaf | 178 | if (!fwnode_property_read_u32(fwnode, "bus-width", &v)) { |
ca50c197 | 179 | bus->bus_width = v; |
c8677aaf SA |
180 | pr_debug("bus-width %u\n", v); |
181 | } | |
ca50c197 | 182 | |
c8677aaf | 183 | if (!fwnode_property_read_u32(fwnode, "data-shift", &v)) { |
ca50c197 | 184 | bus->data_shift = v; |
c8677aaf SA |
185 | pr_debug("data-shift %u\n", v); |
186 | } | |
ca50c197 | 187 | |
c8677aaf | 188 | if (!fwnode_property_read_u32(fwnode, "sync-on-green-active", &v)) { |
ca50c197 SA |
189 | flags |= v ? V4L2_MBUS_VIDEO_SOG_ACTIVE_HIGH : |
190 | V4L2_MBUS_VIDEO_SOG_ACTIVE_LOW; | |
c8677aaf SA |
191 | pr_debug("sync-on-green-active %s\n", v ? "high" : "low"); |
192 | } | |
ca50c197 | 193 | |
c8677aaf | 194 | if (!fwnode_property_read_u32(fwnode, "data-enable-active", &v)) { |
9b04fcc1 JM |
195 | flags |= v ? V4L2_MBUS_DATA_ENABLE_HIGH : |
196 | V4L2_MBUS_DATA_ENABLE_LOW; | |
c8677aaf SA |
197 | pr_debug("data-enable-active %s\n", v ? "high" : "low"); |
198 | } | |
9b04fcc1 | 199 | |
175b18b8 SA |
200 | switch (bus_type) { |
201 | default: | |
202 | bus->flags = flags; | |
203 | if (flags & PARALLEL_MBUS_FLAGS) | |
204 | vep->bus_type = V4L2_MBUS_PARALLEL; | |
205 | else | |
206 | vep->bus_type = V4L2_MBUS_BT656; | |
207 | break; | |
208 | case V4L2_FWNODE_BUS_TYPE_PARALLEL: | |
2835b5b1 | 209 | vep->bus_type = V4L2_MBUS_PARALLEL; |
175b18b8 SA |
210 | bus->flags = flags; |
211 | break; | |
212 | case V4L2_FWNODE_BUS_TYPE_BT656: | |
2835b5b1 | 213 | vep->bus_type = V4L2_MBUS_BT656; |
175b18b8 SA |
214 | bus->flags = flags & ~PARALLEL_MBUS_FLAGS; |
215 | break; | |
216 | } | |
ca50c197 SA |
217 | } |
218 | ||
abc5b2cb MCC |
219 | static void |
220 | v4l2_fwnode_endpoint_parse_csi1_bus(struct fwnode_handle *fwnode, | |
221 | struct v4l2_fwnode_endpoint *vep, | |
2835b5b1 | 222 | enum v4l2_fwnode_bus_type bus_type) |
97bbdf02 SA |
223 | { |
224 | struct v4l2_fwnode_bus_mipi_csi1 *bus = &vep->bus.mipi_csi1; | |
225 | u32 v; | |
226 | ||
c8677aaf | 227 | if (!fwnode_property_read_u32(fwnode, "clock-inv", &v)) { |
97bbdf02 | 228 | bus->clock_inv = v; |
c8677aaf SA |
229 | pr_debug("clock-inv %u\n", v); |
230 | } | |
97bbdf02 | 231 | |
c8677aaf | 232 | if (!fwnode_property_read_u32(fwnode, "strobe", &v)) { |
97bbdf02 | 233 | bus->strobe = v; |
c8677aaf SA |
234 | pr_debug("strobe %u\n", v); |
235 | } | |
97bbdf02 | 236 | |
c8677aaf | 237 | if (!fwnode_property_read_u32(fwnode, "data-lanes", &v)) { |
97bbdf02 | 238 | bus->data_lane = v; |
c8677aaf SA |
239 | pr_debug("data-lanes %u\n", v); |
240 | } | |
97bbdf02 | 241 | |
c8677aaf | 242 | if (!fwnode_property_read_u32(fwnode, "clock-lanes", &v)) { |
97bbdf02 | 243 | bus->clock_lane = v; |
c8677aaf SA |
244 | pr_debug("clock-lanes %u\n", v); |
245 | } | |
97bbdf02 SA |
246 | |
247 | if (bus_type == V4L2_FWNODE_BUS_TYPE_CCP2) | |
248 | vep->bus_type = V4L2_MBUS_CCP2; | |
249 | else | |
250 | vep->bus_type = V4L2_MBUS_CSI1; | |
251 | } | |
252 | ||
c8677aaf SA |
253 | static int __v4l2_fwnode_endpoint_parse(struct fwnode_handle *fwnode, |
254 | struct v4l2_fwnode_endpoint *vep) | |
ca50c197 | 255 | { |
e07a41f9 | 256 | u32 bus_type = 0; |
ca50c197 SA |
257 | int rval; |
258 | ||
c8677aaf SA |
259 | pr_debug("===== begin V4L2 endpoint properties\n"); |
260 | ||
ca50c197 SA |
261 | fwnode_graph_parse_endpoint(fwnode, &vep->base); |
262 | ||
263 | /* Zero fields from bus_type to until the end */ | |
264 | memset(&vep->bus_type, 0, sizeof(*vep) - | |
265 | offsetof(typeof(*vep), bus_type)); | |
266 | ||
e07a41f9 SA |
267 | fwnode_property_read_u32(fwnode, "bus-type", &bus_type); |
268 | ||
97bbdf02 SA |
269 | switch (bus_type) { |
270 | case V4L2_FWNODE_BUS_TYPE_GUESS: | |
271 | rval = v4l2_fwnode_endpoint_parse_csi2_bus(fwnode, vep); | |
272 | if (rval) | |
273 | return rval; | |
2835b5b1 SA |
274 | |
275 | if (vep->bus_type == V4L2_MBUS_UNKNOWN) | |
175b18b8 SA |
276 | v4l2_fwnode_endpoint_parse_parallel_bus( |
277 | fwnode, vep, V4L2_MBUS_UNKNOWN); | |
97bbdf02 | 278 | |
c8677aaf | 279 | break; |
97bbdf02 SA |
280 | case V4L2_FWNODE_BUS_TYPE_CCP2: |
281 | case V4L2_FWNODE_BUS_TYPE_CSI1: | |
282 | v4l2_fwnode_endpoint_parse_csi1_bus(fwnode, vep, bus_type); | |
283 | ||
175b18b8 SA |
284 | break; |
285 | case V4L2_FWNODE_BUS_TYPE_CSI2_DPHY: | |
286 | vep->bus_type = V4L2_MBUS_CSI2_DPHY; | |
287 | rval = v4l2_fwnode_endpoint_parse_csi2_bus(fwnode, vep); | |
288 | if (rval) | |
289 | return rval; | |
290 | ||
291 | break; | |
292 | case V4L2_FWNODE_BUS_TYPE_PARALLEL: | |
293 | case V4L2_FWNODE_BUS_TYPE_BT656: | |
294 | v4l2_fwnode_endpoint_parse_parallel_bus(fwnode, vep, bus_type); | |
295 | ||
c8677aaf | 296 | break; |
97bbdf02 SA |
297 | default: |
298 | pr_warn("unsupported bus type %u\n", bus_type); | |
299 | return -EINVAL; | |
300 | } | |
c8677aaf SA |
301 | |
302 | return 0; | |
303 | } | |
304 | ||
305 | int v4l2_fwnode_endpoint_parse(struct fwnode_handle *fwnode, | |
306 | struct v4l2_fwnode_endpoint *vep) | |
307 | { | |
308 | int ret; | |
309 | ||
310 | ret = __v4l2_fwnode_endpoint_parse(fwnode, vep); | |
311 | ||
312 | pr_debug("===== end V4L2 endpoint properties\n"); | |
313 | ||
314 | return ret; | |
ca50c197 SA |
315 | } |
316 | EXPORT_SYMBOL_GPL(v4l2_fwnode_endpoint_parse); | |
317 | ||
ca50c197 SA |
318 | void v4l2_fwnode_endpoint_free(struct v4l2_fwnode_endpoint *vep) |
319 | { | |
320 | if (IS_ERR_OR_NULL(vep)) | |
321 | return; | |
322 | ||
323 | kfree(vep->link_frequencies); | |
ca50c197 SA |
324 | } |
325 | EXPORT_SYMBOL_GPL(v4l2_fwnode_endpoint_free); | |
326 | ||
6970d37c SA |
327 | int v4l2_fwnode_endpoint_alloc_parse( |
328 | struct fwnode_handle *fwnode, struct v4l2_fwnode_endpoint *vep) | |
ca50c197 | 329 | { |
ca50c197 SA |
330 | int rval; |
331 | ||
c8677aaf | 332 | rval = __v4l2_fwnode_endpoint_parse(fwnode, vep); |
ca50c197 | 333 | if (rval < 0) |
6970d37c | 334 | return rval; |
ca50c197 SA |
335 | |
336 | rval = fwnode_property_read_u64_array(fwnode, "link-frequencies", | |
337 | NULL, 0); | |
06f81520 | 338 | if (rval > 0) { |
c8677aaf SA |
339 | unsigned int i; |
340 | ||
06f81520 SA |
341 | vep->link_frequencies = |
342 | kmalloc_array(rval, sizeof(*vep->link_frequencies), | |
343 | GFP_KERNEL); | |
6970d37c SA |
344 | if (!vep->link_frequencies) |
345 | return -ENOMEM; | |
ca50c197 | 346 | |
06f81520 | 347 | vep->nr_of_link_frequencies = rval; |
ca50c197 | 348 | |
06f81520 SA |
349 | rval = fwnode_property_read_u64_array( |
350 | fwnode, "link-frequencies", vep->link_frequencies, | |
351 | vep->nr_of_link_frequencies); | |
6970d37c SA |
352 | if (rval < 0) { |
353 | v4l2_fwnode_endpoint_free(vep); | |
354 | return rval; | |
355 | } | |
c8677aaf SA |
356 | |
357 | for (i = 0; i < vep->nr_of_link_frequencies; i++) | |
358 | pr_info("link-frequencies %u value %llu\n", i, | |
359 | vep->link_frequencies[i]); | |
06f81520 | 360 | } |
ca50c197 | 361 | |
c8677aaf SA |
362 | pr_debug("===== end V4L2 endpoint properties\n"); |
363 | ||
6970d37c | 364 | return 0; |
ca50c197 SA |
365 | } |
366 | EXPORT_SYMBOL_GPL(v4l2_fwnode_endpoint_alloc_parse); | |
367 | ||
ca50c197 SA |
368 | int v4l2_fwnode_parse_link(struct fwnode_handle *__fwnode, |
369 | struct v4l2_fwnode_link *link) | |
370 | { | |
371 | const char *port_prop = is_of_node(__fwnode) ? "reg" : "port"; | |
372 | struct fwnode_handle *fwnode; | |
373 | ||
374 | memset(link, 0, sizeof(*link)); | |
375 | ||
376 | fwnode = fwnode_get_parent(__fwnode); | |
377 | fwnode_property_read_u32(fwnode, port_prop, &link->local_port); | |
378 | fwnode = fwnode_get_next_parent(fwnode); | |
379 | if (is_of_node(fwnode) && | |
380 | of_node_cmp(to_of_node(fwnode)->name, "ports") == 0) | |
381 | fwnode = fwnode_get_next_parent(fwnode); | |
382 | link->local_node = fwnode; | |
383 | ||
384 | fwnode = fwnode_graph_get_remote_endpoint(__fwnode); | |
385 | if (!fwnode) { | |
386 | fwnode_handle_put(fwnode); | |
387 | return -ENOLINK; | |
388 | } | |
389 | ||
390 | fwnode = fwnode_get_parent(fwnode); | |
391 | fwnode_property_read_u32(fwnode, port_prop, &link->remote_port); | |
392 | fwnode = fwnode_get_next_parent(fwnode); | |
393 | if (is_of_node(fwnode) && | |
394 | of_node_cmp(to_of_node(fwnode)->name, "ports") == 0) | |
395 | fwnode = fwnode_get_next_parent(fwnode); | |
396 | link->remote_node = fwnode; | |
397 | ||
398 | return 0; | |
399 | } | |
400 | EXPORT_SYMBOL_GPL(v4l2_fwnode_parse_link); | |
401 | ||
ca50c197 SA |
402 | void v4l2_fwnode_put_link(struct v4l2_fwnode_link *link) |
403 | { | |
404 | fwnode_handle_put(link->local_node); | |
405 | fwnode_handle_put(link->remote_node); | |
406 | } | |
407 | EXPORT_SYMBOL_GPL(v4l2_fwnode_put_link); | |
408 | ||
9ca46531 SA |
409 | static int v4l2_async_notifier_fwnode_parse_endpoint( |
410 | struct device *dev, struct v4l2_async_notifier *notifier, | |
411 | struct fwnode_handle *endpoint, unsigned int asd_struct_size, | |
412 | int (*parse_endpoint)(struct device *dev, | |
413 | struct v4l2_fwnode_endpoint *vep, | |
414 | struct v4l2_async_subdev *asd)) | |
415 | { | |
6970d37c | 416 | struct v4l2_fwnode_endpoint vep = { .bus_type = 0 }; |
9ca46531 | 417 | struct v4l2_async_subdev *asd; |
6970d37c | 418 | int ret; |
9ca46531 SA |
419 | |
420 | asd = kzalloc(asd_struct_size, GFP_KERNEL); | |
421 | if (!asd) | |
422 | return -ENOMEM; | |
423 | ||
424 | asd->match_type = V4L2_ASYNC_MATCH_FWNODE; | |
4e48afec | 425 | asd->match.fwnode = |
9ca46531 | 426 | fwnode_graph_get_remote_port_parent(endpoint); |
4e48afec | 427 | if (!asd->match.fwnode) { |
9ca46531 | 428 | dev_warn(dev, "bad remote port parent\n"); |
4382f37b | 429 | ret = -ENOTCONN; |
9ca46531 SA |
430 | goto out_err; |
431 | } | |
432 | ||
6970d37c SA |
433 | ret = v4l2_fwnode_endpoint_alloc_parse(endpoint, &vep); |
434 | if (ret) { | |
9ca46531 SA |
435 | dev_warn(dev, "unable to parse V4L2 fwnode endpoint (%d)\n", |
436 | ret); | |
437 | goto out_err; | |
438 | } | |
439 | ||
6970d37c | 440 | ret = parse_endpoint ? parse_endpoint(dev, &vep, asd) : 0; |
9ca46531 | 441 | if (ret == -ENOTCONN) |
6970d37c SA |
442 | dev_dbg(dev, "ignoring port@%u/endpoint@%u\n", vep.base.port, |
443 | vep.base.id); | |
9ca46531 SA |
444 | else if (ret < 0) |
445 | dev_warn(dev, | |
446 | "driver could not parse port@%u/endpoint@%u (%d)\n", | |
6970d37c SA |
447 | vep.base.port, vep.base.id, ret); |
448 | v4l2_fwnode_endpoint_free(&vep); | |
9ca46531 SA |
449 | if (ret < 0) |
450 | goto out_err; | |
451 | ||
eae2aed1 SL |
452 | ret = v4l2_async_notifier_add_subdev(notifier, asd); |
453 | if (ret < 0) { | |
454 | /* not an error if asd already exists */ | |
455 | if (ret == -EEXIST) | |
456 | ret = 0; | |
457 | goto out_err; | |
458 | } | |
9ca46531 SA |
459 | |
460 | return 0; | |
461 | ||
462 | out_err: | |
4e48afec | 463 | fwnode_handle_put(asd->match.fwnode); |
9ca46531 SA |
464 | kfree(asd); |
465 | ||
466 | return ret == -ENOTCONN ? 0 : ret; | |
467 | } | |
468 | ||
469 | static int __v4l2_async_notifier_parse_fwnode_endpoints( | |
470 | struct device *dev, struct v4l2_async_notifier *notifier, | |
471 | size_t asd_struct_size, unsigned int port, bool has_port, | |
472 | int (*parse_endpoint)(struct device *dev, | |
473 | struct v4l2_fwnode_endpoint *vep, | |
474 | struct v4l2_async_subdev *asd)) | |
475 | { | |
476 | struct fwnode_handle *fwnode; | |
eae2aed1 | 477 | int ret = 0; |
9ca46531 SA |
478 | |
479 | if (WARN_ON(asd_struct_size < sizeof(struct v4l2_async_subdev))) | |
480 | return -EINVAL; | |
481 | ||
106ee387 | 482 | fwnode_graph_for_each_endpoint(dev_fwnode(dev), fwnode) { |
9ca46531 SA |
483 | struct fwnode_handle *dev_fwnode; |
484 | bool is_available; | |
485 | ||
486 | dev_fwnode = fwnode_graph_get_port_parent(fwnode); | |
487 | is_available = fwnode_device_is_available(dev_fwnode); | |
488 | fwnode_handle_put(dev_fwnode); | |
1acce5f7 | 489 | if (!is_available) |
9ca46531 SA |
490 | continue; |
491 | ||
9ca46531 SA |
492 | if (has_port) { |
493 | struct fwnode_endpoint ep; | |
494 | ||
495 | ret = fwnode_graph_parse_endpoint(fwnode, &ep); | |
496 | if (ret) | |
497 | break; | |
498 | ||
499 | if (ep.port != port) | |
500 | continue; | |
501 | } | |
502 | ||
503 | ret = v4l2_async_notifier_fwnode_parse_endpoint( | |
504 | dev, notifier, fwnode, asd_struct_size, parse_endpoint); | |
505 | if (ret < 0) | |
506 | break; | |
507 | } | |
508 | ||
509 | fwnode_handle_put(fwnode); | |
510 | ||
511 | return ret; | |
512 | } | |
513 | ||
514 | int v4l2_async_notifier_parse_fwnode_endpoints( | |
515 | struct device *dev, struct v4l2_async_notifier *notifier, | |
516 | size_t asd_struct_size, | |
517 | int (*parse_endpoint)(struct device *dev, | |
518 | struct v4l2_fwnode_endpoint *vep, | |
519 | struct v4l2_async_subdev *asd)) | |
520 | { | |
521 | return __v4l2_async_notifier_parse_fwnode_endpoints( | |
522 | dev, notifier, asd_struct_size, 0, false, parse_endpoint); | |
523 | } | |
524 | EXPORT_SYMBOL_GPL(v4l2_async_notifier_parse_fwnode_endpoints); | |
525 | ||
526 | int v4l2_async_notifier_parse_fwnode_endpoints_by_port( | |
527 | struct device *dev, struct v4l2_async_notifier *notifier, | |
528 | size_t asd_struct_size, unsigned int port, | |
529 | int (*parse_endpoint)(struct device *dev, | |
530 | struct v4l2_fwnode_endpoint *vep, | |
531 | struct v4l2_async_subdev *asd)) | |
532 | { | |
533 | return __v4l2_async_notifier_parse_fwnode_endpoints( | |
534 | dev, notifier, asd_struct_size, port, true, parse_endpoint); | |
535 | } | |
536 | EXPORT_SYMBOL_GPL(v4l2_async_notifier_parse_fwnode_endpoints_by_port); | |
537 | ||
d8428539 SA |
538 | /* |
539 | * v4l2_fwnode_reference_parse - parse references for async sub-devices | |
540 | * @dev: the device node the properties of which are parsed for references | |
541 | * @notifier: the async notifier where the async subdevs will be added | |
542 | * @prop: the name of the property | |
543 | * | |
544 | * Return: 0 on success | |
545 | * -ENOENT if no entries were found | |
546 | * -ENOMEM if memory allocation failed | |
547 | * -EINVAL if property parsing failed | |
548 | */ | |
549 | static int v4l2_fwnode_reference_parse( | |
550 | struct device *dev, struct v4l2_async_notifier *notifier, | |
551 | const char *prop) | |
552 | { | |
553 | struct fwnode_reference_args args; | |
554 | unsigned int index; | |
555 | int ret; | |
556 | ||
557 | for (index = 0; | |
558 | !(ret = fwnode_property_get_reference_args( | |
559 | dev_fwnode(dev), prop, NULL, 0, index, &args)); | |
560 | index++) | |
561 | fwnode_handle_put(args.fwnode); | |
562 | ||
563 | if (!index) | |
564 | return -ENOENT; | |
565 | ||
566 | /* | |
567 | * Note that right now both -ENODATA and -ENOENT may signal | |
568 | * out-of-bounds access. Return the error in cases other than that. | |
569 | */ | |
570 | if (ret != -ENOENT && ret != -ENODATA) | |
571 | return ret; | |
572 | ||
d8428539 SA |
573 | for (index = 0; !fwnode_property_get_reference_args( |
574 | dev_fwnode(dev), prop, NULL, 0, index, &args); | |
575 | index++) { | |
576 | struct v4l2_async_subdev *asd; | |
577 | ||
eae2aed1 SL |
578 | asd = v4l2_async_notifier_add_fwnode_subdev( |
579 | notifier, args.fwnode, sizeof(*asd)); | |
580 | if (IS_ERR(asd)) { | |
581 | ret = PTR_ERR(asd); | |
582 | /* not an error if asd already exists */ | |
583 | if (ret == -EEXIST) { | |
584 | fwnode_handle_put(args.fwnode); | |
585 | continue; | |
586 | } | |
d8428539 | 587 | |
d8428539 SA |
588 | goto error; |
589 | } | |
d8428539 SA |
590 | } |
591 | ||
592 | return 0; | |
593 | ||
594 | error: | |
595 | fwnode_handle_put(args.fwnode); | |
596 | return ret; | |
597 | } | |
598 | ||
a1699a4e SA |
599 | /* |
600 | * v4l2_fwnode_reference_get_int_prop - parse a reference with integer | |
601 | * arguments | |
602 | * @fwnode: fwnode to read @prop from | |
603 | * @notifier: notifier for @dev | |
604 | * @prop: the name of the property | |
605 | * @index: the index of the reference to get | |
606 | * @props: the array of integer property names | |
607 | * @nprops: the number of integer property names in @nprops | |
608 | * | |
609 | * First find an fwnode referred to by the reference at @index in @prop. | |
610 | * | |
611 | * Then under that fwnode, @nprops times, for each property in @props, | |
612 | * iteratively follow child nodes starting from fwnode such that they have the | |
613 | * property in @props array at the index of the child node distance from the | |
614 | * root node and the value of that property matching with the integer argument | |
615 | * of the reference, at the same index. | |
616 | * | |
617 | * The child fwnode reched at the end of the iteration is then returned to the | |
618 | * caller. | |
619 | * | |
620 | * The core reason for this is that you cannot refer to just any node in ACPI. | |
621 | * So to refer to an endpoint (easy in DT) you need to refer to a device, then | |
622 | * provide a list of (property name, property value) tuples where each tuple | |
623 | * uniquely identifies a child node. The first tuple identifies a child directly | |
624 | * underneath the device fwnode, the next tuple identifies a child node | |
625 | * underneath the fwnode identified by the previous tuple, etc. until you | |
626 | * reached the fwnode you need. | |
627 | * | |
628 | * An example with a graph, as defined in Documentation/acpi/dsd/graph.txt: | |
629 | * | |
630 | * Scope (\_SB.PCI0.I2C2) | |
631 | * { | |
632 | * Device (CAM0) | |
633 | * { | |
634 | * Name (_DSD, Package () { | |
635 | * ToUUID("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"), | |
636 | * Package () { | |
637 | * Package () { | |
638 | * "compatible", | |
639 | * Package () { "nokia,smia" } | |
640 | * }, | |
641 | * }, | |
642 | * ToUUID("dbb8e3e6-5886-4ba6-8795-1319f52a966b"), | |
643 | * Package () { | |
644 | * Package () { "port0", "PRT0" }, | |
645 | * } | |
646 | * }) | |
647 | * Name (PRT0, Package() { | |
648 | * ToUUID("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"), | |
649 | * Package () { | |
650 | * Package () { "port", 0 }, | |
651 | * }, | |
652 | * ToUUID("dbb8e3e6-5886-4ba6-8795-1319f52a966b"), | |
653 | * Package () { | |
654 | * Package () { "endpoint0", "EP00" }, | |
655 | * } | |
656 | * }) | |
657 | * Name (EP00, Package() { | |
658 | * ToUUID("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"), | |
659 | * Package () { | |
660 | * Package () { "endpoint", 0 }, | |
661 | * Package () { | |
662 | * "remote-endpoint", | |
663 | * Package() { | |
664 | * \_SB.PCI0.ISP, 4, 0 | |
665 | * } | |
666 | * }, | |
667 | * } | |
668 | * }) | |
669 | * } | |
670 | * } | |
671 | * | |
672 | * Scope (\_SB.PCI0) | |
673 | * { | |
674 | * Device (ISP) | |
675 | * { | |
676 | * Name (_DSD, Package () { | |
677 | * ToUUID("dbb8e3e6-5886-4ba6-8795-1319f52a966b"), | |
678 | * Package () { | |
679 | * Package () { "port4", "PRT4" }, | |
680 | * } | |
681 | * }) | |
682 | * | |
683 | * Name (PRT4, Package() { | |
684 | * ToUUID("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"), | |
685 | * Package () { | |
686 | * Package () { "port", 4 }, | |
687 | * }, | |
688 | * ToUUID("dbb8e3e6-5886-4ba6-8795-1319f52a966b"), | |
689 | * Package () { | |
690 | * Package () { "endpoint0", "EP40" }, | |
691 | * } | |
692 | * }) | |
693 | * | |
694 | * Name (EP40, Package() { | |
695 | * ToUUID("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"), | |
696 | * Package () { | |
697 | * Package () { "endpoint", 0 }, | |
698 | * Package () { | |
699 | * "remote-endpoint", | |
700 | * Package () { | |
701 | * \_SB.PCI0.I2C2.CAM0, | |
702 | * 0, 0 | |
703 | * } | |
704 | * }, | |
705 | * } | |
706 | * }) | |
707 | * } | |
708 | * } | |
709 | * | |
710 | * From the EP40 node under ISP device, you could parse the graph remote | |
711 | * endpoint using v4l2_fwnode_reference_get_int_prop with these arguments: | |
712 | * | |
713 | * @fwnode: fwnode referring to EP40 under ISP. | |
714 | * @prop: "remote-endpoint" | |
715 | * @index: 0 | |
716 | * @props: "port", "endpoint" | |
717 | * @nprops: 2 | |
718 | * | |
719 | * And you'd get back fwnode referring to EP00 under CAM0. | |
720 | * | |
721 | * The same works the other way around: if you use EP00 under CAM0 as the | |
722 | * fwnode, you'll get fwnode referring to EP40 under ISP. | |
723 | * | |
724 | * The same example in DT syntax would look like this: | |
725 | * | |
726 | * cam: cam0 { | |
727 | * compatible = "nokia,smia"; | |
728 | * | |
729 | * port { | |
730 | * port = <0>; | |
731 | * endpoint { | |
732 | * endpoint = <0>; | |
733 | * remote-endpoint = <&isp 4 0>; | |
734 | * }; | |
735 | * }; | |
736 | * }; | |
737 | * | |
738 | * isp: isp { | |
739 | * ports { | |
740 | * port@4 { | |
741 | * port = <4>; | |
742 | * endpoint { | |
743 | * endpoint = <0>; | |
744 | * remote-endpoint = <&cam 0 0>; | |
745 | * }; | |
746 | * }; | |
747 | * }; | |
748 | * }; | |
749 | * | |
750 | * Return: 0 on success | |
751 | * -ENOENT if no entries (or the property itself) were found | |
752 | * -EINVAL if property parsing otherwise failed | |
753 | * -ENOMEM if memory allocation failed | |
754 | */ | |
755 | static struct fwnode_handle *v4l2_fwnode_reference_get_int_prop( | |
756 | struct fwnode_handle *fwnode, const char *prop, unsigned int index, | |
757 | const char * const *props, unsigned int nprops) | |
758 | { | |
759 | struct fwnode_reference_args fwnode_args; | |
977d5ad3 | 760 | u64 *args = fwnode_args.args; |
a1699a4e SA |
761 | struct fwnode_handle *child; |
762 | int ret; | |
763 | ||
764 | /* | |
765 | * Obtain remote fwnode as well as the integer arguments. | |
766 | * | |
767 | * Note that right now both -ENODATA and -ENOENT may signal | |
768 | * out-of-bounds access. Return -ENOENT in that case. | |
769 | */ | |
770 | ret = fwnode_property_get_reference_args(fwnode, prop, NULL, nprops, | |
771 | index, &fwnode_args); | |
772 | if (ret) | |
773 | return ERR_PTR(ret == -ENODATA ? -ENOENT : ret); | |
774 | ||
775 | /* | |
776 | * Find a node in the tree under the referred fwnode corresponding to | |
777 | * the integer arguments. | |
778 | */ | |
779 | fwnode = fwnode_args.fwnode; | |
780 | while (nprops--) { | |
781 | u32 val; | |
782 | ||
783 | /* Loop over all child nodes under fwnode. */ | |
784 | fwnode_for_each_child_node(fwnode, child) { | |
785 | if (fwnode_property_read_u32(child, *props, &val)) | |
786 | continue; | |
787 | ||
788 | /* Found property, see if its value matches. */ | |
789 | if (val == *args) | |
790 | break; | |
791 | } | |
792 | ||
793 | fwnode_handle_put(fwnode); | |
794 | ||
795 | /* No property found; return an error here. */ | |
796 | if (!child) { | |
797 | fwnode = ERR_PTR(-ENOENT); | |
798 | break; | |
799 | } | |
800 | ||
801 | props++; | |
802 | args++; | |
803 | fwnode = child; | |
804 | } | |
805 | ||
806 | return fwnode; | |
807 | } | |
808 | ||
809 | /* | |
810 | * v4l2_fwnode_reference_parse_int_props - parse references for async | |
811 | * sub-devices | |
812 | * @dev: struct device pointer | |
813 | * @notifier: notifier for @dev | |
814 | * @prop: the name of the property | |
815 | * @props: the array of integer property names | |
816 | * @nprops: the number of integer properties | |
817 | * | |
818 | * Use v4l2_fwnode_reference_get_int_prop to find fwnodes through reference in | |
819 | * property @prop with integer arguments with child nodes matching in properties | |
820 | * @props. Then, set up V4L2 async sub-devices for those fwnodes in the notifier | |
821 | * accordingly. | |
822 | * | |
823 | * While it is technically possible to use this function on DT, it is only | |
824 | * meaningful on ACPI. On Device tree you can refer to any node in the tree but | |
825 | * on ACPI the references are limited to devices. | |
826 | * | |
827 | * Return: 0 on success | |
828 | * -ENOENT if no entries (or the property itself) were found | |
829 | * -EINVAL if property parsing otherwisefailed | |
830 | * -ENOMEM if memory allocation failed | |
831 | */ | |
832 | static int v4l2_fwnode_reference_parse_int_props( | |
833 | struct device *dev, struct v4l2_async_notifier *notifier, | |
834 | const char *prop, const char * const *props, unsigned int nprops) | |
835 | { | |
836 | struct fwnode_handle *fwnode; | |
837 | unsigned int index; | |
838 | int ret; | |
839 | ||
9879c9d3 MCC |
840 | index = 0; |
841 | do { | |
842 | fwnode = v4l2_fwnode_reference_get_int_prop(dev_fwnode(dev), | |
843 | prop, index, | |
844 | props, nprops); | |
845 | if (IS_ERR(fwnode)) { | |
846 | /* | |
847 | * Note that right now both -ENODATA and -ENOENT may | |
848 | * signal out-of-bounds access. Return the error in | |
849 | * cases other than that. | |
850 | */ | |
851 | if (PTR_ERR(fwnode) != -ENOENT && | |
852 | PTR_ERR(fwnode) != -ENODATA) | |
853 | return PTR_ERR(fwnode); | |
854 | break; | |
855 | } | |
a1699a4e | 856 | fwnode_handle_put(fwnode); |
9879c9d3 MCC |
857 | index++; |
858 | } while (1); | |
a1699a4e | 859 | |
a1699a4e SA |
860 | for (index = 0; !IS_ERR((fwnode = v4l2_fwnode_reference_get_int_prop( |
861 | dev_fwnode(dev), prop, index, props, | |
862 | nprops))); index++) { | |
863 | struct v4l2_async_subdev *asd; | |
864 | ||
eae2aed1 SL |
865 | asd = v4l2_async_notifier_add_fwnode_subdev(notifier, fwnode, |
866 | sizeof(*asd)); | |
867 | if (IS_ERR(asd)) { | |
868 | ret = PTR_ERR(asd); | |
869 | /* not an error if asd already exists */ | |
870 | if (ret == -EEXIST) { | |
871 | fwnode_handle_put(fwnode); | |
872 | continue; | |
873 | } | |
a1699a4e | 874 | |
a1699a4e SA |
875 | goto error; |
876 | } | |
a1699a4e SA |
877 | } |
878 | ||
879 | return PTR_ERR(fwnode) == -ENOENT ? 0 : PTR_ERR(fwnode); | |
880 | ||
881 | error: | |
882 | fwnode_handle_put(fwnode); | |
883 | return ret; | |
884 | } | |
885 | ||
7a9ec808 SA |
886 | int v4l2_async_notifier_parse_fwnode_sensor_common( |
887 | struct device *dev, struct v4l2_async_notifier *notifier) | |
888 | { | |
889 | static const char * const led_props[] = { "led" }; | |
890 | static const struct { | |
891 | const char *name; | |
892 | const char * const *props; | |
893 | unsigned int nprops; | |
894 | } props[] = { | |
895 | { "flash-leds", led_props, ARRAY_SIZE(led_props) }, | |
896 | { "lens-focus", NULL, 0 }, | |
897 | }; | |
898 | unsigned int i; | |
899 | ||
900 | for (i = 0; i < ARRAY_SIZE(props); i++) { | |
901 | int ret; | |
902 | ||
903 | if (props[i].props && is_acpi_node(dev_fwnode(dev))) | |
904 | ret = v4l2_fwnode_reference_parse_int_props( | |
905 | dev, notifier, props[i].name, | |
906 | props[i].props, props[i].nprops); | |
907 | else | |
908 | ret = v4l2_fwnode_reference_parse( | |
909 | dev, notifier, props[i].name); | |
910 | if (ret && ret != -ENOENT) { | |
911 | dev_warn(dev, "parsing property \"%s\" failed (%d)\n", | |
912 | props[i].name, ret); | |
913 | return ret; | |
914 | } | |
915 | } | |
916 | ||
917 | return 0; | |
918 | } | |
919 | EXPORT_SYMBOL_GPL(v4l2_async_notifier_parse_fwnode_sensor_common); | |
920 | ||
aef69d54 SA |
921 | int v4l2_async_register_subdev_sensor_common(struct v4l2_subdev *sd) |
922 | { | |
923 | struct v4l2_async_notifier *notifier; | |
924 | int ret; | |
925 | ||
926 | if (WARN_ON(!sd->dev)) | |
927 | return -ENODEV; | |
928 | ||
929 | notifier = kzalloc(sizeof(*notifier), GFP_KERNEL); | |
930 | if (!notifier) | |
931 | return -ENOMEM; | |
932 | ||
eae2aed1 SL |
933 | v4l2_async_notifier_init(notifier); |
934 | ||
aef69d54 SA |
935 | ret = v4l2_async_notifier_parse_fwnode_sensor_common(sd->dev, |
936 | notifier); | |
937 | if (ret < 0) | |
938 | goto out_cleanup; | |
939 | ||
940 | ret = v4l2_async_subdev_notifier_register(sd, notifier); | |
941 | if (ret < 0) | |
942 | goto out_cleanup; | |
943 | ||
944 | ret = v4l2_async_register_subdev(sd); | |
945 | if (ret < 0) | |
946 | goto out_unregister; | |
947 | ||
948 | sd->subdev_notifier = notifier; | |
949 | ||
950 | return 0; | |
951 | ||
952 | out_unregister: | |
953 | v4l2_async_notifier_unregister(notifier); | |
954 | ||
955 | out_cleanup: | |
956 | v4l2_async_notifier_cleanup(notifier); | |
957 | kfree(notifier); | |
958 | ||
959 | return ret; | |
960 | } | |
961 | EXPORT_SYMBOL_GPL(v4l2_async_register_subdev_sensor_common); | |
962 | ||
1634f0ed SL |
963 | int v4l2_async_register_fwnode_subdev( |
964 | struct v4l2_subdev *sd, size_t asd_struct_size, | |
965 | unsigned int *ports, unsigned int num_ports, | |
966 | int (*parse_endpoint)(struct device *dev, | |
967 | struct v4l2_fwnode_endpoint *vep, | |
968 | struct v4l2_async_subdev *asd)) | |
969 | { | |
970 | struct v4l2_async_notifier *notifier; | |
971 | struct device *dev = sd->dev; | |
972 | struct fwnode_handle *fwnode; | |
973 | int ret; | |
974 | ||
975 | if (WARN_ON(!dev)) | |
976 | return -ENODEV; | |
977 | ||
978 | fwnode = dev_fwnode(dev); | |
979 | if (!fwnode_device_is_available(fwnode)) | |
980 | return -ENODEV; | |
981 | ||
982 | notifier = kzalloc(sizeof(*notifier), GFP_KERNEL); | |
983 | if (!notifier) | |
984 | return -ENOMEM; | |
985 | ||
986 | v4l2_async_notifier_init(notifier); | |
987 | ||
988 | if (!ports) { | |
989 | ret = v4l2_async_notifier_parse_fwnode_endpoints( | |
990 | dev, notifier, asd_struct_size, parse_endpoint); | |
991 | if (ret < 0) | |
992 | goto out_cleanup; | |
993 | } else { | |
994 | unsigned int i; | |
995 | ||
996 | for (i = 0; i < num_ports; i++) { | |
997 | ret = v4l2_async_notifier_parse_fwnode_endpoints_by_port( | |
998 | dev, notifier, asd_struct_size, | |
999 | ports[i], parse_endpoint); | |
1000 | if (ret < 0) | |
1001 | goto out_cleanup; | |
1002 | } | |
1003 | } | |
1004 | ||
1005 | ret = v4l2_async_subdev_notifier_register(sd, notifier); | |
1006 | if (ret < 0) | |
1007 | goto out_cleanup; | |
1008 | ||
1009 | ret = v4l2_async_register_subdev(sd); | |
1010 | if (ret < 0) | |
1011 | goto out_unregister; | |
1012 | ||
1013 | sd->subdev_notifier = notifier; | |
1014 | ||
1015 | return 0; | |
1016 | ||
1017 | out_unregister: | |
1018 | v4l2_async_notifier_unregister(notifier); | |
1019 | out_cleanup: | |
1020 | v4l2_async_notifier_cleanup(notifier); | |
1021 | kfree(notifier); | |
1022 | ||
1023 | return ret; | |
1024 | } | |
1025 | EXPORT_SYMBOL_GPL(v4l2_async_register_fwnode_subdev); | |
1026 | ||
ca50c197 SA |
1027 | MODULE_LICENSE("GPL"); |
1028 | MODULE_AUTHOR("Sakari Ailus <sakari.ailus@linux.intel.com>"); | |
1029 | MODULE_AUTHOR("Sylwester Nawrocki <s.nawrocki@samsung.com>"); | |
1030 | MODULE_AUTHOR("Guennadi Liakhovetski <g.liakhovetski@gmx.de>"); |