1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* |
3 | * TI LP8788 MFD - backlight driver |
4 | * |
5 | * Copyright 2012 Texas Instruments |
6 | * |
7 | * Author: Milo(Woogyom) Kim <milo.kim@ti.com> |
8 | */ |
9 | |
10 | #include <linux/backlight.h> |
11 | #include <linux/err.h> |
12 | #include <linux/mfd/lp8788.h> |
13 | #include <linux/module.h> |
14 | #include <linux/platform_device.h> |
15 | #include <linux/pwm.h> |
16 | #include <linux/slab.h> |
17 | |
18 | /* Register address */ |
19 | #define LP8788_BL_CONFIG 0x96 |
20 | #define LP8788_BL_EN BIT(0) |
21 | #define LP8788_BL_PWM_INPUT_EN BIT(5) |
22 | #define LP8788_BL_FULLSCALE_SHIFT 2 |
23 | #define LP8788_BL_DIM_MODE_SHIFT 1 |
24 | #define LP8788_BL_PWM_POLARITY_SHIFT 6 |
25 | |
26 | #define LP8788_BL_BRIGHTNESS 0x97 |
27 | |
28 | #define LP8788_BL_RAMP 0x98 |
29 | #define LP8788_BL_RAMP_RISE_SHIFT 4 |
30 | |
31 | #define MAX_BRIGHTNESS 127 |
32 | #define DEFAULT_BL_NAME "lcd-backlight" |
33 | |
34 | struct lp8788_bl_config { |
35 | enum lp8788_bl_ctrl_mode bl_mode; |
36 | enum lp8788_bl_dim_mode dim_mode; |
37 | enum lp8788_bl_full_scale_current full_scale; |
38 | enum lp8788_bl_ramp_step rise_time; |
39 | enum lp8788_bl_ramp_step fall_time; |
40 | enum pwm_polarity pwm_pol; |
41 | }; |
42 | |
43 | struct lp8788_bl { |
44 | struct lp8788 *lp; |
45 | struct backlight_device *bl_dev; |
46 | struct lp8788_backlight_platform_data *pdata; |
47 | enum lp8788_bl_ctrl_mode mode; |
48 | struct pwm_device *pwm; |
49 | }; |
50 | |
51 | static struct lp8788_bl_config default_bl_config = { |
52 | .bl_mode = LP8788_BL_REGISTER_ONLY, |
53 | .dim_mode = LP8788_DIM_EXPONENTIAL, |
54 | .full_scale = LP8788_FULLSCALE_1900uA, |
55 | .rise_time = LP8788_RAMP_8192us, |
56 | .fall_time = LP8788_RAMP_8192us, |
57 | .pwm_pol = PWM_POLARITY_NORMAL, |
58 | }; |
59 | |
60 | static inline bool is_brightness_ctrl_by_pwm(enum lp8788_bl_ctrl_mode mode) |
61 | { |
62 | return mode == LP8788_BL_COMB_PWM_BASED; |
63 | } |
64 | |
65 | static inline bool is_brightness_ctrl_by_register(enum lp8788_bl_ctrl_mode mode) |
66 | { |
67 | return mode == LP8788_BL_REGISTER_ONLY || |
68 | mode == LP8788_BL_COMB_REGISTER_BASED; |
69 | } |
70 | |
71 | static int lp8788_backlight_configure(struct lp8788_bl *bl) |
72 | { |
73 | struct lp8788_backlight_platform_data *pdata = bl->pdata; |
74 | struct lp8788_bl_config *cfg = &default_bl_config; |
75 | int ret; |
76 | u8 val; |
77 | |
78 | /* |
79 | * Update chip configuration if platform data exists, |
80 | * otherwise use the default settings. |
81 | */ |
82 | if (pdata) { |
83 | cfg->bl_mode = pdata->bl_mode; |
84 | cfg->dim_mode = pdata->dim_mode; |
85 | cfg->full_scale = pdata->full_scale; |
86 | cfg->rise_time = pdata->rise_time; |
87 | cfg->fall_time = pdata->fall_time; |
88 | cfg->pwm_pol = pdata->pwm_pol; |
89 | } |
90 | |
91 | /* Brightness ramp up/down */ |
92 | val = (cfg->rise_time << LP8788_BL_RAMP_RISE_SHIFT) | cfg->fall_time; |
93 | ret = lp8788_write_byte(lp: bl->lp, LP8788_BL_RAMP, data: val); |
94 | if (ret) |
95 | return ret; |
96 | |
97 | /* Fullscale current setting */ |
98 | val = (cfg->full_scale << LP8788_BL_FULLSCALE_SHIFT) | |
99 | (cfg->dim_mode << LP8788_BL_DIM_MODE_SHIFT); |
100 | |
101 | /* Brightness control mode */ |
102 | switch (cfg->bl_mode) { |
103 | case LP8788_BL_REGISTER_ONLY: |
104 | val |= LP8788_BL_EN; |
105 | break; |
106 | case LP8788_BL_COMB_PWM_BASED: |
107 | case LP8788_BL_COMB_REGISTER_BASED: |
108 | val |= LP8788_BL_EN | LP8788_BL_PWM_INPUT_EN | |
109 | (cfg->pwm_pol << LP8788_BL_PWM_POLARITY_SHIFT); |
110 | break; |
111 | default: |
112 | dev_err(bl->lp->dev, "invalid mode: %d\n" , cfg->bl_mode); |
113 | return -EINVAL; |
114 | } |
115 | |
116 | bl->mode = cfg->bl_mode; |
117 | |
118 | return lp8788_write_byte(lp: bl->lp, LP8788_BL_CONFIG, data: val); |
119 | } |
120 | |
121 | static void lp8788_pwm_ctrl(struct lp8788_bl *bl, int br, int max_br) |
122 | { |
123 | unsigned int period; |
124 | unsigned int duty; |
125 | struct device *dev; |
126 | struct pwm_device *pwm; |
127 | |
128 | if (!bl->pdata) |
129 | return; |
130 | |
131 | period = bl->pdata->period_ns; |
132 | duty = br * period / max_br; |
133 | dev = bl->lp->dev; |
134 | |
135 | /* request PWM device with the consumer name */ |
136 | if (!bl->pwm) { |
137 | pwm = devm_pwm_get(dev, LP8788_DEV_BACKLIGHT); |
138 | if (IS_ERR(ptr: pwm)) { |
139 | dev_err(dev, "can not get PWM device\n" ); |
140 | return; |
141 | } |
142 | |
143 | bl->pwm = pwm; |
144 | |
145 | /* |
146 | * FIXME: pwm_apply_args() should be removed when switching to |
147 | * the atomic PWM API. |
148 | */ |
149 | pwm_apply_args(pwm); |
150 | } |
151 | |
152 | pwm_config(pwm: bl->pwm, duty_ns: duty, period_ns: period); |
153 | if (duty) |
154 | pwm_enable(pwm: bl->pwm); |
155 | else |
156 | pwm_disable(pwm: bl->pwm); |
157 | } |
158 | |
159 | static int lp8788_bl_update_status(struct backlight_device *bl_dev) |
160 | { |
161 | struct lp8788_bl *bl = bl_get_data(bl_dev); |
162 | enum lp8788_bl_ctrl_mode mode = bl->mode; |
163 | |
164 | if (bl_dev->props.state & BL_CORE_SUSPENDED) |
165 | bl_dev->props.brightness = 0; |
166 | |
167 | if (is_brightness_ctrl_by_pwm(mode)) { |
168 | int brt = bl_dev->props.brightness; |
169 | int max = bl_dev->props.max_brightness; |
170 | |
171 | lp8788_pwm_ctrl(bl, br: brt, max_br: max); |
172 | } else if (is_brightness_ctrl_by_register(mode)) { |
173 | u8 brt = bl_dev->props.brightness; |
174 | |
175 | lp8788_write_byte(lp: bl->lp, LP8788_BL_BRIGHTNESS, data: brt); |
176 | } |
177 | |
178 | return 0; |
179 | } |
180 | |
181 | static const struct backlight_ops lp8788_bl_ops = { |
182 | .options = BL_CORE_SUSPENDRESUME, |
183 | .update_status = lp8788_bl_update_status, |
184 | }; |
185 | |
186 | static int lp8788_backlight_register(struct lp8788_bl *bl) |
187 | { |
188 | struct backlight_device *bl_dev; |
189 | struct backlight_properties props; |
190 | struct lp8788_backlight_platform_data *pdata = bl->pdata; |
191 | int init_brt; |
192 | char *name; |
193 | |
194 | memset(&props, 0, sizeof(struct backlight_properties)); |
195 | props.type = BACKLIGHT_PLATFORM; |
196 | props.max_brightness = MAX_BRIGHTNESS; |
197 | |
198 | /* Initial brightness */ |
199 | if (pdata) |
200 | init_brt = min_t(int, pdata->initial_brightness, |
201 | props.max_brightness); |
202 | else |
203 | init_brt = 0; |
204 | |
205 | props.brightness = init_brt; |
206 | |
207 | /* Backlight device name */ |
208 | if (!pdata || !pdata->name) |
209 | name = DEFAULT_BL_NAME; |
210 | else |
211 | name = pdata->name; |
212 | |
213 | bl_dev = backlight_device_register(name, dev: bl->lp->dev, devdata: bl, |
214 | ops: &lp8788_bl_ops, props: &props); |
215 | if (IS_ERR(ptr: bl_dev)) |
216 | return PTR_ERR(ptr: bl_dev); |
217 | |
218 | bl->bl_dev = bl_dev; |
219 | |
220 | return 0; |
221 | } |
222 | |
223 | static void lp8788_backlight_unregister(struct lp8788_bl *bl) |
224 | { |
225 | struct backlight_device *bl_dev = bl->bl_dev; |
226 | |
227 | backlight_device_unregister(bd: bl_dev); |
228 | } |
229 | |
230 | static ssize_t lp8788_get_bl_ctl_mode(struct device *dev, |
231 | struct device_attribute *attr, char *buf) |
232 | { |
233 | struct lp8788_bl *bl = dev_get_drvdata(dev); |
234 | enum lp8788_bl_ctrl_mode mode = bl->mode; |
235 | char *strmode; |
236 | |
237 | if (is_brightness_ctrl_by_pwm(mode)) |
238 | strmode = "PWM based" ; |
239 | else if (is_brightness_ctrl_by_register(mode)) |
240 | strmode = "Register based" ; |
241 | else |
242 | strmode = "Invalid mode" ; |
243 | |
244 | return scnprintf(buf, PAGE_SIZE, fmt: "%s\n" , strmode); |
245 | } |
246 | |
247 | static DEVICE_ATTR(bl_ctl_mode, S_IRUGO, lp8788_get_bl_ctl_mode, NULL); |
248 | |
249 | static struct attribute *lp8788_attributes[] = { |
250 | &dev_attr_bl_ctl_mode.attr, |
251 | NULL, |
252 | }; |
253 | |
254 | static const struct attribute_group lp8788_attr_group = { |
255 | .attrs = lp8788_attributes, |
256 | }; |
257 | |
258 | static int lp8788_backlight_probe(struct platform_device *pdev) |
259 | { |
260 | struct lp8788 *lp = dev_get_drvdata(dev: pdev->dev.parent); |
261 | struct lp8788_bl *bl; |
262 | int ret; |
263 | |
264 | bl = devm_kzalloc(dev: lp->dev, size: sizeof(struct lp8788_bl), GFP_KERNEL); |
265 | if (!bl) |
266 | return -ENOMEM; |
267 | |
268 | bl->lp = lp; |
269 | if (lp->pdata) |
270 | bl->pdata = lp->pdata->bl_pdata; |
271 | |
272 | platform_set_drvdata(pdev, data: bl); |
273 | |
274 | ret = lp8788_backlight_configure(bl); |
275 | if (ret) { |
276 | dev_err(lp->dev, "backlight config err: %d\n" , ret); |
277 | goto err_dev; |
278 | } |
279 | |
280 | ret = lp8788_backlight_register(bl); |
281 | if (ret) { |
282 | dev_err(lp->dev, "register backlight err: %d\n" , ret); |
283 | goto err_dev; |
284 | } |
285 | |
286 | ret = sysfs_create_group(kobj: &pdev->dev.kobj, grp: &lp8788_attr_group); |
287 | if (ret) { |
288 | dev_err(lp->dev, "register sysfs err: %d\n" , ret); |
289 | goto err_sysfs; |
290 | } |
291 | |
292 | backlight_update_status(bd: bl->bl_dev); |
293 | |
294 | return 0; |
295 | |
296 | err_sysfs: |
297 | lp8788_backlight_unregister(bl); |
298 | err_dev: |
299 | return ret; |
300 | } |
301 | |
302 | static void lp8788_backlight_remove(struct platform_device *pdev) |
303 | { |
304 | struct lp8788_bl *bl = platform_get_drvdata(pdev); |
305 | struct backlight_device *bl_dev = bl->bl_dev; |
306 | |
307 | bl_dev->props.brightness = 0; |
308 | backlight_update_status(bd: bl_dev); |
309 | sysfs_remove_group(kobj: &pdev->dev.kobj, grp: &lp8788_attr_group); |
310 | lp8788_backlight_unregister(bl); |
311 | } |
312 | |
313 | static struct platform_driver lp8788_bl_driver = { |
314 | .probe = lp8788_backlight_probe, |
315 | .remove_new = lp8788_backlight_remove, |
316 | .driver = { |
317 | .name = LP8788_DEV_BACKLIGHT, |
318 | }, |
319 | }; |
320 | module_platform_driver(lp8788_bl_driver); |
321 | |
322 | MODULE_DESCRIPTION("Texas Instruments LP8788 Backlight Driver" ); |
323 | MODULE_AUTHOR("Milo Kim" ); |
324 | MODULE_LICENSE("GPL" ); |
325 | MODULE_ALIAS("platform:lp8788-backlight" ); |
326 | |