| /* |
| * Copyright 2021 Matija Tudan |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <zephyr/drivers/i2c.h> |
| #include <zephyr/drivers/sensor.h> |
| |
| #include <zephyr/logging/log.h> |
| LOG_MODULE_REGISTER(max17262, CONFIG_SENSOR_LOG_LEVEL); |
| |
| #include "max17262.h" |
| |
| #define DT_DRV_COMPAT maxim_max17262 |
| |
| /** |
| * @brief Read a register value |
| * |
| * Registers have an address and a 16-bit value |
| * |
| * @param dev MAX17262 device to access |
| * @param reg_addr Register address to read |
| * @param valp Place to put the value on success |
| * @return 0 if successful, or negative error code from I2C API |
| */ |
| static int max17262_reg_read(const struct device *dev, uint8_t reg_addr, |
| int16_t *valp) |
| { |
| const struct max17262_config *cfg = dev->config; |
| uint8_t i2c_data[2]; |
| int rc; |
| |
| rc = i2c_burst_read_dt(&cfg->i2c, reg_addr, i2c_data, 2); |
| if (rc < 0) { |
| LOG_ERR("Unable to read register"); |
| return rc; |
| } |
| *valp = ((int16_t)i2c_data[1] << 8) | i2c_data[0]; |
| |
| return 0; |
| } |
| |
| /** |
| * @brief Write a register value |
| * |
| * Registers have an address and a 16-bit value |
| * |
| * @param dev MAX17262 device to access |
| * @param reg_addr Register address to write to |
| * @param val Register value to write |
| * @return 0 if successful, or negative error code from I2C API |
| */ |
| static int max17262_reg_write(const struct device *dev, uint8_t reg_addr, |
| int16_t val) |
| { |
| const struct max17262_config *cfg = dev->config; |
| uint8_t i2c_data[3] = {reg_addr, val & 0xFF, (uint16_t)val >> 8}; |
| |
| return i2c_write_dt(&cfg->i2c, i2c_data, sizeof(i2c_data)); |
| } |
| |
| /** |
| * @brief Convert sensor value from millis |
| * |
| * @param val Where to store converted value in sensor_value format |
| * @param val_millis Value in millis |
| */ |
| static void convert_millis(struct sensor_value *val, int32_t val_millis) |
| { |
| val->val1 = val_millis / 1000; |
| val->val2 = (val_millis % 1000) * 1000; |
| } |
| |
| /** |
| * @brief Convert raw register values for specific channel |
| * |
| * @param dev MAX17262 device to access |
| * @param chan Channel number to read |
| * @param valp Returns the sensor value read on success |
| * @return 0 if successful |
| * @return -ENOTSUP for unsupported channels |
| */ |
| static int max17262_channel_get(const struct device *dev, |
| enum sensor_channel chan, |
| struct sensor_value *valp) |
| { |
| const struct max17262_config *const config = dev->config; |
| struct max17262_data *const data = dev->data; |
| int32_t tmp; |
| |
| switch (chan) { |
| case SENSOR_CHAN_GAUGE_VOLTAGE: |
| /* Get voltage in uV */ |
| tmp = data->voltage * VOLTAGE_MULTIPLIER_UV; |
| /* Convert to V */ |
| valp->val1 = tmp / 1000000; |
| valp->val2 = tmp % 1000000; |
| break; |
| case SENSOR_CHAN_GAUGE_AVG_CURRENT: { |
| int current; |
| /* Get avg current in nA */ |
| current = data->avg_current * CURRENT_MULTIPLIER_NA; |
| /* Convert to mA */ |
| valp->val1 = current / 1000000; |
| valp->val2 = current % 1000000; |
| break; |
| } |
| case SENSOR_CHAN_GAUGE_STATE_OF_CHARGE: |
| valp->val1 = data->state_of_charge / 256; |
| valp->val2 = data->state_of_charge % 256 * 1000000 / 256; |
| break; |
| case SENSOR_CHAN_GAUGE_TEMP: |
| valp->val1 = data->internal_temp / 256; |
| valp->val2 = data->internal_temp % 256 * 1000000 / 256; |
| break; |
| case SENSOR_CHAN_GAUGE_FULL_CHARGE_CAPACITY: |
| convert_millis(valp, data->full_cap); |
| break; |
| case SENSOR_CHAN_GAUGE_REMAINING_CHARGE_CAPACITY: |
| convert_millis(valp, data->remaining_cap); |
| break; |
| case SENSOR_CHAN_GAUGE_TIME_TO_EMPTY: |
| /* Get time in ms */ |
| if (data->time_to_empty == 0xffff) { |
| valp->val1 = 0; |
| valp->val2 = 0; |
| } else { |
| tmp = data->time_to_empty * TIME_MULTIPLIER_MS; |
| convert_millis(valp, tmp); |
| } |
| break; |
| case SENSOR_CHAN_GAUGE_TIME_TO_FULL: |
| /* Get time in ms */ |
| if (data->time_to_full == 0xffff) { |
| valp->val1 = 0; |
| valp->val2 = 0; |
| } else { |
| tmp = data->time_to_full * TIME_MULTIPLIER_MS; |
| convert_millis(valp, tmp); |
| } |
| break; |
| case SENSOR_CHAN_GAUGE_CYCLE_COUNT: |
| valp->val1 = data->cycle_count / 100; |
| valp->val2 = data->cycle_count % 100 * 10000; |
| break; |
| case SENSOR_CHAN_GAUGE_NOM_AVAIL_CAPACITY: |
| convert_millis(valp, data->design_cap); |
| break; |
| case SENSOR_CHAN_GAUGE_DESIGN_VOLTAGE: |
| convert_millis(valp, config->design_voltage); |
| break; |
| case SENSOR_CHAN_GAUGE_DESIRED_VOLTAGE: |
| convert_millis(valp, config->desired_voltage); |
| break; |
| case SENSOR_CHAN_GAUGE_DESIRED_CHARGING_CURRENT: |
| valp->val1 = data->ichg_term; |
| valp->val2 = 0; |
| break; |
| case MAX17262_COULOMB_COUNTER: |
| /* Get spent capacity in mAh */ |
| data->coulomb_counter = 0xffff - data->coulomb_counter; |
| valp->val1 = data->coulomb_counter / 2; |
| valp->val2 = data->coulomb_counter % 2 * 10 / 2; |
| break; |
| default: |
| LOG_ERR("Unsupported channel!"); |
| return -ENOTSUP; |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * @brief Read register values for supported channels |
| * |
| * @param dev MAX17262 device to access |
| * @return 0 if successful, or negative error code from I2C API |
| */ |
| static int max17262_sample_fetch(const struct device *dev, |
| enum sensor_channel chan) |
| { |
| struct max17262_data *data = dev->data; |
| struct { |
| int reg_addr; |
| int16_t *dest; |
| } regs[] = { |
| { VCELL, &data->voltage }, |
| { AVG_CURRENT, &data->avg_current }, |
| { ICHG_TERM, &data->ichg_term }, |
| { REP_SOC, &data->state_of_charge }, |
| { INT_TEMP, &data->internal_temp }, |
| { REP_CAP, &data->remaining_cap }, |
| { FULL_CAP_REP, &data->full_cap }, |
| { TTE, &data->time_to_empty }, |
| { TTF, &data->time_to_full }, |
| { CYCLES, &data->cycle_count }, |
| { DESIGN_CAP, &data->design_cap }, |
| { COULOMB_COUNTER, &data->coulomb_counter }, |
| }; |
| |
| __ASSERT_NO_MSG(chan == SENSOR_CHAN_ALL); |
| for (size_t i = 0; i < ARRAY_SIZE(regs); i++) { |
| int rc; |
| |
| rc = max17262_reg_read(dev, regs[i].reg_addr, regs[i].dest); |
| if (rc != 0) { |
| LOG_ERR("Failed to read channel %d", chan); |
| return rc; |
| } |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * @brief Initialise the fuel gauge |
| * |
| * @param dev MAX17262 device to access |
| * @return 0 for success |
| * @return -EINVAL if the I2C controller could not be found |
| */ |
| static int max17262_gauge_init(const struct device *dev) |
| { |
| const struct max17262_config *const config = dev->config; |
| int16_t tmp, hibcfg; |
| |
| if (!device_is_ready(config->i2c.bus)) { |
| LOG_ERR("Bus device is not ready"); |
| return -ENODEV; |
| } |
| |
| /* Read Status register */ |
| max17262_reg_read(dev, STATUS, &tmp); |
| |
| if (!(tmp & STATUS_POR)) { |
| /* |
| * Status.POR bit is set to 1 when MAX17262 detects that |
| * a software or hardware POR event has occurred and |
| * therefore a custom configuration needs to be set... |
| * If POR event did not happen (Status.POR == 0), skip |
| * init and continue with measurements. |
| */ |
| LOG_DBG("No POR event detected - skip device configuration"); |
| return 0; |
| } |
| LOG_DBG("POR detected, setting custom device configuration..."); |
| |
| /** STEP 1 */ |
| max17262_reg_read(dev, FSTAT, &tmp); |
| |
| /* Do not continue until FSTAT.DNR bit is cleared */ |
| while (tmp & FSTAT_DNR) { |
| k_sleep(K_MSEC(10)); |
| max17262_reg_read(dev, FSTAT, &tmp); |
| } |
| |
| /** STEP 2 */ |
| /* Store original HibCFG value */ |
| max17262_reg_read(dev, HIBCFG, &hibcfg); |
| |
| /* Exit Hibernate Mode step 1 */ |
| max17262_reg_write(dev, SOFT_WAKEUP, 0x0090); |
| /* Exit Hibernate Mode step 2 */ |
| max17262_reg_write(dev, HIBCFG, 0x0000); |
| /* Exit Hibernate Mode step 3 */ |
| max17262_reg_write(dev, SOFT_WAKEUP, 0x0000); |
| |
| /** STEP 2.1 --> OPTION 1 EZ Config (No INI file is needed) */ |
| /* Write DesignCap */ |
| max17262_reg_write(dev, DESIGN_CAP, config->design_cap); |
| |
| /* Write IChgTerm */ |
| max17262_reg_write(dev, ICHG_TERM, config->desired_charging_current); |
| |
| /* Write VEmpty */ |
| max17262_reg_write(dev, VEMPTY, ((config->empty_voltage / 10) << 7) | |
| ((config->recovery_voltage / 40) & 0x7F)); |
| |
| /* Write ModelCFG */ |
| if (config->charge_voltage > 4275) { |
| max17262_reg_write(dev, MODELCFG, 0x8400); |
| } else { |
| max17262_reg_write(dev, MODELCFG, 0x8000); |
| } |
| |
| /* |
| * Read ModelCFG.Refresh (highest bit), |
| * proceed to Step 3 when ModelCFG.Refresh == 0 |
| */ |
| max17262_reg_read(dev, MODELCFG, &tmp); |
| |
| /* Do not continue until ModelCFG.Refresh == 0 */ |
| while (tmp & MODELCFG_REFRESH) { |
| k_sleep(K_MSEC(10)); |
| max17262_reg_read(dev, MODELCFG, &tmp); |
| } |
| |
| /* Restore Original HibCFG value */ |
| max17262_reg_write(dev, HIBCFG, hibcfg); |
| |
| /** STEP 3 */ |
| /* Read Status register */ |
| max17262_reg_read(dev, STATUS, &tmp); |
| |
| /* Clear PowerOnReset bit */ |
| tmp &= ~STATUS_POR; |
| max17262_reg_write(dev, STATUS, tmp); |
| |
| return 0; |
| } |
| |
| static const struct sensor_driver_api max17262_battery_driver_api = { |
| .sample_fetch = max17262_sample_fetch, |
| .channel_get = max17262_channel_get, |
| }; |
| |
| #define MAX17262_INIT(n) \ |
| static struct max17262_data max17262_data_##n; \ |
| \ |
| static const struct max17262_config max17262_config_##n = { \ |
| .i2c = I2C_DT_SPEC_INST_GET(n), \ |
| .design_voltage = DT_INST_PROP(n, design_voltage), \ |
| .desired_voltage = DT_INST_PROP(n, desired_voltage), \ |
| .desired_charging_current = \ |
| DT_INST_PROP(n, desired_charging_current), \ |
| .design_cap = DT_INST_PROP(n, design_cap), \ |
| .empty_voltage = DT_INST_PROP(n, empty_voltage), \ |
| .recovery_voltage = DT_INST_PROP(n, recovery_voltage), \ |
| .charge_voltage = DT_INST_PROP(n, charge_voltage), \ |
| }; \ |
| \ |
| DEVICE_DT_INST_DEFINE(n, &max17262_gauge_init, \ |
| NULL, \ |
| &max17262_data_##n, \ |
| &max17262_config_##n, POST_KERNEL, \ |
| CONFIG_SENSOR_INIT_PRIORITY, \ |
| &max17262_battery_driver_api); |
| |
| DT_INST_FOREACH_STATUS_OKAY(MAX17262_INIT) |