Contributors: 1
Author Tokens Token Proportion Commits Commit Proportion
André Apitzsch 2407 100.00% 1 100.00%
Total 2407 1


// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Silergy SY7802 flash LED driver with an I2C interface
 *
 * Copyright 2024 André Apitzsch <git@apitzsch.eu>
 */

#include <linux/gpio/consumer.h>
#include <linux/i2c.h>
#include <linux/kernel.h>
#include <linux/led-class-flash.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/regmap.h>
#include <linux/regulator/consumer.h>

#define SY7802_MAX_LEDS 2
#define SY7802_LED_JOINT 2

#define SY7802_REG_ENABLE		0x10
#define SY7802_REG_TORCH_BRIGHTNESS	0xa0
#define SY7802_REG_FLASH_BRIGHTNESS	0xb0
#define SY7802_REG_FLASH_DURATION	0xc0
#define SY7802_REG_FLAGS		0xd0
#define SY7802_REG_CONFIG_1		0xe0
#define SY7802_REG_CONFIG_2		0xf0
#define SY7802_REG_VIN_MONITOR		0x80
#define SY7802_REG_LAST_FLASH		0x81
#define SY7802_REG_VLED_MONITOR		0x30
#define SY7802_REG_ADC_DELAY		0x31
#define SY7802_REG_DEV_ID		0xff

#define SY7802_MODE_OFF		0
#define SY7802_MODE_TORCH	2
#define SY7802_MODE_FLASH	3
#define SY7802_MODE_MASK	GENMASK(1, 0)

#define SY7802_LEDS_SHIFT	3
#define SY7802_LEDS_MASK(_id)	(BIT(_id) << SY7802_LEDS_SHIFT)
#define SY7802_LEDS_MASK_ALL	(SY7802_LEDS_MASK(0) | SY7802_LEDS_MASK(1))

#define SY7802_TORCH_CURRENT_SHIFT	3
#define SY7802_TORCH_CURRENT_MASK(_id) \
	(GENMASK(2, 0) << (SY7802_TORCH_CURRENT_SHIFT * (_id)))
#define SY7802_TORCH_CURRENT_MASK_ALL \
	(SY7802_TORCH_CURRENT_MASK(0) | SY7802_TORCH_CURRENT_MASK(1))

#define SY7802_FLASH_CURRENT_SHIFT	4
#define SY7802_FLASH_CURRENT_MASK(_id) \
	(GENMASK(3, 0) << (SY7802_FLASH_CURRENT_SHIFT * (_id)))
#define SY7802_FLASH_CURRENT_MASK_ALL \
	(SY7802_FLASH_CURRENT_MASK(0) | SY7802_FLASH_CURRENT_MASK(1))

#define SY7802_TIMEOUT_DEFAULT_US	512000U
#define SY7802_TIMEOUT_MIN_US		32000U
#define SY7802_TIMEOUT_MAX_US		1024000U
#define SY7802_TIMEOUT_STEPSIZE_US	32000U

#define SY7802_TORCH_BRIGHTNESS_MAX 8

#define SY7802_FLASH_BRIGHTNESS_DEFAULT	14
#define SY7802_FLASH_BRIGHTNESS_MIN	0
#define SY7802_FLASH_BRIGHTNESS_MAX	15
#define SY7802_FLASH_BRIGHTNESS_STEP	1

#define SY7802_FLAG_TIMEOUT			BIT(0)
#define SY7802_FLAG_THERMAL_SHUTDOWN		BIT(1)
#define SY7802_FLAG_LED_FAULT			BIT(2)
#define SY7802_FLAG_TX1_INTERRUPT		BIT(3)
#define SY7802_FLAG_TX2_INTERRUPT		BIT(4)
#define SY7802_FLAG_LED_THERMAL_FAULT		BIT(5)
#define SY7802_FLAG_FLASH_INPUT_VOLTAGE_LOW	BIT(6)
#define SY7802_FLAG_INPUT_VOLTAGE_LOW		BIT(7)

#define SY7802_CHIP_ID	0x51

