new file mode 100644
@@ -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 */
+ };
+ };
@@ -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
@@ -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
new file mode 100644
@@ -0,0 +1,578 @@
+// 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 <common.h>
+#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),
+};