diff options
author | Martin Kurbanov <mmkurbanov@sberdevices.ru> | 2023-05-19 16:04:03 +0300 |
---|---|---|
committer | Lee Jones <lee@kernel.org> | 2023-06-01 19:46:35 +0100 |
commit | 36a87f371b7a1b69584a40c873c0b62dc87d3f80 (patch) | |
tree | c6e7bc8bda154561124289279998a9f16d402ec3 /drivers/leds | |
parent | 8f38f8fa7261819eb7d4fb369dc3bfab72259033 (diff) |
leds: Add AW20xx driver
This commit adds support for AWINIC AW20036/AW20054/AW20072 LED driver.
This driver supports following AW200XX features:
- Individual 64-level DIM currents
Signed-off-by: Martin Kurbanov <mmkurbanov@sberdevices.ru>
Reviewed-by: Andy Shevchenko <andy.shevchenko@gmail.com>
Link: https://lore.kernel.org/r/20230519130403.212479-3-mmkurbanov@sberdevices.ru
Signed-off-by: Lee Jones <lee@kernel.org>
Diffstat (limited to 'drivers/leds')
-rw-r--r-- | drivers/leds/Kconfig | 13 | ||||
-rw-r--r-- | drivers/leds/Makefile | 1 | ||||
-rw-r--r-- | drivers/leds/leds-aw200xx.c | 594 |
3 files changed, 608 insertions, 0 deletions
diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig index 83c3cff9fa08..6046dfeca16f 100644 --- a/drivers/leds/Kconfig +++ b/drivers/leds/Kconfig @@ -94,6 +94,19 @@ config LEDS_ARIEL Say Y to if your machine is a Dell Wyse 3020 thin client. +config LEDS_AW200XX + tristate "LED support for Awinic AW20036/AW20054/AW20072" + depends on LEDS_CLASS + depends on I2C + help + This option enables support for the AW20036/AW20054/AW20072 LED driver. + It is a 3x12/6x9/6x12 matrix LED driver programmed via + an I2C interface, up to 36/54/72 LEDs or 12/18/24 RGBs, + 3 pattern controllers for auto breathing or group dimming control. + + To compile this driver as a module, choose M here: the module + will be called leds-aw200xx. + config LEDS_AW2013 tristate "LED support for Awinic AW2013" depends on LEDS_CLASS && I2C && OF diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile index b0db0980508f..df6bf408212c 100644 --- a/drivers/leds/Makefile +++ b/drivers/leds/Makefile @@ -14,6 +14,7 @@ obj-$(CONFIG_LEDS_ADP5520) += leds-adp5520.o obj-$(CONFIG_LEDS_AN30259A) += leds-an30259a.o obj-$(CONFIG_LEDS_APU) += leds-apu.o obj-$(CONFIG_LEDS_ARIEL) += leds-ariel.o +obj-$(CONFIG_LEDS_W200XX) += leds-aw200xx.o obj-$(CONFIG_LEDS_AW2013) += leds-aw2013.o obj-$(CONFIG_LEDS_BCM6328) += leds-bcm6328.o obj-$(CONFIG_LEDS_BCM6358) += leds-bcm6358.o diff --git a/drivers/leds/leds-aw200xx.c b/drivers/leds/leds-aw200xx.c new file mode 100644 index 000000000000..96979b8e09b7 --- /dev/null +++ b/drivers/leds/leds-aw200xx.c @@ -0,0 +1,594 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Awinic AW20036/AW20054/AW20072 LED driver + * + * Copyright (c) 2023, SberDevices. All Rights Reserved. + * + * Author: Martin Kurbanov <mmkurbanov@sberdevices.ru> + */ + +#include <linux/bitfield.h> +#include <linux/bits.h> +#include <linux/container_of.h> +#include <linux/i2c.h> +#include <linux/leds.h> +#include <linux/mod_devicetable.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/regmap.h> +#include <linux/time.h> +#include <linux/units.h> + +#define AW200XX_DIM_MAX (BIT(6) - 1) +#define AW200XX_FADE_MAX (BIT(8) - 1) +#define AW200XX_IMAX_DEFAULT_uA 60000 +#define AW200XX_IMAX_MAX_uA 160000 +#define AW200XX_IMAX_MIN_uA 3300 + +/* Page 0 */ +#define AW200XX_REG_PAGE0_BASE 0xc000 + +/* Select page register */ +#define AW200XX_REG_PAGE 0xF0 +#define AW200XX_PAGE_MASK (GENMASK(7, 6) | GENMASK(2, 0)) +#define AW200XX_PAGE_SHIFT 0 +#define AW200XX_NUM_PAGES 6 +#define AW200XX_PAGE_SIZE 256 +#define AW200XX_REG(page, reg) \ + (AW200XX_REG_PAGE0_BASE + (page) * AW200XX_PAGE_SIZE + (reg)) +#define AW200XX_REG_MAX \ + AW200XX_REG(AW200XX_NUM_PAGES - 1, AW200XX_PAGE_SIZE - 1) +#define AW200XX_PAGE0 0 +#define AW200XX_PAGE1 1 +#define AW200XX_PAGE2 2 +#define AW200XX_PAGE3 3 +#define AW200XX_PAGE4 4 +#define AW200XX_PAGE5 5 + +/* Chip ID register */ +#define AW200XX_REG_IDR AW200XX_REG(AW200XX_PAGE0, 0x00) +#define AW200XX_IDR_CHIPID 0x18 + +/* Sleep mode register */ +#define AW200XX_REG_SLPCR AW200XX_REG(AW200XX_PAGE0, 0x01) +#define AW200XX_SLPCR_ACTIVE 0x00 + +/* Reset register */ +#define AW200XX_REG_RSTR AW200XX_REG(AW200XX_PAGE0, 0x02) +#define AW200XX_RSTR_RESET 0x01 + +/* Global current configuration register */ +#define AW200XX_REG_GCCR AW200XX_REG(AW200XX_PAGE0, 0x03) +#define AW200XX_GCCR_IMAX_MASK GENMASK(7, 4) +#define AW200XX_GCCR_IMAX(x) ((x) << 4) +#define AW200XX_GCCR_ALLON BIT(3) + +/* Fast clear display control register */ +#define AW200XX_REG_FCD AW200XX_REG(AW200XX_PAGE0, 0x04) +#define AW200XX_FCD_CLEAR 0x01 + +/* Display size configuration */ +#define AW200XX_REG_DSIZE AW200XX_REG(AW200XX_PAGE0, 0x80) +#define AW200XX_DSIZE_COLUMNS_MAX 12 + +#define AW200XX_LED2REG(x, columns) \ + ((x) + (((x) / (columns)) * (AW200XX_DSIZE_COLUMNS_MAX - (columns)))) + +/* + * DIM current configuration register (page 4). + * The even address for current DIM configuration. + * The odd address for current FADE configuration + */ +#define AW200XX_REG_DIM(x, columns) \ + AW200XX_REG(AW200XX_PAGE4, AW200XX_LED2REG(x, columns) * 2) +#define AW200XX_REG_DIM2FADE(x) ((x) + 1) + +/* + * Duty ratio of display scan (see p.15 of datasheet for formula): + * duty = (592us / 600.5us) * (1 / (display_rows + 1)) + * + * Multiply to 1000 (MILLI) to improve the accuracy of calculations. + */ +#define AW200XX_DUTY_RATIO(rows) \ + (((592UL * USEC_PER_SEC) / 600500UL) * (MILLI / (rows)) / MILLI) + +struct aw200xx_chipdef { + u32 channels; + u32 display_size_rows_max; + u32 display_size_columns; +}; + +struct aw200xx_led { + struct led_classdev cdev; + struct aw200xx *chip; + int dim; + u32 num; +}; + +struct aw200xx { + const struct aw200xx_chipdef *cdef; + struct i2c_client *client; + struct regmap *regmap; + struct mutex mutex; + u32 num_leds; + u32 display_rows; + struct aw200xx_led leds[]; +}; + +static ssize_t dim_show(struct device *dev, struct device_attribute *devattr, + char *buf) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct aw200xx_led *led = container_of(cdev, struct aw200xx_led, cdev); + int dim = led->dim; + + if (dim < 0) + return sysfs_emit(buf, "auto\n"); + + return sysfs_emit(buf, "%d\n", dim); +} + +static ssize_t dim_store(struct device *dev, struct device_attribute *devattr, + const char *buf, size_t count) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct aw200xx_led *led = container_of(cdev, struct aw200xx_led, cdev); + struct aw200xx *chip = led->chip; + u32 columns = chip->cdef->display_size_columns; + int dim; + ssize_t ret; + + if (sysfs_streq(buf, "auto")) { + dim = -1; + } else { + ret = kstrtoint(buf, 0, &dim); + if (ret) + return ret; + + if (dim > AW200XX_DIM_MAX) + return -EINVAL; + } + + mutex_lock(&chip->mutex); + + if (dim >= 0) { + ret = regmap_write(chip->regmap, + AW200XX_REG_DIM(led->num, columns), dim); + if (ret) + goto out_unlock; + } + + led->dim = dim; + ret = count; + +out_unlock: + mutex_unlock(&chip->mutex); + return ret; +} +static DEVICE_ATTR_RW(dim); + +static struct attribute *dim_attrs[] = { + &dev_attr_dim.attr, + NULL +}; +ATTRIBUTE_GROUPS(dim); + +static int aw200xx_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct aw200xx_led *led = container_of(cdev, struct aw200xx_led, cdev); + struct aw200xx *chip = led->chip; + int dim; + u32 reg; + int ret; + + mutex_lock(&chip->mutex); + + reg = AW200XX_REG_DIM(led->num, chip->cdef->display_size_columns); + + dim = led->dim; + if (dim < 0) + dim = max_t(int, + brightness / (AW200XX_FADE_MAX / AW200XX_DIM_MAX), + 1); + + ret = regmap_write(chip->regmap, reg, dim); + if (ret) + goto out_unlock; + + ret = regmap_write(chip->regmap, + AW200XX_REG_DIM2FADE(reg), brightness); + +out_unlock: + mutex_unlock(&chip->mutex); + + return ret; +} + +static u32 aw200xx_imax_from_global(const struct aw200xx *const chip, + u32 global_imax_uA) +{ + u64 led_imax_uA; + + /* + * The output current of each LED (see p.14 of datasheet for formula): + * Iled = Imax * (dim / 63) * ((fade + 1) / 256) * duty + * + * The value of duty is determined by the following formula: + * duty = (592us / 600.5us) * (1 / (display_rows + 1)) + * + * Calculated for the maximum values of fade and dim. + * We divide by 1000 because we earlier multiplied by 1000 to improve + * accuracy when calculating the duty. + */ + led_imax_uA = global_imax_uA * AW200XX_DUTY_RATIO(chip->display_rows); + do_div(led_imax_uA, MILLI); + + return led_imax_uA; +} + +static u32 aw200xx_imax_to_global(const struct aw200xx *const chip, + u32 led_imax_uA) +{ + u32 duty = AW200XX_DUTY_RATIO(chip->display_rows); + + /* The output current of each LED (see p.14 of datasheet for formula) */ + return (led_imax_uA * 1000U) / duty; +} + +#define AW200XX_IMAX_MULTIPLIER1 10000 +#define AW200XX_IMAX_MULTIPLIER2 3333 +#define AW200XX_IMAX_BASE_VAL1 0 +#define AW200XX_IMAX_BASE_VAL2 8 + +/* + * The AW200XX has a 4-bit register (GCCR) to configure the global current, + * which ranges from 3.3mA to 160mA. The following table indicates the values + * of the global current, divided into two parts: + * + * +-----------+-----------------+-----------+-----------------+ + * | reg value | global max (mA) | reg value | global max (mA) | + * +-----------+-----------------+-----------+-----------------+ + * | 0 | 10 | 8 | 3.3 | + * | 1 | 20 | 9 | 6.7 | + * | 2 | 30 | 10 | 10 | + * | 3 | 40 | 11 | 13.3 | + * | 4 | 60 | 12 | 20 | + * | 5 | 80 | 13 | 26.7 | + * | 6 | 120 | 14 | 40 | + * | 7 | 160 | 15 | 53.3 | + * +-----------+-----------------+-----------+-----------------+ + * + * The left part with a multiplier of 10, and the right part with a multiplier + * of 3.3. + * So we have two formulas to calculate the global current: + * for the left part of the table: + * imax = coefficient * 10 + * + * for the right part of the table: + * imax = coefficient * 3.3 + * + * The coefficient table consists of the following values: + * 1, 2, 3, 4, 6, 8, 12, 16. + */ +static int aw200xx_set_imax(const struct aw200xx *const chip, + u32 led_imax_uA) +{ + u32 g_imax_uA = aw200xx_imax_to_global(chip, led_imax_uA); + u32 coeff_table[] = {1, 2, 3, 4, 6, 8, 12, 16}; + u32 gccr_imax = UINT_MAX; + u32 cur_imax = 0; + int i; + + for (i = 0; i < ARRAY_SIZE(coeff_table); i++) { + u32 imax; + + /* select closest ones */ + imax = coeff_table[i] * AW200XX_IMAX_MULTIPLIER1; + if (g_imax_uA >= imax && imax > cur_imax) { + cur_imax = imax; + gccr_imax = i + AW200XX_IMAX_BASE_VAL1; + } + + imax = coeff_table[i] * AW200XX_IMAX_MULTIPLIER2; + imax = DIV_ROUND_CLOSEST(imax, 100) * 100; + if (g_imax_uA >= imax && imax > cur_imax) { + cur_imax = imax; + gccr_imax = i + AW200XX_IMAX_BASE_VAL2; + } + } + + if (gccr_imax == UINT_MAX) + return -EINVAL; + + return regmap_update_bits(chip->regmap, AW200XX_REG_GCCR, + AW200XX_GCCR_IMAX_MASK, + AW200XX_GCCR_IMAX(gccr_imax)); +} + +static int aw200xx_chip_reset(const struct aw200xx *const chip) +{ + int ret; + + ret = regmap_write(chip->regmap, AW200XX_REG_RSTR, AW200XX_RSTR_RESET); + if (ret) + return ret; + + regcache_mark_dirty(chip->regmap); + return regmap_write(chip->regmap, AW200XX_REG_FCD, AW200XX_FCD_CLEAR); +} + +static int aw200xx_chip_init(const struct aw200xx *const chip) +{ + int ret; + + ret = regmap_write(chip->regmap, AW200XX_REG_DSIZE, + chip->display_rows - 1); + if (ret) + return ret; + + ret = regmap_write(chip->regmap, AW200XX_REG_SLPCR, + AW200XX_SLPCR_ACTIVE); + if (ret) + return ret; + + return regmap_update_bits(chip->regmap, AW200XX_REG_GCCR, + AW200XX_GCCR_ALLON, AW200XX_GCCR_ALLON); +} + +static int aw200xx_chip_check(const struct aw200xx *const chip) +{ + struct device *dev = &chip->client->dev; + u32 chipid; + int ret; + + ret = regmap_read(chip->regmap, AW200XX_REG_IDR, &chipid); + if (ret) + return dev_err_probe(dev, ret, "Failed to read chip ID\n"); + + if (chipid != AW200XX_IDR_CHIPID) + return dev_err_probe(dev, -ENODEV, + "Chip reported wrong ID: %x\n", chipid); + + return 0; +} + +static int aw200xx_probe_fw(struct device *dev, struct aw200xx *chip) +{ + struct fwnode_handle *child; + u32 current_min, current_max, min_uA; + int ret; + int i; + + ret = device_property_read_u32(dev, "awinic,display-rows", + &chip->display_rows); + if (ret) + return dev_err_probe(dev, ret, + "Failed to read 'display-rows' property\n"); + + if (!chip->display_rows || + chip->display_rows > chip->cdef->display_size_rows_max) { + return dev_err_probe(dev, ret, + "Invalid leds display size %u\n", + chip->display_rows); + } + + current_max = aw200xx_imax_from_global(chip, AW200XX_IMAX_MAX_uA); + current_min = aw200xx_imax_from_global(chip, AW200XX_IMAX_MIN_uA); + min_uA = UINT_MAX; + i = 0; + + device_for_each_child_node(dev, child) { + struct led_init_data init_data = {}; + struct aw200xx_led *led; + u32 source, imax; + + ret = fwnode_property_read_u32(child, "reg", &source); + if (ret) { + dev_err(dev, "Missing reg property\n"); + chip->num_leds--; + continue; + } + + if (source >= chip->cdef->channels) { + dev_err(dev, "LED reg %u out of range (max %u)\n", + source, chip->cdef->channels); + chip->num_leds--; + continue; + } + + ret = fwnode_property_read_u32(child, "led-max-microamp", + &imax); + if (ret) { + dev_info(&chip->client->dev, + "DT property led-max-microamp is missing\n"); + } else if (imax < current_min || imax > current_max) { + dev_err(dev, "Invalid value %u for led-max-microamp\n", + imax); + chip->num_leds--; + continue; + } else { + min_uA = min(min_uA, imax); + } + + led = &chip->leds[i]; + led->dim = -1; + led->num = source; + led->chip = chip; + led->cdev.brightness_set_blocking = aw200xx_brightness_set; + led->cdev.groups = dim_groups; + init_data.fwnode = child; + + ret = devm_led_classdev_register_ext(dev, &led->cdev, + &init_data); + if (ret) { + fwnode_handle_put(child); + break; + } + + i++; + } + + if (!chip->num_leds) + return -EINVAL; + + if (min_uA == UINT_MAX) { + min_uA = aw200xx_imax_from_global(chip, + AW200XX_IMAX_DEFAULT_uA); + } + + return aw200xx_set_imax(chip, min_uA); +} + +static const struct regmap_range_cfg aw200xx_ranges[] = { + { + .name = "aw200xx", + .range_min = 0, + .range_max = AW200XX_REG_MAX, + .selector_reg = AW200XX_REG_PAGE, + .selector_mask = AW200XX_PAGE_MASK, + .selector_shift = AW200XX_PAGE_SHIFT, + .window_start = 0, + .window_len = AW200XX_PAGE_SIZE, + }, +}; + +static const struct regmap_range aw200xx_writeonly_ranges[] = { + regmap_reg_range(AW200XX_REG(AW200XX_PAGE1, 0x00), AW200XX_REG_MAX), +}; + +static const struct regmap_access_table aw200xx_readable_table = { + .no_ranges = aw200xx_writeonly_ranges, + .n_no_ranges = ARRAY_SIZE(aw200xx_writeonly_ranges), +}; + +static const struct regmap_range aw200xx_readonly_ranges[] = { + regmap_reg_range(AW200XX_REG_IDR, AW200XX_REG_IDR), +}; + +static const struct regmap_access_table aw200xx_writeable_table = { + .no_ranges = aw200xx_readonly_ranges, + .n_no_ranges = ARRAY_SIZE(aw200xx_readonly_ranges), +}; + +static const struct regmap_config aw200xx_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = AW200XX_REG_MAX, + .ranges = aw200xx_ranges, + .num_ranges = ARRAY_SIZE(aw200xx_ranges), + .rd_table = &aw200xx_readable_table, + .wr_table = &aw200xx_writeable_table, + .cache_type = REGCACHE_RBTREE, +}; + +static int aw200xx_probe(struct i2c_client *client) +{ + const struct aw200xx_chipdef *cdef; + struct aw200xx *chip; + int count; + int ret; + + cdef = device_get_match_data(&client->dev); + if (!cdef) + return -ENODEV; + + count = device_get_child_node_count(&client->dev); + if (!count || count > cdef->channels) + return dev_err_probe(&client->dev, -EINVAL, + "Incorrect number of leds (%d)", count); + + chip = devm_kzalloc(&client->dev, struct_size(chip, leds, count), + GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->cdef = cdef; + chip->num_leds = count; + chip->client = client; + i2c_set_clientdata(client, chip); + + chip->regmap = devm_regmap_init_i2c(client, &aw200xx_regmap_config); + if (IS_ERR(chip->regmap)) + return PTR_ERR(chip->regmap); + + ret = aw200xx_chip_check(chip); + if (ret) + return ret; + + mutex_init(&chip->mutex); + + /* Need a lock now since after call aw200xx_probe_fw, sysfs nodes created */ + mutex_lock(&chip->mutex); + + ret = aw200xx_chip_reset(chip); + if (ret) + goto out_unlock; + + ret = aw200xx_probe_fw(&client->dev, chip); + if (ret) + goto out_unlock; + + ret = aw200xx_chip_init(chip); + +out_unlock: + mutex_unlock(&chip->mutex); + return ret; +} + +static void aw200xx_remove(struct i2c_client *client) +{ + struct aw200xx *chip = i2c_get_clientdata(client); + + aw200xx_chip_reset(chip); + mutex_destroy(&chip->mutex); +} + +static const struct aw200xx_chipdef aw20036_cdef = { + .channels = 36, + .display_size_rows_max = 3, + .display_size_columns = 12, +}; + +static const struct aw200xx_chipdef aw20054_cdef = { + .channels = 54, + .display_size_rows_max = 6, + .display_size_columns = 9, +}; + +static const struct aw200xx_chipdef aw20072_cdef = { + .channels = 72, + .display_size_rows_max = 6, + .display_size_columns = 12, +}; + +static const struct i2c_device_id aw200xx_id[] = { + { "aw20036" }, + { "aw20054" }, + { "aw20072" }, + {} +}; +MODULE_DEVICE_TABLE(i2c, aw200xx_id); + +static const struct of_device_id aw200xx_match_table[] = { + { .compatible = "awinic,aw20036", .data = &aw20036_cdef, }, + { .compatible = "awinic,aw20054", .data = &aw20054_cdef, }, + { .compatible = "awinic,aw20072", .data = &aw20072_cdef, }, + {} +}; +MODULE_DEVICE_TABLE(of, aw200xx_match_table); + +static struct i2c_driver aw200xx_driver = { + .driver = { + .name = "aw200xx", + .of_match_table = aw200xx_match_table, + }, + .probe_new = aw200xx_probe, + .remove = aw200xx_remove, + .id_table = aw200xx_id, +}; +module_i2c_driver(aw200xx_driver); + +MODULE_AUTHOR("Martin Kurbanov <mmkurbanov@sberdevices.ru>"); +MODULE_DESCRIPTION("AW200XX LED driver"); +MODULE_LICENSE("GPL"); |