static const struct reg_default sy7802_regmap_defs[] = {
	{ SY7802_REG_ENABLE, SY7802_LEDS_MASK_ALL },
	{ SY7802_REG_TORCH_BRIGHTNESS, 0x92 },
	{ SY7802_REG_FLASH_BRIGHTNESS, SY7802_FLASH_BRIGHTNESS_DEFAULT |
		SY7802_FLASH_BRIGHTNESS_DEFAULT << SY7802_FLASH_CURRENT_SHIFT },
	{ SY7802_REG_FLASH_DURATION, 0x6f },
	{ SY7802_REG_FLAGS, 0x0 },
	{ SY7802_REG_CONFIG_1, 0x68 },
	{ SY7802_REG_CONFIG_2, 0xf0 },
};

struct sy7802_led {
	struct led_classdev_flash flash;
	struct sy7802 *chip;
	u8 led_id;
};

struct sy7802 {
	struct device *dev;
	struct regmap *regmap;
	struct mutex mutex;

	struct gpio_desc *enable_gpio;
	struct regulator *vin_regulator;

	unsigned int fled_strobe_used;
	unsigned int fled_torch_used;
	unsigned int leds_active;
	int num_leds;
	struct sy7802_led leds[] __counted_by(num_leds);
};

static int sy7802_torch_brightness_set(struct led_classdev *lcdev, enum led_brightness brightness)
{
	struct sy7802_led *led = container_of(lcdev, struct sy7802_led, flash.led_cdev);
	struct sy7802 *chip = led->chip;
	u32 fled_torch_used_tmp;
	u32 led_enable_mask;
	u32 enable_mask;
	u32 torch_mask;
	u32 val;
	int ret;

	mutex_lock(&chip->mutex);

	if (chip->fled_strobe_used) {
		dev_warn(chip->dev, "Cannot set torch brightness whilst strobe is enabled\n");
		ret = -EBUSY;
		goto unlock;
	}

	if (brightness)
		fled_torch_used_tmp = chip->fled_torch_used | BIT(led->led_id);
	else
		fled_torch_used_tmp = chip->fled_torch_used & ~BIT(led->led_id);

	led_enable_mask = led->led_id == SY7802_LED_JOINT ?
			  SY7802_LEDS_MASK_ALL :
			  SY7802_LEDS_MASK(led->led_id);

	val = brightness ? led_enable_mask : SY7802_MODE_OFF;
	if (fled_torch_used_tmp)
		val |= SY7802_MODE_TORCH;

	/* Disable torch to apply brightness */
	ret = regmap_update_bits(chip->regmap, SY7802_REG_ENABLE, SY7802_MODE_MASK,
				 SY7802_MODE_OFF);
	if (ret)
		goto unlock;

	torch_mask = led->led_id == SY7802_LED_JOINT ?
		     SY7802_TORCH_CURRENT_MASK_ALL :
		     SY7802_TORCH_CURRENT_MASK(led->led_id);

	/* Register expects brightness between 0 and MAX_BRIGHTNESS - 1 */
	if (brightness)
		brightness -= 1;

	brightness |= (brightness << SY7802_TORCH_CURRENT_SHIFT);

	ret = regmap_update_bits(chip->regmap, SY7802_REG_TORCH_BRIGHTNESS, torch_mask, brightness);
	if (ret)
		goto unlock;

	enable_mask = SY7802_MODE_MASK | led_enable_mask;
	ret = regmap_update_bits(chip->regmap, SY7802_REG_ENABLE, enable_mask, val);
	if (ret)
		goto unlock;

	chip->fled_torch_used = fled_torch_used_tmp;

unlock:
	mutex_unlock(&chip->mutex);
	return ret;
}

