| /* |
| * Fuel gauge driver for common |
| * |
| * Copyright (C) 2023 Google Inc. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| */ |
| |
| #include <linux/of.h> |
| #include <linux/debugfs.h> |
| #include "maxfg_common.h" |
| |
| /* dump FG model data */ |
| void dump_model(struct device *dev, u16 model_start, u16 *data, int count) |
| { |
| int i, j, len; |
| char buff[16 * 5 + 1] = {}; |
| |
| for (i = 0; i < count; i += 16) { |
| |
| for (len = 0, j = 0; j < 16; j++) |
| len += scnprintf(&buff[len], sizeof(buff) - len, |
| "%04x ", data[i + j]); |
| |
| dev_info(dev, "%x: %s\n", i + model_start, buff); |
| } |
| } |
| |
| int maxfg_get_fade_rate(struct device *dev, int bhi_fcn_count, int *fade_rate, enum gbms_property p) |
| { |
| struct maxfg_eeprom_history hist = { 0 }; |
| int ret, ratio, i, fcn_sum = 0, fcr_sum = 0; |
| u16 hist_idx; |
| |
| ret = gbms_storage_read(GBMS_TAG_HCNT, &hist_idx, sizeof(hist_idx)); |
| if (ret < 0) { |
| dev_err(dev, "failed to get history index (%d)\n", ret); |
| return -EIO; |
| } |
| |
| dev_dbg(dev, "%s: hist_idx=%d\n", __func__, hist_idx); |
| |
| /* no fade for new battery (less than 30 cycles) */ |
| if (hist_idx < bhi_fcn_count) |
| return 0; |
| |
| while (hist_idx >= BATT_MAX_HIST_CNT && bhi_fcn_count > 1) { |
| hist_idx--; |
| bhi_fcn_count--; |
| if (bhi_fcn_count == 1) { |
| hist_idx = BATT_MAX_HIST_CNT - 1; |
| break; |
| } |
| } |
| |
| for (i = bhi_fcn_count; i ; i--, hist_idx--) { |
| ret = gbms_storage_read_data(GBMS_TAG_HIST, &hist, |
| sizeof(hist), hist_idx); |
| |
| dev_dbg(dev, "%s: idx=%d hist.fcn=%d (%x) hist.fcr=%d (%x) ret=%d\n", |
| __func__, hist_idx, hist.fullcapnom, hist.fullcapnom, |
| hist.fullcaprep, hist.fullcaprep, ret); |
| |
| if (ret < 0 || ret != sizeof(hist)) |
| return -EINVAL; |
| |
| /* hist.fullcapnom = fullcapnom * 800 / designcap */ |
| fcn_sum += hist.fullcapnom; |
| fcr_sum += hist.fullcaprep; |
| } |
| |
| /* convert from maxfg_eeprom_history to percent */ |
| ratio = (p == GBMS_PROP_CAPACITY_FADE_RATE_FCR) ? fcr_sum / (bhi_fcn_count * 8) |
| : fcn_sum / (bhi_fcn_count * 8); |
| |
| /* allow negative value when capacity larger than design */ |
| *fade_rate = 100 - ratio; |
| |
| return 0; |
| } |
| |
| static const struct maxfg_reg * maxfg_find_by_index(struct maxfg_regtags *tags, int index) |
| { |
| if (index < 0 || !tags || index >= tags->max) |
| return NULL; |
| |
| return &tags->map[index]; |
| } |
| |
| const struct maxfg_reg * maxfg_find_by_tag(struct maxfg_regmap *map, enum maxfg_reg_tags tag) |
| { |
| return maxfg_find_by_index(&map->regtags, tag); |
| } |
| |
| int maxfg_reg_read(struct maxfg_regmap *map, enum maxfg_reg_tags tag, u16 *val) |
| { |
| const struct maxfg_reg *reg; |
| unsigned int tmp; |
| int rtn; |
| |
| reg = maxfg_find_by_tag(map, tag); |
| if (!reg) |
| return -EINVAL; |
| |
| rtn = regmap_read(map->regmap, reg->reg, &tmp); |
| if (rtn) |
| pr_err("Failed to read %x\n", reg->reg); |
| else |
| *val = tmp; |
| |
| return rtn; |
| } |
| |
| static int maxfg_reg_read_addr(struct maxfg_regmap *map, enum maxfg_reg_tags tag, |
| u16 *val, u16 *addr) |
| { |
| const struct maxfg_reg *reg; |
| unsigned int tmp; |
| int rtn; |
| |
| reg = maxfg_find_by_tag(map, tag); |
| if (!reg) |
| return -EINVAL; |
| |
| *addr = reg->reg; |
| |
| rtn = regmap_read(map->regmap, reg->reg, &tmp); |
| if (rtn) |
| pr_err("Failed to read %x\n", reg->reg); |
| else |
| *val = tmp; |
| |
| return rtn; |
| } |
| |
| static int maxfg_reg_write_verify(struct maxfg_regmap *map, enum maxfg_reg_tags tag, u16 val) |
| { |
| const struct maxfg_reg *reg; |
| unsigned int tmp = val; |
| unsigned int check_tmp; |
| int rtn; |
| |
| reg = maxfg_find_by_tag(map, tag); |
| if (!reg) |
| return -EINVAL; |
| |
| rtn = regmap_write(map->regmap, reg->reg, tmp); |
| if (rtn == 0) |
| rtn = regmap_read(map->regmap, reg->reg, &check_tmp); |
| if (rtn) |
| return -EIO; |
| if (check_tmp != tmp) |
| return -EAGAIN; |
| return 0; |
| } |
| |
| |
| #define REG_HALF_HIGH(reg) ((reg >> 8) & 0x00FF) |
| #define REG_HALF_LOW(reg) (reg & 0x00FF) |
| int maxfg_collect_history_data(void *buff, size_t size, bool is_por, u16 designcap, u16 RSense, |
| struct maxfg_regmap *regmap, struct maxfg_regmap *regmap_debug) |
| { |
| struct maxfg_eeprom_history hist = { 0 }; |
| u16 data; |
| int temp, ret; |
| |
| if (is_por) |
| return -EINVAL; |
| |
| ret = maxfg_reg_read(regmap_debug, MAXFG_TAG_tempco, &data); |
| if (ret) |
| return ret; |
| |
| hist.tempco = data; |
| |
| ret = maxfg_reg_read(regmap_debug, MAXFG_TAG_rcomp0, &data); |
| if (ret) |
| return ret; |
| |
| hist.rcomp0 = data; |
| |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_timerh, &data); |
| if (ret) |
| return ret; |
| |
| /* Convert LSB from 3.2hours(192min) to 5days(7200min) */ |
| hist.timerh = data * 192 / 7200; |
| |
| if (!designcap) { |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_descap, &designcap); |
| if (ret) |
| return ret; |
| } |
| |
| /* multiply by 100 to convert from mAh to %, LSB 0.125% */ |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_fcnom, &data); |
| if (ret) |
| return ret; |
| |
| temp = (int)data * 800 / (int)designcap; |
| hist.fullcapnom = temp > MAX_HIST_FULLCAP ? MAX_HIST_FULLCAP : temp; |
| |
| /* multiply by 100 to convert from mAh to %, LSB 0.125% */ |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_fcrep, &data); |
| if (ret) |
| return ret; |
| |
| temp = (int)data * 800 / (int)designcap; |
| hist.fullcaprep = temp > MAX_HIST_FULLCAP ? MAX_HIST_FULLCAP : temp; |
| |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_msoc, &data); |
| if (ret) |
| return ret; |
| |
| /* Convert LSB from 1% to 2% */ |
| hist.mixsoc = REG_HALF_HIGH(data) / 2; |
| |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_vfsoc, &data); |
| if (ret) |
| return ret; |
| |
| /* Convert LSB from 1% to 2% */ |
| hist.vfsoc = REG_HALF_HIGH(data) / 2; |
| |
| |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_mmdv, &data); |
| if (ret) |
| return ret; |
| |
| /* LSB is 20mV, store values from 4.2V min */ |
| hist.maxvolt = (REG_HALF_HIGH(data) * 20 - 4200) / 20; |
| /* Convert LSB from 20mV to 10mV, store values from 2.5V min */ |
| hist.minvolt = (REG_HALF_LOW(data) * 20 - 2500) / 10; |
| |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_mmdt, &data); |
| if (ret) |
| return ret; |
| |
| /* Convert LSB from 1degC to 3degC, store values from 25degC min to 70degC max */ |
| hist.maxtemp = s8_to_u4_boundary(((s8)REG_HALF_HIGH(data) - 25) / 3); |
| /* Convert LSB from 1degC to 3degC, store values from -20degC min to 25degC max */ |
| hist.mintemp = s8_to_u4_boundary(((s8)REG_HALF_LOW(data) + 20) / 3); |
| |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_mmdc, &data); |
| if (ret) |
| return ret; |
| |
| /* Convert LSB from 400uV/RSENSE(Rsense LSB is 10μΩ) to 0.5A, range 0A to 7.5A */ |
| hist.maxchgcurr = (s8)REG_HALF_HIGH(data) * 400 * 2 / (RSense * 10); |
| hist.maxdischgcurr = -(s8)REG_HALF_LOW(data) * 400 * 2 / (RSense * 10); |
| |
| memcpy(buff, &hist, sizeof(hist)); |
| return (size_t)sizeof(hist); |
| } |
| |
| /* resistance and impedance ------------------------------------------------ */ |
| |
| int maxfg_read_resistance_avg(u16 RSense) |
| { |
| u16 ravg; |
| int ret = 0; |
| |
| ret = gbms_storage_read(GBMS_TAG_RAVG, &ravg, sizeof(ravg)); |
| if (ret < 0) |
| return ret; |
| |
| return reg_to_resistance_micro_ohms(ravg, RSense); |
| } |
| |
| int maxfg_read_resistance_raw(struct maxfg_regmap *map) |
| { |
| u16 data; |
| int ret; |
| |
| ret = maxfg_reg_read(map, MAXFG_TAG_rslow, &data); |
| if (ret < 0) |
| return ret; |
| |
| return data; |
| } |
| |
| int maxfg_read_resistance(struct maxfg_regmap *map, u16 RSense) |
| { |
| int rslow; |
| |
| rslow = maxfg_read_resistance_raw(map); |
| if (rslow < 0) |
| return rslow; |
| |
| return reg_to_resistance_micro_ohms(rslow, RSense); |
| } |
| |
| /* ----------------------------------------------------------------------- */ |
| |
| /* will return error if the value is not valid */ |
| int maxfg_health_get_ai(struct device *dev, int bhi_acim, u16 RSense) |
| { |
| u16 act_impedance, act_timerh; |
| int ret; |
| |
| if (bhi_acim != 0) |
| return bhi_acim; |
| |
| /* read both and recalculate for compatibility */ |
| ret = gbms_storage_read(GBMS_TAG_ACIM, &act_impedance, sizeof(act_impedance)); |
| if (ret < 0) |
| return -EIO; |
| |
| ret = gbms_storage_read(GBMS_TAG_THAS, &act_timerh, sizeof(act_timerh)); |
| if (ret < 0) |
| return -EIO; |
| |
| /* need to get starting impedance (if qualified) */ |
| if (act_impedance == 0xffff || act_timerh == 0xffff) |
| return -EINVAL; |
| |
| /* not zero, not negative */ |
| bhi_acim = reg_to_resistance_micro_ohms(act_impedance, RSense); |
| |
| /* TODO: correct impedance with timerh */ |
| |
| dev_info(dev, "%s: bhi_acim =%d act_impedance=%x act_timerh=%x\n", |
| __func__, bhi_acim, act_impedance, act_timerh); |
| |
| return bhi_acim; |
| } |
| |
| /* Capacity Estimation functions*/ |
| static int batt_ce_regmap_read(struct maxfg_regmap *map, const struct maxfg_reg *bcea, u32 reg, u16 *data) |
| { |
| int err; |
| u16 val; |
| |
| if (!bcea) |
| return -EINVAL; |
| |
| err = REGMAP_READ(map, bcea->map[reg], &val); |
| if (err) |
| return err; |
| |
| switch(reg) { |
| case CE_DELTA_CC_SUM_REG: |
| case CE_DELTA_VFSOC_SUM_REG: |
| *data = val; |
| break; |
| case CE_CAP_FILTER_COUNT: |
| val = val & 0x0F00; |
| *data = val >> 8; |
| break; |
| default: |
| break; |
| } |
| |
| return err; |
| } |
| |
| int batt_ce_load_data(struct maxfg_regmap *map, struct gbatt_capacity_estimation *cap_esti) |
| { |
| u16 data; |
| const struct maxfg_reg *bcea = cap_esti->bcea; |
| |
| cap_esti->estimate_state = ESTIMATE_NONE; |
| if (batt_ce_regmap_read(map, bcea, CE_DELTA_CC_SUM_REG, &data) == 0) |
| cap_esti->delta_cc_sum = data; |
| else |
| cap_esti->delta_cc_sum = 0; |
| |
| if (batt_ce_regmap_read(map, bcea, CE_DELTA_VFSOC_SUM_REG, &data) == 0) |
| cap_esti->delta_vfsoc_sum = data; |
| else |
| cap_esti->delta_vfsoc_sum = 0; |
| |
| if (batt_ce_regmap_read(map, bcea, CE_CAP_FILTER_COUNT, &data) == 0) |
| cap_esti->cap_filter_count = data; |
| else |
| cap_esti->cap_filter_count = 0; |
| return 0; |
| } |
| |
| void batt_ce_dump_data(const struct gbatt_capacity_estimation *cap_esti, struct logbuffer *log) |
| { |
| logbuffer_log(log, "cap_filter_count: %d" |
| " start_cc: %d" |
| " start_vfsoc: %d" |
| " delta_cc_sum: %d" |
| " delta_vfsoc_sum: %d" |
| " state: %d" |
| " cable: %d", |
| cap_esti->cap_filter_count, |
| cap_esti->start_cc, |
| cap_esti->start_vfsoc, |
| cap_esti->delta_cc_sum, |
| cap_esti->delta_vfsoc_sum, |
| cap_esti->estimate_state, |
| cap_esti->cable_in); |
| } |
| |
| static int batt_ce_regmap_write(struct maxfg_regmap *map, |
| const struct maxfg_reg *bcea, |
| u32 reg, u16 data) |
| { |
| int err = -EINVAL; |
| u16 val; |
| |
| if (!bcea) |
| return -EINVAL; |
| |
| switch(reg) { |
| case CE_DELTA_CC_SUM_REG: |
| case CE_DELTA_VFSOC_SUM_REG: |
| err = REGMAP_WRITE(map, bcea->map[reg], data); |
| break; |
| case CE_CAP_FILTER_COUNT: |
| err = REGMAP_READ(map, bcea->map[reg], &val); |
| if (err) |
| return err; |
| val = val & 0xF0FF; |
| if (data > CE_FILTER_COUNT_MAX) |
| val = val | 0x0F00; |
| else |
| val = val | (data << 8); |
| err = REGMAP_WRITE(map, bcea->map[reg], val); |
| break; |
| default: |
| break; |
| } |
| |
| return err; |
| } |
| |
| /* call holding &cap_esti->batt_ce_lock */ |
| void batt_ce_store_data(struct maxfg_regmap *map, struct gbatt_capacity_estimation *cap_esti) |
| { |
| if (cap_esti->cap_filter_count <= CE_FILTER_COUNT_MAX) { |
| batt_ce_regmap_write(map, cap_esti->bcea, |
| CE_CAP_FILTER_COUNT, |
| cap_esti->cap_filter_count); |
| } |
| |
| batt_ce_regmap_write(map, cap_esti->bcea, |
| CE_DELTA_VFSOC_SUM_REG, |
| cap_esti->delta_vfsoc_sum); |
| batt_ce_regmap_write(map, cap_esti->bcea, |
| CE_DELTA_CC_SUM_REG, |
| cap_esti->delta_cc_sum); |
| } |
| |
| /* call holding &cap_esti->batt_ce_lock */ |
| void batt_ce_stop_estimation(struct gbatt_capacity_estimation *cap_esti, int reason) |
| { |
| cap_esti->estimate_state = reason; |
| cap_esti->start_vfsoc = 0; |
| cap_esti->start_cc = 0; |
| } |
| |
| int maxfg_health_write_ai(u16 act_impedance, u16 act_timerh) |
| { |
| int ret; |
| |
| ret = gbms_storage_write(GBMS_TAG_ACIM, &act_impedance, sizeof(act_impedance)); |
| if (ret < 0) |
| return -EIO; |
| |
| ret = gbms_storage_write(GBMS_TAG_THAS, &act_timerh, sizeof(act_timerh)); |
| if (ret < 0) |
| return -EIO; |
| |
| return 0; |
| } |
| |
| /* for abnormal event log */ |
| static enum maxfg_reg_tags fg_event_regs[] = { |
| MAXFG_TAG_cycles, |
| MAXFG_TAG_vcel, |
| MAXFG_TAG_avgv, |
| MAXFG_TAG_curr, |
| MAXFG_TAG_avgc, |
| MAXFG_TAG_timerh, |
| MAXFG_TAG_temp, |
| MAXFG_TAG_repcap, |
| MAXFG_TAG_mixcap, |
| MAXFG_TAG_fcrep, |
| MAXFG_TAG_fcnom, |
| MAXFG_TAG_qresd, |
| MAXFG_TAG_avcap, |
| MAXFG_TAG_vfremcap, |
| MAXFG_TAG_repsoc, |
| MAXFG_TAG_vfsoc, |
| MAXFG_TAG_msoc, |
| MAXFG_TAG_vfocv, |
| MAXFG_TAG_dpacc, |
| MAXFG_TAG_dqacc, |
| MAXFG_TAG_qh, |
| MAXFG_TAG_qh0, |
| MAXFG_TAG_vfsoc0, |
| MAXFG_TAG_qrtable20, |
| MAXFG_TAG_qrtable30, |
| MAXFG_TAG_status, |
| MAXFG_TAG_fstat, |
| }; |
| |
| static enum maxfg_reg_tags fg_event_dbg_regs[] = { |
| MAXFG_TAG_rcomp0, |
| MAXFG_TAG_tempco, |
| }; |
| |
| |
| int maxfg_reg_log_abnormal(struct maxfg_regmap *map, struct maxfg_regmap *map_debug, |
| char *buf, int buf_len) |
| { |
| u16 ret, i, addr, val, pos = 0; |
| size_t reg_cnt = sizeof(fg_event_regs) / sizeof(enum maxfg_reg_tags); |
| size_t dbg_reg_cnt = sizeof(fg_event_dbg_regs) / sizeof(enum maxfg_reg_tags); |
| |
| for (i = 0; i < reg_cnt; i++) { |
| ret = maxfg_reg_read_addr(map, fg_event_regs[i], &val, &addr); |
| if (ret < 0) |
| return ret; |
| pos += scnprintf(&buf[pos], buf_len - pos, " %04X", val); |
| } |
| |
| for (i = 0; i < dbg_reg_cnt; i++) { |
| ret = maxfg_reg_read_addr(map_debug, fg_event_dbg_regs[i], &val, &addr); |
| if (ret < 0) |
| return ret; |
| pos += scnprintf(&buf[pos], buf_len - pos, " %04X", val); |
| } |
| |
| return 0; |
| } |
| |
| int maxfg_reg_log_data(struct maxfg_regmap *map, struct maxfg_regmap *map_debug, char *buf) |
| { |
| u16 vfsoc, avcap, repcap, fullcap, fullcaprep, fullcapnom, qh0, qh, dqacc, dpacc, fstat; |
| u16 qresidual, rcomp0, cycles, learncfg, tempco, filtercfg, mixcap, vfremcap, vcell, ibat; |
| u16 vfsoc_addr, avcap_addr, repcap_addr, fullcap_addr, fullcaprep_addr, fullcapnom_addr; |
| u16 qh0_addr, qh_addr, dqacc_addr, dpacc_addr, fstat_addr, qresidual_addr, rcomp0_addr; |
| u16 cycles_addr, learncfg_addr, tempco_addr, filtercfg_addr, mixcap_addr, vfremcap_addr; |
| u16 vcell_addr, ibat_addr; |
| int ret, len; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_vfsoc, &vfsoc, &vfsoc_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_avcap, &avcap, &avcap_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_repcap, &repcap, &repcap_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_fulcap, &fullcap, &fullcap_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_fcrep, &fullcaprep, &fullcaprep_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_fcnom, &fullcapnom, &fullcapnom_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_qh0, &qh0, &qh0_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_qh, &qh, &qh_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_dqacc, &dqacc, &dqacc_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_dpacc, &dpacc, &dpacc_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_qresd, &qresidual, &qresidual_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_fstat, &fstat, &fstat_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_learn, &learncfg, &learncfg_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map_debug, MAXFG_TAG_tempco, &tempco, &tempco_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map_debug, MAXFG_TAG_filcfg, &filtercfg, &filtercfg_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_mcap, &mixcap, &mixcap_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_vfcap, &vfremcap, &vfremcap_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_vcel, &vcell, &vcell_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_curr, &ibat, &ibat_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map_debug, MAXFG_TAG_rcomp0, &rcomp0, &rcomp0_addr); |
| if (ret < 0) |
| return ret; |
| |
| ret = maxfg_reg_read_addr(map, MAXFG_TAG_cycles, &cycles, &cycles_addr); |
| if (ret < 0) |
| return ret; |
| |
| len = scnprintf(&buf[0], PAGE_SIZE, "%02X:%04X %02X:%04X %02X:%04X %02X:%04X" |
| " %02X:%04X %02X:%04X %02X:%04X %02X:%04X %02X:%04X %02X:%04X" |
| " %02X:%04X %02X:%04X %02X:%04X %02X:%04X %02X:%04X %02X:%04X" |
| " %02X:%04X %02X:%04X %02X:%04X %02X:%04X %02X:%04X", |
| vfsoc_addr, vfsoc, avcap_addr, avcap, repcap_addr, repcap, |
| fullcap_addr, fullcap, fullcaprep_addr, fullcaprep, |
| fullcapnom_addr, fullcapnom, qh0_addr, qh0, qh_addr, qh, dqacc_addr, dqacc, |
| dpacc_addr, dpacc, qresidual_addr, qresidual, fstat_addr, fstat, |
| learncfg_addr, learncfg, tempco_addr, tempco, filtercfg_addr, filtercfg, |
| mixcap_addr, mixcap, vfremcap_addr, vfremcap, vcell_addr, vcell, |
| ibat_addr , ibat, rcomp0_addr, rcomp0, cycles_addr, cycles); |
| |
| return len; |
| } |
| |
| /* learning parameters */ |
| #define MAX_FG_LEARNING_CONFIG_NORMAL_REGS 14 |
| #define MAX_FG_LEARNING_CONFIG_DEBUG_REGS 2 |
| |
| static enum maxfg_reg_tags fg_learning_param[] ={ |
| /* from normal regmap */ |
| MAXFG_TAG_fcnom, |
| MAXFG_TAG_dpacc, |
| MAXFG_TAG_dqacc, |
| MAXFG_TAG_fcrep, |
| MAXFG_TAG_repsoc, |
| MAXFG_TAG_msoc, |
| MAXFG_TAG_vfsoc, |
| MAXFG_TAG_fstat, |
| MAXFG_TAG_avgt, |
| MAXFG_TAG_temp, |
| MAXFG_TAG_qh, |
| MAXFG_TAG_vcel, |
| MAXFG_TAG_avgv, |
| MAXFG_TAG_vfocv, |
| |
| /* from debug_regmap */ |
| MAXFG_TAG_rcomp0, |
| MAXFG_TAG_tempco, |
| }; |
| |
| void maxfg_init_fg_learn_capture_config(struct maxfg_capture_config *config, |
| struct maxfg_regmap *regmap, |
| struct maxfg_regmap *debug_regmap) |
| { |
| if (!config) { |
| pr_err("no config for logging FG learn\n"); |
| return; |
| } |
| |
| scnprintf(&config->name[0], MAX_FG_CAPTURE_CONFIG_NAME_MAX, "FG Learning Parameters"); |
| config->normal.tag = &fg_learning_param[0]; |
| config->normal.reg_cnt = MAX_FG_LEARNING_CONFIG_NORMAL_REGS; |
| config->normal.regmap = regmap; |
| |
| config->debug.tag = &fg_learning_param[MAX_FG_LEARNING_CONFIG_NORMAL_REGS]; |
| config->debug.reg_cnt = MAX_FG_LEARNING_CONFIG_DEBUG_REGS; |
| config->debug.regmap = debug_regmap; |
| |
| config->data_size = (config->normal.reg_cnt + config->debug.reg_cnt) * sizeof(u16); |
| } |
| |
| static inline int maxfg_read_registers(struct maxfg_capture_regs *regs, u16 *buffer) |
| { |
| int ret, idx; |
| |
| for (idx = 0; idx < regs->reg_cnt; idx++) { |
| ret = maxfg_reg_read(regs->regmap, regs->tag[idx], &buffer[idx]); |
| if (ret < 0) { |
| pr_err("failed to reg_tag(%u) %d\n", regs->tag[idx], ret); |
| return ret; |
| } |
| } |
| |
| return 0; |
| } |
| |
| int maxfg_alloc_capture_buf(struct maxfg_capture_buf *buf, int slots) |
| { |
| if ((slots & (slots-1)) || !buf || !buf->config.data_size || !slots) |
| return -EINVAL; |
| |
| buf->slots = 0; |
| buf->cb.buf = kzalloc(buf->config.data_size * slots, GFP_KERNEL); |
| if (!buf->cb.buf) |
| return -ENOMEM; |
| |
| buf->cb.head = 0; |
| buf->cb.tail = 0; |
| buf->slots = slots; |
| buf->latest_entry = NULL; |
| |
| mutex_init(&buf->cb_wr_lock); |
| mutex_init(&buf->cb_rd_lock); |
| |
| return 0; |
| } |
| |
| void maxfg_clear_capture_buf(struct maxfg_capture_buf *buf) |
| { |
| int head, tail; |
| |
| if (!buf || !buf->cb.buf) |
| return; |
| |
| mutex_lock(&buf->cb_wr_lock); |
| mutex_lock(&buf->cb_rd_lock); |
| |
| head = buf->cb.head; |
| tail = buf->cb.tail; |
| |
| if (CIRC_CNT(head, tail, buf->slots)) { |
| head = (head + 1) & (buf->slots - 1); |
| |
| smp_wmb(); |
| |
| /* make buffer empty by (head == tail) while preserving latest_entry as a seed */ |
| WRITE_ONCE(buf->cb.head, head); |
| WRITE_ONCE(buf->cb.tail, head); |
| } |
| |
| mutex_unlock(&buf->cb_rd_lock); |
| mutex_unlock(&buf->cb_wr_lock); |
| } |
| |
| void maxfg_free_capture_buf(struct maxfg_capture_buf *buf) |
| { |
| if (!buf || !buf->cb.buf ){ |
| pr_err("Invalid maxfg_capture_buf\n"); |
| return; |
| } |
| |
| if (buf->cb.buf && buf->slots > 0) |
| kfree(buf->cb.buf); |
| |
| mutex_destroy(&buf->cb_wr_lock); |
| mutex_destroy(&buf->cb_rd_lock); |
| |
| buf->cb.buf = NULL; |
| buf->slots = 0; |
| } |
| |
| int maxfg_capture_registers(struct maxfg_capture_buf *buf) |
| { |
| struct maxfg_capture_config *config = &buf->config; |
| u16* reg_val; |
| void* latest_entry; |
| int head, tail, ret; |
| |
| mutex_lock(&buf->cb_wr_lock); |
| |
| head = buf->cb.head; |
| tail = READ_ONCE(buf->cb.tail); |
| |
| /* if buffer is full, drop the last entry */ |
| if (CIRC_SPACE(head, tail, buf->slots) == 0) { |
| mutex_lock(&buf->cb_rd_lock); |
| WRITE_ONCE(buf->cb.tail, (tail + 1) & (buf->slots - 1)); |
| mutex_unlock(&buf->cb_rd_lock); |
| } |
| |
| reg_val = (u16*)&buf->cb.buf[head * buf->config.data_size]; |
| latest_entry = reg_val; |
| |
| ret = maxfg_read_registers(&config->normal, reg_val); |
| if (ret < 0) { |
| mutex_unlock(&buf->cb_wr_lock); |
| return ret; |
| } |
| |
| reg_val += config->normal.reg_cnt; |
| |
| ret = maxfg_read_registers(&config->debug, reg_val); |
| if (ret < 0) { |
| mutex_unlock(&buf->cb_wr_lock); |
| return ret; |
| } |
| |
| smp_wmb(); |
| WRITE_ONCE(buf->cb.head, (head + 1) & (buf->slots - 1)); |
| |
| buf->latest_entry = latest_entry; |
| mutex_unlock(&buf->cb_wr_lock); |
| |
| return 0; |
| } |
| |
| int maxfg_capture_to_cstr(struct maxfg_capture_config *config, u16* reg_val, |
| char* str_buf, int buf_len) |
| { |
| const struct maxfg_reg *fg_reg; |
| int len = 0, reg_idx = 0; |
| |
| for (reg_idx = 0; reg_idx < config->normal.reg_cnt && len < buf_len; reg_idx++) { |
| fg_reg = maxfg_find_by_tag(config->normal.regmap, config->normal.tag[reg_idx]); |
| if (!fg_reg) |
| return len; |
| |
| len += scnprintf(&str_buf[len], buf_len - len, "%02X:%04X ", fg_reg->reg, |
| reg_val[reg_idx]); |
| } |
| |
| reg_val += config->normal.reg_cnt; |
| |
| for (reg_idx = 0; reg_idx < config->debug.reg_cnt && len < buf_len; reg_idx++) { |
| fg_reg = maxfg_find_by_tag(config->debug.regmap, config->debug.tag[reg_idx]); |
| if (!fg_reg) |
| return len; |
| |
| len += scnprintf(&str_buf[len], buf_len - len, "%02X:%04X ", fg_reg->reg, |
| reg_val[reg_idx]); |
| } |
| |
| len += scnprintf(&str_buf[len], buf_len - len, "TS:%X", |
| (unsigned int)ktime_get_real_seconds()); |
| |
| return len; |
| } |
| |
| int maxfg_show_captured_buffer(struct maxfg_capture_buf *buf, char *str_buf, int buf_len) |
| { |
| struct maxfg_capture_config *config = &buf->config; |
| u16* reg_val; |
| int head, tail, count, to_end, idx, rt; |
| |
| if (!buf) |
| return -EINVAL; |
| |
| mutex_lock(&buf->cb_rd_lock); |
| |
| head = READ_ONCE(buf->cb.head); |
| tail = buf->cb.tail; |
| |
| count = CIRC_CNT(head, tail, buf->slots); |
| rt = scnprintf(&str_buf[0], buf_len, "%s (%d):\n", config->name, count); |
| |
| if (count == 0) |
| goto maxfg_show_captured_buffer_exit; |
| |
| to_end = CIRC_CNT_TO_END(head, tail, buf->slots); |
| |
| for (idx = 0; idx < to_end && rt < buf_len; idx++) { |
| reg_val = (u16*)&buf->cb.buf[(tail+idx) * buf->config.data_size]; |
| rt += maxfg_capture_to_cstr(config, reg_val, &str_buf[rt], buf_len - rt); |
| rt += scnprintf(&str_buf[rt], buf_len - rt, "\n"); |
| } |
| |
| count -= idx; |
| |
| for (idx = 0; idx < count && rt < buf_len; idx++) { |
| reg_val = (u16*)&buf->cb.buf[idx * buf->config.data_size]; |
| rt += maxfg_capture_to_cstr(config, reg_val, &str_buf[rt], buf_len - rt); |
| rt += scnprintf(&str_buf[rt], buf_len - rt, "\n"); |
| } |
| |
| maxfg_show_captured_buffer_exit: |
| mutex_unlock(&buf->cb_rd_lock); |
| |
| return rt; |
| } |
| |
| /* |
| * data in prev_val follows the order of fg_learning_param[] |
| * prev_val[0]: fcnom |
| * prev_val[1]: dpacc |
| * prev_val[2]: dqacc |
| * prev_val[7]: fstat |
| */ |
| bool maxfg_ce_relaxed(struct maxfg_regmap *regmap, const u16 relax_mask, const u16* prev_val) |
| { |
| u16 fstat, fcnom, dqacc, dpacc; |
| int ret; |
| |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_fstat, &fstat); |
| if (ret < 0) |
| return false; |
| |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_fcnom, &fcnom); |
| if (ret < 0) |
| return false; |
| |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_dpacc, &dpacc); |
| if (ret < 0) |
| return false; |
| |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_dqacc, &dqacc); |
| if (ret < 0) |
| return false; |
| |
| /* |
| * log when relaxed state changes, when fcnom, dpacc, dqacc change |
| * TODO: b/326639382 |
| * - log only when dpacc, dqacc or fcnom change and simply |
| * count the relaxation event otherwise. |
| */ |
| return (fstat & relax_mask) != (prev_val[7] & relax_mask) || |
| dpacc != prev_val[1] || dqacc != prev_val[2] || |
| fcnom != prev_val[0]; |
| } |
| |
| bool maxfg_is_relaxed(struct maxfg_regmap *regmap, u16 *fstat, u16 mask) |
| { |
| return maxfg_reg_read(regmap, MAXFG_TAG_fstat, fstat) == 0 && |
| (*fstat & mask); |
| } |
| |
| #define MAXFG_DR_VFSOC_DELTA_DEFAULT 0 |
| #define MAXFG_DR_LEARN_STAGE_MIN_DEFAULT 7 |
| #define MAXFG_DR_TEMP_MIN_DEFAULT 150 |
| #define MAXFG_DR_TEMP_MAX_DEFAULT 350 |
| #define MAXFG_DR_VFOCV_MV_INHIB_MIN_DEFAULT 3900 |
| #define MAXFG_DR_VFOCV_MV_INHIB_MAX_DEFAULT 4200 |
| #define MAXFG_DR_RELAX_INVALID 0xffff |
| #define MAXFG_DR_RELCFG_INHIBIT 0x1ff |
| #define MAXFG_DR_RELAX_FIRST false |
| |
| /* true if the device is allowed to relax given the parameters */ |
| bool maxfg_dynrel_can_relax(struct maxfg_dynrel_state *dr_state, |
| struct maxfg_regmap *regmap) |
| { |
| const bool has_vfocv_range = dr_state->vfocv_inhibit.min != |
| dr_state->vfocv_inhibit.max; |
| const bool has_temp_range = dr_state->temp_qual.min != |
| dr_state->temp_qual.max; |
| bool allowed = true; |
| u16 delta_vfsoc; |
| int ret; |
| |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_vfsoc, &dr_state->vfsoc_last); |
| if (ret < 0) |
| allowed = false; |
| |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_temp, &dr_state->temp_last); |
| if (ret < 0 || (has_temp_range && |
| (dr_state->temp_last < dr_state->temp_qual.min || |
| dr_state->temp_last > dr_state->temp_qual.max))) |
| allowed = false; |
| |
| /* exclude */ |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_vfocv, &dr_state->vfocv_last); |
| if (ret < 0 || (has_vfocv_range && |
| dr_state->vfocv_last >= dr_state->vfocv_inhibit.min && |
| dr_state->vfocv_last <= dr_state->vfocv_inhibit.max)) |
| allowed = false; |
| |
| /* |
| * define MAXFG_DR_RELAX_FIRST to true to always qualify the first |
| * relaxation after boot. Set it to false to qualify the first |
| * relaxation after boot with valid soc, temperature and inhibit ranges |
| * (if defined). |
| */ |
| if (dr_state->vfsoc_det == MAXFG_DR_RELAX_INVALID) |
| return MAXFG_DR_RELAX_FIRST || allowed; |
| |
| /* ->vfsoc_delta=0 will void this test */ |
| delta_vfsoc = abs(dr_state->vfsoc_last - dr_state->vfsoc_det); |
| if (delta_vfsoc < dr_state->vfsoc_delta) |
| allowed = false; |
| |
| return allowed; |
| } |
| |
| int maxfg_dynrel_mark_det(struct maxfg_dynrel_state *dr_state, |
| struct maxfg_regmap *regmap) |
| { |
| int ret; |
| |
| /* needs vfsoc, dpacc, dqacc for next round */ |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_vfsoc, &dr_state->vfsoc_det); |
| if (ret == 0) |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_dpacc, &dr_state->dpacc_det); |
| if (ret == 0) |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_dqacc, &dr_state->dqacc_det); |
| if (ret < 0) |
| return -EIO; |
| |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_temp, &dr_state->temp_det); |
| if (ret < 0) |
| dr_state->temp_det = 0xffff; |
| ret = maxfg_reg_read(regmap, MAXFG_TAG_vfocv, &dr_state->vfocv_det); |
| if (ret < 0) |
| dr_state->vfocv_det = 0xffff; |
| |
| return 0; |
| } |
| |
| int maxfg_dynrel_override_dxacc(struct maxfg_dynrel_state *dr_state, |
| struct maxfg_regmap *regmap) |
| { |
| int ret; |
| |
| /* ignore if there is no previous relaxation */ |
| if (dr_state->vfsoc_det == MAXFG_DR_RELAX_INVALID) |
| return -EINVAL; |
| |
| ret = maxfg_reg_write_verify(regmap, MAXFG_TAG_dpacc, |
| dr_state->dpacc_det); |
| if (ret == 0) |
| ret = maxfg_reg_write_verify(regmap, MAXFG_TAG_dqacc, |
| dr_state->dqacc_det); |
| |
| return ret; |
| } |
| |
| /* enable=false inhibit relaxation unless ->relcfg_allow==->relcfg_inhibit */ |
| int maxfg_dynrel_relaxcfg(struct maxfg_dynrel_state *dr_state, |
| struct maxfg_regmap *regmap, bool enable) |
| { |
| return maxfg_reg_write_verify(regmap, MAXFG_TAG_relaxcfg, enable ? |
| dr_state->relcfg_allow : dr_state->relcfg_inhibit); |
| } |
| |
| void maxfg_dynrel_init(struct maxfg_dynrel_state *dr_state, |
| struct device_node *node) |
| { |
| u16 value16; |
| u32 value; |
| int ret; |
| |
| dr_state->vfsoc_det = MAXFG_DR_RELAX_INVALID; |
| |
| ret = of_property_read_u16(node, "maxfg,dr_relcfg_inhibit", &value16); |
| if (ret < 0) |
| value16 = MAXFG_DR_RELCFG_INHIBIT; |
| dr_state->relcfg_inhibit = value16; |
| |
| /* if set override the one from the model */ |
| ret = of_property_read_u16(node, "maxfg,dr_relcfg_allow", &value16); |
| if (ret == 0) |
| dr_state->relcfg_allow = value16; |
| |
| /* default to override_mode if allow=relax will set if explicit */ |
| dr_state->override_mode = |
| dr_state->relcfg_inhibit == dr_state->relcfg_allow || |
| of_property_read_bool(node, "maxfg,dr_mode_override"); |
| |
| ret = of_property_read_u32(node, "maxfg,dr_vfsoc_delta", &value); |
| if (ret < 0) |
| value = MAXFG_DR_VFSOC_DELTA_DEFAULT; |
| dr_state->vfsoc_delta = percentage_to_reg(value); |
| |
| ret = of_property_read_u32(node, "maxfg,learn_stage_min", &value); |
| if (ret < 0) |
| value = MAXFG_DR_LEARN_STAGE_MIN_DEFAULT; |
| dr_state->learn_stage_min = value; |
| |
| ret = of_property_read_u32(node, "maxfg,dr_min_deci_temp_c", &value); |
| if (ret < 0) |
| value = MAXFG_DR_TEMP_MIN_DEFAULT; |
| dr_state->temp_qual.min = deci_deg_cel_to_reg(value); |
| ret = of_property_read_u32(node, "maxfg,dr_max_deci_temp_c", &value); |
| if (ret < 0) |
| value = MAXFG_DR_TEMP_MAX_DEFAULT; |
| dr_state->temp_qual.max = deci_deg_cel_to_reg(value); |
| |
| ret = of_property_read_u32(node, "maxfg,vfocv_inhibit_min_mv", &value); |
| if (ret < 0) |
| value = MAXFG_DR_VFOCV_MV_INHIB_MIN_DEFAULT; |
| dr_state->vfocv_inhibit.min = micro_volt_to_reg(value * 1000); |
| ret = of_property_read_u32(node, "maxfg,vfocv_inhibit_max_mv", &value); |
| if (ret < 0) |
| value = MAXFG_DR_VFOCV_MV_INHIB_MAX_DEFAULT; |
| dr_state->vfocv_inhibit.max = micro_volt_to_reg(value * 1000); |
| } |
| |
| void maxfg_dynrel_log_cfg(struct logbuffer *mon, struct device *dev, |
| const struct maxfg_dynrel_state *dr_state) |
| { |
| gbms_logbuffer_devlog(mon, dev, LOGLEVEL_INFO, 0, LOGLEVEL_INFO, |
| "dynrel_cfg temp=%d,%d vfocv=%d,%d delta=%d cfg=%x,%x dxacc=%d", |
| reg_to_deci_deg_cel(dr_state->temp_qual.min), |
| reg_to_deci_deg_cel(dr_state->temp_qual.max), |
| reg_to_micro_volt(dr_state->vfocv_inhibit.min) / 1000, |
| reg_to_micro_volt(dr_state->vfocv_inhibit.max) / 1000, |
| reg_to_percentage(dr_state->vfsoc_delta), |
| dr_state->relcfg_allow, dr_state->relcfg_inhibit, |
| dr_state->override_mode); |
| } |
| |
| static void maxfg_dynrel_log__(struct logbuffer *mon, struct device *dev, |
| const struct maxfg_dynrel_state *dr_state, |
| u16 fstat, u16 vfocv, u16 vfsoc, u16 temp) |
| { |
| int vfsoc_det; |
| |
| if (dr_state->vfsoc_det == MAXFG_DR_RELAX_INVALID) { |
| vfsoc_det = -1; |
| } else { |
| vfsoc_det = reg_to_percentage(dr_state->vfsoc_det); |
| } |
| |
| gbms_logbuffer_devlog(mon, dev, LOGLEVEL_INFO, 0, LOGLEVEL_INFO, |
| "dynrel fstat=%x sticky=%d allowed=%d vsoc_det=%d, temp=%d vfocv=%d vfsoc=%d dpacc_det=%d dqacc_det=%d", |
| fstat, dr_state->sticky_cnt, dr_state->relax_allowed, |
| vfsoc_det, reg_to_deci_deg_cel(temp), |
| reg_to_micro_volt(vfocv) / 1000, |
| reg_to_percentage(vfsoc), |
| dr_state->dpacc_det, dr_state->dqacc_det); |
| } |
| |
| |
| void maxfg_dynrel_log_rel(struct logbuffer *mon, struct device *dev, u16 fstat, |
| const struct maxfg_dynrel_state *dr_state) |
| { |
| maxfg_dynrel_log__(mon, dev, dr_state, fstat, dr_state->vfocv_det, |
| dr_state->vfsoc_det, dr_state->temp_det); |
| } |
| |
| void maxfg_dynrel_log(struct logbuffer *mon, struct device *dev, u16 fstat, |
| const struct maxfg_dynrel_state *dr_state) |
| { |
| maxfg_dynrel_log__(mon, dev, dr_state, fstat, dr_state->vfocv_last, |
| dr_state->vfsoc_last, dr_state->temp_last); |
| } |