The TLC59108/TLC59116 is an I2C bus controlled 8-bit LED driver. Each LED output has its own 8-bit resolution (256 steps) fixed-frequency individual PWM controller that operates at 97 kHz, with a duty cycle that is adjustable from 0% to 100%. The individual PWM controller allows each LED to be set to a specific brightness value. TLC59108 has 8 outputs, TLC59116 has 16 outputs. Signed-off-by: Tomi Valkeinen <tomi.valkeinen@xxxxxx> --- .../devicetree/bindings/pwm/ti,tlc59108-pwm.txt | 16 ++ drivers/pwm/Kconfig | 10 + drivers/pwm/Makefile | 1 + drivers/pwm/pwm-tlc591xx.c | 273 +++++++++++++++++++++ 4 files changed, 300 insertions(+) create mode 100644 Documentation/devicetree/bindings/pwm/ti,tlc59108-pwm.txt create mode 100644 drivers/pwm/pwm-tlc591xx.c diff --git a/Documentation/devicetree/bindings/pwm/ti,tlc59108-pwm.txt b/Documentation/devicetree/bindings/pwm/ti,tlc59108-pwm.txt new file mode 100644 index 000000000000..5b9580b6ac94 --- /dev/null +++ b/Documentation/devicetree/bindings/pwm/ti,tlc59108-pwm.txt @@ -0,0 +1,16 @@ +TI TLC59108/TLC59116 8/16-channel 8-bit PWM LED Sink Driver +=========================================================== + +Required properties: + - compatible: "ti,tlc59108-pwm" or "ti,tlc59116-pwm" + - #pwm-cells: should be 2. See pwm.txt in this directory for a description of + the cells format. + - reg: physical base address and size of the registers map. + +Example: + +tlc59108: tlc59108@40 { + compatible = "ti,tlc59108-pwm"; + reg = <0x40>; + #pwm-cells = <2>; +}; diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig index b1541f40fd8d..0a9c8fcf993e 100644 --- a/drivers/pwm/Kconfig +++ b/drivers/pwm/Kconfig @@ -373,4 +373,14 @@ config PWM_VT8500 To compile this driver as a module, choose M here: the module will be called pwm-vt8500. +config PWM_TLC591XX + tristate "TLC59108/TLC59116 PWM driver" + depends on OF && I2C + select REGMAP_I2C + help + Generic PWM framework driver for TI TLC59108/TLC59116 PWM controller. + + To compile this driver as a module, choose M here: the module + will be called pwm-tlc591xx. + endif diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile index ec50eb5b5a8f..75896890d6ec 100644 --- a/drivers/pwm/Makefile +++ b/drivers/pwm/Makefile @@ -35,3 +35,4 @@ obj-$(CONFIG_PWM_TIPWMSS) += pwm-tipwmss.o obj-$(CONFIG_PWM_TWL) += pwm-twl.o obj-$(CONFIG_PWM_TWL_LED) += pwm-twl-led.o obj-$(CONFIG_PWM_VT8500) += pwm-vt8500.o +obj-$(CONFIG_PWM_TLC591XX) += pwm-tlc591xx.o diff --git a/drivers/pwm/pwm-tlc591xx.c b/drivers/pwm/pwm-tlc591xx.c new file mode 100644 index 000000000000..cd49faa75eff --- /dev/null +++ b/drivers/pwm/pwm-tlc591xx.c @@ -0,0 +1,273 @@ +/* + * Driver for TLC59108/TLC59116 I2C based PWM/LED chip + * + * Copyright 2014 Belkin Inc. + * Copyright 2015 Andrew Lunn + * Copyright 2015 Texas Instruments + * + * Authors: + * Andrew Lunn <andrew@xxxxxxx> + * Vignesh R <vigneshr@xxxxxx> + * Tomi Valkeinen <tomi.valkeinen@xxxxxx> + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published by + * the Free Software Foundation. + */ + +#include <linux/delay.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/pwm.h> +#include <linux/regmap.h> + +#define TLC591XX_MAX_LEDS 16 + +#define TLC591XX_MODE1 0x00 +#define TLC591XX_PWM(n) (0x02 + (n)) +#define TLC591XX_MAXREG 0x1e + +#define LEDOUT_OFF 0x0 +#define LEDOUT_ON 0x1 +#define LEDOUT_DIM 0x2 +#define LEDOUT_MASK 0x3 + +#define TLC591XX_CLK_PERIOD 10309 /* period in ns */ +#define TLC591XX_NO_STEPS 256 +#define TLC591XX_LEDS_PER_REG 4 + +struct tlc591xx_led { + unsigned int led_no; + struct tlc591xx_priv *priv; +}; + +struct tlc591xx_priv { + struct pwm_chip chip; + struct regmap *regmap; + struct tlc591xx_led leds[TLC591XX_MAX_LEDS]; + unsigned int reg_ledout_offset; +}; + +static struct regmap_config tlc591xx_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = TLC591XX_MAXREG, +}; + +struct tlc591xx_hw { + unsigned int max_leds; + unsigned int reg_ledout_offset; +}; + +static const struct tlc591xx_hw tlc59116 = { + .max_leds = 16, + .reg_ledout_offset = 0x14, +}; + +static const struct tlc591xx_hw tlc59108 = { + .max_leds = 8, + .reg_ledout_offset = 0x0c, +}; + +static inline struct tlc591xx_priv *to_tlc591xx_priv(struct pwm_chip *c) +{ + return container_of(c, struct tlc591xx_priv, chip); +} + +static int tlc591xx_write_ledout(struct pwm_chip *chip, struct pwm_device *pwm, + unsigned val) +{ + struct tlc591xx_priv *priv = to_tlc591xx_priv(chip); + int led_no, led_grp; + unsigned int reg, mask; + unsigned shift; + + led_no = (pwm->hwpwm) % TLC591XX_LEDS_PER_REG; + led_grp = (pwm->hwpwm) / TLC591XX_LEDS_PER_REG; + + reg = priv->reg_ledout_offset + led_grp; + shift = led_no * 2; + mask = LEDOUT_MASK << shift; + val <<= shift; + + return regmap_update_bits(priv->regmap, reg, mask, val); +} + +static int tlc591xx_pwm_config(struct pwm_chip *chip, struct pwm_device *pwm, + int duty_ns, int period_ns) +{ + struct tlc591xx_priv *priv = to_tlc591xx_priv(chip); + unsigned val; + int r; + + if (period_ns != TLC591XX_CLK_PERIOD) { + dev_err(chip->dev, "only period of 10309 ns is supported\n"); + return -EINVAL; + } + + val = DIV_ROUND_CLOSEST(duty_ns * (TLC591XX_NO_STEPS - 1), period_ns); + + r = regmap_write(priv->regmap, TLC591XX_PWM(pwm->hwpwm), val); + if (r) + return r; + + if (test_bit(PWMF_ENABLED, &pwm->flags)) { + if (duty_ns == period_ns) + r = tlc591xx_write_ledout(chip, pwm, LEDOUT_ON); + else if (duty_ns == 0) + r = tlc591xx_write_ledout(chip, pwm, LEDOUT_OFF); + else + r = tlc591xx_write_ledout(chip, pwm, LEDOUT_DIM); + + if (r) + return r; + } + + return 0; +} + +static int tlc591xx_pwm_enable(struct pwm_chip *chip, struct pwm_device *pwm) +{ + if (pwm->duty_cycle == pwm->period) + return tlc591xx_write_ledout(chip, pwm, LEDOUT_ON); + else + return tlc591xx_write_ledout(chip, pwm, LEDOUT_DIM); +} + +static void tlc591xx_pwm_disable(struct pwm_chip *chip, struct pwm_device *pwm) +{ + tlc591xx_write_ledout(chip, pwm, LEDOUT_OFF); +} + +static int tlc591xx_set_polarity(struct pwm_chip *chip, struct pwm_device *pwm, + enum pwm_polarity polarity) +{ + return polarity != PWM_POLARITY_INVERSED ? -EINVAL : 0; +} + +static const struct pwm_ops tlc591xx_pwm_ops = { + .config = tlc591xx_pwm_config, + .enable = tlc591xx_pwm_enable, + .disable = tlc591xx_pwm_disable, + .set_polarity = tlc591xx_set_polarity, + .owner = THIS_MODULE, +}; + +static const struct of_device_id tlc591xx_pwm_dt_ids[] = { + { .compatible = "ti,tlc59116-pwm", .data = &tlc59116 }, + { .compatible = "ti,tlc59108-pwm", .data = &tlc59108 }, + { } +}; + +MODULE_DEVICE_TABLE(of, tlc591xx_pwm_dt_ids); + +static int tlc591xx_pwm_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct device_node *np = client->dev.of_node; + struct device *dev = &client->dev; + struct tlc591xx_priv *priv; + const struct of_device_id *match; + const struct tlc591xx_hw *tlc591xx_hw; + struct regmap *regmap; + int err; + int i; + + if (!np) + return -ENODEV; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_BYTE_DATA)) + return -EIO; + + match = of_match_device(tlc591xx_pwm_dt_ids, dev); + if (!match) + return -ENODEV; + + tlc591xx_hw = match->data; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + i2c_set_clientdata(client, priv); + + regmap = devm_regmap_init_i2c(client, &tlc591xx_regmap_config); + if (IS_ERR(regmap)) { + err = PTR_ERR(regmap); + dev_err(dev, "Failed to allocate register map: %d\n", err); + return err; + } + + priv->reg_ledout_offset = tlc591xx_hw->reg_ledout_offset; + priv->regmap = regmap; + + priv->chip.dev = dev; + priv->chip.ops = &tlc591xx_pwm_ops; + priv->chip.base = -1; + priv->chip.npwm = tlc591xx_hw->max_leds; + priv->chip.can_sleep = true; + + err = pwmchip_add(&priv->chip); + if (err < 0) { + dev_err(dev, "failed to add PWM chip %d\n", err); + return err; + } + + for (i = 0; i < priv->chip.npwm; ++i) { + priv->chip.pwms[i].period = 10309; + priv->chip.pwms[i].polarity = PWM_POLARITY_INVERSED; + } + + /* enable oscillator */ + regmap_update_bits(priv->regmap, TLC591XX_MODE1, 1 << 4, 0 << 4); + + /* + * Delay of 500 micro second is required before accessing + * the PWMx or LEDOUTx registers. + */ + usleep_range(500, 1000); + + return 0; +} + +static int tlc591xx_pwm_remove(struct i2c_client *client) +{ + struct tlc591xx_priv *priv = i2c_get_clientdata(client); + int err; + + /* disable oscillator */ + regmap_update_bits(priv->regmap, TLC591XX_MODE1, 1 << 4, 1 << 4); + + err = pwmchip_remove(&priv->chip); + if (err < 0) + dev_err(&client->dev, + "pwmchip_remove failed with error %d\n", err); + + return err; +} + +static const struct i2c_device_id tlc591xx_pwm_id[] = { + { "tlc59108-pwm", }, + { "tlc59116-pwm", }, + { } +}; +MODULE_DEVICE_TABLE(i2c, tlc591xx_pwm_id); + +static struct i2c_driver tlc591xx_pwm_driver = { + .driver = { + .name = "tlc591xx-pwm", + .of_match_table = tlc591xx_pwm_dt_ids, + }, + .probe = tlc591xx_pwm_probe, + .remove = tlc591xx_pwm_remove, + .id_table = tlc591xx_pwm_id, +}; + +module_i2c_driver(tlc591xx_pwm_driver); + +MODULE_AUTHOR("Tomi Valkeinen <tomi.valkeinen@xxxxxx>"); +MODULE_DESCRIPTION("PWM driver for TLC59108/TLC59116"); +MODULE_LICENSE("GPL"); -- 2.1.4 -- To unsubscribe from this list: send the line "unsubscribe linux-leds" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html