static int sy7802_flash_brightness_set(struct led_classdev_flash *fl_cdev, u32 brightness)
{
	struct sy7802_led *led = container_of(fl_cdev, struct sy7802_led, flash);
	struct led_flash_setting *s = &fl_cdev->brightness;
	u32 val = (brightness - s->min) / s->step;
	struct sy7802 *chip = led->chip;
	u32 flash_mask;
	int ret;

	val |= (val << SY7802_FLASH_CURRENT_SHIFT);
	flash_mask = led->led_id == SY7802_LED_JOINT ?
		     SY7802_FLASH_CURRENT_MASK_ALL :
		     SY7802_FLASH_CURRENT_MASK(led->led_id);

	mutex_lock(&chip->mutex);
	ret = regmap_update_bits(chip->regmap, SY7802_REG_FLASH_BRIGHTNESS, flash_mask, val);
	mutex_unlock(&chip->mutex);

	return ret;
}

static int sy7802_strobe_set(struct led_classdev_flash *fl_cdev, bool state)
{
	struct sy7802_led *led = container_of(fl_cdev, struct sy7802_led, flash);
	struct sy7802 *chip = led->chip;
	u32 fled_strobe_used_tmp;
	u32 led_enable_mask;
	u32 enable_mask;
	u32 val;
	int ret;

	mutex_lock(&chip->mutex);

	if (chip->fled_torch_used) {
		dev_warn(chip->dev, "Cannot set strobe brightness whilst torch is enabled\n");
		ret = -EBUSY;
		goto unlock;
	}

	if (state)
		fled_strobe_used_tmp = chip->fled_strobe_used | BIT(led->led_id);
	else
		fled_strobe_used_tmp = chip->fled_strobe_used & ~BIT(led->led_id);

	led_enable_mask = led->led_id == SY7802_LED_JOINT ?
			  SY7802_LEDS_MASK_ALL :
			  SY7802_LEDS_MASK(led->led_id);

	val = state ? led_enable_mask : SY7802_MODE_OFF;
	if (fled_strobe_used_tmp)
		val |= SY7802_MODE_FLASH;

	enable_mask = SY7802_MODE_MASK | led_enable_mask;
	ret = regmap_update_bits(chip->regmap, SY7802_REG_ENABLE, enable_mask, val);

	if (ret)
		goto unlock;

	chip->fled_strobe_used = fled_strobe_used_tmp;

unlock:
	mutex_unlock(&chip->mutex);
	return ret;
}

static int sy7802_strobe_get(struct led_classdev_flash *fl_cdev, bool *state)
{
	struct sy7802_led *led = container_of(fl_cdev, struct sy7802_led, flash);
	struct sy7802 *chip = led->chip;

	mutex_lock(&chip->mutex);
	*state = !!(chip->fled_strobe_used & BIT(led->led_id));
	mutex_unlock(&chip->mutex);

	return 0;
}

static int sy7802_timeout_set(struct led_classdev_flash *fl_cdev, u32 timeout)
{
	struct sy7802_led *led = container_of(fl_cdev, struct sy7802_led, flash);
	struct led_flash_setting *s = &fl_cdev->timeout;
	u32 val = (timeout - s->min) / s->step;
	struct sy7802 *chip = led->chip;

	return regmap_write(chip->regmap, SY7802_REG_FLASH_DURATION, val);
}

static int sy7802_fault_get(struct led_classdev_flash *fl_cdev, u32 *fault)
{
	struct sy7802_led *led = container_of(fl_cdev, struct sy7802_led, flash);
	struct sy7802 *chip = led->chip;
	u32 val, led_faults = 0;
	int ret;

	/* NOTE: reading register clears fault status */
	ret = regmap_read(chip->regmap, SY7802_REG_FLAGS, &val);
	if (ret)
		return ret;

	if (val & (SY7802_FLAG_FLASH_INPUT_VOLTAGE_LOW | SY7802_FLAG_INPUT_VOLTAGE_LOW))
		led_faults |= LED_FAULT_INPUT_VOLTAGE;

	if (val & SY7802_FLAG_THERMAL_SHUTDOWN)
		led_faults |= LED_FAULT_OVER_TEMPERATURE;

	if (val & SY7802_FLAG_TIMEOUT)
		led_faults |= LED_FAULT_TIMEOUT;

	*fault = led_faults;
	return 0;
}

static const struct led_flash_ops sy7802_flash_ops = {
	.flash_brightness_set = sy7802_flash_brightness_set,
	.strobe_set = sy7802_strobe_set,
	.strobe_get = sy7802_strobe_get,
	.timeout_set = sy7802_timeout_set,
	.fault_get = sy7802_fault_get,
};

