| // SPDX-License-Identifier: GPL-2.0-or-later |
| |
| #include <linux/array_size.h> |
| #include <linux/delay.h> |
| #include <linux/devm-helpers.h> |
| #include <linux/err.h> |
| #include <linux/i2c.h> |
| #include <linux/leds.h> |
| #include <linux/mod_devicetable.h> |
| #include <linux/module.h> |
| #include <linux/power_supply.h> |
| #include <linux/regmap.h> |
| |
| #define CHAGALL_REG_LED_AMBER 0x60 |
| #define CHAGALL_REG_LED_WHITE 0x70 |
| #define CHAGALL_REG_BATTERY_TEMPERATURE 0xa2 |
| #define CHAGALL_REG_BATTERY_VOLTAGE 0xa4 |
| #define CHAGALL_REG_BATTERY_CURRENT 0xa6 |
| #define CHAGALL_REG_BATTERY_CAPACITY 0xa8 |
| #define CHAGALL_REG_BATTERY_CHARGING_CURRENT 0xaa |
| #define CHAGALL_REG_BATTERY_CHARGING_VOLTAGE 0xac |
| #define CHAGALL_REG_BATTERY_STATUS 0xae |
| #define BATTERY_DISCHARGING BIT(6) |
| #define BATTERY_FULL_CHARGED BIT(5) |
| #define BATTERY_FULL_DISCHARGED BIT(4) |
| #define CHAGALL_REG_BATTERY_REMAIN_CAPACITY 0xb0 |
| #define CHAGALL_REG_BATTERY_FULL_CAPACITY 0xb2 |
| #define CHAGALL_REG_MAX_COUNT 0xb4 |
| |
| #define CHAGALL_BATTERY_DATA_REFRESH 5000 |
| #define TEMP_CELSIUS_OFFSET 2731 |
| |
| static const struct regmap_config chagall_battery_regmap_config = { |
| .reg_bits = 8, |
| .val_bits = 8, |
| .max_register = CHAGALL_REG_MAX_COUNT, |
| .reg_format_endian = REGMAP_ENDIAN_LITTLE, |
| .val_format_endian = REGMAP_ENDIAN_LITTLE, |
| }; |
| |
| struct chagall_battery_data { |
| struct regmap *regmap; |
| struct led_classdev amber_led; |
| struct led_classdev white_led; |
| struct power_supply *battery; |
| struct delayed_work poll_work; |
| u16 last_state; |
| }; |
| |
| static void chagall_led_set_brightness_amber(struct led_classdev *led, |
| enum led_brightness brightness) |
| { |
| struct chagall_battery_data *cg = |
| container_of(led, struct chagall_battery_data, amber_led); |
| |
| regmap_write(cg->regmap, CHAGALL_REG_LED_AMBER, brightness); |
| } |
| |
| static void chagall_led_set_brightness_white(struct led_classdev *led, |
| enum led_brightness brightness) |
| { |
| struct chagall_battery_data *cg = |
| container_of(led, struct chagall_battery_data, white_led); |
| |
| regmap_write(cg->regmap, CHAGALL_REG_LED_WHITE, brightness); |
| } |
| |
| static const enum power_supply_property chagall_battery_properties[] = { |
| POWER_SUPPLY_PROP_STATUS, |
| POWER_SUPPLY_PROP_PRESENT, |
| POWER_SUPPLY_PROP_VOLTAGE_NOW, |
| POWER_SUPPLY_PROP_VOLTAGE_MAX, |
| POWER_SUPPLY_PROP_CURRENT_NOW, |
| POWER_SUPPLY_PROP_CURRENT_MAX, |
| POWER_SUPPLY_PROP_CAPACITY, |
| POWER_SUPPLY_PROP_TEMP, |
| POWER_SUPPLY_PROP_CHARGE_FULL, |
| POWER_SUPPLY_PROP_CHARGE_NOW, |
| }; |
| |
| static const unsigned int chagall_battery_prop_offs[] = { |
| [POWER_SUPPLY_PROP_STATUS] = CHAGALL_REG_BATTERY_STATUS, |
| [POWER_SUPPLY_PROP_VOLTAGE_NOW] = CHAGALL_REG_BATTERY_VOLTAGE, |
| [POWER_SUPPLY_PROP_VOLTAGE_MAX] = CHAGALL_REG_BATTERY_CHARGING_VOLTAGE, |
| [POWER_SUPPLY_PROP_CURRENT_NOW] = CHAGALL_REG_BATTERY_CURRENT, |
| [POWER_SUPPLY_PROP_CURRENT_MAX] = CHAGALL_REG_BATTERY_CHARGING_CURRENT, |
| [POWER_SUPPLY_PROP_CAPACITY] = CHAGALL_REG_BATTERY_CAPACITY, |
| [POWER_SUPPLY_PROP_TEMP] = CHAGALL_REG_BATTERY_TEMPERATURE, |
| [POWER_SUPPLY_PROP_CHARGE_FULL] = CHAGALL_REG_BATTERY_FULL_CAPACITY, |
| [POWER_SUPPLY_PROP_CHARGE_NOW] = CHAGALL_REG_BATTERY_REMAIN_CAPACITY, |
| }; |
| |
| static int chagall_battery_get_value(struct chagall_battery_data *cg, |
| enum power_supply_property psp, u32 *val) |
| { |
| if (psp >= ARRAY_SIZE(chagall_battery_prop_offs)) |
| return -EINVAL; |
| if (!chagall_battery_prop_offs[psp]) |
| return -EINVAL; |
| |
| /* Battery data is stored in 2 consecutive registers with little-endian */ |
| return regmap_bulk_read(cg->regmap, chagall_battery_prop_offs[psp], val, 2); |
| } |
| |
| static int chagall_battery_get_status(u32 status_reg) |
| { |
| if (status_reg & BATTERY_FULL_CHARGED) |
| return POWER_SUPPLY_STATUS_FULL; |
| else if (status_reg & BATTERY_DISCHARGING) |
| return POWER_SUPPLY_STATUS_DISCHARGING; |
| else |
| return POWER_SUPPLY_STATUS_CHARGING; |
| } |
| |
| static int chagall_battery_get_property(struct power_supply *psy, |
| enum power_supply_property psp, |
| union power_supply_propval *val) |
| { |
| struct chagall_battery_data *cg = power_supply_get_drvdata(psy); |
| int ret; |
| |
| switch (psp) { |
| case POWER_SUPPLY_PROP_PRESENT: |
| val->intval = 1; |
| break; |
| |
| default: |
| ret = chagall_battery_get_value(cg, psp, &val->intval); |
| if (ret) |
| return ret; |
| |
| switch (psp) { |
| case POWER_SUPPLY_PROP_TEMP: |
| val->intval -= TEMP_CELSIUS_OFFSET; |
| break; |
| |
| case POWER_SUPPLY_PROP_VOLTAGE_MAX: |
| case POWER_SUPPLY_PROP_VOLTAGE_NOW: |
| case POWER_SUPPLY_PROP_CURRENT_MAX: |
| case POWER_SUPPLY_PROP_CURRENT_NOW: |
| case POWER_SUPPLY_PROP_CHARGE_FULL: |
| case POWER_SUPPLY_PROP_CHARGE_NOW: |
| val->intval *= 1000; |
| break; |
| |
| case POWER_SUPPLY_PROP_STATUS: |
| val->intval = chagall_battery_get_status(val->intval); |
| break; |
| |
| default: |
| break; |
| } |
| |
| break; |
| } |
| |
| return 0; |
| } |
| |
| static void chagall_battery_poll_work(struct work_struct *work) |
| { |
| struct chagall_battery_data *cg = |
| container_of(work, struct chagall_battery_data, poll_work.work); |
| u32 state; |
| int ret; |
| |
| ret = chagall_battery_get_value(cg, POWER_SUPPLY_PROP_STATUS, &state); |
| if (ret) |
| return; |
| |
| state = chagall_battery_get_status(state); |
| |
| if (cg->last_state != state) { |
| cg->last_state = state; |
| power_supply_changed(cg->battery); |
| } |
| |
| /* continuously send uevent notification */ |
| schedule_delayed_work(&cg->poll_work, |
| msecs_to_jiffies(CHAGALL_BATTERY_DATA_REFRESH)); |
| } |
| |
| static const struct power_supply_desc chagall_battery_desc = { |
| .name = "chagall-battery", |
| .type = POWER_SUPPLY_TYPE_BATTERY, |
| .properties = chagall_battery_properties, |
| .num_properties = ARRAY_SIZE(chagall_battery_properties), |
| .get_property = chagall_battery_get_property, |
| .external_power_changed = power_supply_changed, |
| }; |
| |
| static int chagall_battery_probe(struct i2c_client *client) |
| { |
| struct chagall_battery_data *cg; |
| struct device *dev = &client->dev; |
| struct power_supply_config cfg = { }; |
| int ret; |
| |
| cg = devm_kzalloc(dev, sizeof(*cg), GFP_KERNEL); |
| if (!cg) |
| return -ENOMEM; |
| |
| cfg.drv_data = cg; |
| cfg.fwnode = dev_fwnode(dev); |
| |
| i2c_set_clientdata(client, cg); |
| |
| cg->regmap = devm_regmap_init_i2c(client, &chagall_battery_regmap_config); |
| if (IS_ERR(cg->regmap)) |
| return dev_err_probe(dev, PTR_ERR(cg->regmap), "cannot allocate regmap\n"); |
| |
| cg->last_state = POWER_SUPPLY_STATUS_UNKNOWN; |
| cg->battery = devm_power_supply_register(dev, &chagall_battery_desc, &cfg); |
| if (IS_ERR(cg->battery)) |
| return dev_err_probe(dev, PTR_ERR(cg->battery), |
| "failed to register power supply\n"); |
| |
| cg->amber_led.name = "power::amber"; |
| cg->amber_led.max_brightness = 1; |
| cg->amber_led.flags = LED_CORE_SUSPENDRESUME; |
| cg->amber_led.brightness_set = chagall_led_set_brightness_amber; |
| cg->amber_led.default_trigger = "chagall-battery-charging"; |
| |
| ret = devm_led_classdev_register(dev, &cg->amber_led); |
| if (ret) |
| return dev_err_probe(dev, ret, "failed to register amber LED\n"); |
| |
| cg->white_led.name = "power::white"; |
| cg->white_led.max_brightness = 1; |
| cg->white_led.flags = LED_CORE_SUSPENDRESUME; |
| cg->white_led.brightness_set = chagall_led_set_brightness_white; |
| cg->white_led.default_trigger = "chagall-battery-full"; |
| |
| ret = devm_led_classdev_register(dev, &cg->white_led); |
| if (ret) |
| return dev_err_probe(dev, ret, "failed to register white LED\n"); |
| |
| led_set_brightness(&cg->amber_led, LED_OFF); |
| led_set_brightness(&cg->white_led, LED_OFF); |
| |
| ret = devm_delayed_work_autocancel(dev, &cg->poll_work, chagall_battery_poll_work); |
| if (ret) |
| return ret; |
| |
| schedule_delayed_work(&cg->poll_work, msecs_to_jiffies(CHAGALL_BATTERY_DATA_REFRESH)); |
| |
| return 0; |
| } |
| |
| static int __maybe_unused chagall_battery_suspend(struct device *dev) |
| { |
| struct i2c_client *client = to_i2c_client(dev); |
| struct chagall_battery_data *cg = i2c_get_clientdata(client); |
| |
| cancel_delayed_work_sync(&cg->poll_work); |
| |
| return 0; |
| } |
| |
| static int __maybe_unused chagall_battery_resume(struct device *dev) |
| { |
| struct i2c_client *client = to_i2c_client(dev); |
| struct chagall_battery_data *cg = i2c_get_clientdata(client); |
| |
| schedule_delayed_work(&cg->poll_work, msecs_to_jiffies(CHAGALL_BATTERY_DATA_REFRESH)); |
| |
| return 0; |
| } |
| |
| static SIMPLE_DEV_PM_OPS(chagall_battery_pm_ops, |
| chagall_battery_suspend, chagall_battery_resume); |
| |
| static const struct of_device_id chagall_of_match[] = { |
| { .compatible = "pegatron,chagall-ec" }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(of, chagall_of_match); |
| |
| static struct i2c_driver chagall_battery_driver = { |
| .driver = { |
| .name = "chagall-battery", |
| .pm = &chagall_battery_pm_ops, |
| .of_match_table = chagall_of_match, |
| }, |
| .probe = chagall_battery_probe, |
| }; |
| module_i2c_driver(chagall_battery_driver); |
| |
| MODULE_AUTHOR("Svyatoslav Ryhel <clamor95@gmail.com>"); |
| MODULE_DESCRIPTION("Pegatron Chagall fuel gauge driver"); |
| MODULE_LICENSE("GPL"); |