Re: [PATCH v2 1/5] leds: Add Intel Cherry Trail Whiskey Cove PMIC LED driver

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Thu, 20 Apr 2023, Hans de Goede wrote:

> From: Yauhen Kharuzhy <jekhor@xxxxxxxxx>
> 
> Add support for LEDs connected to the Intel Cherry Trail Whiskey Cove
> PMIC. Charger and general-purpose LEDs are supported. Hardware blinking
> is implemented, breathing is not.
> 
> This driver was tested with Lenovo Yoga Book notebook.
> 
> Changes by Hans de Goede (in response to review of v2):
> - Since the PMIC is connected to the battery any changes we make to
>   the LED settings are permanent, even surviving reboot / poweroff.
>   Save LED1 register settings on probe() and if auto-/hw-control was
>   enabled on probe() restore the settings on remove() and shutdown().
> - Delay switching LED1 to software control mode to first brightness write.
> - Use dynamically allocated drvdata instead of a global drvdata variable.
> - Ensure the LED is on when activating blinking.
> - Fix CHT_WC_LED_EFF_BREATHING val ((3 << 1) rather then BIT(3)).
> 
> Link: https://lore.kernel.org/r/20190212205901.13037-2-jekhor@xxxxxxxxx
> Signed-off-by: Yauhen Kharuzhy <jekhor@xxxxxxxxx>
> Co-developed-by: Hans de Goede <hdegoede@xxxxxxxxxx>
> Signed-off-by: Hans de Goede <hdegoede@xxxxxxxxxx>
> ---
> Changes in v2:
> - Update comment about YB1 kernel source usage for register info
> - Replace "cht-wc::" LED name prefix with "platform::"
> - Add leds-cht-wcove to list of drivers using "platform::charging" in
>   Documentation/leds/well-known-leds.txt
> - Bail from cht_wc_leds_brightness_set() on first error
> - Make default blink 1Hz, like sw-blink default blink
> ---
>  Documentation/leds/well-known-leds.txt |   2 +-
>  drivers/leds/Kconfig                   |  11 +
>  drivers/leds/Makefile                  |   1 +
>  drivers/leds/leds-cht-wcove.c          | 373 +++++++++++++++++++++++++
>  4 files changed, 386 insertions(+), 1 deletion(-)
>  create mode 100644 drivers/leds/leds-cht-wcove.c

Generally nice.  Couple of small nits.

> diff --git a/Documentation/leds/well-known-leds.txt b/Documentation/leds/well-known-leds.txt
> index 2160382c86be..7640debee6c0 100644
> --- a/Documentation/leds/well-known-leds.txt
> +++ b/Documentation/leds/well-known-leds.txt
> @@ -65,7 +65,7 @@ Phones usually have multi-color status LED.
>  
>  * Power management
>  
> -Good: "platform:*:charging" (allwinner sun50i)
> +Good: "platform:*:charging" (allwinner sun50i, leds-cht-wcove)
>  
>  * Screen
>  
> diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig
> index 9dbce09eabac..90835716f14a 100644
> --- a/drivers/leds/Kconfig
> +++ b/drivers/leds/Kconfig
> @@ -122,6 +122,17 @@ config LEDS_BCM6358
>  	  This option enables support for LEDs connected to the BCM6358
>  	  LED HW controller accessed via MMIO registers.
>  
> +config LEDS_CHT_WCOVE
> +	tristate "LED support for Intel Cherry Trail Whiskey Cove PMIC"
> +	depends on LEDS_CLASS
> +	depends on INTEL_SOC_PMIC_CHTWC
> +	help
> +	  This option enables support for charger and general purpose LEDs
> +	  connected to the Intel Cherrytrail Whiskey Cove PMIC.
> +
> +	  To compile this driver as a module, choose M here: the module
> +	  will be called leds-cht-wcove.
> +
>  config LEDS_CPCAP
>  	tristate "LED Support for Motorola CPCAP"
>  	depends on LEDS_CLASS
> diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile
> index d30395d11fd8..78b5b69f9c54 100644
> --- a/drivers/leds/Makefile
> +++ b/drivers/leds/Makefile
> @@ -19,6 +19,7 @@ obj-$(CONFIG_LEDS_BCM6328)		+= leds-bcm6328.o
>  obj-$(CONFIG_LEDS_BCM6358)		+= leds-bcm6358.o
>  obj-$(CONFIG_LEDS_BD2802)		+= leds-bd2802.o
>  obj-$(CONFIG_LEDS_BLINKM)		+= leds-blinkm.o
> +obj-$(CONFIG_LEDS_CHT_WCOVE)		+= leds-cht-wcove.o
>  obj-$(CONFIG_LEDS_CLEVO_MAIL)		+= leds-clevo-mail.o
>  obj-$(CONFIG_LEDS_COBALT_QUBE)		+= leds-cobalt-qube.o
>  obj-$(CONFIG_LEDS_COBALT_RAQ)		+= leds-cobalt-raq.o
> diff --git a/drivers/leds/leds-cht-wcove.c b/drivers/leds/leds-cht-wcove.c
> new file mode 100644
> index 000000000000..908965e25552
> --- /dev/null
> +++ b/drivers/leds/leds-cht-wcove.c
> @@ -0,0 +1,373 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Driver for LEDs connected to the Intel Cherry Trail Whiskey Cove PMIC
> + *
> + * Copyright 2019 Yauhen Kharuzhy <jekhor@xxxxxxxxx>
> + * Copyright 2023 Hans de Goede <hansg@xxxxxxxxxx>
> + *
> + * Register info comes from the Lenovo Yoga Book Android kernel sources:
> + * YB1_source_code/kernel/cht/drivers/misc/charger_gp_led.c.