static void sy7802_init_flash_brightness(struct led_classdev_flash *fl_cdev)
{
	struct led_flash_setting *s;

	/* Init flash brightness setting */
	s = &fl_cdev->brightness;
	s->min = SY7802_FLASH_BRIGHTNESS_MIN;
	s->max = SY7802_FLASH_BRIGHTNESS_MAX;
	s->step = SY7802_FLASH_BRIGHTNESS_STEP;
	s->val = SY7802_FLASH_BRIGHTNESS_DEFAULT;
}

static void sy7802_init_flash_timeout(struct led_classdev_flash *fl_cdev)
{
	struct led_flash_setting *s;

	/* Init flash timeout setting */
	s = &fl_cdev->timeout;
	s->min = SY7802_TIMEOUT_MIN_US;
	s->max = SY7802_TIMEOUT_MAX_US;
	s->step = SY7802_TIMEOUT_STEPSIZE_US;
	s->val = SY7802_TIMEOUT_DEFAULT_US;
}

static int sy7802_led_register(struct device *dev, struct sy7802_led *led,
			       struct device_node *np)
{
	struct led_init_data init_data = {};
	int ret;

	init_data.fwnode = of_fwnode_handle(np);

	ret = devm_led_classdev_flash_register_ext(dev, &led->flash, &init_data);
	if (ret) {
		dev_err(dev, "Couldn't register flash %d\n", led->led_id);
		return ret;
	}

	return 0;
}

static int sy7802_init_flash_properties(struct device *dev, struct sy7802_led *led,
					struct device_node *np)
{
	struct led_classdev_flash *flash = &led->flash;
	struct led_classdev *lcdev = &flash->led_cdev;
	u32 sources[SY7802_MAX_LEDS];
	int i, num, ret;

	num = of_property_count_u32_elems(np, "led-sources");
	if (num < 1) {
		dev_err(dev, "Not specified or wrong number of led-sources\n");
		return -EINVAL;
	}

	ret = of_property_read_u32_array(np, "led-sources", sources, num);
	if (ret)
		return ret;

	for (i = 0; i < num; i++) {
		if (sources[i] >= SY7802_MAX_LEDS)
			return -EINVAL;
		if (led->chip->leds_active & BIT(sources[i]))
			return -EINVAL;
		led->chip->leds_active |= BIT(sources[i]);
	}

	/* If both channels are specified in 'led-sources', joint flash output mode is used */
	led->led_id = num == 2 ? SY7802_LED_JOINT : sources[0];

	lcdev->max_brightness = SY7802_TORCH_BRIGHTNESS_MAX;
	lcdev->brightness_set_blocking = sy7802_torch_brightness_set;
	lcdev->flags |= LED_DEV_CAP_FLASH;

	flash->ops = &sy7802_flash_ops;

	sy7802_init_flash_brightness(flash);
	sy7802_init_flash_timeout(flash);

	return 0;
}

static int sy7802_chip_check(struct sy7802 *chip)
{
	struct device *dev = chip->dev;
	u32 chipid;
	int ret;

	ret = regmap_read(chip->regmap, SY7802_REG_DEV_ID, &chipid);
	if (ret)
		return dev_err_probe(dev, ret, "Failed to read chip ID\n");

	if (chipid != SY7802_CHIP_ID)
		return dev_err_probe(dev, -ENODEV, "Unsupported chip detected: %x\n", chipid);

	return 0;
}

static void sy7802_enable(struct sy7802 *chip)
{
	gpiod_set_value_cansleep(chip->enable_gpio, 1);
	usleep_range(200, 300);
}

static void sy7802_disable(struct sy7802 *chip)
{
	gpiod_set_value_cansleep(chip->enable_gpio, 0);
}

