]>
Commit | Line | Data |
---|---|---|
249fa821 BN |
1 | // SPDX-License-Identifier: GPL-2.0+ |
2 | /* | |
3 | * Fast-charge control for Apple "MFi" devices | |
4 | * | |
5 | * Copyright (C) 2019 Bastien Nocera <hadess@hadess.net> | |
6 | */ | |
7 | ||
8 | /* Standard include files */ | |
9 | #include <linux/module.h> | |
10 | #include <linux/power_supply.h> | |
11 | #include <linux/slab.h> | |
12 | #include <linux/usb.h> | |
13 | ||
14 | MODULE_AUTHOR("Bastien Nocera <hadess@hadess.net>"); | |
15 | MODULE_DESCRIPTION("Fast-charge control for Apple \"MFi\" devices"); | |
16 | MODULE_LICENSE("GPL"); | |
17 | ||
18 | #define TRICKLE_CURRENT_MA 0 | |
19 | #define FAST_CURRENT_MA 2500 | |
20 | ||
21 | #define APPLE_VENDOR_ID 0x05ac /* Apple */ | |
22 | ||
23 | /* The product ID is defined as starting with 0x12nn, as per the | |
24 | * "Choosing an Apple Device USB Configuration" section in | |
25 | * release R9 (2012) of the "MFi Accessory Hardware Specification" | |
26 | * | |
27 | * To distinguish an Apple device, a USB host can check the device | |
28 | * descriptor of attached USB devices for the following fields: | |
29 | * ■ Vendor ID: 0x05AC | |
30 | * ■ Product ID: 0x12nn | |
31 | * | |
32 | * Those checks will be done in .match() and .probe(). | |
33 | */ | |
34 | ||
35 | static const struct usb_device_id mfi_fc_id_table[] = { | |
36 | { .idVendor = APPLE_VENDOR_ID, | |
37 | .match_flags = USB_DEVICE_ID_MATCH_VENDOR }, | |
38 | {}, | |
39 | }; | |
40 | ||
41 | MODULE_DEVICE_TABLE(usb, mfi_fc_id_table); | |
42 | ||
43 | /* Driver-local specific stuff */ | |
44 | struct mfi_device { | |
45 | struct usb_device *udev; | |
46 | struct power_supply *battery; | |
47 | int charge_type; | |
48 | }; | |
49 | ||
50 | static int apple_mfi_fc_set_charge_type(struct mfi_device *mfi, | |
51 | const union power_supply_propval *val) | |
52 | { | |
53 | int current_ma; | |
54 | int retval; | |
55 | __u8 request_type; | |
56 | ||
57 | if (mfi->charge_type == val->intval) { | |
58 | dev_dbg(&mfi->udev->dev, "charge type %d already set\n", | |
59 | mfi->charge_type); | |
60 | return 0; | |
61 | } | |
62 | ||
63 | switch (val->intval) { | |
64 | case POWER_SUPPLY_CHARGE_TYPE_TRICKLE: | |
65 | current_ma = TRICKLE_CURRENT_MA; | |
66 | break; | |
67 | case POWER_SUPPLY_CHARGE_TYPE_FAST: | |
68 | current_ma = FAST_CURRENT_MA; | |
69 | break; | |
70 | default: | |
71 | return -EINVAL; | |
72 | } | |
73 | ||
74 | request_type = USB_DIR_OUT | USB_TYPE_VENDOR | USB_RECIP_DEVICE; | |
75 | retval = usb_control_msg(mfi->udev, usb_sndctrlpipe(mfi->udev, 0), | |
76 | 0x40, /* Vendor‐defined power request */ | |
77 | request_type, | |
78 | current_ma, /* wValue, current offset */ | |
79 | current_ma, /* wIndex, current offset */ | |
80 | NULL, 0, USB_CTRL_GET_TIMEOUT); | |
81 | if (retval) { | |
82 | dev_dbg(&mfi->udev->dev, "retval = %d\n", retval); | |
83 | return retval; | |
84 | } | |
85 | ||
86 | mfi->charge_type = val->intval; | |
87 | ||
88 | return 0; | |
89 | } | |
90 | ||
91 | static int apple_mfi_fc_get_property(struct power_supply *psy, | |
92 | enum power_supply_property psp, | |
93 | union power_supply_propval *val) | |
94 | { | |
95 | struct mfi_device *mfi = power_supply_get_drvdata(psy); | |
96 | ||
97 | dev_dbg(&mfi->udev->dev, "prop: %d\n", psp); | |
98 | ||
99 | switch (psp) { | |
100 | case POWER_SUPPLY_PROP_CHARGE_TYPE: | |
101 | val->intval = mfi->charge_type; | |
102 | break; | |
103 | case POWER_SUPPLY_PROP_SCOPE: | |
104 | val->intval = POWER_SUPPLY_SCOPE_DEVICE; | |
105 | break; | |
106 | default: | |
107 | return -ENODATA; | |
108 | } | |
109 | ||
110 | return 0; | |
111 | } | |
112 | ||
113 | static int apple_mfi_fc_set_property(struct power_supply *psy, | |
114 | enum power_supply_property psp, | |
115 | const union power_supply_propval *val) | |
116 | { | |
117 | struct mfi_device *mfi = power_supply_get_drvdata(psy); | |
118 | int ret; | |
119 | ||
120 | dev_dbg(&mfi->udev->dev, "prop: %d\n", psp); | |
121 | ||
122 | ret = pm_runtime_get_sync(&mfi->udev->dev); | |
00bd6bca ZQ |
123 | if (ret < 0) { |
124 | pm_runtime_put_noidle(&mfi->udev->dev); | |
249fa821 | 125 | return ret; |
00bd6bca | 126 | } |
249fa821 BN |
127 | |
128 | switch (psp) { | |
129 | case POWER_SUPPLY_PROP_CHARGE_TYPE: | |
130 | ret = apple_mfi_fc_set_charge_type(mfi, val); | |
131 | break; | |
132 | default: | |
133 | ret = -EINVAL; | |
134 | } | |
135 | ||
136 | pm_runtime_mark_last_busy(&mfi->udev->dev); | |
137 | pm_runtime_put_autosuspend(&mfi->udev->dev); | |
138 | ||
139 | return ret; | |
140 | } | |
141 | ||
142 | static int apple_mfi_fc_property_is_writeable(struct power_supply *psy, | |
143 | enum power_supply_property psp) | |
144 | { | |
145 | switch (psp) { | |
146 | case POWER_SUPPLY_PROP_CHARGE_TYPE: | |
147 | return 1; | |
148 | default: | |
149 | return 0; | |
150 | } | |
151 | } | |
152 | ||
153 | static enum power_supply_property apple_mfi_fc_properties[] = { | |
154 | POWER_SUPPLY_PROP_CHARGE_TYPE, | |
155 | POWER_SUPPLY_PROP_SCOPE | |
156 | }; | |
157 | ||
158 | static const struct power_supply_desc apple_mfi_fc_desc = { | |
159 | .name = "apple_mfi_fastcharge", | |
160 | .type = POWER_SUPPLY_TYPE_BATTERY, | |
161 | .properties = apple_mfi_fc_properties, | |
162 | .num_properties = ARRAY_SIZE(apple_mfi_fc_properties), | |
163 | .get_property = apple_mfi_fc_get_property, | |
164 | .set_property = apple_mfi_fc_set_property, | |
165 | .property_is_writeable = apple_mfi_fc_property_is_writeable | |
166 | }; | |
167 | ||
0cb68669 BN |
168 | static bool mfi_fc_match(struct usb_device *udev) |
169 | { | |
170 | int idProduct; | |
171 | ||
172 | idProduct = le16_to_cpu(udev->descriptor.idProduct); | |
173 | /* See comment above mfi_fc_id_table[] */ | |
174 | return (idProduct >= 0x1200 && idProduct <= 0x12ff); | |
175 | } | |
176 | ||
249fa821 BN |
177 | static int mfi_fc_probe(struct usb_device *udev) |
178 | { | |
179 | struct power_supply_config battery_cfg = {}; | |
180 | struct mfi_device *mfi = NULL; | |
0cb68669 | 181 | int err; |
249fa821 | 182 | |
0cb68669 | 183 | if (!mfi_fc_match(udev)) |
249fa821 | 184 | return -ENODEV; |
249fa821 BN |
185 | |
186 | mfi = kzalloc(sizeof(struct mfi_device), GFP_KERNEL); | |
187 | if (!mfi) { | |
188 | err = -ENOMEM; | |
189 | goto error; | |
190 | } | |
191 | ||
192 | battery_cfg.drv_data = mfi; | |
193 | ||
194 | mfi->charge_type = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; | |
195 | mfi->battery = power_supply_register(&udev->dev, | |
196 | &apple_mfi_fc_desc, | |
197 | &battery_cfg); | |
198 | if (IS_ERR(mfi->battery)) { | |
199 | dev_err(&udev->dev, "Can't register battery\n"); | |
200 | err = PTR_ERR(mfi->battery); | |
201 | goto error; | |
202 | } | |
203 | ||
204 | mfi->udev = usb_get_dev(udev); | |
205 | dev_set_drvdata(&udev->dev, mfi); | |
206 | ||
207 | return 0; | |
208 | ||
209 | error: | |
210 | kfree(mfi); | |
211 | return err; | |
212 | } | |
213 | ||
214 | static void mfi_fc_disconnect(struct usb_device *udev) | |
215 | { | |
216 | struct mfi_device *mfi; | |
217 | ||
218 | mfi = dev_get_drvdata(&udev->dev); | |
219 | if (mfi->battery) | |
220 | power_supply_unregister(mfi->battery); | |
221 | dev_set_drvdata(&udev->dev, NULL); | |
222 | usb_put_dev(mfi->udev); | |
223 | kfree(mfi); | |
224 | } | |
225 | ||
226 | static struct usb_device_driver mfi_fc_driver = { | |
227 | .name = "apple-mfi-fastcharge", | |
228 | .probe = mfi_fc_probe, | |
229 | .disconnect = mfi_fc_disconnect, | |
230 | .id_table = mfi_fc_id_table, | |
0cb68669 | 231 | .match = mfi_fc_match, |
249fa821 BN |
232 | .generic_subclass = 1, |
233 | }; | |
234 | ||
235 | static int __init mfi_fc_driver_init(void) | |
236 | { | |
237 | return usb_register_device_driver(&mfi_fc_driver, THIS_MODULE); | |
238 | } | |
239 | ||
240 | static void __exit mfi_fc_driver_exit(void) | |
241 | { | |
242 | usb_deregister_device_driver(&mfi_fc_driver); | |
243 | } | |
244 | ||
245 | module_init(mfi_fc_driver_init); | |
246 | module_exit(mfi_fc_driver_exit); |