1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* |
3 | * Copyright 2020 Linaro Limited |
4 | * |
5 | * Author: Daniel Lezcano <daniel.lezcano@linaro.org> |
6 | * |
7 | * The powercap based Dynamic Thermal Power Management framework |
8 | * provides to the userspace a consistent API to set the power limit |
9 | * on some devices. |
10 | * |
11 | * DTPM defines the functions to create a tree of constraints. Each |
12 | * parent node is a virtual description of the aggregation of the |
13 | * children. It propagates the constraints set at its level to its |
14 | * children and collect the children power information. The leaves of |
15 | * the tree are the real devices which have the ability to get their |
16 | * current power consumption and set their power limit. |
17 | */ |
18 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
19 | |
20 | #include <linux/dtpm.h> |
21 | #include <linux/init.h> |
22 | #include <linux/kernel.h> |
23 | #include <linux/powercap.h> |
24 | #include <linux/slab.h> |
25 | #include <linux/mutex.h> |
26 | #include <linux/of.h> |
27 | |
28 | #include "dtpm_subsys.h" |
29 | |
30 | #define DTPM_POWER_LIMIT_FLAG 0 |
31 | |
32 | static const char *constraint_name[] = { |
33 | "Instantaneous" , |
34 | }; |
35 | |
36 | static DEFINE_MUTEX(dtpm_lock); |
37 | static struct powercap_control_type *pct; |
38 | static struct dtpm *root; |
39 | |
40 | static int get_time_window_us(struct powercap_zone *pcz, int cid, u64 *window) |
41 | { |
42 | return -ENOSYS; |
43 | } |
44 | |
45 | static int set_time_window_us(struct powercap_zone *pcz, int cid, u64 window) |
46 | { |
47 | return -ENOSYS; |
48 | } |
49 | |
50 | static int get_max_power_range_uw(struct powercap_zone *pcz, u64 *max_power_uw) |
51 | { |
52 | struct dtpm *dtpm = to_dtpm(zone: pcz); |
53 | |
54 | *max_power_uw = dtpm->power_max - dtpm->power_min; |
55 | |
56 | return 0; |
57 | } |
58 | |
59 | static int __get_power_uw(struct dtpm *dtpm, u64 *power_uw) |
60 | { |
61 | struct dtpm *child; |
62 | u64 power; |
63 | int ret = 0; |
64 | |
65 | if (dtpm->ops) { |
66 | *power_uw = dtpm->ops->get_power_uw(dtpm); |
67 | return 0; |
68 | } |
69 | |
70 | *power_uw = 0; |
71 | |
72 | list_for_each_entry(child, &dtpm->children, sibling) { |
73 | ret = __get_power_uw(dtpm: child, power_uw: &power); |
74 | if (ret) |
75 | break; |
76 | *power_uw += power; |
77 | } |
78 | |
79 | return ret; |
80 | } |
81 | |
82 | static int get_power_uw(struct powercap_zone *pcz, u64 *power_uw) |
83 | { |
84 | return __get_power_uw(dtpm: to_dtpm(zone: pcz), power_uw); |
85 | } |
86 | |
87 | static void __dtpm_rebalance_weight(struct dtpm *dtpm) |
88 | { |
89 | struct dtpm *child; |
90 | |
91 | list_for_each_entry(child, &dtpm->children, sibling) { |
92 | |
93 | pr_debug("Setting weight '%d' for '%s'\n" , |
94 | child->weight, child->zone.name); |
95 | |
96 | child->weight = DIV64_U64_ROUND_CLOSEST( |
97 | child->power_max * 1024, dtpm->power_max); |
98 | |
99 | __dtpm_rebalance_weight(dtpm: child); |
100 | } |
101 | } |
102 | |
103 | static void __dtpm_sub_power(struct dtpm *dtpm) |
104 | { |
105 | struct dtpm *parent = dtpm->parent; |
106 | |
107 | while (parent) { |
108 | parent->power_min -= dtpm->power_min; |
109 | parent->power_max -= dtpm->power_max; |
110 | parent->power_limit -= dtpm->power_limit; |
111 | parent = parent->parent; |
112 | } |
113 | } |
114 | |
115 | static void __dtpm_add_power(struct dtpm *dtpm) |
116 | { |
117 | struct dtpm *parent = dtpm->parent; |
118 | |
119 | while (parent) { |
120 | parent->power_min += dtpm->power_min; |
121 | parent->power_max += dtpm->power_max; |
122 | parent->power_limit += dtpm->power_limit; |
123 | parent = parent->parent; |
124 | } |
125 | } |
126 | |
127 | /** |
128 | * dtpm_update_power - Update the power on the dtpm |
129 | * @dtpm: a pointer to a dtpm structure to update |
130 | * |
131 | * Function to update the power values of the dtpm node specified in |
132 | * parameter. These new values will be propagated to the tree. |
133 | * |
134 | * Return: zero on success, -EINVAL if the values are inconsistent |
135 | */ |
136 | int dtpm_update_power(struct dtpm *dtpm) |
137 | { |
138 | int ret; |
139 | |
140 | __dtpm_sub_power(dtpm); |
141 | |
142 | ret = dtpm->ops->update_power_uw(dtpm); |
143 | if (ret) |
144 | pr_err("Failed to update power for '%s': %d\n" , |
145 | dtpm->zone.name, ret); |
146 | |
147 | if (!test_bit(DTPM_POWER_LIMIT_FLAG, &dtpm->flags)) |
148 | dtpm->power_limit = dtpm->power_max; |
149 | |
150 | __dtpm_add_power(dtpm); |
151 | |
152 | if (root) |
153 | __dtpm_rebalance_weight(dtpm: root); |
154 | |
155 | return ret; |
156 | } |
157 | |
158 | /** |
159 | * dtpm_release_zone - Cleanup when the node is released |
160 | * @pcz: a pointer to a powercap_zone structure |
161 | * |
162 | * Do some housecleaning and update the weight on the tree. The |
163 | * release will be denied if the node has children. This function must |
164 | * be called by the specific release callback of the different |
165 | * backends. |
166 | * |
167 | * Return: 0 on success, -EBUSY if there are children |
168 | */ |
169 | int dtpm_release_zone(struct powercap_zone *pcz) |
170 | { |
171 | struct dtpm *dtpm = to_dtpm(zone: pcz); |
172 | struct dtpm *parent = dtpm->parent; |
173 | |
174 | if (!list_empty(head: &dtpm->children)) |
175 | return -EBUSY; |
176 | |
177 | if (parent) |
178 | list_del(entry: &dtpm->sibling); |
179 | |
180 | __dtpm_sub_power(dtpm); |
181 | |
182 | if (dtpm->ops) |
183 | dtpm->ops->release(dtpm); |
184 | else |
185 | kfree(objp: dtpm); |
186 | |
187 | return 0; |
188 | } |
189 | |
190 | static int get_power_limit_uw(struct powercap_zone *pcz, |
191 | int cid, u64 *power_limit) |
192 | { |
193 | *power_limit = to_dtpm(zone: pcz)->power_limit; |
194 | |
195 | return 0; |
196 | } |
197 | |
198 | /* |
199 | * Set the power limit on the nodes, the power limit is distributed |
200 | * given the weight of the children. |
201 | * |
202 | * The dtpm node lock must be held when calling this function. |
203 | */ |
204 | static int __set_power_limit_uw(struct dtpm *dtpm, int cid, u64 power_limit) |
205 | { |
206 | struct dtpm *child; |
207 | int ret = 0; |
208 | u64 power; |
209 | |
210 | /* |
211 | * A max power limitation means we remove the power limit, |
212 | * otherwise we set a constraint and flag the dtpm node. |
213 | */ |
214 | if (power_limit == dtpm->power_max) { |
215 | clear_bit(DTPM_POWER_LIMIT_FLAG, addr: &dtpm->flags); |
216 | } else { |
217 | set_bit(DTPM_POWER_LIMIT_FLAG, addr: &dtpm->flags); |
218 | } |
219 | |
220 | pr_debug("Setting power limit for '%s': %llu uW\n" , |
221 | dtpm->zone.name, power_limit); |
222 | |
223 | /* |
224 | * Only leaves of the dtpm tree has ops to get/set the power |
225 | */ |
226 | if (dtpm->ops) { |
227 | dtpm->power_limit = dtpm->ops->set_power_uw(dtpm, power_limit); |
228 | } else { |
229 | dtpm->power_limit = 0; |
230 | |
231 | list_for_each_entry(child, &dtpm->children, sibling) { |
232 | |
233 | /* |
234 | * Integer division rounding will inevitably |
235 | * lead to a different min or max value when |
236 | * set several times. In order to restore the |
237 | * initial value, we force the child's min or |
238 | * max power every time if the constraint is |
239 | * at the boundaries. |
240 | */ |
241 | if (power_limit == dtpm->power_max) { |
242 | power = child->power_max; |
243 | } else if (power_limit == dtpm->power_min) { |
244 | power = child->power_min; |
245 | } else { |
246 | power = DIV_ROUND_CLOSEST_ULL( |
247 | power_limit * child->weight, 1024); |
248 | } |
249 | |
250 | pr_debug("Setting power limit for '%s': %llu uW\n" , |
251 | child->zone.name, power); |
252 | |
253 | ret = __set_power_limit_uw(dtpm: child, cid, power_limit: power); |
254 | if (!ret) |
255 | ret = get_power_limit_uw(pcz: &child->zone, cid, power_limit: &power); |
256 | |
257 | if (ret) |
258 | break; |
259 | |
260 | dtpm->power_limit += power; |
261 | } |
262 | } |
263 | |
264 | return ret; |
265 | } |
266 | |
267 | static int set_power_limit_uw(struct powercap_zone *pcz, |
268 | int cid, u64 power_limit) |
269 | { |
270 | struct dtpm *dtpm = to_dtpm(zone: pcz); |
271 | int ret; |
272 | |
273 | /* |
274 | * Don't allow values outside of the power range previously |
275 | * set when initializing the power numbers. |
276 | */ |
277 | power_limit = clamp_val(power_limit, dtpm->power_min, dtpm->power_max); |
278 | |
279 | ret = __set_power_limit_uw(dtpm, cid, power_limit); |
280 | |
281 | pr_debug("%s: power limit: %llu uW, power max: %llu uW\n" , |
282 | dtpm->zone.name, dtpm->power_limit, dtpm->power_max); |
283 | |
284 | return ret; |
285 | } |
286 | |
287 | static const char *get_constraint_name(struct powercap_zone *pcz, int cid) |
288 | { |
289 | return constraint_name[cid]; |
290 | } |
291 | |
292 | static int get_max_power_uw(struct powercap_zone *pcz, int id, u64 *max_power) |
293 | { |
294 | *max_power = to_dtpm(zone: pcz)->power_max; |
295 | |
296 | return 0; |
297 | } |
298 | |
299 | static struct powercap_zone_constraint_ops constraint_ops = { |
300 | .set_power_limit_uw = set_power_limit_uw, |
301 | .get_power_limit_uw = get_power_limit_uw, |
302 | .set_time_window_us = set_time_window_us, |
303 | .get_time_window_us = get_time_window_us, |
304 | .get_max_power_uw = get_max_power_uw, |
305 | .get_name = get_constraint_name, |
306 | }; |
307 | |
308 | static struct powercap_zone_ops zone_ops = { |
309 | .get_max_power_range_uw = get_max_power_range_uw, |
310 | .get_power_uw = get_power_uw, |
311 | .release = dtpm_release_zone, |
312 | }; |
313 | |
314 | /** |
315 | * dtpm_init - Allocate and initialize a dtpm struct |
316 | * @dtpm: The dtpm struct pointer to be initialized |
317 | * @ops: The dtpm device specific ops, NULL for a virtual node |
318 | */ |
319 | void dtpm_init(struct dtpm *dtpm, struct dtpm_ops *ops) |
320 | { |
321 | if (dtpm) { |
322 | INIT_LIST_HEAD(list: &dtpm->children); |
323 | INIT_LIST_HEAD(list: &dtpm->sibling); |
324 | dtpm->weight = 1024; |
325 | dtpm->ops = ops; |
326 | } |
327 | } |
328 | |
329 | /** |
330 | * dtpm_unregister - Unregister a dtpm node from the hierarchy tree |
331 | * @dtpm: a pointer to a dtpm structure corresponding to the node to be removed |
332 | * |
333 | * Call the underlying powercap unregister function. That will call |
334 | * the release callback of the powercap zone. |
335 | */ |
336 | void dtpm_unregister(struct dtpm *dtpm) |
337 | { |
338 | powercap_unregister_zone(control_type: pct, power_zone: &dtpm->zone); |
339 | |
340 | pr_debug("Unregistered dtpm node '%s'\n" , dtpm->zone.name); |
341 | } |
342 | |
343 | /** |
344 | * dtpm_register - Register a dtpm node in the hierarchy tree |
345 | * @name: a string specifying the name of the node |
346 | * @dtpm: a pointer to a dtpm structure corresponding to the new node |
347 | * @parent: a pointer to a dtpm structure corresponding to the parent node |
348 | * |
349 | * Create a dtpm node in the tree. If no parent is specified, the node |
350 | * is the root node of the hierarchy. If the root node already exists, |
351 | * then the registration will fail. The powercap controller must be |
352 | * initialized before calling this function. |
353 | * |
354 | * The dtpm structure must be initialized with the power numbers |
355 | * before calling this function. |
356 | * |
357 | * Return: zero on success, a negative value in case of error: |
358 | * -EAGAIN: the function is called before the framework is initialized. |
359 | * -EBUSY: the root node is already inserted |
360 | * -EINVAL: * there is no root node yet and @parent is specified |
361 | * * no all ops are defined |
362 | * * parent have ops which are reserved for leaves |
363 | * Other negative values are reported back from the powercap framework |
364 | */ |
365 | int dtpm_register(const char *name, struct dtpm *dtpm, struct dtpm *parent) |
366 | { |
367 | struct powercap_zone *pcz; |
368 | |
369 | if (!pct) |
370 | return -EAGAIN; |
371 | |
372 | if (root && !parent) |
373 | return -EBUSY; |
374 | |
375 | if (!root && parent) |
376 | return -EINVAL; |
377 | |
378 | if (parent && parent->ops) |
379 | return -EINVAL; |
380 | |
381 | if (!dtpm) |
382 | return -EINVAL; |
383 | |
384 | if (dtpm->ops && !(dtpm->ops->set_power_uw && |
385 | dtpm->ops->get_power_uw && |
386 | dtpm->ops->update_power_uw && |
387 | dtpm->ops->release)) |
388 | return -EINVAL; |
389 | |
390 | pcz = powercap_register_zone(power_zone: &dtpm->zone, control_type: pct, name, |
391 | parent: parent ? &parent->zone : NULL, |
392 | ops: &zone_ops, MAX_DTPM_CONSTRAINTS, |
393 | const_ops: &constraint_ops); |
394 | if (IS_ERR(ptr: pcz)) |
395 | return PTR_ERR(ptr: pcz); |
396 | |
397 | if (parent) { |
398 | list_add_tail(new: &dtpm->sibling, head: &parent->children); |
399 | dtpm->parent = parent; |
400 | } else { |
401 | root = dtpm; |
402 | } |
403 | |
404 | if (dtpm->ops && !dtpm->ops->update_power_uw(dtpm)) { |
405 | __dtpm_add_power(dtpm); |
406 | dtpm->power_limit = dtpm->power_max; |
407 | } |
408 | |
409 | pr_debug("Registered dtpm node '%s' / %llu-%llu uW, \n" , |
410 | dtpm->zone.name, dtpm->power_min, dtpm->power_max); |
411 | |
412 | return 0; |
413 | } |
414 | |
415 | static struct dtpm *dtpm_setup_virtual(const struct dtpm_node *hierarchy, |
416 | struct dtpm *parent) |
417 | { |
418 | struct dtpm *dtpm; |
419 | int ret; |
420 | |
421 | dtpm = kzalloc(size: sizeof(*dtpm), GFP_KERNEL); |
422 | if (!dtpm) |
423 | return ERR_PTR(error: -ENOMEM); |
424 | dtpm_init(dtpm, NULL); |
425 | |
426 | ret = dtpm_register(name: hierarchy->name, dtpm, parent); |
427 | if (ret) { |
428 | pr_err("Failed to register dtpm node '%s': %d\n" , |
429 | hierarchy->name, ret); |
430 | kfree(objp: dtpm); |
431 | return ERR_PTR(error: ret); |
432 | } |
433 | |
434 | return dtpm; |
435 | } |
436 | |
437 | static struct dtpm *dtpm_setup_dt(const struct dtpm_node *hierarchy, |
438 | struct dtpm *parent) |
439 | { |
440 | struct device_node *np; |
441 | int i, ret; |
442 | |
443 | np = of_find_node_by_path(path: hierarchy->name); |
444 | if (!np) { |
445 | pr_err("Failed to find '%s'\n" , hierarchy->name); |
446 | return ERR_PTR(error: -ENXIO); |
447 | } |
448 | |
449 | for (i = 0; i < ARRAY_SIZE(dtpm_subsys); i++) { |
450 | |
451 | if (!dtpm_subsys[i]->setup) |
452 | continue; |
453 | |
454 | ret = dtpm_subsys[i]->setup(parent, np); |
455 | if (ret) { |
456 | pr_err("Failed to setup '%s': %d\n" , dtpm_subsys[i]->name, ret); |
457 | of_node_put(node: np); |
458 | return ERR_PTR(error: ret); |
459 | } |
460 | } |
461 | |
462 | of_node_put(node: np); |
463 | |
464 | /* |
465 | * By returning a NULL pointer, we let know the caller there |
466 | * is no child for us as we are a leaf of the tree |
467 | */ |
468 | return NULL; |
469 | } |
470 | |
471 | typedef struct dtpm * (*dtpm_node_callback_t)(const struct dtpm_node *, struct dtpm *); |
472 | |
473 | static dtpm_node_callback_t dtpm_node_callback[] = { |
474 | [DTPM_NODE_VIRTUAL] = dtpm_setup_virtual, |
475 | [DTPM_NODE_DT] = dtpm_setup_dt, |
476 | }; |
477 | |
478 | static int dtpm_for_each_child(const struct dtpm_node *hierarchy, |
479 | const struct dtpm_node *it, struct dtpm *parent) |
480 | { |
481 | struct dtpm *dtpm; |
482 | int i, ret; |
483 | |
484 | for (i = 0; hierarchy[i].name; i++) { |
485 | |
486 | if (hierarchy[i].parent != it) |
487 | continue; |
488 | |
489 | dtpm = dtpm_node_callback[hierarchy[i].type](&hierarchy[i], parent); |
490 | |
491 | /* |
492 | * A NULL pointer means there is no children, hence we |
493 | * continue without going deeper in the recursivity. |
494 | */ |
495 | if (!dtpm) |
496 | continue; |
497 | |
498 | /* |
499 | * There are multiple reasons why the callback could |
500 | * fail. The generic glue is abstracting the backend |
501 | * and therefore it is not possible to report back or |
502 | * take a decision based on the error. In any case, |
503 | * if this call fails, it is not critical in the |
504 | * hierarchy creation, we can assume the underlying |
505 | * service is not found, so we continue without this |
506 | * branch in the tree but with a warning to log the |
507 | * information the node was not created. |
508 | */ |
509 | if (IS_ERR(ptr: dtpm)) { |
510 | pr_warn("Failed to create '%s' in the hierarchy\n" , |
511 | hierarchy[i].name); |
512 | continue; |
513 | } |
514 | |
515 | ret = dtpm_for_each_child(hierarchy, it: &hierarchy[i], parent: dtpm); |
516 | if (ret) |
517 | return ret; |
518 | } |
519 | |
520 | return 0; |
521 | } |
522 | |
523 | /** |
524 | * dtpm_create_hierarchy - Create the dtpm hierarchy |
525 | * @dtpm_match_table: Pointer to the array of device ID structures |
526 | * |
527 | * The function is called by the platform specific code with the |
528 | * description of the different node in the hierarchy. It creates the |
529 | * tree in the sysfs filesystem under the powercap dtpm entry. |
530 | * |
531 | * The expected tree has the format: |
532 | * |
533 | * struct dtpm_node hierarchy[] = { |
534 | * [0] { .name = "topmost", type = DTPM_NODE_VIRTUAL }, |
535 | * [1] { .name = "package", .type = DTPM_NODE_VIRTUAL, .parent = &hierarchy[0] }, |
536 | * [2] { .name = "/cpus/cpu0", .type = DTPM_NODE_DT, .parent = &hierarchy[1] }, |
537 | * [3] { .name = "/cpus/cpu1", .type = DTPM_NODE_DT, .parent = &hierarchy[1] }, |
538 | * [4] { .name = "/cpus/cpu2", .type = DTPM_NODE_DT, .parent = &hierarchy[1] }, |
539 | * [5] { .name = "/cpus/cpu3", .type = DTPM_NODE_DT, .parent = &hierarchy[1] }, |
540 | * [6] { } |
541 | * }; |
542 | * |
543 | * The last element is always an empty one and marks the end of the |
544 | * array. |
545 | * |
546 | * Return: zero on success, a negative value in case of error. Errors |
547 | * are reported back from the underlying functions. |
548 | */ |
549 | int dtpm_create_hierarchy(struct of_device_id *dtpm_match_table) |
550 | { |
551 | const struct of_device_id *match; |
552 | const struct dtpm_node *hierarchy; |
553 | struct device_node *np; |
554 | int i, ret; |
555 | |
556 | mutex_lock(&dtpm_lock); |
557 | |
558 | if (pct) { |
559 | ret = -EBUSY; |
560 | goto out_unlock; |
561 | } |
562 | |
563 | pct = powercap_register_control_type(NULL, name: "dtpm" , NULL); |
564 | if (IS_ERR(ptr: pct)) { |
565 | pr_err("Failed to register control type\n" ); |
566 | ret = PTR_ERR(ptr: pct); |
567 | goto out_pct; |
568 | } |
569 | |
570 | ret = -ENODEV; |
571 | np = of_find_node_by_path(path: "/" ); |
572 | if (!np) |
573 | goto out_err; |
574 | |
575 | match = of_match_node(matches: dtpm_match_table, node: np); |
576 | |
577 | of_node_put(node: np); |
578 | |
579 | if (!match) |
580 | goto out_err; |
581 | |
582 | hierarchy = match->data; |
583 | if (!hierarchy) { |
584 | ret = -EFAULT; |
585 | goto out_err; |
586 | } |
587 | |
588 | ret = dtpm_for_each_child(hierarchy, NULL, NULL); |
589 | if (ret) |
590 | goto out_err; |
591 | |
592 | for (i = 0; i < ARRAY_SIZE(dtpm_subsys); i++) { |
593 | |
594 | if (!dtpm_subsys[i]->init) |
595 | continue; |
596 | |
597 | ret = dtpm_subsys[i]->init(); |
598 | if (ret) |
599 | pr_info("Failed to initialize '%s': %d" , |
600 | dtpm_subsys[i]->name, ret); |
601 | } |
602 | |
603 | mutex_unlock(lock: &dtpm_lock); |
604 | |
605 | return 0; |
606 | |
607 | out_err: |
608 | powercap_unregister_control_type(instance: pct); |
609 | out_pct: |
610 | pct = NULL; |
611 | out_unlock: |
612 | mutex_unlock(lock: &dtpm_lock); |
613 | |
614 | return ret; |
615 | } |
616 | EXPORT_SYMBOL_GPL(dtpm_create_hierarchy); |
617 | |
618 | static void __dtpm_destroy_hierarchy(struct dtpm *dtpm) |
619 | { |
620 | struct dtpm *child, *aux; |
621 | |
622 | list_for_each_entry_safe(child, aux, &dtpm->children, sibling) |
623 | __dtpm_destroy_hierarchy(dtpm: child); |
624 | |
625 | /* |
626 | * At this point, we know all children were removed from the |
627 | * recursive call before |
628 | */ |
629 | dtpm_unregister(dtpm); |
630 | } |
631 | |
632 | void dtpm_destroy_hierarchy(void) |
633 | { |
634 | int i; |
635 | |
636 | mutex_lock(&dtpm_lock); |
637 | |
638 | if (!pct) |
639 | goto out_unlock; |
640 | |
641 | __dtpm_destroy_hierarchy(dtpm: root); |
642 | |
643 | |
644 | for (i = 0; i < ARRAY_SIZE(dtpm_subsys); i++) { |
645 | |
646 | if (!dtpm_subsys[i]->exit) |
647 | continue; |
648 | |
649 | dtpm_subsys[i]->exit(); |
650 | } |
651 | |
652 | powercap_unregister_control_type(instance: pct); |
653 | |
654 | pct = NULL; |
655 | |
656 | root = NULL; |
657 | |
658 | out_unlock: |
659 | mutex_unlock(lock: &dtpm_lock); |
660 | } |
661 | EXPORT_SYMBOL_GPL(dtpm_destroy_hierarchy); |
662 | |