diff mbox series

[v5] backlight: mp3309c: Add support for MPS MP3309C

Message ID 20231020135434.2598578-3-f.suligoi@asem.it (mailing list archive)
State New, archived
Headers show
Series [v5] backlight: mp3309c: Add support for MPS MP3309C | expand

Commit Message

Flavio Suligoi Oct. 20, 2023, 1:54 p.m. UTC
The Monolithic Power (MPS) MP3309C is a WLED step-up converter, featuring a
programmable switching frequency to optimize efficiency.
The brightness can be controlled either by I2C commands (called "analog"
mode) or by a PWM input signal (PWM mode).
This driver supports both modes.

For DT configuration details, please refer to:
- Documentation/devicetree/bindings/leds/backlight/mps,mp3309c.yaml

The datasheet is available at:
- https://www.monolithicpower.com/en/mp3309c.html

Signed-off-by: Flavio Suligoi <f.suligoi@asem.it>
---

v5:
 - checked and resend for updated kernel 6.6.0-rc1
v4:
 - add brightness-levels property
 - force fixed 32 brightness levels (0..31) in analog-i2c dimming mode
 - remove useless irq and reset_gpio from mp3309c_chip structure
v3:
 - fix SPDX obsolete spelling
 - in mp3309c_bl_update_status, change from msleep_interruptible() to msleep()
   and improve the related comment
v2:
 - fix dependecies in Kconfig
 - fix Kconfig MP3309C entry order
 - remove switch-on-delay-ms property
 - remove optional gpio property to reset external devices
 - remove dimming-mode property (the analog-i2c dimming mode is the default; the
   presence of the pwms property, in DT, selects automatically the pwm dimming
   mode)
 - substitute three boolean properties, used for the overvoltage-protection
   values, with a single enum property
 - drop simple tracing messages
 - use dev_err_probe() in probe function
 - change device name from mp3309c_bl to the simple mp3309c
 - remove shutdown function
v1:
 - first version

 MAINTAINERS                       |   7 +
 drivers/video/backlight/Kconfig   |  11 +
 drivers/video/backlight/Makefile  |   1 +
 drivers/video/backlight/mp3309c.c | 443 ++++++++++++++++++++++++++++++
 4 files changed, 462 insertions(+)
 create mode 100644 drivers/video/backlight/mp3309c.c

Comments

Conor Dooley Oct. 20, 2023, 3:56 p.m. UTC | #1
On Fri, Oct 20, 2023 at 03:54:34PM +0200, Flavio Suligoi wrote:
> The Monolithic Power (MPS) MP3309C is a WLED step-up converter, featuring a
> programmable switching frequency to optimize efficiency.
> The brightness can be controlled either by I2C commands (called "analog"
> mode) or by a PWM input signal (PWM mode).
> This driver supports both modes.
> 
> For DT configuration details, please refer to:
> - Documentation/devicetree/bindings/leds/backlight/mps,mp3309c.yaml
> 
> The datasheet is available at:
> - https://www.monolithicpower.com/en/mp3309c.html
> 
> Signed-off-by: Flavio Suligoi <f.suligoi@asem.it>
> ---
> 
> v5:
>  - checked and resend for updated kernel 6.6.0-rc1

Why is this v5 patch attached to a 1 patch "series" that only purports
to contain a binding patch?

Thanks,
Conor.

