Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 1 | // SPDX-License-Identifier: GPL-2.0 |
| 2 | /* |
| 3 | * CZ.NIC's Turris Omnia LEDs driver |
| 4 | * |
Marek Behún | b37c384 | 2021-04-09 13:27:04 -0700 | [diff] [blame] | 5 | * 2020 by Marek Behún <kabel@kernel.org> |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 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 | |
Marek Behún | 5d47ce1 | 2020-10-30 03:39:03 +0100 | [diff] [blame] | 15 | #define OMNIA_BOARD_LEDS 12 |
| 16 | #define OMNIA_LED_NUM_CHANNELS 3 |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 17 | |
Marek Behún | 5d47ce1 | 2020-10-30 03:39:03 +0100 | [diff] [blame] | 18 | #define CMD_LED_MODE 3 |
| 19 | #define CMD_LED_MODE_LED(l) ((l) & 0x0f) |
| 20 | #define CMD_LED_MODE_USER 0x10 |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 21 | |
Marek Behún | 5d47ce1 | 2020-10-30 03:39:03 +0100 | [diff] [blame] | 22 | #define CMD_LED_STATE 4 |
| 23 | #define CMD_LED_STATE_LED(l) ((l) & 0x0f) |
| 24 | #define CMD_LED_STATE_ON 0x10 |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 25 | |
Marek Behún | 5d47ce1 | 2020-10-30 03:39:03 +0100 | [diff] [blame] | 26 | #define CMD_LED_COLOR 5 |
| 27 | #define CMD_LED_SET_BRIGHTNESS 7 |
| 28 | #define CMD_LED_GET_BRIGHTNESS 8 |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 29 | |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 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 | |
Marek Behún | 5d47ce1 | 2020-10-30 03:39:03 +0100 | [diff] [blame] | 36 | #define to_omnia_led(l) container_of(l, struct omnia_led, mc_cdev) |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 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); |
Marek Behún | 493d2e4 | 2020-10-30 03:39:02 +0100 | [diff] [blame] | 50 | u8 buf[5], state; |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 51 | int ret; |
| 52 | |
| 53 | mutex_lock(&leds->lock); |
| 54 | |
| 55 | led_mc_calc_color_components(&led->mc_cdev, brightness); |
| 56 | |
Marek Behún | 493d2e4 | 2020-10-30 03:39:02 +0100 | [diff] [blame] | 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; |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 62 | |
| 63 | state = CMD_LED_STATE_LED(led->reg); |
Marek Behún | 493d2e4 | 2020-10-30 03:39:02 +0100 | [diff] [blame] | 64 | if (buf[2] || buf[3] || buf[4]) |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 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); |
Marek Behún | 98650b0 | 2020-10-30 03:39:06 +0100 | [diff] [blame] | 93 | if (ret || color != LED_COLOR_ID_RGB) { |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 94 | dev_warn(dev, |
Marek Behún | 98650b0 | 2020-10-30 03:39:06 +0100 | [diff] [blame] | 95 | "Node %pOF: must contain 'color' property with value LED_COLOR_ID_RGB\n", |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 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 | |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 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) { |
Marek Behún | 5d47ce1 | 2020-10-30 03:39:03 +0100 | [diff] [blame] | 121 | dev_err(dev, "Cannot set LED %pOF to software mode: %i\n", np, |
| 122 | ret); |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 123 | return ret; |
| 124 | } |
| 125 | |
| 126 | /* disable the LED */ |
Marek Behún | 5d47ce1 | 2020-10-30 03:39:03 +0100 | [diff] [blame] | 127 | ret = i2c_smbus_write_byte_data(client, CMD_LED_STATE, |
| 128 | CMD_LED_STATE_LED(led->reg)); |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 129 | if (ret < 0) { |
| 130 | dev_err(dev, "Cannot set LED %pOF brightness: %i\n", np, ret); |
| 131 | return ret; |
| 132 | } |
| 133 | |
Marek Behún | 5d47ce1 | 2020-10-30 03:39:03 +0100 | [diff] [blame] | 134 | ret = devm_led_classdev_multicolor_register_ext(dev, &led->mc_cdev, |
| 135 | &init_data); |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 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 | */ |
Marek Behún | 5d47ce1 | 2020-10-30 03:39:03 +0100 | [diff] [blame] | 155 | static ssize_t brightness_show(struct device *dev, struct device_attribute *a, |
| 156 | char *buf) |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 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 | |
Marek Behún | 5d47ce1 | 2020-10-30 03:39:03 +0100 | [diff] [blame] | 172 | static ssize_t brightness_store(struct device *dev, struct device_attribute *a, |
| 173 | const char *buf, size_t count) |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 174 | { |
| 175 | struct i2c_client *client = to_i2c_client(dev); |
| 176 | struct omnia_leds *leds = i2c_get_clientdata(client); |
Marek Behún | fca050b | 2020-10-30 03:39:04 +0100 | [diff] [blame] | 177 | unsigned long brightness; |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 178 | int ret; |
| 179 | |
Marek Behún | fca050b | 2020-10-30 03:39:04 +0100 | [diff] [blame] | 180 | if (kstrtoul(buf, 10, &brightness)) |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 181 | return -EINVAL; |
| 182 | |
| 183 | if (brightness > 100) |
| 184 | return -EINVAL; |
| 185 | |
| 186 | mutex_lock(&leds->lock); |
Marek Behún | 5d47ce1 | 2020-10-30 03:39:03 +0100 | [diff] [blame] | 187 | ret = i2c_smbus_write_byte_data(client, CMD_LED_SET_BRIGHTNESS, |
| 188 | (u8)brightness); |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 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; |
Marek Behún | 8853c95e9 | 2020-09-18 00:32:54 +0200 | [diff] [blame] | 208 | struct device_node *np = dev_of_node(dev), *child; |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 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); |
Marek Behún | 2c67756 | 2020-09-18 00:32:57 +0200 | [diff] [blame] | 234 | if (ret < 0) { |
| 235 | of_node_put(child); |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 236 | return ret; |
Marek Behún | 2c67756 | 2020-09-18 00:32:57 +0200 | [diff] [blame] | 237 | } |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 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 | { |
Marek Behún | 493d2e4 | 2020-10-30 03:39:02 +0100 | [diff] [blame] | 250 | u8 buf[5]; |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 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] */ |
Marek Behún | 493d2e4 | 2020-10-30 03:39:02 +0100 | [diff] [blame] | 257 | buf[0] = CMD_LED_COLOR; |
| 258 | buf[1] = OMNIA_BOARD_LEDS; |
| 259 | buf[2] = 255; |
| 260 | buf[3] = 255; |
| 261 | buf[4] = 255; |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 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 | }; |
Zou Wei | 9d0150db | 2021-05-12 14:49:18 +0800 | [diff] [blame] | 277 | MODULE_DEVICE_TABLE(i2c, omnia_id); |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 278 | |
| 279 | static struct i2c_driver omnia_leds_driver = { |
| 280 | .probe = omnia_leds_probe, |
| 281 | .remove = omnia_leds_remove, |
| 282 | .id_table = omnia_id, |
| 283 | .driver = { |
| 284 | .name = "leds-turris-omnia", |
| 285 | .of_match_table = of_omnia_leds_match, |
| 286 | }, |
| 287 | }; |
| 288 | |
| 289 | module_i2c_driver(omnia_leds_driver); |
| 290 | |
Marek Behún | b37c384 | 2021-04-09 13:27:04 -0700 | [diff] [blame] | 291 | MODULE_AUTHOR("Marek Behun <kabel@kernel.org>"); |
Marek Behún | 089381b | 2020-07-23 14:53:19 +0200 | [diff] [blame] | 292 | MODULE_DESCRIPTION("CZ.NIC's Turris Omnia LEDs"); |
| 293 | MODULE_LICENSE("GPL v2"); |