/* * AS3711 PMIC backlight driver, using DCDC Step Up Converters * * Copyright (C) 2012 Renesas Electronics Corporation * Author: Guennadi Liakhovetski, <g.liakhovetski@gmx.de> * * This program is free software; you can redistribute it and/or modify * it under the terms of the version 2 of the GNU General Public License as * published by the Free Software Foundation */ #include <linux/backlight.h> #include <linux/delay.h> #include <linux/device.h> #include <linux/err.h> #include <linux/fb.h> #include <linux/kernel.h> #include <linux/mfd/as3711.h> #include <linux/module.h> #include <linux/platform_device.h> #include <linux/regmap.h> #include <linux/slab.h> enum as3711_bl_type { AS3711_BL_SU1, AS3711_BL_SU2, }; struct as3711_bl_data { bool powered; const char *fb_name; struct device *fb_dev; enum as3711_bl_type type; int brightness; struct backlight_device *bl; }; struct as3711_bl_supply { struct as3711_bl_data su1; struct as3711_bl_data su2; const struct as3711_bl_pdata *pdata; struct as3711 *as3711; }; static struct as3711_bl_supply *to_supply(struct as3711_bl_data *su) { switch (su->type) { case AS3711_BL_SU1: return container_of(su, struct as3711_bl_supply, su1); case AS3711_BL_SU2: return container_of(su, struct as3711_bl_supply, su2); } return NULL; } static int as3711_set_brightness_auto_i(struct as3711_bl_data *data, unsigned int brightness) { struct as3711_bl_supply *supply = to_supply(data); struct as3711 *as3711 = supply->as3711; const struct as3711_bl_pdata *pdata = supply->pdata; int ret = 0; /* Only all equal current values are supported */ if (pdata->su2_auto_curr1) ret = regmap_write(as3711->regmap, AS3711_CURR1_VALUE, brightness); if (!ret && pdata->su2_auto_curr2) ret = regmap_write(as3711->regmap, AS3711_CURR2_VALUE, brightness); if (!ret && pdata->su2_auto_curr3) ret = regmap_write(as3711->regmap, AS3711_CURR3_VALUE, brightness); return ret; } static int as3711_set_brightness_v(struct as3711 *as3711, unsigned int brightness, unsigned int reg) { if (brightness > 31) return -EINVAL; return regmap_update_bits(as3711->regmap, reg, 0xf0, brightness << 4); } static int as3711_bl_su2_reset(struct as3711_bl_supply *supply) { struct as3711 *as3711 = supply->as3711; int ret = regmap_update_bits(as3711->regmap, AS3711_STEPUP_CONTROL_5, 3, supply->pdata->su2_fbprot); if (!ret) ret = regmap_update_bits(as3711->regmap, AS3711_STEPUP_CONTROL_2, 1, 0); if (!ret) ret = regmap_update_bits(as3711->regmap, AS3711_STEPUP_CONTROL_2, 1, 1); return ret; } /* * Someone with less fragile or less expensive hardware could try to simplify * the brightness adjustment procedure. */ static int as3711_bl_update_status(struct backlight_device *bl) { struct as3711_bl_data *data = bl_get_data(bl); struct as3711_bl_supply *supply = to_supply(data); struct as3711 *as3711 = supply->as3711; int brightness = bl->props.brightness; int ret = 0; dev_dbg(&bl->dev, "%s(): brightness %u, pwr %x, blank %x, state %x\n", __func__, bl->props.brightness, bl->props.power, bl->props.fb_blank, bl->props.state); if (bl->props.power != FB_BLANK_UNBLANK || bl->props.fb_blank != FB_BLANK_UNBLANK || bl->props.state & (BL_CORE_SUSPENDED | BL_CORE_FBBLANK)) brightness = 0; if (data->type == AS3711_BL_SU1) { ret = as3711_set_brightness_v(as3711, brightness, AS3711_STEPUP_CONTROL_1); } else { const struct as3711_bl_pdata *pdata = supply->pdata; switch (pdata->su2_feedback) { case AS3711_SU2_VOLTAGE: ret = as3711_set_brightness_v(as3711, brightness, AS3711_STEPUP_CONTROL_2); break; case AS3711_SU2_CURR_AUTO: ret = as3711_set_brightness_auto_i(data, brightness / 4); if (ret < 0) return ret; if (brightness) { ret = as3711_bl_su2_reset(supply); if (ret < 0) return ret; udelay(500); ret = as3711_set_brightness_auto_i(data, brightness); } else { ret = regmap_update_bits(as3711->regmap, AS3711_STEPUP_CONTROL_2, 1, 0); } break; /* Manual one current feedback pin below */ case AS3711_SU2_CURR1: ret = regmap_write(as3711->regmap, AS3711_CURR1_VALUE, brightness); break; case AS3711_SU2_CURR2: ret = regmap_write(as3711->regmap, AS3711_CURR2_VALUE, brightness); break; case AS3711_SU2_CURR3: ret = regmap_write(as3711->regmap, AS3711_CURR3_VALUE, brightness); break; default: ret = -EINVAL; } } if (!ret) data->brightness = brightness; return ret; } static int as3711_bl_get_brightness(struct backlight_device *bl) { struct as3711_bl_data *data = bl_get_data(bl); return data->brightness; } static const struct backlight_ops as3711_bl_ops = { .update_status = as3711_bl_update_status, .get_brightness = as3711_bl_get_brightness, }; static int as3711_bl_init_su2(struct as3711_bl_supply *supply) { struct as3711 *as3711 = supply->as3711; const struct as3711_bl_pdata *pdata = supply->pdata; u8 ctl = 0; int ret; dev_dbg(as3711->dev, "%s(): use %u\n", __func__, pdata->su2_feedback); /* Turn SU2 off */ ret = regmap_write(as3711->regmap, AS3711_STEPUP_CONTROL_2, 0); if (ret < 0) return ret; switch (pdata->su2_feedback) { case AS3711_SU2_VOLTAGE: ret = regmap_update_bits(as3711->regmap, AS3711_STEPUP_CONTROL_4, 3, 0); break; case AS3711_SU2_CURR1: ctl = 1; ret = regmap_update_bits(as3711->regmap, AS3711_STEPUP_CONTROL_4, 3, 1); break; case AS3711_SU2_CURR2: ctl = 4; ret = regmap_update_bits(as3711->regmap, AS3711_STEPUP_CONTROL_4, 3, 2); break; case AS3711_SU2_CURR3: ctl = 0x10; ret = regmap_update_bits(as3711->regmap, AS3711_STEPUP_CONTROL_4, 3, 3); break; case AS3711_SU2_CURR_AUTO: if (pdata->su2_auto_curr1) ctl = 2; if (pdata->su2_auto_curr2) ctl |= 8; if (pdata->su2_auto_curr3) ctl |= 0x20; ret = 0; break; default: return -EINVAL; } if (!ret) ret = regmap_write(as3711->regmap, AS3711_CURR_CONTROL, ctl); return ret; } static int as3711_bl_register(struct platform_device *pdev, unsigned int max_brightness, struct as3711_bl_data *su) { struct backlight_properties props = {.type = BACKLIGHT_RAW,}; struct backlight_device *bl; /* max tuning I = 31uA for voltage- and 38250uA for current-feedback */ props.max_brightness = max_brightness; bl = devm_backlight_device_register(&pdev->dev, su->type == AS3711_BL_SU1 ? "as3711-su1" : "as3711-su2", &pdev->dev, su, &as3711_bl_ops, &props); if (IS_ERR(bl)) { dev_err(&pdev->dev, "failed to register backlight\n"); return PTR_ERR(bl); } bl->props.brightness = props.max_brightness; backlight_update_status(bl); su->bl = bl; return 0; } static int as3711_backlight_parse_dt(struct device *dev) { struct as3711_bl_pdata *pdata = dev_get_platdata(dev); struct device_node *bl = of_find_node_by_name(dev->parent->of_node, "backlight"), *fb; int ret; if (!bl) { dev_dbg(dev, "backlight node not found\n"); return -ENODEV; } fb = of_parse_phandle(bl, "su1-dev", 0); if (fb) { pdata->su1_fb = fb->full_name; ret = of_property_read_u32(bl, "su1-max-uA", &pdata->su1_max_uA); if (pdata->su1_max_uA <= 0) ret = -EINVAL; if (ret < 0) return ret; } fb = of_parse_phandle(bl, "su2-dev", 0); if (fb) { int count = 0; pdata->su2_fb = fb->full_name; ret = of_property_read_u32(bl, "su2-max-uA", &pdata->su2_max_uA); if (pdata->su2_max_uA <= 0) ret = -EINVAL; if (ret < 0) return ret; if (of_find_property(bl, "su2-feedback-voltage", NULL)) { pdata->su2_feedback = AS3711_SU2_VOLTAGE; count++; } if (of_find_property(bl, "su2-feedback-curr1", NULL)) { pdata->su2_feedback = AS3711_SU2_CURR1; count++; } if (of_find_property(bl, "su2-feedback-curr2", NULL)) { pdata->su2_feedback = AS3711_SU2_CURR2; count++; } if (of_find_property(bl, "su2-feedback-curr3", NULL)) { pdata->su2_feedback = AS3711_SU2_CURR3; count++; } if (of_find_property(bl, "su2-feedback-curr-auto", NULL)) { pdata->su2_feedback = AS3711_SU2_CURR_AUTO; count++; } if (count != 1) return -EINVAL; count = 0; if (of_find_property(bl, "su2-fbprot-lx-sd4", NULL)) { pdata->su2_fbprot = AS3711_SU2_LX_SD4; count++; } if (of_find_property(bl, "su2-fbprot-gpio2", NULL)) { pdata->su2_fbprot = AS3711_SU2_GPIO2; count++; } if (of_find_property(bl, "su2-fbprot-gpio3", NULL)) { pdata->su2_fbprot = AS3711_SU2_GPIO3; count++; } if (of_find_property(bl, "su2-fbprot-gpio4", NULL)) { pdata->su2_fbprot = AS3711_SU2_GPIO4; count++; } if (count != 1) return -EINVAL; count = 0; if (of_find_property(bl, "su2-auto-curr1", NULL)) { pdata->su2_auto_curr1 = true; count++; } if (of_find_property(bl, "su2-auto-curr2", NULL)) { pdata->su2_auto_curr2 = true; count++; } if (of_find_property(bl, "su2-auto-curr3", NULL)) { pdata->su2_auto_curr3 = true; count++; } /* * At least one su2-auto-curr* must be specified iff * AS3711_SU2_CURR_AUTO is used */ if (!count ^ (pdata->su2_feedback != AS3711_SU2_CURR_AUTO)) return -EINVAL; } return 0; } static int as3711_backlight_probe(struct platform_device *pdev) { struct as3711_bl_pdata *pdata = dev_get_platdata(&pdev->dev); struct as3711 *as3711 = dev_get_drvdata(pdev->dev.parent); struct as3711_bl_supply *supply; struct as3711_bl_data *su; unsigned int max_brightness; int ret; if (!pdata) { dev_err(&pdev->dev, "No platform data, exiting...\n"); return -ENODEV; } if (pdev->dev.parent->of_node) { ret = as3711_backlight_parse_dt(&pdev->dev); if (ret < 0) { dev_err(&pdev->dev, "DT parsing failed: %d\n", ret); return ret; } } if (!pdata->su1_fb && !pdata->su2_fb) { dev_err(&pdev->dev, "No framebuffer specified\n"); return -EINVAL; } /* * Due to possible hardware damage I chose to block all modes, * unsupported on my hardware. Anyone, wishing to use any of those modes * will have to first review the code, then activate and test it. */ if (pdata->su1_fb || pdata->su2_fbprot != AS3711_SU2_GPIO4 || pdata->su2_feedback != AS3711_SU2_CURR_AUTO) { dev_warn(&pdev->dev, "Attention! An untested mode has been chosen!\n" "Please, review the code, enable, test, and report success:-)\n"); return -EINVAL; } supply = devm_kzalloc(&pdev->dev, sizeof(*supply), GFP_KERNEL); if (!supply) return -ENOMEM; supply->as3711 = as3711; supply->pdata = pdata; if (pdata->su1_fb) { su = &supply->su1; su->fb_name = pdata->su1_fb; su->type = AS3711_BL_SU1; max_brightness = min(pdata->su1_max_uA, 31); ret = as3711_bl_register(pdev, max_brightness, su); if (ret < 0) return ret; } if (pdata->su2_fb) { su = &supply->su2; su->fb_name = pdata->su2_fb; su->type = AS3711_BL_SU2; switch (pdata->su2_fbprot) { case AS3711_SU2_GPIO2: case AS3711_SU2_GPIO3: case AS3711_SU2_GPIO4: case AS3711_SU2_LX_SD4: break; default: return -EINVAL; } switch (pdata->su2_feedback) { case AS3711_SU2_VOLTAGE: max_brightness = min(pdata->su2_max_uA, 31); break; case AS3711_SU2_CURR1: case AS3711_SU2_CURR2: case AS3711_SU2_CURR3: case AS3711_SU2_CURR_AUTO: max_brightness = min(pdata->su2_max_uA / 150, 255); break; default: return -EINVAL; } ret = as3711_bl_init_su2(supply); if (ret < 0) return ret; ret = as3711_bl_register(pdev, max_brightness, su); if (ret < 0) return ret; } platform_set_drvdata(pdev, supply); return 0; } static struct platform_driver as3711_backlight_driver = { .driver = { .name = "as3711-backlight", }, .probe = as3711_backlight_probe, }; module_platform_driver(as3711_backlight_driver); MODULE_DESCRIPTION("Backlight Driver for AS3711 PMICs"); MODULE_AUTHOR("Guennadi Liakhovetski <g.liakhovetski@gmx.de"); MODULE_LICENSE("GPL"); MODULE_ALIAS("platform:as3711-backlight");