| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (c) 2013-2019, The Linux Foundation. All rights reserved. |
| */ |
| |
| #define pr_fmt(fmt) "%s: " fmt, __func__ |
| |
| #include <linux/err.h> |
| #include <linux/of.h> |
| #include <linux/slab.h> |
| #include <linux/module.h> |
| #include <linux/types.h> |
| #include <linux/batterydata-lib.h> |
| #include <linux/of_batterydata.h> |
| #include <linux/power_supply.h> |
| |
| static int of_batterydata_read_lut(const struct device_node *np, |
| int max_cols, int max_rows, int *ncols, int *nrows, |
| int *col_legend_data, int *row_legend_data, |
| int *lut_data) |
| { |
| struct property *prop; |
| const __be32 *data; |
| int cols, rows, size, i, j, *out_values; |
| |
| prop = of_find_property(np, "qcom,lut-col-legend", NULL); |
| if (!prop) { |
| pr_err("%s: No col legend found\n", np->name); |
| return -EINVAL; |
| } else if (!prop->value) { |
| pr_err("%s: No col legend value found, np->name\n", np->name); |
| return -ENODATA; |
| } else if (prop->length > max_cols * sizeof(int)) { |
| pr_err("%s: Too many columns\n", np->name); |
| return -EINVAL; |
| } |
| |
| cols = prop->length/sizeof(int); |
| *ncols = cols; |
| data = prop->value; |
| for (i = 0; i < cols; i++) |
| *col_legend_data++ = be32_to_cpup(data++); |
| |
| rows = 0; |
| |
| prop = of_find_property(np, "qcom,lut-row-legend", NULL); |
| if (!prop || row_legend_data == NULL) { |
| /* single row lut */ |
| rows = 1; |
| } else if (!prop->value) { |
| pr_err("%s: No row legend value found\n", np->name); |
| return -ENODATA; |
| } else if (prop->length > max_rows * sizeof(int)) { |
| pr_err("%s: Too many rows\n", np->name); |
| return -EINVAL; |
| } |
| |
| if (rows != 1) { |
| rows = prop->length/sizeof(int); |
| *nrows = rows; |
| data = prop->value; |
| for (i = 0; i < rows; i++) |
| *row_legend_data++ = be32_to_cpup(data++); |
| } |
| |
| prop = of_find_property(np, "qcom,lut-data", NULL); |
| if (!prop) { |
| pr_err("prop 'qcom,lut-data' not found\n"); |
| return -EINVAL; |
| } |
| data = prop->value; |
| size = prop->length/sizeof(int); |
| if (size != cols * rows) { |
| pr_err("%s: data size mismatch, %dx%d != %d\n", |
| np->name, cols, rows, size); |
| return -EINVAL; |
| } |
| for (i = 0; i < rows; i++) { |
| out_values = lut_data + (max_cols * i); |
| for (j = 0; j < cols; j++) { |
| *out_values++ = be32_to_cpup(data++); |
| pr_debug("Value = %d\n", *(out_values-1)); |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int of_batterydata_read_sf_lut(struct device_node *data_node, |
| const char *name, struct sf_lut *lut) |
| { |
| struct device_node *node = of_find_node_by_name(data_node, name); |
| int rc; |
| |
| if (!lut) { |
| pr_debug("No lut provided, skipping\n"); |
| return 0; |
| } else if (!node) { |
| pr_err("Couldn't find %s node.\n", name); |
| return -EINVAL; |
| } |
| |
| rc = of_batterydata_read_lut(node, PC_CC_COLS, PC_CC_ROWS, |
| &lut->cols, &lut->rows, lut->row_entries, |
| lut->percent, *lut->sf); |
| if (rc) { |
| pr_err("Failed to read %s node.\n", name); |
| return rc; |
| } |
| |
| return 0; |
| } |
| |
| static int of_batterydata_read_pc_temp_ocv_lut(struct device_node *data_node, |
| const char *name, struct pc_temp_ocv_lut *lut) |
| { |
| struct device_node *node = of_find_node_by_name(data_node, name); |
| int rc; |
| |
| if (!lut) { |
| pr_debug("No lut provided, skipping\n"); |
| return 0; |
| } else if (!node) { |
| pr_err("Couldn't find %s node.\n", name); |
| return -EINVAL; |
| } |
| rc = of_batterydata_read_lut(node, PC_TEMP_COLS, PC_TEMP_ROWS, |
| &lut->cols, &lut->rows, lut->temp, lut->percent, |
| *lut->ocv); |
| if (rc) { |
| pr_err("Failed to read %s node.\n", name); |
| return rc; |
| } |
| |
| return 0; |
| } |
| |
| static int of_batterydata_read_ibat_temp_acc_lut(struct device_node *data_node, |
| const char *name, struct ibat_temp_acc_lut *lut) |
| { |
| struct device_node *node = of_find_node_by_name(data_node, name); |
| int rc; |
| |
| if (!lut) { |
| pr_debug("No lut provided, skipping\n"); |
| return 0; |
| } else if (!node) { |
| pr_debug("Couldn't find %s node.\n", name); |
| return 0; |
| } |
| rc = of_batterydata_read_lut(node, ACC_TEMP_COLS, ACC_IBAT_ROWS, |
| &lut->cols, &lut->rows, lut->temp, lut->ibat, |
| *lut->acc); |
| if (rc) { |
| pr_err("Failed to read %s node.\n", name); |
| return rc; |
| } |
| |
| return 0; |
| } |
| |
| static int of_batterydata_read_single_row_lut(struct device_node *data_node, |
| const char *name, struct single_row_lut *lut) |
| { |
| struct device_node *node = of_find_node_by_name(data_node, name); |
| int rc; |
| |
| if (!lut) { |
| pr_debug("No lut provided, skipping\n"); |
| return 0; |
| } else if (!node) { |
| pr_err("Couldn't find %s node.\n", name); |
| return -EINVAL; |
| } |
| |
| rc = of_batterydata_read_lut(node, MAX_SINGLE_LUT_COLS, 1, |
| &lut->cols, NULL, lut->x, NULL, lut->y); |
| if (rc) { |
| pr_err("Failed to read %s node.\n", name); |
| return rc; |
| } |
| |
| return 0; |
| } |
| |
| static int of_batterydata_read_batt_id_kohm(const struct device_node *np, |
| const char *propname, struct batt_ids *batt_ids) |
| { |
| struct property *prop; |
| const __be32 *data; |
| int num, i, *id_kohm = batt_ids->kohm; |
| |
| prop = of_find_property(np, "qcom,batt-id-kohm", NULL); |
| if (!prop) { |
| pr_err("%s: No battery id resistor found\n", np->name); |
| return -EINVAL; |
| } else if (!prop->value) { |
| pr_err("%s: No battery id resistor value found, np->name\n", |
| np->name); |
| return -ENODATA; |
| } else if (prop->length > MAX_BATT_ID_NUM * sizeof(__be32)) { |
| pr_err("%s: Too many battery id resistors\n", np->name); |
| return -EINVAL; |
| } |
| |
| num = prop->length/sizeof(__be32); |
| batt_ids->num = num; |
| data = prop->value; |
| for (i = 0; i < num; i++) |
| *id_kohm++ = be32_to_cpup(data++); |
| |
| return 0; |
| } |
| |
| #define OF_PROP_READ(property, qpnp_dt_property, node, rc, optional) \ |
| do { \ |
| if (rc) \ |
| break; \ |
| rc = of_property_read_u32(node, "qcom," qpnp_dt_property, \ |
| &property); \ |
| \ |
| if ((rc == -EINVAL) && optional) { \ |
| property = -EINVAL; \ |
| rc = 0; \ |
| } else if (rc) { \ |
| pr_err("Error reading " #qpnp_dt_property \ |
| " property rc = %d\n", rc); \ |
| } \ |
| } while (0) |
| |
| static int of_batterydata_load_battery_data(struct device_node *node, |
| int best_id_kohm, |
| struct bms_battery_data *batt_data) |
| { |
| int rc; |
| |
| rc = of_batterydata_read_single_row_lut(node, "qcom,fcc-temp-lut", |
| batt_data->fcc_temp_lut); |
| if (rc) |
| return rc; |
| |
| rc = of_batterydata_read_pc_temp_ocv_lut(node, |
| "qcom,pc-temp-ocv-lut", |
| batt_data->pc_temp_ocv_lut); |
| if (rc) |
| return rc; |
| |
| rc = of_batterydata_read_sf_lut(node, "qcom,rbatt-sf-lut", |
| batt_data->rbatt_sf_lut); |
| if (rc) |
| return rc; |
| |
| rc = of_batterydata_read_ibat_temp_acc_lut(node, "qcom,ibat-acc-lut", |
| batt_data->ibat_acc_lut); |
| if (rc) |
| return rc; |
| |
| rc = of_property_read_string(node, "qcom,battery-type", |
| &batt_data->battery_type); |
| if (rc) { |
| pr_err("Error reading qcom,battery-type property rc=%d\n", rc); |
| batt_data->battery_type = NULL; |
| return rc; |
| } |
| |
| OF_PROP_READ(batt_data->fcc, "fcc-mah", node, rc, false); |
| OF_PROP_READ(batt_data->default_rbatt_mohm, |
| "default-rbatt-mohm", node, rc, false); |
| OF_PROP_READ(batt_data->rbatt_capacitive_mohm, |
| "rbatt-capacitive-mohm", node, rc, false); |
| OF_PROP_READ(batt_data->flat_ocv_threshold_uv, |
| "flat-ocv-threshold-uv", node, rc, true); |
| OF_PROP_READ(batt_data->max_voltage_uv, |
| "max-voltage-uv", node, rc, true); |
| OF_PROP_READ(batt_data->cutoff_uv, "v-cutoff-uv", node, rc, true); |
| OF_PROP_READ(batt_data->iterm_ua, "chg-term-ua", node, rc, true); |
| OF_PROP_READ(batt_data->fastchg_current_ma, |
| "fastchg-current-ma", node, rc, true); |
| OF_PROP_READ(batt_data->fg_cc_cv_threshold_mv, |
| "fg-cc-cv-threshold-mv", node, rc, true); |
| |
| batt_data->batt_id_kohm = best_id_kohm; |
| |
| return rc; |
| } |
| |
| static int64_t of_batterydata_convert_battery_id_kohm(int batt_id_uv, |
| int rpull_up, int vadc_vdd) |
| { |
| int64_t resistor_value_kohm, denom; |
| |
| if (batt_id_uv == 0) { |
| /* vadc not correct or batt id line grounded, report 0 kohms */ |
| return 0; |
| } |
| /* calculate the battery id resistance reported via ADC */ |
| denom = div64_s64(vadc_vdd * 1000000LL, batt_id_uv) - 1000000LL; |
| |
| if (denom == 0) { |
| /* batt id connector might be open, return 0 kohms */ |
| return 0; |
| } |
| resistor_value_kohm = div64_s64(rpull_up * 1000000LL + denom/2, denom); |
| |
| pr_debug("batt id voltage = %d, resistor value = %lld\n", |
| batt_id_uv, resistor_value_kohm); |
| |
| return resistor_value_kohm; |
| } |
| |
| struct device_node *of_batterydata_get_best_profile( |
| const struct device_node *batterydata_container_node, |
| int batt_id_kohm, const char *batt_type) |
| { |
| struct batt_ids batt_ids; |
| struct device_node *node, *best_node = NULL; |
| const char *battery_type = NULL; |
| int delta = 0, best_delta = 0, best_id_kohm = 0, id_range_pct, |
| i = 0, rc = 0, limit = 0; |
| bool in_range = false; |
| |
| /* read battery id range percentage for best profile */ |
| rc = of_property_read_u32(batterydata_container_node, |
| "qcom,batt-id-range-pct", &id_range_pct); |
| |
| if (rc) { |
| if (rc == -EINVAL) { |
| id_range_pct = 0; |
| } else { |
| pr_err("failed to read battery id range\n"); |
| return ERR_PTR(-ENXIO); |
| } |
| } |
| |
| /* |
| * Find the battery data with a battery id resistor closest to this one |
| */ |
| for_each_child_of_node(batterydata_container_node, node) { |
| if (batt_type != NULL) { |
| rc = of_property_read_string(node, "qcom,battery-type", |
| &battery_type); |
| if (!rc && strcmp(battery_type, batt_type) == 0) { |
| best_node = node; |
| best_id_kohm = batt_id_kohm; |
| break; |
| } |
| } else { |
| rc = of_batterydata_read_batt_id_kohm(node, |
| "qcom,batt-id-kohm", |
| &batt_ids); |
| if (rc) |
| continue; |
| for (i = 0; i < batt_ids.num; i++) { |
| delta = abs(batt_ids.kohm[i] - batt_id_kohm); |
| limit = (batt_ids.kohm[i] * id_range_pct) / 100; |
| in_range = (delta <= limit); |
| /* |
| * Check if the delta is the lowest one |
| * and also if the limits are in range |
| * before selecting the best node. |
| */ |
| if ((delta < best_delta || !best_node) |
| && in_range) { |
| best_node = node; |
| best_delta = delta; |
| best_id_kohm = batt_ids.kohm[i]; |
| } |
| } |
| } |
| } |
| |
| if (best_node == NULL) { |
| pr_err("No battery data found\n"); |
| return best_node; |
| } |
| |
| /* check that profile id is in range of the measured batt_id */ |
| if (abs(best_id_kohm - batt_id_kohm) > |
| ((best_id_kohm * id_range_pct) / 100)) { |
| pr_err("out of range: profile id %d batt id %d pct %d\n", |
| best_id_kohm, batt_id_kohm, id_range_pct); |
| return NULL; |
| } |
| |
| rc = of_property_read_string(best_node, "qcom,battery-type", |
| &battery_type); |
| if (!rc) |
| pr_info("%s found\n", battery_type); |
| else |
| pr_info("%s found\n", best_node->name); |
| |
| return best_node; |
| } |
| |
| struct device_node *of_batterydata_get_best_aged_profile( |
| const struct device_node *batterydata_container_node, |
| int batt_id_kohm, int batt_age_level, int *avail_age_level) |
| { |
| struct batt_ids batt_ids; |
| struct device_node *node, *best_node = NULL; |
| const char *battery_type = NULL; |
| int delta = 0, best_id_kohm = 0, id_range_pct, i = 0, rc = 0, limit = 0; |
| u32 val; |
| bool in_range = false; |
| |
| /* read battery id range percentage for best profile */ |
| rc = of_property_read_u32(batterydata_container_node, |
| "qcom,batt-id-range-pct", &id_range_pct); |
| |
| if (rc) { |
| if (rc == -EINVAL) { |
| id_range_pct = 0; |
| } else { |
| pr_err("failed to read battery id range\n"); |
| return ERR_PTR(-ENXIO); |
| } |
| } |
| |
| /* |
| * Find the battery data with a battery id resistor closest to this one |
| */ |
| for_each_available_child_of_node(batterydata_container_node, node) { |
| val = 0; |
| of_property_read_u32(node, "qcom,batt-age-level", &val); |
| rc = of_batterydata_read_batt_id_kohm(node, |
| "qcom,batt-id-kohm", &batt_ids); |
| if (rc) |
| continue; |
| for (i = 0; i < batt_ids.num; i++) { |
| delta = abs(batt_ids.kohm[i] - batt_id_kohm); |
| limit = (batt_ids.kohm[i] * id_range_pct) / 100; |
| in_range = (delta <= limit); |
| |
| /* |
| * Check if the battery aging level matches and the |
| * limits are in range before selecting the best node. |
| */ |
| if ((batt_age_level == val || !best_node) && in_range) { |
| best_node = node; |
| best_id_kohm = batt_ids.kohm[i]; |
| *avail_age_level = val; |
| break; |
| } |
| } |
| } |
| |
| if (best_node == NULL) { |
| pr_err("No battery data found\n"); |
| return best_node; |
| } |
| |
| /* check that profile id is in range of the measured batt_id */ |
| if (abs(best_id_kohm - batt_id_kohm) > |
| ((best_id_kohm * id_range_pct) / 100)) { |
| pr_err("out of range: profile id %d batt id %d pct %d\n", |
| best_id_kohm, batt_id_kohm, id_range_pct); |
| return NULL; |
| } |
| |
| rc = of_property_read_string(best_node, "qcom,battery-type", |
| &battery_type); |
| if (!rc) |
| pr_info("%s age level %d found\n", battery_type, |
| *avail_age_level); |
| else |
| pr_info("%s age level %d found\n", best_node->name, |
| *avail_age_level); |
| |
| return best_node; |
| } |
| |
| int of_batterydata_get_aged_profile_count( |
| const struct device_node *batterydata_node, |
| int batt_id_kohm, int *count) |
| { |
| struct device_node *node; |
| int id_range_pct, i = 0, rc = 0, limit = 0, delta = 0; |
| bool in_range = false; |
| u32 batt_id; |
| |
| /* read battery id range percentage for best profile */ |
| rc = of_property_read_u32(batterydata_node, |
| "qcom,batt-id-range-pct", &id_range_pct); |
| if (rc) { |
| if (rc == -EINVAL) { |
| id_range_pct = 0; |
| } else { |
| pr_err("failed to read battery id range\n"); |
| return -ENXIO; |
| } |
| } |
| |
| for_each_available_child_of_node(batterydata_node, node) { |
| if (!of_find_property(node, "qcom,batt-age-level", NULL)) |
| continue; |
| |
| if (!of_find_property(node, "qcom,soh-range", NULL)) |
| continue; |
| |
| rc = of_property_read_u32(node, "qcom,batt-id-kohm", &batt_id); |
| if (rc) |
| continue; |
| |
| delta = abs(batt_id_kohm - batt_id); |
| limit = (batt_id_kohm * id_range_pct) / 100; |
| in_range = (delta <= limit); |
| |
| if (!in_range) { |
| pr_debug("not in range batt_id: %d\n", batt_id); |
| continue; |
| } |
| |
| i++; |
| } |
| |
| if (i <= 1) { |
| pr_err("Less number of profiles to support SOH\n"); |
| return -EINVAL; |
| } |
| |
| *count = i; |
| return 0; |
| } |
| |
| int of_batterydata_read_soh_aged_profiles( |
| const struct device_node *batterydata_node, |
| int batt_id_kohm, struct soh_range *soh_data) |
| { |
| struct device_node *node; |
| u32 val, temp[2], i = 0; |
| int rc, batt_id, id_range_pct, limit = 0, delta = 0; |
| bool in_range = false; |
| |
| if (!batterydata_node || !soh_data) |
| return -ENODEV; |
| |
| /* read battery id range percentage for best profile */ |
| rc = of_property_read_u32(batterydata_node, |
| "qcom,batt-id-range-pct", &id_range_pct); |
| if (rc) { |
| if (rc == -EINVAL) { |
| id_range_pct = 0; |
| } else { |
| pr_err("failed to read battery id range\n"); |
| return -ENXIO; |
| } |
| } |
| |
| for_each_available_child_of_node(batterydata_node, node) { |
| rc = of_property_read_u32(node, "qcom,batt-age-level", &val); |
| if (rc) |
| continue; |
| |
| rc = of_property_read_u32(node, "qcom,batt-id-kohm", &batt_id); |
| if (rc) |
| continue; |
| |
| delta = abs(batt_id_kohm - batt_id); |
| limit = (batt_id_kohm * id_range_pct) / 100; |
| in_range = (delta <= limit); |
| |
| if (!in_range) { |
| pr_debug("not in range batt_id: %d\n", batt_id); |
| continue; |
| } |
| |
| if (!of_find_property(node, "qcom,soh-range", NULL)) |
| continue; |
| |
| rc = of_property_count_elems_of_size(node, "qcom,soh-range", |
| sizeof(u32)); |
| if (rc != 2) { |
| pr_err("Incorrect element size for qcom,soh-range, rc=%d\n", |
| rc); |
| return -EINVAL; |
| } |
| |
| rc = of_property_read_u32_array(node, "qcom,soh-range", temp, |
| 2); |
| if (rc < 0) { |
| pr_err("Error in reading qcom,soh-range, rc=%d\n", rc); |
| return rc; |
| } |
| |
| if (temp[0] > 100 || temp[1] > 100 || (temp[0] > temp[1])) { |
| pr_err("Incorrect SOH range [%d %d]\n", temp[0], |
| temp[1]); |
| return -ERANGE; |
| } |
| |
| pr_debug("batt_age_level: %d soh: [%d %d]\n", val, temp[0], |
| temp[1]); |
| soh_data[i].batt_age_level = val; |
| soh_data[i].soh_min = temp[0]; |
| soh_data[i].soh_max = temp[1]; |
| i++; |
| } |
| |
| return 0; |
| } |
| |
| int of_batterydata_read_data(struct device_node *batterydata_container_node, |
| struct bms_battery_data *batt_data, |
| int batt_id_uv) |
| { |
| struct device_node *node, *best_node; |
| struct batt_ids batt_ids; |
| const char *battery_type = NULL; |
| int delta, best_delta, batt_id_kohm, rpull_up_kohm, |
| vadc_vdd_uv, best_id_kohm, i, rc = 0; |
| |
| node = batterydata_container_node; |
| OF_PROP_READ(rpull_up_kohm, "rpull-up-kohm", node, rc, false); |
| OF_PROP_READ(vadc_vdd_uv, "vref-batt-therm", node, rc, false); |
| if (rc) |
| return rc; |
| |
| batt_id_kohm = of_batterydata_convert_battery_id_kohm(batt_id_uv, |
| rpull_up_kohm, vadc_vdd_uv); |
| best_node = NULL; |
| best_delta = 0; |
| best_id_kohm = 0; |
| |
| /* |
| * Find the battery data with a battery id resistor closest to this one |
| */ |
| for_each_child_of_node(batterydata_container_node, node) { |
| rc = of_batterydata_read_batt_id_kohm(node, |
| "qcom,batt-id-kohm", |
| &batt_ids); |
| if (rc) |
| continue; |
| for (i = 0; i < batt_ids.num; i++) { |
| delta = abs(batt_ids.kohm[i] - batt_id_kohm); |
| if (delta < best_delta || !best_node) { |
| best_node = node; |
| best_delta = delta; |
| best_id_kohm = batt_ids.kohm[i]; |
| } |
| } |
| } |
| |
| if (best_node == NULL) { |
| pr_err("No battery data found\n"); |
| return -ENODATA; |
| } |
| rc = of_property_read_string(best_node, "qcom,battery-type", |
| &battery_type); |
| if (!rc) |
| pr_info("%s loaded\n", battery_type); |
| else |
| pr_info("%s loaded\n", best_node->name); |
| |
| return of_batterydata_load_battery_data(best_node, |
| best_id_kohm, batt_data); |
| } |
| |
| MODULE_LICENSE("GPL v2"); |