Commit | Line | Data |
---|---|---|
089381b2 MB |
1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* | |
3 | * CZ.NIC's Turris Omnia LEDs driver | |
4 | * | |
b37c3848 | 5 | * 2020 by Marek Behún <kabel@kernel.org> |
089381b2 MB |
6 | */ |
7 | ||
8 | #include <linux/i2c.h> | |
9 | #include <linux/led-class-multicolor.h> | |
10 | #include <linux/module.h> | |
11 | #include <linux/mutex.h> | |
12 | #include <linux/of.h> | |
13 | #include "leds.h" | |
14 | ||
5d47ce1d MB |
15 | #define OMNIA_BOARD_LEDS 12 |
16 | #define OMNIA_LED_NUM_CHANNELS 3 | |
089381b2 | 17 | |
5d47ce1d MB |
18 | #define CMD_LED_MODE 3 |
19 | #define CMD_LED_MODE_LED(l) ((l) & 0x0f) | |
20 | #define CMD_LED_MODE_USER 0x10 | |
089381b2 | 21 | |
5d47ce1d MB |
22 | #define CMD_LED_STATE 4 |
23 | #define CMD_LED_STATE_LED(l) ((l) & 0x0f) | |
24 | #define CMD_LED_STATE_ON 0x10 | |
089381b2 | 25 | |
5d47ce1d MB |
26 | #define CMD_LED_COLOR 5 |
27 | #define CMD_LED_SET_BRIGHTNESS 7 | |
28 | #define CMD_LED_GET_BRIGHTNESS 8 | |
089381b2 | 29 | |
089381b2 MB |
30 | struct omnia_led { |
31 | struct led_classdev_mc mc_cdev; | |
32 | struct mc_subled subled_info[OMNIA_LED_NUM_CHANNELS]; | |
33 | int reg; | |
34 | }; | |
35 | ||
5d47ce1d | 36 | #define to_omnia_led(l) container_of(l, struct omnia_led, mc_cdev) |
089381b2 MB |
37 | |
38 | struct omnia_leds { | |
39 | struct i2c_client *client; | |
40 | struct mutex lock; | |
41 | struct omnia_led leds[]; | |
42 | }; | |
43 | ||
44 | static int omnia_led_brightness_set_blocking(struct led_classdev *cdev, | |
45 | enum led_brightness brightness) | |
46 | { | |
47 | struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev); | |
48 | struct omnia_leds *leds = dev_get_drvdata(cdev->dev->parent); | |
49 | struct omnia_led *led = to_omnia_led(mc_cdev); | |
493d2e43 | 50 | u8 buf[5], state; |
089381b2 MB |
51 | int ret; |
52 | ||
53 | mutex_lock(&leds->lock); | |
54 | ||
55 | led_mc_calc_color_components(&led->mc_cdev, brightness); | |
56 | ||
493d2e43 MB |
57 | buf[0] = CMD_LED_COLOR; |
58 | buf[1] = led->reg; | |
59 | buf[2] = mc_cdev->subled_info[0].brightness; | |
60 | buf[3] = mc_cdev->subled_info[1].brightness; | |
61 | buf[4] = mc_cdev->subled_info[2].brightness; | |
089381b2 MB |
62 | |
63 | state = CMD_LED_STATE_LED(led->reg); | |
493d2e43 | 64 | if (buf[2] || buf[3] || buf[4]) |
089381b2 MB |
65 | state |= CMD_LED_STATE_ON; |
66 | ||
67 | ret = i2c_smbus_write_byte_data(leds->client, CMD_LED_STATE, state); | |
68 | if (ret >= 0 && (state & CMD_LED_STATE_ON)) | |
69 | ret = i2c_master_send(leds->client, buf, 5); | |
70 | ||
71 | mutex_unlock(&leds->lock); | |
72 | ||
73 | return ret; | |
74 | } | |
75 | ||
76 | static int omnia_led_register(struct i2c_client *client, struct omnia_led *led, | |
77 | struct device_node *np) | |
78 | { | |
79 | struct led_init_data init_data = {}; | |
80 | struct device *dev = &client->dev; | |
81 | struct led_classdev *cdev; | |
82 | int ret, color; | |
83 | ||
84 | ret = of_property_read_u32(np, "reg", &led->reg); | |
85 | if (ret || led->reg >= OMNIA_BOARD_LEDS) { | |
86 | dev_warn(dev, | |
87 | "Node %pOF: must contain 'reg' property with values between 0 and %i\n", | |
88 | np, OMNIA_BOARD_LEDS - 1); | |
89 | return 0; | |
90 | } | |
91 | ||
92 | ret = of_property_read_u32(np, "color", &color); | |
98650b08 | 93 | if (ret || color != LED_COLOR_ID_RGB) { |
089381b2 | 94 | dev_warn(dev, |
98650b08 | 95 | "Node %pOF: must contain 'color' property with value LED_COLOR_ID_RGB\n", |
089381b2 MB |
96 | np); |
97 | return 0; | |
98 | } | |
99 | ||
100 | led->subled_info[0].color_index = LED_COLOR_ID_RED; | |
101 | led->subled_info[0].channel = 0; | |
102 | led->subled_info[1].color_index = LED_COLOR_ID_GREEN; | |
103 | led->subled_info[1].channel = 1; | |
104 | led->subled_info[2].color_index = LED_COLOR_ID_BLUE; | |
105 | led->subled_info[2].channel = 2; | |
106 | ||
107 | led->mc_cdev.subled_info = led->subled_info; | |
108 | led->mc_cdev.num_colors = OMNIA_LED_NUM_CHANNELS; | |
109 | ||
110 | init_data.fwnode = &np->fwnode; | |
111 | ||
112 | cdev = &led->mc_cdev.led_cdev; | |
113 | cdev->max_brightness = 255; | |
114 | cdev->brightness_set_blocking = omnia_led_brightness_set_blocking; | |
115 | ||
089381b2 MB |
116 | /* put the LED into software mode */ |
117 | ret = i2c_smbus_write_byte_data(client, CMD_LED_MODE, | |
118 | CMD_LED_MODE_LED(led->reg) | | |
119 | CMD_LED_MODE_USER); | |
120 | if (ret < 0) { | |
5d47ce1d MB |
121 | dev_err(dev, "Cannot set LED %pOF to software mode: %i\n", np, |
122 | ret); | |
089381b2 MB |
123 | return ret; |
124 | } | |
125 | ||
126 | /* disable the LED */ | |
5d47ce1d MB |
127 | ret = i2c_smbus_write_byte_data(client, CMD_LED_STATE, |
128 | CMD_LED_STATE_LED(led->reg)); | |
089381b2 MB |
129 | if (ret < 0) { |
130 | dev_err(dev, "Cannot set LED %pOF brightness: %i\n", np, ret); | |
131 | return ret; | |
132 | } | |
133 | ||
5d47ce1d MB |
134 | ret = devm_led_classdev_multicolor_register_ext(dev, &led->mc_cdev, |
135 | &init_data); | |
089381b2 MB |
136 | if (ret < 0) { |
137 | dev_err(dev, "Cannot register LED %pOF: %i\n", np, ret); | |
138 | return ret; | |
139 | } | |
140 | ||
141 | return 1; | |
142 | } | |
143 | ||
144 | /* | |
145 | * On the front panel of the Turris Omnia router there is also a button which | |
146 | * can be used to control the intensity of all the LEDs at once, so that if they | |
147 | * are too bright, user can dim them. | |
148 | * The microcontroller cycles between 8 levels of this global brightness (from | |
149 | * 100% to 0%), but this setting can have any integer value between 0 and 100. | |
150 | * It is therefore convenient to be able to change this setting from software. | |
151 | * We expose this setting via a sysfs attribute file called "brightness". This | |
152 | * file lives in the device directory of the LED controller, not an individual | |
153 | * LED, so it should not confuse users. | |
154 | */ | |
5d47ce1d MB |
155 | static ssize_t brightness_show(struct device *dev, struct device_attribute *a, |
156 | char *buf) | |
089381b2 MB |
157 | { |
158 | struct i2c_client *client = to_i2c_client(dev); | |
159 | struct omnia_leds *leds = i2c_get_clientdata(client); | |
160 | int ret; | |
161 | ||
162 | mutex_lock(&leds->lock); | |
163 | ret = i2c_smbus_read_byte_data(client, CMD_LED_GET_BRIGHTNESS); | |
164 | mutex_unlock(&leds->lock); | |
165 | ||
166 | if (ret < 0) | |
167 | return ret; | |
168 | ||
169 | return sprintf(buf, "%d\n", ret); | |
170 | } | |
171 | ||
5d47ce1d MB |
172 | static ssize_t brightness_store(struct device *dev, struct device_attribute *a, |
173 | const char *buf, size_t count) | |
089381b2 MB |
174 | { |
175 | struct i2c_client *client = to_i2c_client(dev); | |
176 | struct omnia_leds *leds = i2c_get_clientdata(client); | |
fca050bb | 177 | unsigned long brightness; |
089381b2 MB |
178 | int ret; |
179 | ||
fca050bb | 180 | if (kstrtoul(buf, 10, &brightness)) |
089381b2 MB |
181 | return -EINVAL; |
182 | ||
183 | if (brightness > 100) | |
184 | return -EINVAL; | |
185 | ||
186 | mutex_lock(&leds->lock); | |
5d47ce1d MB |
187 | ret = i2c_smbus_write_byte_data(client, CMD_LED_SET_BRIGHTNESS, |
188 | (u8)brightness); | |
089381b2 MB |
189 | mutex_unlock(&leds->lock); |
190 | ||
191 | if (ret < 0) | |
192 | return ret; | |
193 | ||
194 | return count; | |
195 | } | |
196 | static DEVICE_ATTR_RW(brightness); | |
197 | ||
198 | static struct attribute *omnia_led_controller_attrs[] = { | |
199 | &dev_attr_brightness.attr, | |
200 | NULL, | |
201 | }; | |
202 | ATTRIBUTE_GROUPS(omnia_led_controller); | |
203 | ||
204 | static int omnia_leds_probe(struct i2c_client *client, | |
205 | const struct i2c_device_id *id) | |
206 | { | |
207 | struct device *dev = &client->dev; | |
8853c95e | 208 | struct device_node *np = dev_of_node(dev), *child; |
089381b2 MB |
209 | struct omnia_leds *leds; |
210 | struct omnia_led *led; | |
211 | int ret, count; | |
212 | ||
213 | count = of_get_available_child_count(np); | |
214 | if (!count) { | |
215 | dev_err(dev, "LEDs are not defined in device tree!\n"); | |
216 | return -ENODEV; | |
217 | } else if (count > OMNIA_BOARD_LEDS) { | |
218 | dev_err(dev, "Too many LEDs defined in device tree!\n"); | |
219 | return -EINVAL; | |
220 | } | |
221 | ||
222 | leds = devm_kzalloc(dev, struct_size(leds, leds, count), GFP_KERNEL); | |
223 | if (!leds) | |
224 | return -ENOMEM; | |
225 | ||
226 | leds->client = client; | |
227 | i2c_set_clientdata(client, leds); | |
228 | ||
229 | mutex_init(&leds->lock); | |
230 | ||
231 | led = &leds->leds[0]; | |
232 | for_each_available_child_of_node(np, child) { | |
233 | ret = omnia_led_register(client, led, child); | |
2c677562 MB |
234 | if (ret < 0) { |
235 | of_node_put(child); | |
089381b2 | 236 | return ret; |
2c677562 | 237 | } |
089381b2 MB |
238 | |
239 | led += ret; | |
240 | } | |
241 | ||
242 | if (devm_device_add_groups(dev, omnia_led_controller_groups)) | |
243 | dev_warn(dev, "Could not add attribute group!\n"); | |
244 | ||
245 | return 0; | |
246 | } | |
247 | ||
248 | static int omnia_leds_remove(struct i2c_client *client) | |
249 | { | |
493d2e43 | 250 | u8 buf[5]; |
089381b2 MB |
251 | |
252 | /* put all LEDs into default (HW triggered) mode */ | |
253 | i2c_smbus_write_byte_data(client, CMD_LED_MODE, | |
254 | CMD_LED_MODE_LED(OMNIA_BOARD_LEDS)); | |
255 | ||
256 | /* set all LEDs color to [255, 255, 255] */ | |
493d2e43 MB |
257 | buf[0] = CMD_LED_COLOR; |
258 | buf[1] = OMNIA_BOARD_LEDS; | |
259 | buf[2] = 255; | |
260 | buf[3] = 255; | |
261 | buf[4] = 255; | |
089381b2 MB |
262 | |
263 | i2c_master_send(client, buf, 5); | |
264 | ||
265 | return 0; | |
266 | } | |
267 | ||
268 | static const struct of_device_id of_omnia_leds_match[] = { | |
269 | { .compatible = "cznic,turris-omnia-leds", }, | |
270 | {}, | |
271 | }; | |
272 | ||
273 | static const struct i2c_device_id omnia_id[] = { | |
274 | { "omnia", 0 }, | |
275 | { } | |
276 | }; | |
277 | ||
278 | static struct i2c_driver omnia_leds_driver = { | |
279 | .probe = omnia_leds_probe, | |
280 | .remove = omnia_leds_remove, | |
281 | .id_table = omnia_id, | |
282 | .driver = { | |
283 | .name = "leds-turris-omnia", | |
284 | .of_match_table = of_omnia_leds_match, | |
285 | }, | |
286 | }; | |
287 | ||
288 | module_i2c_driver(omnia_leds_driver); | |
289 | ||
b37c3848 | 290 | MODULE_AUTHOR("Marek Behun <kabel@kernel.org>"); |
089381b2 MB |
291 | MODULE_DESCRIPTION("CZ.NIC's Turris Omnia LEDs"); |
292 | MODULE_LICENSE("GPL v2"); |