| // 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/property.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: | |
| * - enable/disable synchronous mode | |
| * - set overvoltage protection (OVP) | |
| */ | |
| reg_val = 0x00; | |
| 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_might_sleep(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 mp3309c_parse_fwnode(struct mp3309c_chip *chip, | |
| struct mp3309c_platform_data *pdata) | |
| { | |
| int ret, i; | |
| unsigned int tmp_value; | |
| struct device *dev = chip->dev; | |
| int num_levels; | |
| if (!dev_fwnode(dev)) | |
| return dev_err_probe(dev, -ENODEV, "failed to get firmware node\n"); | |
| /* | |
| * 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; | |
| if (device_property_present(dev, "pwms")) { | |
| chip->pwmd = devm_pwm_get(dev, NULL); | |
| if (IS_ERR(chip->pwmd)) | |
| return dev_err_probe(dev, PTR_ERR(chip->pwmd), "error getting pwm data\n"); | |
| pdata->dimming_mode = DIMMING_PWM; | |
| } | |
| /* | |
| * 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(dev, "enable", GPIOD_OUT_HIGH); | |
| if (IS_ERR(chip->enable_gpio)) | |
| return dev_err_probe(dev, PTR_ERR(chip->enable_gpio), | |
| "error getting enable gpio\n"); | |
| } else { | |
| /* | |
| * PWM control mode: check for brightness level in DT | |
| */ | |
| if (device_property_present(dev, "brightness-levels")) { | |
| /* Read brightness levels from DT */ | |
| num_levels = device_property_count_u32(dev, "brightness-levels"); | |
| 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(dev, num_levels, sizeof(*pdata->levels), GFP_KERNEL); | |
| if (!pdata->levels) | |
| return -ENOMEM; | |
| if (device_property_present(dev, "brightness-levels")) { | |
| ret = device_property_read_u32_array(dev, "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 = device_property_read_u32(dev, "default-brightness", &pdata->default_brightness); | |
| if (ret) | |
| pdata->default_brightness = pdata->max_brightness; | |
| if (pdata->default_brightness > pdata->max_brightness) { | |
| dev_err_probe(dev, -ERANGE, "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; | |
| ret = device_property_read_u32(dev, "mps,overvoltage-protection-microvolt", &tmp_value); | |
| if (!ret) { | |
| 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 = !device_property_read_bool(dev, "mps,no-sync-mode"); | |
| return 0; | |
| } | |
| static int mp3309c_probe(struct i2c_client *client) | |
| { | |
| struct device *dev = &client->dev; | |
| struct mp3309c_platform_data *pdata = dev_get_platdata(dev); | |
| struct mp3309c_chip *chip; | |
| struct backlight_properties props; | |
| struct pwm_state pwmstate; | |
| int ret; | |
| if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) | |
| return dev_err_probe(dev, -EOPNOTSUPP, "failed to check i2c functionality\n"); | |
| chip = devm_kzalloc(dev, sizeof(*chip), GFP_KERNEL); | |
| if (!chip) | |
| return -ENOMEM; | |
| chip->dev = dev; | |
| chip->regmap = devm_regmap_init_i2c(client, &mp3309c_regmap); | |
| if (IS_ERR(chip->regmap)) | |
| return dev_err_probe(dev, PTR_ERR(chip->regmap), | |
| "failed to allocate register map\n"); | |
| i2c_set_clientdata(client, chip); | |
| if (!pdata) { | |
| pdata = devm_kzalloc(dev, sizeof(*pdata), GFP_KERNEL); | |
| if (!pdata) | |
| return -ENOMEM; | |
| ret = mp3309c_parse_fwnode(chip, pdata); | |
| if (ret) | |
| return ret; | |
| } | |
| chip->pdata = pdata; | |
| /* Backlight properties */ | |
| props = (typeof(props)){ | |
| .brightness = pdata->default_brightness, | |
| .max_brightness = pdata->max_brightness, | |
| .scale = BACKLIGHT_SCALE_LINEAR, | |
| .type = BACKLIGHT_RAW, | |
| .power = BACKLIGHT_POWER_ON, | |
| }; | |
| chip->bl = devm_backlight_device_register(dev, "mp3309c", dev, chip, | |
| &mp3309c_bl_ops, &props); | |
| if (IS_ERR(chip->bl)) | |
| return dev_err_probe(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_might_sleep(chip->pwmd, &pwmstate); | |
| if (ret) | |
| return dev_err_probe(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 = BACKLIGHT_POWER_OFF; | |
| 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[] = { | |
| { .name = "mp3309c" }, | |
| { } | |
| }; | |
| 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"); |