294 lines
8.0 KiB
C
294 lines
8.0 KiB
C
/* 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 = *response * 78125 / 1000000;
|
|
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_prop(const struct device *dev, struct fuel_gauge_get_property *prop)
|
|
{
|
|
struct max17048_data *data = dev->data;
|
|
int rc = 0;
|
|
|
|
switch (prop->property_type) {
|
|
case FUEL_GAUGE_RUNTIME_TO_EMPTY:
|
|
prop->value.runtime_to_empty = data->time_to_empty;
|
|
break;
|
|
case FUEL_GAUGE_RUNTIME_TO_FULL:
|
|
prop->value.runtime_to_full = data->time_to_full;
|
|
break;
|
|
case FUEL_GAUGE_STATE_OF_CHARGE:
|
|
prop->value.state_of_charge = data->charge;
|
|
break;
|
|
case FUEL_GAUGE_VOLTAGE:
|
|
prop->value.voltage = data->voltage;
|
|
break;
|
|
default:
|
|
rc = -ENOTSUP;
|
|
}
|
|
|
|
prop->status = rc;
|
|
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* Get all possible properties from the fuel gague
|
|
*/
|
|
static int max17048_get_props(const struct device *dev, struct fuel_gauge_get_property *props,
|
|
size_t len)
|
|
{
|
|
int err_count = 0;
|
|
struct max17048_data *data = dev->data;
|
|
int rc = max17048_percent(dev, &data->charge);
|
|
int16_t crate;
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
for (int i = 0; i < len; i++) {
|
|
int ret = max17048_get_prop(dev, props + i);
|
|
|
|
err_count += ret ? 1 : 0;
|
|
}
|
|
|
|
err_count = (err_count == len) ? -1 : err_count;
|
|
|
|
return err_count;
|
|
}
|
|
|
|
static const struct fuel_gauge_driver_api max17048_driver_api = {
|
|
.get_property = &max17048_get_props,
|
|
};
|
|
|
|
#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)
|