LCD panels on boards with PMI632, PM6150L or PM8150L take their bias
rails from the LCDB block. Without a driver they only work as long as
the bootloader has enabled it and nothing turns it off.

Both rails sit behind a single module enable, so the module is kept on
while at least one of them is in use. The boost output is not exposed:
it follows the higher of the two rails plus the headroom. Enabling
starts at a lower voltage and ramps up in steps, and the LDO pull-down
is forced while disabling, both as done by the vendor driver.

On a short circuit the module is restarted, up to ten times within a
second. After that the outputs are kept off and the consumers are
notified.

Touch-to-wake and the current limit and soft start tuning are not
supported. Neither is PM660L, whose boost is programmed differently.

Assisted-by: LLM
Signed-off-by: Dmitry Baryshkov <[email protected]>
---
 drivers/regulator/Kconfig               |   8 +
 drivers/regulator/Makefile              |   1 +
 drivers/regulator/qcom-lcdb-regulator.c | 494 ++++++++++++++++++++++++++++++++
 3 files changed, 503 insertions(+)

diff --git a/drivers/regulator/Kconfig b/drivers/regulator/Kconfig
index be6b72daa76f..9fd3f2c7e365 100644
--- a/drivers/regulator/Kconfig
+++ b/drivers/regulator/Kconfig
@@ -1915,4 +1915,12 @@ config REGULATOR_QCOM_LABIBB
          boost regulator and IBB can be used as a negative boost regulator
          for LCD display panel.
 
+config REGULATOR_QCOM_LCDB
+       tristate "Qualcomm LCDB regulator support"
+       depends on SPMI || COMPILE_TEST
+       help
+         This driver supports the LCDB (LCD bias) block found on Qualcomm
+         PMICs such as PMI632, PM6150L and PM8150L. It provides the positive
+         (LDO) and negative (NCP) bias rails for LCD panels.
+
 endif
diff --git a/drivers/regulator/Makefile b/drivers/regulator/Makefile
index 6aabcc54382a..84164d2b6c45 100644
--- a/drivers/regulator/Makefile
+++ b/drivers/regulator/Makefile
@@ -123,6 +123,7 @@ obj-$(CONFIG_REGULATOR_MT6397)      += mt6397-regulator.o
 obj-$(CONFIG_REGULATOR_MTK_DVFSRC) += mtk-dvfsrc-regulator.o
 obj-$(CONFIG_REGULATOR_NEX10000UB) += nex10000ub-regulator.o
 obj-$(CONFIG_REGULATOR_QCOM_LABIBB) += qcom-labibb-regulator.o
+obj-$(CONFIG_REGULATOR_QCOM_LCDB) += qcom-lcdb-regulator.o
 obj-$(CONFIG_REGULATOR_QCOM_PM8008) += qcom-pm8008-regulator.o
 obj-$(CONFIG_REGULATOR_QCOM_REFGEN) += qcom-refgen-regulator.o
 obj-$(CONFIG_REGULATOR_QCOM_RPM) += qcom_rpm-regulator.o
