557 lines · c
1// SPDX-License-Identifier: GPL-2.02/*3 * CZ.NIC's Turris Omnia LEDs driver4 *5 * 2020, 2023 by Marek Behún <kabel@kernel.org>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 15#define OMNIA_BOARD_LEDS 1216#define OMNIA_LED_NUM_CHANNELS 317 18/* MCU controller commands at I2C address 0x2a */19#define OMNIA_MCU_I2C_ADDR 0x2a20 21#define CMD_GET_STATUS_WORD 0x0122#define STS_FEATURES_SUPPORTED BIT(2)23 24#define CMD_GET_FEATURES 0x1025#define FEAT_LED_GAMMA_CORRECTION BIT(5)26 27/* LED controller commands at I2C address 0x2b */28#define CMD_LED_MODE 0x0329#define CMD_LED_MODE_LED(l) ((l) & 0x0f)30#define CMD_LED_MODE_USER 0x1031 32#define CMD_LED_STATE 0x0433#define CMD_LED_STATE_LED(l) ((l) & 0x0f)34#define CMD_LED_STATE_ON 0x1035 36#define CMD_LED_COLOR 0x0537#define CMD_LED_SET_BRIGHTNESS 0x0738#define CMD_LED_GET_BRIGHTNESS 0x0839 40#define CMD_SET_GAMMA_CORRECTION 0x3041#define CMD_GET_GAMMA_CORRECTION 0x3142 43struct omnia_led {44 struct led_classdev_mc mc_cdev;45 struct mc_subled subled_info[OMNIA_LED_NUM_CHANNELS];46 u8 cached_channels[OMNIA_LED_NUM_CHANNELS];47 bool on, hwtrig;48 int reg;49};50 51#define to_omnia_led(l) container_of(l, struct omnia_led, mc_cdev)52 53struct omnia_leds {54 struct i2c_client *client;55 struct mutex lock;56 bool has_gamma_correction;57 struct omnia_led leds[];58};59 60static int omnia_cmd_write_u8(const struct i2c_client *client, u8 cmd, u8 val)61{62 u8 buf[2] = { cmd, val };63 int ret;64 65 ret = i2c_master_send(client, buf, sizeof(buf));66 67 return ret < 0 ? ret : 0;68}69 70static int omnia_cmd_read_raw(struct i2c_adapter *adapter, u8 addr, u8 cmd,71 void *reply, size_t len)72{73 struct i2c_msg msgs[2];74 int ret;75 76 msgs[0].addr = addr;77 msgs[0].flags = 0;78 msgs[0].len = 1;79 msgs[0].buf = &cmd;80 msgs[1].addr = addr;81 msgs[1].flags = I2C_M_RD;82 msgs[1].len = len;83 msgs[1].buf = reply;84 85 ret = i2c_transfer(adapter, msgs, ARRAY_SIZE(msgs));86 if (likely(ret == ARRAY_SIZE(msgs)))87 return 0;88 else if (ret < 0)89 return ret;90 else91 return -EIO;92}93 94static int omnia_cmd_read_u8(const struct i2c_client *client, u8 cmd)95{96 u8 reply;97 int err;98 99 err = omnia_cmd_read_raw(client->adapter, client->addr, cmd, &reply, 1);100 if (err)101 return err;102 103 return reply;104}105 106static int omnia_led_send_color_cmd(const struct i2c_client *client,107 struct omnia_led *led)108{109 char cmd[5];110 int ret;111 112 cmd[0] = CMD_LED_COLOR;113 cmd[1] = led->reg;114 cmd[2] = led->subled_info[0].brightness;115 cmd[3] = led->subled_info[1].brightness;116 cmd[4] = led->subled_info[2].brightness;117 118 /* Send the color change command */119 ret = i2c_master_send(client, cmd, 5);120 if (ret < 0)121 return ret;122 123 /* Cache the RGB channel brightnesses */124 for (int i = 0; i < OMNIA_LED_NUM_CHANNELS; ++i)125 led->cached_channels[i] = led->subled_info[i].brightness;126 127 return 0;128}129 130/* Determine if the computed RGB channels are different from the cached ones */131static bool omnia_led_channels_changed(struct omnia_led *led)132{133 for (int i = 0; i < OMNIA_LED_NUM_CHANNELS; ++i)134 if (led->subled_info[i].brightness != led->cached_channels[i])135 return true;136 137 return false;138}139 140static int omnia_led_brightness_set_blocking(struct led_classdev *cdev,141 enum led_brightness brightness)142{143 struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev);144 struct omnia_leds *leds = dev_get_drvdata(cdev->dev->parent);145 struct omnia_led *led = to_omnia_led(mc_cdev);146 int err = 0;147 148 mutex_lock(&leds->lock);149 150 /*151 * Only recalculate RGB brightnesses from intensities if brightness is152 * non-zero (if it is zero and the LED is in HW blinking mode, we use153 * max_brightness as brightness). Otherwise we won't be using them and154 * we can save ourselves some software divisions (Omnia's CPU does not155 * implement the division instruction).156 */157 if (brightness || led->hwtrig) {158 led_mc_calc_color_components(mc_cdev, brightness ?:159 cdev->max_brightness);160 161 /*162 * Send color command only if brightness is non-zero and the RGB163 * channel brightnesses changed.164 */165 if (omnia_led_channels_changed(led))166 err = omnia_led_send_color_cmd(leds->client, led);167 }168 169 /*170 * Send on/off state change only if (bool)brightness changed and the LED171 * is not being blinked by HW.172 */173 if (!err && !led->hwtrig && !brightness != !led->on) {174 u8 state = CMD_LED_STATE_LED(led->reg);175 176 if (brightness)177 state |= CMD_LED_STATE_ON;178 179 err = omnia_cmd_write_u8(leds->client, CMD_LED_STATE, state);180 if (!err)181 led->on = !!brightness;182 }183 184 mutex_unlock(&leds->lock);185 186 return err;187}188 189static struct led_hw_trigger_type omnia_hw_trigger_type;190 191static int omnia_hwtrig_activate(struct led_classdev *cdev)192{193 struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev);194 struct omnia_leds *leds = dev_get_drvdata(cdev->dev->parent);195 struct omnia_led *led = to_omnia_led(mc_cdev);196 int err = 0;197 198 mutex_lock(&leds->lock);199 200 if (!led->on) {201 /*202 * If the LED is off (brightness was set to 0), the last203 * configured color was not necessarily sent to the MCU.204 * Recompute with max_brightness and send if needed.205 */206 led_mc_calc_color_components(mc_cdev, cdev->max_brightness);207 208 if (omnia_led_channels_changed(led))209 err = omnia_led_send_color_cmd(leds->client, led);210 }211 212 if (!err) {213 /* Put the LED into MCU controlled mode */214 err = omnia_cmd_write_u8(leds->client, CMD_LED_MODE,215 CMD_LED_MODE_LED(led->reg));216 if (!err)217 led->hwtrig = true;218 }219 220 mutex_unlock(&leds->lock);221 222 return err;223}224 225static void omnia_hwtrig_deactivate(struct led_classdev *cdev)226{227 struct omnia_leds *leds = dev_get_drvdata(cdev->dev->parent);228 struct omnia_led *led = to_omnia_led(lcdev_to_mccdev(cdev));229 int err;230 231 mutex_lock(&leds->lock);232 233 led->hwtrig = false;234 235 /* Put the LED into software mode */236 err = omnia_cmd_write_u8(leds->client, CMD_LED_MODE,237 CMD_LED_MODE_LED(led->reg) |238 CMD_LED_MODE_USER);239 240 mutex_unlock(&leds->lock);241 242 if (err)243 dev_err(cdev->dev, "Cannot put LED to software mode: %i\n",244 err);245}246 247static struct led_trigger omnia_hw_trigger = {248 .name = "omnia-mcu",249 .activate = omnia_hwtrig_activate,250 .deactivate = omnia_hwtrig_deactivate,251 .trigger_type = &omnia_hw_trigger_type,252};253 254static int omnia_led_register(struct i2c_client *client, struct omnia_led *led,255 struct device_node *np)256{257 struct led_init_data init_data = {};258 struct device *dev = &client->dev;259 struct led_classdev *cdev;260 int ret, color;261 262 ret = of_property_read_u32(np, "reg", &led->reg);263 if (ret || led->reg >= OMNIA_BOARD_LEDS) {264 dev_warn(dev,265 "Node %pOF: must contain 'reg' property with values between 0 and %i\n",266 np, OMNIA_BOARD_LEDS - 1);267 return 0;268 }269 270 ret = of_property_read_u32(np, "color", &color);271 if (ret || color != LED_COLOR_ID_RGB) {272 dev_warn(dev,273 "Node %pOF: must contain 'color' property with value LED_COLOR_ID_RGB\n",274 np);275 return 0;276 }277 278 led->subled_info[0].color_index = LED_COLOR_ID_RED;279 led->subled_info[1].color_index = LED_COLOR_ID_GREEN;280 led->subled_info[2].color_index = LED_COLOR_ID_BLUE;281 282 /* Initial color is white */283 for (int i = 0; i < OMNIA_LED_NUM_CHANNELS; ++i) {284 led->subled_info[i].intensity = 255;285 led->subled_info[i].brightness = 255;286 led->subled_info[i].channel = i;287 }288 289 led->mc_cdev.subled_info = led->subled_info;290 led->mc_cdev.num_colors = OMNIA_LED_NUM_CHANNELS;291 292 init_data.fwnode = &np->fwnode;293 294 cdev = &led->mc_cdev.led_cdev;295 cdev->max_brightness = 255;296 cdev->brightness_set_blocking = omnia_led_brightness_set_blocking;297 cdev->trigger_type = &omnia_hw_trigger_type;298 /*299 * Use the omnia-mcu trigger as the default trigger. It may be rewritten300 * by LED class from the linux,default-trigger property.301 */302 cdev->default_trigger = omnia_hw_trigger.name;303 304 /* put the LED into software mode */305 ret = omnia_cmd_write_u8(client, CMD_LED_MODE,306 CMD_LED_MODE_LED(led->reg) |307 CMD_LED_MODE_USER);308 if (ret) {309 dev_err(dev, "Cannot set LED %pOF to software mode: %i\n", np,310 ret);311 return ret;312 }313 314 /* disable the LED */315 ret = omnia_cmd_write_u8(client, CMD_LED_STATE,316 CMD_LED_STATE_LED(led->reg));317 if (ret) {318 dev_err(dev, "Cannot set LED %pOF brightness: %i\n", np, ret);319 return ret;320 }321 322 /* Set initial color and cache it */323 ret = omnia_led_send_color_cmd(client, led);324 if (ret < 0) {325 dev_err(dev, "Cannot set LED %pOF initial color: %i\n", np,326 ret);327 return ret;328 }329 330 ret = devm_led_classdev_multicolor_register_ext(dev, &led->mc_cdev,331 &init_data);332 if (ret < 0) {333 dev_err(dev, "Cannot register LED %pOF: %i\n", np, ret);334 return ret;335 }336 337 return 1;338}339 340/*341 * On the front panel of the Turris Omnia router there is also a button which342 * can be used to control the intensity of all the LEDs at once, so that if they343 * are too bright, user can dim them.344 * The microcontroller cycles between 8 levels of this global brightness (from345 * 100% to 0%), but this setting can have any integer value between 0 and 100.346 * It is therefore convenient to be able to change this setting from software.347 * We expose this setting via a sysfs attribute file called "brightness". This348 * file lives in the device directory of the LED controller, not an individual349 * LED, so it should not confuse users.350 */351static ssize_t brightness_show(struct device *dev, struct device_attribute *a,352 char *buf)353{354 struct i2c_client *client = to_i2c_client(dev);355 int ret;356 357 ret = omnia_cmd_read_u8(client, CMD_LED_GET_BRIGHTNESS);358 359 if (ret < 0)360 return ret;361 362 return sysfs_emit(buf, "%d\n", ret);363}364 365static ssize_t brightness_store(struct device *dev, struct device_attribute *a,366 const char *buf, size_t count)367{368 struct i2c_client *client = to_i2c_client(dev);369 unsigned long brightness;370 int err;371 372 if (kstrtoul(buf, 10, &brightness))373 return -EINVAL;374 375 if (brightness > 100)376 return -EINVAL;377 378 err = omnia_cmd_write_u8(client, CMD_LED_SET_BRIGHTNESS, brightness);379 380 return err ?: count;381}382static DEVICE_ATTR_RW(brightness);383 384static ssize_t gamma_correction_show(struct device *dev,385 struct device_attribute *a, char *buf)386{387 struct i2c_client *client = to_i2c_client(dev);388 struct omnia_leds *leds = i2c_get_clientdata(client);389 int ret;390 391 if (leds->has_gamma_correction) {392 ret = omnia_cmd_read_u8(client, CMD_GET_GAMMA_CORRECTION);393 if (ret < 0)394 return ret;395 } else {396 ret = 0;397 }398 399 return sysfs_emit(buf, "%d\n", !!ret);400}401 402static ssize_t gamma_correction_store(struct device *dev,403 struct device_attribute *a,404 const char *buf, size_t count)405{406 struct i2c_client *client = to_i2c_client(dev);407 struct omnia_leds *leds = i2c_get_clientdata(client);408 bool val;409 int err;410 411 if (!leds->has_gamma_correction)412 return -EOPNOTSUPP;413 414 if (kstrtobool(buf, &val) < 0)415 return -EINVAL;416 417 err = omnia_cmd_write_u8(client, CMD_SET_GAMMA_CORRECTION, val);418 419 return err ?: count;420}421static DEVICE_ATTR_RW(gamma_correction);422 423static struct attribute *omnia_led_controller_attrs[] = {424 &dev_attr_brightness.attr,425 &dev_attr_gamma_correction.attr,426 NULL,427};428ATTRIBUTE_GROUPS(omnia_led_controller);429 430static int omnia_mcu_get_features(const struct i2c_client *client)431{432 u16 reply;433 int err;434 435 err = omnia_cmd_read_raw(client->adapter, OMNIA_MCU_I2C_ADDR,436 CMD_GET_STATUS_WORD, &reply, sizeof(reply));437 if (err)438 return err;439 440 /* Check whether MCU firmware supports the CMD_GET_FEAUTRES command */441 if (!(le16_to_cpu(reply) & STS_FEATURES_SUPPORTED))442 return 0;443 444 err = omnia_cmd_read_raw(client->adapter, OMNIA_MCU_I2C_ADDR,445 CMD_GET_FEATURES, &reply, sizeof(reply));446 if (err)447 return err;448 449 return le16_to_cpu(reply);450}451 452static int omnia_leds_probe(struct i2c_client *client)453{454 struct device *dev = &client->dev;455 struct device_node *np = dev_of_node(dev);456 struct omnia_leds *leds;457 struct omnia_led *led;458 int ret, count;459 460 count = of_get_available_child_count(np);461 if (!count) {462 dev_err(dev, "LEDs are not defined in device tree!\n");463 return -ENODEV;464 } else if (count > OMNIA_BOARD_LEDS) {465 dev_err(dev, "Too many LEDs defined in device tree!\n");466 return -EINVAL;467 }468 469 leds = devm_kzalloc(dev, struct_size(leds, leds, count), GFP_KERNEL);470 if (!leds)471 return -ENOMEM;472 473 leds->client = client;474 i2c_set_clientdata(client, leds);475 476 ret = omnia_mcu_get_features(client);477 if (ret < 0) {478 dev_err(dev, "Cannot determine MCU supported features: %d\n",479 ret);480 return ret;481 }482 483 leds->has_gamma_correction = ret & FEAT_LED_GAMMA_CORRECTION;484 if (!leds->has_gamma_correction) {485 dev_info(dev,486 "Your board's MCU firmware does not support the LED gamma correction feature.\n");487 dev_info(dev,488 "Consider upgrading MCU firmware with the omnia-mcutool utility.\n");489 }490 491 mutex_init(&leds->lock);492 493 ret = devm_led_trigger_register(dev, &omnia_hw_trigger);494 if (ret < 0) {495 dev_err(dev, "Cannot register private LED trigger: %d\n", ret);496 return ret;497 }498 499 led = &leds->leds[0];500 for_each_available_child_of_node_scoped(np, child) {501 ret = omnia_led_register(client, led, child);502 if (ret < 0)503 return ret;504 505 led += ret;506 }507 508 return 0;509}510 511static void omnia_leds_remove(struct i2c_client *client)512{513 u8 buf[5];514 515 /* put all LEDs into default (HW triggered) mode */516 omnia_cmd_write_u8(client, CMD_LED_MODE,517 CMD_LED_MODE_LED(OMNIA_BOARD_LEDS));518 519 /* set all LEDs color to [255, 255, 255] */520 buf[0] = CMD_LED_COLOR;521 buf[1] = OMNIA_BOARD_LEDS;522 buf[2] = 255;523 buf[3] = 255;524 buf[4] = 255;525 526 i2c_master_send(client, buf, 5);527}528 529static const struct of_device_id of_omnia_leds_match[] = {530 { .compatible = "cznic,turris-omnia-leds", },531 {},532};533MODULE_DEVICE_TABLE(of, of_omnia_leds_match);534 535static const struct i2c_device_id omnia_id[] = {536 { "omnia" },537 { }538};539MODULE_DEVICE_TABLE(i2c, omnia_id);540 541static struct i2c_driver omnia_leds_driver = {542 .probe = omnia_leds_probe,543 .remove = omnia_leds_remove,544 .id_table = omnia_id,545 .driver = {546 .name = "leds-turris-omnia",547 .of_match_table = of_omnia_leds_match,548 .dev_groups = omnia_led_controller_groups,549 },550};551 552module_i2c_driver(omnia_leds_driver);553 554MODULE_AUTHOR("Marek Behun <kabel@kernel.org>");555MODULE_DESCRIPTION("CZ.NIC's Turris Omnia LEDs");556MODULE_LICENSE("GPL v2");557