You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
520 lines
13 KiB
520 lines
13 KiB
/* Copyright (c) 2018 The Linux Foundation. All rights reserved.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 and
|
|
* only version 2 as published by the Free Software Foundation.
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
#define pr_fmt(fmt) "QG-K: %s: " fmt, __func__
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/device.h>
|
|
#include <linux/cdev.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/fcntl.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/of.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/uaccess.h>
|
|
#include <uapi/linux/qg-profile.h>
|
|
#include "qg-battery-profile.h"
|
|
#include "qg-profile-lib.h"
|
|
#include "qg-defs.h"
|
|
|
|
struct qg_battery_data {
|
|
/* battery-data class node */
|
|
dev_t dev_no;
|
|
struct class *battery_class;
|
|
struct device *battery_device;
|
|
struct cdev battery_cdev;
|
|
|
|
/* profile */
|
|
struct device_node *profile_node;
|
|
struct profile_table_data profile[TABLE_MAX];
|
|
};
|
|
|
|
struct tables {
|
|
int table_index;
|
|
char *table_name;
|
|
};
|
|
|
|
static struct tables table[] = {
|
|
{TABLE_SOC_OCV1, "qcom,pc-temp-v1-lut"},
|
|
{TABLE_SOC_OCV2, "qcom,pc-temp-v2-lut"},
|
|
{TABLE_FCC1, "qcom,fcc1-temp-lut"},
|
|
{TABLE_FCC2, "qcom,fcc2-temp-lut"},
|
|
{TABLE_Z1, "qcom,pc-temp-z1-lut"},
|
|
{TABLE_Z2, "qcom,pc-temp-z2-lut"},
|
|
{TABLE_Z3, "qcom,pc-temp-z3-lut"},
|
|
{TABLE_Z4, "qcom,pc-temp-z4-lut"},
|
|
{TABLE_Z5, "qcom,pc-temp-z5-lut"},
|
|
{TABLE_Z6, "qcom,pc-temp-z6-lut"},
|
|
{TABLE_Y1, "qcom,pc-temp-y1-lut"},
|
|
{TABLE_Y2, "qcom,pc-temp-y2-lut"},
|
|
{TABLE_Y3, "qcom,pc-temp-y3-lut"},
|
|
{TABLE_Y4, "qcom,pc-temp-y4-lut"},
|
|
{TABLE_Y5, "qcom,pc-temp-y5-lut"},
|
|
{TABLE_Y6, "qcom,pc-temp-y6-lut"},
|
|
};
|
|
|
|
static struct qg_battery_data *the_battery;
|
|
|
|
static int qg_battery_data_open(struct inode *inode, struct file *file)
|
|
{
|
|
struct qg_battery_data *battery = container_of(inode->i_cdev,
|
|
struct qg_battery_data, battery_cdev);
|
|
|
|
file->private_data = battery;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static long qg_battery_data_ioctl(struct file *file, unsigned int cmd,
|
|
unsigned long arg)
|
|
{
|
|
struct qg_battery_data *battery = file->private_data;
|
|
struct battery_params __user *bp_user =
|
|
(struct battery_params __user *)arg;
|
|
struct battery_params bp;
|
|
int rc = 0, soc, ocv_uv, fcc_mah, var, slope;
|
|
|
|
if (!battery->profile_node) {
|
|
pr_err("Battery data not set!\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (!bp_user) {
|
|
pr_err("Invalid battery-params user pointer\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (copy_from_user(&bp, bp_user, sizeof(bp))) {
|
|
pr_err("Failed in copy_from_user\n");
|
|
return -EFAULT;
|
|
}
|
|
|
|
switch (cmd) {
|
|
case BPIOCXSOC:
|
|
if (bp.table_index != TABLE_SOC_OCV1 &&
|
|
bp.table_index != TABLE_SOC_OCV2) {
|
|
pr_err("Invalid table index %d for SOC-OCV lookup\n",
|
|
bp.table_index);
|
|
rc = -EINVAL;
|
|
} else {
|
|
/* OCV is passed as deci-uV - 10^-4 V */
|
|
soc = interpolate_soc(&battery->profile[bp.table_index],
|
|
bp.batt_temp, UV_TO_DECIUV(bp.ocv_uv));
|
|
soc = CAP(QG_MIN_SOC, QG_MAX_SOC, soc);
|
|
rc = put_user(soc, &bp_user->soc);
|
|
if (rc < 0) {
|
|
pr_err("BPIOCXSOC: Failed rc=%d\n", rc);
|
|
goto ret_err;
|
|
}
|
|
pr_debug("BPIOCXSOC: lut=%s ocv=%d batt_temp=%d soc=%d\n",
|
|
battery->profile[bp.table_index].name,
|
|
bp.ocv_uv, bp.batt_temp, soc);
|
|
}
|
|
break;
|
|
case BPIOCXOCV:
|
|
if (bp.table_index != TABLE_SOC_OCV1 &&
|
|
bp.table_index != TABLE_SOC_OCV2) {
|
|
pr_err("Invalid table index %d for SOC-OCV lookup\n",
|
|
bp.table_index);
|
|
rc = -EINVAL;
|
|
} else {
|
|
ocv_uv = interpolate_var(
|
|
&battery->profile[bp.table_index],
|
|
bp.batt_temp, bp.soc);
|
|
ocv_uv = DECIUV_TO_UV(ocv_uv);
|
|
ocv_uv = CAP(QG_MIN_OCV_UV, QG_MAX_OCV_UV, ocv_uv);
|
|
rc = put_user(ocv_uv, &bp_user->ocv_uv);
|
|
if (rc < 0) {
|
|
pr_err("BPIOCXOCV: Failed rc=%d\n", rc);
|
|
goto ret_err;
|
|
}
|
|
pr_debug("BPIOCXOCV: lut=%s ocv=%d batt_temp=%d soc=%d\n",
|
|
battery->profile[bp.table_index].name,
|
|
ocv_uv, bp.batt_temp, bp.soc);
|
|
}
|
|
break;
|
|
case BPIOCXFCC:
|
|
if (bp.table_index != TABLE_FCC1 &&
|
|
bp.table_index != TABLE_FCC2) {
|
|
pr_err("Invalid table index %d for FCC lookup\n",
|
|
bp.table_index);
|
|
rc = -EINVAL;
|
|
} else {
|
|
fcc_mah = interpolate_single_row_lut(
|
|
&battery->profile[bp.table_index],
|
|
bp.batt_temp, DEGC_SCALE);
|
|
fcc_mah = CAP(QG_MIN_FCC_MAH, QG_MAX_FCC_MAH, fcc_mah);
|
|
rc = put_user(fcc_mah, &bp_user->fcc_mah);
|
|
if (rc) {
|
|
pr_err("BPIOCXFCC: Failed rc=%d\n", rc);
|
|
goto ret_err;
|
|
}
|
|
pr_debug("BPIOCXFCC: lut=%s batt_temp=%d fcc_mah=%d\n",
|
|
battery->profile[bp.table_index].name,
|
|
bp.batt_temp, fcc_mah);
|
|
}
|
|
break;
|
|
case BPIOCXVAR:
|
|
if (bp.table_index < TABLE_Z1 || bp.table_index >= TABLE_MAX) {
|
|
pr_err("Invalid table index %d for VAR lookup\n",
|
|
bp.table_index);
|
|
rc = -EINVAL;
|
|
} else {
|
|
var = interpolate_var(&battery->profile[bp.table_index],
|
|
bp.batt_temp, bp.soc);
|
|
var = CAP(QG_MIN_VAR, QG_MAX_VAR, var);
|
|
rc = put_user(var, &bp_user->var);
|
|
if (rc < 0) {
|
|
pr_err("BPIOCXVAR: Failed rc=%d\n", rc);
|
|
goto ret_err;
|
|
}
|
|
pr_debug("BPIOCXVAR: lut=%s var=%d batt_temp=%d soc=%d\n",
|
|
battery->profile[bp.table_index].name,
|
|
var, bp.batt_temp, bp.soc);
|
|
}
|
|
break;
|
|
case BPIOCXSLOPE:
|
|
if (bp.table_index != TABLE_SOC_OCV1 &&
|
|
bp.table_index != TABLE_SOC_OCV2) {
|
|
pr_err("Invalid table index %d for Slope lookup\n",
|
|
bp.table_index);
|
|
rc = -EINVAL;
|
|
} else {
|
|
slope = interpolate_slope(
|
|
&battery->profile[bp.table_index],
|
|
bp.batt_temp, bp.soc);
|
|
slope = CAP(QG_MIN_SLOPE, QG_MAX_SLOPE, slope);
|
|
rc = put_user(slope, &bp_user->slope);
|
|
if (rc) {
|
|
pr_err("BPIOCXSLOPE: Failed rc=%d\n", rc);
|
|
goto ret_err;
|
|
}
|
|
pr_debug("BPIOCXSLOPE: lut=%s soc=%d batt_temp=%d slope=%d\n",
|
|
battery->profile[bp.table_index].name,
|
|
bp.soc, bp.batt_temp, slope);
|
|
}
|
|
break;
|
|
default:
|
|
pr_err("IOCTL %d not supported\n", cmd);
|
|
rc = -EINVAL;
|
|
}
|
|
ret_err:
|
|
return rc;
|
|
}
|
|
|
|
static int qg_battery_data_release(struct inode *inode, struct file *file)
|
|
{
|
|
pr_debug("battery_data device closed\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct file_operations qg_battery_data_fops = {
|
|
.owner = THIS_MODULE,
|
|
.open = qg_battery_data_open,
|
|
.unlocked_ioctl = qg_battery_data_ioctl,
|
|
.compat_ioctl = qg_battery_data_ioctl,
|
|
.release = qg_battery_data_release,
|
|
};
|
|
|
|
static int get_length(struct device_node *node,
|
|
int *length, char *prop_name, bool ignore_null)
|
|
{
|
|
struct property *prop;
|
|
|
|
prop = of_find_property(node, prop_name, NULL);
|
|
if (!prop) {
|
|
if (ignore_null) {
|
|
*length = 1;
|
|
return 0;
|
|
}
|
|
pr_err("Failed to find %s property\n", prop_name);
|
|
return -ENODATA;
|
|
} else if (!prop->value) {
|
|
pr_err("Failed to find value for %s property\n", prop_name);
|
|
return -ENODATA;
|
|
}
|
|
|
|
*length = prop->length / sizeof(u32);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int qg_parse_battery_profile(struct qg_battery_data *battery)
|
|
{
|
|
int i, j, k, rows = 0, cols = 0, lut_length = 0, rc = 0;
|
|
struct device_node *node;
|
|
struct property *prop;
|
|
const __be32 *data;
|
|
|
|
for (i = 0; i < TABLE_MAX; i++) {
|
|
node = of_find_node_by_name(battery->profile_node,
|
|
table[i].table_name);
|
|
if (!node) {
|
|
pr_err("%s table not found\n", table[i].table_name);
|
|
rc = -ENODEV;
|
|
goto cleanup;
|
|
}
|
|
|
|
rc = get_length(node, &cols, "qcom,lut-col-legend", false);
|
|
if (rc < 0) {
|
|
pr_err("Failed to get col-length for %s table rc=%d\n",
|
|
table[i].table_name, rc);
|
|
goto cleanup;
|
|
}
|
|
|
|
rc = get_length(node, &rows, "qcom,lut-row-legend", true);
|
|
if (rc < 0) {
|
|
pr_err("Failed to get row-length for %s table rc=%d\n",
|
|
table[i].table_name, rc);
|
|
goto cleanup;
|
|
}
|
|
|
|
rc = get_length(node, &lut_length, "qcom,lut-data", false);
|
|
if (rc < 0) {
|
|
pr_err("Failed to get lut-length for %s table rc=%d\n",
|
|
table[i].table_name, rc);
|
|
goto cleanup;
|
|
}
|
|
|
|
if (lut_length != cols * rows) {
|
|
pr_err("Invalid lut-length for %s table\n",
|
|
table[i].table_name);
|
|
rc = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
|
|
battery->profile[i].name = kzalloc(strlen(table[i].table_name)
|
|
+ 1, GFP_KERNEL);
|
|
if (!battery->profile[i].name) {
|
|
rc = -ENOMEM;
|
|
goto cleanup;
|
|
}
|
|
|
|
strlcpy(battery->profile[i].name, table[i].table_name,
|
|
strlen(table[i].table_name));
|
|
battery->profile[i].rows = rows;
|
|
battery->profile[i].cols = cols;
|
|
|
|
if (rows != 1) {
|
|
battery->profile[i].row_entries = kcalloc(rows,
|
|
sizeof(*battery->profile[i].row_entries),
|
|
GFP_KERNEL);
|
|
if (!battery->profile[i].row_entries) {
|
|
rc = -ENOMEM;
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
battery->profile[i].col_entries = kcalloc(cols,
|
|
sizeof(*battery->profile[i].col_entries),
|
|
GFP_KERNEL);
|
|
if (!battery->profile[i].col_entries) {
|
|
rc = -ENOMEM;
|
|
goto cleanup;
|
|
}
|
|
|
|
battery->profile[i].data = kcalloc(rows,
|
|
sizeof(*battery->profile[i].data), GFP_KERNEL);
|
|
if (!battery->profile[i].data) {
|
|
rc = -ENOMEM;
|
|
goto cleanup;
|
|
}
|
|
|
|
for (j = 0; j < rows; j++) {
|
|
battery->profile[i].data[j] = kcalloc(cols,
|
|
sizeof(**battery->profile[i].data),
|
|
GFP_KERNEL);
|
|
if (!battery->profile[i].data[j]) {
|
|
rc = -ENOMEM;
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
/* read profile data */
|
|
rc = of_property_read_u32_array(node, "qcom,lut-col-legend",
|
|
battery->profile[i].col_entries, cols);
|
|
if (rc < 0) {
|
|
pr_err("Failed to read cols values for table %s rc=%d\n",
|
|
table[i].table_name, rc);
|
|
goto cleanup;
|
|
}
|
|
|
|
if (rows != 1) {
|
|
rc = of_property_read_u32_array(node,
|
|
"qcom,lut-row-legend",
|
|
battery->profile[i].row_entries, rows);
|
|
if (rc < 0) {
|
|
pr_err("Failed to read row values for table %s rc=%d\n",
|
|
table[i].table_name, rc);
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
prop = of_find_property(node, "qcom,lut-data", NULL);
|
|
if (!prop) {
|
|
pr_err("Failed to find lut-data\n");
|
|
rc = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
data = prop->value;
|
|
for (j = 0; j < rows; j++) {
|
|
for (k = 0; k < cols; k++)
|
|
battery->profile[i].data[j][k] =
|
|
be32_to_cpup(data++);
|
|
}
|
|
|
|
pr_debug("Profile table %s parsed rows=%d cols=%d\n",
|
|
battery->profile[i].name, battery->profile[i].rows,
|
|
battery->profile[i].cols);
|
|
}
|
|
|
|
return 0;
|
|
|
|
cleanup:
|
|
for (; i >= 0; i++) {
|
|
kfree(battery->profile[i].name);
|
|
kfree(battery->profile[i].row_entries);
|
|
kfree(battery->profile[i].col_entries);
|
|
for (j = 0; j < battery->profile[i].rows; j++) {
|
|
if (battery->profile[i].data)
|
|
kfree(battery->profile[i].data[j]);
|
|
}
|
|
kfree(battery->profile[i].data);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
int lookup_soc_ocv(u32 *soc, u32 ocv_uv, int batt_temp, bool charging)
|
|
{
|
|
u8 table_index = charging ? TABLE_SOC_OCV1 : TABLE_SOC_OCV2;
|
|
|
|
if (!the_battery || !the_battery->profile_node)
|
|
return -ENODEV;
|
|
|
|
*soc = interpolate_soc(&the_battery->profile[table_index],
|
|
batt_temp, UV_TO_DECIUV(ocv_uv));
|
|
|
|
*soc = CAP(0, 100, DIV_ROUND_CLOSEST(*soc, 100));
|
|
|
|
return 0;
|
|
}
|
|
|
|
int qg_get_nominal_capacity(u32 *nom_cap_uah, int batt_temp, bool charging)
|
|
{
|
|
u8 table_index = charging ? TABLE_FCC1 : TABLE_FCC2;
|
|
u32 fcc_mah;
|
|
|
|
if (!the_battery || !the_battery->profile_node)
|
|
return -ENODEV;
|
|
|
|
fcc_mah = interpolate_single_row_lut(
|
|
&the_battery->profile[table_index],
|
|
batt_temp, DEGC_SCALE);
|
|
fcc_mah = CAP(QG_MIN_FCC_MAH, QG_MAX_FCC_MAH, fcc_mah);
|
|
|
|
*nom_cap_uah = fcc_mah * 1000;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int qg_batterydata_init(struct device_node *profile_node)
|
|
{
|
|
int rc = 0;
|
|
struct qg_battery_data *battery;
|
|
|
|
battery = kzalloc(sizeof(*battery), GFP_KERNEL);
|
|
if (!battery)
|
|
return -ENOMEM;
|
|
|
|
battery->profile_node = profile_node;
|
|
|
|
/* char device to access battery-profile data */
|
|
rc = alloc_chrdev_region(&battery->dev_no, 0, 1, "qg_battery");
|
|
if (rc < 0) {
|
|
pr_err("Failed to allocate chrdev rc=%d\n", rc);
|
|
goto free_battery;
|
|
}
|
|
|
|
cdev_init(&battery->battery_cdev, &qg_battery_data_fops);
|
|
rc = cdev_add(&battery->battery_cdev, battery->dev_no, 1);
|
|
if (rc) {
|
|
pr_err("Failed to add battery_cdev rc=%d\n", rc);
|
|
goto unregister_chrdev;
|
|
}
|
|
|
|
battery->battery_class = class_create(THIS_MODULE, "qg_battery");
|
|
if (IS_ERR_OR_NULL(battery->battery_class)) {
|
|
pr_err("Failed to create qg-battery class\n");
|
|
rc = -ENODEV;
|
|
goto delete_cdev;
|
|
}
|
|
|
|
battery->battery_device = device_create(battery->battery_class,
|
|
NULL, battery->dev_no,
|
|
NULL, "qg_battery");
|
|
if (IS_ERR_OR_NULL(battery->battery_device)) {
|
|
pr_err("Failed to create battery_device device\n");
|
|
rc = -ENODEV;
|
|
goto delete_cdev;
|
|
}
|
|
|
|
/* parse the battery profile */
|
|
rc = qg_parse_battery_profile(battery);
|
|
if (rc < 0) {
|
|
pr_err("Failed to parse battery profile rc=%d\n", rc);
|
|
goto destroy_device;
|
|
}
|
|
|
|
the_battery = battery;
|
|
|
|
pr_info("QG Battery-profile loaded, '/dev/qg_battery' created!\n");
|
|
|
|
return 0;
|
|
|
|
destroy_device:
|
|
device_destroy(battery->battery_class, battery->dev_no);
|
|
delete_cdev:
|
|
cdev_del(&battery->battery_cdev);
|
|
unregister_chrdev:
|
|
unregister_chrdev_region(battery->dev_no, 1);
|
|
free_battery:
|
|
kfree(battery);
|
|
return rc;
|
|
}
|
|
|
|
void qg_batterydata_exit(void)
|
|
{
|
|
int i, j;
|
|
|
|
if (the_battery) {
|
|
/* unregister the device node */
|
|
device_destroy(the_battery->battery_class, the_battery->dev_no);
|
|
cdev_del(&the_battery->battery_cdev);
|
|
unregister_chrdev_region(the_battery->dev_no, 1);
|
|
|
|
/* delete all the battery profile memory */
|
|
for (i = 0; i < TABLE_MAX; i++) {
|
|
kfree(the_battery->profile[i].name);
|
|
kfree(the_battery->profile[i].row_entries);
|
|
kfree(the_battery->profile[i].col_entries);
|
|
for (j = 0; j < the_battery->profile[i].rows; j++) {
|
|
if (the_battery->profile[i].data)
|
|
kfree(the_battery->profile[i].data[j]);
|
|
}
|
|
kfree(the_battery->profile[i].data);
|
|
}
|
|
}
|
|
|
|
kfree(the_battery);
|
|
the_battery = NULL;
|
|
}
|
|
|