1 // SPDX-License-Identifier: GPL-2.0
3 * CZ.NIC's Turris Omnia LEDs driver
5 * 2020, 2023 by Marek BehĂșn <kabel@kernel.org>
9 #include <linux/led-class-multicolor.h>
10 #include <linux/module.h>
11 #include <linux/mutex.h>
14 #define OMNIA_BOARD_LEDS 12
15 #define OMNIA_LED_NUM_CHANNELS 3
17 /* MCU controller commands at I2C address 0x2a */
18 #define OMNIA_MCU_I2C_ADDR 0x2a
20 #define CMD_GET_STATUS_WORD 0x01
21 #define STS_FEATURES_SUPPORTED BIT(2)
23 #define CMD_GET_FEATURES 0x10
24 #define FEAT_LED_GAMMA_CORRECTION BIT(5)
26 /* LED controller commands at I2C address 0x2b */
27 #define CMD_LED_MODE 0x03
28 #define CMD_LED_MODE_LED(l) ((l) & 0x0f)
29 #define CMD_LED_MODE_USER 0x10
31 #define CMD_LED_STATE 0x04
32 #define CMD_LED_STATE_LED(l) ((l) & 0x0f)
33 #define CMD_LED_STATE_ON 0x10
35 #define CMD_LED_COLOR 0x05
36 #define CMD_LED_SET_BRIGHTNESS 0x07
37 #define CMD_LED_GET_BRIGHTNESS 0x08
39 #define CMD_SET_GAMMA_CORRECTION 0x30
40 #define CMD_GET_GAMMA_CORRECTION 0x31
43 struct led_classdev_mc mc_cdev
;
44 struct mc_subled subled_info
[OMNIA_LED_NUM_CHANNELS
];
45 u8 cached_channels
[OMNIA_LED_NUM_CHANNELS
];
50 #define to_omnia_led(l) container_of(l, struct omnia_led, mc_cdev)
53 struct i2c_client
*client
;
55 bool has_gamma_correction
;
56 struct omnia_led leds
[];
59 static int omnia_cmd_write_u8(const struct i2c_client
*client
, u8 cmd
, u8 val
)
61 u8 buf
[2] = { cmd
, val
};
64 ret
= i2c_master_send(client
, buf
, sizeof(buf
));
66 return ret
< 0 ? ret
: 0;
69 static int omnia_cmd_read_raw(struct i2c_adapter
*adapter
, u8 addr
, u8 cmd
,
70 void *reply
, size_t len
)
72 struct i2c_msg msgs
[2];
80 msgs
[1].flags
= I2C_M_RD
;
84 ret
= i2c_transfer(adapter
, msgs
, ARRAY_SIZE(msgs
));
85 if (likely(ret
== ARRAY_SIZE(msgs
)))
93 static int omnia_cmd_read_u8(const struct i2c_client
*client
, u8 cmd
)
98 err
= omnia_cmd_read_raw(client
->adapter
, client
->addr
, cmd
, &reply
, 1);
105 static int omnia_led_send_color_cmd(const struct i2c_client
*client
,
106 struct omnia_led
*led
)
111 cmd
[0] = CMD_LED_COLOR
;
113 cmd
[2] = led
->subled_info
[0].brightness
;
114 cmd
[3] = led
->subled_info
[1].brightness
;
115 cmd
[4] = led
->subled_info
[2].brightness
;
117 /* Send the color change command */
118 ret
= i2c_master_send(client
, cmd
, 5);
122 /* Cache the RGB channel brightnesses */
123 for (int i
= 0; i
< OMNIA_LED_NUM_CHANNELS
; ++i
)
124 led
->cached_channels
[i
] = led
->subled_info
[i
].brightness
;
129 /* Determine if the computed RGB channels are different from the cached ones */
130 static bool omnia_led_channels_changed(struct omnia_led
*led
)
132 for (int i
= 0; i
< OMNIA_LED_NUM_CHANNELS
; ++i
)
133 if (led
->subled_info
[i
].brightness
!= led
->cached_channels
[i
])
139 static int omnia_led_brightness_set_blocking(struct led_classdev
*cdev
,
140 enum led_brightness brightness
)
142 struct led_classdev_mc
*mc_cdev
= lcdev_to_mccdev(cdev
);
143 struct omnia_leds
*leds
= dev_get_drvdata(cdev
->dev
->parent
);
144 struct omnia_led
*led
= to_omnia_led(mc_cdev
);
147 mutex_lock(&leds
->lock
);
150 * Only recalculate RGB brightnesses from intensities if brightness is
151 * non-zero (if it is zero and the LED is in HW blinking mode, we use
152 * max_brightness as brightness). Otherwise we won't be using them and
153 * we can save ourselves some software divisions (Omnia's CPU does not
154 * implement the division instruction).
156 if (brightness
|| led
->hwtrig
) {
157 led_mc_calc_color_components(mc_cdev
, brightness
?:
158 cdev
->max_brightness
);
161 * Send color command only if brightness is non-zero and the RGB
162 * channel brightnesses changed.
164 if (omnia_led_channels_changed(led
))
165 err
= omnia_led_send_color_cmd(leds
->client
, led
);
169 * Send on/off state change only if (bool)brightness changed and the LED
170 * is not being blinked by HW.
172 if (!err
&& !led
->hwtrig
&& !brightness
!= !led
->on
) {
173 u8 state
= CMD_LED_STATE_LED(led
->reg
);
176 state
|= CMD_LED_STATE_ON
;
178 err
= omnia_cmd_write_u8(leds
->client
, CMD_LED_STATE
, state
);
180 led
->on
= !!brightness
;
183 mutex_unlock(&leds
->lock
);
188 static struct led_hw_trigger_type omnia_hw_trigger_type
;
190 static int omnia_hwtrig_activate(struct led_classdev
*cdev
)
192 struct led_classdev_mc
*mc_cdev
= lcdev_to_mccdev(cdev
);
193 struct omnia_leds
*leds
= dev_get_drvdata(cdev
->dev
->parent
);
194 struct omnia_led
*led
= to_omnia_led(mc_cdev
);
197 mutex_lock(&leds
->lock
);
201 * If the LED is off (brightness was set to 0), the last
202 * configured color was not necessarily sent to the MCU.
203 * Recompute with max_brightness and send if needed.
205 led_mc_calc_color_components(mc_cdev
, cdev
->max_brightness
);
207 if (omnia_led_channels_changed(led
))
208 err
= omnia_led_send_color_cmd(leds
->client
, led
);
212 /* Put the LED into MCU controlled mode */
213 err
= omnia_cmd_write_u8(leds
->client
, CMD_LED_MODE
,
214 CMD_LED_MODE_LED(led
->reg
));
219 mutex_unlock(&leds
->lock
);
224 static void omnia_hwtrig_deactivate(struct led_classdev
*cdev
)
226 struct omnia_leds
*leds
= dev_get_drvdata(cdev
->dev
->parent
);
227 struct omnia_led
*led
= to_omnia_led(lcdev_to_mccdev(cdev
));
230 mutex_lock(&leds
->lock
);
234 /* Put the LED into software mode */
235 err
= omnia_cmd_write_u8(leds
->client
, CMD_LED_MODE
,
236 CMD_LED_MODE_LED(led
->reg
) |
239 mutex_unlock(&leds
->lock
);
242 dev_err(cdev
->dev
, "Cannot put LED to software mode: %i\n",
246 static struct led_trigger omnia_hw_trigger
= {
248 .activate
= omnia_hwtrig_activate
,
249 .deactivate
= omnia_hwtrig_deactivate
,
250 .trigger_type
= &omnia_hw_trigger_type
,
253 static int omnia_led_register(struct i2c_client
*client
, struct omnia_led
*led
,
254 struct device_node
*np
)
256 struct led_init_data init_data
= {};
257 struct device
*dev
= &client
->dev
;
258 struct led_classdev
*cdev
;
261 ret
= of_property_read_u32(np
, "reg", &led
->reg
);
262 if (ret
|| led
->reg
>= OMNIA_BOARD_LEDS
) {
264 "Node %pOF: must contain 'reg' property with values between 0 and %i\n",
265 np
, OMNIA_BOARD_LEDS
- 1);
269 ret
= of_property_read_u32(np
, "color", &color
);
270 if (ret
|| color
!= LED_COLOR_ID_RGB
) {
272 "Node %pOF: must contain 'color' property with value LED_COLOR_ID_RGB\n",
277 led
->subled_info
[0].color_index
= LED_COLOR_ID_RED
;
278 led
->subled_info
[1].color_index
= LED_COLOR_ID_GREEN
;
279 led
->subled_info
[2].color_index
= LED_COLOR_ID_BLUE
;
281 /* Initial color is white */
282 for (int i
= 0; i
< OMNIA_LED_NUM_CHANNELS
; ++i
) {
283 led
->subled_info
[i
].intensity
= 255;
284 led
->subled_info
[i
].brightness
= 255;
285 led
->subled_info
[i
].channel
= i
;
288 led
->mc_cdev
.subled_info
= led
->subled_info
;
289 led
->mc_cdev
.num_colors
= OMNIA_LED_NUM_CHANNELS
;
291 init_data
.fwnode
= &np
->fwnode
;
293 cdev
= &led
->mc_cdev
.led_cdev
;
294 cdev
->max_brightness
= 255;
295 cdev
->brightness_set_blocking
= omnia_led_brightness_set_blocking
;
296 cdev
->trigger_type
= &omnia_hw_trigger_type
;
298 * Use the omnia-mcu trigger as the default trigger. It may be rewritten
299 * by LED class from the linux,default-trigger property.
301 cdev
->default_trigger
= omnia_hw_trigger
.name
;
303 /* put the LED into software mode */
304 ret
= omnia_cmd_write_u8(client
, CMD_LED_MODE
,
305 CMD_LED_MODE_LED(led
->reg
) |
308 dev_err(dev
, "Cannot set LED %pOF to software mode: %i\n", np
,
313 /* disable the LED */
314 ret
= omnia_cmd_write_u8(client
, CMD_LED_STATE
,
315 CMD_LED_STATE_LED(led
->reg
));
317 dev_err(dev
, "Cannot set LED %pOF brightness: %i\n", np
, ret
);
321 /* Set initial color and cache it */
322 ret
= omnia_led_send_color_cmd(client
, led
);
324 dev_err(dev
, "Cannot set LED %pOF initial color: %i\n", np
,
329 ret
= devm_led_classdev_multicolor_register_ext(dev
, &led
->mc_cdev
,
332 dev_err(dev
, "Cannot register LED %pOF: %i\n", np
, ret
);
340 * On the front panel of the Turris Omnia router there is also a button which
341 * can be used to control the intensity of all the LEDs at once, so that if they
342 * are too bright, user can dim them.
343 * The microcontroller cycles between 8 levels of this global brightness (from
344 * 100% to 0%), but this setting can have any integer value between 0 and 100.
345 * It is therefore convenient to be able to change this setting from software.
346 * We expose this setting via a sysfs attribute file called "brightness". This
347 * file lives in the device directory of the LED controller, not an individual
348 * LED, so it should not confuse users.
350 static ssize_t
brightness_show(struct device
*dev
, struct device_attribute
*a
,
353 struct i2c_client
*client
= to_i2c_client(dev
);
356 ret
= omnia_cmd_read_u8(client
, CMD_LED_GET_BRIGHTNESS
);
361 return sysfs_emit(buf
, "%d\n", ret
);
364 static ssize_t
brightness_store(struct device
*dev
, struct device_attribute
*a
,
365 const char *buf
, size_t count
)
367 struct i2c_client
*client
= to_i2c_client(dev
);
368 unsigned long brightness
;
371 if (kstrtoul(buf
, 10, &brightness
))
374 if (brightness
> 100)
377 err
= omnia_cmd_write_u8(client
, CMD_LED_SET_BRIGHTNESS
, brightness
);
381 static DEVICE_ATTR_RW(brightness
);
383 static ssize_t
gamma_correction_show(struct device
*dev
,
384 struct device_attribute
*a
, char *buf
)
386 struct i2c_client
*client
= to_i2c_client(dev
);
387 struct omnia_leds
*leds
= i2c_get_clientdata(client
);
390 if (leds
->has_gamma_correction
) {
391 ret
= omnia_cmd_read_u8(client
, CMD_GET_GAMMA_CORRECTION
);
398 return sysfs_emit(buf
, "%d\n", !!ret
);
401 static ssize_t
gamma_correction_store(struct device
*dev
,
402 struct device_attribute
*a
,
403 const char *buf
, size_t count
)
405 struct i2c_client
*client
= to_i2c_client(dev
);
406 struct omnia_leds
*leds
= i2c_get_clientdata(client
);
410 if (!leds
->has_gamma_correction
)
413 if (kstrtobool(buf
, &val
) < 0)
416 err
= omnia_cmd_write_u8(client
, CMD_SET_GAMMA_CORRECTION
, val
);
420 static DEVICE_ATTR_RW(gamma_correction
);
422 static struct attribute
*omnia_led_controller_attrs
[] = {
423 &dev_attr_brightness
.attr
,
424 &dev_attr_gamma_correction
.attr
,
427 ATTRIBUTE_GROUPS(omnia_led_controller
);
429 static int omnia_mcu_get_features(const struct i2c_client
*client
)
434 err
= omnia_cmd_read_raw(client
->adapter
, OMNIA_MCU_I2C_ADDR
,
435 CMD_GET_STATUS_WORD
, &reply
, sizeof(reply
));
439 /* Check whether MCU firmware supports the CMD_GET_FEAUTRES command */
440 if (!(le16_to_cpu(reply
) & STS_FEATURES_SUPPORTED
))
443 err
= omnia_cmd_read_raw(client
->adapter
, OMNIA_MCU_I2C_ADDR
,
444 CMD_GET_FEATURES
, &reply
, sizeof(reply
));
448 return le16_to_cpu(reply
);
451 static int omnia_leds_probe(struct i2c_client
*client
)
453 struct device
*dev
= &client
->dev
;
454 struct device_node
*np
= dev_of_node(dev
);
455 struct omnia_leds
*leds
;
456 struct omnia_led
*led
;
459 count
= of_get_available_child_count(np
);
461 dev_err(dev
, "LEDs are not defined in device tree!\n");
463 } else if (count
> OMNIA_BOARD_LEDS
) {
464 dev_err(dev
, "Too many LEDs defined in device tree!\n");
468 leds
= devm_kzalloc(dev
, struct_size(leds
, leds
, count
), GFP_KERNEL
);
472 leds
->client
= client
;
473 i2c_set_clientdata(client
, leds
);
475 ret
= omnia_mcu_get_features(client
);
477 dev_err(dev
, "Cannot determine MCU supported features: %d\n",
482 leds
->has_gamma_correction
= ret
& FEAT_LED_GAMMA_CORRECTION
;
483 if (!leds
->has_gamma_correction
) {
485 "Your board's MCU firmware does not support the LED gamma correction feature.\n");
487 "Consider upgrading MCU firmware with the omnia-mcutool utility.\n");
490 mutex_init(&leds
->lock
);
492 ret
= devm_led_trigger_register(dev
, &omnia_hw_trigger
);
494 dev_err(dev
, "Cannot register private LED trigger: %d\n", ret
);
498 led
= &leds
->leds
[0];
499 for_each_available_child_of_node_scoped(np
, child
) {
500 ret
= omnia_led_register(client
, led
, child
);
510 static void omnia_leds_remove(struct i2c_client
*client
)
514 /* put all LEDs into default (HW triggered) mode */
515 omnia_cmd_write_u8(client
, CMD_LED_MODE
,
516 CMD_LED_MODE_LED(OMNIA_BOARD_LEDS
));
518 /* set all LEDs color to [255, 255, 255] */
519 buf
[0] = CMD_LED_COLOR
;
520 buf
[1] = OMNIA_BOARD_LEDS
;
525 i2c_master_send(client
, buf
, 5);
528 static const struct of_device_id of_omnia_leds_match
[] = {
529 { .compatible
= "cznic,turris-omnia-leds", },
532 MODULE_DEVICE_TABLE(of
, of_omnia_leds_match
);
534 static const struct i2c_device_id omnia_id
[] = {
538 MODULE_DEVICE_TABLE(i2c
, omnia_id
);
540 static struct i2c_driver omnia_leds_driver
= {
541 .probe
= omnia_leds_probe
,
542 .remove
= omnia_leds_remove
,
543 .id_table
= omnia_id
,
545 .name
= "leds-turris-omnia",
546 .of_match_table
= of_omnia_leds_match
,
547 .dev_groups
= omnia_led_controller_groups
,
551 module_i2c_driver(omnia_leds_driver
);
553 MODULE_AUTHOR("Marek Behun <kabel@kernel.org>");
554 MODULE_DESCRIPTION("CZ.NIC's Turris Omnia LEDs");
555 MODULE_LICENSE("GPL v2");