| /* |
| * 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"); |