/* * Copyright (c) 2014-2018, The Linux Foundation. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 and * only version 2 as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ #define pr_fmt(fmt) "%s:%s " fmt, KBUILD_MODNAME, __func__ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../thermal_core.h" #define BCL_DRIVER_NAME "bcl_peripheral" #define BCL_VBAT_INT "bcl-low-vbat" #define BCL_VLOW_VBAT_INT "bcl-very-low-vbat" #define BCL_CLOW_VBAT_INT "bcl-crit-low-vbat" #define BCL_IBAT_INT "bcl-high-ibat" #define BCL_VHIGH_IBAT_INT "bcl-very-high-ibat" #define BCL_MONITOR_EN 0x46 #define BCL_VBAT_MIN 0x5C #define BCL_IBAT_MAX 0x5D #define BCL_MAX_MIN_CLR 0x48 #define BCL_IBAT_MAX_CLR 3 #define BCL_VBAT_MIN_CLR 2 #define BCL_VBAT_ADC_LOW 0x72 #define BCL_VBAT_COMP_LOW 0x75 #define BCL_VBAT_COMP_TLOW 0x76 #define BCL_IBAT_HIGH 0x78 #define BCL_IBAT_TOO_HIGH 0x79 #define BCL_LMH_CFG 0xA3 #define BCL_CFG 0x6A #define LMH_INT_POL_HIGH 0x12 #define LMH_INT_EN 0x15 #define BCL_VBAT_SCALING 39000 #define BCL_IBAT_SCALING 80 #define BCL_LMH_CFG_VAL 0x3 #define BCL_CFG_VAL 0x81 #define LMH_INT_VAL 0x7 #define BCL_READ_RETRY_LIMIT 3 #define VAL_CP_REG_BUF_LEN 3 #define VAL_REG_BUF_OFFSET 0 #define VAL_CP_REG_BUF_OFFSET 2 #define BCL_STD_VBAT_NR 9 #define BCL_VBAT_NO_READING 127 enum bcl_dev_type { BCL_HIGH_IBAT, BCL_VHIGH_IBAT, BCL_LOW_VBAT, BCL_VLOW_VBAT, BCL_CLOW_VBAT, BCL_SOC_MONITOR, BCL_TYPE_MAX, }; struct bcl_peripheral_data { int irq_num; long int trip_temp; int trip_val; int last_val; struct mutex state_trans_lock; bool irq_enabled; struct thermal_zone_of_device_ops ops; struct thermal_zone_device *tz_dev; }; struct bcl_device { struct regmap *regmap; uint16_t fg_bcl_addr; uint16_t fg_lmh_addr; struct notifier_block psy_nb; struct work_struct soc_eval_work; struct bcl_peripheral_data param[BCL_TYPE_MAX]; }; static struct bcl_device *bcl_perph; static int vbat_low[BCL_STD_VBAT_NR] = { 2400, 2500, 2600, 2700, 2800, 2900, 3000, 3100, 3200}; static int bcl_read_multi_register(int16_t reg_offset, uint8_t *data, int len) { int ret = 0; if (!bcl_perph) { pr_err("BCL device not initialized\n"); return -EINVAL; } ret = regmap_bulk_read(bcl_perph->regmap, (bcl_perph->fg_bcl_addr + reg_offset), data, len); if (ret < 0) { pr_err("Error reading register %d. err:%d", reg_offset, ret); return ret; } return ret; } static int bcl_write_general_register(int16_t reg_offset, uint16_t base, uint8_t data) { int ret = 0; uint8_t *write_buf = &data; if (!bcl_perph) { pr_err("BCL device not initialized\n"); return -EINVAL; } ret = regmap_write(bcl_perph->regmap, (base + reg_offset), *write_buf); if (ret < 0) { pr_err("Error reading register %d. err:%d", reg_offset, ret); return ret; } pr_debug("wrote 0x%02x to 0x%04x\n", data, base + reg_offset); return ret; } static int bcl_write_register(int16_t reg_offset, uint8_t data) { return bcl_write_general_register(reg_offset, bcl_perph->fg_bcl_addr, data); } static void convert_vbat_to_adc_val(int *val) { *val = (*val * 1000) / BCL_VBAT_SCALING; } static void convert_adc_to_vbat_val(int *val) { *val = *val * BCL_VBAT_SCALING / 1000; } static void convert_ibat_to_adc_val(int *val) { *val = *val / BCL_IBAT_SCALING; } static void convert_adc_to_ibat_val(int *val) { *val = *val * BCL_IBAT_SCALING; } static int bcl_set_ibat(void *data, int low, int high) { int ret = 0, ibat_ua, thresh_value; int8_t val = 0; int16_t addr; struct bcl_peripheral_data *bat_data = (struct bcl_peripheral_data *)data; thresh_value = high; if (bat_data->trip_temp == thresh_value) return 0; mutex_lock(&bat_data->state_trans_lock); if (bat_data->irq_num && bat_data->irq_enabled) { disable_irq_nosync(bat_data->irq_num); bat_data->irq_enabled = false; } if (thresh_value == INT_MAX) { bat_data->trip_temp = thresh_value; goto set_trip_exit; } ibat_ua = thresh_value; convert_ibat_to_adc_val(&thresh_value); val = (int8_t)thresh_value; if (&bcl_perph->param[BCL_HIGH_IBAT] == bat_data) { addr = BCL_IBAT_HIGH; pr_debug("ibat high threshold:%d mA ADC:0x%02x\n", ibat_ua, val); } else if (&bcl_perph->param[BCL_VHIGH_IBAT] == bat_data) { addr = BCL_IBAT_TOO_HIGH; pr_debug("ibat too high threshold:%d mA ADC:0x%02x\n", ibat_ua, val); } else { goto set_trip_exit; } ret = bcl_write_register(addr, val); if (ret) { pr_err("Error accessing BCL peripheral. err:%d\n", ret); goto set_trip_exit; } bat_data->trip_temp = ibat_ua; if (bat_data->irq_num && !bat_data->irq_enabled) { enable_irq(bat_data->irq_num); bat_data->irq_enabled = true; } set_trip_exit: mutex_unlock(&bat_data->state_trans_lock); return ret; } static int bcl_set_vbat(void *data, int low, int high) { int ret = 0, vbat_uv, vbat_idx, thresh_value; int8_t val = 0; struct bcl_peripheral_data *bat_data = (struct bcl_peripheral_data *)data; uint16_t addr; thresh_value = low; if (bat_data->trip_temp == thresh_value) return 0; mutex_lock(&bat_data->state_trans_lock); if (bat_data->irq_num && bat_data->irq_enabled) { disable_irq_nosync(bat_data->irq_num); bat_data->irq_enabled = false; } if (thresh_value == INT_MIN) { bat_data->trip_temp = thresh_value; goto set_trip_exit; } vbat_uv = thresh_value; convert_vbat_to_adc_val(&thresh_value); val = (int8_t)thresh_value; /* * very low and critical low trip can support only standard * trip thresholds */ if (&bcl_perph->param[BCL_LOW_VBAT] == bat_data) { addr = BCL_VBAT_ADC_LOW; pr_debug("vbat low threshold:%d mv ADC:0x%02x\n", vbat_uv, val); } else if (&bcl_perph->param[BCL_VLOW_VBAT] == bat_data) { /* * Scan the standard voltage table, sorted in ascending order * and find the closest threshold that is lower or equal to * the requested value. Passive trip supports thresholds * indexed from 1...BCL_STD_VBAT_NR in the voltage table. */ for (vbat_idx = 2; vbat_idx < BCL_STD_VBAT_NR; vbat_idx++) { if (vbat_uv >= vbat_low[vbat_idx]) continue; break; } addr = BCL_VBAT_COMP_LOW; val = vbat_idx - 2; vbat_uv = vbat_low[vbat_idx - 1]; pr_debug("vbat too low threshold:%d mv ADC:0x%02x\n", vbat_uv, val); } else if (&bcl_perph->param[BCL_CLOW_VBAT] == bat_data) { /* Hot trip supports thresholds indexed from * 0...BCL_STD_VBAT_NR-1 in the voltage table. */ for (vbat_idx = 1; vbat_idx < (BCL_STD_VBAT_NR - 1); vbat_idx++) { if (vbat_uv >= vbat_low[vbat_idx]) continue; break; } addr = BCL_VBAT_COMP_TLOW; val = vbat_idx - 1; vbat_uv = vbat_low[vbat_idx - 1]; pr_debug("vbat critic low threshold:%d mv ADC:0x%02x\n", vbat_uv, val); } else { goto set_trip_exit; } ret = bcl_write_register(addr, val); if (ret) { pr_err("Error accessing BCL peripheral. err:%d\n", ret); goto set_trip_exit; } bat_data->trip_temp = vbat_uv; if (bat_data->irq_num && !bat_data->irq_enabled) { enable_irq(bat_data->irq_num); bat_data->irq_enabled = true; } set_trip_exit: mutex_unlock(&bat_data->state_trans_lock); return ret; } static int bcl_clear_vbat_min(void) { int ret = 0; ret = bcl_write_register(BCL_MAX_MIN_CLR, BIT(BCL_VBAT_MIN_CLR)); if (ret) pr_err("Error in clearing vbat min reg. err:%d", ret); return ret; } static int bcl_clear_ibat_max(void) { int ret = 0; ret = bcl_write_register(BCL_MAX_MIN_CLR, BIT(BCL_IBAT_MAX_CLR)); if (ret) pr_err("Error in clearing ibat max reg. err:%d", ret); return ret; } static int bcl_read_ibat(void *data, int *adc_value) { int ret = 0, timeout = 0; int8_t val[VAL_CP_REG_BUF_LEN] = {0}; struct bcl_peripheral_data *bat_data = (struct bcl_peripheral_data *)data; *adc_value = (int)val[VAL_REG_BUF_OFFSET]; do { ret = bcl_read_multi_register(BCL_IBAT_MAX, val, VAL_CP_REG_BUF_LEN); if (ret) { pr_err("BCL register read error. err:%d\n", ret); goto bcl_read_exit; } } while (val[VAL_REG_BUF_OFFSET] != val[VAL_CP_REG_BUF_OFFSET] && timeout++ < BCL_READ_RETRY_LIMIT); if (val[VAL_REG_BUF_OFFSET] != val[VAL_CP_REG_BUF_OFFSET]) { ret = -ENODEV; *adc_value = bat_data->last_val; goto bcl_read_exit; } *adc_value = (int)val[VAL_REG_BUF_OFFSET]; if (*adc_value == 0) { /* * The sensor sometime can read a value 0 if there is * consequtive reads */ *adc_value = bat_data->last_val; } else { convert_adc_to_ibat_val(adc_value); bat_data->last_val = *adc_value; } pr_debug("ibat:%d mA\n", bat_data->last_val); bcl_read_exit: return ret; } static int bcl_read_ibat_and_clear(void *data, int *adc_value) { int ret = 0; ret = bcl_read_ibat(data, adc_value); if (ret) return ret; return bcl_clear_ibat_max(); } static int bcl_read_vbat(void *data, int *adc_value) { int ret = 0, timeout = 0; int8_t val[VAL_CP_REG_BUF_LEN] = {0}; struct bcl_peripheral_data *bat_data = (struct bcl_peripheral_data *)data; *adc_value = (int)val[VAL_REG_BUF_OFFSET]; do { ret = bcl_read_multi_register(BCL_VBAT_MIN, val, VAL_CP_REG_BUF_LEN); if (ret) { pr_err("BCL register read error. err:%d\n", ret); goto bcl_read_exit; } } while (val[VAL_REG_BUF_OFFSET] != val[VAL_CP_REG_BUF_OFFSET] && timeout++ < BCL_READ_RETRY_LIMIT); if (val[VAL_REG_BUF_OFFSET] != val[VAL_CP_REG_BUF_OFFSET]) { ret = -ENODEV; goto bcl_read_exit; } *adc_value = (int)val[VAL_REG_BUF_OFFSET]; if (*adc_value == BCL_VBAT_NO_READING) { *adc_value = bat_data->last_val; } else { convert_adc_to_vbat_val(adc_value); bat_data->last_val = *adc_value; } pr_debug("vbat:%d mv\n", bat_data->last_val); bcl_read_exit: return ret; } static int bcl_read_vbat_and_clear(void *data, int *adc_value) { int ret; ret = bcl_read_vbat(data, adc_value); if (ret) return ret; return bcl_clear_vbat_min(); } static irqreturn_t bcl_handle_ibat(int irq, void *data) { struct bcl_peripheral_data *perph_data = (struct bcl_peripheral_data *)data; mutex_lock(&perph_data->state_trans_lock); if (!perph_data->irq_enabled) { WARN_ON(1); disable_irq_nosync(irq); perph_data->irq_enabled = false; goto exit_intr; } mutex_unlock(&perph_data->state_trans_lock); of_thermal_handle_trip(perph_data->tz_dev); return IRQ_HANDLED; exit_intr: mutex_unlock(&perph_data->state_trans_lock); return IRQ_HANDLED; } static irqreturn_t bcl_handle_vbat(int irq, void *data) { struct bcl_peripheral_data *perph_data = (struct bcl_peripheral_data *)data; mutex_lock(&perph_data->state_trans_lock); if (!perph_data->irq_enabled) { WARN_ON(1); disable_irq_nosync(irq); perph_data->irq_enabled = false; goto exit_intr; } mutex_unlock(&perph_data->state_trans_lock); of_thermal_handle_trip(perph_data->tz_dev); return IRQ_HANDLED; exit_intr: mutex_unlock(&perph_data->state_trans_lock); return IRQ_HANDLED; } static int bcl_get_devicetree_data(struct platform_device *pdev) { int ret = 0; const __be32 *prop = NULL; struct device_node *dev_node = pdev->dev.of_node; prop = of_get_address(dev_node, 0, NULL, NULL); if (prop) { bcl_perph->fg_bcl_addr = be32_to_cpu(*prop); pr_debug("fg_user_adc@%04x\n", bcl_perph->fg_bcl_addr); } else { dev_err(&pdev->dev, "No fg_user_adc registers found\n"); return -ENODEV; } prop = of_get_address(dev_node, 1, NULL, NULL); if (prop) { bcl_perph->fg_lmh_addr = be32_to_cpu(*prop); pr_debug("fg_lmh@%04x\n", bcl_perph->fg_lmh_addr); } else { dev_err(&pdev->dev, "No fg_lmh registers found\n"); return -ENODEV; } return ret; } static int bcl_set_soc(void *data, int low, int high) { struct bcl_peripheral_data *bat_data = (struct bcl_peripheral_data *)data; if (low == bat_data->trip_temp) return 0; mutex_lock(&bat_data->state_trans_lock); pr_debug("low soc threshold:%d\n", low); bat_data->trip_temp = low; if (low == INT_MIN) { bat_data->irq_enabled = false; goto unlock_and_exit; } bat_data->irq_enabled = true; schedule_work(&bcl_perph->soc_eval_work); unlock_and_exit: mutex_unlock(&bat_data->state_trans_lock); return 0; } static int bcl_read_soc(void *data, int *val) { static struct power_supply *batt_psy; union power_supply_propval ret = {0,}; int err = 0; *val = 100; if (!batt_psy) batt_psy = power_supply_get_by_name("battery"); if (batt_psy) { err = power_supply_get_property(batt_psy, POWER_SUPPLY_PROP_CAPACITY, &ret); if (err) { pr_err("battery percentage read error:%d\n", err); return err; } *val = ret.intval; } pr_debug("soc:%d\n", *val); return err; } static void bcl_evaluate_soc(struct work_struct *work) { int battery_percentage; struct bcl_peripheral_data *perph_data = &bcl_perph->param[BCL_SOC_MONITOR]; if (bcl_read_soc((void *)perph_data, &battery_percentage)) return; mutex_lock(&perph_data->state_trans_lock); if (!perph_data->irq_enabled) goto eval_exit; if (battery_percentage > perph_data->trip_temp) goto eval_exit; perph_data->trip_val = battery_percentage; mutex_unlock(&perph_data->state_trans_lock); of_thermal_handle_trip(perph_data->tz_dev); return; eval_exit: mutex_unlock(&perph_data->state_trans_lock); } static int battery_supply_callback(struct notifier_block *nb, unsigned long event, void *data) { struct power_supply *psy = data; if (strcmp(psy->desc->name, "battery")) return NOTIFY_OK; schedule_work(&bcl_perph->soc_eval_work); return NOTIFY_OK; } static void bcl_fetch_trip(struct platform_device *pdev, const char *int_name, struct bcl_peripheral_data *data, irqreturn_t (*handle)(int, void *)) { int ret = 0, irq_num = 0; /* * Allow flexibility for the HLOS to set the trip temperature for * all the thresholds but handle the interrupt for only one vbat * and ibat interrupt. The LMH-DCVSh will handle and mitigate for the * rest of the ibat/vbat interrupts. */ if (!handle) { mutex_lock(&data->state_trans_lock); data->irq_num = 0; data->irq_enabled = false; mutex_unlock(&data->state_trans_lock); return; } irq_num = platform_get_irq_byname(pdev, int_name); if (irq_num) { mutex_lock(&data->state_trans_lock); ret = devm_request_threaded_irq(&pdev->dev, irq_num, NULL, handle, IRQF_TRIGGER_RISING | IRQF_ONESHOT, int_name, data); if (ret) { dev_err(&pdev->dev, "Error requesting trip irq. err:%d", ret); mutex_unlock(&data->state_trans_lock); return; } disable_irq_nosync(irq_num); data->irq_num = irq_num; data->irq_enabled = false; mutex_unlock(&data->state_trans_lock); } } static void bcl_probe_soc(struct platform_device *pdev) { int ret = 0; struct bcl_peripheral_data *soc_data; soc_data = &bcl_perph->param[BCL_SOC_MONITOR]; mutex_init(&soc_data->state_trans_lock); soc_data->ops.get_temp = bcl_read_soc; soc_data->ops.set_trips = bcl_set_soc; INIT_WORK(&bcl_perph->soc_eval_work, bcl_evaluate_soc); bcl_perph->psy_nb.notifier_call = battery_supply_callback; ret = power_supply_reg_notifier(&bcl_perph->psy_nb); if (ret < 0) { pr_err("Unable to register soc notifier. err:%d\n", ret); return; } soc_data->tz_dev = thermal_zone_of_sensor_register(&pdev->dev, BCL_SOC_MONITOR, soc_data, &soc_data->ops); if (IS_ERR(soc_data->tz_dev)) { pr_err("vbat register failed. err:%ld\n", PTR_ERR(soc_data->tz_dev)); return; } thermal_zone_device_update(soc_data->tz_dev, THERMAL_DEVICE_UP); schedule_work(&bcl_perph->soc_eval_work); } static void bcl_vbat_init(struct platform_device *pdev, struct bcl_peripheral_data *vbat, enum bcl_dev_type type) { mutex_init(&vbat->state_trans_lock); switch (type) { case BCL_LOW_VBAT: bcl_fetch_trip(pdev, BCL_VBAT_INT, vbat, bcl_handle_vbat); break; case BCL_VLOW_VBAT: bcl_fetch_trip(pdev, BCL_VLOW_VBAT_INT, vbat, NULL); break; case BCL_CLOW_VBAT: bcl_fetch_trip(pdev, BCL_CLOW_VBAT_INT, vbat, NULL); break; default: return; } vbat->ops.get_temp = bcl_read_vbat_and_clear; vbat->ops.set_trips = bcl_set_vbat; vbat->tz_dev = thermal_zone_of_sensor_register(&pdev->dev, type, vbat, &vbat->ops); if (IS_ERR(vbat->tz_dev)) { pr_err("vbat register failed. err:%ld\n", PTR_ERR(vbat->tz_dev)); return; } thermal_zone_device_update(vbat->tz_dev, THERMAL_DEVICE_UP); } static void bcl_probe_vbat(struct platform_device *pdev) { bcl_vbat_init(pdev, &bcl_perph->param[BCL_LOW_VBAT], BCL_LOW_VBAT); bcl_vbat_init(pdev, &bcl_perph->param[BCL_VLOW_VBAT], BCL_VLOW_VBAT); bcl_vbat_init(pdev, &bcl_perph->param[BCL_CLOW_VBAT], BCL_CLOW_VBAT); } static void bcl_ibat_init(struct platform_device *pdev, struct bcl_peripheral_data *ibat, enum bcl_dev_type type) { mutex_init(&ibat->state_trans_lock); if (type == BCL_HIGH_IBAT) bcl_fetch_trip(pdev, BCL_IBAT_INT, ibat, bcl_handle_ibat); else bcl_fetch_trip(pdev, BCL_VHIGH_IBAT_INT, ibat, NULL); ibat->ops.get_temp = bcl_read_ibat_and_clear; ibat->ops.set_trips = bcl_set_ibat; ibat->tz_dev = thermal_zone_of_sensor_register(&pdev->dev, type, ibat, &ibat->ops); if (IS_ERR(ibat->tz_dev)) { pr_err("ibat register failed. err:%ld\n", PTR_ERR(ibat->tz_dev)); return; } thermal_zone_device_update(ibat->tz_dev, THERMAL_DEVICE_UP); } static void bcl_probe_ibat(struct platform_device *pdev) { bcl_ibat_init(pdev, &bcl_perph->param[BCL_HIGH_IBAT], BCL_HIGH_IBAT); bcl_ibat_init(pdev, &bcl_perph->param[BCL_VHIGH_IBAT], BCL_VHIGH_IBAT); } static void bcl_configure_lmh_peripheral(void) { bcl_write_register(BCL_LMH_CFG, BCL_LMH_CFG_VAL); bcl_write_register(BCL_CFG, BCL_CFG_VAL); bcl_write_general_register(LMH_INT_POL_HIGH, bcl_perph->fg_lmh_addr, LMH_INT_VAL); bcl_write_general_register(LMH_INT_EN, bcl_perph->fg_lmh_addr, LMH_INT_VAL); } static int bcl_remove(struct platform_device *pdev) { int i = 0; for (; i < BCL_TYPE_MAX; i++) { if (!bcl_perph->param[i].tz_dev) continue; if (i == BCL_SOC_MONITOR) { power_supply_unreg_notifier(&bcl_perph->psy_nb); flush_work(&bcl_perph->soc_eval_work); } thermal_zone_of_sensor_unregister(&pdev->dev, bcl_perph->param[i].tz_dev); } bcl_perph = NULL; return 0; } static int bcl_probe(struct platform_device *pdev) { int ret = 0; bcl_perph = devm_kzalloc(&pdev->dev, sizeof(*bcl_perph), GFP_KERNEL); if (!bcl_perph) return -ENOMEM; bcl_perph->regmap = dev_get_regmap(pdev->dev.parent, NULL); if (!bcl_perph->regmap) { dev_err(&pdev->dev, "Couldn't get parent's regmap\n"); return -EINVAL; } bcl_get_devicetree_data(pdev); bcl_probe_ibat(pdev); bcl_probe_vbat(pdev); bcl_probe_soc(pdev); bcl_configure_lmh_peripheral(); dev_set_drvdata(&pdev->dev, bcl_perph); ret = bcl_write_register(BCL_MONITOR_EN, BIT(7)); if (ret) { pr_err("Error accessing BCL peripheral. err:%d\n", ret); goto bcl_probe_exit; } return 0; bcl_probe_exit: bcl_remove(pdev); return ret; } static const struct of_device_id bcl_match[] = { { .compatible = "qcom,msm-bcl-lmh", }, {}, }; static struct platform_driver bcl_driver = { .probe = bcl_probe, .remove = bcl_remove, .driver = { .name = BCL_DRIVER_NAME, .owner = THIS_MODULE, .of_match_table = bcl_match, }, }; builtin_platform_driver(bcl_driver);