]>
Commit | Line | Data |
---|---|---|
69fb4dca HG |
1 | /* |
2 | * AXP20x PMIC USB power supply status driver | |
3 | * | |
4 | * Copyright (C) 2015 Hans de Goede <hdegoede@redhat.com> | |
5 | * Copyright (C) 2014 Bruno Prémont <bonbons@linux-vserver.org> | |
6 | * | |
7 | * This program is free software; you can redistribute it and/or modify it | |
8 | * under the terms of the GNU General Public License as published by the | |
9 | * Free Software Foundation; either version 2 of the License, or (at your | |
10 | * option) any later version. | |
11 | */ | |
12 | ||
13 | #include <linux/device.h> | |
14 | #include <linux/init.h> | |
15 | #include <linux/interrupt.h> | |
16 | #include <linux/kernel.h> | |
17 | #include <linux/mfd/axp20x.h> | |
18 | #include <linux/module.h> | |
19 | #include <linux/of.h> | |
20 | #include <linux/platform_device.h> | |
21 | #include <linux/power_supply.h> | |
22 | #include <linux/regmap.h> | |
23 | #include <linux/slab.h> | |
24 | ||
25 | #define DRVNAME "axp20x-usb-power-supply" | |
26 | ||
27 | #define AXP20X_PWR_STATUS_VBUS_PRESENT BIT(5) | |
28 | #define AXP20X_PWR_STATUS_VBUS_USED BIT(4) | |
29 | ||
30 | #define AXP20X_USB_STATUS_VBUS_VALID BIT(2) | |
31 | ||
32 | #define AXP20X_VBUS_VHOLD_uV(b) (4000000 + (((b) >> 3) & 7) * 100000) | |
33 | #define AXP20X_VBUS_CLIMIT_MASK 3 | |
34 | #define AXP20X_VBUC_CLIMIT_900mA 0 | |
35 | #define AXP20X_VBUC_CLIMIT_500mA 1 | |
36 | #define AXP20X_VBUC_CLIMIT_100mA 2 | |
37 | #define AXP20X_VBUC_CLIMIT_NONE 3 | |
38 | ||
39 | #define AXP20X_ADC_EN1_VBUS_CURR BIT(2) | |
40 | #define AXP20X_ADC_EN1_VBUS_VOLT BIT(3) | |
41 | ||
42 | #define AXP20X_VBUS_MON_VBUS_VALID BIT(3) | |
43 | ||
44 | struct axp20x_usb_power { | |
cecbf8d5 | 45 | struct device_node *np; |
69fb4dca HG |
46 | struct regmap *regmap; |
47 | struct power_supply *supply; | |
48 | }; | |
49 | ||
50 | static irqreturn_t axp20x_usb_power_irq(int irq, void *devid) | |
51 | { | |
52 | struct axp20x_usb_power *power = devid; | |
53 | ||
54 | power_supply_changed(power->supply); | |
55 | ||
56 | return IRQ_HANDLED; | |
57 | } | |
58 | ||
59 | static int axp20x_usb_power_get_property(struct power_supply *psy, | |
60 | enum power_supply_property psp, union power_supply_propval *val) | |
61 | { | |
62 | struct axp20x_usb_power *power = power_supply_get_drvdata(psy); | |
63 | unsigned int input, v; | |
64 | int ret; | |
65 | ||
66 | switch (psp) { | |
67 | case POWER_SUPPLY_PROP_VOLTAGE_MIN: | |
68 | ret = regmap_read(power->regmap, AXP20X_VBUS_IPSOUT_MGMT, &v); | |
69 | if (ret) | |
70 | return ret; | |
71 | ||
72 | val->intval = AXP20X_VBUS_VHOLD_uV(v); | |
73 | return 0; | |
74 | case POWER_SUPPLY_PROP_VOLTAGE_NOW: | |
75 | ret = axp20x_read_variable_width(power->regmap, | |
76 | AXP20X_VBUS_V_ADC_H, 12); | |
77 | if (ret < 0) | |
78 | return ret; | |
79 | ||
80 | val->intval = ret * 1700; /* 1 step = 1.7 mV */ | |
81 | return 0; | |
82 | case POWER_SUPPLY_PROP_CURRENT_MAX: | |
83 | ret = regmap_read(power->regmap, AXP20X_VBUS_IPSOUT_MGMT, &v); | |
84 | if (ret) | |
85 | return ret; | |
86 | ||
87 | switch (v & AXP20X_VBUS_CLIMIT_MASK) { | |
88 | case AXP20X_VBUC_CLIMIT_100mA: | |
cecbf8d5 HG |
89 | if (of_device_is_compatible(power->np, |
90 | "x-powers,axp202-usb-power-supply")) { | |
91 | val->intval = 100000; | |
92 | } else { | |
93 | val->intval = -1; /* No 100mA limit */ | |
94 | } | |
69fb4dca HG |
95 | break; |
96 | case AXP20X_VBUC_CLIMIT_500mA: | |
97 | val->intval = 500000; | |
98 | break; | |
99 | case AXP20X_VBUC_CLIMIT_900mA: | |
100 | val->intval = 900000; | |
101 | break; | |
102 | case AXP20X_VBUC_CLIMIT_NONE: | |
103 | val->intval = -1; | |
104 | break; | |
105 | } | |
106 | return 0; | |
107 | case POWER_SUPPLY_PROP_CURRENT_NOW: | |
108 | ret = axp20x_read_variable_width(power->regmap, | |
109 | AXP20X_VBUS_I_ADC_H, 12); | |
110 | if (ret < 0) | |
111 | return ret; | |
112 | ||
113 | val->intval = ret * 375; /* 1 step = 0.375 mA */ | |
114 | return 0; | |
115 | default: | |
116 | break; | |
117 | } | |
118 | ||
119 | /* All the properties below need the input-status reg value */ | |
120 | ret = regmap_read(power->regmap, AXP20X_PWR_INPUT_STATUS, &input); | |
121 | if (ret) | |
122 | return ret; | |
123 | ||
124 | switch (psp) { | |
125 | case POWER_SUPPLY_PROP_HEALTH: | |
126 | if (!(input & AXP20X_PWR_STATUS_VBUS_PRESENT)) { | |
127 | val->intval = POWER_SUPPLY_HEALTH_UNKNOWN; | |
128 | break; | |
129 | } | |
130 | ||
cecbf8d5 | 131 | val->intval = POWER_SUPPLY_HEALTH_GOOD; |
69fb4dca | 132 | |
cecbf8d5 HG |
133 | if (of_device_is_compatible(power->np, |
134 | "x-powers,axp202-usb-power-supply")) { | |
135 | ret = regmap_read(power->regmap, | |
136 | AXP20X_USB_OTG_STATUS, &v); | |
137 | if (ret) | |
138 | return ret; | |
69fb4dca | 139 | |
cecbf8d5 HG |
140 | if (!(v & AXP20X_USB_STATUS_VBUS_VALID)) |
141 | val->intval = | |
142 | POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; | |
143 | } | |
69fb4dca HG |
144 | break; |
145 | case POWER_SUPPLY_PROP_PRESENT: | |
146 | val->intval = !!(input & AXP20X_PWR_STATUS_VBUS_PRESENT); | |
147 | break; | |
148 | case POWER_SUPPLY_PROP_ONLINE: | |
149 | val->intval = !!(input & AXP20X_PWR_STATUS_VBUS_USED); | |
150 | break; | |
151 | default: | |
152 | return -EINVAL; | |
153 | } | |
154 | ||
155 | return 0; | |
156 | } | |
157 | ||
158 | static enum power_supply_property axp20x_usb_power_properties[] = { | |
159 | POWER_SUPPLY_PROP_HEALTH, | |
160 | POWER_SUPPLY_PROP_PRESENT, | |
161 | POWER_SUPPLY_PROP_ONLINE, | |
162 | POWER_SUPPLY_PROP_VOLTAGE_MIN, | |
163 | POWER_SUPPLY_PROP_VOLTAGE_NOW, | |
164 | POWER_SUPPLY_PROP_CURRENT_MAX, | |
165 | POWER_SUPPLY_PROP_CURRENT_NOW, | |
166 | }; | |
167 | ||
cecbf8d5 HG |
168 | static enum power_supply_property axp22x_usb_power_properties[] = { |
169 | POWER_SUPPLY_PROP_HEALTH, | |
170 | POWER_SUPPLY_PROP_PRESENT, | |
171 | POWER_SUPPLY_PROP_ONLINE, | |
172 | POWER_SUPPLY_PROP_VOLTAGE_MIN, | |
173 | POWER_SUPPLY_PROP_CURRENT_MAX, | |
174 | }; | |
175 | ||
69fb4dca HG |
176 | static const struct power_supply_desc axp20x_usb_power_desc = { |
177 | .name = "axp20x-usb", | |
178 | .type = POWER_SUPPLY_TYPE_USB, | |
179 | .properties = axp20x_usb_power_properties, | |
180 | .num_properties = ARRAY_SIZE(axp20x_usb_power_properties), | |
181 | .get_property = axp20x_usb_power_get_property, | |
182 | }; | |
183 | ||
cecbf8d5 HG |
184 | static const struct power_supply_desc axp22x_usb_power_desc = { |
185 | .name = "axp20x-usb", | |
186 | .type = POWER_SUPPLY_TYPE_USB, | |
187 | .properties = axp22x_usb_power_properties, | |
188 | .num_properties = ARRAY_SIZE(axp22x_usb_power_properties), | |
189 | .get_property = axp20x_usb_power_get_property, | |
190 | }; | |
191 | ||
69fb4dca HG |
192 | static int axp20x_usb_power_probe(struct platform_device *pdev) |
193 | { | |
194 | struct axp20x_dev *axp20x = dev_get_drvdata(pdev->dev.parent); | |
195 | struct power_supply_config psy_cfg = {}; | |
196 | struct axp20x_usb_power *power; | |
cecbf8d5 HG |
197 | static const char * const axp20x_irq_names[] = { "VBUS_PLUGIN", |
198 | "VBUS_REMOVAL", "VBUS_VALID", "VBUS_NOT_VALID", NULL }; | |
199 | static const char * const axp22x_irq_names[] = { | |
200 | "VBUS_PLUGIN", "VBUS_REMOVAL", NULL }; | |
201 | static const char * const *irq_names; | |
202 | const struct power_supply_desc *usb_power_desc; | |
69fb4dca HG |
203 | int i, irq, ret; |
204 | ||
205 | if (!of_device_is_available(pdev->dev.of_node)) | |
206 | return -ENODEV; | |
207 | ||
208 | if (!axp20x) { | |
209 | dev_err(&pdev->dev, "Parent drvdata not set\n"); | |
210 | return -EINVAL; | |
211 | } | |
212 | ||
213 | power = devm_kzalloc(&pdev->dev, sizeof(*power), GFP_KERNEL); | |
214 | if (!power) | |
215 | return -ENOMEM; | |
216 | ||
cecbf8d5 | 217 | power->np = pdev->dev.of_node; |
69fb4dca HG |
218 | power->regmap = axp20x->regmap; |
219 | ||
cecbf8d5 HG |
220 | if (of_device_is_compatible(power->np, |
221 | "x-powers,axp202-usb-power-supply")) { | |
222 | /* Enable vbus valid checking */ | |
223 | ret = regmap_update_bits(power->regmap, AXP20X_VBUS_MON, | |
224 | AXP20X_VBUS_MON_VBUS_VALID, | |
225 | AXP20X_VBUS_MON_VBUS_VALID); | |
226 | if (ret) | |
227 | return ret; | |
69fb4dca | 228 | |
cecbf8d5 HG |
229 | /* Enable vbus voltage and current measurement */ |
230 | ret = regmap_update_bits(power->regmap, AXP20X_ADC_EN1, | |
69fb4dca HG |
231 | AXP20X_ADC_EN1_VBUS_CURR | AXP20X_ADC_EN1_VBUS_VOLT, |
232 | AXP20X_ADC_EN1_VBUS_CURR | AXP20X_ADC_EN1_VBUS_VOLT); | |
cecbf8d5 HG |
233 | if (ret) |
234 | return ret; | |
235 | ||
236 | usb_power_desc = &axp20x_usb_power_desc; | |
237 | irq_names = axp20x_irq_names; | |
238 | } else if (of_device_is_compatible(power->np, | |
239 | "x-powers,axp221-usb-power-supply")) { | |
240 | usb_power_desc = &axp22x_usb_power_desc; | |
241 | irq_names = axp22x_irq_names; | |
242 | } else { | |
243 | dev_err(&pdev->dev, "Unsupported AXP variant: %ld\n", | |
244 | axp20x->variant); | |
245 | return -EINVAL; | |
246 | } | |
69fb4dca HG |
247 | |
248 | psy_cfg.of_node = pdev->dev.of_node; | |
249 | psy_cfg.drv_data = power; | |
250 | ||
cecbf8d5 HG |
251 | power->supply = devm_power_supply_register(&pdev->dev, usb_power_desc, |
252 | &psy_cfg); | |
69fb4dca HG |
253 | if (IS_ERR(power->supply)) |
254 | return PTR_ERR(power->supply); | |
255 | ||
256 | /* Request irqs after registering, as irqs may trigger immediately */ | |
cecbf8d5 | 257 | for (i = 0; irq_names[i]; i++) { |
69fb4dca HG |
258 | irq = platform_get_irq_byname(pdev, irq_names[i]); |
259 | if (irq < 0) { | |
260 | dev_warn(&pdev->dev, "No IRQ for %s: %d\n", | |
261 | irq_names[i], irq); | |
262 | continue; | |
263 | } | |
264 | irq = regmap_irq_get_virq(axp20x->regmap_irqc, irq); | |
265 | ret = devm_request_any_context_irq(&pdev->dev, irq, | |
266 | axp20x_usb_power_irq, 0, DRVNAME, power); | |
267 | if (ret < 0) | |
268 | dev_warn(&pdev->dev, "Error requesting %s IRQ: %d\n", | |
269 | irq_names[i], ret); | |
270 | } | |
271 | ||
272 | return 0; | |
273 | } | |
274 | ||
275 | static const struct of_device_id axp20x_usb_power_match[] = { | |
276 | { .compatible = "x-powers,axp202-usb-power-supply" }, | |
cecbf8d5 | 277 | { .compatible = "x-powers,axp221-usb-power-supply" }, |
69fb4dca HG |
278 | { } |
279 | }; | |
280 | MODULE_DEVICE_TABLE(of, axp20x_usb_power_match); | |
281 | ||
282 | static struct platform_driver axp20x_usb_power_driver = { | |
283 | .probe = axp20x_usb_power_probe, | |
284 | .driver = { | |
285 | .name = DRVNAME, | |
286 | .of_match_table = axp20x_usb_power_match, | |
287 | }, | |
288 | }; | |
289 | ||
290 | module_platform_driver(axp20x_usb_power_driver); | |
291 | ||
292 | MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com>"); | |
293 | MODULE_DESCRIPTION("AXP20x PMIC USB power supply status driver"); | |
294 | MODULE_LICENSE("GPL"); |