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.

Touch-to-wake, current limit and soft start tuning and PM660L are not
supported.

Assisted-by: LLM
Signed-off-by: Dmitry Baryshkov <[email protected]>
---
 drivers/regulator/Kconfig               |   8 +
 drivers/regulator/Makefile              |   1 +
 drivers/regulator/qcom-lcdb-regulator.c | 475 ++++++++++++++++++++++++++++++++
 3 files changed, 484 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..45b3b7118e31
--- /dev/null
+++ b/drivers/regulator/qcom-lcdb-regulator.c
@@ -0,0 +1,475 @@
+// 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;
+
+       /* 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)
+               return ret;
+
+       lcdb->off_time = ktime_get();
+
+       usleep_range(10000, 11000);
+
+       return regmap_write(lcdb->regmap, lcdb->base + LCDB_LDO_FORCE_PD_CTL, 
0);
+}
+
+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);
+
+       guard(mutex)(&lcdb->lock);
+
+       lcdb->enabled_mask &= ~BIT(rdev_get_id(rdev));
+       if (lcdb->enabled_mask)
+               return 0;
+
+       return qcom_lcdb_module_disable(lcdb);
+}
+
+static int qcom_lcdb_is_enabled(struct regulator_dev *rdev)
+{
+       struct qcom_lcdb *lcdb = rdev_get_drvdata(rdev);
+
+       guard(mutex)(&lcdb->lock);
+
+       return !!(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);
+
+       guard(mutex)(&lcdb->lock);
+
+       lcdb->uV[rdev_get_id(rdev)] = qcom_lcdb_sel_to_uV(sel);
+
+       return qcom_lcdb_write_voltages(lcdb, lcdb->uV[LCDB_LDO],
+                                       lcdb->uV[LCDB_NCP]);
+}
+
+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 void qcom_lcdb_handle_sc(struct qcom_lcdb *lcdb)
+{
+       int i;
+
+       guard(mutex)(&lcdb->lock);
+
+       if (!lcdb->enabled_mask || lcdb->sc_disabled)
+               return;
+
+       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;
+
+               for (i = 0; i < LCDB_NUM_RAILS; i++)
+                       regulator_notifier_call_chain(lcdb->rdev[i],
+                                                     REGULATOR_EVENT_FAIL, 
NULL);
+               return;
+       }
+
+       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");
+}
+
+static irqreturn_t qcom_lcdb_sc_irq(int irq, void *data)
+{
+       struct qcom_lcdb *lcdb = data;
+       unsigned int val;
+       int ret;
+
+       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;
+
+       qcom_lcdb_handle_sc(lcdb);
+
+       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