static int sy7802_probe_dt(struct sy7802 *chip)
{
	struct device_node *np = dev_of_node(chip->dev);
	int child_num;
	int ret;

	regmap_write(chip->regmap, SY7802_REG_ENABLE, SY7802_MODE_OFF);
	regmap_write(chip->regmap, SY7802_REG_TORCH_BRIGHTNESS, LED_OFF);

	child_num = 0;
	for_each_available_child_of_node_scoped(np, child) {
		struct sy7802_led *led = chip->leds + child_num;

		led->chip = chip;
		led->led_id = child_num;

		ret = sy7802_init_flash_properties(chip->dev, led, child);
		if (ret)
			return ret;

		ret = sy7802_led_register(chip->dev, led, child);
		if (ret)
			return ret;

		child_num++;
	}
	return 0;
}

static void sy7802_chip_disable_action(void *data)
{
	struct sy7802 *chip = data;

	sy7802_disable(chip);
}

static void sy7802_regulator_disable_action(void *data)
{
	struct sy7802 *chip = data;

	regulator_disable(chip->vin_regulator);
}

static const struct regmap_config sy7802_regmap_config = {
	.reg_bits = 8,
	.val_bits = 8,
	.max_register = 0xff,
	.cache_type = REGCACHE_MAPLE,
	.reg_defaults = sy7802_regmap_defs,
	.num_reg_defaults = ARRAY_SIZE(sy7802_regmap_defs),
};

static int sy7802_probe(struct i2c_client *client)
{
	struct device *dev = &client->dev;
	struct sy7802 *chip;
	size_t count;
	int ret;

	count = device_get_child_node_count(dev);
	if (!count || count > SY7802_MAX_LEDS)
		return dev_err_probe(dev, -EINVAL, "Invalid amount of LED nodes %zu\n", count);

	chip = devm_kzalloc(dev, struct_size(chip, leds, count), GFP_KERNEL);
	if (!chip)
		return -ENOMEM;

	chip->num_leds = count;

	chip->dev = dev;
	i2c_set_clientdata(client, chip);

	chip->enable_gpio = devm_gpiod_get(dev, "enable", GPIOD_OUT_LOW);
	ret = PTR_ERR_OR_ZERO(chip->enable_gpio);
	if (ret)
		return dev_err_probe(dev, ret, "Failed to request enable gpio\n");

	chip->vin_regulator = devm_regulator_get(dev, "vin");
	ret = PTR_ERR_OR_ZERO(chip->vin_regulator);
	if (ret)
		return dev_err_probe(dev, ret, "Failed to request regulator\n");

	ret = regulator_enable(chip->vin_regulator);
	if (ret)
		return dev_err_probe(dev, ret, "Failed to enable regulator\n");

	ret = devm_add_action_or_reset(dev, sy7802_regulator_disable_action, chip);
	if (ret)
		return ret;

	ret = devm_mutex_init(dev, &chip->mutex);
	if (ret)
		return ret;

	mutex_lock(&chip->mutex);

	chip->regmap = devm_regmap_init_i2c(client, &sy7802_regmap_config);
	if (IS_ERR(chip->regmap)) {
		ret = PTR_ERR(chip->regmap);
		dev_err_probe(dev, ret, "Failed to allocate register map\n");
		goto error;
	}

	ret = sy7802_probe_dt(chip);
	if (ret < 0)
		goto error;

	sy7802_enable(chip);

	ret = devm_add_action_or_reset(dev, sy7802_chip_disable_action, chip);
	if (ret)
		goto error;

	ret = sy7802_chip_check(chip);

error:
	mutex_unlock(&chip->mutex);
	return ret;
}

static const struct of_device_id __maybe_unused sy7802_leds_match[] = {
	{ .compatible = "silergy,sy7802", },
	{}
};
MODULE_DEVICE_TABLE(of, sy7802_leds_match);

static struct i2c_driver sy7802_driver = {
	.driver = {
		.name = "sy7802",
		.of_match_table = of_match_ptr(sy7802_leds_match),
	},
	.probe = sy7802_probe,
};
module_i2c_driver(sy7802_driver);

MODULE_AUTHOR("André Apitzsch <git@apitzsch.eu>");
MODULE_DESCRIPTION("Silergy SY7802 flash LED driver");
MODULE_LICENSE("GPL");