| // 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[] = { | |
| { .name = "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"); |