xref: /openbmc/linux/drivers/usb/typec/port-mapper.c (revision ae196ddb)
1 // SPDX-License-Identifier: GPL-2.0
2 /*
3  * USB Type-C Connector Class Port Mapping Utility
4  *
5  * Copyright (C) 2021, Intel Corporation
6  * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
7  */
8 
9 #include <linux/acpi.h>
10 #include <linux/usb.h>
11 #include <linux/usb/typec.h>
12 
13 #include "class.h"
14 
15 struct port_node {
16 	struct list_head list;
17 	struct device *dev;
18 	void *pld;
19 };
20 
21 static int acpi_pld_match(const struct acpi_pld_info *pld1,
22 			  const struct acpi_pld_info *pld2)
23 {
24 	if (!pld1 || !pld2)
25 		return 0;
26 
27 	/*
28 	 * To speed things up, first checking only the group_position. It seems
29 	 * to often have the first unique value in the _PLD.
30 	 */
31 	if (pld1->group_position == pld2->group_position)
32 		return !memcmp(pld1, pld2, sizeof(struct acpi_pld_info));
33 
34 	return 0;
35 }
36 
37 void *get_pld(struct device *dev)
38 {
39 #ifdef CONFIG_ACPI
40 	struct acpi_pld_info *pld;
41 	acpi_status status;
42 
43 	if (!has_acpi_companion(dev))
44 		return NULL;
45 
46 	status = acpi_get_physical_device_location(ACPI_HANDLE(dev), &pld);
47 	if (ACPI_FAILURE(status))
48 		return NULL;
49 
50 	return pld;
51 #else
52 	return NULL;
53 #endif
54 }
55 
56 void free_pld(void *pld)
57 {
58 #ifdef CONFIG_ACPI
59 	ACPI_FREE(pld);
60 #endif
61 }
62 
63 static int __link_port(struct typec_port *con, struct port_node *node)
64 {
65 	int ret;
66 
67 	ret = sysfs_create_link(&node->dev->kobj, &con->dev.kobj, "connector");
68 	if (ret)
69 		return ret;
70 
71 	ret = sysfs_create_link(&con->dev.kobj, &node->dev->kobj,
72 				dev_name(node->dev));
73 	if (ret) {
74 		sysfs_remove_link(&node->dev->kobj, "connector");
75 		return ret;
76 	}
77 
78 	list_add_tail(&node->list, &con->port_list);
79 
80 	return 0;
81 }
82 
83 static int link_port(struct typec_port *con, struct port_node *node)
84 {
85 	int ret;
86 
87 	mutex_lock(&con->port_list_lock);
88 	ret = __link_port(con, node);
89 	mutex_unlock(&con->port_list_lock);
90 
91 	return ret;
92 }
93 
94 static void __unlink_port(struct typec_port *con, struct port_node *node)
95 {
96 	sysfs_remove_link(&con->dev.kobj, dev_name(node->dev));
97 	sysfs_remove_link(&node->dev->kobj, "connector");
98 	list_del(&node->list);
99 }
100 
101 static void unlink_port(struct typec_port *con, struct port_node *node)
102 {
103 	mutex_lock(&con->port_list_lock);
104 	__unlink_port(con, node);
105 	mutex_unlock(&con->port_list_lock);
106 }
107 
108 static struct port_node *create_port_node(struct device *port)
109 {
110 	struct port_node *node;
111 
112 	node = kzalloc(sizeof(*node), GFP_KERNEL);
113 	if (!node)
114 		return ERR_PTR(-ENOMEM);
115 
116 	node->dev = get_device(port);
117 	node->pld = get_pld(port);
118 
119 	return node;
120 }
121 
122 static void remove_port_node(struct port_node *node)
123 {
124 	put_device(node->dev);
125 	free_pld(node->pld);
126 	kfree(node);
127 }
128 
129 static int connector_match(struct device *dev, const void *data)
130 {
131 	const struct port_node *node = data;
132 
133 	if (!is_typec_port(dev))
134 		return 0;
135 
136 	return acpi_pld_match(to_typec_port(dev)->pld, node->pld);
137 }
138 
139 static struct device *find_connector(struct port_node *node)
140 {
141 	if (!node->pld)
142 		return NULL;
143 
144 	return class_find_device(&typec_class, NULL, node, connector_match);
145 }
146 
147 /**
148  * typec_link_port - Link a port to its connector
149  * @port: The port device
150  *
151  * Find the connector of @port and create symlink named "connector" for it.
152  * Returns 0 on success, or errno in case of a failure.
153  *
154  * NOTE. The function increments the reference count of @port on success.
155  */
156 int typec_link_port(struct device *port)
157 {
158 	struct device *connector;
159 	struct port_node *node;
160 	int ret = 0;
161 
162 	node = create_port_node(port);
163 	if (IS_ERR(node))
164 		return PTR_ERR(node);
165 
166 	connector = find_connector(node);
167 	if (!connector)
168 		goto remove_node;
169 
170 	ret = link_port(to_typec_port(connector), node);
171 	if (ret)
172 		goto put_connector;
173 
174 	return 0;
175 
176 put_connector:
177 	put_device(connector);
178 remove_node:
179 	remove_port_node(node);
180 
181 	return ret;
182 }
183 EXPORT_SYMBOL_GPL(typec_link_port);
184 
185 static int port_match_and_unlink(struct device *connector, void *port)
186 {
187 	struct port_node *node;
188 	struct port_node *tmp;
189 	int ret = 0;
190 
191 	if (!is_typec_port(connector))
192 		return 0;
193 
194 	mutex_lock(&to_typec_port(connector)->port_list_lock);
195 	list_for_each_entry_safe(node, tmp, &to_typec_port(connector)->port_list, list) {
196 		ret = node->dev == port;
197 		if (ret) {
198 			unlink_port(to_typec_port(connector), node);
199 			remove_port_node(node);
200 			put_device(connector);
201 			break;
202 		}
203 	}
204 	mutex_unlock(&to_typec_port(connector)->port_list_lock);
205 
206 	return ret;
207 }
208 
209 /**
210  * typec_unlink_port - Unlink port from its connector
211  * @port: The port device
212  *
213  * Removes the symlink "connector" and decrements the reference count of @port.
214  */
215 void typec_unlink_port(struct device *port)
216 {
217 	class_for_each_device(&typec_class, NULL, port, port_match_and_unlink);
218 }
219 EXPORT_SYMBOL_GPL(typec_unlink_port);
220