Christian Mauderer | e9a804d | 2019-05-13 21:33:07 +0200 | [diff] [blame] | 1 | // SPDX-License-Identifier: GPL-2.0 |
| 2 | // Copyright (c) 2019 Christian Mauderer <oss@c-mauderer.de> |
| 3 | |
| 4 | /* |
| 5 | * The driver supports controllers with a very simple SPI protocol: |
| 6 | * - one LED is controlled by a single byte on MOSI |
| 7 | * - the value of the byte gives the brightness between two values (lowest to |
| 8 | * highest) |
| 9 | * - no return value is necessary (no MISO signal) |
| 10 | * |
| 11 | * The value for minimum and maximum brightness depends on the device |
| 12 | * (compatible string). |
| 13 | * |
| 14 | * Supported devices: |
| 15 | * - "ubnt,acb-spi-led": Microcontroller (SONiX 8F26E611LA) based device used |
| 16 | * for example in Ubiquiti airCube ISP. Reverse engineered protocol for this |
| 17 | * controller: |
| 18 | * * Higher two bits set a mode. Lower six bits are a parameter. |
| 19 | * * Mode: 00 -> set brightness between 0x00 (min) and 0x3F (max) |
| 20 | * * Mode: 01 -> pulsing pattern (min -> max -> min) with an interval. From |
| 21 | * some tests, the period is about (50ms + 102ms * parameter). There is a |
| 22 | * slightly different pattern starting from 0x10 (longer gap between the |
| 23 | * pulses) but the time still follows that calculation. |
| 24 | * * Mode: 10 -> same as 01 but with only a ramp from min to max. Again a |
| 25 | * slight jump in the pattern at 0x10. |
| 26 | * * Mode: 11 -> blinking (off -> 25% -> off -> 25% -> ...) with a period of |
| 27 | * (105ms * parameter) |
| 28 | * NOTE: This driver currently only supports mode 00. |
| 29 | */ |
| 30 | |
| 31 | #include <linux/leds.h> |
| 32 | #include <linux/module.h> |
| 33 | #include <linux/of_device.h> |
| 34 | #include <linux/spi/spi.h> |
| 35 | #include <linux/mutex.h> |
| 36 | #include <uapi/linux/uleds.h> |
| 37 | |
| 38 | struct spi_byte_chipdef { |
| 39 | /* SPI byte that will be send to switch the LED off */ |
| 40 | u8 off_value; |
| 41 | /* SPI byte that will be send to switch the LED to maximum brightness */ |
| 42 | u8 max_value; |
| 43 | }; |
| 44 | |
| 45 | struct spi_byte_led { |
| 46 | struct led_classdev ldev; |
| 47 | struct spi_device *spi; |
| 48 | char name[LED_MAX_NAME_SIZE]; |
| 49 | struct mutex mutex; |
| 50 | const struct spi_byte_chipdef *cdef; |
| 51 | }; |
| 52 | |
| 53 | static const struct spi_byte_chipdef ubnt_acb_spi_led_cdef = { |
| 54 | .off_value = 0x0, |
| 55 | .max_value = 0x3F, |
| 56 | }; |
| 57 | |
| 58 | static const struct of_device_id spi_byte_dt_ids[] = { |
| 59 | { .compatible = "ubnt,acb-spi-led", .data = &ubnt_acb_spi_led_cdef }, |
| 60 | {}, |
| 61 | }; |
| 62 | |
| 63 | MODULE_DEVICE_TABLE(of, spi_byte_dt_ids); |
| 64 | |
| 65 | static int spi_byte_brightness_set_blocking(struct led_classdev *dev, |
| 66 | enum led_brightness brightness) |
| 67 | { |
| 68 | struct spi_byte_led *led = container_of(dev, struct spi_byte_led, ldev); |
| 69 | u8 value; |
| 70 | int ret; |
| 71 | |
| 72 | value = (u8) brightness + led->cdef->off_value; |
| 73 | |
| 74 | mutex_lock(&led->mutex); |
| 75 | ret = spi_write(led->spi, &value, sizeof(value)); |
| 76 | mutex_unlock(&led->mutex); |
| 77 | |
| 78 | return ret; |
| 79 | } |
| 80 | |
| 81 | static int spi_byte_probe(struct spi_device *spi) |
| 82 | { |
Christian Mauderer | e9a804d | 2019-05-13 21:33:07 +0200 | [diff] [blame] | 83 | struct device_node *child; |
| 84 | struct device *dev = &spi->dev; |
| 85 | struct spi_byte_led *led; |
| 86 | const char *name = "leds-spi-byte::"; |
| 87 | const char *state; |
| 88 | int ret; |
| 89 | |
Marek Behún | 99a013c | 2020-09-18 00:32:56 +0200 | [diff] [blame] | 90 | if (of_get_available_child_count(dev_of_node(dev)) != 1) { |
Christian Mauderer | e9a804d | 2019-05-13 21:33:07 +0200 | [diff] [blame] | 91 | dev_err(dev, "Device must have exactly one LED sub-node."); |
| 92 | return -EINVAL; |
| 93 | } |
Marek Behún | 99a013c | 2020-09-18 00:32:56 +0200 | [diff] [blame] | 94 | child = of_get_next_available_child(dev_of_node(dev), NULL); |
Christian Mauderer | e9a804d | 2019-05-13 21:33:07 +0200 | [diff] [blame] | 95 | |
| 96 | led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); |
| 97 | if (!led) |
| 98 | return -ENOMEM; |
| 99 | |
| 100 | of_property_read_string(child, "label", &name); |
| 101 | strlcpy(led->name, name, sizeof(led->name)); |
| 102 | led->spi = spi; |
| 103 | mutex_init(&led->mutex); |
Marek Behún | 2779f47 | 2020-09-18 00:32:52 +0200 | [diff] [blame] | 104 | led->cdef = device_get_match_data(dev); |
Christian Mauderer | e9a804d | 2019-05-13 21:33:07 +0200 | [diff] [blame] | 105 | led->ldev.name = led->name; |
| 106 | led->ldev.brightness = LED_OFF; |
| 107 | led->ldev.max_brightness = led->cdef->max_value - led->cdef->off_value; |
| 108 | led->ldev.brightness_set_blocking = spi_byte_brightness_set_blocking; |
| 109 | |
| 110 | state = of_get_property(child, "default-state", NULL); |
| 111 | if (state) { |
| 112 | if (!strcmp(state, "on")) { |
| 113 | led->ldev.brightness = led->ldev.max_brightness; |
| 114 | } else if (strcmp(state, "off")) { |
| 115 | /* all other cases except "off" */ |
| 116 | dev_err(dev, "default-state can only be 'on' or 'off'"); |
| 117 | return -EINVAL; |
| 118 | } |
| 119 | } |
| 120 | spi_byte_brightness_set_blocking(&led->ldev, |
| 121 | led->ldev.brightness); |
| 122 | |
| 123 | ret = devm_led_classdev_register(&spi->dev, &led->ldev); |
| 124 | if (ret) { |
| 125 | mutex_destroy(&led->mutex); |
| 126 | return ret; |
| 127 | } |
| 128 | spi_set_drvdata(spi, led); |
| 129 | |
| 130 | return 0; |
| 131 | } |
| 132 | |
| 133 | static int spi_byte_remove(struct spi_device *spi) |
| 134 | { |
| 135 | struct spi_byte_led *led = spi_get_drvdata(spi); |
| 136 | |
| 137 | mutex_destroy(&led->mutex); |
| 138 | |
| 139 | return 0; |
| 140 | } |
| 141 | |
| 142 | static struct spi_driver spi_byte_driver = { |
| 143 | .probe = spi_byte_probe, |
| 144 | .remove = spi_byte_remove, |
| 145 | .driver = { |
| 146 | .name = KBUILD_MODNAME, |
| 147 | .of_match_table = spi_byte_dt_ids, |
| 148 | }, |
| 149 | }; |
| 150 | |
| 151 | module_spi_driver(spi_byte_driver); |
| 152 | |
| 153 | MODULE_AUTHOR("Christian Mauderer <oss@c-mauderer.de>"); |
| 154 | MODULE_DESCRIPTION("single byte SPI LED driver"); |
| 155 | MODULE_LICENSE("GPL v2"); |
| 156 | MODULE_ALIAS("spi:leds-spi-byte"); |