2018-04-07 13:50:43

by Craig Tatlor

[permalink] [raw]
Subject: [PATCH 1/3] power: supply: Add support for the Qualcomm Battery Monitoring System

This patch adds a driver for the BMS (Battery Monitoring System)
block of the PM8941 PMIC, it uses a lookup table defined in the
device tree to generate a capacity from the BMS supplied OCV, it
then ammends the coulomb counter to that to increase the accuracy
of the estimated capacity.

Signed-off-by: Craig Tatlor <[email protected]>
---
drivers/power/supply/Kconfig | 9 +
drivers/power/supply/Makefile | 1 +
drivers/power/supply/qcom_bms.c | 500 ++++++++++++++++++++++++++++++++
3 files changed, 510 insertions(+)
create mode 100644 drivers/power/supply/qcom_bms.c

diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig
index 428b426842f4..6c354c37bc55 100644
--- a/drivers/power/supply/Kconfig
+++ b/drivers/power/supply/Kconfig
@@ -82,6 +82,15 @@ config BATTERY_ACT8945A
Say Y here to enable support for power supply provided by
Active-semi ActivePath ACT8945A charger.

+config BATTERY_BMS
+ tristate "Qualcomm Battery Monitoring System driver"
+ depends on MFD_SPMI_PMIC || COMPILE_TEST
+ depends on OF
+ depends on REGMAP_SPMI
+ help
+ Say Y to include support for the Battery Monitoring hardware
+ found in some Qualcomm PM series PMICs.
+
config BATTERY_CPCAP
tristate "Motorola CPCAP PMIC battery driver"
depends on MFD_CPCAP && IIO
diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile
index e83aa843bcc6..04204174b047 100644
--- a/drivers/power/supply/Makefile
+++ b/drivers/power/supply/Makefile
@@ -21,6 +21,7 @@ obj-$(CONFIG_BATTERY_88PM860X) += 88pm860x_battery.o
obj-$(CONFIG_BATTERY_ACT8945A) += act8945a_charger.o
obj-$(CONFIG_BATTERY_AXP20X) += axp20x_battery.o
obj-$(CONFIG_CHARGER_AXP20X) += axp20x_ac_power.o
+obj-$(CONFIG_BATTERY_BMS) += qcom_bms.o
obj-$(CONFIG_BATTERY_CPCAP) += cpcap-battery.o
obj-$(CONFIG_BATTERY_DS2760) += ds2760_battery.o
obj-$(CONFIG_BATTERY_DS2780) += ds2780_battery.o
diff --git a/drivers/power/supply/qcom_bms.c b/drivers/power/supply/qcom_bms.c
new file mode 100644
index 000000000000..5aa6e906d1b9
--- /dev/null
+++ b/drivers/power/supply/qcom_bms.c
@@ -0,0 +1,500 @@
+// SPDX-License-Identifier: GPL
+
+/*
+ * Qualcomm Battery Monitoring System driver
+ *
+ * Copyright (C) 2018 Craig Tatlor <[email protected]>
+ */
+
+#include <linux/module.h>
+#include <linux/param.h>
+#include <linux/platform_device.h>
+#include <linux/power_supply.h>
+#include <linux/slab.h>
+#include <linux/of.h>
+#include <linux/of_platform.h>
+#include <linux/regmap.h>
+#include <linux/irq.h>
+#include <linux/interrupt.h>
+#include <linux/iio/consumer.h>
+
+#define REG_BMS_OCV_FOR_SOC_DATA0 0x90
+#define REG_BMS_SHDW_CC_DATA0 0xA8
+#define REG_BMS_CC_DATA_CTL 0x42
+#define REG_BMS_CC_CLEAR_CTL 0x4
+
+#define BMS_HOLD_OREG_DATA BIT(0)
+#define BMS_CLEAR_SHDW_CC BIT(6)
+
+#define CC_36_BIT_MASK 0xFFFFFFFFFLL
+#define SIGN_EXTEND_36_TO_64_MASK (-1LL ^ CC_36_BIT_MASK)
+
+#define BMS_CC_READING_RESOLUTION_N 542535
+#define BMS_CC_READING_RESOLUTION_D 10000
+#define BMS_CC_READING_TICKS 56
+#define BMS_SLEEP_CLK_HZ 32764
+
+#define SECONDS_PER_HOUR 3600
+#define TEMPERATURE_COLS 5
+#define MAX_CAPACITY_ROWS 50
+
+/* lookup table for ocv -> capacity conversion */
+struct bms_ocv_lut {
+ int rows;
+ s8 temp_legend[TEMPERATURE_COLS];
+ u8 capacity_legend[MAX_CAPACITY_ROWS];
+ u16 lut[MAX_CAPACITY_ROWS][TEMPERATURE_COLS];
+};
+
+/* lookup table for battery temperature -> fcc conversion */
+struct bms_fcc_lut {
+ s8 temp_legend[TEMPERATURE_COLS];
+ u16 lut[TEMPERATURE_COLS];
+};
+
+struct bms_device_info {
+ struct device *dev;
+ struct regmap *regmap;
+ struct power_supply *bat;
+ struct power_supply_desc bat_desc;
+ struct bms_ocv_lut ocv_lut;
+ struct bms_fcc_lut fcc_lut;
+ struct iio_channel *adc;
+ spinlock_t bms_output_lock;
+ int base_addr;
+
+ int ocv_thr_irq;
+ int ocv;
+};
+
+static s64 sign_extend_s36(uint64_t raw)
+{
+ raw = raw & CC_36_BIT_MASK;
+
+ return (raw >> 35) == 0LL ?
+ raw : (SIGN_EXTEND_36_TO_64_MASK | raw);
+}
+
+static unsigned int interpolate(int y0, int x0, int y1, int x1, int x)
+{
+ if (y0 == y1 || x == x0)
+ return y0;
+ if (x1 == x0 || x == x1)
+ return y1;
+
+ return y0 + ((y1 - y0) * (x - x0) / (x1 - x0));
+}
+
+static unsigned int between(int left, int right, int val)
+{
+ if (left <= val && val >= right)
+ return 1;
+
+ return 0;
+}
+
+static unsigned int interpolate_capacity(int temp, u16 ocv,
+ struct bms_ocv_lut ocv_lut)
+{
+ unsigned int pcj_minus_one = 0, pcj = 0;
+ int i, j;
+
+ for (j = 0; j < TEMPERATURE_COLS; j++)
+ if (temp <= ocv_lut.temp_legend[j])
+ break;
+
+ if (ocv >= ocv_lut.lut[0][j])
+ return ocv_lut.capacity_legend[0];
+
+ if (ocv <= ocv_lut.lut[ocv_lut.rows - 1][j - 1])
+ return ocv_lut.capacity_legend[ocv_lut.rows - 1];
+
+ for (i = 0; i < ocv_lut.rows - 1; i++) {
+ if (pcj == 0 && between(ocv_lut.lut[i][j],
+ ocv_lut.lut[i+1][j], ocv))
+ pcj = interpolate(ocv_lut.capacity_legend[i],
+ ocv_lut.lut[i][j],
+ ocv_lut.capacity_legend[i + 1],
+ ocv_lut.lut[i+1][j],
+ ocv);
+
+ if (pcj_minus_one == 0 && between(ocv_lut.lut[i][j-1],
+ ocv_lut.lut[i+1][j-1], ocv))
+ pcj_minus_one = interpolate(ocv_lut.capacity_legend[i],
+ ocv_lut.lut[i][j-1],
+ ocv_lut.capacity_legend[i + 1],
+ ocv_lut.lut[i+1][j-1],
+ ocv);
+
+ if (pcj && pcj_minus_one)
+ return interpolate(pcj_minus_one,
+ ocv_lut.temp_legend[j-1],
+ pcj,
+ ocv_lut.temp_legend[j],
+ temp);
+ }
+
+ if (pcj)
+ return pcj;
+
+ if (pcj_minus_one)
+ return pcj_minus_one;
+
+ return 100;
+}
+
+static unsigned long interpolate_fcc(int temp, struct bms_fcc_lut fcc_lut)
+{
+ int i, fcc_mv;
+
+ for (i = 0; i < TEMPERATURE_COLS; i++)
+ if (temp <= fcc_lut.temp_legend[i])
+ break;
+
+ fcc_mv = interpolate(fcc_lut.lut[i - 1],
+ fcc_lut.temp_legend[i - 1],
+ fcc_lut.lut[i],
+ fcc_lut.temp_legend[i],
+ temp);
+
+ return fcc_mv * 10000;
+}
+
+static int bms_lock_output_data(struct bms_device_info *di)
+{
+ int ret;
+
+ ret = regmap_update_bits(di->regmap, di->base_addr +
+ REG_BMS_CC_DATA_CTL,
+ BMS_HOLD_OREG_DATA, BMS_HOLD_OREG_DATA);
+ if (ret < 0) {
+ dev_err(di->dev, "failed to lock bms output: %d", ret);
+ return ret;
+ }
+
+ /*
+ * Sleep for 100 microseconds here to make sure there has
+ * been at least three cycles of the sleep clock so that
+ * the registers are correctly locked.
+ */
+ udelay(100);
+
+ return 0;
+}
+
+static int bms_unlock_output_data(struct bms_device_info *di)
+{
+ int ret;
+
+ ret = regmap_update_bits(di->regmap, di->base_addr +
+ REG_BMS_CC_DATA_CTL,
+ BMS_HOLD_OREG_DATA, 0);
+ if (ret < 0) {
+ dev_err(di->dev, "failed to unlock bms output: %d", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static int bms_read_ocv(struct bms_device_info *di, int *ocv)
+{
+ unsigned long flags;
+ int ret;
+ u16 read_ocv;
+
+ spin_lock_irqsave(&di->bms_output_lock, flags);
+
+ ret = bms_lock_output_data(di);
+ if (ret < 0)
+ goto err_lock;
+
+ ret = regmap_bulk_read(di->regmap, di->base_addr +
+ REG_BMS_OCV_FOR_SOC_DATA0, &read_ocv, 2);
+ if (ret < 0) {
+ dev_err(di->dev, "OCV read failed: %d", ret);
+ return ret;
+ }
+
+ dev_dbg(di->dev, "read OCV value of: %d", read_ocv);
+ *ocv = read_ocv;
+
+ ret = bms_unlock_output_data(di);
+
+err_lock:
+ spin_unlock_irqrestore(&di->bms_output_lock, flags);
+
+ return ret;
+}
+
+static int bms_read_cc(struct bms_device_info *di, s64 *cc_uah)
+{
+ unsigned long flags;
+ int ret;
+ s64 cc_raw_s36, cc_raw, cc_uv, cc_pvh;
+
+ spin_lock_irqsave(&di->bms_output_lock, flags);
+
+ ret = bms_lock_output_data(di);
+ if (ret < 0)
+ return ret;
+
+ ret = regmap_bulk_read(di->regmap, di->base_addr +
+ REG_BMS_SHDW_CC_DATA0,
+ &cc_raw_s36, 5);
+ if (ret < 0) {
+ dev_err(di->dev, "coulomb counter read failed: %d", ret);
+ return ret;
+ }
+
+ ret = bms_unlock_output_data(di);
+ if (ret < 0)
+ return ret;
+
+ spin_unlock_irqrestore(&di->bms_output_lock, flags);
+
+ cc_raw = sign_extend_s36(cc_raw_s36);
+
+ /* convert raw to uv */
+ cc_uv = div_s64(cc_raw * BMS_CC_READING_RESOLUTION_N,
+ BMS_CC_READING_RESOLUTION_D);
+
+ /* convert uv to pvh */
+ cc_pvh = div_s64(cc_uv * BMS_CC_READING_TICKS * 100000,
+ BMS_SLEEP_CLK_HZ * SECONDS_PER_HOUR) * 10;
+
+ /* divide by impedance */
+ *cc_uah = div_s64(cc_pvh, 10000);
+
+ dev_dbg(di->dev, "read coulomb counter value of: %lld", *cc_uah);
+
+ return 0;
+}
+
+static void bms_reset_cc(struct bms_device_info *di)
+{
+ int ret;
+ unsigned long flags;
+
+ spin_lock_irqsave(&di->bms_output_lock, flags);
+
+ ret = regmap_update_bits(di->regmap, di->base_addr +
+ REG_BMS_CC_CLEAR_CTL,
+ BMS_CLEAR_SHDW_CC,
+ BMS_CLEAR_SHDW_CC);
+ if (ret < 0) {
+ dev_err(di->dev, "coulomb counter reset failed: %d", ret);
+ goto err_lock;
+ }
+
+ /* wait two sleep cycles for cc to reset */
+ udelay(100);
+
+ ret = regmap_update_bits(di->regmap, di->base_addr +
+ REG_BMS_CC_CLEAR_CTL,
+ BMS_CLEAR_SHDW_CC, 0);
+ if (ret < 0)
+ dev_err(di->dev, "coulomb counter re-enable failed: %d", ret);
+
+err_lock:
+ spin_unlock_irqrestore(&di->bms_output_lock, flags);
+}
+
+static int bms_calculate_capacity(struct bms_device_info *di, int *capacity)
+{
+ unsigned long ocv_capacity, fcc;
+ int ret, temp, temp_degc;
+ s64 cc, capacity_nodiv;
+
+ ret = iio_read_channel_raw(di->adc, &temp);
+ if (ret < 0) {
+ dev_err(di->dev, "failed to read temperature: %d", ret);
+ return ret;
+ }
+
+ temp_degc = (temp + 500) / 1000;
+
+ ret = bms_read_cc(di, &cc);
+ if (ret < 0) {
+ dev_err(di->dev, "failed to read coulomb counter: %d", ret);
+ return ret;
+ }
+
+ ocv_capacity = interpolate_capacity(temp_degc, (di->ocv + 5) / 10,
+ di->ocv_lut);
+ fcc = interpolate_fcc(temp_degc, di->fcc_lut);
+
+ capacity_nodiv = ((fcc * ocv_capacity) / 100 - cc) * 100;
+ *capacity = div64_ul(capacity_nodiv, fcc);
+
+ return 0;
+}
+
+
+
+/*
+ * Return power_supply property
+ */
+static int bms_get_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
+{
+ struct bms_device_info *di = power_supply_get_drvdata(psy);
+ int ret;
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_CAPACITY:
+ ret = bms_calculate_capacity(di, &val->intval);
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+
+ if (val->intval == INT_MAX || val->intval == INT_MIN)
+ ret = -EINVAL;
+
+ return ret;
+}
+
+static enum power_supply_property bms_props[] = {
+ POWER_SUPPLY_PROP_CAPACITY,
+};
+
+static irqreturn_t bms_ocv_thr_irq_handler(int irq, void *dev_id)
+{
+ struct bms_device_info *di = dev_id;
+
+ if (bms_read_ocv(di, &di->ocv) < 0)
+ return IRQ_HANDLED;
+
+ bms_reset_cc(di);
+ return IRQ_HANDLED;
+}
+
+static int bms_probe(struct platform_device *pdev)
+{
+ struct power_supply_config psy_cfg = {};
+ struct bms_device_info *di;
+ int ret;
+
+ di = devm_kzalloc(&pdev->dev, sizeof(*di), GFP_KERNEL);
+ if (!di)
+ return -ENOMEM;
+
+ platform_set_drvdata(pdev, di);
+
+ di->dev = &pdev->dev;
+
+ di->regmap = dev_get_regmap(pdev->dev.parent, NULL);
+ if (!di->regmap) {
+ dev_err(di->dev, "Unable to get regmap");
+ return -EINVAL;
+ }
+
+ di->adc = devm_iio_channel_get(&pdev->dev, "temp");
+ if (IS_ERR(di->adc))
+ return PTR_ERR(di->adc);
+
+ ret = of_property_read_u32(di->dev->of_node, "reg", &di->base_addr);
+ if (ret < 0)
+ return ret;
+
+ ret = of_property_read_u8_array(di->dev->of_node,
+ "qcom,ocv-temp-legend",
+ (u8 *)di->ocv_lut.temp_legend,
+ TEMPERATURE_COLS);
+ if (ret < 0) {
+ dev_err(di->dev, "no ocv temperature legend found");
+ return ret;
+ }
+
+ di->ocv_lut.rows = of_property_read_variable_u8_array(di->dev->of_node,
+ "qcom,ocv-capacity-legend",
+ di->ocv_lut.capacity_legend, 0,
+ MAX_CAPACITY_ROWS);
+ if (di->ocv_lut.rows < 0) {
+ dev_err(di->dev, "no ocv capacity legend found");
+ return ret;
+ }
+
+ ret = of_property_read_variable_u16_array(di->dev->of_node,
+ "qcom,ocv-lut",
+ (u16 *)di->ocv_lut.lut,
+ TEMPERATURE_COLS,
+ TEMPERATURE_COLS *
+ MAX_CAPACITY_ROWS);
+ if (ret < 0) {
+ dev_err(di->dev, "no ocv lut array found");
+ return ret;
+ }
+
+ ret = of_property_read_u8_array(di->dev->of_node,
+ "qcom,fcc-temp-legend",
+ (u8 *)di->fcc_lut.temp_legend,
+ TEMPERATURE_COLS);
+ if (ret < 0) {
+ dev_err(di->dev, "no fcc temperature legend found");
+ return ret;
+ }
+
+ ret = of_property_read_u16_array(di->dev->of_node,
+ "qcom,fcc-lut",
+ di->fcc_lut.lut,
+ TEMPERATURE_COLS);
+ if (ret < 0) {
+ dev_err(di->dev, "no fcc lut array found");
+ return ret;
+ }
+
+ ret = bms_read_ocv(di, &di->ocv);
+ if (ret < 0) {
+ dev_err(di->dev, "failed to read initial ocv: %d", ret);
+ return ret;
+ }
+
+ di->ocv_thr_irq = platform_get_irq_byname(pdev, "ocv_thr");
+
+ ret = devm_request_irq(di->dev, di->ocv_thr_irq,
+ bms_ocv_thr_irq_handler,
+ IRQF_TRIGGER_RISING,
+ pdev->name, di);
+ if (ret < 0) {
+ dev_err(di->dev, "failed to request handler for ocv threshold IRQ");
+ return ret;
+ }
+
+ spin_lock_init(&di->bms_output_lock);
+
+ di->bat_desc.name = "bms";
+ di->bat_desc.type = POWER_SUPPLY_TYPE_BATTERY;
+ di->bat_desc.properties = bms_props;
+ di->bat_desc.num_properties = ARRAY_SIZE(bms_props);
+ di->bat_desc.get_property = bms_get_property;
+
+ psy_cfg.drv_data = di;
+ di->bat = devm_power_supply_register(di->dev, &di->bat_desc, &psy_cfg);
+ if (IS_ERR(di->bat))
+ return PTR_ERR(di->bat);
+
+ return 0;
+}
+
+static const struct of_device_id bms_of_match[] = {
+ {.compatible = "qcom,pm8941-bms", },
+ { },
+};
+MODULE_DEVICE_TABLE(of, bms_of_match);
+
+static struct platform_driver bms_driver = {
+ .probe = bms_probe,
+ .driver = {
+ .name = "qcom-bms",
+ .of_match_table = of_match_ptr(bms_of_match),
+ },
+};
+module_platform_driver(bms_driver);
+
+MODULE_AUTHOR("Craig Tatlor <[email protected]>");
+MODULE_DESCRIPTION("Qualcomm BMS driver");
+MODULE_LICENSE("GPL");
--
2.17.0



2018-04-07 13:51:10

by Craig Tatlor

[permalink] [raw]
Subject: [PATCH 2/3] dt-bindings: power: supply: qcom_bms: Add bindings

Add bindings for the Qualcomm battery measurement system.

Signed-off-by: Craig Tatlor <[email protected]>
---
.../bindings/power/supply/qcom_bms.txt | 93 +++++++++++++++++++
1 file changed, 93 insertions(+)
create mode 100644 Documentation/devicetree/bindings/power/supply/qcom_bms.txt

diff --git a/Documentation/devicetree/bindings/power/supply/qcom_bms.txt b/Documentation/devicetree/bindings/power/supply/qcom_bms.txt
new file mode 100644
index 000000000000..6296399edc09
--- /dev/null
+++ b/Documentation/devicetree/bindings/power/supply/qcom_bms.txt
@@ -0,0 +1,93 @@
+Qualcomm Battery Measurement System
+
+The Qualcomm Battery Measurement System is found inside of Qualcomm PM8941
+PMICs. It provides OCV and coulomb counter registers that allow the kernel
+to infer a capacity level.
+
+Required properties:
+- compatible: Should contain "qcom,pm8941-bms".
+- reg: Specifies the SPMI address and length of the
+ controller's registers.
+- interrupts: OCV threshold interrupt.
+- io-channels: Should contain IIO channel specifier for the
+ ADC channel that reports battery temperature.
+- io-channel-names: Should contain "temp".
+- qcom,fcc-temp-legend: An array containing the temperature, in degC,
+ for each column of the FCC lookup table.
+- qcom,fcc-lut: An array of FCC values in mah, one entry for each
+ temperature defined in in qcom,fcc-temp-legend.
+- qcom,ocv-temp-legend: An array containing the temperature, in degC,
+ for each column of the OCV lookup table.
+- qcom,ocv-capacity-legend: An array containing the capacity for each
+ row of the OCV lookup table.
+- qcom,ocv-lut: An array of OCV values in mV, one entry for each
+ capacity defined in qcom,ocv-capacity-legend.
+
+Example:
+
+ pm8941_vadc: vadc@3100 {
+ compatible = "qcom,spmi-vadc";
+ reg = <0x3100>;
+ interrupts = <0x0 0x31 0x0 IRQ_TYPE_EDGE_RISING>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+ #io-channel-cells = <1>;
+
+ bat_temp {
+ reg = <VADC_LR_MUX1_BAT_THERM>;
+ };
+ };
+
+ bms@4000 {
+ compatible = "qcom,pm8941-bms";
+ reg = <0x4000>;
+ interrupts = <0x0 0x40 0x4 IRQ_TYPE_EDGE_RISING>;
+ interrupt-names = "ocv_thr";
+
+ io-channels = <&pm8941_vadc VADC_LR_MUX1_BAT_THERM>;
+ io-channel-names = "temp";
+
+ qcom,fcc-temp-legend = /bits/ 8 <(-10) 0 25 50 65>;
+ qcom,fcc-lut = /bits/ 16 <6010 6070 6680 6780 6670>;
+
+ qcom,ocv-capacity-legend = /bits/ 8 <100 95 90 85
+ 80 75 70 65
+ 60 55 50 45
+ 40 35 30 25
+ 20 15 10 9
+ 8 7 6 5 4
+ 3 2 1 0>;
+
+ qcom,ocv-temp-legend = /bits/ 8 <(-10) 0 25 50 65>;
+ qcom,ocv-lut = /bits/ 16 <4288 4288 4306 4315 4315
+ 4261 4241 4259 4266 4246
+ 4201 4181 4201 4207 4187
+ 4153 4133 4150 4155 4135
+ 4105 4085 4100 4104 4084
+ 4058 4038 4052 4058 4038
+ 4012 3992 4004 4014 3994
+ 3970 3950 3959 3971 3951
+ 3931 3911 3915 3927 3907
+ 3899 3879 3880 3884 3864
+ 3873 3853 3851 3853 3833
+ 3848 3828 3827 3829 3809
+ 3829 3809 3808 3809 3789
+ 3815 3795 3791 3791 3771
+ 3801 3781 3775 3772 3752
+ 3785 3765 3751 3746 3726
+ 3767 3747 3727 3719 3699
+ 3750 3730 3702 3692 3672
+ 3728 3708 3680 3672 3652
+ 3720 3700 3676 3665 3645
+ 3712 3692 3670 3660 3645
+ 3695 3675 3658 3648 3633
+ 3662 3647 3629 3620 3610
+ 3620 3605 3589 3580 3570
+ 3562 3552 3538 3529 3519
+ 3490 3480 3474 3470 3465
+ 3403 3398 3388 3380 3375
+ 3320 3300 3255 3221 3206
+ 3000 3000 3000 3000 3000>;
+ };
+ };
+};
--
2.17.0


2018-04-07 13:53:49

by Craig Tatlor

[permalink] [raw]
Subject: [PATCH 3/3] MAINTAINERS: Add entry for the Qualcomm BMS

Signed-off-by: Craig Tatlor <[email protected]>
---
MAINTAINERS | 6 ++++++
1 file changed, 6 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 0c3ad62c638c..b7ffa9268384 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -11605,6 +11605,12 @@ W: http://wireless.kernel.org/en/users/Drivers/ath9k
S: Supported
F: drivers/net/wireless/ath/ath9k/

+QUALCOMM BATTERY MEASUREMENT SYSTEM
+M: Craig Tatlor <[email protected]>
+L: [email protected]
+S: Maintained
+F: drivers/power/supply/qcom_bms.c
+
QUALCOMM CAMERA SUBSYSTEM DRIVER
M: Todor Tomov <[email protected]>
L: [email protected]
--
2.17.0


2018-04-26 11:41:41

by Linus Walleij

[permalink] [raw]
Subject: Re: [PATCH 2/3] dt-bindings: power: supply: qcom_bms: Add bindings

On Sat, Apr 7, 2018 at 3:46 PM, Craig Tatlor <[email protected]> wrote:

This is very interesting bindings!

> Add bindings for the Qualcomm battery measurement system.
>
> Signed-off-by: Craig Tatlor <[email protected]>

Please expand on acronyms as requested by Rob.

> +Required properties:
> +- compatible: Should contain "qcom,pm8941-bms".
> +- reg: Specifies the SPMI address and length of the
> + controller's registers.
> +- interrupts: OCV threshold interrupt.
> +- io-channels: Should contain IIO channel specifier for the
> + ADC channel that reports battery temperature.
> +- io-channel-names: Should contain "temp".

These are fine (and good) as bindings go.

> +- qcom,fcc-temp-legend: An array containing the temperature, in degC,
> + for each column of the FCC lookup table.
> +- qcom,fcc-lut: An array of FCC values in mah, one entry for each
> + temperature defined in in qcom,fcc-temp-legend.
> +- qcom,ocv-temp-legend: An array containing the temperature, in degC,
> + for each column of the OCV lookup table.
> +- qcom,ocv-capacity-legend: An array containing the capacity for each
> + row of the OCV lookup table.
> +- qcom,ocv-lut: An array of OCV values in mV, one entry for each
> + capacity defined in qcom,ocv-capacity-legend.

I wonder if these are really Qualcomm-specific.

Can't we just cut the "qcom,*" prefix from all and simply define
these lookup tables in a way (with units) that makes sense to all
kinds of battery capacities?

Maybe spell them out though and avoid the abbreviations.

Just put those in the already existing
Documentation/devicetree/bindings/power/supply/battery.txt
so others can reuse them from there.

You will see that those bindings does not abbreviate but
spells out the natural sciences definitions to it's a bliss to
read for everyone.

Yours,
Linus Walleij

2018-04-26 18:28:34

by Bjorn Andersson

[permalink] [raw]
Subject: Re: [PATCH 1/3] power: supply: Add support for the Qualcomm Battery Monitoring System

On Sat 07 Apr 10:57 PDT 2018, Craig Tatlor wrote:

Looks pretty good, just some minor things inline.

[..]
> diff --git a/drivers/power/supply/qcom_bms.c b/drivers/power/supply/qcom_bms.c
> new file mode 100644
> index 000000000000..f31c99c03518
> --- /dev/null
> +++ b/drivers/power/supply/qcom_bms.c
> @@ -0,0 +1,500 @@
> +// SPDX-License-Identifier: GPL
> +
> +/*
> + * Qualcomm Battery Monitoring System driver
> + *
> + * Copyright (C) 2018 Craig Tatlor <[email protected]>
> + */
> +
> +#include <linux/module.h>
> +#include <linux/param.h>

param.h unused?

> +#include <linux/platform_device.h>
> +#include <linux/power_supply.h>
> +#include <linux/slab.h>
> +#include <linux/of.h>
> +#include <linux/of_platform.h>
> +#include <linux/regmap.h>
> +#include <linux/irq.h>
> +#include <linux/interrupt.h>
> +#include <linux/iio/consumer.h>
> +
> +#define REG_BMS_OCV_FOR_SOC_DATA0 0x90
> +#define REG_BMS_SHDW_CC_DATA0 0xA8
> +#define REG_BMS_CC_DATA_CTL 0x42
> +#define REG_BMS_CC_CLEAR_CTL 0x4
> +
> +#define BMS_HOLD_OREG_DATA BIT(0)
> +#define BMS_CLEAR_SHDW_CC BIT(6)
> +
> +#define CC_36_BIT_MASK 0xFFFFFFFFFLL

How about GENMASK_ULL() ?

> +#define SIGN_EXTEND_36_TO_64_MASK (-1LL ^ CC_36_BIT_MASK)
> +
> +#define BMS_CC_READING_RESOLUTION_N 542535
> +#define BMS_CC_READING_RESOLUTION_D 10000
> +#define BMS_CC_READING_TICKS 56
> +#define BMS_SLEEP_CLK_HZ 32764
> +
> +#define SECONDS_PER_HOUR 3600
> +#define TEMPERATURE_COLS 5
> +#define MAX_CAPACITY_ROWS 50
> +
> +/* lookup table for ocv -> capacity conversion */
> +struct bms_ocv_lut {
> + int rows;
> + s8 temp_legend[TEMPERATURE_COLS];
> + u8 capacity_legend[MAX_CAPACITY_ROWS];
> + u16 lut[MAX_CAPACITY_ROWS][TEMPERATURE_COLS];
> +};
> +
> +/* lookup table for battery temperature -> fcc conversion */
> +struct bms_fcc_lut {
> + s8 temp_legend[TEMPERATURE_COLS];
> + u16 lut[TEMPERATURE_COLS];
> +};
> +
> +struct bms_device_info {
> + struct device *dev;
> + struct regmap *regmap;
> + struct power_supply *bat;

bat is local to bms_probe.

> + struct power_supply_desc bat_desc;
> + struct bms_ocv_lut ocv_lut;
> + struct bms_fcc_lut fcc_lut;
> + struct iio_channel *adc;
> + spinlock_t bms_output_lock;
> + int base_addr;

u32, as you're passing a pointer to this into of_property_read_u32().

> +
> + int ocv_thr_irq;
> + int ocv;
> +};
> +
> +static s64 sign_extend_s36(uint64_t raw)
> +{
> + raw = raw & CC_36_BIT_MASK;

raw &=

> +
> + return (raw >> 35) == 0LL ?
> + raw : (SIGN_EXTEND_36_TO_64_MASK | raw);
> +}
> +
> +static unsigned int interpolate(int y0, int x0, int y1, int x1, int x)
> +{
> + if (y0 == y1 || x == x0)
> + return y0;
> + if (x1 == x0 || x == x1)
> + return y1;
> +
> + return y0 + ((y1 - y0) * (x - x0) / (x1 - x0));
> +}
> +
> +static unsigned int between(int left, int right, int val)

Return bool and use true/false instead.

> +{
> + if (left <= val && val <= right)
> + return 1;
> +
> + return 0;
> +}
> +
> +static unsigned int interpolate_capacity(int temp, u16 ocv,
> + struct bms_ocv_lut ocv_lut)
> +{
> + unsigned int pcj_minus_one = 0, pcj = 0;
> + int i, j;
> +
> + for (j = 0; j < TEMPERATURE_COLS; j++)
> + if (temp <= ocv_lut.temp_legend[j])
> + break;
> +
> + if (ocv >= ocv_lut.lut[0][j])
> + return ocv_lut.capacity_legend[0];
> +
> + if (ocv <= ocv_lut.lut[ocv_lut.rows - 1][j - 1])
> + return ocv_lut.capacity_legend[ocv_lut.rows - 1];
> +
> + for (i = 0; i < ocv_lut.rows - 1; i++) {
> + if (pcj == 0 && between(ocv_lut.lut[i][j],
> + ocv_lut.lut[i+1][j], ocv))
> + pcj = interpolate(ocv_lut.capacity_legend[i],
> + ocv_lut.lut[i][j],
> + ocv_lut.capacity_legend[i + 1],
> + ocv_lut.lut[i+1][j],
> + ocv);
> +
> + if (pcj_minus_one == 0 && between(ocv_lut.lut[i][j-1],
> + ocv_lut.lut[i+1][j-1], ocv))
> + pcj_minus_one = interpolate(ocv_lut.capacity_legend[i],
> + ocv_lut.lut[i][j-1],
> + ocv_lut.capacity_legend[i + 1],
> + ocv_lut.lut[i+1][j-1],
> + ocv);
> +
> + if (pcj && pcj_minus_one)
> + return interpolate(pcj_minus_one,
> + ocv_lut.temp_legend[j-1],
> + pcj,
> + ocv_lut.temp_legend[j],
> + temp);
> + }

How about finding the four indices first and then do the three
interpolation steps after that?

> +
> + if (pcj)
> + return pcj;
> +
> + if (pcj_minus_one)
> + return pcj_minus_one;

Do you need these special cases? Is it even possible that we get out
above loop without pcj and pcj_minus_one assigned?

> +
> + return 100;
> +}
> +
> +static unsigned long interpolate_fcc(int temp, struct bms_fcc_lut fcc_lut)

Pass fcc_lut by reference, as it's "large".

> +{
> + int i, fcc_mv;
> +
> + for (i = 0; i < TEMPERATURE_COLS; i++)
> + if (temp <= fcc_lut.temp_legend[i])
> + break;
> +
> + fcc_mv = interpolate(fcc_lut.lut[i - 1],
> + fcc_lut.temp_legend[i - 1],
> + fcc_lut.lut[i],
> + fcc_lut.temp_legend[i],
> + temp);
> +
> + return fcc_mv * 10000;

What does this function return? Why do you multiply with 10k here? A
comment would be nice.

> +}
> +
> +static int bms_lock_output_data(struct bms_device_info *di)
> +{
> + int ret;
> +
> + ret = regmap_update_bits(di->regmap, di->base_addr +
> + REG_BMS_CC_DATA_CTL,
> + BMS_HOLD_OREG_DATA, BMS_HOLD_OREG_DATA);
> + if (ret < 0) {
> + dev_err(di->dev, "failed to lock bms output: %d", ret);
> + return ret;
> + }
> +
> + /*
> + * Sleep for 100 microseconds here to make sure there has
> + * been at least three cycles of the sleep clock so that
> + * the registers are correctly locked.
> + */
> + udelay(100);

usleep_range(100, 1000); as this is longer than 10us

See Documentation/timers/timers-howto.txt

> +
> + return 0;
> +}
> +
> +static int bms_unlock_output_data(struct bms_device_info *di)
> +{
> + int ret;
> +
> + ret = regmap_update_bits(di->regmap, di->base_addr +
> + REG_BMS_CC_DATA_CTL,
> + BMS_HOLD_OREG_DATA, 0);
> + if (ret < 0) {
> + dev_err(di->dev, "failed to unlock bms output: %d", ret);
> + return ret;
> + }

regmap_update_bits() returns 0 on success, so you can:

ret = regmap_update_bits();
if (ret)
dev_err();

return ret;

> +
> + return 0;
> +}
> +
> +static int bms_read_ocv(struct bms_device_info *di, int *ocv)
> +{
> + unsigned long flags;
> + int ret;
> + u16 read_ocv;
> +
> + spin_lock_irqsave(&di->bms_output_lock, flags);
> +
> + ret = bms_lock_output_data(di);
> + if (ret < 0)
> + goto err_lock;
> +
> + ret = regmap_bulk_read(di->regmap, di->base_addr +
> + REG_BMS_OCV_FOR_SOC_DATA0, &read_ocv, 2);
> + if (ret < 0) {
> + dev_err(di->dev, "OCV read failed: %d", ret);

Returning with spinlock and output lock held.

> + return ret;
> + }
> +
> + dev_dbg(di->dev, "read OCV value of: %d", read_ocv);
> + *ocv = read_ocv;
> +
> + ret = bms_unlock_output_data(di);
> +
> +err_lock:
> + spin_unlock_irqrestore(&di->bms_output_lock, flags);
> +
> + return ret;
> +}
> +
> +static int bms_read_cc(struct bms_device_info *di, s64 *cc_uah)
> +{
> + unsigned long flags;
> + int ret;
> + s64 cc_raw_s36, cc_raw, cc_uv, cc_pvh;
> +
> + spin_lock_irqsave(&di->bms_output_lock, flags);
> +
> + ret = bms_lock_output_data(di);
> + if (ret < 0)
> + return ret;
> +
> + ret = regmap_bulk_read(di->regmap, di->base_addr +
> + REG_BMS_SHDW_CC_DATA0,
> + &cc_raw_s36, 5);
> + if (ret < 0) {
> + dev_err(di->dev, "coulomb counter read failed: %d", ret);

Returning with spinlock and output locked.

> + return ret;
> + }
> +
> + ret = bms_unlock_output_data(di);
> + if (ret < 0)

Returning with spinlock held.

> + return ret;
> +
> + spin_unlock_irqrestore(&di->bms_output_lock, flags);
> +
> + cc_raw = sign_extend_s36(cc_raw_s36);
> +
> + /* convert raw to uv */
> + cc_uv = div_s64(cc_raw * BMS_CC_READING_RESOLUTION_N,
> + BMS_CC_READING_RESOLUTION_D);
> +
> + /* convert uv to pvh */
> + cc_pvh = div_s64(cc_uv * BMS_CC_READING_TICKS * 100000,
> + BMS_SLEEP_CLK_HZ * SECONDS_PER_HOUR) * 10;
> +
> + /* divide by impedance */
> + *cc_uah = div_s64(cc_pvh, 10000);
> +
> + dev_dbg(di->dev, "read coulomb counter value of: %lld", *cc_uah);
> +
> + return 0;
> +}
> +
> +static void bms_reset_cc(struct bms_device_info *di)
> +{
> + int ret;
> + unsigned long flags;
> +
> + spin_lock_irqsave(&di->bms_output_lock, flags);
> +
> + ret = regmap_update_bits(di->regmap, di->base_addr +
> + REG_BMS_CC_CLEAR_CTL,
> + BMS_CLEAR_SHDW_CC,
> + BMS_CLEAR_SHDW_CC);
> + if (ret < 0) {
> + dev_err(di->dev, "coulomb counter reset failed: %d", ret);
> + goto err_lock;
> + }
> +
> + /* wait two sleep cycles for cc to reset */
> + udelay(100);

usleep_range(100, 1000);


Perhaps you can make the irq handler threaded, so that you don't have to
spend so long time with irqs disabled.

> +
> + ret = regmap_update_bits(di->regmap, di->base_addr +
> + REG_BMS_CC_CLEAR_CTL,
> + BMS_CLEAR_SHDW_CC, 0);
> + if (ret < 0)
> + dev_err(di->dev, "coulomb counter re-enable failed: %d", ret);
> +
> +err_lock:
> + spin_unlock_irqrestore(&di->bms_output_lock, flags);
> +}
> +
> +static int bms_calculate_capacity(struct bms_device_info *di, int *capacity)
> +{
> + unsigned long ocv_capacity, fcc;
> + int ret, temp, temp_degc;
> + s64 cc, capacity_nodiv;
> +
> + ret = iio_read_channel_raw(di->adc, &temp);
> + if (ret < 0) {
> + dev_err(di->dev, "failed to read temperature: %d", ret);
> + return ret;
> + }
> +
> + temp_degc = (temp + 500) / 1000;
> +
> + ret = bms_read_cc(di, &cc);
> + if (ret < 0) {
> + dev_err(di->dev, "failed to read coulomb counter: %d", ret);
> + return ret;
> + }
> +
> + ocv_capacity = interpolate_capacity(temp_degc, (di->ocv + 5) / 10,
> + di->ocv_lut);

interpolate_capacity returns unsigned int, but ocv_capacity is unsigned
long. Please pick one.

> + fcc = interpolate_fcc(temp_degc, di->fcc_lut);
> +
> + capacity_nodiv = ((fcc * ocv_capacity) / 100 - cc) * 100;
> + *capacity = div64_ul(capacity_nodiv, fcc);
> +
> + return 0;
> +}
> +
> +
> +

Remove a few empty lines.

> +/*
> + * Return power_supply property
> + */
> +static int bms_get_property(struct power_supply *psy,
> + enum power_supply_property psp,
> + union power_supply_propval *val)
> +{
> + struct bms_device_info *di = power_supply_get_drvdata(psy);
> + int ret;
> +
> + switch (psp) {
> + case POWER_SUPPLY_PROP_CAPACITY:
> + ret = bms_calculate_capacity(di, &val->intval);
> + break;
> + default:
> + ret = -EINVAL;
> + break;
> + }
> +
> + if (val->intval == INT_MAX || val->intval == INT_MIN)
> + ret = -EINVAL;

Can this happen?

> +
> + return ret;
> +}
[..]
> +static int bms_probe(struct platform_device *pdev)
> +{
> + struct power_supply_config psy_cfg = {};
> + struct bms_device_info *di;
> + int ret;
> +
> + di = devm_kzalloc(&pdev->dev, sizeof(*di), GFP_KERNEL);
> + if (!di)
> + return -ENOMEM;
> +
> + platform_set_drvdata(pdev, di);

You don't use this, so no need to set it.

> +
[..]
> +
> + spin_lock_init(&di->bms_output_lock);

Initialize the spinlock before registering the irq handler, just in case
it would fire immediately.

> +
> + di->bat_desc.name = "bms";
> + di->bat_desc.type = POWER_SUPPLY_TYPE_BATTERY;
> + di->bat_desc.properties = bms_props;
> + di->bat_desc.num_properties = ARRAY_SIZE(bms_props);
> + di->bat_desc.get_property = bms_get_property;
> +
> + psy_cfg.drv_data = di;
> + di->bat = devm_power_supply_register(di->dev, &di->bat_desc, &psy_cfg);

Replace:

> + if (IS_ERR(di->bat))
> + return PTR_ERR(di->bat);
> +
> + return 0;

with:

return PTR_ERR_OR_ZERO(di->bat);

> +}
> +
> +static const struct of_device_id bms_of_match[] = {
> + {.compatible = "qcom,pm8941-bms", },
> + { },
> +};
> +MODULE_DEVICE_TABLE(of, bms_of_match);
> +
> +static struct platform_driver bms_driver = {
> + .probe = bms_probe,
> + .driver = {
> + .name = "qcom-bms",
> + .of_match_table = of_match_ptr(bms_of_match),
> + },
> +};
> +module_platform_driver(bms_driver);
> +
> +MODULE_AUTHOR("Craig Tatlor <[email protected]>");
> +MODULE_DESCRIPTION("Qualcomm BMS driver");
> +MODULE_LICENSE("GPL");

Regards,
Bjorn