How does one browse to this?

> + */
> +
> +#include <linux/kernel.h>
> +#include <linux/mfd/intel_soc_pmic.h>
> +#include <linux/leds.h>
> +#include <linux/module.h>
> +#include <linux/mod_devicetable.h>
> +#include <linux/platform_device.h>
> +#include <linux/regmap.h>

So close!

> +#define CHT_WC_LED1_CTRL		0x5e1f
> +#define CHT_WC_LED1_FSM			0x5e20
> +#define CHT_WC_LED1_PWM			0x5e21
> +
> +#define CHT_WC_LED2_CTRL		0x4fdf
> +#define CHT_WC_LED2_FSM			0x4fe0
> +#define CHT_WC_LED2_PWM			0x4fe1
> +
> +/* HW or SW control of charging led */
> +#define CHT_WC_LED1_SWCTL		BIT(0)
> +#define CHT_WC_LED1_ON			BIT(1)
> +
> +#define CHT_WC_LED2_ON			BIT(0)
> +#define CHT_WC_LED_I_MA2_5		(2 << 2)
> +/* LED current limit */

If you're documenting a single define, care to put it on the same line?

> +#define CHT_WC_LED_I_MASK		GENMASK(3, 2)
> +
> +#define CHT_WC_LED_F_1_4_HZ		(0 << 4)
> +#define CHT_WC_LED_F_1_2_HZ		(1 << 4)
> +#define CHT_WC_LED_F_1_HZ		(2 << 4)
> +#define CHT_WC_LED_F_2_HZ		(3 << 4)
> +#define CHT_WC_LED_F_MASK		GENMASK(5, 4)
> +
> +#define CHT_WC_LED_EFF_OFF		(0 << 1)
> +#define CHT_WC_LED_EFF_ON		(1 << 1)
> +#define CHT_WC_LED_EFF_BLINKING		(2 << 1)
> +#define CHT_WC_LED_EFF_BREATHING	(3 << 1)
> +#define CHT_WC_LED_EFF_MASK		GENMASK(2, 1)
> +
> +#define CHT_WC_LED_COUNT		2
> +
> +struct cht_wc_led_regs {
> +	/* Register addresses */
> +	u16 ctrl;
> +	u16 fsm;
> +	u16 pwm;
> +	/* Mask + values for turning the LED on/off */
> +	u8 on_off_mask;
> +	u8 on_val;
> +	u8 off_val;
> +};
> +
> +struct cht_wc_led_saved_regs {
> +	unsigned int ctrl;
> +	unsigned int fsm;
> +	unsigned int pwm;
> +};
> +
> +struct cht_wc_led {
> +	struct led_classdev cdev;
> +	const struct cht_wc_led_regs *regs;
> +	struct regmap *regmap;
> +	struct mutex mutex;
> +};
> +
> +struct cht_wc_leds {
> +	struct cht_wc_led leds[CHT_WC_LED_COUNT];
> +	/* Saved LED1 initial register values */
> +	struct cht_wc_led_saved_regs led1_initial_regs;
> +};
> +
> +static const struct cht_wc_led_regs cht_wc_led_regs[CHT_WC_LED_COUNT] = {
> +	{
> +		.ctrl		= CHT_WC_LED1_CTRL,
> +		.fsm		= CHT_WC_LED1_FSM,
> +		.pwm		= CHT_WC_LED1_PWM,
> +		.on_off_mask	= CHT_WC_LED1_SWCTL | CHT_WC_LED1_ON,
> +		.on_val		= CHT_WC_LED1_SWCTL | CHT_WC_LED1_ON,
> +		.off_val	= CHT_WC_LED1_SWCTL,
> +	},
> +	{
> +		.ctrl		= CHT_WC_LED2_CTRL,
> +		.fsm		= CHT_WC_LED2_FSM,
> +		.pwm		= CHT_WC_LED2_PWM,
> +		.on_off_mask	= CHT_WC_LED2_ON,
> +		.on_val		= CHT_WC_LED2_ON,
> +		.off_val	= 0,
> +	},
> +};
> +
> +static const char * const cht_wc_leds_names[CHT_WC_LED_COUNT] = {
> +	"platform::" LED_FUNCTION_CHARGING,
> +	"platform::" LED_FUNCTION_INDICATOR,
> +};
> +
> +static int cht_wc_leds_brightness_set(struct led_classdev *cdev,
> +				      enum led_brightness value)
> +{
> +	struct cht_wc_led *led = container_of(cdev, struct cht_wc_led, cdev);
> +	int ret;
> +
> +	mutex_lock(&led->mutex);
> +
> +	if (!value) {
> +		ret = regmap_update_bits(led->regmap, led->regs->ctrl,
> +					 led->regs->on_off_mask, led->regs->off_val);
> +		if (ret < 0) {
> +			dev_err(cdev->dev, "Failed to turn off: %d\n", ret);
> +			goto out;
> +		}
> +
> +		/* Disable HW blinking */
> +		ret = regmap_update_bits(led->regmap, led->regs->fsm,
> +					 CHT_WC_LED_EFF_MASK, CHT_WC_LED_EFF_ON);
> +		if (ret < 0)
> +			dev_err(cdev->dev, "Failed to update LED FSM reg: %d\n", ret);
> +	} else {
> +		ret = regmap_write(led->regmap, led->regs->pwm, value);
> +		if (ret < 0) {
> +			dev_err(cdev->dev, "Failed to set brightness: %d\n", ret);
> +			goto out;
> +		}
> +
> +		ret = regmap_update_bits(led->regmap, led->regs->ctrl,
> +					 led->regs->on_off_mask, led->regs->on_val);
> +		if (ret < 0)
> +			dev_err(cdev->dev, "Failed to turn on: %d\n", ret);
> +	}
> +out:
> +	mutex_unlock(&led->mutex);
> +	return ret;
> +}
> +
> +enum led_brightness cht_wc_leds_brightness_get(struct led_classdev *cdev)
> +{
> +	struct cht_wc_led *led = container_of(cdev, struct cht_wc_led, cdev);
> +	unsigned int val;
> +	int ret;
> +
> +	mutex_lock(&led->mutex);
> +
> +	ret = regmap_read(led->regmap, led->regs->ctrl, &val);
> +	if (ret < 0) {
> +		dev_err(cdev->dev, "Failed to read LED CTRL reg: %d\n", ret);
> +		ret = LED_OFF;


include/linux/leds.h:

/* This is obsolete/useless. We now support variable maximum brightness. */
enum led_brightness {
   LED_OFF         = 0,
   LED_ON          = 1,
   LED_HALF        = 127,
   LED_FULL        = 255,
};

> +		goto done;
> +	}
> +
> +	val &= led->regs->on_off_mask;
> +	if (val != led->regs->on_val) {
> +		ret = LED_OFF;
> +		goto done;
> +	}
> +
> +	ret = regmap_read(led->regmap, led->regs->pwm, &val);
> +	if (ret < 0) {
> +		dev_err(cdev->dev, "Failed to read LED PWM reg: %d\n", ret);
> +		ret = LED_ON;
> +		goto done;
> +	}
> +
> +	ret = val;
> +done:
> +	mutex_unlock(&led->mutex);
> +
> +	return ret;
> +}
> +
> +/* Return blinking period for given CTRL reg value */
> +static unsigned long cht_wc_leds_get_period(int ctrl)
> +{
> +	ctrl &= CHT_WC_LED_F_MASK;
> +
> +	switch (ctrl) {
> +	case CHT_WC_LED_F_1_4_HZ:
> +		return 1000 * 4;
> +	case CHT_WC_LED_F_1_2_HZ:
> +		return 1000 * 2;
> +	case CHT_WC_LED_F_1_HZ:
> +		return 1000;
> +	case CHT_WC_LED_F_2_HZ:
> +		return 1000 / 2;
> +	};
> +
> +	return 0;
> +}
> +
> +/*
> + * Find suitable hardware blink mode for given period.
> + * period < 750 ms - select 2 HZ
> + * 750 ms <= period < 1500 ms - select 1 HZ
> + * 1500 ms <= period < 3000 ms - select 1/2 HZ
> + * 3000 ms <= period < 5000 ms - select 1/4 HZ
> + * 5000 ms <= period - return -1
> + */
> +static int cht_wc_leds_find_freq(unsigned long period)
> +{
> +	if (period < 750)
> +		return CHT_WC_LED_F_2_HZ;
> +	else if (period < 1500)
> +		return CHT_WC_LED_F_1_HZ;
> +	else if (period < 3000)
> +		return CHT_WC_LED_F_1_2_HZ;
> +	else if (period < 5000)
> +		return CHT_WC_LED_F_1_4_HZ;
> +	else
> +		return -1;
> +}
> +
> +static int cht_wc_leds_blink_set(struct led_classdev *cdev,
> +				 unsigned long *delay_on,
> +				 unsigned long *delay_off)
> +{
> +	struct cht_wc_led *led = container_of(cdev, struct cht_wc_led, cdev);
> +	unsigned int ctrl;
> +	int ret;
> +
> +	mutex_lock(&led->mutex);
> +
> +	/* blink with 1 Hz as default if nothing specified */

Nit: "Blink"

> +	if (!*delay_on && !*delay_off)
> +		*delay_on = *delay_off = 500;
> +
> +	ctrl = cht_wc_leds_find_freq(*delay_on + *delay_off);
> +	if (ctrl < 0) {
> +		/* Disable HW blinking */
> +		ret = regmap_update_bits(led->regmap, led->regs->fsm,
> +					 CHT_WC_LED_EFF_MASK, CHT_WC_LED_EFF_ON);
> +		if (ret < 0)
> +			dev_err(cdev->dev, "Failed to update LED FSM reg: %d\n", ret);
> +
> +		/* Fallback to software timer */
> +		*delay_on = *delay_off = 0;
> +		ret = -EINVAL;
> +		goto done;
> +	}
> +
> +	ret = regmap_update_bits(led->regmap, led->regs->fsm,
> +				 CHT_WC_LED_EFF_MASK, CHT_WC_LED_EFF_BLINKING);
> +	if (ret < 0)
> +		dev_err(cdev->dev, "Failed to update LED FSM reg: %d\n", ret);
> +
> +	/* Set the frequency and make sure the LED is on */
> +	ret = regmap_update_bits(led->regmap, led->regs->ctrl,
> +				 CHT_WC_LED_F_MASK | led->regs->on_off_mask,
> +				 ctrl | led->regs->on_val);
> +	if (ret < 0)
> +		dev_err(cdev->dev, "Failed to update LED CTRL reg: %d\n", ret);
> +
> +	*delay_off = *delay_on = cht_wc_leds_get_period(ctrl) / 2;
> +
> +done:
> +	mutex_unlock(&led->mutex);
> +
> +	return ret;
> +}
> +
> +static int cht_wc_led_save_regs(struct cht_wc_led *led,
> +				struct cht_wc_led_saved_regs *saved_regs)
> +{
> +	int ret;
> +
> +	ret = regmap_read(led->regmap, led->regs->ctrl, &saved_regs->ctrl);
> +	if (ret < 0)
> +		return ret;
> +
> +	ret = regmap_read(led->regmap, led->regs->fsm, &saved_regs->fsm);
> +	if (ret < 0)
> +		return ret;
> +
> +	return regmap_read(led->regmap, led->regs->pwm, &saved_regs->pwm);
> +}
> +
> +static void cht_wc_led_restore_regs(struct cht_wc_led *led,
> +				    const struct cht_wc_led_saved_regs *saved_regs)
> +{
> +	regmap_write(led->regmap, led->regs->ctrl, saved_regs->ctrl);
> +	regmap_write(led->regmap, led->regs->fsm, saved_regs->fsm);
> +	regmap_write(led->regmap, led->regs->pwm, saved_regs->pwm);
> +}
> +
> +static int cht_wc_leds_probe(struct platform_device *pdev)
> +{
> +	struct intel_soc_pmic *pmic = dev_get_drvdata(pdev->dev.parent);

platform_*()

> +	struct cht_wc_leds *leds;
> +	int ret;
> +	int i;
> +
> +	leds = devm_kzalloc(&pdev->dev, sizeof(*leds), GFP_KERNEL);
> +	if (!leds)
> +		return -ENOMEM;
> +
> +	/*
> +	 * LED1 might be in hw-controlled mode when this driver gets loaded; and
> +	 * since the PMIC is always powered by the battery any changes made are
> +	 * permanent. Save LED1 regs to restore them on remove() or shutdown().
> +	 */
> +	leds->leds[0].regs = &cht_wc_led_regs[0];
> +	leds->leds[0].regmap = pmic->regmap;
> +	ret = cht_wc_led_save_regs(&leds->leds[0], &leds->led1_initial_regs);
> +	if (ret < 0)
> +		return ret;
> +
> +	for (i = 0; i < CHT_WC_LED_COUNT; i++) {
> +		struct cht_wc_led *led = &leds->leds[i];
> +
> +		led->regs = &cht_wc_led_regs[i];
> +		led->regmap = pmic->regmap;
> +		mutex_init(&led->mutex);
> +		led->cdev.name = cht_wc_leds_names[i];
> +		led->cdev.brightness_set_blocking = cht_wc_leds_brightness_set;
> +		led->cdev.brightness_get = cht_wc_leds_brightness_get;
> +		led->cdev.blink_set = cht_wc_leds_blink_set;
> +		led->cdev.max_brightness = 255;
> +
> +		ret = led_classdev_register(&pdev->dev, &led->cdev);
> +		if (ret < 0)
> +			return ret;
> +	}
> +
> +	platform_set_drvdata(pdev, leds);
> +	return 0;
> +}
> +
> +static void cht_wc_leds_remove(struct platform_device *pdev)
> +{
> +	struct cht_wc_leds *leds = platform_get_drvdata(pdev);
> +	int i;
> +
> +	for (i = 0; i < CHT_WC_LED_COUNT; i++)
> +		led_classdev_unregister(&leds->leds[i].cdev);
> +
> +	/* Restore LED1 regs if hw-control was active, else leave LED1 off. */

Either use full-stops, or don't.  Please be consistent.

> +	if (!(leds->led1_initial_regs.ctrl & CHT_WC_LED1_SWCTL))
> +		cht_wc_led_restore_regs(&leds->leds[0], &leds->led1_initial_regs);
> +}
> +
> +static void cht_wc_leds_disable(struct platform_device *pdev)
> +{
> +	struct cht_wc_leds *leds = platform_get_drvdata(pdev);
> +	int i;
> +
> +	for (i = 0; i < CHT_WC_LED_COUNT; i++)
> +		cht_wc_leds_brightness_set(&leds->leds[i].cdev, LED_OFF);
> +
> +	/* Restore LED1 regs if hw-control was active, else leave LED1 off. */
> +	if (!(leds->led1_initial_regs.ctrl & CHT_WC_LED1_SWCTL))
> +		cht_wc_led_restore_regs(&leds->leds[0], &leds->led1_initial_regs);
> +}
> +
> +static struct platform_driver cht_wc_leds_driver = {
> +	.probe = cht_wc_leds_probe,
> +	.remove_new = cht_wc_leds_remove,

This is new to me.  What does remove_new do?

Just returns void?

> +	.shutdown = cht_wc_leds_disable,
> +	.driver = {
> +		.name = "cht_wcove_leds",
> +	},
> +};
> +module_platform_driver(cht_wc_leds_driver);
> +
> +MODULE_ALIAS("platform:cht_wcove_leds");
> +MODULE_DESCRIPTION("Intel Cherry Trail Whiskey Cove PMIC LEDs driver");
> +MODULE_AUTHOR("Yauhen Kharuzhy <jekhor@xxxxxxxxx>");
> +MODULE_LICENSE("GPL");
> -- 
> 2.39.2
> 

-- 
Lee Jones [李琼斯]



[Index of Archives]     [Linux ARM Kernel]     [Linux ARM]     [Linux Omap]     [Fedora ARM]     [IETF Annouce]     [Security]     [Bugtraq]     [Linux OMAP]     [Linux MIPS]     [ECOS]     [Asterisk Internet PBX]     [Linux API]

  Powered by Linux