WLED driver provides the interface to the display driver to adjust the brightness of the display backlight. Signed-off-by: Kiran Gunda <kgunda@xxxxxxxxxxxxxx> --- .../bindings/leds/backlight/qcom-spmi-wled.txt | 90 ++++ drivers/video/backlight/Kconfig | 9 + drivers/video/backlight/Makefile | 1 + drivers/video/backlight/qcom-spmi-wled.c | 504 +++++++++++++++++++++ 4 files changed, 604 insertions(+) create mode 100644 Documentation/devicetree/bindings/leds/backlight/qcom-spmi-wled.txt create mode 100644 drivers/video/backlight/qcom-spmi-wled.c diff --git a/Documentation/devicetree/bindings/leds/backlight/qcom-spmi-wled.txt b/Documentation/devicetree/bindings/leds/backlight/qcom-spmi-wled.txt new file mode 100644 index 0000000..f1ea25b --- /dev/null +++ b/Documentation/devicetree/bindings/leds/backlight/qcom-spmi-wled.txt @@ -0,0 +1,90 @@ +Binding for Qualcomm WLED driver + +WLED (White Light Emitting Diode) driver is used for controlling display +backlight that is part of PMIC on Qualcomm Technologies reference platforms. +The PMIC is connected to the host processor via SPMI bus. + +- compatible + Usage: required + Value type: <string> + Definition: should be "qcom,pm8998-spmi-wled". + +- reg + Usage: required + Value type: <prop-encoded-array> + Definition: Base address and size of the WLED modules. + +- reg-names + Usage: required + Value type: <string> + Definition: Names associated with base addresses. should be + "qcom-wled-ctrl-base", "qcom-wled-sink-base". + +- label + Usage: required + Value type: <string> + Definition: The name of the backlight device. + +- default-brightness + Usage: optional + Value type: <u32> + Definition: brightness value on boot, value from: 0-4095 + default: 2048 + +- qcom,fs-current-limit + Usage: optional + Value type: <u32> + Definition: per-string full scale current limit in uA. value from + 0 to 30000 with 5000 uA resolution. default: 25000 uA + +- qcom,current-boost-limit + Usage: optional + Value type: <u32> + Definition: ILIM threshold in mA. values are 105, 280, 450, 620, 970, + 1150, 1300, 1500. default: 970 mA + +- qcom,switching-freq + Usage: optional + Value type: <u32> + Definition: Switching frequency in KHz. values are + 600, 640, 685, 738, 800, 872, 960, 1066, 1200, 1371, + 1600, 1920, 2400, 3200, 4800, 9600. + default: 800 KHz + +- qcom,ovp + Usage: optional + Value type: <u32> + Definition: Over-voltage protection limit in mV. values are 31100, + 29600, 19600, 18100. + default: 29600 mV + +- qcom,string-cfg + Usage: optional + Value type: <u32> + Definition: Bit mask of the wled strings. Bit 0 to 3 indicates strings + 0 to 3 respectively. Wled module has four strings of leds + numbered from 0 to 3. Each string of leds are operated + individually. Specify the strings using the bit mask. Any + combination of led strings can be used. + default value is 15 (b1111). + +- qcom,en-cabc + Usage: optional + Value type: <bool> + Definition: Specify if cabc (content adaptive backlight control) is + needed. + +Example: + +qcom-wled@d800 { + compatible = "qcom,pm8998-spmi-wled"; + reg = <0xd800 0xd900>; + reg-names = "qcom-wled-ctrl-base", "qcom-wled-sink-base"; + label = "backlight"; + + qcom,fs-current-limit = <25000>; + qcom,current-boost-limit = <970>; + qcom,switching-freq = <800>; + qcom,ovp = <29600>; + qcom,string-cfg = <15>; +}; diff --git a/drivers/video/backlight/Kconfig b/drivers/video/backlight/Kconfig index 4e1d2ad..19ea799 100644 --- a/drivers/video/backlight/Kconfig +++ b/drivers/video/backlight/Kconfig @@ -306,6 +306,15 @@ config BACKLIGHT_PM8941_WLED If you have the Qualcomm PM8941, say Y to enable a driver for the WLED block. +config BACKLIGHT_QCOM_SPMI_WLED + tristate "Qualcomm WLED Driver" + select REGMAP + help + If you have the Qualcomm WLED used for backlight control, say Y to + enable a driver for the WLED block. This driver provides the + interface to the display driver to adjust the brightness of the + display backlight. This supports PMI8998 currently. + config BACKLIGHT_SAHARA tristate "Tabletkiosk Sahara Touch-iT Backlight Driver" depends on X86 diff --git a/drivers/video/backlight/Makefile b/drivers/video/backlight/Makefile index 5e28f01..f6627e5 100644 --- a/drivers/video/backlight/Makefile +++ b/drivers/video/backlight/Makefile @@ -51,6 +51,7 @@ obj-$(CONFIG_BACKLIGHT_PANDORA) += pandora_bl.o obj-$(CONFIG_BACKLIGHT_PCF50633) += pcf50633-backlight.o obj-$(CONFIG_BACKLIGHT_PM8941_WLED) += pm8941-wled.o obj-$(CONFIG_BACKLIGHT_PWM) += pwm_bl.o +obj-$(CONFIG_BACKLIGHT_QCOM_SPMI_WLED) += qcom-spmi-wled.o obj-$(CONFIG_BACKLIGHT_SAHARA) += kb3886_bl.o obj-$(CONFIG_BACKLIGHT_SKY81452) += sky81452-backlight.o obj-$(CONFIG_BACKLIGHT_TOSA) += tosa_bl.o diff --git a/drivers/video/backlight/qcom-spmi-wled.c b/drivers/video/backlight/qcom-spmi-wled.c new file mode 100644 index 0000000..14c3adc --- /dev/null +++ b/drivers/video/backlight/qcom-spmi-wled.c @@ -0,0 +1,504 @@ +/* + * Copyright (c) 2017, 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. + */ + +#include <linux/kernel.h> +#include <linux/backlight.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/of_address.h> +#include <linux/regmap.h> + +/* General definitions */ +#define QCOM_WLED_DEFAULT_BRIGHTNESS 2048 +#define QCOM_WLED_MAX_BRIGHTNESS 4095 + +/* WLED control registers */ +#define QCOM_WLED_CTRL_MOD_ENABLE 0x46 +#define QCOM_WLED_CTRL_MOD_EN_MASK BIT(7) +#define QCOM_WLED_CTRL_MODULE_EN_SHIFT 7 + +#define QCOM_WLED_CTRL_SWITCH_FREQ 0x4c +#define QCOM_WLED_CTRL_SWITCH_FREQ_MASK GENMASK(3, 0) + +#define QCOM_WLED_CTRL_OVP 0x4d +#define QCOM_WLED_CTRL_OVP_MASK GENMASK(1, 0) + +#define QCOM_WLED_CTRL_ILIM 0x4e +#define QCOM_WLED_CTRL_ILIM_MASK GENMASK(2, 0) + +/* WLED sink registers */ +#define QCOM_WLED_SINK_CURR_SINK_EN 0x46 +#define QCOM_WLED_SINK_CURR_SINK_MASK GENMASK(7, 4) +#define QCOM_WLED_SINK_CURR_SINK_SHFT 0x04 + +#define QCOM_WLED_SINK_SYNC 0x47 +#define QCOM_WLED_SINK_SYNC_MASK GENMASK(3, 0) +#define QCOM_WLED_SINK_SYNC_LED1 BIT(0) +#define QCOM_WLED_SINK_SYNC_LED2 BIT(1) +#define QCOM_WLED_SINK_SYNC_LED3 BIT(2) +#define QCOM_WLED_SINK_SYNC_LED4 BIT(3) +#define QCOM_WLED_SINK_SYNC_CLEAR 0x00 + +#define QCOM_WLED_SINK_MOD_EN_REG(n) (0x50 + (n * 0x10)) +#define QCOM_WLED_SINK_REG_STR_MOD_MASK BIT(7) +#define QCOM_WLED_SINK_REG_STR_MOD_EN BIT(7) + +#define QCOM_WLED_SINK_SYNC_DLY_REG(n) (0x51 + (n * 0x10)) +#define QCOM_WLED_SINK_FS_CURR_REG(n) (0x52 + (n * 0x10)) +#define QCOM_WLED_SINK_FS_MASK GENMASK(3, 0) + +#define QCOM_WLED_SINK_CABC_REG(n) (0x56 + (n * 0x10)) +#define QCOM_WLED_SINK_CABC_MASK BIT(7) +#define QCOM_WLED_SINK_CABC_EN BIT(7) + +#define QCOM_WLED_SINK_BRIGHT_LSB_REG(n) (0x57 + (n * 0x10)) +#define QCOM_WLED_SINK_BRIGHT_MSB_REG(n) (0x58 + (n * 0x10)) + +struct qcom_wled_config { + u32 i_boost_limit; + u32 ovp; + u32 switch_freq; + u32 fs_current; + u32 string_cfg; + bool en_cabc; +}; + +struct qcom_wled { + const char *name; + struct platform_device *pdev; + struct regmap *regmap; + u16 sink_addr; + u16 ctrl_addr; + u32 brightness; + bool prev_state; + + struct qcom_wled_config cfg; +}; + +static int qcom_wled_module_enable(struct qcom_wled *wled, int val) +{ + int rc; + + rc = regmap_update_bits(wled->regmap, wled->ctrl_addr + + QCOM_WLED_CTRL_MOD_ENABLE, QCOM_WLED_CTRL_MOD_EN_MASK, + val << QCOM_WLED_CTRL_MODULE_EN_SHIFT); + return rc; +} + +static int qcom_wled_get_brightness(struct backlight_device *bl) +{ + struct qcom_wled *wled = bl_get_data(bl); + + return wled->brightness; +} + +static int qcom_wled_sync_toggle(struct qcom_wled *wled) +{ + int rc; + + rc = regmap_update_bits(wled->regmap, + wled->sink_addr + QCOM_WLED_SINK_SYNC, + QCOM_WLED_SINK_SYNC_MASK, QCOM_WLED_SINK_SYNC_MASK); + if (rc < 0) + return rc; + + rc = regmap_update_bits(wled->regmap, + wled->sink_addr + QCOM_WLED_SINK_SYNC, + QCOM_WLED_SINK_SYNC_MASK, QCOM_WLED_SINK_SYNC_CLEAR); + + return rc; +} + +static int qcom_wled_set_brightness(struct qcom_wled *wled, u16 brightness) +{ + int rc, i; + u16 low_limit = QCOM_WLED_MAX_BRIGHTNESS * 4 / 1000; + u8 string_cfg = wled->cfg.string_cfg; + u8 v[2]; + + /* WLED's lower limit of operation is 0.4% */ + if (brightness > 0 && brightness < low_limit) + brightness = low_limit; + + v[0] = brightness & 0xff; + v[1] = (brightness >> 8) & 0xf; + + for (i = 0; (string_cfg >> i) != 0; i++) { + if (string_cfg & BIT(i)) { + rc = regmap_bulk_write(wled->regmap, wled->sink_addr + + QCOM_WLED_SINK_BRIGHT_LSB_REG(i), v, 2); + if (rc < 0) + return rc; + } + } + + return 0; +} + +static int qcom_wled_update_status(struct backlight_device *bl) +{ + struct qcom_wled *wled = bl_get_data(bl); + u16 brightness = bl->props.brightness; + int rc; + + if (bl->props.power != FB_BLANK_UNBLANK || + bl->props.fb_blank != FB_BLANK_UNBLANK || + bl->props.state & BL_CORE_FBBLANK) + brightness = 0; + + if (brightness) { + rc = qcom_wled_set_brightness(wled, brightness); + if (rc < 0) { + pr_err("wled failed to set brightness rc:%d\n", rc); + return rc; + } + + if (!!brightness != wled->prev_state) { + rc = qcom_wled_module_enable(wled, !!brightness); + if (rc < 0) { + pr_err("wled enable failed rc:%d\n", rc); + return rc; + } + } + } else { + rc = qcom_wled_module_enable(wled, brightness); + if (rc < 0) { + pr_err("wled disable failed rc:%d\n", rc); + return rc; + } + } + + wled->prev_state = !!brightness; + + rc = qcom_wled_sync_toggle(wled); + if (rc < 0) { + pr_err("wled sync failed rc:%d\n", rc); + return rc; + } + + wled->brightness = brightness; + + return rc; +} + +static int qcom_wled_setup(struct qcom_wled *wled) +{ + int rc, temp, i; + u8 sink_en = 0; + u8 string_cfg = wled->cfg.string_cfg; + + rc = regmap_update_bits(wled->regmap, + wled->ctrl_addr + QCOM_WLED_CTRL_OVP, + QCOM_WLED_CTRL_OVP_MASK, wled->cfg.ovp); + if (rc < 0) + return rc; + + rc = regmap_update_bits(wled->regmap, + wled->ctrl_addr + QCOM_WLED_CTRL_ILIM, + QCOM_WLED_CTRL_ILIM_MASK, wled->cfg.i_boost_limit); + if (rc < 0) + return rc; + + rc = regmap_update_bits(wled->regmap, + wled->ctrl_addr + QCOM_WLED_CTRL_SWITCH_FREQ, + QCOM_WLED_CTRL_SWITCH_FREQ_MASK, wled->cfg.switch_freq); + if (rc < 0) + return rc; + + for (i = 0; (string_cfg >> i) != 0; i++) { + if (string_cfg & BIT(i)) { + u16 addr = wled->sink_addr + + QCOM_WLED_SINK_MOD_EN_REG(i); + + rc = regmap_update_bits(wled->regmap, addr, + QCOM_WLED_SINK_REG_STR_MOD_MASK, + QCOM_WLED_SINK_REG_STR_MOD_EN); + if (rc < 0) + return rc; + + addr = wled->sink_addr + + QCOM_WLED_SINK_FS_CURR_REG(i); + rc = regmap_update_bits(wled->regmap, addr, + QCOM_WLED_SINK_FS_MASK, + wled->cfg.fs_current); + if (rc < 0) + return rc; + + addr = wled->sink_addr + + QCOM_WLED_SINK_CABC_REG(i); + rc = regmap_update_bits(wled->regmap, addr, + QCOM_WLED_SINK_CABC_MASK, + wled->cfg.en_cabc ? + QCOM_WLED_SINK_CABC_EN : 0); + if (rc) + return rc; + + temp = i + QCOM_WLED_SINK_CURR_SINK_SHFT; + sink_en |= 1 << temp; + } + } + + rc = regmap_update_bits(wled->regmap, + wled->sink_addr + QCOM_WLED_SINK_CURR_SINK_EN, + QCOM_WLED_SINK_CURR_SINK_MASK, sink_en); + if (rc < 0) + return rc; + + rc = qcom_wled_sync_toggle(wled); + if (rc < 0) { + pr_err("Failed to toggle sync reg rc:%d\n", rc); + return rc; + } + + return 0; +} + +static const struct qcom_wled_config wled_config_defaults = { + .i_boost_limit = 4, + .fs_current = 10, + .ovp = 1, + .switch_freq = 11, + .string_cfg = 0xf, + .en_cabc = 0, +}; + +struct qcom_wled_var_cfg { + const u32 *values; + u32 (*fn)(u32); + int size; +}; + +static const u32 wled_i_boost_limit_values[] = { + 105, 280, 450, 620, 970, 1150, 1300, 1500, +}; + +static const struct qcom_wled_var_cfg wled_i_boost_limit_cfg = { + .values = wled_i_boost_limit_values, + .size = ARRAY_SIZE(wled_i_boost_limit_values), +}; + +static const u32 wled_fs_current_values[] = { + 0, 2500, 5000, 7500, 10000, 12500, 15000, 17500, 20000, + 22500, 25000, 27500, 30000, +}; + +static const struct qcom_wled_var_cfg wled_fs_current_cfg = { + .values = wled_fs_current_values, + .size = ARRAY_SIZE(wled_fs_current_values), +}; + +static const u32 wled_ovp_values[] = { + 31100, 29600, 19600, 18100, +}; + +static const struct qcom_wled_var_cfg wled_ovp_cfg = { + .values = wled_ovp_values, + .size = ARRAY_SIZE(wled_ovp_values), +}; + +static u32 qcom_wled_switch_freq_values_fn(u32 idx) +{ + return 19200 / (2 * (1 + idx)); +} + +static const struct qcom_wled_var_cfg wled_switch_freq_cfg = { + .fn = qcom_wled_switch_freq_values_fn, + .size = 16, +}; + +static const struct qcom_wled_var_cfg wled_string_cfg = { + .size = 16, +}; + +static u32 qcom_wled_values(const struct qcom_wled_var_cfg *cfg, u32 idx) +{ + if (idx >= cfg->size) + return UINT_MAX; + if (cfg->fn) + return cfg->fn(idx); + if (cfg->values) + return cfg->values[idx]; + return idx; +} + +static int qcom_wled_configure(struct qcom_wled *wled, struct device *dev) +{ + struct qcom_wled_config *cfg = &wled->cfg; + const __be32 *prop_addr; + u32 val, c; + int rc, i, j; + + const struct { + const char *name; + u32 *val_ptr; + const struct qcom_wled_var_cfg *cfg; + } u32_opts[] = { + { + "qcom,current-boost-limit", + &cfg->i_boost_limit, + .cfg = &wled_i_boost_limit_cfg, + }, + { + "qcom,fs-current-limit", + &cfg->fs_current, + .cfg = &wled_fs_current_cfg, + }, + { + "qcom,ovp", + &cfg->ovp, + .cfg = &wled_ovp_cfg, + }, + { + "qcom,switching-freq", + &cfg->switch_freq, + .cfg = &wled_switch_freq_cfg, + }, + { + "qcom,string-cfg", + &cfg->string_cfg, + .cfg = &wled_string_cfg, + }, + }; + + const struct { + const char *name; + bool *val_ptr; + } bool_opts[] = { + { "qcom,en-cabc", &cfg->en_cabc, }, + }; + + prop_addr = of_get_address(dev->of_node, 0, NULL, NULL); + if (!prop_addr) { + pr_err("invalid IO resources\n"); + return -EINVAL; + } + wled->ctrl_addr = be32_to_cpu(*prop_addr); + + prop_addr = of_get_address(dev->of_node, 1, NULL, NULL); + if (!prop_addr) { + pr_err("invalid IO resources\n"); + return -EINVAL; + } + wled->sink_addr = be32_to_cpu(*prop_addr); + rc = of_property_read_string(dev->of_node, "label", &wled->name); + if (rc < 0) + wled->name = dev->of_node->name; + + *cfg = wled_config_defaults; + for (i = 0; i < ARRAY_SIZE(u32_opts); ++i) { + rc = of_property_read_u32(dev->of_node, u32_opts[i].name, &val); + if (rc == -EINVAL) { + continue; + } else if (rc < 0) { + pr_err("error reading '%s'\n", u32_opts[i].name); + return rc; + } + + c = UINT_MAX; + for (j = 0; c != val; j++) { + c = qcom_wled_values(u32_opts[i].cfg, j); + if (c == UINT_MAX) { + pr_err("invalid value for '%s'\n", + u32_opts[i].name); + return -EINVAL; + } + + if (c == val) + break; + } + + pr_debug("'%s' = %u\n", u32_opts[i].name, c); + *u32_opts[i].val_ptr = j; + } + + for (i = 0; i < ARRAY_SIZE(bool_opts); ++i) { + if (of_property_read_bool(dev->of_node, bool_opts[i].name)) + *bool_opts[i].val_ptr = true; + } + + return 0; +} + +static const struct backlight_ops qcom_wled_ops = { + .update_status = qcom_wled_update_status, + .get_brightness = qcom_wled_get_brightness, +}; + +static int qcom_wled_probe(struct platform_device *pdev) +{ + struct backlight_properties props; + struct backlight_device *bl; + struct qcom_wled *wled; + struct regmap *regmap; + u32 val; + int rc; + + regmap = dev_get_regmap(pdev->dev.parent, NULL); + if (!regmap) { + pr_err("Unable to get regmap\n"); + return -EINVAL; + } + + wled = devm_kzalloc(&pdev->dev, sizeof(*wled), GFP_KERNEL); + if (!wled) + return -ENOMEM; + + wled->regmap = regmap; + wled->pdev = pdev; + + rc = qcom_wled_configure(wled, &pdev->dev); + if (rc < 0) { + pr_err("wled configure failed rc:%d\n", rc); + return rc; + } + + rc = qcom_wled_setup(wled); + if (rc < 0) { + pr_err("wled setup failed rc:%d\n", rc); + return rc; + } + + val = QCOM_WLED_DEFAULT_BRIGHTNESS; + of_property_read_u32(pdev->dev.of_node, "default-brightness", &val); + wled->brightness = val; + + platform_set_drvdata(pdev, wled); + + memset(&props, 0, sizeof(struct backlight_properties)); + props.type = BACKLIGHT_RAW; + props.brightness = val; + props.max_brightness = QCOM_WLED_MAX_BRIGHTNESS; + bl = devm_backlight_device_register(&pdev->dev, pdev->name, + &pdev->dev, wled, + &qcom_wled_ops, &props); + return PTR_ERR_OR_ZERO(bl); +} + +static const struct of_device_id qcom_wled_match_table[] = { + { .compatible = "qcom,pm8998-spmi-wled",}, + { }, +}; + +static struct platform_driver qcom_wled_driver = { + .probe = qcom_wled_probe, + .driver = { + .name = "qcom-spmi-wled", + .of_match_table = qcom_wled_match_table, + }, +}; + +module_platform_driver(qcom_wled_driver); + +MODULE_DESCRIPTION("Qualcomm SPMI PMIC WLED driver"); +MODULE_LICENSE("GPL v2"); -- The Qualcomm Innovation Center, Inc. is a member of the Code Aurora Forum, a Linux Foundation Collaborative Project -- To unsubscribe from this list: send the line "unsubscribe linux-arm-msm" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html