diff options
-rw-r--r-- | MAINTAINERS | 5 | ||||
-rw-r--r-- | doc/device-tree-bindings/leds/leds-lp5562.txt | 63 | ||||
-rw-r--r-- | drivers/led/Kconfig | 8 | ||||
-rw-r--r-- | drivers/led/Makefile | 1 | ||||
-rw-r--r-- | drivers/led/led_lp5562.c | 577 |
5 files changed, 654 insertions, 0 deletions
diff --git a/MAINTAINERS b/MAINTAINERS index 091e6bb5084..936d64c05e0 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1546,6 +1546,11 @@ M: Liviu Dudau <liviu.dudau@foss.arm.com> S: Maintained F: drivers/video/tda19988.c +TI LP5562 LED DRIVER +M: Rasmus Villemoes <rasmus.villemoes@prevas.dk> +S: Supported +F: drivers/led/led_lp5562.c + TI SYSTEM SECURITY M: Andrew F. Davis <afd@ti.com> S: Supported diff --git a/doc/device-tree-bindings/leds/leds-lp5562.txt b/doc/device-tree-bindings/leds/leds-lp5562.txt new file mode 100644 index 00000000000..4e0c742959a --- /dev/null +++ b/doc/device-tree-bindings/leds/leds-lp5562.txt @@ -0,0 +1,63 @@ +LEDs connected to TI LP5562 controller + +This driver works with a TI LP5562 4-channel LED controller. +CONFIG_LED_BLINK is supported using the controller engines. However +there are only 3 engines available for the 4 channels. This means +that the blue and white channels share the same engine. When both +blue and white LEDs are set to blink, they will share the same blink +rate. Changing the blink rate of the blue LED will affect the white +LED and vice-versa. Manual on/off is handled independently for all 4 +channels. + +Required properties: + - compatible : should be "ti,lp5562". + - #address-cells : must be 1. + - #size-cells : must be 0. + - reg : LP5562 LED controller I2C address. + +Optional properties: + - enable-gpios : Enable GPIO + - clock-mode : u8, configures the clock mode: + - 0 # automode + - 1 # internal + - 2 # external + +Each LED is represented as a sub-node of the ti,lp5562 device. + +LED sub-node required properties: + - reg : Zero-based channel identifier: + - 0 red + - 1 green + - 2 blue + - 3 white + +LED sub-node optional properties: + - chan-name : name of LED + - max-cur : LED current at max brightness in 100uA steps (0x00 - 0xFF) + Default : 100 (10 mA) + +Example: + leds0: lp5562@30 { + compatible = "ti,lp5562"; + #address-cells = <1>; + #size-cells = <0>; + enable-gpios = <&gpio3 9 GPIO_ACTIVE_HIGH>; + reg = <0x30>; + clock-mode = /bits/8 <1>; + + led@0 { + reg = <0>; + chan-name = "red"; + max-cur = /bits/ 8 <200>; /* 20mA */ + }; + led@1 { + reg = <1>; + chan-name = "green"; + max-cur = /bits/ 8 <200>; /* 20mA */ + }; + led@2 { + reg = <2>; + chan-name = "blue"; + max-cur = /bits/ 8 <200>; /* 20mA */ + }; + }; diff --git a/drivers/led/Kconfig b/drivers/led/Kconfig index 996b757e6d0..9837960198d 100644 --- a/drivers/led/Kconfig +++ b/drivers/led/Kconfig @@ -49,6 +49,14 @@ config LED_CORTINA This option enables support for LEDs connected to the Cortina Access CAxxxx SOCs. +config LED_LP5562 + bool "LED Support for LP5562" + depends on LED && DM_I2C + help + This option enables support for LEDs connected to the TI LP5562 + 4 channel I2C LED controller. Driver fully supports blink on the + B/G/R LEDs. White LED can blink, but re-uses the period from blue. + config LED_PWM bool "LED PWM" depends on LED && DM_PWM diff --git a/drivers/led/Makefile b/drivers/led/Makefile index 49ae91961d5..2bcb8589087 100644 --- a/drivers/led/Makefile +++ b/drivers/led/Makefile @@ -11,3 +11,4 @@ obj-$(CONFIG_LED_BCM6858) += led_bcm6858.o obj-$(CONFIG_LED_PWM) += led_pwm.o obj-$(CONFIG_$(SPL_)LED_GPIO) += led_gpio.o obj-$(CONFIG_LED_CORTINA) += led_cortina.o +obj-$(CONFIG_LED_LP5562) += led_lp5562.o diff --git a/drivers/led/led_lp5562.c b/drivers/led/led_lp5562.c new file mode 100644 index 00000000000..431d7e10ab7 --- /dev/null +++ b/drivers/led/led_lp5562.c @@ -0,0 +1,577 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright (C) 2018 Doug Zobel <douglas.zobel@climate.com> + * + * Driver for TI lp5562 4 channel LED driver. There are only 3 + * engines available for the 4 LEDs, so white and blue LEDs share + * the same engine. This means that the blink period is shared + * between them. Changing the period of blue blink will affect + * the white period (and vice-versa). Blue and white On/Off + * states remain independent (as would PWM brightness if that's + * ever added to the LED core). + */ + +#include <dm.h> +#include <errno.h> +#include <led.h> +#include <i2c.h> +#include <asm/gpio.h> +#include <linux/delay.h> + +#define DEFAULT_CURRENT 100 /* 10 mA */ +#define MIN_BLINK_PERIOD 32 /* ms */ +#define MAX_BLINK_PERIOD 2248 /* ms */ + +/* Register Map */ +#define REG_ENABLE 0x00 +#define REG_OP_MODE 0x01 +#define REG_B_PWM 0x02 +#define REG_G_PWM 0x03 +#define REG_R_PWM 0x04 +#define REG_B_CUR 0x05 +#define REG_G_CUR 0x06 +#define REG_R_CUR 0x07 +#define REG_CONFIG 0x08 +#define REG_ENG1_PC 0x09 +#define REG_ENG2_PC 0x0A +#define REG_ENG3_PC 0x0B +#define REG_STATUS 0x0C +#define REG_RESET 0x0D +#define REG_W_PWM 0x0E +#define REG_W_CUR 0x0F +#define REG_ENG1_MEM_BEGIN 0x10 +#define REG_ENG2_MEM_BEGIN 0x30 +#define REG_ENG3_MEM_BEGIN 0x50 +#define REG_LED_MAP 0x70 + +/* LED Register Values */ +/* 0x00 ENABLE */ +#define REG_ENABLE_CHIP_ENABLE (0x1 << 6) +#define REG_ENABLE_ENG_EXEC_HOLD 0x0 +#define REG_ENABLE_ENG_EXEC_RUN 0x2 +#define REG_ENABLE_ENG_EXEC_MASK 0x3 + +/* 0x01 OP MODE */ +#define REG_OP_MODE_DISABLED 0x0 +#define REG_OP_MODE_LOAD_SRAM 0x1 +#define REG_OP_MODE_RUN 0x2 +#define REG_OP_MODE_MASK 0x3 + +/* 0x02, 0x03, 0x04, 0x0E PWM */ +#define REG_PWM_MIN_VALUE 0 +#define REG_PWM_MAX_VALUE 0xFF + +/* 0x08 CONFIG */ +#define REG_CONFIG_EXT_CLK 0x0 +#define REG_CONFIG_INT_CLK 0x1 +#define REG_CONFIG_AUTO_CLK 0x2 +#define REG_CONFIG_CLK_MASK 0x3 + +/* 0x0D RESET */ +#define REG_RESET_RESET 0xFF + +/* 0x70 LED MAP */ +#define REG_LED_MAP_ENG_MASK 0x03 +#define REG_LED_MAP_W_ENG_SHIFT 6 +#define REG_LED_MAP_R_ENG_SHIFT 4 +#define REG_LED_MAP_G_ENG_SHIFT 2 +#define REG_LED_MAP_B_ENG_SHIFT 0 + +/* Engine program related */ +#define REG_ENGINE_MEM_SIZE 0x20 +#define LED_PGRM_RAMP_INCREMENT_SHIFT 0 +#define LED_PGRM_RAMP_SIGN_SHIFT 7 +#define LED_PGRM_RAMP_STEP_SHIFT 8 +#define LED_PGRM_RAMP_PRESCALE_SHIFT 14 + +struct lp5562_led_wrap_priv { + struct gpio_desc enable_gpio; +}; + +struct lp5562_led_priv { + u8 reg_pwm; + u8 reg_current; + u8 map_shift; + u8 enginenum; +}; + +/* enum values map to LED_MAP (0x70) values */ +enum lp5562_led_ctl_mode { + I2C = 0x0, +#ifdef CONFIG_LED_BLINK + ENGINE1 = 0x1, + ENGINE2 = 0x2, + ENGINE3 = 0x3 +#endif +}; + +/* + * Update a register value + * dev - I2C udevice (parent of led) + * regnum - register number to update + * value - value to write to register + * mask - mask of bits that should be changed + */ +static int lp5562_led_reg_update(struct udevice *dev, int regnum, + u8 value, u8 mask) +{ + int ret; + + if (mask == 0xFF) + ret = dm_i2c_reg_write(dev, regnum, value); + else + ret = dm_i2c_reg_clrset(dev, regnum, mask, value); + + + /* + * Data sheet says "Delay between consecutive I2C writes to + * ENABLE register (00h) need to be longer than 488 μs + * (typical)." and "Delay between consecutive I2C writes to + * OP_MODE register need to be longer than 153 μs (typ)." + * + * The linux driver does usleep_range(500, 600) and + * usleep_range(200, 300), respectively. + */ + switch (regnum) { + case REG_ENABLE: + udelay(600); + break; + case REG_OP_MODE: + udelay(300); + break; + } + + return ret; +} + +#ifdef CONFIG_LED_BLINK +/* + * Program the lp5562 engine + * dev - I2C udevice (parent of led) + * program - array of commands + * size - number of commands in program array (1-16) + * engine - engine number (1-3) + */ +static int lp5562_led_program_engine(struct udevice *dev, u16 *program, + u8 size, u8 engine) +{ + int ret, cmd; + u8 engine_reg = REG_ENG1_MEM_BEGIN + + ((engine - 1) * REG_ENGINE_MEM_SIZE); + u8 shift = (3 - engine) * 2; + __be16 prog_be[16]; + + if (size < 1 || size > 16 || engine < 1 || engine > 3) + return -EINVAL; + + for (cmd = 0; cmd < size; cmd++) + prog_be[cmd] = cpu_to_be16(program[cmd]); + + /* set engine mode to 'disabled' */ + ret = lp5562_led_reg_update(dev, REG_OP_MODE, + REG_OP_MODE_DISABLED << shift, + REG_OP_MODE_MASK << shift); + if (ret != 0) + goto done; + + /* set exec mode to 'hold' */ + ret = lp5562_led_reg_update(dev, REG_ENABLE, + REG_ENABLE_ENG_EXEC_HOLD << shift, + REG_ENABLE_ENG_EXEC_MASK << shift); + if (ret != 0) + goto done; + + /* set engine mode to 'load SRAM' */ + ret = lp5562_led_reg_update(dev, REG_OP_MODE, + REG_OP_MODE_LOAD_SRAM << shift, + REG_OP_MODE_MASK << shift); + if (ret != 0) + goto done; + + /* send the re-ordered program sequence */ + ret = dm_i2c_write(dev, engine_reg, (uchar *)prog_be, sizeof(u16) * size); + if (ret != 0) + goto done; + + /* set engine mode to 'run' */ + ret = lp5562_led_reg_update(dev, REG_OP_MODE, + REG_OP_MODE_RUN << shift, + REG_OP_MODE_MASK << shift); + if (ret != 0) + goto done; + + /* set engine exec to 'run' */ + ret = lp5562_led_reg_update(dev, REG_ENABLE, + REG_ENABLE_ENG_EXEC_RUN << shift, + REG_ENABLE_ENG_EXEC_MASK << shift); + +done: + return ret; +} + +/* + * Get the LED's current control mode (I2C or ENGINE[1-3]) + * dev - led udevice (child udevice) + */ +static enum lp5562_led_ctl_mode lp5562_led_get_control_mode(struct udevice *dev) +{ + struct lp5562_led_priv *priv = dev_get_priv(dev); + u8 data; + enum lp5562_led_ctl_mode mode = I2C; + + if (dm_i2c_read(dev_get_parent(dev), REG_LED_MAP, &data, 1) == 0) + mode = (data & (REG_LED_MAP_ENG_MASK << priv->map_shift)) + >> priv->map_shift; + + return mode; +} +#endif + +/* + * Set the LED's control mode to I2C or ENGINE[1-3] + * dev - led udevice (child udevice) + * mode - mode to change to + */ +static int lp5562_led_set_control_mode(struct udevice *dev, + enum lp5562_led_ctl_mode mode) +{ + struct lp5562_led_priv *priv = dev_get_priv(dev); + + return (lp5562_led_reg_update(dev_get_parent(dev), REG_LED_MAP, + mode << priv->map_shift, + REG_LED_MAP_ENG_MASK << priv->map_shift)); +} + +/* + * Return the LED's PWM value; If LED is in BLINK state, then it is + * under engine control mode which doesn't use this PWM value. + * dev - led udevice (child udevice) + */ +static int lp5562_led_get_pwm(struct udevice *dev) +{ + struct lp5562_led_priv *priv = dev_get_priv(dev); + u8 data; + + if (dm_i2c_read(dev_get_parent(dev), priv->reg_pwm, &data, 1) != 0) + return -EINVAL; + + return data; +} + +/* + * Set the LED's PWM value and configure it to use this (I2C mode). + * dev - led udevice (child udevice) + * value - PWM value (0 - 255) + */ +static int lp5562_led_set_pwm(struct udevice *dev, u8 value) +{ + struct lp5562_led_priv *priv = dev_get_priv(dev); + + if (lp5562_led_reg_update(dev_get_parent(dev), priv->reg_pwm, + value, 0xff) != 0) + return -EINVAL; + + /* set LED to I2C register mode */ + return lp5562_led_set_control_mode(dev, I2C); +} + +/* + * Return the led's current state + * dev - led udevice (child udevice) + * + */ +static enum led_state_t lp5562_led_get_state(struct udevice *dev) +{ + enum led_state_t state = LEDST_ON; + + if (lp5562_led_get_pwm(dev) == REG_PWM_MIN_VALUE) + state = LEDST_OFF; + +#ifdef CONFIG_LED_BLINK + if (lp5562_led_get_control_mode(dev) != I2C) + state = LEDST_BLINK; +#endif + + return state; +} + +/* + * Set the led state + * dev - led udevice (child udevice) + * state - State to set the LED to + */ +static int lp5562_led_set_state(struct udevice *dev, enum led_state_t state) +{ +#ifdef CONFIG_LED_BLINK + struct lp5562_led_priv *priv = dev_get_priv(dev); +#endif + + switch (state) { + case LEDST_OFF: + return lp5562_led_set_pwm(dev, REG_PWM_MIN_VALUE); + case LEDST_ON: + return lp5562_led_set_pwm(dev, REG_PWM_MAX_VALUE); +#ifdef CONFIG_LED_BLINK + case LEDST_BLINK: + return lp5562_led_set_control_mode(dev, priv->enginenum); +#endif + case LEDST_TOGGLE: + if (lp5562_led_get_state(dev) == LEDST_OFF) + return lp5562_led_set_state(dev, LEDST_ON); + else + return lp5562_led_set_state(dev, LEDST_OFF); + break; + default: + return -EINVAL; + } + + return 0; +} + +#ifdef CONFIG_LED_BLINK +/* + * Set the blink period of an LED; note blue and white share the same + * engine so changing the period of one affects the other. + * dev - led udevice (child udevice) + * period_ms - blink period in ms + */ +static int lp5562_led_set_period(struct udevice *dev, int period_ms) +{ + struct lp5562_led_priv *priv = dev_get_priv(dev); + u8 opcode = 0; + u16 program[7]; + u16 wait_time; + + /* Blink is implemented as an engine program. Simple on/off + * for short periods, or fade in/fade out for longer periods: + * + * if (period_ms < 500): + * set PWM to 100% + * pause for period / 2 + * set PWM to 0% + * pause for period / 2 + * goto start + * + * else + * raise PWM 0% -> 50% in 62.7 ms + * raise PWM 50% -> 100% in 62.7 ms + * pause for (period - 4 * 62.7) / 2 + * lower PWM 100% -> 50% in 62.7 ms + * lower PWM 50% -> 0% in 62.7 ms + * pause for (period - 4 * 62.7) / 2 + * goto start + */ + + if (period_ms < MIN_BLINK_PERIOD) + period_ms = MIN_BLINK_PERIOD; + else if (period_ms > MAX_BLINK_PERIOD) + period_ms = MAX_BLINK_PERIOD; + + if (period_ms < 500) { + /* Simple on/off blink */ + wait_time = period_ms / 2; + + /* 1st command is full brightness */ + program[opcode++] = + (1 << LED_PGRM_RAMP_PRESCALE_SHIFT) | + REG_PWM_MAX_VALUE; + + /* 2nd command is wait (period / 2) using 15.6ms steps */ + program[opcode++] = + (1 << LED_PGRM_RAMP_PRESCALE_SHIFT) | + (((wait_time * 10) / 156) << LED_PGRM_RAMP_STEP_SHIFT) | + (0 << LED_PGRM_RAMP_INCREMENT_SHIFT); + + /* 3rd command is 0% brightness */ + program[opcode++] = + (1 << LED_PGRM_RAMP_PRESCALE_SHIFT); + + /* 4th command is wait (period / 2) using 15.6ms steps */ + program[opcode++] = + (1 << LED_PGRM_RAMP_PRESCALE_SHIFT) | + (((wait_time * 10) / 156) << LED_PGRM_RAMP_STEP_SHIFT) | + (0 << LED_PGRM_RAMP_INCREMENT_SHIFT); + + /* 5th command: repeat */ + program[opcode++] = 0x00; + } else { + /* fade-in / fade-out blink */ + wait_time = ((period_ms - 251) / 2); + + /* ramp up time is 256 * 0.49ms (125.4ms) done in 2 steps */ + /* 1st command is ramp up 1/2 way */ + program[opcode++] = + (0 << LED_PGRM_RAMP_PRESCALE_SHIFT) | + (1 << LED_PGRM_RAMP_STEP_SHIFT) | + (127 << LED_PGRM_RAMP_INCREMENT_SHIFT); + + /* 2nd command is ramp up rest of the way */ + program[opcode++] = + (0 << LED_PGRM_RAMP_PRESCALE_SHIFT) | + (1 << LED_PGRM_RAMP_STEP_SHIFT) | + (127 << LED_PGRM_RAMP_INCREMENT_SHIFT); + + /* 3rd: wait ((period - 2 * ramp_time) / 2) (15.6ms steps) */ + program[opcode++] = + (1 << LED_PGRM_RAMP_PRESCALE_SHIFT) | + (((wait_time * 10) / 156) << LED_PGRM_RAMP_STEP_SHIFT) | + (0 << LED_PGRM_RAMP_INCREMENT_SHIFT); + + /* ramp down is same as ramp up with sign bit set */ + /* 4th command is ramp down 1/2 way */ + program[opcode++] = + (0 << LED_PGRM_RAMP_PRESCALE_SHIFT) | + (1 << LED_PGRM_RAMP_STEP_SHIFT) | + (1 << LED_PGRM_RAMP_SIGN_SHIFT) | + (127 << LED_PGRM_RAMP_INCREMENT_SHIFT); + + /* 5th command is ramp down rest of the way */ + program[opcode++] = + (0 << LED_PGRM_RAMP_PRESCALE_SHIFT) | + (1 << LED_PGRM_RAMP_STEP_SHIFT) | + (1 << LED_PGRM_RAMP_SIGN_SHIFT) | + (127 << LED_PGRM_RAMP_INCREMENT_SHIFT); + + /* 6th: wait ((period - 2 * ramp_time) / 2) (15.6ms steps) */ + program[opcode++] = + (1 << LED_PGRM_RAMP_PRESCALE_SHIFT) | + (((wait_time * 10) / 156) << LED_PGRM_RAMP_STEP_SHIFT) | + (0 << LED_PGRM_RAMP_INCREMENT_SHIFT); + + /* 7th command: repeat */ + program[opcode++] = 0x00; + } + + return lp5562_led_program_engine(dev_get_parent(dev), program, + opcode, priv->enginenum); +} +#endif + +static const struct led_ops lp5562_led_ops = { + .get_state = lp5562_led_get_state, + .set_state = lp5562_led_set_state, +#ifdef CONFIG_LED_BLINK + .set_period = lp5562_led_set_period, +#endif +}; + +static int lp5562_led_probe(struct udevice *dev) +{ + struct lp5562_led_priv *priv = dev_get_priv(dev); + u8 current; + int ret = 0; + + /* Child LED nodes */ + switch (dev_read_addr(dev)) { + case 0: + priv->reg_current = REG_R_CUR; + priv->reg_pwm = REG_R_PWM; + priv->map_shift = REG_LED_MAP_R_ENG_SHIFT; + priv->enginenum = 1; + break; + case 1: + priv->reg_current = REG_G_CUR; + priv->reg_pwm = REG_G_PWM; + priv->map_shift = REG_LED_MAP_G_ENG_SHIFT; + priv->enginenum = 2; + break; + case 2: + priv->reg_current = REG_B_CUR; + priv->reg_pwm = REG_B_PWM; + priv->map_shift = REG_LED_MAP_B_ENG_SHIFT; + priv->enginenum = 3; /* shared with white */ + break; + case 3: + priv->reg_current = REG_W_CUR; + priv->map_shift = REG_LED_MAP_W_ENG_SHIFT; + priv->enginenum = 3; /* shared with blue */ + break; + default: + return -EINVAL; + } + + current = dev_read_u8_default(dev, "max-cur", DEFAULT_CURRENT); + + ret = lp5562_led_reg_update(dev_get_parent(dev), priv->reg_current, + current, 0xff); + + return ret; +} + +static int lp5562_led_bind(struct udevice *dev) +{ + struct led_uc_plat *uc_plat = dev_get_uclass_plat(dev); + + /* + * For the child nodes, parse a "chan-name" property, since + * the DT bindings for this device use that instead of + * "label". + */ + uc_plat->label = dev_read_string(dev, "chan-name"); + + return 0; +} + +U_BOOT_DRIVER(lp5562_led) = { + .name = "lp5562-led", + .id = UCLASS_LED, + .bind = lp5562_led_bind, + .probe = lp5562_led_probe, + .priv_auto = sizeof(struct lp5562_led_priv), + .ops = &lp5562_led_ops, +}; + + +static int lp5562_led_wrap_probe(struct udevice *dev) +{ + struct lp5562_led_wrap_priv *priv = dev_get_priv(dev); + u8 clock_mode; + int ret; + + /* Enable gpio if needed */ + if (gpio_request_by_name(dev, "enabled-gpios", 0, + &priv->enable_gpio, GPIOD_IS_OUT) == 0) { + dm_gpio_set_value(&priv->enable_gpio, 1); + udelay(1000); + } + + /* Ensure all registers have default values. */ + ret = lp5562_led_reg_update(dev, REG_RESET, REG_RESET_RESET, 0xff); + if (ret) + return ret; + udelay(10000); + + /* Enable the chip */ + ret = lp5562_led_reg_update(dev, REG_ENABLE, REG_ENABLE_CHIP_ENABLE, 0xff); + if (ret) + return ret; + + /* + * The DT bindings say 0=auto, 1=internal, 2=external, while + * the register[0:1] values are 0=external, 1=internal, + * 2=auto. + */ + clock_mode = dev_read_u8_default(dev, "clock-mode", 0); + ret = lp5562_led_reg_update(dev, REG_CONFIG, 2 - clock_mode, REG_CONFIG_CLK_MASK); + + return ret; +} + +static int lp5562_led_wrap_bind(struct udevice *dev) +{ + return led_bind_generic(dev, "lp5562-led"); +} + +static const struct udevice_id lp5562_led_ids[] = { + { .compatible = "ti,lp5562" }, + { /* sentinel */ } +}; + +U_BOOT_DRIVER(lp5562_led_wrap) = { + .name = "lp5562-led-wrap", + .id = UCLASS_NOP, + .of_match = lp5562_led_ids, + .bind = lp5562_led_wrap_bind, + .probe = lp5562_led_wrap_probe, + .priv_auto = sizeof(struct lp5562_led_wrap_priv), +}; |