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.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(spi: led->spi, buf: &value, len: sizeof(value)); |
76 | mutex_unlock(lock: &led->mutex); |
77 | |
78 | return ret; |
79 | } |
80 | |
81 | static int spi_byte_probe(struct spi_device *spi) |
82 | { |
83 | struct device_node *child; |
84 | struct device *dev = &spi->dev; |
85 | struct spi_byte_led *led; |
86 | struct led_init_data init_data = {}; |
87 | const char *state; |
88 | int ret; |
89 | |
90 | if (of_get_available_child_count(np: dev_of_node(dev)) != 1) { |
91 | dev_err(dev, "Device must have exactly one LED sub-node." ); |
92 | return -EINVAL; |
93 | } |
94 | child = of_get_next_available_child(node: dev_of_node(dev), NULL); |
95 | |
96 | led = devm_kzalloc(dev, size: sizeof(*led), GFP_KERNEL); |
97 | if (!led) |
98 | return -ENOMEM; |
99 | |
100 | led->spi = spi; |
101 | mutex_init(&led->mutex); |
102 | led->cdef = device_get_match_data(dev); |
103 | led->ldev.brightness = LED_OFF; |
104 | led->ldev.max_brightness = led->cdef->max_value - led->cdef->off_value; |
105 | led->ldev.brightness_set_blocking = spi_byte_brightness_set_blocking; |
106 | |
107 | state = of_get_property(node: child, name: "default-state" , NULL); |
108 | if (state) { |
109 | if (!strcmp(state, "on" )) { |
110 | led->ldev.brightness = led->ldev.max_brightness; |
111 | } else if (strcmp(state, "off" )) { |
112 | /* all other cases except "off" */ |
113 | dev_err(dev, "default-state can only be 'on' or 'off'" ); |
114 | return -EINVAL; |
115 | } |
116 | } |
117 | spi_byte_brightness_set_blocking(dev: &led->ldev, |
118 | brightness: led->ldev.brightness); |
119 | |
120 | init_data.fwnode = of_fwnode_handle(child); |
121 | init_data.devicename = "leds-spi-byte" ; |
122 | init_data.default_label = ":" ; |
123 | |
124 | ret = devm_led_classdev_register_ext(parent: &spi->dev, led_cdev: &led->ldev, init_data: &init_data); |
125 | if (ret) { |
126 | mutex_destroy(lock: &led->mutex); |
127 | return ret; |
128 | } |
129 | spi_set_drvdata(spi, data: led); |
130 | |
131 | return 0; |
132 | } |
133 | |
134 | static void spi_byte_remove(struct spi_device *spi) |
135 | { |
136 | struct spi_byte_led *led = spi_get_drvdata(spi); |
137 | |
138 | mutex_destroy(lock: &led->mutex); |
139 | } |
140 | |
141 | static struct spi_driver spi_byte_driver = { |
142 | .probe = spi_byte_probe, |
143 | .remove = spi_byte_remove, |
144 | .driver = { |
145 | .name = KBUILD_MODNAME, |
146 | .of_match_table = spi_byte_dt_ids, |
147 | }, |
148 | }; |
149 | |
150 | module_spi_driver(spi_byte_driver); |
151 | |
152 | MODULE_AUTHOR("Christian Mauderer <oss@c-mauderer.de>" ); |
153 | MODULE_DESCRIPTION("single byte SPI LED driver" ); |
154 | MODULE_LICENSE("GPL v2" ); |
155 | MODULE_ALIAS("spi:leds-spi-byte" ); |
156 | |