| /* |
| * Copyright (C) 2017, Teknique Ltd. |
| * Driver for battery and charger on Nest Rose Quartz |
| * |
| * 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; either version 2 of the License, or (at your |
| * option) any later version. |
| * |
| * You should have received a copy of the GNU General Public License along |
| * with this program; if not, write to the Free Software Foundation, Inc., |
| * 675 Mass Ave, Cambridge, MA 02139, USA. |
| * |
| */ |
| |
| /* to enable debugging logs -- load module, then run this: |
| * echo 1 > /sys/devices/battery.7/power_supply/rq_battery/debug_enabled |
| */ |
| |
| #include <linux/device.h> |
| #include <linux/gpio.h> |
| #include <linux/init.h> |
| #include <linux/interrupt.h> |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/platform_device.h> |
| #include <linux/power_supply.h> |
| #include <linux/slab.h> |
| #include <linux/timer.h> |
| #include <linux/of_gpio.h> |
| #include <plat/adc.h> |
| #include <linux/delay.h> |
| |
| #include <linux/power/nest_rq_battery.h> |
| |
| struct ntc_compensation { |
| int temp_c; |
| unsigned int ohm; |
| }; |
| |
| static const struct ntc_compensation ntc_table[] = { |
| { .temp_c = -40, .ohm = 188500 }, |
| { .temp_c = -35, .ohm = 144290 }, |
| { .temp_c = -30, .ohm = 111330 }, |
| { .temp_c = -25, .ohm = 86560 }, |
| { .temp_c = -20, .ohm = 67790 }, |
| { .temp_c = -15, .ohm = 53460 }, |
| { .temp_c = -10, .ohm = 42450 }, |
| { .temp_c = -5, .ohm = 33930 }, |
| { .temp_c = 0, .ohm = 27280 }, |
| { .temp_c = 5, .ohm = 22070 }, |
| { .temp_c = 10, .ohm = 17960 }, |
| { .temp_c = 15, .ohm = 14700 }, |
| { .temp_c = 20, .ohm = 12090 }, |
| { .temp_c = 25, .ohm = 10000 }, |
| { .temp_c = 30, .ohm = 8310 }, |
| { .temp_c = 35, .ohm = 6940 }, |
| { .temp_c = 40, .ohm = 5830 }, |
| { .temp_c = 45, .ohm = 4910 }, |
| { .temp_c = 50, .ohm = 4160 }, |
| { .temp_c = 55, .ohm = 3540 }, |
| { .temp_c = 60, .ohm = 3020 }, |
| { .temp_c = 65, .ohm = 2590 }, |
| { .temp_c = 70, .ohm = 2230 }, |
| { .temp_c = 75, .ohm = 1920 }, |
| { .temp_c = 80, .ohm = 1670 }, |
| { .temp_c = 85, .ohm = 1450 }, |
| { .temp_c = 90, .ohm = 1270 }, |
| { .temp_c = 95, .ohm = 1110 }, |
| { .temp_c = 100, .ohm = 975 }, |
| { .temp_c = 105, .ohm = 860 }, |
| { .temp_c = 110, .ohm = 760 }, |
| { .temp_c = 115, .ohm = 674 }, |
| { .temp_c = 120, .ohm = 599 }, |
| { .temp_c = 125, .ohm = 534 }, |
| }; |
| |
| |
| static int nest_rq_battery_sysfs_init(struct nest_rq_battery *batt); |
| |
| //4.35V and 4.10V, converted to uV |
| const int VMAX_SETTINGS[] = {410 * 10000, 435 * 10000}; |
| static int high_vmax = 1; |
| static struct timer_list update_timer; |
| static int vbridge_voltage = 0; |
| static int charger_enabled = 1; |
| static int vbridge_disabled = 0; |
| static int heater_enabled = 0; |
| static int debug_enabled = 0; |
| static int nlmodel; |
| |
| struct nest_rq_battery *nest_rq_battery; |
| EXPORT_SYMBOL(nest_rq_battery); |
| |
| #define UPDATE_TIMER_MS 10 * 1000 //10 Seconds |
| #define VBRIDGE_WAIT_US 8*1000 //Disconnect vbridge for 8ms before reading VIN |
| #define CHARGER_WAIT_US 8*1000 //Disconnect charger for 8ms before reading voltage |
| #define DEAD_BATTERY_UV 3400 * 1000 //3.4v in uV |
| #define VBRIDGE_SAMPLES_NUM 15 |
| // T=1/HZ AC=60Hz, T=0.016 sec, sample rate = 10, => 1.6ms wait period |
| #define VBRIDGE_SAMPLE_WAIT_US 1600 |
| #define DEFAULT_DOUBLE_HEATER_HW_VERSION 10 |
| |
| #define DEBUG(d, fmt, ...) \ |
| do {\ |
| if (debug_enabled)\ |
| {\ |
| dev_info((d), (fmt), ##__VA_ARGS__);\ |
| }\ |
| } while(0) |
| |
| static inline struct nest_rq_battery *battery_to_nest_rq_battery(struct power_supply *psy) |
| { |
| return container_of(psy, struct nest_rq_battery, battery); |
| } |
| |
| static inline struct nest_rq_battery *charger_to_nest_rq_battery(struct power_supply *psy) |
| { |
| return container_of(psy, struct nest_rq_battery, charger); |
| } |
| |
| static inline int adc_to_voltage(int raw_adc) |
| { |
| const int REF_MV = 3300; //3v3 Reference |
| const int ADC_RES = 12; //12 bit ADC |
| return (raw_adc * REF_MV) >> ADC_RES; |
| } |
| |
| void vbridge_disable(struct nest_rq_battery *batt, int state) |
| { |
| dev_dbg(batt->charger.dev, "disable VBRIDGE: %s", state?"true":"false"); |
| vbridge_disabled = state; |
| gpio_set_value(batt->pdata->vbridge_disable_gpio, state); |
| }; |
| |
| static int read_battery_voltage(struct nest_rq_battery * pbatt) |
| { |
| int adc; |
| int volts; |
| |
| adc = ambarella_adc_read_level(pbatt->pdata->volt_adc_chan); |
| if (adc < 0) |
| { |
| dev_err(pbatt->dev, "error %d reading adc channel %d\n", adc, pbatt->pdata->volt_adc_chan); |
| return -1; |
| } |
| |
| volts = adc_to_voltage(adc); |
| |
| //ADC reading is volts * 0.68, |
| //Result should be in uV |
| return volts * 1000 * 100 / 68; |
| } |
| |
| static int read_vbridge_voltage(struct nest_rq_battery *batt) |
| { |
| int adc; |
| int volts = -1; |
| int i; |
| |
| // sample the AC half weave and pick highest value |
| for (i=0; i< VBRIDGE_SAMPLES_NUM; i++) |
| { |
| int cur_volts = 0; |
| adc = ambarella_adc_read_level(batt->pdata->vbridge_adc_chan); |
| if (adc < 0) |
| { |
| dev_err(batt->dev, "error %d reading adc channel %d\n", adc, batt->pdata->vbridge_adc_chan); |
| return -1; |
| } |
| |
| cur_volts = adc_to_voltage(adc); |
| //vbridge voltage is volts * 10 / 1.414 |
| cur_volts = cur_volts * 10000 / 1414; |
| |
| if (cur_volts > volts) |
| volts = cur_volts; |
| |
| usleep_range(VBRIDGE_SAMPLE_WAIT_US, VBRIDGE_SAMPLE_WAIT_US); |
| } |
| |
| //we turn the charger and heater off if voltsn is < 10V |
| if (volts < 10000 && volts >= 0) |
| { |
| dev_dbg(batt->dev, " vbridge voltage (%d) < 10.000V. Disable heater\n", volts); |
| heater_enabled = 0; |
| |
| gpio_set_value(batt->pdata->heater_enable_gpio, heater_enabled); |
| } |
| |
| return volts; |
| } |
| |
| |
| static inline u64 div64_u64_safe(u64 dividend, u64 divisor) |
| { |
| if (divisor == 0 && dividend == 0) |
| return 0; |
| if (divisor == 0) |
| return UINT_MAX; |
| return div64_u64(dividend, divisor); |
| } |
| |
| static int ntc_get_ohm(int uv) |
| { |
| const int PULLUP_MV = 4500; //4.5v |
| const int PULLUP_OHM = 4870; |
| const int PULLDOWN_OHM = 13700; |
| |
| u64 v = uv; |
| u64 pmv = PULLUP_MV; |
| u64 n, puo, pdo; |
| puo = PULLUP_OHM; |
| pdo = PULLDOWN_OHM; |
| |
| n = div64_u64_safe(pdo * puo * v, pdo * (pmv - v) - puo * v); |
| |
| if (n > INT_MAX) |
| n = INT_MAX; |
| |
| return n; |
| } |
| |
| static void lookup_comp(unsigned int ohm,int *i_low, int *i_high) |
| { |
| int start, end, mid; |
| int n_comp = ARRAY_SIZE(ntc_table); |
| /* |
| * Handle special cases: Resistance is higher than or equal to |
| * resistance in first table entry, or resistance is lower or equal |
| * to resistance in last table entry. |
| * In these cases, return i_low == i_high, either pointing to the |
| * beginning or to the end of the table depending on the condition. |
| */ |
| if (ohm >= ntc_table[0].ohm) { |
| *i_low = 0; |
| *i_high = 0; |
| return; |
| } |
| if (ohm <= ntc_table[n_comp - 1].ohm) { |
| *i_low = n_comp - 1; |
| *i_high = n_comp - 1; |
| return; |
| } |
| |
| /* Do a binary search on compensation table */ |
| start = 0; |
| end = n_comp; |
| while (start < end) { |
| mid = start + (end - start) / 2; |
| /* |
| * start <= mid < end |
| * ntc_table[start].ohm > ohm >= ntc_table[end].ohm |
| * |
| * We could check for "ohm == ntc_table[mid].ohm" here, but |
| * that is a quite unlikely condition, and we would have to |
| * check again after updating start. Check it at the end instead |
| * for simplicity. |
| */ |
| if (ohm >= ntc_table[mid].ohm) { |
| end = mid; |
| } else { |
| start = mid + 1; |
| /* |
| * ohm >= ntc_table[start].ohm might be true here, |
| * since we set start to mid + 1. In that case, we are |
| * done. We could keep going, but the condition is quite |
| * likely to occur, so it is worth checking for it. |
| */ |
| if (ohm >= ntc_table[start].ohm) |
| end = start; |
| } |
| /* |
| * start <= end |
| * ntc_table[start].ohm >= ohm >= ntc_table[end].ohm |
| */ |
| } |
| /* |
| * start == end |
| * ohm >= ntc_table[end].ohm |
| */ |
| *i_low = end; |
| if (ohm == ntc_table[end].ohm) |
| *i_high = end; |
| else |
| *i_high = end - 1; |
| } |
| |
| static int get_temp_mc(unsigned int ohm) |
| { |
| int low, high; |
| int temp; |
| |
| lookup_comp(ohm, &low, &high); |
| if (low == high) { |
| /* Unable to use linear approximation */ |
| temp = ntc_table[low].temp_c * 10; |
| } else { |
| temp = ntc_table[low].temp_c * 10 + |
| ((ntc_table[high].temp_c - ntc_table[low].temp_c) * |
| 10 * ((int)ohm - (int)ntc_table[low].ohm)) / |
| ((int)ntc_table[high].ohm - (int)ntc_table[low].ohm); |
| } |
| return temp; |
| } |
| |
| static int read_battery_temp(struct nest_rq_battery *pbatt, int* temp) |
| { |
| int adc; |
| int volts; |
| int ohms; |
| int mc; |
| int n_comp = ARRAY_SIZE(ntc_table); |
| unsigned long flags; |
| |
| if (!temp) { |
| dev_err(pbatt->dev, "temp var is NULL."); |
| return -1; |
| } |
| |
| spin_lock_irqsave(&pbatt->lock, flags); |
| // ACOK like is read two times (before and after reading ADC) and |
| // ADC value will be discarded if ACOK is high. |
| if (vbridge_disabled || gpio_get_value(pbatt->pdata->acok_gpio)) |
| { |
| spin_unlock_irqrestore(&pbatt->lock, flags); |
| dev_err(pbatt->dev, "VBRIDGE is disabled or AC is not present. Can't read battery temperature."); |
| return -1; |
| } |
| |
| adc = ambarella_adc_read_level(pbatt->pdata->temp_adc_chan); |
| spin_unlock_irqrestore(&pbatt->lock, flags); |
| |
| if (gpio_get_value(pbatt->pdata->acok_gpio)) |
| { |
| dev_warn(pbatt->dev, "AC is not present. Can't read battery temperature."); |
| return -1; |
| } |
| |
| if (adc < 0) |
| { |
| dev_err(pbatt->dev, "error %d reading adc channel %d\n", adc, pbatt->pdata->temp_adc_chan); |
| return -1; |
| } |
| |
| volts = adc_to_voltage(adc); |
| ohms = ntc_get_ohm(volts); |
| |
| if (ohms < ntc_table[n_comp - 1].ohm) |
| { |
| dev_warn(pbatt->dev, "wrong ohms value (%d) getting from adc (%d). Skip it.\n", ohms, adc); |
| return -1; |
| } |
| |
| mc = get_temp_mc(ohms); |
| |
| DEBUG(pbatt->dev, " temp_adc %d volts: %d ohms: %d mc:%d\n", adc, volts, ohms, mc); |
| |
| *temp = mc; |
| |
| return 0; |
| } |
| |
| static void update_timer_func(unsigned long pbat) |
| { |
| struct nest_rq_battery* pbatt = (struct nest_rq_battery *) pbat; |
| const struct nest_rq_battery_platform_data *pdata = pbatt->pdata; |
| |
| // check temperature |
| int temp; |
| int ret = read_battery_temp(pbatt, &temp); |
| |
| if (!ret) |
| { |
| int fast_charge = 0; |
| charger_enabled = 1; |
| |
| // Enable/Disable heater based on temperature |
| // heater use depends on VIN, so it is enabled/disabled by vbridge timer |
| if (temp < 250) //heater below 25C |
| { |
| DEBUG(pbatt->dev, " Temperature is below 25C: %d - enabling heater\n", temp); |
| if (vbridge_voltage > 10000) // > 10.000V |
| heater_enabled = 1; |
| } |
| else if (temp > 270) //heater off above 27C |
| { |
| DEBUG(pbatt->dev, " Temperature is above 27C: %d - disabling heater\n", temp); |
| heater_enabled = 0; |
| } |
| |
| // Enable/Disable charging/fast charging based on temperature |
| if (temp <= 0 || temp >= 600) |
| { |
| DEBUG(pbatt->dev, " temperature is outside safe realm: %d - disabling charger\n", temp); |
| charger_enabled = 0; |
| fast_charge = 0; |
| } |
| else if (temp <= 100) // 0-10C, trickle charge |
| { |
| DEBUG(pbatt->dev, " temperature is below 10C: %d - disabling fast_charge\n", temp); |
| fast_charge = 0; |
| } |
| else if (temp >= 150 && temp <= 400) // 15-40C, fast charge |
| { |
| DEBUG(pbatt->dev, " temperature in sweet spot between 150 and 400 - %d - fast charge enabled\n", temp); |
| fast_charge = 1; |
| } |
| else if (temp >= 450 && temp <= 600) // 45-60C, trickle charge |
| { |
| DEBUG(pbatt->dev, " temperature between 45 and 60 %d - trickle charging\n", temp); |
| fast_charge = 0; |
| } |
| |
| // Set battery charging voltage based on temperature |
| if (temp < 100) // < 10C, 4.1V |
| { |
| DEBUG(pbatt->dev, " temperature is below 10C: %d - set charging voltage to 4.1V\n", temp); |
| high_vmax = 0; |
| } |
| else if (temp > 150 && temp < 250) // 15-25C, 4.35V |
| { |
| DEBUG(pbatt->dev, " temperature is between 15C and 40C: %d - set charging voltage to 4.35V\n", temp); |
| high_vmax = 1; |
| } |
| else if (temp > 300) // > 30C, 4.1V |
| { |
| DEBUG(pbatt->dev, " temperature is above 30C: %d - set charging voltage to 4.1V\n", temp); |
| high_vmax = 0; |
| } |
| // For 10-15C and 25-30C, keep previous charging voltage |
| |
| // For newer hardware version limit the maximum voltage to 4.1V, |
| |
| if (nlmodel >= DEFAULT_DOUBLE_HEATER_HW_VERSION) |
| { |
| DEBUG(pbatt->dev, " HW version needs voltage up to 4.1V only\n"); |
| high_vmax = 0; |
| } |
| |
| gpio_set_value(pdata->charger_rate_gpio, fast_charge); |
| gpio_set_value(pdata->vmax_gpio, high_vmax); |
| gpio_set_value(pdata->heater_enable_gpio, heater_enabled); |
| |
| gpio_set_value(pdata->charger_enable_gpio, charger_enabled); |
| } |
| |
| mod_timer(&update_timer, jiffies + msecs_to_jiffies(UPDATE_TIMER_MS)); |
| } |
| |
| static enum power_supply_property nest_rq_battery_properties[] = { |
| POWER_SUPPLY_PROP_STATUS, |
| POWER_SUPPLY_PROP_TEMP, |
| POWER_SUPPLY_PROP_VOLTAGE_NOW, |
| }; |
| |
| static enum power_supply_property nest_rq_charger_properties[] = { |
| POWER_SUPPLY_PROP_CHARGE_TYPE, |
| POWER_SUPPLY_PROP_VOLTAGE_MAX, |
| }; |
| |
| static int nest_rq_battery_get_property(struct power_supply *psy, |
| enum power_supply_property psp, union power_supply_propval *val) |
| { |
| struct nest_rq_battery *battery = battery_to_nest_rq_battery(psy); |
| const struct nest_rq_battery_platform_data *pdata = battery->pdata; |
| int ret = 0; |
| |
| switch (psp) { |
| case POWER_SUPPLY_PROP_STATUS: |
| val->intval = gpio_get_value(pdata->status_gpio) ? POWER_SUPPLY_STATUS_NOT_CHARGING : POWER_SUPPLY_STATUS_CHARGING; |
| break; |
| case POWER_SUPPLY_PROP_TEMP: |
| if (read_battery_temp(battery, &val->intval)) |
| ret = -EINVAL; |
| break; |
| case POWER_SUPPLY_PROP_VOLTAGE_NOW: |
| val->intval = read_battery_voltage(battery); |
| break; |
| default: |
| ret = -EINVAL; |
| } |
| |
| return ret; |
| } |
| |
| static int nest_rq_charger_get_property(struct power_supply *psy, |
| enum power_supply_property psp, union power_supply_propval *val) |
| { |
| struct nest_rq_battery *battery = charger_to_nest_rq_battery(psy); |
| const struct nest_rq_battery_platform_data *pdata = battery->pdata; |
| |
| switch (psp) { |
| case POWER_SUPPLY_PROP_CHARGE_TYPE: |
| val->intval = gpio_get_value(pdata->charger_rate_gpio) ? |
| POWER_SUPPLY_CHARGE_TYPE_FAST : POWER_SUPPLY_CHARGE_TYPE_TRICKLE; |
| break; |
| case POWER_SUPPLY_PROP_VOLTAGE_MAX: |
| val->intval = VMAX_SETTINGS[gpio_get_value(pdata->vmax_gpio)]; |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| int nest_rq_charger_set_property(struct power_supply *psy, |
| enum power_supply_property psp, |
| const union power_supply_propval *val) |
| { |
| struct nest_rq_battery *battery = charger_to_nest_rq_battery(psy); |
| const struct nest_rq_battery_platform_data *pdata = battery->pdata; |
| |
| switch (psp) { |
| case POWER_SUPPLY_PROP_VOLTAGE_MAX: |
| if (val->intval == VMAX_SETTINGS[0]) |
| gpio_set_value(pdata->vmax_gpio, 0); |
| else if (val->intval == VMAX_SETTINGS[1]) |
| gpio_set_value(pdata->vmax_gpio, 1); |
| else |
| return -EINVAL; |
| break; |
| case POWER_SUPPLY_PROP_CHARGE_TYPE: |
| if (val->intval == 0) |
| gpio_set_value(pdata->charger_rate_gpio, 0); |
| else if (val->intval == 1) |
| gpio_set_value(pdata->charger_rate_gpio, 1); |
| else |
| return -EINVAL; |
| break; |
| default: |
| return -EPERM; |
| } |
| |
| return 0; |
| } |
| |
| |
| int nest_rq_battery_property_is_writeable(struct power_supply *psy, |
| enum power_supply_property psp) |
| { |
| return 0; |
| } |
| |
| int nest_rq_charger_property_is_writeable(struct power_supply *psy, |
| enum power_supply_property psp) |
| { |
| switch (psp) { |
| case POWER_SUPPLY_PROP_CHARGE_TYPE: |
| case POWER_SUPPLY_PROP_VOLTAGE_MAX: |
| return 1; |
| |
| default: |
| break; |
| } |
| |
| return 0; |
| } |
| |
| struct nest_rq_battery_platform_data *nest_rq_battery_of_populate_pdata(struct device *dev, |
| struct device_node *np) |
| { |
| struct nest_rq_battery_platform_data *pdata; |
| dev_info(dev, " module populate pdata\n"); |
| pdata = devm_kzalloc(dev, sizeof(*pdata), GFP_KERNEL); |
| if (!pdata) |
| return ERR_PTR(-ENOMEM); |
| |
| pdata->disconnect_gpio = of_get_named_gpio(np, "disconnect-gpio", 0); |
| pdata->status_gpio = of_get_named_gpio(np, "status-gpio", 0); |
| pdata->vmax_gpio = of_get_named_gpio(np, "vmax-gpio", 0); |
| pdata->adc_enable_gpio = of_get_named_gpio(np, "adc-enable-gpio", 0); |
| pdata->charger_enable_gpio = of_get_named_gpio(np, "charger-enable-gpio", 0); |
| pdata->charger_rate_gpio = of_get_named_gpio(np, "charger-rate-gpio", 0); |
| pdata->heater_enable_gpio = of_get_named_gpio(np, "heater-enable-gpio", 0); |
| pdata->vbridge_disable_gpio = of_get_named_gpio(np, "vbridge-disable-gpio", 0); |
| pdata->acok_gpio = of_get_named_gpio(np, "acok-gpio", 0); |
| |
| of_property_read_u32(np, "temp-adc", &pdata->temp_adc_chan); |
| of_property_read_u32(np, "voltage-adc", &pdata->volt_adc_chan); |
| of_property_read_u32(np, "vbridge-adc", &pdata->vbridge_adc_chan); |
| |
| return pdata; |
| } |
| EXPORT_SYMBOL_GPL(nest_rq_battery_of_populate_pdata); |
| |
| static int nest_rq_battery_probe(struct platform_device *pdev) |
| { |
| struct nest_rq_battery_platform_data *pdata = pdev->dev.platform_data; |
| struct device_node *np = pdev->dev.of_node; |
| struct power_supply *battery; |
| struct power_supply *charger; |
| int ret; |
| |
| dev_info(&pdev->dev, " module probe\n"); |
| if (!pdata) { |
| if (np) { |
| pdata = nest_rq_battery_of_populate_pdata(&pdev->dev, np); |
| if (IS_ERR(pdata)) |
| return PTR_ERR(pdata); |
| } else { |
| dev_err(&pdev->dev, "no platform data\n"); |
| return -EINVAL; |
| } |
| } |
| |
| if (!gpio_is_valid(pdata->disconnect_gpio)) { |
| dev_err(&pdev->dev, "Invalid disconnect_gpio pin\n"); |
| return -EINVAL; |
| } |
| |
| if (!gpio_is_valid(pdata->status_gpio)) { |
| dev_err(&pdev->dev, "Invalid status_gpio pin\n"); |
| return -EINVAL; |
| } |
| |
| if (!gpio_is_valid(pdata->vmax_gpio)) { |
| dev_err(&pdev->dev, "Invalid vmax_gpio pin\n"); |
| return -EINVAL; |
| } |
| |
| if (!gpio_is_valid(pdata->adc_enable_gpio)) { |
| dev_err(&pdev->dev, "Invalid vmax_gpio pin\n"); |
| return -EINVAL; |
| } |
| |
| if (!gpio_is_valid(pdata->charger_enable_gpio)) { |
| dev_err(&pdev->dev, "Invalid charger_enable_gpio pin\n"); |
| return -EINVAL; |
| } |
| |
| if (!gpio_is_valid(pdata->charger_rate_gpio)) { |
| dev_err(&pdev->dev, "Invalid charger_rate_gpio pin\n"); |
| return -EINVAL; |
| } |
| |
| if (!gpio_is_valid(pdata->heater_enable_gpio)) { |
| dev_err(&pdev->dev, "Invalid heater_enable_gpio pin\n"); |
| return -EINVAL; |
| } |
| |
| if (!gpio_is_valid(pdata->vbridge_disable_gpio)) { |
| dev_err(&pdev->dev, "Invalid vbridge_disable_gpio pin\n"); |
| return -EINVAL; |
| } |
| |
| if (!gpio_is_valid(pdata->acok_gpio)) { |
| dev_err(&pdev->dev, "Invalid acok_gpio pin\n"); |
| return -EINVAL; |
| } |
| |
| nest_rq_battery = devm_kzalloc(&pdev->dev, sizeof(*nest_rq_battery), |
| GFP_KERNEL); |
| if (!nest_rq_battery) { |
| dev_err(&pdev->dev, "Failed to alloc driver structure\n"); |
| return -ENOMEM; |
| } |
| |
| spin_lock_init(&nest_rq_battery->lock); |
| |
| nest_rq_battery->dev = &pdev->dev; |
| |
| battery = &nest_rq_battery->battery; |
| battery->name = pdata->name ? pdata->name : "rq_battery"; |
| battery->type = POWER_SUPPLY_TYPE_BATTERY; |
| battery->properties = nest_rq_battery_properties; |
| battery->num_properties = ARRAY_SIZE(nest_rq_battery_properties); |
| battery->get_property = nest_rq_battery_get_property; |
| battery->property_is_writeable = nest_rq_battery_property_is_writeable; |
| battery->supplied_to = pdata->supplied_to; |
| battery->num_supplicants = pdata->num_supplicants; |
| |
| charger = &nest_rq_battery->charger; |
| charger->name = pdata->name ? pdata->name : "rq_charger"; |
| charger->type = POWER_SUPPLY_TYPE_UNKNOWN; |
| charger->properties = nest_rq_charger_properties; |
| charger->num_properties = ARRAY_SIZE(nest_rq_charger_properties); |
| charger->get_property = nest_rq_charger_get_property; |
| charger->set_property = nest_rq_charger_set_property; |
| charger->property_is_writeable = nest_rq_charger_property_is_writeable; |
| charger->supplied_to = pdata->supplied_to; |
| charger->num_supplicants = pdata->num_supplicants; |
| |
| ret = gpio_request(pdata->disconnect_gpio, dev_name(&pdev->dev)); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to request disconnect_gpio pin: %d\n", ret); |
| goto err_gpio_free; |
| } |
| ret = gpio_request(pdata->status_gpio, dev_name(&pdev->dev)); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to request status_gpio pin: %d\n", ret); |
| goto err_gpio_free; |
| } |
| ret = gpio_request(pdata->vmax_gpio, dev_name(&pdev->dev)); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to request vmax_gpio: %d\n", ret); |
| goto err_gpio_free; |
| } |
| ret = gpio_request(pdata->adc_enable_gpio, dev_name(&pdev->dev)); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to request adc_enable_gpio: %d\n", ret); |
| goto err_gpio_free; |
| } |
| ret = gpio_request(pdata->charger_enable_gpio, dev_name(&pdev->dev)); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to request charger_enable_gpio: %d\n", ret); |
| goto err_gpio_free; |
| } |
| ret = gpio_request(pdata->charger_rate_gpio, dev_name(&pdev->dev)); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to request charger_rate_gpio: %d\n", ret); |
| goto err_gpio_free; |
| } |
| ret = gpio_request(pdata->heater_enable_gpio, dev_name(&pdev->dev)); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to request heater_enable_gpio: %d\n", ret); |
| goto err_gpio_free; |
| } |
| ret = gpio_request(pdata->vbridge_disable_gpio, dev_name(&pdev->dev)); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to request vbridge_disable_gpio: %d\n", ret); |
| goto err_gpio_free; |
| } |
| ret = gpio_request(pdata->acok_gpio, dev_name(&pdev->dev)); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to request acok_gpio: %d\n", ret); |
| goto err_gpio_free; |
| } |
| |
| ret = gpio_direction_input(pdata->status_gpio); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to set status_gpio to input: %d\n", ret); |
| goto err_gpio_free; |
| } |
| ret = gpio_direction_output(pdata->disconnect_gpio, 0); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to set disconnect_gpio to output: %d\n", ret); |
| goto err_gpio_free; |
| } |
| ret = gpio_direction_output(pdata->vmax_gpio, 0); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to set vmax_gpio to output: %d\n", ret); |
| goto err_gpio_free; |
| } |
| ret = gpio_direction_output(pdata->adc_enable_gpio, 1); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to set adc_enable_gpio to output: %d\n", ret); |
| goto err_gpio_free; |
| } |
| ret = gpio_direction_output(pdata->charger_enable_gpio, 0); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to set charger_enable_gpio to output: %d\n", ret); |
| goto err_gpio_free; |
| } |
| ret = gpio_direction_output(pdata->charger_rate_gpio, 0); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to set charger_rate_gpio to output: %d\n", ret); |
| goto err_gpio_free; |
| } |
| ret = gpio_direction_output(pdata->heater_enable_gpio, 0); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to set heater_enable_gpio to output: %d\n", ret); |
| goto err_gpio_free; |
| } |
| ret = gpio_direction_output(pdata->vbridge_disable_gpio, 0); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to set vbridge_disable_gpio to output: %d\n", ret); |
| goto err_gpio_free; |
| } |
| ret = gpio_direction_output(pdata->acok_gpio, 0); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to set acok_gpio to output: %d\n", ret); |
| goto err_gpio_free; |
| } |
| |
| nest_rq_battery->pdata = pdata; |
| |
| ret = power_supply_register(&pdev->dev, battery); |
| if (ret < 0) { |
| dev_err(&pdev->dev, "Failed to register power supply: %d\n", |
| ret); |
| goto err_gpio_free; |
| } |
| |
| ret = power_supply_register(&pdev->dev, charger); |
| if (ret < 0) { |
| dev_err(&pdev->dev, "Failed to register power supply: %d\n", |
| ret); |
| goto err_gpio_free; |
| } |
| |
| platform_set_drvdata(pdev, nest_rq_battery); |
| |
| ret = nest_rq_battery_sysfs_init(nest_rq_battery); |
| if (ret) { |
| dev_err(charger->dev, "failed to create sysfs entries: %d\n", ret); |
| goto err_gpio_free; |
| } |
| |
| setup_timer(&update_timer, update_timer_func, (unsigned long) nest_rq_battery); |
| |
| mod_timer(&update_timer, jiffies + msecs_to_jiffies(UPDATE_TIMER_MS)); |
| |
| //grab first vbridge reading |
| vbridge_voltage = read_vbridge_voltage(nest_rq_battery); |
| update_timer_func((unsigned long) nest_rq_battery); |
| return 0; |
| |
| err_gpio_free: |
| if (gpio_is_valid(pdata->disconnect_gpio)) |
| gpio_free(pdata->disconnect_gpio); |
| |
| if (gpio_is_valid(pdata->status_gpio)) |
| gpio_free(pdata->status_gpio); |
| |
| if (gpio_is_valid(pdata->vmax_gpio)) |
| gpio_free(pdata->vmax_gpio); |
| |
| if (gpio_is_valid(pdata->adc_enable_gpio)) |
| gpio_free(pdata->adc_enable_gpio); |
| |
| if (gpio_is_valid(pdata->charger_enable_gpio)) |
| gpio_free(pdata->charger_enable_gpio); |
| |
| if (gpio_is_valid(pdata->charger_rate_gpio)) |
| gpio_free(pdata->charger_rate_gpio); |
| |
| if (gpio_is_valid(pdata->heater_enable_gpio)) |
| gpio_free(pdata->heater_enable_gpio); |
| |
| if (gpio_is_valid(pdata->vbridge_disable_gpio)) |
| gpio_free(pdata->heater_enable_gpio); |
| |
| if (gpio_is_valid(pdata->acok_gpio)) |
| gpio_free(pdata->acok_gpio); |
| |
| |
| power_supply_unregister(battery); |
| power_supply_unregister(charger); |
| return ret; |
| } |
| |
| /* Custom sysfs entries for battery */ |
| static ssize_t battery_connected_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| const char* CONNECTED_SETTINGS[] = {"Connected", "Disconnected"}; |
| |
| struct nest_rq_battery *batt = dev_get_drvdata(dev->parent); |
| return sprintf(buf, "%s\n", CONNECTED_SETTINGS[gpio_get_value(batt->pdata->disconnect_gpio)]); |
| } |
| |
| static ssize_t battery_connected_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, size_t count) |
| { |
| struct nest_rq_battery *batt = dev_get_drvdata(dev->parent); |
| |
| struct check { |
| char* str; |
| int val; |
| }; |
| |
| //The GPIO is active high (high == disconnected), so the .val's here are inverted |
| struct check check_values[] = { |
| {.str = "0", .val = 1}, |
| {.str = "1", .val = 0}, |
| }; |
| |
| int i; |
| |
| dev_info(dev, " setting battery_connected"); |
| for (i = 0; i < ARRAY_SIZE(check_values); i++) |
| { |
| struct check chk = check_values[i]; |
| if (sysfs_streq(buf, chk.str)) |
| { |
| gpio_set_value(batt->pdata->disconnect_gpio, chk.val); |
| return count; |
| } |
| } |
| |
| dev_warn(dev, "Invalid state, must be one of: 0,1"); |
| return count; |
| } |
| |
| /* Custom sysfs entries for battery */ |
| static ssize_t heater_enabled_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| const char* CONNECTED_SETTINGS[] = {"Disabled", "Enabled"}; |
| |
| struct nest_rq_battery *batt = dev_get_drvdata(dev->parent); |
| return sprintf(buf, "%s\n", CONNECTED_SETTINGS[gpio_get_value(batt->pdata->heater_enable_gpio)]); |
| } |
| |
| static ssize_t heater_enabled_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, size_t count) |
| { |
| struct nest_rq_battery *batt = dev_get_drvdata(dev->parent); |
| heater_enabled = 1; |
| if (sysfs_streq(buf, "0")) |
| heater_enabled = 0; |
| |
| dev_info(dev, " setting heater_enabled:%s\n", heater_enabled ? "true" : "false"); |
| gpio_set_value(batt->pdata->heater_enable_gpio, heater_enabled); |
| |
| return count; |
| } |
| |
| /* Custom sysfs entries for battery */ |
| static ssize_t charger_enabled_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| const char* ENABLE_SETTINGS[] = {"Disabled", "Enabled"}; |
| |
| struct nest_rq_battery *batt = dev_get_drvdata(dev->parent); |
| return sprintf(buf, "%s\n", ENABLE_SETTINGS[gpio_get_value(batt->pdata->charger_enable_gpio)]); |
| } |
| |
| static ssize_t charger_enabled_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, size_t count) |
| { |
| struct nest_rq_battery *batt = dev_get_drvdata(dev->parent); |
| |
| struct check { |
| char* str; |
| int val; |
| }; |
| |
| struct check check_values[] = { |
| {.str = "0", .val = 0}, |
| {.str = "1", .val = 1}, |
| }; |
| |
| int i; |
| for (i = 0; i < ARRAY_SIZE(check_values); i++) |
| { |
| struct check chk = check_values[i]; |
| if (sysfs_streq(buf, chk.str)) |
| { |
| gpio_set_value(batt->pdata->charger_enable_gpio, chk.val); |
| charger_enabled = chk.val; |
| dev_info(dev, " setting charger_enabled:%s\n", |
| charger_enabled ? "true" : "false"); |
| return count; |
| } |
| } |
| |
| dev_warn(dev, "Invalid state, must be one of: 0,1"); |
| |
| return count; |
| } |
| |
| static ssize_t vbridge_enabled_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| const char* ENABLE_SETTINGS[] = {"Enabled", "Disabled"}; |
| |
| struct nest_rq_battery *batt = dev_get_drvdata(dev->parent); |
| return sprintf(buf, "%s\n", ENABLE_SETTINGS[gpio_get_value(batt->pdata->vbridge_disable_gpio)]); |
| } |
| |
| static ssize_t vbridge_enabled_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, size_t count) |
| { |
| struct nest_rq_battery *batt = dev_get_drvdata(dev->parent); |
| |
| struct check { |
| char* str; |
| int val; |
| }; |
| |
| //The GPIO is for 'vbridge_disabled', so the .val's here are inverted |
| struct check check_values[] = { |
| {.str = "0", .val = 1}, |
| {.str = "1", .val = 0}, |
| }; |
| |
| int i; |
| for (i = 0; i < ARRAY_SIZE(check_values); i++) |
| { |
| struct check chk = check_values[i]; |
| if (sysfs_streq(buf, chk.str)) |
| { |
| nest_rq_battery_vbridge_disable(batt, chk.val); |
| dev_info(dev, " setting vbridge_enabled:%s\n", |
| vbridge_disabled ? "false" : "true"); |
| return count; |
| } |
| } |
| |
| dev_warn(dev, "Invalid state, must be one of: 0,1"); |
| |
| return count; |
| } |
| |
| static ssize_t vbridge_voltage_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| struct nest_rq_battery *batt = dev_get_drvdata(dev->parent); |
| |
| vbridge_voltage = read_vbridge_voltage(batt); |
| |
| return sprintf(buf, "%d\n", vbridge_voltage); |
| } |
| |
| static ssize_t debug_enabled_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, size_t count) |
| { |
| debug_enabled = 1; |
| if (sysfs_streq(buf, "0")) |
| debug_enabled = 0; |
| dev_info(dev, " setting debug_enabled:%s\n", debug_enabled ? "true" : " false"); |
| |
| return count; |
| } |
| |
| static ssize_t debug_enabled_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| return sprintf(buf, "%d\n", debug_enabled); |
| } |
| |
| static ssize_t nlmodel_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| return sprintf(buf, "%d\n", nlmodel); |
| } |
| |
| static ssize_t nlmodel_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, size_t count) |
| { |
| if (kstrtoint(buf, 0, &nlmodel)) |
| nlmodel = DEFAULT_DOUBLE_HEATER_HW_VERSION; |
| |
| return count; |
| } |
| |
| |
| static DEVICE_ATTR(connected, 0644, battery_connected_show, battery_connected_store); |
| static DEVICE_ATTR(debug_enabled, 0644, debug_enabled_show, debug_enabled_store); |
| static DEVICE_ATTR(heater_enabled, 0644, heater_enabled_show, heater_enabled_store); |
| static DEVICE_ATTR(charger_enabled, 0644, charger_enabled_show, charger_enabled_store); |
| static DEVICE_ATTR(vbridge_enabled, 0644, vbridge_enabled_show, vbridge_enabled_store); |
| static DEVICE_ATTR(vbridge_voltage_now, 0444, vbridge_voltage_show, NULL); |
| static DEVICE_ATTR(nlmodel, 0644, nlmodel_show, nlmodel_store); |
| |
| static int nest_rq_battery_sysfs_init(struct nest_rq_battery *batt) |
| { |
| int ret = 0; |
| |
| dev_info(batt->dev, " initializing sysfs interface\n"); |
| ret = device_create_file(batt->battery.dev, &dev_attr_connected); |
| if (ret < 0) |
| dev_warn(batt->battery.dev, "failed to add battery_connected sysfs: %d\n", ret); |
| |
| ret = device_create_file(batt->battery.dev, &dev_attr_heater_enabled); |
| if (ret < 0) |
| dev_warn(batt->battery.dev, "failed to add heater_enabled sysfs: %d\n", ret); |
| |
| ret = device_create_file(batt->battery.dev, &dev_attr_debug_enabled); |
| if (ret < 0) |
| dev_warn(batt->battery.dev, "failed to add debug_enabled sysfs: %d\n", ret); |
| |
| |
| ret = device_create_file(batt->charger.dev, &dev_attr_charger_enabled); |
| if (ret < 0) |
| dev_warn(batt->charger.dev, "failed to add charger_enabled sysfs: %d\n", ret); |
| |
| ret = device_create_file(batt->charger.dev, &dev_attr_vbridge_voltage_now); |
| if (ret < 0) |
| dev_warn(batt->charger.dev, "failed to add vbridge_voltage sysfs: %d\n", ret); |
| |
| ret = device_create_file(batt->charger.dev, &dev_attr_vbridge_enabled); |
| if (ret < 0) |
| dev_warn(batt->charger.dev, "failed to add vbridge_enabled sysfs: %d\n", ret); |
| |
| ret = device_create_file(batt->charger.dev, &dev_attr_nlmodel); |
| if (ret < 0) |
| dev_warn(batt->charger.dev, "failed to add nlmodel sysfs: %d\n", ret); |
| |
| return 0; |
| } |
| |
| static int nest_rq_battery_remove(struct platform_device *pdev) |
| { |
| struct nest_rq_battery *battery = platform_get_drvdata(pdev); |
| |
| dev_info(&pdev->dev, " module removal\n"); |
| del_timer_sync(&update_timer); |
| |
| power_supply_unregister(&battery->battery); |
| power_supply_unregister(&battery->charger); |
| |
| gpio_free(battery->pdata->disconnect_gpio); |
| gpio_free(battery->pdata->status_gpio); |
| gpio_free(battery->pdata->vmax_gpio); |
| gpio_free(battery->pdata->adc_enable_gpio); |
| gpio_free(battery->pdata->charger_enable_gpio); |
| gpio_free(battery->pdata->charger_rate_gpio); |
| gpio_free(battery->pdata->heater_enable_gpio); |
| gpio_free(battery->pdata->vbridge_disable_gpio); |
| gpio_free(battery->pdata->acok_gpio); |
| |
| platform_set_drvdata(pdev, NULL); |
| |
| return 0; |
| } |
| |
| static const struct of_device_id nest_rq_battery_match[] = { |
| { .compatible = "nest-rq-battery" }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(of, nest_rq_battery_match); |
| |
| static struct platform_driver nest_rq_battery_driver = { |
| .probe = nest_rq_battery_probe, |
| .remove = nest_rq_battery_remove, |
| .driver = { |
| .name = "rq_battery", |
| .owner = THIS_MODULE, |
| .of_match_table = nest_rq_battery_match, |
| }, |
| }; |
| |
| void nest_rq_battery_vbridge_disable(struct nest_rq_battery *batt, int state) |
| { |
| unsigned long flags; |
| |
| spin_lock_irqsave(&batt->lock, flags); |
| vbridge_disable(batt, state); |
| spin_unlock_irqrestore(&batt->lock, flags); |
| }; |
| EXPORT_SYMBOL(nest_rq_battery_vbridge_disable); |
| |
| int nest_rq_battery_get_temp(struct nest_rq_battery *batt, int *temp) |
| { |
| return read_battery_temp(batt, temp); |
| }; |
| EXPORT_SYMBOL(nest_rq_battery_get_temp); |
| |
| int nest_rq_battery_get_voltage(struct nest_rq_battery *batt) |
| { |
| return read_battery_voltage(batt);; |
| }; |
| EXPORT_SYMBOL(nest_rq_battery_get_voltage); |
| |
| module_platform_driver(nest_rq_battery_driver); |
| |
| MODULE_AUTHOR("Jonathan Miles <jonathan.miles@teknique.com>"); |
| MODULE_DESCRIPTION("Driver for battery and charger on Nest Rose Quartz"); |
| MODULE_LICENSE("GPL"); |
| MODULE_ALIAS("platform:nest-rq-battery"); |