diff --git a/drivers/regulator/qcom-lcdb-regulator.c 
b/drivers/regulator/qcom-lcdb-regulator.c
new file mode 100644
index 000000000000..bb2d4ee23ad5
--- /dev/null
+++ b/drivers/regulator/qcom-lcdb-regulator.c
@@ -0,0 +1,494 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#include <linux/bits.h>
+#include <linux/delay.h>
+#include <linux/interrupt.h>
+#include <linux/ktime.h>
+#include <linux/minmax.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/regulator/driver.h>
+#include <linux/regulator/of_regulator.h>
+
+#define LCDB_INT_RT_STS                        0x10
+#define  LCDB_VREG_OK_RT_STS           BIT(0)
+#define  LCDB_SC_ERROR_RT_STS          BIT(1)
+
+#define LCDB_BST_OUTPUT_VOLTAGE                0x41
+#define  LCDB_BST_OUTPUT_VOLTAGE_MASK  GENMASK(5, 0)
+
+#define LCDB_MODULE_RDY                        0x45
+#define  LCDB_MODULE_RDY_BIT           BIT(7)
+
+#define LCDB_ENABLE_CTL1               0x46
+#define  LCDB_MODULE_EN                        BIT(7)
+
+#define LCDB_LDO_OUTPUT_VOLTAGE                0x71
+#define LCDB_LDO_FORCE_PD_CTL          0x79
+#define  LCDB_LDO_FORCE_PD_EN          BIT(0)
+#define  LCDB_LDO_FORCE_PD_MODE                BIT(7)
+
+#define LCDB_NCP_OUTPUT_VOLTAGE                0x81
+
+#define LCDB_OUTPUT_VOLTAGE_MASK       GENMASK(4, 0)
+
+#define LCDB_MIN_UV                    4000000
+#define LCDB_STEP_100MV_UV             100000
+#define LCDB_50MV_MIN_UV               4950000
+#define LCDB_STEP_50MV_UV              50000
+#define LCDB_50MV_MIN_SEL              10
+#define LCDB_N_VOLTAGES                        32
+
+#define LCDB_BST_MIN_UV                        4700000
+#define LCDB_BST_MAX_UV                        6275000
+#define LCDB_BST_STEP_UV               25000
+#define LCDB_BST_HEADROOM_UV           150000
+
+#define LCDB_RAMP_START_UV             4500000
+#define LCDB_RAMP_STEP_UV              500000
+
+#define LCDB_OFF_ON_DELAY_US           20000
+#define LCDB_VREG_OK_TIMEOUT_US                50000
+
+#define LCDB_SC_RESET_CNT_DLY_US       1000000
+#define LCDB_SC_CNT_MAX                        10
+
+enum {
+       LCDB_LDO,
+       LCDB_NCP,
+       LCDB_NUM_RAILS,
+};
+
+struct qcom_lcdb {
+       struct device *dev;
+       struct regmap *regmap;
+       u32 base;
+
+       struct mutex lock;
+       struct regulator_dev *rdev[LCDB_NUM_RAILS];
+       int uV[LCDB_NUM_RAILS];
+       unsigned int enabled_mask;
+       ktime_t off_time;
+
+       bool sc_disabled;
+       int sc_count;
+       ktime_t sc_time;
+};
+
+static const u8 qcom_lcdb_vreg[LCDB_NUM_RAILS] = {
+       [LCDB_LDO] = LCDB_LDO_OUTPUT_VOLTAGE,
+       [LCDB_NCP] = LCDB_NCP_OUTPUT_VOLTAGE,
+};
+
+static unsigned int qcom_lcdb_uV_to_sel(int uV)
+{
+       if (uV < LCDB_50MV_MIN_UV)
+               return (uV - LCDB_MIN_UV) / LCDB_STEP_100MV_UV;
+
+       return LCDB_50MV_MIN_SEL + (uV - LCDB_50MV_MIN_UV) / LCDB_STEP_50MV_UV;
+}
+
+static int qcom_lcdb_sel_to_uV(unsigned int sel)
+{
+       if (sel < LCDB_50MV_MIN_SEL)
+               return LCDB_MIN_UV + sel * LCDB_STEP_100MV_UV;
+
+       return LCDB_50MV_MIN_UV + (sel - LCDB_50MV_MIN_SEL) * LCDB_STEP_50MV_UV;
+}
+
+static int qcom_lcdb_write_voltages(struct qcom_lcdb *lcdb, int ldo_uV, int 
ncp_uV)
+{
+       const int uV[LCDB_NUM_RAILS] = { ldo_uV, ncp_uV };
+       unsigned int bst_sel, cur_sel;
+       int bst_uV, ret, i;
+
+       bst_uV = clamp(max(ldo_uV, ncp_uV) + LCDB_BST_HEADROOM_UV,
+                      LCDB_BST_MIN_UV, LCDB_BST_MAX_UV);
+       bst_sel = DIV_ROUND_UP(bst_uV - LCDB_BST_MIN_UV, LCDB_BST_STEP_UV);
+
+       ret = regmap_read(lcdb->regmap, lcdb->base + LCDB_BST_OUTPUT_VOLTAGE, 
&cur_sel);
+       if (ret)
+               return ret;
+       cur_sel &= LCDB_BST_OUTPUT_VOLTAGE_MASK;
+
+       /* The boost has to stay above both outputs while they are changing */
+       if (bst_sel > cur_sel) {
+               ret = regmap_update_bits(lcdb->regmap,
+                                        lcdb->base + LCDB_BST_OUTPUT_VOLTAGE,
+                                        LCDB_BST_OUTPUT_VOLTAGE_MASK, bst_sel);
+               if (ret)
+                       return ret;
+       }
+
+       for (i = 0; i < LCDB_NUM_RAILS; i++) {
+               ret = regmap_update_bits(lcdb->regmap,
+                                        lcdb->base + qcom_lcdb_vreg[i],
+                                        LCDB_OUTPUT_VOLTAGE_MASK,
+                                        qcom_lcdb_uV_to_sel(uV[i]));
+               if (ret)
+                       return ret;
+       }
+
+       if (bst_sel < cur_sel)
+               ret = regmap_update_bits(lcdb->regmap,
+                                        lcdb->base + LCDB_BST_OUTPUT_VOLTAGE,
+                                        LCDB_BST_OUTPUT_VOLTAGE_MASK, bst_sel);
+
+       return ret;
+}
+
+static int qcom_lcdb_write_limited(struct qcom_lcdb *lcdb, int limit_uV)
+{
+       return qcom_lcdb_write_voltages(lcdb,
+                                       min(lcdb->uV[LCDB_LDO], limit_uV),
+                                       min(lcdb->uV[LCDB_NCP], limit_uV));
+}
+
+static int qcom_lcdb_module_disable(struct qcom_lcdb *lcdb)
+{
+       int ret, pd_ret;
+
+       /* Force the LDO pull-down to discharge the output quickly */
+       ret = regmap_write(lcdb->regmap, lcdb->base + LCDB_LDO_FORCE_PD_CTL,
+                          LCDB_LDO_FORCE_PD_EN | LCDB_LDO_FORCE_PD_MODE);
+       if (ret)
+               return ret;
+
+       ret = regmap_write(lcdb->regmap, lcdb->base + LCDB_ENABLE_CTL1, 0);
+       if (!ret) {
+               lcdb->off_time = ktime_get();
+               usleep_range(10000, 11000);
+       }
+
+       pd_ret = regmap_write(lcdb->regmap, lcdb->base + LCDB_LDO_FORCE_PD_CTL, 
0);
+
+       return ret ?: pd_ret;
+}
+
+static int qcom_lcdb_module_enable(struct qcom_lcdb *lcdb)
+{
+       int target_uV = max(lcdb->uV[LCDB_LDO], lcdb->uV[LCDB_NCP]);
+       int uV = LCDB_RAMP_START_UV;
+       unsigned int val;
+       s64 off_us;
+       int ret;
+
+       off_us = ktime_us_delta(ktime_get(), lcdb->off_time);
+       if (off_us < LCDB_OFF_ON_DELAY_US)
+               usleep_range(LCDB_OFF_ON_DELAY_US - off_us,
+                            LCDB_OFF_ON_DELAY_US - off_us + 1000);
+
+       /* Start low and ramp the outputs up in steps to limit the inrush 
current */
+       ret = qcom_lcdb_write_limited(lcdb, uV);
+       if (ret)
+               return ret;
+
+       ret = regmap_write(lcdb->regmap, lcdb->base + LCDB_ENABLE_CTL1,
+                          LCDB_MODULE_EN);
+       if (ret)
+               return ret;
+
+       ret = regmap_read_poll_timeout(lcdb->regmap,
+                                      lcdb->base + LCDB_INT_RT_STS, val,
+                                      val & LCDB_VREG_OK_RT_STS,
+                                      1000, LCDB_VREG_OK_TIMEOUT_US);
+       if (ret) {
+               dev_err(lcdb->dev, "outputs failed to come up: %d\n", ret);
+               goto err_disable;
+       }
+
+       usleep_range(10000, 11000);
+
+       while (uV < target_uV) {
+               uV += LCDB_RAMP_STEP_UV;
+
+               ret = qcom_lcdb_write_limited(lcdb, uV);
+               if (ret)
+                       goto err_disable;
+
+               usleep_range(1000, 1100);
+       }
+
+       return 0;
+
+err_disable:
+       qcom_lcdb_module_disable(lcdb);
+
+       return ret;
+}
+
+static int qcom_lcdb_enable(struct regulator_dev *rdev)
+{
+       struct qcom_lcdb *lcdb = rdev_get_drvdata(rdev);
+       int ret = 0;
+
+       guard(mutex)(&lcdb->lock);
+
+       if (lcdb->sc_disabled)
+               return -EIO;
+
+       if (!lcdb->enabled_mask)
+               ret = qcom_lcdb_module_enable(lcdb);
+       if (ret)
+               return ret;
+
+       lcdb->enabled_mask |= BIT(rdev_get_id(rdev));
+
+       return 0;
+}
+
+static int qcom_lcdb_disable(struct regulator_dev *rdev)
+{
+       struct qcom_lcdb *lcdb = rdev_get_drvdata(rdev);
+       unsigned int mask;
+       int ret;
+
+       guard(mutex)(&lcdb->lock);
+
+       mask = lcdb->enabled_mask & ~BIT(rdev_get_id(rdev));
+       if (!mask && !lcdb->sc_disabled) {
+               ret = qcom_lcdb_module_disable(lcdb);
+               if (ret)
+                       return ret;
+       }
+
+       lcdb->enabled_mask = mask;
+
+       return 0;
+}
+
+static int qcom_lcdb_is_enabled(struct regulator_dev *rdev)
+{
+       struct qcom_lcdb *lcdb = rdev_get_drvdata(rdev);
+
+       guard(mutex)(&lcdb->lock);
+
+       return !lcdb->sc_disabled && (lcdb->enabled_mask & 
BIT(rdev_get_id(rdev)));
+}
+
+static int qcom_lcdb_set_voltage_sel(struct regulator_dev *rdev, unsigned int 
sel)
+{
+       struct qcom_lcdb *lcdb = rdev_get_drvdata(rdev);
+       int id = rdev_get_id(rdev);
+       int uV[LCDB_NUM_RAILS];
+       int ret;
+
+       guard(mutex)(&lcdb->lock);
+
+       memcpy(uV, lcdb->uV, sizeof(uV));
+       uV[id] = qcom_lcdb_sel_to_uV(sel);
+
+       ret = qcom_lcdb_write_voltages(lcdb, uV[LCDB_LDO], uV[LCDB_NCP]);
+       if (ret)
+               return ret;
+
+       lcdb->uV[id] = uV[id];
+
+       return 0;
+}
+
+static int qcom_lcdb_get_voltage_sel(struct regulator_dev *rdev)
+{
+       struct qcom_lcdb *lcdb = rdev_get_drvdata(rdev);
+
+       guard(mutex)(&lcdb->lock);
+
+       return qcom_lcdb_uV_to_sel(lcdb->uV[rdev_get_id(rdev)]);
+}
+
+static const struct regulator_ops qcom_lcdb_ops = {
+       .enable                 = qcom_lcdb_enable,
+       .disable                = qcom_lcdb_disable,
+       .is_enabled             = qcom_lcdb_is_enabled,
+       .set_voltage_sel        = qcom_lcdb_set_voltage_sel,
+       .get_voltage_sel        = qcom_lcdb_get_voltage_sel,
+       .list_voltage           = regulator_list_voltage_linear_range,
+       .map_voltage            = regulator_map_voltage_linear_range,
+};
+
+static const struct linear_range qcom_lcdb_ranges[] = {
+       REGULATOR_LINEAR_RANGE(LCDB_MIN_UV, 0, LCDB_50MV_MIN_SEL - 1,
+                              LCDB_STEP_100MV_UV),
+       REGULATOR_LINEAR_RANGE(LCDB_50MV_MIN_UV, LCDB_50MV_MIN_SEL,
+                              LCDB_N_VOLTAGES - 1, LCDB_STEP_50MV_UV),
+};
+
+static const struct regulator_desc qcom_lcdb_descs[LCDB_NUM_RAILS] = {
+       [LCDB_LDO] = {
+               .name                   = "lcdb_ldo",
+               .of_match               = "ldo",
+               .id                     = LCDB_LDO,
+               .ops                    = &qcom_lcdb_ops,
+               .type                   = REGULATOR_VOLTAGE,
+               .linear_ranges          = qcom_lcdb_ranges,
+               .n_linear_ranges        = ARRAY_SIZE(qcom_lcdb_ranges),
+               .n_voltages             = LCDB_N_VOLTAGES,
+               .owner                  = THIS_MODULE,
+       },
+       [LCDB_NCP] = {
+               .name                   = "lcdb_ncp",
+               .of_match               = "ncp",
+               .id                     = LCDB_NCP,
+               .ops                    = &qcom_lcdb_ops,
+               .type                   = REGULATOR_VOLTAGE,
+               .linear_ranges          = qcom_lcdb_ranges,
+               .n_linear_ranges        = ARRAY_SIZE(qcom_lcdb_ranges),
+               .n_voltages             = LCDB_N_VOLTAGES,
+               .owner                  = THIS_MODULE,
+       },
+};
+
+static bool qcom_lcdb_handle_sc(struct qcom_lcdb *lcdb)
+{
+       guard(mutex)(&lcdb->lock);
+
+       if (!lcdb->enabled_mask || lcdb->sc_disabled)
+               return false;
+
+       qcom_lcdb_module_disable(lcdb);
+
+       if (ktime_us_delta(ktime_get(), lcdb->sc_time) > 
LCDB_SC_RESET_CNT_DLY_US) {
+               lcdb->sc_count = 0;
+       } else if (lcdb->sc_count > LCDB_SC_CNT_MAX) {
+               dev_err(lcdb->dev, "persistent short circuit, keeping the 
outputs off\n");
+               lcdb->sc_disabled = true;
+               return true;
+       }
+
+       lcdb->sc_count++;
+       lcdb->sc_time = ktime_get();
+
+       if (qcom_lcdb_module_enable(lcdb)) {
+               dev_err(lcdb->dev, "failed to recover from a short circuit\n");
+               lcdb->sc_disabled = true;
+               return true;
+       }
+
+       return false;
+}
+
+static irqreturn_t qcom_lcdb_sc_irq(int irq, void *data)
+{
+       struct qcom_lcdb *lcdb = data;
+       unsigned int val;
+       int ret, i;
+
+       ret = regmap_read(lcdb->regmap, lcdb->base + LCDB_INT_RT_STS, &val);
+       if (ret || !(val & LCDB_SC_ERROR_RT_STS))
+               return IRQ_HANDLED;
+
+       /* Blanking time, a real short circuit is still reported afterwards */
+       usleep_range(2000, 2100);
+
+       ret = regmap_read(lcdb->regmap, lcdb->base + LCDB_INT_RT_STS, &val);
+       if (ret || !(val & LCDB_SC_ERROR_RT_STS))
+               return IRQ_HANDLED;
+
+       if (qcom_lcdb_handle_sc(lcdb)) {
+               for (i = 0; i < LCDB_NUM_RAILS; i++)
+                       regulator_notifier_call_chain(lcdb->rdev[i],
+                                                     REGULATOR_EVENT_FAIL, 
NULL);
+       }
+
+       return IRQ_HANDLED;
+}
+
+static int qcom_lcdb_hw_init(struct qcom_lcdb *lcdb)
+{
+       unsigned int val;
+       int ret, i;
+
+       for (i = 0; i < LCDB_NUM_RAILS; i++) {
+               ret = regmap_read(lcdb->regmap, lcdb->base + qcom_lcdb_vreg[i], 
&val);
+               if (ret)
+                       return ret;
+
+               lcdb->uV[i] = qcom_lcdb_sel_to_uV(val & 
LCDB_OUTPUT_VOLTAGE_MASK);
+       }
+
+       ret = regmap_read(lcdb->regmap, lcdb->base + LCDB_ENABLE_CTL1, &val);
+       if (ret)
+               return ret;
+
+       if (val & LCDB_MODULE_EN) {
+               lcdb->enabled_mask = GENMASK(LCDB_NUM_RAILS - 1, 0);
+               return 0;
+       }
+
+       return regmap_set_bits(lcdb->regmap, lcdb->base + LCDB_MODULE_RDY,
+                              LCDB_MODULE_RDY_BIT);
+}
+
+static int qcom_lcdb_probe(struct platform_device *pdev)
+{
+       struct device *dev = &pdev->dev;
+       struct regulator_config cfg = {};
+       struct qcom_lcdb *lcdb;
+       int irq, ret, i;
+
+       lcdb = devm_kzalloc(dev, sizeof(*lcdb), GFP_KERNEL);
+       if (!lcdb)
+               return -ENOMEM;
+
+       lcdb->dev = dev;
+
+       lcdb->regmap = dev_get_regmap(dev->parent, NULL);
+       if (!lcdb->regmap)
+               return dev_err_probe(dev, -ENODEV, "failed to get the parent 
regmap\n");
+
+       ret = device_property_read_u32(dev, "reg", &lcdb->base);
+       if (ret)
+               return dev_err_probe(dev, ret, "failed to get the base 
address\n");
+
+       ret = devm_mutex_init(dev, &lcdb->lock);
+       if (ret)
+               return ret;
+
+       ret = qcom_lcdb_hw_init(lcdb);
+       if (ret)
+               return dev_err_probe(dev, ret, "failed to read the initial 
state\n");
+
+       cfg.dev = dev;
+       cfg.regmap = lcdb->regmap;
+       cfg.driver_data = lcdb;
+
+       for (i = 0; i < LCDB_NUM_RAILS; i++) {
+               lcdb->rdev[i] = devm_regulator_register(dev, 
&qcom_lcdb_descs[i], &cfg);
+               if (IS_ERR(lcdb->rdev[i]))
+                       return dev_err_probe(dev, PTR_ERR(lcdb->rdev[i]),
+                                            "failed to register %s\n",
+                                            qcom_lcdb_descs[i].name);
+       }
+
+       irq = platform_get_irq(pdev, 0);
+       if (irq < 0)
+               return irq;
+
+       return devm_request_threaded_irq(dev, irq, NULL, qcom_lcdb_sc_irq,
+                                        IRQF_ONESHOT, "lcdb-sc", lcdb);
+}
+
+static const struct of_device_id qcom_lcdb_match[] = {
+       { .compatible = "qcom,pm8150l-lcdb" },
+       { }
+};
+MODULE_DEVICE_TABLE(of, qcom_lcdb_match);
+
+static struct platform_driver qcom_lcdb_driver = {
+       .driver = {
+               .name = "qcom-lcdb-regulator",
+               .of_match_table = qcom_lcdb_match,
+       },
+       .probe = qcom_lcdb_probe,
+};
+module_platform_driver(qcom_lcdb_driver);
+
+MODULE_DESCRIPTION("Qualcomm LCDB (LCD bias) regulator driver");
+MODULE_LICENSE("GPL");

-- 
2.47.3

Reply via email to