blob: 44d226675a93dbe911780580619396c9b149688b [file] [log] [blame]
/*
* Copyright (C) 2014 Nest Labs
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation version 2.
*
* This program is distributed "as is" WITHOUT ANY WARRANTY of any
* kind, whether express or implied; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include <linux/err.h>
#include <linux/gpio/consumer.h>
#include <linux/iio/consumer.h>
#include <linux/iio/types.h>
#include <linux/iio/iio.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <linux/power_supply.h>
#include <linux/printk.h>
#include <linux/mutex.h>
#include <linux/delay.h>
struct adc_div_device {
struct power_supply_desc bat_psd;
struct power_supply *bat_ps;
struct iio_channel *chan;
unsigned int r1_ohm;
unsigned int r2_ohm;
struct gpio_desc *enable_gpio;
unsigned int delay_us;
struct mutex adc_lock;
};
static enum power_supply_property adc_div_bat_props[] = {
POWER_SUPPLY_PROP_ONLINE,
POWER_SUPPLY_PROP_VOLTAGE_NOW,
};
static int adc_div_bat_get_property(struct power_supply *bat_ps,
enum power_supply_property prop,
union power_supply_propval *val)
{
struct adc_div_device *dev = container_of(bat_ps->desc,
struct adc_div_device,
bat_psd);
int ret = 0;
switch (prop) {
case POWER_SUPPLY_PROP_ONLINE:
/* Always online */
val->intval = 1;
break;
case POWER_SUPPLY_PROP_VOLTAGE_NOW:
BUG_ON(!dev->chan);
if(dev->enable_gpio) {
mutex_lock(&dev->adc_lock);
gpiod_set_value(dev->enable_gpio, 1);
usleep_range(dev->delay_us, dev->delay_us + 500);
}
/* Read ADC voltage and then compensate for the divider */
ret = iio_read_channel_processed(dev->chan, &val->intval);
val->intval *= dev->r1_ohm + dev->r2_ohm;
val->intval /= dev->r2_ohm;
/* Processed IIO returns mV but power supply returns uV */
val->intval *= 1000;
if (dev->enable_gpio) {
gpiod_set_value(dev->enable_gpio, 0);
mutex_unlock(&dev->adc_lock);
}
break;
default:
ret = -EINVAL;
}
return ret;
}
static int adc_div_probe(struct platform_device *pdev)
{
struct adc_div_device *dev;
struct device_node *np;
int ret;
/* Check for OF node */
np = pdev->dev.of_node;
if (!np) {
dev_err(&pdev->dev, "Missing DT node\n");
return -ENODEV;
}
/* Claim memory */
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
platform_set_drvdata(pdev, dev);
/* Read DT Properties */
/* Request the enable gpio if given */
dev->enable_gpio = devm_gpiod_get(&pdev->dev, "vbatt-adc-input-enable");
if (!IS_ERR(dev->enable_gpio)) {
ret = gpiod_direction_output(dev->enable_gpio, 0);
if(!ret) {
char* active_low = gpiod_is_active_low(dev->enable_gpio) ? "active low" : "active high";
dev_info(&pdev->dev, "register vbatt-adc-input-enable-gpio %s\n", active_low);
}
else
{
dev_info(&pdev->dev, "could not configure vbat-adc-input-enable %d\n", ret);
}
}
else
{
dev_info(&pdev->dev, "failed to register vbatt-adc-input-enable-gpio err %ld\n", PTR_ERR(dev->enable_gpio));
dev->enable_gpio = NULL;
}
/* Set the delay us if given */
ret = of_property_read_u32(np, "delay-us", &dev->delay_us);
if (ret)
dev->delay_us = 0;
dev_info(&pdev->dev, "register delay-us %d\n", dev->delay_us);
ret = of_property_read_u32(np, "divider-r1-ohm", &dev->r1_ohm);
if (ret) {
dev_err(&pdev->dev, "Couldn't get r1 property from DT\n");
return ret;
}
ret = of_property_read_u32(np, "divider-r2-ohm", &dev->r2_ohm);
if (ret) {
dev_err(&pdev->dev, "Couldn't get r2 property from DT\n");
return ret;
}
if (!dev->r1_ohm || !dev->r2_ohm) {
dev_err(&pdev->dev, "Divider values must be non-zero\n");
return -EINVAL;
}
mutex_init(&dev->adc_lock);
/* Register as power supply */
dev->bat_psd.name = "adc-div-battery";
dev->bat_psd.type = POWER_SUPPLY_TYPE_BATTERY;
dev->bat_psd.get_property = adc_div_bat_get_property;
dev->bat_psd.properties = adc_div_bat_props;
dev->bat_psd.num_properties = ARRAY_SIZE(adc_div_bat_props);
dev->bat_ps = devm_power_supply_register(&pdev->dev, &dev->bat_psd,
NULL);
if (IS_ERR(dev->bat_ps)) {
dev->bat_ps = NULL;
return PTR_ERR(dev->bat_ps);
}
dev->chan = iio_channel_get(&pdev->dev, "vbat");
if (IS_ERR(dev->chan)) {
dev_err(&pdev->dev, "Couldn't get ADC channel error %ld \n", PTR_ERR(dev->chan));
if (PTR_ERR(dev->chan) == -ENODEV) {
return -EPROBE_DEFER;
} else {
return PTR_ERR(dev->chan);
}
}
//FIXME: this isn't supported by mainline linux at 3.14
//iio_channel_write(dev->chan, 12453, 0, IIO_CHAN_INFO_SAMP_FREQ);
return 0;
}
static int adc_div_remove(struct platform_device *pdev)
{
struct adc_div_device *dev = platform_get_drvdata(pdev);
iio_channel_release(dev->chan);
return 0;
}
static const struct of_device_id adc_div_match[] = {
{ .compatible = "adc-div-battery", },
{ },
};
static struct platform_driver adc_div_driver = {
.probe = adc_div_probe,
.remove = adc_div_remove,
.driver = {
.name = "adc-div-battery",
.of_match_table = adc_div_match,
},
};
module_platform_driver(adc_div_driver);
MODULE_AUTHOR("Tim Kryger <tkryger@nestlabs.com>");
MODULE_DESCRIPTION("Generic ADC Divider Battery driver");
MODULE_LICENSE("GPL v2");