| /* max17048.c - Driver for max17048 battery fuel gauge */ |
| |
| /* |
| * Copyright (c) 2023 Alvaro Garcia Gomez <maxpowel@gmail.com> |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #define DT_DRV_COMPAT maxim_max17048 |
| |
| #include "max17048.h" |
| |
| #include <zephyr/drivers/fuel_gauge.h> |
| #include <zephyr/kernel.h> |
| #include <zephyr/init.h> |
| #include <zephyr/drivers/gpio.h> |
| #include <zephyr/pm/device.h> |
| #include <zephyr/sys/byteorder.h> |
| #include <zephyr/sys/__assert.h> |
| #include <zephyr/logging/log.h> |
| |
| LOG_MODULE_REGISTER(MAX17048); |
| |
| #if DT_NUM_INST_STATUS_OKAY(DT_DRV_COMPAT) == 0 |
| #warning "MAX17048 driver enabled without any devices" |
| #endif |
| |
| /** |
| * Storage for the fuel gauge basic information |
| */ |
| struct max17048_data { |
| /* Charge as percentage */ |
| uint8_t charge; |
| /* Voltage as mV */ |
| uint16_t voltage; |
| |
| /* Time in minutes */ |
| uint16_t time_to_full; |
| uint16_t time_to_empty; |
| /* True if battery chargin, false if discharging */ |
| bool charging; |
| }; |
| |
| /** |
| * I2C communication |
| * The way we read a value is first writing the address we want to read and then |
| * wait for 2 bytes containing the data. |
| */ |
| int max17048_read_register(const struct device *dev, uint8_t registerId, uint16_t *response) |
| { |
| uint8_t max17048_buffer[2]; |
| const struct max17048_config *cfg = dev->config; |
| int rc = i2c_write_read_dt(&cfg->i2c, ®isterId, sizeof(registerId), max17048_buffer, |
| sizeof(max17048_buffer)); |
| if (rc != 0) { |
| LOG_ERR("Unable to read register, error %d", rc); |
| return rc; |
| } |
| |
| *response = sys_get_be16(max17048_buffer); |
| return 0; |
| } |
| |
| /** |
| * Raw value from the internal ADC |
| */ |
| int max17048_adc(const struct device *i2c_dev, uint16_t *response) |
| { |
| return max17048_read_register(i2c_dev, REGISTER_VCELL, response); |
| } |
| |
| /** |
| * Battery voltage |
| */ |
| int max17048_voltage(const struct device *i2c_dev, uint16_t *response) |
| { |
| int rc = max17048_adc(i2c_dev, response); |
| |
| if (rc < 0) { |
| return rc; |
| } |
| /** |
| * Once the value is read, it has to be converted to volts. The datasheet |
| * https://www.analog.com/media/en/technical-documentation/data-sheets/ |
| * MAX17048-MAX17049.pdf |
| * Page 10, Table 2. Register Summary: 78.125µV/cell |
| * Max17048 only supports one cell so we just have to multiply the value by 78.125 to |
| * obtain µV and then divide the value to obtain V. |
| * But to avoid floats, instead of using 78.125 we will use 78125 and use this value as |
| * milli volts instead of volts. |
| */ |
| |
| *response = (uint16_t)((uint32_t)*response * 78125L / 1000000L); |
| return 0; |
| } |
| |
| /** |
| * Battery percentage still available |
| */ |
| int max17048_percent(const struct device *i2c_dev, uint8_t *response) |
| { |
| uint16_t data; |
| int rc = max17048_read_register(i2c_dev, REGISTER_SOC, &data); |
| |
| if (rc < 0) { |
| return rc; |
| } |
| /** |
| * Once the value is read, it has to be converted to percentage. The datasheet |
| * https://www.analog.com/media/en/technical-documentation/data-she4ets/ |
| * MAX17048-MAX17049.pdf |
| * Page 10, Table 2. Register Summary: 1%/256 |
| * So to obtain the total percentaje we just divide the read value by 256 |
| */ |
| *response = data / 256; |
| return 0; |
| } |
| |
| /** |
| * Percentage of the total battery capacity per hour, positive is charging or |
| * negative if discharging |
| */ |
| int max17048_crate(const struct device *i2c_dev, int16_t *response) |
| { |
| int rc = max17048_read_register(i2c_dev, REGISTER_CRATE, response); |
| |
| if (rc < 0) { |
| return rc; |
| } |
| |
| /** |
| * Once the value is read, it has to be converted to something useful. The datasheet |
| * https://www.analog.com/media/en/technical-documentation/data-sheets/ |
| * MAX17048-MAX17049.pdf |
| * Page 11, Table 2. Register Summary (continued): 0.208%/hr |
| * To avoid floats, the value will be multiplied by 208 instead of 0.208, taking into |
| * account that the value will be 1000 times higher |
| */ |
| *response = *response * 208; |
| return 0; |
| } |
| |
| /** |
| * Initialize and verify the chip. The datasheet says that the version register |
| * should be 0x10. If not, or the chip is malfunctioning or it is not a MAX17048 at all |
| */ |
| static int max17048_init(const struct device *dev) |
| { |
| const struct max17048_config *cfg = dev->config; |
| uint16_t version; |
| int rc = max17048_read_register(dev, REGISTER_VERSION, &version); |
| |
| if (!device_is_ready(cfg->i2c.bus)) { |
| LOG_ERR("Bus device is not ready"); |
| return -ENODEV; |
| } |
| |
| if (rc < 0) { |
| LOG_ERR("Cannot read from I2C"); |
| return rc; |
| } |
| |
| version = version & 0xFFF0; |
| if (version != 0x10) { |
| LOG_ERR("Something found at the provided I2C address, but it is not a MAX17048"); |
| LOG_ERR("The version registers should be 0x10 but got %x. Maybe your wiring is " |
| "wrong or it is a fake chip\n", |
| version); |
| return -ENODEV; |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * Get a single property from the fuel gauge |
| */ |
| static int max17048_get_single_prop_impl(const struct device *dev, fuel_gauge_prop_t prop, |
| union fuel_gauge_prop_val *val) |
| { |
| struct max17048_data *data = dev->data; |
| int rc = 0; |
| |
| switch (prop) { |
| case FUEL_GAUGE_RUNTIME_TO_EMPTY: |
| val->runtime_to_empty = data->time_to_empty; |
| break; |
| case FUEL_GAUGE_RUNTIME_TO_FULL: |
| val->runtime_to_full = data->time_to_full; |
| break; |
| case FUEL_GAUGE_RELATIVE_STATE_OF_CHARGE: |
| val->relative_state_of_charge = data->charge; |
| break; |
| case FUEL_GAUGE_VOLTAGE: |
| val->voltage = data->voltage; |
| break; |
| default: |
| rc = -ENOTSUP; |
| } |
| |
| return rc; |
| } |
| |
| /** |
| * Get properties from the fuel gauge |
| */ |
| static int max17048_get_prop(const struct device *dev, fuel_gauge_prop_t prop, |
| union fuel_gauge_prop_val *val) |
| { |
| struct max17048_data *data = dev->data; |
| int rc = max17048_percent(dev, &data->charge); |
| int16_t crate; |
| int ret; |
| |
| if (rc < 0) { |
| LOG_ERR("Error while reading battery percentage"); |
| return rc; |
| } |
| |
| rc = max17048_voltage(dev, &data->voltage); |
| if (rc < 0) { |
| LOG_ERR("Error while reading battery voltage"); |
| return rc; |
| } |
| |
| /** |
| * Crate (current rate) is the current percentage of the battery charged or drained |
| * per hour |
| */ |
| rc = max17048_crate(dev, &crate); |
| if (rc < 0) { |
| LOG_ERR("Error while reading battery current rate"); |
| return rc; |
| } |
| |
| if (crate != 0) { |
| |
| /** |
| * May take some time until the chip detects the change between discharging to |
| * charging (and vice versa) especially if your device consumes little power |
| */ |
| data->charging = crate > 0; |
| |
| /** |
| * In the following code, we multiply by 1000 the charge to increase the |
| * precision. If we just truncate the division without this multiplier, |
| * the precision lost is very significant when converting it into minutes |
| * (the value given is in hours) |
| * |
| * The value coming from crate is already 1000 times higher (check the |
| * function max17048_crate to see the reason) so the multiplier for the |
| * charge will be 1000000 |
| */ |
| if (data->charging) { |
| uint8_t percentage_pending = 100 - data->charge; |
| uint32_t hours_pending = percentage_pending * 1000000 / crate; |
| |
| data->time_to_empty = 0; |
| data->time_to_full = hours_pending * 60 / 1000; |
| } else { |
| /* Discharging */ |
| uint32_t hours_pending = data->charge * 1000000 / -crate; |
| |
| data->time_to_empty = hours_pending * 60 / 1000; |
| data->time_to_full = 0; |
| } |
| } else { |
| /** |
| * This case is to avoid a division by 0 when the charge rate is the same |
| * than consumption rate. It could also happen when the sensor is still |
| * calibrating the battery |
| */ |
| data->charging = false; |
| data->time_to_full = 0; |
| data->time_to_empty = 0; |
| } |
| |
| ret = max17048_get_single_prop_impl(dev, prop, val); |
| |
| return ret; |
| } |
| |
| static const struct fuel_gauge_driver_api max17048_driver_api = { |
| .get_property = &max17048_get_prop, |
| }; |
| |
| #define MAX17048_DEFINE(inst) \ |
| static struct max17048_data max17048_data_##inst; \ |
| \ |
| static const struct max17048_config max17048_config_##inst = { \ |
| .i2c = I2C_DT_SPEC_INST_GET(inst)}; \ |
| \ |
| DEVICE_DT_INST_DEFINE(inst, &max17048_init, NULL, &max17048_data_##inst, \ |
| &max17048_config_##inst, POST_KERNEL, \ |
| CONFIG_FUEL_GAUGE_INIT_PRIORITY, &max17048_driver_api); |
| |
| DT_INST_FOREACH_STATUS_OKAY(MAX17048_DEFINE) |