> v4:
>  - add brightness-levels property
>  - force fixed 32 brightness levels (0..31) in analog-i2c dimming mode
>  - remove useless irq and reset_gpio from mp3309c_chip structure
> v3:
>  - fix SPDX obsolete spelling
>  - in mp3309c_bl_update_status, change from msleep_interruptible() to msleep()
>    and improve the related comment
> v2:
>  - fix dependecies in Kconfig
>  - fix Kconfig MP3309C entry order
>  - remove switch-on-delay-ms property
>  - remove optional gpio property to reset external devices
>  - remove dimming-mode property (the analog-i2c dimming mode is the default; the
>    presence of the pwms property, in DT, selects automatically the pwm dimming
>    mode)
>  - substitute three boolean properties, used for the overvoltage-protection
>    values, with a single enum property
>  - drop simple tracing messages
>  - use dev_err_probe() in probe function
>  - change device name from mp3309c_bl to the simple mp3309c
>  - remove shutdown function
> v1:
>  - first version
> 
>  MAINTAINERS                       |   7 +
>  drivers/video/backlight/Kconfig   |  11 +
>  drivers/video/backlight/Makefile  |   1 +
>  drivers/video/backlight/mp3309c.c | 443 ++++++++++++++++++++++++++++++
>  4 files changed, 462 insertions(+)
>  create mode 100644 drivers/video/backlight/mp3309c.c
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 90f13281d297..3d23b869e2aa 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -14474,6 +14474,13 @@ S:	Maintained
>  F:	Documentation/driver-api/tty/moxa-smartio.rst
>  F:	drivers/tty/mxser.*
>  
> +MP3309C BACKLIGHT DRIVER
> +M:	Flavio Suligoi <f.suligoi@asem.it>
> +L:	dri-devel@lists.freedesktop.org
> +S:	Maintained
> +F:	Documentation/devicetree/bindings/leds/backlight/mps,mp3309c.yaml
> +F:	drivers/video/backlight/mp3309c.c
> +
>  MR800 AVERMEDIA USB FM RADIO DRIVER
>  M:	Alexey Klimov <klimov.linux@gmail.com>
>  L:	linux-media@vger.kernel.org
> diff --git a/drivers/video/backlight/Kconfig b/drivers/video/backlight/Kconfig
> index 51387b1ef012..1144a54a35c0 100644
> --- a/drivers/video/backlight/Kconfig
> +++ b/drivers/video/backlight/Kconfig
> @@ -402,6 +402,17 @@ config BACKLIGHT_LP8788
>  	help
>  	  This supports TI LP8788 backlight driver.
>  
> +config BACKLIGHT_MP3309C
> +	tristate "Backlight Driver for MPS MP3309C"
> +	depends on I2C && PWM
> +	select REGMAP_I2C
> +	help
> +	  This supports MPS MP3309C backlight WLED driver in both PWM and
> +	  analog/I2C dimming modes.
> +
> +	  To compile this driver as a module, choose M here: the module will
> +	  be called mp3309c.
> +
>  config BACKLIGHT_PANDORA
>  	tristate "Backlight driver for Pandora console"
>  	depends on TWL4030_CORE
> diff --git a/drivers/video/backlight/Makefile b/drivers/video/backlight/Makefile
> index f72e1c3c59e9..1af583de665b 100644
> --- a/drivers/video/backlight/Makefile
> +++ b/drivers/video/backlight/Makefile
> @@ -44,6 +44,7 @@ obj-$(CONFIG_BACKLIGHT_LP855X)		+= lp855x_bl.o
>  obj-$(CONFIG_BACKLIGHT_LP8788)		+= lp8788_bl.o
>  obj-$(CONFIG_BACKLIGHT_LV5207LP)	+= lv5207lp.o
>  obj-$(CONFIG_BACKLIGHT_MAX8925)		+= max8925_bl.o
> +obj-$(CONFIG_BACKLIGHT_MP3309C)		+= mp3309c.o
>  obj-$(CONFIG_BACKLIGHT_MT6370)		+= mt6370-backlight.o
>  obj-$(CONFIG_BACKLIGHT_OMAP1)		+= omap1_bl.o
>  obj-$(CONFIG_BACKLIGHT_PANDORA)		+= pandora_bl.o
> diff --git a/drivers/video/backlight/mp3309c.c b/drivers/video/backlight/mp3309c.c
> new file mode 100644
> index 000000000000..3fe4469ef43f
> --- /dev/null
> +++ b/drivers/video/backlight/mp3309c.c
> @@ -0,0 +1,443 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Driver for MPS MP3309C White LED driver with I2C interface
> + *
> + * This driver support both analog (by I2C commands) and PWM dimming control
> + * modes.
> + *
> + * Copyright (C) 2023 ASEM Srl
> + * Author: Flavio Suligoi <f.suligoi@asem.it>
> + *
> + * Based on pwm_bl.c
> + */
> +
> +#include <linux/backlight.h>
> +#include <linux/delay.h>
> +#include <linux/gpio/consumer.h>
> +#include <linux/i2c.h>
> +#include <linux/pwm.h>
> +#include <linux/regmap.h>
> +
> +#define REG_I2C_0	0x00
> +#define REG_I2C_1	0x01
> +
> +#define REG_I2C_0_EN	0x80
> +#define REG_I2C_0_D0	0x40
> +#define REG_I2C_0_D1	0x20
> +#define REG_I2C_0_D2	0x10
> +#define REG_I2C_0_D3	0x08
> +#define REG_I2C_0_D4	0x04
> +#define REG_I2C_0_RSRV1	0x02
> +#define REG_I2C_0_RSRV2	0x01
> +
> +#define REG_I2C_1_RSRV1	0x80
> +#define REG_I2C_1_DIMS	0x40
> +#define REG_I2C_1_SYNC	0x20
> +#define REG_I2C_1_OVP0	0x10
> +#define REG_I2C_1_OVP1	0x08
> +#define REG_I2C_1_VOS	0x04
> +#define REG_I2C_1_LEDO	0x02
> +#define REG_I2C_1_OTP	0x01
> +
> +#define ANALOG_I2C_NUM_LEVELS	32		/* 0..31 */
> +#define ANALOG_I2C_REG_MASK	0x7c
> +
> +#define MP3309C_PWM_DEFAULT_NUM_LEVELS	256	/* 0..255 */
> +
> +enum mp3309c_status_value {
> +	FIRST_POWER_ON,
> +	BACKLIGHT_OFF,
> +	BACKLIGHT_ON,
> +};
> +
> +enum mp3309c_dimming_mode_value {
> +	DIMMING_PWM,
> +	DIMMING_ANALOG_I2C,
> +};
> +
> +struct mp3309c_platform_data {
> +	unsigned int max_brightness;
> +	unsigned int default_brightness;
> +	unsigned int *levels;
> +	u8  dimming_mode;
> +	u8  over_voltage_protection;
> +	bool sync_mode;
> +	u8 status;
> +};
> +
> +struct mp3309c_chip {
> +	struct device *dev;
> +	struct mp3309c_platform_data *pdata;
> +	struct backlight_device *bl;
> +	struct gpio_desc *enable_gpio;
> +	struct regmap *regmap;
> +	struct pwm_device *pwmd;
> +};
> +
> +static const struct regmap_config mp3309c_regmap = {
> +	.name = "mp3309c_regmap",
> +	.reg_bits = 8,
> +	.reg_stride = 1,
> +	.val_bits = 8,
> +	.max_register = REG_I2C_1,
> +};
> +
> +static int mp3309c_enable_device(struct mp3309c_chip *chip)
> +{
> +	u8 reg_val;
> +	int ret;
> +
> +	/* I2C register #0 - Device enable */
> +	ret = regmap_update_bits(chip->regmap, REG_I2C_0, REG_I2C_0_EN,
> +				 REG_I2C_0_EN);
> +	if (ret)
> +		return ret;
> +
> +	/*
> +	 * I2C register #1 - Set working mode:
> +	 *  - set one of the two dimming mode:
> +	 *    - PWM dimming using an external PWM dimming signal
> +	 *    - analog dimming using I2C commands
> +	 *  - enable/disable synchronous mode
> +	 *  - set overvoltage protection (OVP)
> +	 */
> +	reg_val = 0x00;
> +	if (chip->pdata->dimming_mode == DIMMING_PWM)
> +		reg_val |= REG_I2C_1_DIMS;
> +	if (chip->pdata->sync_mode)
> +		reg_val |= REG_I2C_1_SYNC;
> +	reg_val |= chip->pdata->over_voltage_protection;
> +	ret = regmap_write(chip->regmap, REG_I2C_1, reg_val);
> +	if (ret)
> +		return ret;
> +
> +	return 0;
> +}
> +
> +static int mp3309c_bl_update_status(struct backlight_device *bl)
> +{
> +	struct mp3309c_chip *chip = bl_get_data(bl);
> +	int brightness = backlight_get_brightness(bl);
> +	struct pwm_state pwmstate;
> +	unsigned int analog_val, bits_val;
> +	int i, ret;
> +
> +	if (chip->pdata->dimming_mode == DIMMING_PWM) {
> +		/*
> +		 * PWM control mode
> +		 */
> +		pwm_get_state(chip->pwmd, &pwmstate);
> +		pwm_set_relative_duty_cycle(&pwmstate,
> +					    chip->pdata->levels[brightness],
> +					    chip->pdata->levels[chip->pdata->max_brightness]);
> +		pwmstate.enabled = true;
> +		ret = pwm_apply_state(chip->pwmd, &pwmstate);
> +		if (ret)
> +			return ret;
> +
> +		switch (chip->pdata->status) {
> +		case FIRST_POWER_ON:
> +		case BACKLIGHT_OFF:
> +			/*
> +			 * After 20ms of low pwm signal level, the chip turns
> +			 * off automatically. In this case, before enabling the
> +			 * chip again, we must wait about 10ms for pwm signal to
> +			 * stabilize.
> +			 */
> +			if (brightness > 0) {
> +				msleep(10);
> +				mp3309c_enable_device(chip);
> +				chip->pdata->status = BACKLIGHT_ON;
> +			} else {
> +				chip->pdata->status = BACKLIGHT_OFF;
> +			}
> +			break;
> +		case BACKLIGHT_ON:
> +			if (brightness == 0)
> +				chip->pdata->status = BACKLIGHT_OFF;
> +			break;
> +		}
> +	} else {
> +		/*
> +		 * Analog (by I2C command) control mode
> +		 *
> +		 * The first time, before setting brightness, we must enable the
> +		 * device
> +		 */
> +		if (chip->pdata->status == FIRST_POWER_ON)
> +			mp3309c_enable_device(chip);
> +
> +		/*
> +		 * Dimming mode I2C command (fixed dimming range 0..31)
> +		 *
> +		 * The 5 bits of the dimming analog value D4..D0 is allocated
> +		 * in the I2C register #0, in the following way:
> +		 *
> +		 *     +--+--+--+--+--+--+--+--+
> +		 *     |EN|D0|D1|D2|D3|D4|XX|XX|
> +		 *     +--+--+--+--+--+--+--+--+
> +		 */
> +		analog_val = brightness;
> +		bits_val = 0;
> +		for (i = 0; i <= 5; i++)
> +			bits_val += ((analog_val >> i) & 0x01) << (6 - i);
> +		ret = regmap_update_bits(chip->regmap, REG_I2C_0,
> +					 ANALOG_I2C_REG_MASK, bits_val);
> +		if (ret)
> +			return ret;
> +
> +		if (brightness > 0)
> +			chip->pdata->status = BACKLIGHT_ON;
> +		else
> +			chip->pdata->status = BACKLIGHT_OFF;
> +	}
> +
> +	return 0;
> +}
> +
> +static const struct backlight_ops mp3309c_bl_ops = {
> +	.update_status = mp3309c_bl_update_status,
> +};
> +
> +static int pm3309c_parse_dt_node(struct mp3309c_chip *chip,
> +				 struct mp3309c_platform_data *pdata)
> +{
> +	struct device_node *node = chip->dev->of_node;
> +	struct property *prop_pwms, *prop_levels;
> +	int length = 0;
> +	int ret, i;
> +	unsigned int num_levels, tmp_value;
> +
> +	if (!node) {
> +		dev_err(chip->dev, "failed to get DT node\n");
> +		return -ENODEV;
> +	}
> +
> +	/*
> +	 * Dimming mode: the MP3309C provides two dimming control mode:
> +	 *
> +	 * - PWM mode
> +	 * - Analog by I2C control mode (default)
> +	 *
> +	 * I2C control mode is assumed as default but, if the pwms property is
> +	 * found in the backlight node, the mode switches to PWM mode.
> +	 */
> +	pdata->dimming_mode = DIMMING_ANALOG_I2C;
> +	prop_pwms = of_find_property(node, "pwms", &length);
> +	if (prop_pwms) {
> +		chip->pwmd = devm_pwm_get(chip->dev, NULL);
> +		if (IS_ERR(chip->pwmd))
> +			return dev_err_probe(chip->dev, PTR_ERR(chip->pwmd),
> +					     "error getting pwm data\n");
> +		pdata->dimming_mode = DIMMING_PWM;
> +		pwm_apply_args(chip->pwmd);
> +	}
> +
> +	/*
> +	 * In I2C control mode the dimming levels (0..31) are fixed by the
> +	 * hardware, while in PWM control mode they can be chosen by the user,
> +	 * to allow nonlinear mappings.
> +	 */
> +	if  (pdata->dimming_mode == DIMMING_ANALOG_I2C) {
> +		/*
> +		 * Analog (by I2C commands) control mode: fixed 0..31 brightness
> +		 * levels
> +		 */
> +		num_levels = ANALOG_I2C_NUM_LEVELS;
> +
> +		/* Enable GPIO used in I2C dimming mode only */
> +		chip->enable_gpio = devm_gpiod_get(chip->dev, "enable",
> +						   GPIOD_OUT_HIGH);
> +		if (IS_ERR(chip->enable_gpio))
> +			return dev_err_probe(chip->dev,
> +					     PTR_ERR(chip->enable_gpio),
> +					     "error getting enable gpio\n");
> +	} else {
> +		/*
> +		 * PWM control mode: check for brightness level in DT
> +		 */
> +		prop_levels = of_find_property(node, "brightness-levels",
> +					       &length);
> +		if (prop_levels) {
> +			/* Read brightness levels from DT */
> +			num_levels = length / sizeof(u32);
> +			if (num_levels < 2)
> +				return -EINVAL;
> +		} else {
> +			/* Use default brightness levels */
> +			num_levels = MP3309C_PWM_DEFAULT_NUM_LEVELS;
> +		}
> +	}
> +
> +	/* Fill brightness levels array */
> +	pdata->levels = devm_kcalloc(chip->dev, num_levels,
> +				     sizeof(*pdata->levels), GFP_KERNEL);
> +	if (!pdata->levels)
> +		return -ENOMEM;
> +	if (prop_levels) {
> +		ret = of_property_read_u32_array(node, "brightness-levels",
> +						 pdata->levels,
> +						 num_levels);
> +		if (ret < 0)
> +			return ret;
> +	} else {
> +		for (i = 0; i < num_levels; i++)
> +			pdata->levels[i] = i;
> +	}
> +
> +	pdata->max_brightness = num_levels - 1;
> +
> +	ret = of_property_read_u32(node, "default-brightness",
> +				   &pdata->default_brightness);
> +	if (ret)
> +		pdata->default_brightness = pdata->max_brightness;
> +	if (pdata->default_brightness > pdata->max_brightness) {
> +		dev_err(chip->dev,
> +			"default brightness exceeds max brightness\n");
> +		pdata->default_brightness = pdata->max_brightness;
> +	}
> +
> +	/*
> +	 * Over-voltage protection (OVP)
> +	 *
> +	 * This (optional) property values are:
> +	 *
> +	 *  - 13.5V
> +	 *  - 24V
> +	 *  - 35.5V (hardware default setting)
> +	 *
> +	 * If missing, the default value for OVP is 35.5V
> +	 */
> +	pdata->over_voltage_protection = REG_I2C_1_OVP1;
> +	if (!of_property_read_u32(node, "mps,overvoltage-protection-microvolt",
> +				  &tmp_value)) {
> +		switch (tmp_value) {
> +		case 13500000:
> +			pdata->over_voltage_protection = 0x00;
> +			break;
> +		case 24000000:
> +			pdata->over_voltage_protection = REG_I2C_1_OVP0;
> +			break;
> +		case 35500000:
> +			pdata->over_voltage_protection = REG_I2C_1_OVP1;
> +			break;
> +		default:
> +			return -EINVAL;
> +		}
> +	}
> +
> +	/* Synchronous (default) and non-synchronous mode */
> +	pdata->sync_mode = true;
> +	if (of_property_read_bool(node, "mps,no-sync-mode"))
> +		pdata->sync_mode = false;
> +
> +	return 0;
> +}
> +
> +static int mp3309c_probe(struct i2c_client *client)
> +{
> +	struct mp3309c_platform_data *pdata = dev_get_platdata(&client->dev);
> +	struct mp3309c_chip *chip;
> +	struct backlight_properties props;
> +	struct pwm_state pwmstate;
> +	int ret;
> +
> +	if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
> +		dev_err(&client->dev, "failed to check i2c functionality\n");
> +		return -EOPNOTSUPP;
> +	}
> +
> +	chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL);
> +	if (!chip)
> +		return -ENOMEM;
> +
> +	chip->dev = &client->dev;
> +
> +	chip->regmap = devm_regmap_init_i2c(client, &mp3309c_regmap);
> +	if (IS_ERR(chip->regmap))
> +		return dev_err_probe(&client->dev, PTR_ERR(chip->regmap),
> +				     "failed to allocate register map\n");
> +
> +	i2c_set_clientdata(client, chip);
> +
> +	if (!pdata) {
> +		pdata = devm_kzalloc(chip->dev, sizeof(*pdata), GFP_KERNEL);
> +		if (!pdata)
> +			return -ENOMEM;
> +
> +		ret = pm3309c_parse_dt_node(chip, pdata);
> +		if (ret)
> +			return ret;
> +	}
> +	chip->pdata = pdata;
> +
> +	/* Backlight properties */
> +	props.brightness = pdata->default_brightness;
> +	props.max_brightness = pdata->max_brightness;
> +	props.scale = BACKLIGHT_SCALE_LINEAR;
> +	props.type = BACKLIGHT_RAW;
> +	props.power = FB_BLANK_UNBLANK;
> +	props.fb_blank = FB_BLANK_UNBLANK;
> +	chip->bl = devm_backlight_device_register(chip->dev, "mp3309c",
> +						  chip->dev, chip,
> +						  &mp3309c_bl_ops, &props);
> +	if (IS_ERR(chip->bl))
> +		return dev_err_probe(chip->dev, PTR_ERR(chip->bl),
> +				     "error registering backlight device\n");
> +
> +	/* In PWM dimming mode, enable pwm device */
> +	if (chip->pdata->dimming_mode == DIMMING_PWM) {
> +		pwm_init_state(chip->pwmd, &pwmstate);
> +		pwm_set_relative_duty_cycle(&pwmstate,
> +					    chip->pdata->default_brightness,
> +					    chip->pdata->max_brightness);
> +		pwmstate.enabled = true;
> +		ret = pwm_apply_state(chip->pwmd, &pwmstate);
> +		if (ret)
> +			return dev_err_probe(chip->dev, ret,
> +					     "error setting pwm device\n");
> +	}
> +
> +	chip->pdata->status = FIRST_POWER_ON;
> +	backlight_update_status(chip->bl);
> +
> +	return 0;
> +}
> +
> +static void mp3309c_remove(struct i2c_client *client)
> +{
> +	struct mp3309c_chip *chip = i2c_get_clientdata(client);
> +	struct backlight_device *bl = chip->bl;
> +
> +	bl->props.power = FB_BLANK_POWERDOWN;
> +	bl->props.brightness = 0;
> +	backlight_update_status(chip->bl);
> +}
> +
> +static const struct of_device_id mp3309c_match_table[] = {
> +	{ .compatible = "mps,mp3309c", },
> +	{ },
> +};
> +MODULE_DEVICE_TABLE(of, mp3309c_match_table);
> +
> +static const struct i2c_device_id mp3309c_id[] = {
> +	{ "mp3309c", 0 },
> +	{ }
> +};
> +MODULE_DEVICE_TABLE(i2c, mp3309c_id);
> +
> +static struct i2c_driver mp3309c_i2c_driver = {
> +	.driver	= {
> +			.name		= KBUILD_MODNAME,
> +			.of_match_table	= mp3309c_match_table,
> +	},
> +	.probe		= mp3309c_probe,
> +	.remove		= mp3309c_remove,
> +	.id_table	= mp3309c_id,
> +};
> +
> +module_i2c_driver(mp3309c_i2c_driver);
> +
> +MODULE_DESCRIPTION("Backlight Driver for MPS MP3309C");
> +MODULE_AUTHOR("Flavio Suligoi <f.suligoi@asem.it>");
> +MODULE_LICENSE("GPL");
> -- 
> 2.34.1
>
Flavio Suligoi Oct. 23, 2023, 9:11 a.m. UTC | #2
Hi Conor,

