Commit c7dd343a authored by Lukas Timmermann's avatar Lukas Timmermann Committed by Lee Jones
Browse files

leds: as3668: Driver for the ams Osram 4-channel i2c LED driver



Since there were no existing drivers for the AS3668 or related devices,
a new driver was introduced in a separate file. Similar devices were
reviewed, but none shared enough characteristics to justify code reuse.
As a result, this driver is written specifically for the AS3668.

Signed-off-by: default avatarLukas Timmermann <linux@timmermann.space>
Link: https://patch.msgid.link/20260118165010.902086-3-linux@timmermann.space


Signed-off-by: default avatarLee Jones <lee@kernel.org>
parent 93396086
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -3791,6 +3791,7 @@ M: Lukas Timmermann <linux@timmermann.space>
L:	linux-leds@vger.kernel.org
S:	Maintained
F:	Documentation/devicetree/bindings/leds/ams,as3668.yaml
F:	drivers/leds/leds-as3668.c
ASAHI KASEI AK7375 LENS VOICE COIL DRIVER
M:	Tianshu Qiu <tian.shu.qiu@intel.com>
+13 −0
Original line number Diff line number Diff line
@@ -107,6 +107,19 @@ config LEDS_ARIEL

	  Say Y to if your machine is a Dell Wyse 3020 thin client.

config LEDS_OSRAM_AMS_AS3668
	tristate "LED support for Osram AMS AS3668"
	depends on LEDS_CLASS
	depends on I2C
	help
	  This option enables support for the Osram AMS AS3668 LED controller.
	  The AS3668 provides up to four LED channels and is controlled via
	  the I2C bus. This driver offers basic brightness control for each
	  channel, without support for blinking or other advanced features.

	  To compile this driver as a module, choose M here: the module
	  will be called leds-as3668.

config LEDS_AW200XX
	tristate "LED support for Awinic AW20036/AW20054/AW20072/AW20108"
	depends on LEDS_CLASS
+1 −0
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@ obj-$(CONFIG_LEDS_ADP5520) += leds-adp5520.o
obj-$(CONFIG_LEDS_AN30259A)		+= leds-an30259a.o
obj-$(CONFIG_LEDS_APU)			+= leds-apu.o
obj-$(CONFIG_LEDS_ARIEL)		+= leds-ariel.o
obj-$(CONFIG_LEDS_AS3668)		+= leds-as3668.o
obj-$(CONFIG_LEDS_AW200XX)		+= leds-aw200xx.o
obj-$(CONFIG_LEDS_AW2013)		+= leds-aw2013.o
obj-$(CONFIG_LEDS_BCM6328)		+= leds-bcm6328.o
+202 −0
Original line number Diff line number Diff line
// SPDX-License-Identifier: GPL-2.0-or-later
/*
 *  Osram AMS AS3668 LED Driver IC
 *
 *  Copyright (C) 2025 Lukas Timmermann <linux@timmermann.space>
 */

#include <linux/bitfield.h>
#include <linux/i2c.h>
#include <linux/leds.h>
#include <linux/module.h>
#include <linux/uleds.h>

#define AS3668_MAX_LEDS			4

/* Chip Ident */

#define AS3668_CHIP_ID1_REG		0x3e
#define AS3668_CHIP_ID			0xa5

/* Current Control */

#define AS3668_CURR_MODE_REG		0x01
#define AS3668_CURR_MODE_OFF		0x0
#define AS3668_CURR_MODE_ON		0x1
#define AS3668_CURR1_MODE_MASK		GENMASK(1, 0)
#define AS3668_CURR2_MODE_MASK		GENMASK(3, 2)
#define AS3668_CURR3_MODE_MASK		GENMASK(5, 4)
#define AS3668_CURR4_MODE_MASK		GENMASK(7, 6)
#define AS3668_CURR1_REG		0x02
#define AS3668_CURR2_REG		0x03
#define AS3668_CURR3_REG		0x04
#define AS3668_CURR4_REG		0x05

#define AS3668_CURR_MODE_PACK(mode)	(((mode) << 0) | \
					((mode) << 2) | \
					((mode) << 4) | \
					((mode) << 6))

struct as3668_led {
	struct led_classdev cdev;
	struct as3668 *chip;
	struct fwnode_handle *fwnode;
	u8 mode_mask;
	u8 current_reg;
};

struct as3668 {
	struct i2c_client *client;
	struct as3668_led leds[AS3668_MAX_LEDS];
};

static int as3668_channel_mode_set(struct as3668_led *led, u8 mode)
{
	int ret;
	u8 channel_modes;

	ret = i2c_smbus_read_byte_data(led->chip->client, AS3668_CURR_MODE_REG);
	if (ret < 0) {
		dev_err(led->cdev.dev, "failed to read channel modes\n");
		return ret;
	}
	channel_modes = (u8)ret;

	channel_modes &= ~led->mode_mask;
	channel_modes |= led->mode_mask & (AS3668_CURR_MODE_PACK(mode));

	return i2c_smbus_write_byte_data(led->chip->client, AS3668_CURR_MODE_REG, channel_modes);
}

