From 743cc37561188e222eff5200d4507eabcccf9d41 Mon Sep 17 00:00:00 2001 From: Julia Cartwright Date: Thu, 9 Mar 2017 10:21:59 -0600 Subject: i2c: mux: pca954x: make use of raw_spinlock variants The pca954x i2c mux driver currently implements an irq_chip for handling interrupts; due to how irq_chip handling is done, it's necessary for the irq_chip methods to be invoked from hardirq context, even on a a real-time kernel. Because the spinlock_t type becomes a "sleeping" spinlock w/ RT kernels, it is not suitable to be used with irq_chips. A quick audit of the operations under the lock reveal that they do only minimal, bounded work, and are therefore safe to do under a raw spinlock. Signed-off-by: Julia Cartwright Signed-off-by: Peter Rosin --- drivers/i2c/muxes/i2c-mux-pca954x.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'drivers/i2c/muxes') diff --git a/drivers/i2c/muxes/i2c-mux-pca954x.c b/drivers/i2c/muxes/i2c-mux-pca954x.c index dfc1c0e37c40..15dfc1648716 100644 --- a/drivers/i2c/muxes/i2c-mux-pca954x.c +++ b/drivers/i2c/muxes/i2c-mux-pca954x.c @@ -85,7 +85,7 @@ struct pca954x { struct irq_domain *irq; unsigned int irq_mask; - spinlock_t lock; + raw_spinlock_t lock; }; /* Provide specs for the PCA954x types we know about */ @@ -264,13 +264,13 @@ static void pca954x_irq_mask(struct irq_data *idata) unsigned int pos = idata->hwirq; unsigned long flags; - spin_lock_irqsave(&data->lock, flags); + raw_spin_lock_irqsave(&data->lock, flags); data->irq_mask &= ~BIT(pos); if (!data->irq_mask) disable_irq(data->client->irq); - spin_unlock_irqrestore(&data->lock, flags); + raw_spin_unlock_irqrestore(&data->lock, flags); } static void pca954x_irq_unmask(struct irq_data *idata) @@ -279,13 +279,13 @@ static void pca954x_irq_unmask(struct irq_data *idata) unsigned int pos = idata->hwirq; unsigned long flags; - spin_lock_irqsave(&data->lock, flags); + raw_spin_lock_irqsave(&data->lock, flags); if (!data->irq_mask) enable_irq(data->client->irq); data->irq_mask |= BIT(pos); - spin_unlock_irqrestore(&data->lock, flags); + raw_spin_unlock_irqrestore(&data->lock, flags); } static int pca954x_irq_set_type(struct irq_data *idata, unsigned int type) @@ -311,7 +311,7 @@ static int pca954x_irq_setup(struct i2c_mux_core *muxc) if (!data->chip->has_irq || client->irq <= 0) return 0; - spin_lock_init(&data->lock); + raw_spin_lock_init(&data->lock); data->irq = irq_domain_add_linear(client->dev.of_node, data->chip->nchans, -- cgit v1.2.3 From 1144d13eaaaec21a55614ca7ade2c8fc1d9f1685 Mon Sep 17 00:00:00 2001 From: Peter Rosin Date: Mon, 3 Apr 2017 10:14:06 +0200 Subject: i2c: arb: gpio-challenge: stop double error reporting i2c_mux_add_adapter already logs a message on failure. Reviewed-by: Wolfram Sang Signed-off-by: Peter Rosin --- drivers/i2c/muxes/i2c-arb-gpio-challenge.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'drivers/i2c/muxes') diff --git a/drivers/i2c/muxes/i2c-arb-gpio-challenge.c b/drivers/i2c/muxes/i2c-arb-gpio-challenge.c index 86fc2d4c081b..812b8cff265f 100644 --- a/drivers/i2c/muxes/i2c-arb-gpio-challenge.c +++ b/drivers/i2c/muxes/i2c-arb-gpio-challenge.c @@ -202,10 +202,8 @@ static int i2c_arbitrator_probe(struct platform_device *pdev) /* Actually add the mux adapter */ ret = i2c_mux_add_adapter(muxc, 0, 0, 0); - if (ret) { - dev_err(dev, "Failed to add adapter\n"); + if (ret) i2c_put_adapter(muxc->parent); - } return ret; } -- cgit v1.2.3 From 1b00900fa0bdd4a634f6864efe3d07c216523688 Mon Sep 17 00:00:00 2001 From: Peter Rosin Date: Mon, 3 Apr 2017 10:14:11 +0200 Subject: i2c: mux: gpio: stop double error reporting i2c_mux_add_adapter already logs a message on failure. Reviewed-by: Wolfram Sang Signed-off-by: Peter Rosin --- drivers/i2c/muxes/i2c-mux-gpio.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'drivers/i2c/muxes') diff --git a/drivers/i2c/muxes/i2c-mux-gpio.c b/drivers/i2c/muxes/i2c-mux-gpio.c index 655684d621a4..1a9973ede443 100644 --- a/drivers/i2c/muxes/i2c-mux-gpio.c +++ b/drivers/i2c/muxes/i2c-mux-gpio.c @@ -245,10 +245,8 @@ static int i2c_mux_gpio_probe(struct platform_device *pdev) unsigned int class = mux->data.classes ? mux->data.classes[i] : 0; ret = i2c_mux_add_adapter(muxc, nr, mux->data.values[i], class); - if (ret) { - dev_err(&pdev->dev, "Failed to add adapter %d\n", i); + if (ret) goto add_adapter_failed; - } } dev_info(&pdev->dev, "%d port mux on %s adapter\n", -- cgit v1.2.3 From 664d9bbadaf61c80a6e5c7c6c83f80a0731bc184 Mon Sep 17 00:00:00 2001 From: Peter Rosin Date: Mon, 3 Apr 2017 10:14:19 +0200 Subject: i2c: mux: pca9541: stop double error reporting i2c_mux_add_adapter already logs a message on failure. Reviewed-by: Wolfram Sang Signed-off-by: Peter Rosin --- drivers/i2c/muxes/i2c-mux-pca9541.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'drivers/i2c/muxes') diff --git a/drivers/i2c/muxes/i2c-mux-pca9541.c b/drivers/i2c/muxes/i2c-mux-pca9541.c index 77840f7845a1..9e318c9516c7 100644 --- a/drivers/i2c/muxes/i2c-mux-pca9541.c +++ b/drivers/i2c/muxes/i2c-mux-pca9541.c @@ -369,10 +369,8 @@ static int pca9541_probe(struct i2c_client *client, i2c_set_clientdata(client, muxc); ret = i2c_mux_add_adapter(muxc, force, 0, 0); - if (ret) { - dev_err(&client->dev, "failed to register master selector\n"); + if (ret) return ret; - } dev_info(&client->dev, "registered master selector for I2C %s\n", client->name); -- cgit v1.2.3 From 0756ac323573406662c56d3f26a20b4382451c96 Mon Sep 17 00:00:00 2001 From: Peter Rosin Date: Mon, 3 Apr 2017 10:14:22 +0200 Subject: i2c: mux: pca954x: stop double error reporting i2c_mux_add_adapter already logs a message on failure. Reviewed-by: Wolfram Sang Signed-off-by: Peter Rosin --- drivers/i2c/muxes/i2c-mux-pca954x.c | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) (limited to 'drivers/i2c/muxes') diff --git a/drivers/i2c/muxes/i2c-mux-pca954x.c b/drivers/i2c/muxes/i2c-mux-pca954x.c index 15dfc1648716..b2a85a2d00f7 100644 --- a/drivers/i2c/muxes/i2c-mux-pca954x.c +++ b/drivers/i2c/muxes/i2c-mux-pca954x.c @@ -434,13 +434,8 @@ static int pca954x_probe(struct i2c_client *client, idle_disconnect_dt) << num; ret = i2c_mux_add_adapter(muxc, force, num, class); - - if (ret) { - dev_err(&client->dev, - "failed to register multiplexed adapter" - " %d as bus %d\n", num, force); + if (ret) goto fail_del_adapters; - } } dev_info(&client->dev, -- cgit v1.2.3 From c99a23e55fbe97acb235e7ebbd8800870c3e689f Mon Sep 17 00:00:00 2001 From: Peter Rosin Date: Mon, 3 Apr 2017 10:14:25 +0200 Subject: i2c: mux: pinctrl: stop double error reporting i2c_mux_add_adapter already logs a message on failure. Reviewed-by: Wolfram Sang Signed-off-by: Peter Rosin --- drivers/i2c/muxes/i2c-mux-pinctrl.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'drivers/i2c/muxes') diff --git a/drivers/i2c/muxes/i2c-mux-pinctrl.c b/drivers/i2c/muxes/i2c-mux-pinctrl.c index 35bb775e1b74..7c0c264b07bc 100644 --- a/drivers/i2c/muxes/i2c-mux-pinctrl.c +++ b/drivers/i2c/muxes/i2c-mux-pinctrl.c @@ -245,10 +245,8 @@ static int i2c_mux_pinctrl_probe(struct platform_device *pdev) (mux->pdata->base_bus_num + i) : 0; ret = i2c_mux_add_adapter(muxc, bus, i, 0); - if (ret) { - dev_err(&pdev->dev, "Failed to add adapter %d\n", i); + if (ret) goto err_del_adapter; - } } return 0; -- cgit v1.2.3 From f089236114cb09594d7e27a15ed72c2ebb669448 Mon Sep 17 00:00:00 2001 From: Peter Rosin Date: Mon, 3 Apr 2017 10:14:29 +0200 Subject: i2c: mux: reg: stop double error reporting i2c_mux_add_adapter already logs a message on failure. Reviewed-by: Wolfram Sang Signed-off-by: Peter Rosin --- drivers/i2c/muxes/i2c-mux-reg.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'drivers/i2c/muxes') diff --git a/drivers/i2c/muxes/i2c-mux-reg.c b/drivers/i2c/muxes/i2c-mux-reg.c index c6a90b4a9c62..406d5059072c 100644 --- a/drivers/i2c/muxes/i2c-mux-reg.c +++ b/drivers/i2c/muxes/i2c-mux-reg.c @@ -222,10 +222,8 @@ static int i2c_mux_reg_probe(struct platform_device *pdev) class = mux->data.classes ? mux->data.classes[i] : 0; ret = i2c_mux_add_adapter(muxc, nr, mux->data.values[i], class); - if (ret) { - dev_err(&pdev->dev, "Failed to add adapter %d\n", i); + if (ret) goto add_adapter_failed; - } } dev_dbg(&pdev->dev, "%d port mux on %s adapter\n", -- cgit v1.2.3 From dbed8a803bd3fb64339a6180adaff2cec46242ce Mon Sep 17 00:00:00 2001 From: Michael Hennerich Date: Tue, 11 Apr 2017 14:16:16 +0200 Subject: i2c: mux: ltc4306: LTC4306 and LTC4305 I2C multiplexer/switch This patch adds support for the Analog Devices / Linear Technology LTC4306 and LTC4305 4/2 Channel I2C Bus Multiplexer/Switches. The LTC4306 optionally provides two general purpose input/output pins (GPIOs) that can be configured as logic inputs, opendrain outputs or push-pull outputs via the generic GPIOLIB framework. Signed-off-by: Michael Hennerich Reviewed-by: Linus Walleij Signed-off-by: Peter Rosin --- MAINTAINERS | 8 + drivers/i2c/muxes/Kconfig | 11 ++ drivers/i2c/muxes/Makefile | 1 + drivers/i2c/muxes/i2c-mux-ltc4306.c | 322 ++++++++++++++++++++++++++++++++++++ 4 files changed, 342 insertions(+) create mode 100644 drivers/i2c/muxes/i2c-mux-ltc4306.c (limited to 'drivers/i2c/muxes') diff --git a/MAINTAINERS b/MAINTAINERS index c776906f67a9..9a27a197b1f6 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -7698,6 +7698,14 @@ S: Maintained F: Documentation/hwmon/ltc4261 F: drivers/hwmon/ltc4261.c +LTC4306 I2C MULTIPLEXER DRIVER +M: Michael Hennerich +W: http://ez.analog.com/community/linux-device-drivers +L: linux-i2c@vger.kernel.org +S: Supported +F: drivers/i2c/muxes/i2c-mux-ltc4306.c +F: Documentation/devicetree/bindings/i2c/i2c-mux-ltc4306.txt + LTP (Linux Test Project) M: Mike Frysinger M: Cyril Hrubis diff --git a/drivers/i2c/muxes/Kconfig b/drivers/i2c/muxes/Kconfig index 10b3d17ae3ea..1e160fc37ecc 100644 --- a/drivers/i2c/muxes/Kconfig +++ b/drivers/i2c/muxes/Kconfig @@ -30,6 +30,17 @@ config I2C_MUX_GPIO This driver can also be built as a module. If so, the module will be called i2c-mux-gpio. +config I2C_MUX_LTC4306 + tristate "LTC LTC4306/5 I2C multiplexer" + select GPIOLIB + select REGMAP_I2C + help + If you say yes here you get support for the Analog Devices + LTC4306 or LTC4305 I2C mux/switch devices. + + This driver can also be built as a module. If so, the module + will be called i2c-mux-ltc4306. + config I2C_MUX_PCA9541 tristate "NXP PCA9541 I2C Master Selector" help diff --git a/drivers/i2c/muxes/Makefile b/drivers/i2c/muxes/Makefile index 9948fa45037f..ff7618cd5312 100644 --- a/drivers/i2c/muxes/Makefile +++ b/drivers/i2c/muxes/Makefile @@ -6,6 +6,7 @@ obj-$(CONFIG_I2C_ARB_GPIO_CHALLENGE) += i2c-arb-gpio-challenge.o obj-$(CONFIG_I2C_DEMUX_PINCTRL) += i2c-demux-pinctrl.o obj-$(CONFIG_I2C_MUX_GPIO) += i2c-mux-gpio.o +obj-$(CONFIG_I2C_MUX_LTC4306) += i2c-mux-ltc4306.o obj-$(CONFIG_I2C_MUX_MLXCPLD) += i2c-mux-mlxcpld.o obj-$(CONFIG_I2C_MUX_PCA9541) += i2c-mux-pca9541.o obj-$(CONFIG_I2C_MUX_PCA954x) += i2c-mux-pca954x.o diff --git a/drivers/i2c/muxes/i2c-mux-ltc4306.c b/drivers/i2c/muxes/i2c-mux-ltc4306.c new file mode 100644 index 000000000000..311b1cced0c0 --- /dev/null +++ b/drivers/i2c/muxes/i2c-mux-ltc4306.c @@ -0,0 +1,322 @@ +/* + * Linear Technology LTC4306 and LTC4305 I2C multiplexer/switch + * + * Copyright (C) 2017 Analog Devices Inc. + * + * Licensed under the GPL-2. + * + * Based on: i2c-mux-pca954x.c + * + * Datasheet: http://cds.linear.com/docs/en/datasheet/4306.pdf + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LTC4305_MAX_NCHANS 2 +#define LTC4306_MAX_NCHANS 4 + +#define LTC_REG_STATUS 0x0 +#define LTC_REG_CONFIG 0x1 +#define LTC_REG_MODE 0x2 +#define LTC_REG_SWITCH 0x3 + +#define LTC_DOWNSTREAM_ACCL_EN BIT(6) +#define LTC_UPSTREAM_ACCL_EN BIT(7) + +#define LTC_GPIO_ALL_INPUT 0xC0 +#define LTC_SWITCH_MASK 0xF0 + +enum ltc_type { + ltc_4305, + ltc_4306, +}; + +struct chip_desc { + u8 nchans; + u8 num_gpios; +}; + +struct ltc4306 { + struct regmap *regmap; + struct gpio_chip gpiochip; + const struct chip_desc *chip; +}; + +static const struct chip_desc chips[] = { + [ltc_4305] = { + .nchans = LTC4305_MAX_NCHANS, + }, + [ltc_4306] = { + .nchans = LTC4306_MAX_NCHANS, + .num_gpios = 2, + }, +}; + +static bool ltc4306_is_volatile_reg(struct device *dev, unsigned int reg) +{ + return (reg == LTC_REG_CONFIG) ? true : false; +} + +static const struct regmap_config ltc4306_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = LTC_REG_SWITCH, + .volatile_reg = ltc4306_is_volatile_reg, + .cache_type = REGCACHE_FLAT, +}; + +static int ltc4306_gpio_get(struct gpio_chip *chip, unsigned int offset) +{ + struct ltc4306 *data = gpiochip_get_data(chip); + unsigned int val; + int ret; + + ret = regmap_read(data->regmap, LTC_REG_CONFIG, &val); + if (ret < 0) + return ret; + + return !!(val & BIT(1 - offset)); +} + +static void ltc4306_gpio_set(struct gpio_chip *chip, unsigned int offset, + int value) +{ + struct ltc4306 *data = gpiochip_get_data(chip); + + regmap_update_bits(data->regmap, LTC_REG_CONFIG, BIT(5 - offset), + value ? BIT(5 - offset) : 0); +} + +static int ltc4306_gpio_get_direction(struct gpio_chip *chip, + unsigned int offset) +{ + struct ltc4306 *data = gpiochip_get_data(chip); + unsigned int val; + int ret; + + ret = regmap_read(data->regmap, LTC_REG_MODE, &val); + if (ret < 0) + return ret; + + return !!(val & BIT(7 - offset)); +} + +static int ltc4306_gpio_direction_input(struct gpio_chip *chip, + unsigned int offset) +{ + struct ltc4306 *data = gpiochip_get_data(chip); + + return regmap_update_bits(data->regmap, LTC_REG_MODE, + BIT(7 - offset), BIT(7 - offset)); +} + +static int ltc4306_gpio_direction_output(struct gpio_chip *chip, + unsigned int offset, int value) +{ + struct ltc4306 *data = gpiochip_get_data(chip); + + ltc4306_gpio_set(chip, offset, value); + return regmap_update_bits(data->regmap, LTC_REG_MODE, + BIT(7 - offset), 0); +} + +static int ltc4306_gpio_set_config(struct gpio_chip *chip, + unsigned int offset, unsigned long config) +{ + struct ltc4306 *data = gpiochip_get_data(chip); + unsigned int val; + + switch (pinconf_to_config_param(config)) { + case PIN_CONFIG_DRIVE_OPEN_DRAIN: + val = 0; + break; + case PIN_CONFIG_DRIVE_PUSH_PULL: + val = BIT(4 - offset); + break; + default: + return -ENOTSUPP; + } + + return regmap_update_bits(data->regmap, LTC_REG_MODE, + BIT(4 - offset), val); +} + +static int ltc4306_gpio_init(struct ltc4306 *data) +{ + struct device *dev = regmap_get_device(data->regmap); + + if (!data->chip->num_gpios) + return 0; + + data->gpiochip.label = dev_name(dev); + data->gpiochip.base = -1; + data->gpiochip.ngpio = data->chip->num_gpios; + data->gpiochip.parent = dev; + data->gpiochip.can_sleep = true; + data->gpiochip.get_direction = ltc4306_gpio_get_direction; + data->gpiochip.direction_input = ltc4306_gpio_direction_input; + data->gpiochip.direction_output = ltc4306_gpio_direction_output; + data->gpiochip.get = ltc4306_gpio_get; + data->gpiochip.set = ltc4306_gpio_set; + data->gpiochip.set_config = ltc4306_gpio_set_config; + data->gpiochip.owner = THIS_MODULE; + + /* gpiolib assumes all GPIOs default input */ + regmap_write(data->regmap, LTC_REG_MODE, LTC_GPIO_ALL_INPUT); + + return devm_gpiochip_add_data(dev, &data->gpiochip, data); +} + +static int ltc4306_select_mux(struct i2c_mux_core *muxc, u32 chan) +{ + struct ltc4306 *data = i2c_mux_priv(muxc); + + return regmap_update_bits(data->regmap, LTC_REG_SWITCH, + LTC_SWITCH_MASK, BIT(7 - chan)); +} + +static int ltc4306_deselect_mux(struct i2c_mux_core *muxc, u32 chan) +{ + struct ltc4306 *data = i2c_mux_priv(muxc); + + return regmap_update_bits(data->regmap, LTC_REG_SWITCH, + LTC_SWITCH_MASK, 0); +} + +static const struct i2c_device_id ltc4306_id[] = { + { "ltc4305", ltc_4305 }, + { "ltc4306", ltc_4306 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, ltc4306_id); + +static const struct of_device_id ltc4306_of_match[] = { + { .compatible = "lltc,ltc4305", .data = &chips[ltc_4305] }, + { .compatible = "lltc,ltc4306", .data = &chips[ltc_4306] }, + { } +}; +MODULE_DEVICE_TABLE(of, ltc4306_of_match); + +static int ltc4306_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct i2c_adapter *adap = to_i2c_adapter(client->dev.parent); + const struct chip_desc *chip; + struct i2c_mux_core *muxc; + struct ltc4306 *data; + struct gpio_desc *gpio; + bool idle_disc; + unsigned int val = 0; + int num, ret; + + chip = of_device_get_match_data(&client->dev); + + if (!chip) + chip = &chips[id->driver_data]; + + idle_disc = device_property_read_bool(&client->dev, + "i2c-mux-idle-disconnect"); + + muxc = i2c_mux_alloc(adap, &client->dev, + chip->nchans, sizeof(*data), + I2C_MUX_LOCKED, ltc4306_select_mux, + idle_disc ? ltc4306_deselect_mux : NULL); + if (!muxc) + return -ENOMEM; + data = i2c_mux_priv(muxc); + data->chip = chip; + + i2c_set_clientdata(client, muxc); + + data->regmap = devm_regmap_init_i2c(client, <c4306_regmap_config); + if (IS_ERR(data->regmap)) { + ret = PTR_ERR(data->regmap); + dev_err(&client->dev, "Failed to allocate register map: %d\n", + ret); + return ret; + } + + /* Reset and enable the mux if an enable GPIO is specified. */ + gpio = devm_gpiod_get_optional(&client->dev, "enable", GPIOD_OUT_LOW); + if (IS_ERR(gpio)) + return PTR_ERR(gpio); + + if (gpio) { + udelay(1); + gpiod_set_value(gpio, 1); + } + + /* + * Write the mux register at addr to verify + * that the mux is in fact present. This also + * initializes the mux to disconnected state. + */ + if (regmap_write(data->regmap, LTC_REG_SWITCH, 0) < 0) { + dev_warn(&client->dev, "probe failed\n"); + return -ENODEV; + } + + if (device_property_read_bool(&client->dev, + "ltc,downstream-accelerators-enable")) + val |= LTC_DOWNSTREAM_ACCL_EN; + + if (device_property_read_bool(&client->dev, + "ltc,upstream-accelerators-enable")) + val |= LTC_UPSTREAM_ACCL_EN; + + if (regmap_write(data->regmap, LTC_REG_CONFIG, val) < 0) + return -ENODEV; + + ret = ltc4306_gpio_init(data); + if (ret < 0) + return ret; + + /* Now create an adapter for each channel */ + for (num = 0; num < chip->nchans; num++) { + ret = i2c_mux_add_adapter(muxc, 0, num, 0); + if (ret) { + i2c_mux_del_adapters(muxc); + return ret; + } + } + + dev_info(&client->dev, + "registered %d multiplexed busses for I2C switch %s\n", + num, client->name); + + return 0; +} + +static int ltc4306_remove(struct i2c_client *client) +{ + struct i2c_mux_core *muxc = i2c_get_clientdata(client); + + i2c_mux_del_adapters(muxc); + + return 0; +} + +static struct i2c_driver ltc4306_driver = { + .driver = { + .name = "ltc4306", + .of_match_table = of_match_ptr(ltc4306_of_match), + }, + .probe = ltc4306_probe, + .remove = ltc4306_remove, + .id_table = ltc4306_id, +}; + +module_i2c_driver(ltc4306_driver); + +MODULE_AUTHOR("Michael Hennerich "); +MODULE_DESCRIPTION("Linear Technology LTC4306, LTC4305 I2C mux/switch driver"); +MODULE_LICENSE("GPL v2"); -- cgit v1.2.3