| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * TI LP8864/LP8866 4/6 Channel LED Driver |
| * |
| * Copyright (C) 2024 Siemens AG |
| * |
| * Based on LP8860 driver by Dan Murphy <dmurphy@ti.com> |
| */ |
| |
| #include <linux/gpio/consumer.h> |
| #include <linux/i2c.h> |
| #include <linux/init.h> |
| #include <linux/leds.h> |
| #include <linux/module.h> |
| #include <linux/mutex.h> |
| #include <linux/of.h> |
| #include <linux/regmap.h> |
| #include <linux/regulator/consumer.h> |
| #include <linux/slab.h> |
| |
| #define LP8864_BRT_CONTROL 0x00 |
| #define LP8864_USER_CONFIG1 0x04 |
| #define LP8864_BRT_MODE_MASK GENMASK(9, 8) |
| #define LP8864_BRT_MODE_REG BIT(9) /* Brightness control by DISPLAY_BRT reg */ |
| #define LP8864_SUPPLY_STATUS 0x0e |
| #define LP8864_BOOST_STATUS 0x10 |
| #define LP8864_LED_STATUS 0x12 |
| #define LP8864_LED_STATUS_WR_MASK GENMASK(14, 9) /* Writeable bits in the LED_STATUS reg */ |
| |
| /* Textual meaning for status bits, starting from bit 1 */ |
| static const char *const lp8864_supply_status_msg[] = { |
| "Vin under-voltage fault", |
| "Vin over-voltage fault", |
| "Vdd under-voltage fault", |
| "Vin over-current fault", |
| "Missing charge pump fault", |
| "Charge pump fault", |
| "Missing boost sync fault", |
| "CRC error fault ", |
| }; |
| |
| /* Textual meaning for status bits, starting from bit 1 */ |
| static const char *const lp8864_boost_status_msg[] = { |
| "Boost OVP low fault", |
| "Boost OVP high fault", |
| "Boost over-current fault", |
| "Missing boost FSET resistor fault", |
| "Missing MODE SEL resistor fault", |
| "Missing LED resistor fault", |
| "ISET resistor short to ground fault", |
| "Thermal shutdown fault", |
| }; |
| |
| /* Textual meaning for every register bit */ |
| static const char *const lp8864_led_status_msg[] = { |
| "LED 1 fault", |
| "LED 2 fault", |
| "LED 3 fault", |
| "LED 4 fault", |
| "LED 5 fault", |
| "LED 6 fault", |
| "LED open fault", |
| "LED internal short fault", |
| "LED short to GND fault", |
| NULL, NULL, NULL, |
| "Invalid string configuration fault", |
| NULL, |
| "I2C time out fault", |
| }; |
| |
| /** |
| * struct lp8864_led |
| * @client: Pointer to the I2C client |
| * @led_dev: led class device pointer |
| * @regmap: Devices register map |
| * @led_status_mask: Helps to report LED fault only once |
| */ |
| struct lp8864_led { |
| struct i2c_client *client; |
| struct led_classdev led_dev; |
| struct regmap *regmap; |
| u16 led_status_mask; |
| }; |
| |
| static int lp8864_fault_check(struct lp8864_led *led) |
| { |
| int ret, i; |
| unsigned int val; |
| |
| ret = regmap_read(led->regmap, LP8864_SUPPLY_STATUS, &val); |
| if (ret) |
| goto err; |
| |
| /* Odd bits are status bits, even bits are clear bits */ |
| for (i = 0; i < ARRAY_SIZE(lp8864_supply_status_msg); i++) |
| if (val & BIT(i * 2 + 1)) |
| dev_warn(&led->client->dev, "%s\n", lp8864_supply_status_msg[i]); |
| |
| /* |
| * Clear bits have an index preceding the corresponding Status bits; |
| * both have to be written "1" simultaneously to clear the corresponding |
| * Status bit. |
| */ |
| if (val) |
| ret = regmap_write(led->regmap, LP8864_SUPPLY_STATUS, val >> 1 | val); |
| if (ret) |
| goto err; |
| |
| ret = regmap_read(led->regmap, LP8864_BOOST_STATUS, &val); |
| if (ret) |
| goto err; |
| |
| /* Odd bits are status bits, even bits are clear bits */ |
| for (i = 0; i < ARRAY_SIZE(lp8864_boost_status_msg); i++) |
| if (val & BIT(i * 2 + 1)) |
| dev_warn(&led->client->dev, "%s\n", lp8864_boost_status_msg[i]); |
| |
| if (val) |
| ret = regmap_write(led->regmap, LP8864_BOOST_STATUS, val >> 1 | val); |
| if (ret) |
| goto err; |
| |
| ret = regmap_read(led->regmap, LP8864_LED_STATUS, &val); |
| if (ret) |
| goto err; |
| |
| /* |
| * Clear already reported faults that maintain their value until device |
| * power-down |
| */ |
| val &= ~led->led_status_mask; |
| |
| for (i = 0; i < ARRAY_SIZE(lp8864_led_status_msg); i++) |
| if (lp8864_led_status_msg[i] && val & BIT(i)) |
| dev_warn(&led->client->dev, "%s\n", lp8864_led_status_msg[i]); |
| |
| /* |
| * Mark those which maintain their value until device power-down as |
| * "already reported" |
| */ |
| led->led_status_mask |= val & ~LP8864_LED_STATUS_WR_MASK; |
| |
| /* |
| * Only bits 14, 12, 10 have to be cleared here, but others are RO, |
| * we don't care what we write to them. |
| */ |
| if (val & LP8864_LED_STATUS_WR_MASK) |
| ret = regmap_write(led->regmap, LP8864_LED_STATUS, val >> 1 | val); |
| if (ret) |
| goto err; |
| |
| return 0; |
| |
| err: |
| dev_err(&led->client->dev, "Failed to read/clear faults (%pe)\n", ERR_PTR(ret)); |
| |
| return ret; |
| } |
| |
| static int lp8864_brightness_set(struct led_classdev *led_cdev, |
| enum led_brightness brt_val) |
| { |
| struct lp8864_led *led = container_of(led_cdev, struct lp8864_led, led_dev); |
| /* Scale 0..LED_FULL into 16-bit HW brightness */ |
| unsigned int val = brt_val * 0xffff / LED_FULL; |
| int ret; |
| |
| ret = lp8864_fault_check(led); |
| if (ret) |
| return ret; |
| |
| ret = regmap_write(led->regmap, LP8864_BRT_CONTROL, val); |
| if (ret) |
| dev_err(&led->client->dev, "Failed to write brightness value\n"); |
| |
| return ret; |
| } |
| |
| static enum led_brightness lp8864_brightness_get(struct led_classdev *led_cdev) |
| { |
| struct lp8864_led *led = container_of(led_cdev, struct lp8864_led, led_dev); |
| unsigned int val; |
| int ret; |
| |
| ret = regmap_read(led->regmap, LP8864_BRT_CONTROL, &val); |
| if (ret) { |
| dev_err(&led->client->dev, "Failed to read brightness value\n"); |
| return ret; |
| } |
| |
| /* Scale 16-bit HW brightness into 0..LED_FULL */ |
| return val * LED_FULL / 0xffff; |
| } |
| |
| static const struct regmap_config lp8864_regmap_config = { |
| .reg_bits = 8, |
| .val_bits = 16, |
| .val_format_endian = REGMAP_ENDIAN_LITTLE, |
| }; |
| |
| static void lp8864_disable_gpio(void *data) |
| { |
| struct gpio_desc *gpio = data; |
| |
| gpiod_set_value(gpio, 0); |
| } |
| |
| static int lp8864_probe(struct i2c_client *client) |
| { |
| int ret; |
| struct lp8864_led *led; |
| struct device_node *np = dev_of_node(&client->dev); |
| struct device_node *child_node; |
| struct led_init_data init_data = {}; |
| struct gpio_desc *enable_gpio; |
| |
| led = devm_kzalloc(&client->dev, sizeof(*led), GFP_KERNEL); |
| if (!led) |
| return -ENOMEM; |
| |
| child_node = of_get_next_available_child(np, NULL); |
| if (!child_node) { |
| dev_err(&client->dev, "No LED function defined\n"); |
| return -EINVAL; |
| } |
| |
| ret = devm_regulator_get_enable_optional(&client->dev, "vled"); |
| if (ret && ret != -ENODEV) |
| return dev_err_probe(&client->dev, ret, "Failed to enable vled regulator\n"); |
| |
| enable_gpio = devm_gpiod_get_optional(&client->dev, "enable", GPIOD_OUT_HIGH); |
| if (IS_ERR(enable_gpio)) |
| return dev_err_probe(&client->dev, PTR_ERR(enable_gpio), |
| "Failed to get enable GPIO\n"); |
| |
| ret = devm_add_action_or_reset(&client->dev, lp8864_disable_gpio, enable_gpio); |
| if (ret) |
| return ret; |
| |
| led->client = client; |
| led->led_dev.brightness_set_blocking = lp8864_brightness_set; |
| led->led_dev.brightness_get = lp8864_brightness_get; |
| |
| led->regmap = devm_regmap_init_i2c(client, &lp8864_regmap_config); |
| if (IS_ERR(led->regmap)) |
| return dev_err_probe(&client->dev, PTR_ERR(led->regmap), |
| "Failed to allocate regmap\n"); |
| |
| /* Control brightness by DISPLAY_BRT register */ |
| ret = regmap_update_bits(led->regmap, LP8864_USER_CONFIG1, LP8864_BRT_MODE_MASK, |
| LP8864_BRT_MODE_REG); |
| if (ret) { |
| dev_err(&led->client->dev, "Failed to set brightness control mode\n"); |
| return ret; |
| } |
| |
| ret = lp8864_fault_check(led); |
| if (ret) |
| return ret; |
| |
| init_data.fwnode = of_fwnode_handle(child_node); |
| init_data.devicename = "lp8864"; |
| init_data.default_label = ":display_cluster"; |
| |
| ret = devm_led_classdev_register_ext(&client->dev, &led->led_dev, &init_data); |
| if (ret) |
| dev_err(&client->dev, "Failed to register LED device (%pe)\n", ERR_PTR(ret)); |
| |
| return ret; |
| } |
| |
| static const struct i2c_device_id lp8864_id[] = { |
| { "lp8864" }, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(i2c, lp8864_id); |
| |
| static const struct of_device_id of_lp8864_leds_match[] = { |
| { .compatible = "ti,lp8864" }, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(of, of_lp8864_leds_match); |
| |
| static struct i2c_driver lp8864_driver = { |
| .driver = { |
| .name = "lp8864", |
| .of_match_table = of_lp8864_leds_match, |
| }, |
| .probe = lp8864_probe, |
| .id_table = lp8864_id, |
| }; |
| module_i2c_driver(lp8864_driver); |
| |
| MODULE_DESCRIPTION("Texas Instruments LP8864/LP8866 LED driver"); |
| MODULE_AUTHOR("Alexander Sverdlin <alexander.sverdlin@siemens.com>"); |
| MODULE_LICENSE("GPL"); |