| // 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 us | |
| * (typical)." and "Delay between consecutive I2C writes to | |
| * OP_MODE register need to be longer than 153 us (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), | |
| }; |