// SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #include #include #include #include #include #include #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 "); MODULE_DESCRIPTION("Pegatron Chagall fuel gauge driver"); MODULE_LICENSE("GPL");