brintos

brintos / linux-shallow public Read only

0
0
Text · 13.8 KiB · 4cff8c4 Raw
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