...

> Subject: Re: [PATCH v5] backlight: mp3309c: Add support for MPS MP3309C
> 
> On Fri, Oct 20, 2023 at 03:54:34PM +0200, Flavio Suligoi wrote:
> > The Monolithic Power (MPS) MP3309C is a WLED step-up converter,
> > featuring a programmable switching frequency to optimize efficiency.
> > The brightness can be controlled either by I2C commands (called "analog"
> > mode) or by a PWM input signal (PWM mode).
> > This driver supports both modes.
> >
> > For DT configuration details, please refer to:
> > - Documentation/devicetree/bindings/leds/backlight/mps,mp3309c.yaml
> >
> > The datasheet is available at:
> > - https://www.monolithicpower.com/en/mp3309c.html
> >
> > Signed-off-by: Flavio Suligoi <f.suligoi@asem.it>
> > ---
> >
> > v5:
> >  - checked and resend for updated kernel 6.6.0-rc1
> 
> Why is this v5 patch attached to a 1 patch "series" that only purports to
> contain a binding patch?

Sorry Conor, what do you mean for "binding patch"? The cover letter of this patch or the
other patch about the dt-bindings for the mp3309c backlight ?

> Thanks,
> Conor.

Thanks,
Flavio.


> 
> > v4:
> >  - add brightness-levels property
> >  - force fixed 32 brightness levels (0..31) in analog-i2c dimming mode
> >  - remove useless irq and reset_gpio from mp3309c_chip structure
> > v3:
> >  - fix SPDX obsolete spelling
> >  - in mp3309c_bl_update_status, change from msleep_interruptible() to
> msleep()
> >    and improve the related comment
> > v2:
> >  - fix dependecies in Kconfig
> >  - fix Kconfig MP3309C entry order
> >  - remove switch-on-delay-ms property
> >  - remove optional gpio property to reset external devices
> >  - remove dimming-mode property (the analog-i2c dimming mode is the
> default; the
> >    presence of the pwms property, in DT, selects automatically the pwm
> dimming
> >    mode)
> >  - substitute three boolean properties, used for the overvoltage-protection
> >    values, with a single enum property
> >  - drop simple tracing messages
> >  - use dev_err_probe() in probe function
> >  - change device name from mp3309c_bl to the simple mp3309c
> >  - remove shutdown function
> > v1:
> >  - first version
> >
> >  MAINTAINERS                       |   7 +
> >  drivers/video/backlight/Kconfig   |  11 +
> >  drivers/video/backlight/Makefile  |   1 +
> >  drivers/video/backlight/mp3309c.c | 443
> > ++++++++++++++++++++++++++++++
> >  4 files changed, 462 insertions(+)
> >  create mode 100644 drivers/video/backlight/mp3309c.c
> >
> > diff --git a/MAINTAINERS b/MAINTAINERS index
> > 90f13281d297..3d23b869e2aa 100644
> > --- a/MAINTAINERS
> > +++ b/MAINTAINERS
> > @@ -14474,6 +14474,13 @@ S:	Maintained
> >  F:	Documentation/driver-api/tty/moxa-smartio.rst
> >  F:	drivers/tty/mxser.*
> >
> > +MP3309C BACKLIGHT DRIVER
> > +M:	Flavio Suligoi <f.suligoi@asem.it>
> > +L:	dri-devel@lists.freedesktop.org
> > +S:	Maintained
> > +F:
> 	Documentation/devicetree/bindings/leds/backlight/mps,mp3309c.ya
> ml
> > +F:	drivers/video/backlight/mp3309c.c
> > +
> >  MR800 AVERMEDIA USB FM RADIO DRIVER
> >  M:	Alexey Klimov <klimov.linux@gmail.com>
> >  L:	linux-media@vger.kernel.org
> > diff --git a/drivers/video/backlight/Kconfig
> > b/drivers/video/backlight/Kconfig index 51387b1ef012..1144a54a35c0
> > 100644
> > --- a/drivers/video/backlight/Kconfig
> > +++ b/drivers/video/backlight/Kconfig
> > @@ -402,6 +402,17 @@ config BACKLIGHT_LP8788
> >  	help
> >  	  This supports TI LP8788 backlight driver.
> >
> > +config BACKLIGHT_MP3309C
> > +	tristate "Backlight Driver for MPS MP3309C"
> > +	depends on I2C && PWM
> > +	select REGMAP_I2C
> > +	help
> > +	  This supports MPS MP3309C backlight WLED driver in both PWM
> and
> > +	  analog/I2C dimming modes.
> > +
> > +	  To compile this driver as a module, choose M here: the module will
> > +	  be called mp3309c.
> > +
> >  config BACKLIGHT_PANDORA
> >  	tristate "Backlight driver for Pandora console"
> >  	depends on TWL4030_CORE
> > diff --git a/drivers/video/backlight/Makefile
> > b/drivers/video/backlight/Makefile
> > index f72e1c3c59e9..1af583de665b 100644
> > --- a/drivers/video/backlight/Makefile
> > +++ b/drivers/video/backlight/Makefile
> > @@ -44,6 +44,7 @@ obj-$(CONFIG_BACKLIGHT_LP855X)		+=
> lp855x_bl.o
> >  obj-$(CONFIG_BACKLIGHT_LP8788)		+= lp8788_bl.o
> >  obj-$(CONFIG_BACKLIGHT_LV5207LP)	+= lv5207lp.o
> >  obj-$(CONFIG_BACKLIGHT_MAX8925)		+= max8925_bl.o
> > +obj-$(CONFIG_BACKLIGHT_MP3309C)		+= mp3309c.o
> >  obj-$(CONFIG_BACKLIGHT_MT6370)		+= mt6370-backlight.o
> >  obj-$(CONFIG_BACKLIGHT_OMAP1)		+= omap1_bl.o
> >  obj-$(CONFIG_BACKLIGHT_PANDORA)		+= pandora_bl.o
> > diff --git a/drivers/video/backlight/mp3309c.c
> > b/drivers/video/backlight/mp3309c.c
> > new file mode 100644
> > index 000000000000..3fe4469ef43f
> > --- /dev/null
> > +++ b/drivers/video/backlight/mp3309c.c
> > @@ -0,0 +1,443 @@
> > +// SPDX-License-Identifier: GPL-2.0-or-later
> > +/*
> > + * Driver for MPS MP3309C White LED driver with I2C interface
> > + *
> > + * This driver support both analog (by I2C commands) and PWM dimming
> > +control
> > + * modes.
> > + *
> > + * Copyright (C) 2023 ASEM Srl
> > + * Author: Flavio Suligoi <f.suligoi@asem.it>
> > + *
> > + * Based on pwm_bl.c
> > + */
> > +
> > +#include <linux/backlight.h>
> > +#include <linux/delay.h>
> > +#include <linux/gpio/consumer.h>
> > +#include <linux/i2c.h>
> > +#include <linux/pwm.h>
> > +#include <linux/regmap.h>
> > +
> > +#define REG_I2C_0	0x00
> > +#define REG_I2C_1	0x01
> > +
> > +#define REG_I2C_0_EN	0x80
> > +#define REG_I2C_0_D0	0x40
> > +#define REG_I2C_0_D1	0x20
> > +#define REG_I2C_0_D2	0x10
> > +#define REG_I2C_0_D3	0x08
> > +#define REG_I2C_0_D4	0x04
> > +#define REG_I2C_0_RSRV1	0x02
> > +#define REG_I2C_0_RSRV2	0x01
> > +
> > +#define REG_I2C_1_RSRV1	0x80
> > +#define REG_I2C_1_DIMS	0x40
> > +#define REG_I2C_1_SYNC	0x20
> > +#define REG_I2C_1_OVP0	0x10
> > +#define REG_I2C_1_OVP1	0x08
> > +#define REG_I2C_1_VOS	0x04
> > +#define REG_I2C_1_LEDO	0x02
> > +#define REG_I2C_1_OTP	0x01
> > +
> > +#define ANALOG_I2C_NUM_LEVELS	32		/* 0..31 */
> > +#define ANALOG_I2C_REG_MASK	0x7c
> > +
> > +#define MP3309C_PWM_DEFAULT_NUM_LEVELS	256	/* 0..255 */
> > +
> > +enum mp3309c_status_value {
> > +	FIRST_POWER_ON,
> > +	BACKLIGHT_OFF,
> > +	BACKLIGHT_ON,
> > +};
> > +
> > +enum mp3309c_dimming_mode_value {
> > +	DIMMING_PWM,
> > +	DIMMING_ANALOG_I2C,
> > +};
> > +
> > +struct mp3309c_platform_data {
> > +	unsigned int max_brightness;
> > +	unsigned int default_brightness;
> > +	unsigned int *levels;
> > +	u8  dimming_mode;
> > +	u8  over_voltage_protection;
> > +	bool sync_mode;
> > +	u8 status;
> > +};
> > +
> > +struct mp3309c_chip {
> > +	struct device *dev;
> > +	struct mp3309c_platform_data *pdata;
> > +	struct backlight_device *bl;
> > +	struct gpio_desc *enable_gpio;
> > +	struct regmap *regmap;
> > +	struct pwm_device *pwmd;
> > +};
> > +
> > +static const struct regmap_config mp3309c_regmap = {
> > +	.name = "mp3309c_regmap",
> > +	.reg_bits = 8,
> > +	.reg_stride = 1,
> > +	.val_bits = 8,
> > +	.max_register = REG_I2C_1,
> > +};
> > +
> > +static int mp3309c_enable_device(struct mp3309c_chip *chip) {
> > +	u8 reg_val;
> > +	int ret;
> > +
> > +	/* I2C register #0 - Device enable */
> > +	ret = regmap_update_bits(chip->regmap, REG_I2C_0, REG_I2C_0_EN,
> > +				 REG_I2C_0_EN);
> > +	if (ret)
> > +		return ret;
> > +
> > +	/*
> > +	 * I2C register #1 - Set working mode:
> > +	 *  - set one of the two dimming mode:
> > +	 *    - PWM dimming using an external PWM dimming signal
> > +	 *    - analog dimming using I2C commands
> > +	 *  - enable/disable synchronous mode
> > +	 *  - set overvoltage protection (OVP)
> > +	 */
> > +	reg_val = 0x00;
> > +	if (chip->pdata->dimming_mode == DIMMING_PWM)
> > +		reg_val |= REG_I2C_1_DIMS;
> > +	if (chip->pdata->sync_mode)
> > +		reg_val |= REG_I2C_1_SYNC;
> > +	reg_val |= chip->pdata->over_voltage_protection;
> > +	ret = regmap_write(chip->regmap, REG_I2C_1, reg_val);
> > +	if (ret)
> > +		return ret;
> > +
> > +	return 0;
> > +}
> > +
> > +static int mp3309c_bl_update_status(struct backlight_device *bl) {
> > +	struct mp3309c_chip *chip = bl_get_data(bl);
> > +	int brightness = backlight_get_brightness(bl);
> > +	struct pwm_state pwmstate;
> > +	unsigned int analog_val, bits_val;
> > +	int i, ret;
> > +
> > +	if (chip->pdata->dimming_mode == DIMMING_PWM) {
> > +		/*
> > +		 * PWM control mode
> > +		 */
> > +		pwm_get_state(chip->pwmd, &pwmstate);
> > +		pwm_set_relative_duty_cycle(&pwmstate,
> > +					    chip->pdata->levels[brightness],
> > +					    chip->pdata->levels[chip->pdata-
> >max_brightness]);
> > +		pwmstate.enabled = true;
> > +		ret = pwm_apply_state(chip->pwmd, &pwmstate);
> > +		if (ret)
> > +			return ret;
> > +
> > +		switch (chip->pdata->status) {
> > +		case FIRST_POWER_ON:
> > +		case BACKLIGHT_OFF:
> > +			/*
> > +			 * After 20ms of low pwm signal level, the chip turns
> > +			 * off automatically. In this case, before enabling the
> > +			 * chip again, we must wait about 10ms for pwm
> signal to
> > +			 * stabilize.
> > +			 */
> > +			if (brightness > 0) {
> > +				msleep(10);
> > +				mp3309c_enable_device(chip);
> > +				chip->pdata->status = BACKLIGHT_ON;
> > +			} else {
> > +				chip->pdata->status = BACKLIGHT_OFF;
> > +			}
> > +			break;
> > +		case BACKLIGHT_ON:
> > +			if (brightness == 0)
> > +				chip->pdata->status = BACKLIGHT_OFF;
> > +			break;
> > +		}
> > +	} else {
> > +		/*
> > +		 * Analog (by I2C command) control mode
> > +		 *
> > +		 * The first time, before setting brightness, we must enable the
> > +		 * device
> > +		 */
> > +		if (chip->pdata->status == FIRST_POWER_ON)
> > +			mp3309c_enable_device(chip);
> > +
> > +		/*
> > +		 * Dimming mode I2C command (fixed dimming range 0..31)
> > +		 *
> > +		 * The 5 bits of the dimming analog value D4..D0 is allocated
> > +		 * in the I2C register #0, in the following way:
> > +		 *
> > +		 *     +--+--+--+--+--+--+--+--+
> > +		 *     |EN|D0|D1|D2|D3|D4|XX|XX|
> > +		 *     +--+--+--+--+--+--+--+--+
> > +		 */
> > +		analog_val = brightness;
> > +		bits_val = 0;
> > +		for (i = 0; i <= 5; i++)
> > +			bits_val += ((analog_val >> i) & 0x01) << (6 - i);
> > +		ret = regmap_update_bits(chip->regmap, REG_I2C_0,
> > +					 ANALOG_I2C_REG_MASK, bits_val);
> > +		if (ret)
> > +			return ret;
> > +
> > +		if (brightness > 0)
> > +			chip->pdata->status = BACKLIGHT_ON;
> > +		else
> > +			chip->pdata->status = BACKLIGHT_OFF;
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +static const struct backlight_ops mp3309c_bl_ops = {
> > +	.update_status = mp3309c_bl_update_status, };
> > +
> > +static int pm3309c_parse_dt_node(struct mp3309c_chip *chip,
> > +				 struct mp3309c_platform_data *pdata) {
> > +	struct device_node *node = chip->dev->of_node;
> > +	struct property *prop_pwms, *prop_levels;
> > +	int length = 0;
> > +	int ret, i;
> > +	unsigned int num_levels, tmp_value;
> > +
> > +	if (!node) {
> > +		dev_err(chip->dev, "failed to get DT node\n");
> > +		return -ENODEV;
> > +	}
> > +
> > +	/*
> > +	 * Dimming mode: the MP3309C provides two dimming control mode:
> > +	 *
> > +	 * - PWM mode
> > +	 * - Analog by I2C control mode (default)
> > +	 *
> > +	 * I2C control mode is assumed as default but, if the pwms property is
> > +	 * found in the backlight node, the mode switches to PWM mode.
> > +	 */
> > +	pdata->dimming_mode = DIMMING_ANALOG_I2C;
> > +	prop_pwms = of_find_property(node, "pwms", &length);
> > +	if (prop_pwms) {
> > +		chip->pwmd = devm_pwm_get(chip->dev, NULL);
> > +		if (IS_ERR(chip->pwmd))
> > +			return dev_err_probe(chip->dev, PTR_ERR(chip-
> >pwmd),
> > +					     "error getting pwm data\n");
> > +		pdata->dimming_mode = DIMMING_PWM;
> > +		pwm_apply_args(chip->pwmd);
> > +	}
> > +
> > +	/*
> > +	 * In I2C control mode the dimming levels (0..31) are fixed by the
> > +	 * hardware, while in PWM control mode they can be chosen by the
> user,
> > +	 * to allow nonlinear mappings.
> > +	 */
> > +	if  (pdata->dimming_mode == DIMMING_ANALOG_I2C) {
> > +		/*
> > +		 * Analog (by I2C commands) control mode: fixed 0..31
> brightness
> > +		 * levels
> > +		 */
> > +		num_levels = ANALOG_I2C_NUM_LEVELS;
> > +
> > +		/* Enable GPIO used in I2C dimming mode only */
> > +		chip->enable_gpio = devm_gpiod_get(chip->dev, "enable",
> > +						   GPIOD_OUT_HIGH);
> > +		if (IS_ERR(chip->enable_gpio))
> > +			return dev_err_probe(chip->dev,
> > +					     PTR_ERR(chip->enable_gpio),
> > +					     "error getting enable gpio\n");
> > +	} else {
> > +		/*
> > +		 * PWM control mode: check for brightness level in DT
> > +		 */
> > +		prop_levels = of_find_property(node, "brightness-levels",
> > +					       &length);
> > +		if (prop_levels) {
> > +			/* Read brightness levels from DT */
> > +			num_levels = length / sizeof(u32);
> > +			if (num_levels < 2)
> > +				return -EINVAL;
> > +		} else {
> > +			/* Use default brightness levels */
> > +			num_levels =
> MP3309C_PWM_DEFAULT_NUM_LEVELS;
> > +		}
> > +	}
> > +
> > +	/* Fill brightness levels array */
> > +	pdata->levels = devm_kcalloc(chip->dev, num_levels,
> > +				     sizeof(*pdata->levels), GFP_KERNEL);
> > +	if (!pdata->levels)
> > +		return -ENOMEM;
> > +	if (prop_levels) {
> > +		ret = of_property_read_u32_array(node, "brightness-levels",
> > +						 pdata->levels,
> > +						 num_levels);
> > +		if (ret < 0)
> > +			return ret;
> > +	} else {
> > +		for (i = 0; i < num_levels; i++)
> > +			pdata->levels[i] = i;
> > +	}
> > +
> > +	pdata->max_brightness = num_levels - 1;
> > +
> > +	ret = of_property_read_u32(node, "default-brightness",
> > +				   &pdata->default_brightness);
> > +	if (ret)
> > +		pdata->default_brightness = pdata->max_brightness;
> > +	if (pdata->default_brightness > pdata->max_brightness) {
> > +		dev_err(chip->dev,
> > +			"default brightness exceeds max brightness\n");
> > +		pdata->default_brightness = pdata->max_brightness;
> > +	}
> > +
> > +	/*
> > +	 * Over-voltage protection (OVP)
> > +	 *
> > +	 * This (optional) property values are:
> > +	 *
> > +	 *  - 13.5V
> > +	 *  - 24V
> > +	 *  - 35.5V (hardware default setting)
> > +	 *
> > +	 * If missing, the default value for OVP is 35.5V
> > +	 */
> > +	pdata->over_voltage_protection = REG_I2C_1_OVP1;
> > +	if (!of_property_read_u32(node, "mps,overvoltage-protection-
> microvolt",
> > +				  &tmp_value)) {
> > +		switch (tmp_value) {
> > +		case 13500000:
> > +			pdata->over_voltage_protection = 0x00;
> > +			break;
> > +		case 24000000:
> > +			pdata->over_voltage_protection = REG_I2C_1_OVP0;
> > +			break;
> > +		case 35500000:
> > +			pdata->over_voltage_protection = REG_I2C_1_OVP1;
> > +			break;
> > +		default:
> > +			return -EINVAL;
> > +		}
> > +	}
> > +
> > +	/* Synchronous (default) and non-synchronous mode */
> > +	pdata->sync_mode = true;
> > +	if (of_property_read_bool(node, "mps,no-sync-mode"))
> > +		pdata->sync_mode = false;
> > +
> > +	return 0;
> > +}
> > +
> > +static int mp3309c_probe(struct i2c_client *client) {
> > +	struct mp3309c_platform_data *pdata = dev_get_platdata(&client-
> >dev);
> > +	struct mp3309c_chip *chip;
> > +	struct backlight_properties props;
> > +	struct pwm_state pwmstate;
> > +	int ret;
> > +
> > +	if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
> > +		dev_err(&client->dev, "failed to check i2c functionality\n");
> > +		return -EOPNOTSUPP;
> > +	}
> > +
> > +	chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL);
> > +	if (!chip)
> > +		return -ENOMEM;
> > +
> > +	chip->dev = &client->dev;
> > +
> > +	chip->regmap = devm_regmap_init_i2c(client, &mp3309c_regmap);
> > +	if (IS_ERR(chip->regmap))
> > +		return dev_err_probe(&client->dev, PTR_ERR(chip->regmap),
> > +				     "failed to allocate register map\n");
> > +
> > +	i2c_set_clientdata(client, chip);
> > +
> > +	if (!pdata) {
> > +		pdata = devm_kzalloc(chip->dev, sizeof(*pdata),
> GFP_KERNEL);
> > +		if (!pdata)
> > +			return -ENOMEM;
> > +
> > +		ret = pm3309c_parse_dt_node(chip, pdata);
> > +		if (ret)
> > +			return ret;
> > +	}
> > +	chip->pdata = pdata;
> > +
> > +	/* Backlight properties */
> > +	props.brightness = pdata->default_brightness;
> > +	props.max_brightness = pdata->max_brightness;
> > +	props.scale = BACKLIGHT_SCALE_LINEAR;
> > +	props.type = BACKLIGHT_RAW;
> > +	props.power = FB_BLANK_UNBLANK;
> > +	props.fb_blank = FB_BLANK_UNBLANK;
> > +	chip->bl = devm_backlight_device_register(chip->dev, "mp3309c",
> > +						  chip->dev, chip,
> > +						  &mp3309c_bl_ops, &props);
> > +	if (IS_ERR(chip->bl))
> > +		return dev_err_probe(chip->dev, PTR_ERR(chip->bl),
> > +				     "error registering backlight device\n");
> > +
> > +	/* In PWM dimming mode, enable pwm device */
> > +	if (chip->pdata->dimming_mode == DIMMING_PWM) {
> > +		pwm_init_state(chip->pwmd, &pwmstate);
> > +		pwm_set_relative_duty_cycle(&pwmstate,
> > +					    chip->pdata->default_brightness,
> > +					    chip->pdata->max_brightness);
> > +		pwmstate.enabled = true;
> > +		ret = pwm_apply_state(chip->pwmd, &pwmstate);
> > +		if (ret)
> > +			return dev_err_probe(chip->dev, ret,
> > +					     "error setting pwm device\n");
> > +	}
> > +
> > +	chip->pdata->status = FIRST_POWER_ON;
> > +	backlight_update_status(chip->bl);
> > +
> > +	return 0;
> > +}
> > +
> > +static void mp3309c_remove(struct i2c_client *client) {
> > +	struct mp3309c_chip *chip = i2c_get_clientdata(client);
> > +	struct backlight_device *bl = chip->bl;
> > +
> > +	bl->props.power = FB_BLANK_POWERDOWN;
> > +	bl->props.brightness = 0;
> > +	backlight_update_status(chip->bl);
> > +}
> > +
> > +static const struct of_device_id mp3309c_match_table[] = {
> > +	{ .compatible = "mps,mp3309c", },
> > +	{ },
> > +};
> > +MODULE_DEVICE_TABLE(of, mp3309c_match_table);
> > +
> > +static const struct i2c_device_id mp3309c_id[] = {
> > +	{ "mp3309c", 0 },
> > +	{ }
> > +};
> > +MODULE_DEVICE_TABLE(i2c, mp3309c_id);
> > +
> > +static struct i2c_driver mp3309c_i2c_driver = {
> > +	.driver	= {
> > +			.name		= KBUILD_MODNAME,
> > +			.of_match_table	= mp3309c_match_table,
> > +	},
> > +	.probe		= mp3309c_probe,
> > +	.remove		= mp3309c_remove,
> > +	.id_table	= mp3309c_id,
> > +};
> > +
> > +module_i2c_driver(mp3309c_i2c_driver);
> > +
> > +MODULE_DESCRIPTION("Backlight Driver for MPS MP3309C");
> > +MODULE_AUTHOR("Flavio Suligoi <f.suligoi@asem.it>");
> > +MODULE_LICENSE("GPL");
> > --
> > 2.34.1
> >
diff mbox series

