mirror of
https://github.com/torvalds/linux.git
synced 2024-12-05 02:23:16 +00:00
50da8d04ee
The MAX8903 uses up to 5 different GPIO lines to control and monitor charging. When converting to use GPIO descriptors instead of the old GPIO numbers the following side-refactorings were done: - Decomission the platform data container struct as all GPIO descriptors are now "live" members of the driver state container. The "dc_valid" and "usb_valid" just indicate the presence of a DC or USB charger detection line, and this can be handled by just checking if the optional GPIO descriptor for each is != NULL. - The gpiolib will now respect the GPIO_ACTIVE_LOW flag for each of the lines, meaning gpiod_get_value() for example will return 1 (asserted) if a line is flagged as active low and is also physically low. The same applies to output lines, vice versa mutatis mutandis. The code has been augmented to account for this in all sites. - The terse parenthesis such as this: gpio_set_value(pdata->cen, ta_in ? 0 : (data->usb_in ? 0 : 1)); have been expanded to more readable if / else if / else statements that are easier for humans to read. - Comments were inserted to underscore polarity in each case where it could be confusing to users of the old code. One thing is notable: the device tree bindings does not show an example of polarity assigned for the line "dcm-gpios" DC current monitor, is assumed to be flagged GPIO_ACTIVE_HIGH and driving it high (asserted) will achieve DC charger current limits and driving it low will achieve USB charger current limits. Device trees with this (optional) GPIO line defined should definately be flagged as GPIO_ACTIVE_HIGH. Cc: Chris Lapa <chris@lapa.com.au> Cc: MyungJoo Ham <myungjoo.ham@samsung.com> Signed-off-by: Linus Walleij <linus.walleij@linaro.org> Signed-off-by: Sebastian Reichel <sebastian.reichel@collabora.com>
424 lines
11 KiB
C
424 lines
11 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* max8903_charger.c - Maxim 8903 USB/Adapter Charger Driver
|
|
*
|
|
* Copyright (C) 2011 Samsung Electronics
|
|
* MyungJoo Ham <myungjoo.ham@samsung.com>
|
|
*/
|
|
|
|
#include <linux/gpio/consumer.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/module.h>
|
|
#include <linux/of.h>
|
|
#include <linux/of_device.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/power_supply.h>
|
|
#include <linux/platform_device.h>
|
|
|
|
struct max8903_data {
|
|
struct device *dev;
|
|
struct power_supply *psy;
|
|
struct power_supply_desc psy_desc;
|
|
/*
|
|
* GPIOs
|
|
* chg, flt, dcm and usus are optional.
|
|
* dok or uok must be present.
|
|
* If dok is present, cen must be present.
|
|
*/
|
|
struct gpio_desc *cen; /* Charger Enable input */
|
|
struct gpio_desc *dok; /* DC (Adapter) Power OK output */
|
|
struct gpio_desc *uok; /* USB Power OK output */
|
|
struct gpio_desc *chg; /* Charger status output */
|
|
struct gpio_desc *flt; /* Fault output */
|
|
struct gpio_desc *dcm; /* Current-Limit Mode input (1: DC, 2: USB) */
|
|
struct gpio_desc *usus; /* USB Suspend Input (1: suspended) */
|
|
bool fault;
|
|
bool usb_in;
|
|
bool ta_in;
|
|
};
|
|
|
|
static enum power_supply_property max8903_charger_props[] = {
|
|
POWER_SUPPLY_PROP_STATUS, /* Charger status output */
|
|
POWER_SUPPLY_PROP_ONLINE, /* External power source */
|
|
POWER_SUPPLY_PROP_HEALTH, /* Fault or OK */
|
|
};
|
|
|
|
static int max8903_get_property(struct power_supply *psy,
|
|
enum power_supply_property psp,
|
|
union power_supply_propval *val)
|
|
{
|
|
struct max8903_data *data = power_supply_get_drvdata(psy);
|
|
|
|
switch (psp) {
|
|
case POWER_SUPPLY_PROP_STATUS:
|
|
val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
|
|
if (data->chg) {
|
|
if (gpiod_get_value(data->chg))
|
|
/* CHG asserted */
|
|
val->intval = POWER_SUPPLY_STATUS_CHARGING;
|
|
else if (data->usb_in || data->ta_in)
|
|
val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING;
|
|
else
|
|
val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
|
|
}
|
|
break;
|
|
case POWER_SUPPLY_PROP_ONLINE:
|
|
val->intval = 0;
|
|
if (data->usb_in || data->ta_in)
|
|
val->intval = 1;
|
|
break;
|
|
case POWER_SUPPLY_PROP_HEALTH:
|
|
val->intval = POWER_SUPPLY_HEALTH_GOOD;
|
|
if (data->fault)
|
|
val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE;
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static irqreturn_t max8903_dcin(int irq, void *_data)
|
|
{
|
|
struct max8903_data *data = _data;
|
|
bool ta_in;
|
|
enum power_supply_type old_type;
|
|
|
|
/*
|
|
* This means the line is asserted.
|
|
*
|
|
* The signal is active low, but the inversion is handled in the GPIO
|
|
* library as the line should be flagged GPIO_ACTIVE_LOW in the device
|
|
* tree.
|
|
*/
|
|
ta_in = gpiod_get_value(data->dok);
|
|
|
|
if (ta_in == data->ta_in)
|
|
return IRQ_HANDLED;
|
|
|
|
data->ta_in = ta_in;
|
|
|
|
/* Set Current-Limit-Mode 1:DC 0:USB */
|
|
if (data->dcm)
|
|
gpiod_set_value(data->dcm, ta_in);
|
|
|
|
/* Charger Enable / Disable */
|
|
if (data->cen) {
|
|
int val;
|
|
|
|
if (ta_in)
|
|
/* Certainly enable if DOK is asserted */
|
|
val = 1;
|
|
else if (data->usb_in)
|
|
/* Enable if the USB charger is enabled */
|
|
val = 1;
|
|
else
|
|
/* Else default-disable */
|
|
val = 0;
|
|
|
|
gpiod_set_value(data->cen, val);
|
|
}
|
|
|
|
dev_dbg(data->dev, "TA(DC-IN) Charger %s.\n", ta_in ?
|
|
"Connected" : "Disconnected");
|
|
|
|
old_type = data->psy_desc.type;
|
|
|
|
if (data->ta_in)
|
|
data->psy_desc.type = POWER_SUPPLY_TYPE_MAINS;
|
|
else if (data->usb_in)
|
|
data->psy_desc.type = POWER_SUPPLY_TYPE_USB;
|
|
else
|
|
data->psy_desc.type = POWER_SUPPLY_TYPE_BATTERY;
|
|
|
|
if (old_type != data->psy_desc.type)
|
|
power_supply_changed(data->psy);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static irqreturn_t max8903_usbin(int irq, void *_data)
|
|
{
|
|
struct max8903_data *data = _data;
|
|
bool usb_in;
|
|
enum power_supply_type old_type;
|
|
|
|
/*
|
|
* This means the line is asserted.
|
|
*
|
|
* The signal is active low, but the inversion is handled in the GPIO
|
|
* library as the line should be flagged GPIO_ACTIVE_LOW in the device
|
|
* tree.
|
|
*/
|
|
usb_in = gpiod_get_value(data->uok);
|
|
|
|
if (usb_in == data->usb_in)
|
|
return IRQ_HANDLED;
|
|
|
|
data->usb_in = usb_in;
|
|
|
|
/* Do not touch Current-Limit-Mode */
|
|
|
|
/* Charger Enable / Disable */
|
|
if (data->cen) {
|
|
int val;
|
|
|
|
if (usb_in)
|
|
/* Certainly enable if UOK is asserted */
|
|
val = 1;
|
|
else if (data->ta_in)
|
|
/* Enable if the DC charger is enabled */
|
|
val = 1;
|
|
else
|
|
/* Else default-disable */
|
|
val = 0;
|
|
|
|
gpiod_set_value(data->cen, val);
|
|
}
|
|
|
|
dev_dbg(data->dev, "USB Charger %s.\n", usb_in ?
|
|
"Connected" : "Disconnected");
|
|
|
|
old_type = data->psy_desc.type;
|
|
|
|
if (data->ta_in)
|
|
data->psy_desc.type = POWER_SUPPLY_TYPE_MAINS;
|
|
else if (data->usb_in)
|
|
data->psy_desc.type = POWER_SUPPLY_TYPE_USB;
|
|
else
|
|
data->psy_desc.type = POWER_SUPPLY_TYPE_BATTERY;
|
|
|
|
if (old_type != data->psy_desc.type)
|
|
power_supply_changed(data->psy);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static irqreturn_t max8903_fault(int irq, void *_data)
|
|
{
|
|
struct max8903_data *data = _data;
|
|
bool fault;
|
|
|
|
/*
|
|
* This means the line is asserted.
|
|
*
|
|
* The signal is active low, but the inversion is handled in the GPIO
|
|
* library as the line should be flagged GPIO_ACTIVE_LOW in the device
|
|
* tree.
|
|
*/
|
|
fault = gpiod_get_value(data->flt);
|
|
|
|
if (fault == data->fault)
|
|
return IRQ_HANDLED;
|
|
|
|
data->fault = fault;
|
|
|
|
if (fault)
|
|
dev_err(data->dev, "Charger suffers a fault and stops.\n");
|
|
else
|
|
dev_err(data->dev, "Charger recovered from a fault.\n");
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static int max8903_setup_gpios(struct platform_device *pdev)
|
|
{
|
|
struct max8903_data *data = platform_get_drvdata(pdev);
|
|
struct device *dev = &pdev->dev;
|
|
bool ta_in = false;
|
|
bool usb_in = false;
|
|
enum gpiod_flags flags;
|
|
|
|
data->dok = devm_gpiod_get_optional(dev, "dok", GPIOD_IN);
|
|
if (IS_ERR(data->dok))
|
|
return dev_err_probe(dev, PTR_ERR(data->dok),
|
|
"failed to get DOK GPIO");
|
|
if (data->dok) {
|
|
gpiod_set_consumer_name(data->dok, data->psy_desc.name);
|
|
/*
|
|
* The DC OK is pulled up to 1 and goes low when a charger
|
|
* is plugged in (active low) but in the device tree the
|
|
* line is marked as GPIO_ACTIVE_LOW so we get a 1 (asserted)
|
|
* here if the DC charger is plugged in.
|
|
*/
|
|
ta_in = gpiod_get_value(data->dok);
|
|
}
|
|
|
|
data->uok = devm_gpiod_get_optional(dev, "uok", GPIOD_IN);
|
|
if (IS_ERR(data->uok))
|
|
return dev_err_probe(dev, PTR_ERR(data->uok),
|
|
"failed to get UOK GPIO");
|
|
if (data->uok) {
|
|
gpiod_set_consumer_name(data->uok, data->psy_desc.name);
|
|
/*
|
|
* The USB OK is pulled up to 1 and goes low when a USB charger
|
|
* is plugged in (active low) but in the device tree the
|
|
* line is marked as GPIO_ACTIVE_LOW so we get a 1 (asserted)
|
|
* here if the USB charger is plugged in.
|
|
*/
|
|
usb_in = gpiod_get_value(data->uok);
|
|
}
|
|
|
|
/* Either DC OK or USB OK must be provided */
|
|
if (!data->dok && !data->uok) {
|
|
dev_err(dev, "no valid power source\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
/*
|
|
* If either charger is already connected at this point,
|
|
* assert the CEN line and enable charging from the start.
|
|
*
|
|
* The line is active low but also marked with GPIO_ACTIVE_LOW
|
|
* in the device tree, so when we assert the line with
|
|
* GPIOD_OUT_HIGH the line will be driven low.
|
|
*/
|
|
flags = (ta_in || usb_in) ? GPIOD_OUT_HIGH : GPIOD_OUT_LOW;
|
|
/*
|
|
* If DC OK is provided, Charger Enable CEN is compulsory
|
|
* so this is not optional here.
|
|
*/
|
|
data->cen = devm_gpiod_get(dev, "cen", flags);
|
|
if (IS_ERR(data->cen))
|
|
return dev_err_probe(dev, PTR_ERR(data->cen),
|
|
"failed to get CEN GPIO");
|
|
gpiod_set_consumer_name(data->cen, data->psy_desc.name);
|
|
|
|
/*
|
|
* If the DC charger is connected, then select it.
|
|
*
|
|
* The DCM line should be marked GPIO_ACTIVE_HIGH in the
|
|
* device tree. Driving it high will enable the DC charger
|
|
* input over the USB charger input.
|
|
*/
|
|
flags = ta_in ? GPIOD_OUT_HIGH : GPIOD_OUT_LOW;
|
|
data->dcm = devm_gpiod_get_optional(dev, "dcm", flags);
|
|
if (IS_ERR(data->dcm))
|
|
return dev_err_probe(dev, PTR_ERR(data->dcm),
|
|
"failed to get DCM GPIO");
|
|
gpiod_set_consumer_name(data->dcm, data->psy_desc.name);
|
|
|
|
data->chg = devm_gpiod_get_optional(dev, "chg", GPIOD_IN);
|
|
if (IS_ERR(data->chg))
|
|
return dev_err_probe(dev, PTR_ERR(data->chg),
|
|
"failed to get CHG GPIO");
|
|
gpiod_set_consumer_name(data->chg, data->psy_desc.name);
|
|
|
|
data->flt = devm_gpiod_get_optional(dev, "flt", GPIOD_IN);
|
|
if (IS_ERR(data->flt))
|
|
return dev_err_probe(dev, PTR_ERR(data->flt),
|
|
"failed to get FLT GPIO");
|
|
gpiod_set_consumer_name(data->flt, data->psy_desc.name);
|
|
|
|
data->usus = devm_gpiod_get_optional(dev, "usus", GPIOD_IN);
|
|
if (IS_ERR(data->usus))
|
|
return dev_err_probe(dev, PTR_ERR(data->usus),
|
|
"failed to get USUS GPIO");
|
|
gpiod_set_consumer_name(data->usus, data->psy_desc.name);
|
|
|
|
data->fault = false;
|
|
data->ta_in = ta_in;
|
|
data->usb_in = usb_in;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int max8903_probe(struct platform_device *pdev)
|
|
{
|
|
struct max8903_data *data;
|
|
struct device *dev = &pdev->dev;
|
|
struct power_supply_config psy_cfg = {};
|
|
int ret = 0;
|
|
|
|
data = devm_kzalloc(dev, sizeof(struct max8903_data), GFP_KERNEL);
|
|
if (!data)
|
|
return -ENOMEM;
|
|
|
|
data->dev = dev;
|
|
platform_set_drvdata(pdev, data);
|
|
|
|
ret = max8903_setup_gpios(pdev);
|
|
if (ret)
|
|
return ret;
|
|
|
|
data->psy_desc.name = "max8903_charger";
|
|
data->psy_desc.type = (data->ta_in) ? POWER_SUPPLY_TYPE_MAINS :
|
|
((data->usb_in) ? POWER_SUPPLY_TYPE_USB :
|
|
POWER_SUPPLY_TYPE_BATTERY);
|
|
data->psy_desc.get_property = max8903_get_property;
|
|
data->psy_desc.properties = max8903_charger_props;
|
|
data->psy_desc.num_properties = ARRAY_SIZE(max8903_charger_props);
|
|
|
|
psy_cfg.of_node = dev->of_node;
|
|
psy_cfg.drv_data = data;
|
|
|
|
data->psy = devm_power_supply_register(dev, &data->psy_desc, &psy_cfg);
|
|
if (IS_ERR(data->psy)) {
|
|
dev_err(dev, "failed: power supply register.\n");
|
|
return PTR_ERR(data->psy);
|
|
}
|
|
|
|
if (data->dok) {
|
|
ret = devm_request_threaded_irq(dev, gpiod_to_irq(data->dok),
|
|
NULL, max8903_dcin,
|
|
IRQF_TRIGGER_FALLING |
|
|
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
|
|
"MAX8903 DC IN", data);
|
|
if (ret) {
|
|
dev_err(dev, "Cannot request irq %d for DC (%d)\n",
|
|
gpiod_to_irq(data->dok), ret);
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
if (data->uok) {
|
|
ret = devm_request_threaded_irq(dev, gpiod_to_irq(data->uok),
|
|
NULL, max8903_usbin,
|
|
IRQF_TRIGGER_FALLING |
|
|
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
|
|
"MAX8903 USB IN", data);
|
|
if (ret) {
|
|
dev_err(dev, "Cannot request irq %d for USB (%d)\n",
|
|
gpiod_to_irq(data->uok), ret);
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
if (data->flt) {
|
|
ret = devm_request_threaded_irq(dev, gpiod_to_irq(data->flt),
|
|
NULL, max8903_fault,
|
|
IRQF_TRIGGER_FALLING |
|
|
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
|
|
"MAX8903 Fault", data);
|
|
if (ret) {
|
|
dev_err(dev, "Cannot request irq %d for Fault (%d)\n",
|
|
gpiod_to_irq(data->flt), ret);
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct of_device_id max8903_match_ids[] = {
|
|
{ .compatible = "maxim,max8903", },
|
|
{ /* sentinel */ }
|
|
};
|
|
MODULE_DEVICE_TABLE(of, max8903_match_ids);
|
|
|
|
static struct platform_driver max8903_driver = {
|
|
.probe = max8903_probe,
|
|
.driver = {
|
|
.name = "max8903-charger",
|
|
.of_match_table = max8903_match_ids
|
|
},
|
|
};
|
|
|
|
module_platform_driver(max8903_driver);
|
|
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_DESCRIPTION("MAX8903 Charger Driver");
|
|
MODULE_AUTHOR("MyungJoo Ham <myungjoo.ham@samsung.com>");
|
|
MODULE_ALIAS("platform:max8903-charger");
|