static enum led_brightness as3668_brightness_get(struct led_classdev *cdev)
{
	struct as3668_led *led = container_of(cdev, struct as3668_led, cdev);

	return i2c_smbus_read_byte_data(led->chip->client, led->current_reg);
}

static void as3668_brightness_set(struct led_classdev *cdev, enum led_brightness brightness)
{
	struct as3668_led *led = container_of(cdev, struct as3668_led, cdev);
	int err;

	err = as3668_channel_mode_set(led, !!brightness);
	if (err)
		dev_err(cdev->dev, "failed to set channel mode: %d\n", err);

	err = i2c_smbus_write_byte_data(led->chip->client, led->current_reg, brightness);
	if (err)
		dev_err(cdev->dev, "failed to set brightness: %d\n", err);
}

static int as3668_dt_init(struct as3668 *as3668)
{
	struct device *dev = &as3668->client->dev;
	struct as3668_led *led;
	struct led_init_data init_data = {};
	int err;
	u32 reg;

	for_each_available_child_of_node_scoped(dev_of_node(dev), child) {
		err = of_property_read_u32(child, "reg", &reg);
		if (err)
			return dev_err_probe(dev, err, "failed to read 'reg' property");

		if (reg < 0 || reg >= AS3668_MAX_LEDS)
			return dev_err_probe(dev, -EINVAL,
					     "unsupported LED: %d\n", reg);

		led = &as3668->leds[reg];
		led->fwnode = of_fwnode_handle(child);

		led->current_reg = reg + AS3668_CURR1_REG;
		led->mode_mask = AS3668_CURR1_MODE_MASK << (reg * 2);
		led->chip = as3668;

		led->cdev.max_brightness = U8_MAX;
		led->cdev.brightness_get = as3668_brightness_get;
		led->cdev.brightness_set = as3668_brightness_set;

		init_data.fwnode = led->fwnode;
		init_data.default_label = ":";

		err = devm_led_classdev_register_ext(dev, &led->cdev, &init_data);
		if (err)
			return dev_err_probe(dev, err, "failed to register LED %d\n", reg);
	}

	return 0;
}

static int as3668_probe(struct i2c_client *client)
{
	struct as3668 *as3668;
	int err;
	u8 chip_id;

	chip_id = i2c_smbus_read_byte_data(client, AS3668_CHIP_ID1_REG);
	if (chip_id != AS3668_CHIP_ID)
		return dev_err_probe(&client->dev, -ENODEV,
				     "expected chip ID 0x%02x, got 0x%02x\n",
				     AS3668_CHIP_ID, chip_id);

	as3668 = devm_kzalloc(&client->dev, sizeof(*as3668), GFP_KERNEL);
	if (!as3668)
		return -ENOMEM;

	as3668->client = client;

	err = as3668_dt_init(as3668);
	if (err)
		return err;

	/* Set all four channel modes to 'off' */
	err = i2c_smbus_write_byte_data(client, AS3668_CURR_MODE_REG,
					FIELD_PREP(AS3668_CURR1_MODE_MASK, AS3668_CURR_MODE_OFF) |
					FIELD_PREP(AS3668_CURR2_MODE_MASK, AS3668_CURR_MODE_OFF) |
					FIELD_PREP(AS3668_CURR3_MODE_MASK, AS3668_CURR_MODE_OFF) |
					FIELD_PREP(AS3668_CURR4_MODE_MASK, AS3668_CURR_MODE_OFF));

	/* Set initial currents to 0mA */
	err |= i2c_smbus_write_byte_data(client, AS3668_CURR1_REG, 0);
	err |= i2c_smbus_write_byte_data(client, AS3668_CURR2_REG, 0);
	err |= i2c_smbus_write_byte_data(client, AS3668_CURR3_REG, 0);
	err |= i2c_smbus_write_byte_data(client, AS3668_CURR4_REG, 0);

	if (err)
		return dev_err_probe(&client->dev, -EIO, "failed to set zero initial current levels\n");

	return 0;
}

static void as3668_remove(struct i2c_client *client)
{
	i2c_smbus_write_byte_data(client, AS3668_CURR_MODE_REG, 0);
}

static const struct i2c_device_id as3668_idtable[] = {
	{ "as3668" },
	{ }
};
MODULE_DEVICE_TABLE(i2c, as3668_idtable);

static const struct of_device_id as3668_match_table[] = {
	{ .compatible = "ams,as3668" },
	{ }
};
MODULE_DEVICE_TABLE(of, as3668_match_table);

static struct i2c_driver as3668_driver = {
	.driver = {
		.name = "leds_as3668",
		.of_match_table = as3668_match_table,
	},
	.probe = as3668_probe,
	.remove = as3668_remove,
	.id_table = as3668_idtable,
};
module_i2c_driver(as3668_driver);

MODULE_AUTHOR("Lukas Timmermann <linux@timmermann.space>");
MODULE_DESCRIPTION("AS3668 LED driver");
MODULE_LICENSE("GPL");