Patch

diff --git a/MAINTAINERS b/MAINTAINERS
index 90f13281d297..3d23b869e2aa 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14474,6 +14474,13 @@  S:	Maintained
 F:	Documentation/driver-api/tty/moxa-smartio.rst
 F:	drivers/tty/mxser.*
 
+MP3309C BACKLIGHT DRIVER
+M:	Flavio Suligoi <f.suligoi@asem.it>
+L:	dri-devel@lists.freedesktop.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/leds/backlight/mps,mp3309c.yaml
+F:	drivers/video/backlight/mp3309c.c
+
 MR800 AVERMEDIA USB FM RADIO DRIVER
 M:	Alexey Klimov <klimov.linux@gmail.com>
 L:	linux-media@vger.kernel.org
diff --git a/drivers/video/backlight/Kconfig b/drivers/video/backlight/Kconfig
index 51387b1ef012..1144a54a35c0 100644
--- a/drivers/video/backlight/Kconfig
+++ b/drivers/video/backlight/Kconfig
@@ -402,6 +402,17 @@  config BACKLIGHT_LP8788
 	help
 	  This supports TI LP8788 backlight driver.
 
+config BACKLIGHT_MP3309C
+	tristate "Backlight Driver for MPS MP3309C"
+	depends on I2C && PWM
+	select REGMAP_I2C
+	help
+	  This supports MPS MP3309C backlight WLED driver in both PWM and
+	  analog/I2C dimming modes.
+
+	  To compile this driver as a module, choose M here: the module will
+	  be called mp3309c.
+
 config BACKLIGHT_PANDORA
 	tristate "Backlight driver for Pandora console"
 	depends on TWL4030_CORE
