162306a36Sopenharmony_ci// SPDX-License-Identifier: GPL-2.0
262306a36Sopenharmony_ci// Copyright (c) 2019 Christian Mauderer <oss@c-mauderer.de>
362306a36Sopenharmony_ci
462306a36Sopenharmony_ci/*
562306a36Sopenharmony_ci * The driver supports controllers with a very simple SPI protocol:
662306a36Sopenharmony_ci * - one LED is controlled by a single byte on MOSI
762306a36Sopenharmony_ci * - the value of the byte gives the brightness between two values (lowest to
862306a36Sopenharmony_ci *   highest)
962306a36Sopenharmony_ci * - no return value is necessary (no MISO signal)
1062306a36Sopenharmony_ci *
1162306a36Sopenharmony_ci * The value for minimum and maximum brightness depends on the device
1262306a36Sopenharmony_ci * (compatible string).
1362306a36Sopenharmony_ci *
1462306a36Sopenharmony_ci * Supported devices:
1562306a36Sopenharmony_ci * - "ubnt,acb-spi-led": Microcontroller (SONiX 8F26E611LA) based device used
1662306a36Sopenharmony_ci *   for example in Ubiquiti airCube ISP. Reverse engineered protocol for this
1762306a36Sopenharmony_ci *   controller:
1862306a36Sopenharmony_ci *   * Higher two bits set a mode. Lower six bits are a parameter.
1962306a36Sopenharmony_ci *   * Mode: 00 -> set brightness between 0x00 (min) and 0x3F (max)
2062306a36Sopenharmony_ci *   * Mode: 01 -> pulsing pattern (min -> max -> min) with an interval. From
2162306a36Sopenharmony_ci *     some tests, the period is about (50ms + 102ms * parameter). There is a
2262306a36Sopenharmony_ci *     slightly different pattern starting from 0x10 (longer gap between the
2362306a36Sopenharmony_ci *     pulses) but the time still follows that calculation.
2462306a36Sopenharmony_ci *   * Mode: 10 -> same as 01 but with only a ramp from min to max. Again a
2562306a36Sopenharmony_ci *     slight jump in the pattern at 0x10.
2662306a36Sopenharmony_ci *   * Mode: 11 -> blinking (off -> 25% -> off -> 25% -> ...) with a period of
2762306a36Sopenharmony_ci *     (105ms * parameter)
2862306a36Sopenharmony_ci *   NOTE: This driver currently only supports mode 00.
2962306a36Sopenharmony_ci */
3062306a36Sopenharmony_ci
3162306a36Sopenharmony_ci#include <linux/leds.h>
3262306a36Sopenharmony_ci#include <linux/module.h>
3362306a36Sopenharmony_ci#include <linux/of.h>
3462306a36Sopenharmony_ci#include <linux/spi/spi.h>
3562306a36Sopenharmony_ci#include <linux/mutex.h>
3662306a36Sopenharmony_ci#include <uapi/linux/uleds.h>
3762306a36Sopenharmony_ci
3862306a36Sopenharmony_cistruct spi_byte_chipdef {
3962306a36Sopenharmony_ci	/* SPI byte that will be send to switch the LED off */
4062306a36Sopenharmony_ci	u8	off_value;
4162306a36Sopenharmony_ci	/* SPI byte that will be send to switch the LED to maximum brightness */
4262306a36Sopenharmony_ci	u8	max_value;
4362306a36Sopenharmony_ci};
4462306a36Sopenharmony_ci
4562306a36Sopenharmony_cistruct spi_byte_led {
4662306a36Sopenharmony_ci	struct led_classdev		ldev;
4762306a36Sopenharmony_ci	struct spi_device		*spi;
4862306a36Sopenharmony_ci	char				name[LED_MAX_NAME_SIZE];
4962306a36Sopenharmony_ci	struct mutex			mutex;
5062306a36Sopenharmony_ci	const struct spi_byte_chipdef	*cdef;
5162306a36Sopenharmony_ci};
5262306a36Sopenharmony_ci
5362306a36Sopenharmony_cistatic const struct spi_byte_chipdef ubnt_acb_spi_led_cdef = {
5462306a36Sopenharmony_ci	.off_value = 0x0,
5562306a36Sopenharmony_ci	.max_value = 0x3F,
5662306a36Sopenharmony_ci};
5762306a36Sopenharmony_ci
5862306a36Sopenharmony_cistatic const struct of_device_id spi_byte_dt_ids[] = {
5962306a36Sopenharmony_ci	{ .compatible = "ubnt,acb-spi-led", .data = &ubnt_acb_spi_led_cdef },
6062306a36Sopenharmony_ci	{},
6162306a36Sopenharmony_ci};
6262306a36Sopenharmony_ci
6362306a36Sopenharmony_ciMODULE_DEVICE_TABLE(of, spi_byte_dt_ids);
6462306a36Sopenharmony_ci
6562306a36Sopenharmony_cistatic int spi_byte_brightness_set_blocking(struct led_classdev *dev,
6662306a36Sopenharmony_ci					    enum led_brightness brightness)
6762306a36Sopenharmony_ci{
6862306a36Sopenharmony_ci	struct spi_byte_led *led = container_of(dev, struct spi_byte_led, ldev);
6962306a36Sopenharmony_ci	u8 value;
7062306a36Sopenharmony_ci	int ret;
7162306a36Sopenharmony_ci
7262306a36Sopenharmony_ci	value = (u8) brightness + led->cdef->off_value;
7362306a36Sopenharmony_ci
7462306a36Sopenharmony_ci	mutex_lock(&led->mutex);
7562306a36Sopenharmony_ci	ret = spi_write(led->spi, &value, sizeof(value));
7662306a36Sopenharmony_ci	mutex_unlock(&led->mutex);
7762306a36Sopenharmony_ci
7862306a36Sopenharmony_ci	return ret;
7962306a36Sopenharmony_ci}
8062306a36Sopenharmony_ci
8162306a36Sopenharmony_cistatic int spi_byte_probe(struct spi_device *spi)
8262306a36Sopenharmony_ci{
8362306a36Sopenharmony_ci	struct device_node *child;
8462306a36Sopenharmony_ci	struct device *dev = &spi->dev;
8562306a36Sopenharmony_ci	struct spi_byte_led *led;
8662306a36Sopenharmony_ci	const char *name = "leds-spi-byte::";
8762306a36Sopenharmony_ci	const char *state;
8862306a36Sopenharmony_ci	int ret;
8962306a36Sopenharmony_ci
9062306a36Sopenharmony_ci	if (of_get_available_child_count(dev_of_node(dev)) != 1) {
9162306a36Sopenharmony_ci		dev_err(dev, "Device must have exactly one LED sub-node.");
9262306a36Sopenharmony_ci		return -EINVAL;
9362306a36Sopenharmony_ci	}
9462306a36Sopenharmony_ci	child = of_get_next_available_child(dev_of_node(dev), NULL);
9562306a36Sopenharmony_ci
9662306a36Sopenharmony_ci	led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL);
9762306a36Sopenharmony_ci	if (!led)
9862306a36Sopenharmony_ci		return -ENOMEM;
9962306a36Sopenharmony_ci
10062306a36Sopenharmony_ci	of_property_read_string(child, "label", &name);
10162306a36Sopenharmony_ci	strscpy(led->name, name, sizeof(led->name));
10262306a36Sopenharmony_ci	led->spi = spi;
10362306a36Sopenharmony_ci	mutex_init(&led->mutex);
10462306a36Sopenharmony_ci	led->cdef = device_get_match_data(dev);
10562306a36Sopenharmony_ci	led->ldev.name = led->name;
10662306a36Sopenharmony_ci	led->ldev.brightness = LED_OFF;
10762306a36Sopenharmony_ci	led->ldev.max_brightness = led->cdef->max_value - led->cdef->off_value;
10862306a36Sopenharmony_ci	led->ldev.brightness_set_blocking = spi_byte_brightness_set_blocking;
10962306a36Sopenharmony_ci
11062306a36Sopenharmony_ci	state = of_get_property(child, "default-state", NULL);
11162306a36Sopenharmony_ci	if (state) {
11262306a36Sopenharmony_ci		if (!strcmp(state, "on")) {
11362306a36Sopenharmony_ci			led->ldev.brightness = led->ldev.max_brightness;
11462306a36Sopenharmony_ci		} else if (strcmp(state, "off")) {
11562306a36Sopenharmony_ci			/* all other cases except "off" */
11662306a36Sopenharmony_ci			dev_err(dev, "default-state can only be 'on' or 'off'");
11762306a36Sopenharmony_ci			return -EINVAL;
11862306a36Sopenharmony_ci		}
11962306a36Sopenharmony_ci	}
12062306a36Sopenharmony_ci	spi_byte_brightness_set_blocking(&led->ldev,
12162306a36Sopenharmony_ci					 led->ldev.brightness);
12262306a36Sopenharmony_ci
12362306a36Sopenharmony_ci	ret = devm_led_classdev_register(&spi->dev, &led->ldev);
12462306a36Sopenharmony_ci	if (ret) {
12562306a36Sopenharmony_ci		mutex_destroy(&led->mutex);
12662306a36Sopenharmony_ci		return ret;
12762306a36Sopenharmony_ci	}
12862306a36Sopenharmony_ci	spi_set_drvdata(spi, led);
12962306a36Sopenharmony_ci
13062306a36Sopenharmony_ci	return 0;
13162306a36Sopenharmony_ci}
13262306a36Sopenharmony_ci
13362306a36Sopenharmony_cistatic void spi_byte_remove(struct spi_device *spi)
13462306a36Sopenharmony_ci{
13562306a36Sopenharmony_ci	struct spi_byte_led	*led = spi_get_drvdata(spi);
13662306a36Sopenharmony_ci
13762306a36Sopenharmony_ci	mutex_destroy(&led->mutex);
13862306a36Sopenharmony_ci}
13962306a36Sopenharmony_ci
14062306a36Sopenharmony_cistatic struct spi_driver spi_byte_driver = {
14162306a36Sopenharmony_ci	.probe		= spi_byte_probe,
14262306a36Sopenharmony_ci	.remove		= spi_byte_remove,
14362306a36Sopenharmony_ci	.driver = {
14462306a36Sopenharmony_ci		.name		= KBUILD_MODNAME,
14562306a36Sopenharmony_ci		.of_match_table	= spi_byte_dt_ids,
14662306a36Sopenharmony_ci	},
14762306a36Sopenharmony_ci};
14862306a36Sopenharmony_ci
14962306a36Sopenharmony_cimodule_spi_driver(spi_byte_driver);
15062306a36Sopenharmony_ci
15162306a36Sopenharmony_ciMODULE_AUTHOR("Christian Mauderer <oss@c-mauderer.de>");
15262306a36Sopenharmony_ciMODULE_DESCRIPTION("single byte SPI LED driver");
15362306a36Sopenharmony_ciMODULE_LICENSE("GPL v2");
15462306a36Sopenharmony_ciMODULE_ALIAS("spi:leds-spi-byte");
155