diff --git a/drivers/video/backlight/Makefile b/drivers/video/backlight/Makefile
index f72e1c3c59e9..1af583de665b 100644
--- a/drivers/video/backlight/Makefile
+++ b/drivers/video/backlight/Makefile
@@ -44,6 +44,7 @@  obj-$(CONFIG_BACKLIGHT_LP855X)		+= lp855x_bl.o
 obj-$(CONFIG_BACKLIGHT_LP8788)		+= lp8788_bl.o
 obj-$(CONFIG_BACKLIGHT_LV5207LP)	+= lv5207lp.o
 obj-$(CONFIG_BACKLIGHT_MAX8925)		+= max8925_bl.o
+obj-$(CONFIG_BACKLIGHT_MP3309C)		+= mp3309c.o
 obj-$(CONFIG_BACKLIGHT_MT6370)		+= mt6370-backlight.o
 obj-$(CONFIG_BACKLIGHT_OMAP1)		+= omap1_bl.o
 obj-$(CONFIG_BACKLIGHT_PANDORA)		+= pandora_bl.o
diff --git a/drivers/video/backlight/mp3309c.c b/drivers/video/backlight/mp3309c.c
new file mode 100644
index 000000000000..3fe4469ef43f
--- /dev/null
+++ b/drivers/video/backlight/mp3309c.c
@@ -0,0 +1,443 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Driver for MPS MP3309C White LED driver with I2C interface
+ *
+ * This driver support both analog (by I2C commands) and PWM dimming control
+ * modes.
+ *
+ * Copyright (C) 2023 ASEM Srl
+ * Author: Flavio Suligoi <f.suligoi@asem.it>
+ *
+ * Based on pwm_bl.c
+ */
+
+#include <linux/backlight.h>
+#include <linux/delay.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/pwm.h>
+#include <linux/regmap.h>
+
+#define REG_I2C_0	0x00
+#define REG_I2C_1	0x01
+
+#define REG_I2C_0_EN	0x80
+#define REG_I2C_0_D0	0x40
+#define REG_I2C_0_D1	0x20
+#define REG_I2C_0_D2	0x10
+#define REG_I2C_0_D3	0x08
+#define REG_I2C_0_D4	0x04
+#define REG_I2C_0_RSRV1	0x02
+#define REG_I2C_0_RSRV2	0x01
+
+#define REG_I2C_1_RSRV1	0x80
+#define REG_I2C_1_DIMS	0x40
+#define REG_I2C_1_SYNC	0x20
+#define REG_I2C_1_OVP0	0x10
+#define REG_I2C_1_OVP1	0x08
+#define REG_I2C_1_VOS	0x04
+#define REG_I2C_1_LEDO	0x02
+#define REG_I2C_1_OTP	0x01
+
+#define ANALOG_I2C_NUM_LEVELS	32		/* 0..31 */
+#define ANALOG_I2C_REG_MASK	0x7c
+
+#define MP3309C_PWM_DEFAULT_NUM_LEVELS	256	/* 0..255 */
+
+enum mp3309c_status_value {
+	FIRST_POWER_ON,
+	BACKLIGHT_OFF,
+	BACKLIGHT_ON,
+};
+
+enum mp3309c_dimming_mode_value {
+	DIMMING_PWM,
+	DIMMING_ANALOG_I2C,
+};
+
+struct mp3309c_platform_data {
+	unsigned int max_brightness;
+	unsigned int default_brightness;
+	unsigned int *levels;
+	u8  dimming_mode;
+	u8  over_voltage_protection;
+	bool sync_mode;
+	u8 status;
+};
+
+struct mp3309c_chip {
+	struct device *dev;
+	struct mp3309c_platform_data *pdata;
+	struct backlight_device *bl;
+	struct gpio_desc *enable_gpio;
+	struct regmap *regmap;
+	struct pwm_device *pwmd;
+};
+
+static const struct regmap_config mp3309c_regmap = {
+	.name = "mp3309c_regmap",
+	.reg_bits = 8,
+	.reg_stride = 1,
+	.val_bits = 8,
+	.max_register = REG_I2C_1,
+};
+
+static int mp3309c_enable_device(struct mp3309c_chip *chip)
+{
+	u8 reg_val;
+	int ret;
+
+	/* I2C register #0 - Device enable */
+	ret = regmap_update_bits(chip->regmap, REG_I2C_0, REG_I2C_0_EN,
+				 REG_I2C_0_EN);
+	if (ret)
+		return ret;
+
+	/*
+	 * I2C register #1 - Set working mode:
+	 *  - set one of the two dimming mode:
+	 *    - PWM dimming using an external PWM dimming signal
+	 *    - analog dimming using I2C commands
+	 *  - enable/disable synchronous mode
+	 *  - set overvoltage protection (OVP)
+	 */
+	reg_val = 0x00;
+	if (chip->pdata->dimming_mode == DIMMING_PWM)
+		reg_val |= REG_I2C_1_DIMS;
+	if (chip->pdata->sync_mode)
+		reg_val |= REG_I2C_1_SYNC;
+	reg_val |= chip->pdata->over_voltage_protection;
+	ret = regmap_write(chip->regmap, REG_I2C_1, reg_val);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static int mp3309c_bl_update_status(struct backlight_device *bl)
+{
+	struct mp3309c_chip *chip = bl_get_data(bl);
+	int brightness = backlight_get_brightness(bl);
+	struct pwm_state pwmstate;
+	unsigned int analog_val, bits_val;
+	int i, ret;
+
+	if (chip->pdata->dimming_mode == DIMMING_PWM) {
+		/*
+		 * PWM control mode
+		 */
+		pwm_get_state(chip->pwmd, &pwmstate);
+		pwm_set_relative_duty_cycle(&pwmstate,
+					    chip->pdata->levels[brightness],
+					    chip->pdata->levels[chip->pdata->max_brightness]);
+		pwmstate.enabled = true;
+		ret = pwm_apply_state(chip->pwmd, &pwmstate);
+		if (ret)
+			return ret;
+
+		switch (chip->pdata->status) {
+		case FIRST_POWER_ON:
+		case BACKLIGHT_OFF:
+			/*
+			 * After 20ms of low pwm signal level, the chip turns
+			 * off automatically. In this case, before enabling the
+			 * chip again, we must wait about 10ms for pwm signal to
+			 * stabilize.
+			 */
+			if (brightness > 0) {
+				msleep(10);
+				mp3309c_enable_device(chip);
+				chip->pdata->status = BACKLIGHT_ON;
+			} else {
+				chip->pdata->status = BACKLIGHT_OFF;
+			}
+			break;
+		case BACKLIGHT_ON:
+			if (brightness == 0)
+				chip->pdata->status = BACKLIGHT_OFF;
+			break;
+		}
+	} else {
+		/*
+		 * Analog (by I2C command) control mode
+		 *
+		 * The first time, before setting brightness, we must enable the
+		 * device
+		 */
+		if (chip->pdata->status == FIRST_POWER_ON)
+			mp3309c_enable_device(chip);
+
+		/*
+		 * Dimming mode I2C command (fixed dimming range 0..31)
+		 *
+		 * The 5 bits of the dimming analog value D4..D0 is allocated
+		 * in the I2C register #0, in the following way:
+		 *
+		 *     +--+--+--+--+--+--+--+--+
+		 *     |EN|D0|D1|D2|D3|D4|XX|XX|
+		 *     +--+--+--+--+--+--+--+--+
+		 */
+		analog_val = brightness;
+		bits_val = 0;
+		for (i = 0; i <= 5; i++)
+			bits_val += ((analog_val >> i) & 0x01) << (6 - i);
+		ret = regmap_update_bits(chip->regmap, REG_I2C_0,
+					 ANALOG_I2C_REG_MASK, bits_val);
+		if (ret)
+			return ret;
+
+		if (brightness > 0)
+			chip->pdata->status = BACKLIGHT_ON;
+		else
+			chip->pdata->status = BACKLIGHT_OFF;
+	}
+
+	return 0;
+}
+
+static const struct backlight_ops mp3309c_bl_ops = {
+	.update_status = mp3309c_bl_update_status,
+};
+
+static int pm3309c_parse_dt_node(struct mp3309c_chip *chip,
+				 struct mp3309c_platform_data *pdata)
+{
+	struct device_node *node = chip->dev->of_node;
+	struct property *prop_pwms, *prop_levels;
+	int length = 0;
+	int ret, i;
+	unsigned int num_levels, tmp_value;
+
+	if (!node) {
+		dev_err(chip->dev, "failed to get DT node\n");
+		return -ENODEV;
+	}
+
+	/*
+	 * Dimming mode: the MP3309C provides two dimming control mode:
+	 *
+	 * - PWM mode
+	 * - Analog by I2C control mode (default)
+	 *
+	 * I2C control mode is assumed as default but, if the pwms property is
+	 * found in the backlight node, the mode switches to PWM mode.
+	 */
+	pdata->dimming_mode = DIMMING_ANALOG_I2C;
+	prop_pwms = of_find_property(node, "pwms", &length);
+	if (prop_pwms) {
+		chip->pwmd = devm_pwm_get(chip->dev, NULL);
+		if (IS_ERR(chip->pwmd))
+			return dev_err_probe(chip->dev, PTR_ERR(chip->pwmd),
+					     "error getting pwm data\n");
+		pdata->dimming_mode = DIMMING_PWM;
+		pwm_apply_args(chip->pwmd);
+	}
+
+	/*
+	 * In I2C control mode the dimming levels (0..31) are fixed by the
+	 * hardware, while in PWM control mode they can be chosen by the user,
+	 * to allow nonlinear mappings.
+	 */
+	if  (pdata->dimming_mode == DIMMING_ANALOG_I2C) {
+		/*
+		 * Analog (by I2C commands) control mode: fixed 0..31 brightness
+		 * levels
+		 */
+		num_levels = ANALOG_I2C_NUM_LEVELS;
+
+		/* Enable GPIO used in I2C dimming mode only */
+		chip->enable_gpio = devm_gpiod_get(chip->dev, "enable",
+						   GPIOD_OUT_HIGH);
+		if (IS_ERR(chip->enable_gpio))
+			return dev_err_probe(chip->dev,
+					     PTR_ERR(chip->enable_gpio),
+					     "error getting enable gpio\n");
+	} else {
+		/*
+		 * PWM control mode: check for brightness level in DT
+		 */
+		prop_levels = of_find_property(node, "brightness-levels",
+					       &length);
+		if (prop_levels) {
+			/* Read brightness levels from DT */
+			num_levels = length / sizeof(u32);
+			if (num_levels < 2)
+				return -EINVAL;
+		} else {
+			/* Use default brightness levels */
+			num_levels = MP3309C_PWM_DEFAULT_NUM_LEVELS;
+		}
+	}
+
+	/* Fill brightness levels array */
+	pdata->levels = devm_kcalloc(chip->dev, num_levels,
+				     sizeof(*pdata->levels), GFP_KERNEL);
+	if (!pdata->levels)
+		return -ENOMEM;
+	if (prop_levels) {
+		ret = of_property_read_u32_array(node, "brightness-levels",
+						 pdata->levels,
+						 num_levels);
+		if (ret < 0)
+			return ret;
+	} else {
+		for (i = 0; i < num_levels; i++)
+			pdata->levels[i] = i;
+	}
+
+	pdata->max_brightness = num_levels - 1;
+
+	ret = of_property_read_u32(node, "default-brightness",
+				   &pdata->default_brightness);
+	if (ret)
+		pdata->default_brightness = pdata->max_brightness;
+	if (pdata->default_brightness > pdata->max_brightness) {
+		dev_err(chip->dev,
+			"default brightness exceeds max brightness\n");
+		pdata->default_brightness = pdata->max_brightness;
+	}
+
+	/*
+	 * Over-voltage protection (OVP)
+	 *
+	 * This (optional) property values are:
+	 *
+	 *  - 13.5V
+	 *  - 24V
+	 *  - 35.5V (hardware default setting)
+	 *
+	 * If missing, the default value for OVP is 35.5V
+	 */
+	pdata->over_voltage_protection = REG_I2C_1_OVP1;
+	if (!of_property_read_u32(node, "mps,overvoltage-protection-microvolt",
+				  &tmp_value)) {
+		switch (tmp_value) {
+		case 13500000:
+			pdata->over_voltage_protection = 0x00;
+			break;
+		case 24000000:
+			pdata->over_voltage_protection = REG_I2C_1_OVP0;
+			break;
+		case 35500000:
+			pdata->over_voltage_protection = REG_I2C_1_OVP1;
+			break;
+		default:
+			return -EINVAL;
+		}
+	}
+
+	/* Synchronous (default) and non-synchronous mode */
+	pdata->sync_mode = true;
+	if (of_property_read_bool(node, "mps,no-sync-mode"))
+		pdata->sync_mode = false;
+
+	return 0;
+}
+
+static int mp3309c_probe(struct i2c_client *client)
+{
+	struct mp3309c_platform_data *pdata = dev_get_platdata(&client->dev);
+	struct mp3309c_chip *chip;
+	struct backlight_properties props;
+	struct pwm_state pwmstate;
+	int ret;
+
+	if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
+		dev_err(&client->dev, "failed to check i2c functionality\n");
+		return -EOPNOTSUPP;
+	}
+
+	chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL);
+	if (!chip)
+		return -ENOMEM;
+
+	chip->dev = &client->dev;
+
+	chip->regmap = devm_regmap_init_i2c(client, &mp3309c_regmap);
+	if (IS_ERR(chip->regmap))
+		return dev_err_probe(&client->dev, PTR_ERR(chip->regmap),
+				     "failed to allocate register map\n");
+
+	i2c_set_clientdata(client, chip);
+
+	if (!pdata) {
+		pdata = devm_kzalloc(chip->dev, sizeof(*pdata), GFP_KERNEL);
+		if (!pdata)
+			return -ENOMEM;
+
+		ret = pm3309c_parse_dt_node(chip, pdata);
+		if (ret)
+			return ret;
+	}
+	chip->pdata = pdata;
+
+	/* Backlight properties */
+	props.brightness = pdata->default_brightness;
+	props.max_brightness = pdata->max_brightness;
+	props.scale = BACKLIGHT_SCALE_LINEAR;
+	props.type = BACKLIGHT_RAW;
+	props.power = FB_BLANK_UNBLANK;
+	props.fb_blank = FB_BLANK_UNBLANK;
+	chip->bl = devm_backlight_device_register(chip->dev, "mp3309c",
+						  chip->dev, chip,
+						  &mp3309c_bl_ops, &props);
+	if (IS_ERR(chip->bl))
+		return dev_err_probe(chip->dev, PTR_ERR(chip->bl),
+				     "error registering backlight device\n");
+
+	/* In PWM dimming mode, enable pwm device */
+	if (chip->pdata->dimming_mode == DIMMING_PWM) {
+		pwm_init_state(chip->pwmd, &pwmstate);
+		pwm_set_relative_duty_cycle(&pwmstate,
+					    chip->pdata->default_brightness,
+					    chip->pdata->max_brightness);
+		pwmstate.enabled = true;
+		ret = pwm_apply_state(chip->pwmd, &pwmstate);
+		if (ret)
+			return dev_err_probe(chip->dev, ret,
+					     "error setting pwm device\n");
+	}
+
+	chip->pdata->status = FIRST_POWER_ON;
+	backlight_update_status(chip->bl);
+
+	return 0;
+}
+
+static void mp3309c_remove(struct i2c_client *client)
+{
+	struct mp3309c_chip *chip = i2c_get_clientdata(client);
+	struct backlight_device *bl = chip->bl;
+
+	bl->props.power = FB_BLANK_POWERDOWN;
+	bl->props.brightness = 0;
+	backlight_update_status(chip->bl);
+}
+
+static const struct of_device_id mp3309c_match_table[] = {
+	{ .compatible = "mps,mp3309c", },
+	{ },
+};
+MODULE_DEVICE_TABLE(of, mp3309c_match_table);
+
+static const struct i2c_device_id mp3309c_id[] = {
+	{ "mp3309c", 0 },
+	{ }
+};
+MODULE_DEVICE_TABLE(i2c, mp3309c_id);
+
+static struct i2c_driver mp3309c_i2c_driver = {
+	.driver	= {
+			.name		= KBUILD_MODNAME,
+			.of_match_table	= mp3309c_match_table,
+	},
+	.probe		= mp3309c_probe,
+	.remove		= mp3309c_remove,
+	.id_table	= mp3309c_id,
+};
+
+module_i2c_driver(mp3309c_i2c_driver);
+
+MODULE_DESCRIPTION("Backlight Driver for MPS MP3309C");
+MODULE_AUTHOR("Flavio Suligoi <f.suligoi@asem.it>");
+MODULE_LICENSE("GPL");