1 | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | /* |
3 | * ams369fg06 AMOLED LCD panel driver. |
4 | * |
5 | * Copyright (c) 2011 Samsung Electronics Co., Ltd. |
6 | * Author: Jingoo Han <jg1.han@samsung.com> |
7 | * |
8 | * Derived from drivers/video/s6e63m0.c |
9 | */ |
10 | |
11 | #include <linux/backlight.h> |
12 | #include <linux/delay.h> |
13 | #include <linux/fb.h> |
14 | #include <linux/lcd.h> |
15 | #include <linux/module.h> |
16 | #include <linux/spi/spi.h> |
17 | #include <linux/wait.h> |
18 | |
19 | #define SLEEPMSEC 0x1000 |
20 | #define ENDDEF 0x2000 |
21 | #define DEFMASK 0xFF00 |
22 | #define COMMAND_ONLY 0xFE |
23 | #define DATA_ONLY 0xFF |
24 | |
25 | #define MAX_GAMMA_LEVEL 5 |
26 | #define GAMMA_TABLE_COUNT 21 |
27 | |
28 | #define MIN_BRIGHTNESS 0 |
29 | #define MAX_BRIGHTNESS 255 |
30 | #define DEFAULT_BRIGHTNESS 150 |
31 | |
32 | struct ams369fg06 { |
33 | struct device *dev; |
34 | struct spi_device *spi; |
35 | unsigned int power; |
36 | struct lcd_device *ld; |
37 | struct backlight_device *bd; |
38 | struct lcd_platform_data *lcd_pd; |
39 | }; |
40 | |
41 | static const unsigned short seq_display_on[] = { |
42 | 0x14, 0x03, |
43 | ENDDEF, 0x0000 |
44 | }; |
45 | |
46 | static const unsigned short seq_display_off[] = { |
47 | 0x14, 0x00, |
48 | ENDDEF, 0x0000 |
49 | }; |
50 | |
51 | static const unsigned short seq_stand_by_on[] = { |
52 | 0x1D, 0xA1, |
53 | SLEEPMSEC, 200, |
54 | ENDDEF, 0x0000 |
55 | }; |
56 | |
57 | static const unsigned short seq_stand_by_off[] = { |
58 | 0x1D, 0xA0, |
59 | SLEEPMSEC, 250, |
60 | ENDDEF, 0x0000 |
61 | }; |
62 | |
63 | static const unsigned short seq_setting[] = { |
64 | 0x31, 0x08, |
65 | 0x32, 0x14, |
66 | 0x30, 0x02, |
67 | 0x27, 0x01, |
68 | 0x12, 0x08, |
69 | 0x13, 0x08, |
70 | 0x15, 0x00, |
71 | 0x16, 0x00, |
72 | |
73 | 0xef, 0xd0, |
74 | DATA_ONLY, 0xe8, |
75 | |
76 | 0x39, 0x44, |
77 | 0x40, 0x00, |
78 | 0x41, 0x3f, |
79 | 0x42, 0x2a, |
80 | 0x43, 0x27, |
81 | 0x44, 0x27, |
82 | 0x45, 0x1f, |
83 | 0x46, 0x44, |
84 | 0x50, 0x00, |
85 | 0x51, 0x00, |
86 | 0x52, 0x17, |
87 | 0x53, 0x24, |
88 | 0x54, 0x26, |
89 | 0x55, 0x1f, |
90 | 0x56, 0x43, |
91 | 0x60, 0x00, |
92 | 0x61, 0x3f, |
93 | 0x62, 0x2a, |
94 | 0x63, 0x25, |
95 | 0x64, 0x24, |
96 | 0x65, 0x1b, |
97 | 0x66, 0x5c, |
98 | |
99 | 0x17, 0x22, |
100 | 0x18, 0x33, |
101 | 0x19, 0x03, |
102 | 0x1a, 0x01, |
103 | 0x22, 0xa4, |
104 | 0x23, 0x00, |
105 | 0x26, 0xa0, |
106 | |
107 | 0x1d, 0xa0, |
108 | SLEEPMSEC, 300, |
109 | |
110 | 0x14, 0x03, |
111 | |
112 | ENDDEF, 0x0000 |
113 | }; |
114 | |
115 | /* gamma value: 2.2 */ |
116 | static const unsigned int ams369fg06_22_250[] = { |
117 | 0x00, 0x3f, 0x2a, 0x27, 0x27, 0x1f, 0x44, |
118 | 0x00, 0x00, 0x17, 0x24, 0x26, 0x1f, 0x43, |
119 | 0x00, 0x3f, 0x2a, 0x25, 0x24, 0x1b, 0x5c, |
120 | }; |
121 | |
122 | static const unsigned int ams369fg06_22_200[] = { |
123 | 0x00, 0x3f, 0x28, 0x29, 0x27, 0x21, 0x3e, |
124 | 0x00, 0x00, 0x10, 0x25, 0x27, 0x20, 0x3d, |
125 | 0x00, 0x3f, 0x28, 0x27, 0x25, 0x1d, 0x53, |
126 | }; |
127 | |
128 | static const unsigned int ams369fg06_22_150[] = { |
129 | 0x00, 0x3f, 0x2d, 0x29, 0x28, 0x23, 0x37, |
130 | 0x00, 0x00, 0x0b, 0x25, 0x28, 0x22, 0x36, |
131 | 0x00, 0x3f, 0x2b, 0x28, 0x26, 0x1f, 0x4a, |
132 | }; |
133 | |
134 | static const unsigned int ams369fg06_22_100[] = { |
135 | 0x00, 0x3f, 0x30, 0x2a, 0x2b, 0x24, 0x2f, |
136 | 0x00, 0x00, 0x00, 0x25, 0x29, 0x24, 0x2e, |
137 | 0x00, 0x3f, 0x2f, 0x29, 0x29, 0x21, 0x3f, |
138 | }; |
139 | |
140 | static const unsigned int ams369fg06_22_50[] = { |
141 | 0x00, 0x3f, 0x3c, 0x2c, 0x2d, 0x27, 0x24, |
142 | 0x00, 0x00, 0x00, 0x22, 0x2a, 0x27, 0x23, |
143 | 0x00, 0x3f, 0x3b, 0x2c, 0x2b, 0x24, 0x31, |
144 | }; |
145 | |
146 | struct ams369fg06_gamma { |
147 | unsigned int *gamma_22_table[MAX_GAMMA_LEVEL]; |
148 | }; |
149 | |
150 | static struct ams369fg06_gamma gamma_table = { |
151 | .gamma_22_table[0] = (unsigned int *)&ams369fg06_22_50, |
152 | .gamma_22_table[1] = (unsigned int *)&ams369fg06_22_100, |
153 | .gamma_22_table[2] = (unsigned int *)&ams369fg06_22_150, |
154 | .gamma_22_table[3] = (unsigned int *)&ams369fg06_22_200, |
155 | .gamma_22_table[4] = (unsigned int *)&ams369fg06_22_250, |
156 | }; |
157 | |
158 | static int ams369fg06_spi_write_byte(struct ams369fg06 *lcd, int addr, int data) |
159 | { |
160 | u16 buf[1]; |
161 | struct spi_message msg; |
162 | |
163 | struct spi_transfer xfer = { |
164 | .len = 2, |
165 | .tx_buf = buf, |
166 | }; |
167 | |
168 | buf[0] = (addr << 8) | data; |
169 | |
170 | spi_message_init(m: &msg); |
171 | spi_message_add_tail(t: &xfer, m: &msg); |
172 | |
173 | return spi_sync(spi: lcd->spi, message: &msg); |
174 | } |
175 | |
176 | static int ams369fg06_spi_write(struct ams369fg06 *lcd, unsigned char address, |
177 | unsigned char command) |
178 | { |
179 | int ret = 0; |
180 | |
181 | if (address != DATA_ONLY) |
182 | ret = ams369fg06_spi_write_byte(lcd, addr: 0x70, data: address); |
183 | if (command != COMMAND_ONLY) |
184 | ret = ams369fg06_spi_write_byte(lcd, addr: 0x72, data: command); |
185 | |
186 | return ret; |
187 | } |
188 | |
189 | static int ams369fg06_panel_send_sequence(struct ams369fg06 *lcd, |
190 | const unsigned short *wbuf) |
191 | { |
192 | int ret = 0, i = 0; |
193 | |
194 | while ((wbuf[i] & DEFMASK) != ENDDEF) { |
195 | if ((wbuf[i] & DEFMASK) != SLEEPMSEC) { |
196 | ret = ams369fg06_spi_write(lcd, address: wbuf[i], command: wbuf[i+1]); |
197 | if (ret) |
198 | break; |
199 | } else { |
200 | msleep(msecs: wbuf[i+1]); |
201 | } |
202 | i += 2; |
203 | } |
204 | |
205 | return ret; |
206 | } |
207 | |
208 | static int _ams369fg06_gamma_ctl(struct ams369fg06 *lcd, |
209 | const unsigned int *gamma) |
210 | { |
211 | unsigned int i = 0; |
212 | int ret = 0; |
213 | |
214 | for (i = 0 ; i < GAMMA_TABLE_COUNT / 3; i++) { |
215 | ret = ams369fg06_spi_write(lcd, address: 0x40 + i, command: gamma[i]); |
216 | ret = ams369fg06_spi_write(lcd, address: 0x50 + i, command: gamma[i+7*1]); |
217 | ret = ams369fg06_spi_write(lcd, address: 0x60 + i, command: gamma[i+7*2]); |
218 | if (ret) { |
219 | dev_err(lcd->dev, "failed to set gamma table.\n" ); |
220 | goto gamma_err; |
221 | } |
222 | } |
223 | |
224 | gamma_err: |
225 | return ret; |
226 | } |
227 | |
228 | static int ams369fg06_gamma_ctl(struct ams369fg06 *lcd, int brightness) |
229 | { |
230 | int ret = 0; |
231 | int gamma = 0; |
232 | |
233 | if ((brightness >= 0) && (brightness <= 50)) |
234 | gamma = 0; |
235 | else if ((brightness > 50) && (brightness <= 100)) |
236 | gamma = 1; |
237 | else if ((brightness > 100) && (brightness <= 150)) |
238 | gamma = 2; |
239 | else if ((brightness > 150) && (brightness <= 200)) |
240 | gamma = 3; |
241 | else if ((brightness > 200) && (brightness <= 255)) |
242 | gamma = 4; |
243 | |
244 | ret = _ams369fg06_gamma_ctl(lcd, gamma: gamma_table.gamma_22_table[gamma]); |
245 | |
246 | return ret; |
247 | } |
248 | |
249 | static int ams369fg06_ldi_init(struct ams369fg06 *lcd) |
250 | { |
251 | int ret, i; |
252 | static const unsigned short *init_seq[] = { |
253 | seq_setting, |
254 | seq_stand_by_off, |
255 | }; |
256 | |
257 | for (i = 0; i < ARRAY_SIZE(init_seq); i++) { |
258 | ret = ams369fg06_panel_send_sequence(lcd, wbuf: init_seq[i]); |
259 | if (ret) |
260 | break; |
261 | } |
262 | |
263 | return ret; |
264 | } |
265 | |
266 | static int ams369fg06_ldi_enable(struct ams369fg06 *lcd) |
267 | { |
268 | int ret, i; |
269 | static const unsigned short *init_seq[] = { |
270 | seq_stand_by_off, |
271 | seq_display_on, |
272 | }; |
273 | |
274 | for (i = 0; i < ARRAY_SIZE(init_seq); i++) { |
275 | ret = ams369fg06_panel_send_sequence(lcd, wbuf: init_seq[i]); |
276 | if (ret) |
277 | break; |
278 | } |
279 | |
280 | return ret; |
281 | } |
282 | |
283 | static int ams369fg06_ldi_disable(struct ams369fg06 *lcd) |
284 | { |
285 | int ret, i; |
286 | |
287 | static const unsigned short *init_seq[] = { |
288 | seq_display_off, |
289 | seq_stand_by_on, |
290 | }; |
291 | |
292 | for (i = 0; i < ARRAY_SIZE(init_seq); i++) { |
293 | ret = ams369fg06_panel_send_sequence(lcd, wbuf: init_seq[i]); |
294 | if (ret) |
295 | break; |
296 | } |
297 | |
298 | return ret; |
299 | } |
300 | |
301 | static int ams369fg06_power_is_on(int power) |
302 | { |
303 | return power <= FB_BLANK_NORMAL; |
304 | } |
305 | |
306 | static int ams369fg06_power_on(struct ams369fg06 *lcd) |
307 | { |
308 | int ret = 0; |
309 | struct lcd_platform_data *pd; |
310 | struct backlight_device *bd; |
311 | |
312 | pd = lcd->lcd_pd; |
313 | bd = lcd->bd; |
314 | |
315 | if (pd->power_on) { |
316 | pd->power_on(lcd->ld, 1); |
317 | msleep(msecs: pd->power_on_delay); |
318 | } |
319 | |
320 | if (!pd->reset) { |
321 | dev_err(lcd->dev, "reset is NULL.\n" ); |
322 | return -EINVAL; |
323 | } |
324 | |
325 | pd->reset(lcd->ld); |
326 | msleep(msecs: pd->reset_delay); |
327 | |
328 | ret = ams369fg06_ldi_init(lcd); |
329 | if (ret) { |
330 | dev_err(lcd->dev, "failed to initialize ldi.\n" ); |
331 | return ret; |
332 | } |
333 | |
334 | ret = ams369fg06_ldi_enable(lcd); |
335 | if (ret) { |
336 | dev_err(lcd->dev, "failed to enable ldi.\n" ); |
337 | return ret; |
338 | } |
339 | |
340 | /* set brightness to current value after power on or resume. */ |
341 | ret = ams369fg06_gamma_ctl(lcd, brightness: bd->props.brightness); |
342 | if (ret) { |
343 | dev_err(lcd->dev, "lcd gamma setting failed.\n" ); |
344 | return ret; |
345 | } |
346 | |
347 | return 0; |
348 | } |
349 | |
350 | static int ams369fg06_power_off(struct ams369fg06 *lcd) |
351 | { |
352 | int ret; |
353 | struct lcd_platform_data *pd; |
354 | |
355 | pd = lcd->lcd_pd; |
356 | |
357 | ret = ams369fg06_ldi_disable(lcd); |
358 | if (ret) { |
359 | dev_err(lcd->dev, "lcd setting failed.\n" ); |
360 | return -EIO; |
361 | } |
362 | |
363 | msleep(msecs: pd->power_off_delay); |
364 | |
365 | if (pd->power_on) |
366 | pd->power_on(lcd->ld, 0); |
367 | |
368 | return 0; |
369 | } |
370 | |
371 | static int ams369fg06_power(struct ams369fg06 *lcd, int power) |
372 | { |
373 | int ret = 0; |
374 | |
375 | if (ams369fg06_power_is_on(power) && |
376 | !ams369fg06_power_is_on(power: lcd->power)) |
377 | ret = ams369fg06_power_on(lcd); |
378 | else if (!ams369fg06_power_is_on(power) && |
379 | ams369fg06_power_is_on(power: lcd->power)) |
380 | ret = ams369fg06_power_off(lcd); |
381 | |
382 | if (!ret) |
383 | lcd->power = power; |
384 | |
385 | return ret; |
386 | } |
387 | |
388 | static int ams369fg06_get_power(struct lcd_device *ld) |
389 | { |
390 | struct ams369fg06 *lcd = lcd_get_data(ld_dev: ld); |
391 | |
392 | return lcd->power; |
393 | } |
394 | |
395 | static int ams369fg06_set_power(struct lcd_device *ld, int power) |
396 | { |
397 | struct ams369fg06 *lcd = lcd_get_data(ld_dev: ld); |
398 | |
399 | if (power != FB_BLANK_UNBLANK && power != FB_BLANK_POWERDOWN && |
400 | power != FB_BLANK_NORMAL) { |
401 | dev_err(lcd->dev, "power value should be 0, 1 or 4.\n" ); |
402 | return -EINVAL; |
403 | } |
404 | |
405 | return ams369fg06_power(lcd, power); |
406 | } |
407 | |
408 | static int ams369fg06_set_brightness(struct backlight_device *bd) |
409 | { |
410 | int ret = 0; |
411 | int brightness = bd->props.brightness; |
412 | struct ams369fg06 *lcd = bl_get_data(bl_dev: bd); |
413 | |
414 | if (brightness < MIN_BRIGHTNESS || |
415 | brightness > bd->props.max_brightness) { |
416 | dev_err(&bd->dev, "lcd brightness should be %d to %d.\n" , |
417 | MIN_BRIGHTNESS, MAX_BRIGHTNESS); |
418 | return -EINVAL; |
419 | } |
420 | |
421 | ret = ams369fg06_gamma_ctl(lcd, brightness: bd->props.brightness); |
422 | if (ret) { |
423 | dev_err(&bd->dev, "lcd brightness setting failed.\n" ); |
424 | return -EIO; |
425 | } |
426 | |
427 | return ret; |
428 | } |
429 | |
430 | static struct lcd_ops ams369fg06_lcd_ops = { |
431 | .get_power = ams369fg06_get_power, |
432 | .set_power = ams369fg06_set_power, |
433 | }; |
434 | |
435 | static const struct backlight_ops ams369fg06_backlight_ops = { |
436 | .update_status = ams369fg06_set_brightness, |
437 | }; |
438 | |
439 | static int ams369fg06_probe(struct spi_device *spi) |
440 | { |
441 | int ret = 0; |
442 | struct ams369fg06 *lcd = NULL; |
443 | struct lcd_device *ld = NULL; |
444 | struct backlight_device *bd = NULL; |
445 | struct backlight_properties props; |
446 | |
447 | lcd = devm_kzalloc(dev: &spi->dev, size: sizeof(struct ams369fg06), GFP_KERNEL); |
448 | if (!lcd) |
449 | return -ENOMEM; |
450 | |
451 | /* ams369fg06 lcd panel uses 3-wire 16bits SPI Mode. */ |
452 | spi->bits_per_word = 16; |
453 | |
454 | ret = spi_setup(spi); |
455 | if (ret < 0) { |
456 | dev_err(&spi->dev, "spi setup failed.\n" ); |
457 | return ret; |
458 | } |
459 | |
460 | lcd->spi = spi; |
461 | lcd->dev = &spi->dev; |
462 | |
463 | lcd->lcd_pd = dev_get_platdata(dev: &spi->dev); |
464 | if (!lcd->lcd_pd) { |
465 | dev_err(&spi->dev, "platform data is NULL\n" ); |
466 | return -EINVAL; |
467 | } |
468 | |
469 | ld = devm_lcd_device_register(dev: &spi->dev, name: "ams369fg06" , parent: &spi->dev, devdata: lcd, |
470 | ops: &ams369fg06_lcd_ops); |
471 | if (IS_ERR(ptr: ld)) |
472 | return PTR_ERR(ptr: ld); |
473 | |
474 | lcd->ld = ld; |
475 | |
476 | memset(&props, 0, sizeof(struct backlight_properties)); |
477 | props.type = BACKLIGHT_RAW; |
478 | props.max_brightness = MAX_BRIGHTNESS; |
479 | |
480 | bd = devm_backlight_device_register(dev: &spi->dev, name: "ams369fg06-bl" , |
481 | parent: &spi->dev, devdata: lcd, |
482 | ops: &ams369fg06_backlight_ops, props: &props); |
483 | if (IS_ERR(ptr: bd)) |
484 | return PTR_ERR(ptr: bd); |
485 | |
486 | bd->props.brightness = DEFAULT_BRIGHTNESS; |
487 | lcd->bd = bd; |
488 | |
489 | if (!lcd->lcd_pd->lcd_enabled) { |
490 | /* |
491 | * if lcd panel was off from bootloader then |
492 | * current lcd status is powerdown and then |
493 | * it enables lcd panel. |
494 | */ |
495 | lcd->power = FB_BLANK_POWERDOWN; |
496 | |
497 | ams369fg06_power(lcd, power: FB_BLANK_UNBLANK); |
498 | } else { |
499 | lcd->power = FB_BLANK_UNBLANK; |
500 | } |
501 | |
502 | spi_set_drvdata(spi, data: lcd); |
503 | |
504 | dev_info(&spi->dev, "ams369fg06 panel driver has been probed.\n" ); |
505 | |
506 | return 0; |
507 | } |
508 | |
509 | static void ams369fg06_remove(struct spi_device *spi) |
510 | { |
511 | struct ams369fg06 *lcd = spi_get_drvdata(spi); |
512 | |
513 | ams369fg06_power(lcd, power: FB_BLANK_POWERDOWN); |
514 | } |
515 | |
516 | #ifdef CONFIG_PM_SLEEP |
517 | static int ams369fg06_suspend(struct device *dev) |
518 | { |
519 | struct ams369fg06 *lcd = dev_get_drvdata(dev); |
520 | |
521 | dev_dbg(dev, "lcd->power = %d\n" , lcd->power); |
522 | |
523 | /* |
524 | * when lcd panel is suspend, lcd panel becomes off |
525 | * regardless of status. |
526 | */ |
527 | return ams369fg06_power(lcd, power: FB_BLANK_POWERDOWN); |
528 | } |
529 | |
530 | static int ams369fg06_resume(struct device *dev) |
531 | { |
532 | struct ams369fg06 *lcd = dev_get_drvdata(dev); |
533 | |
534 | lcd->power = FB_BLANK_POWERDOWN; |
535 | |
536 | return ams369fg06_power(lcd, power: FB_BLANK_UNBLANK); |
537 | } |
538 | #endif |
539 | |
540 | static SIMPLE_DEV_PM_OPS(ams369fg06_pm_ops, ams369fg06_suspend, |
541 | ams369fg06_resume); |
542 | |
543 | static void ams369fg06_shutdown(struct spi_device *spi) |
544 | { |
545 | struct ams369fg06 *lcd = spi_get_drvdata(spi); |
546 | |
547 | ams369fg06_power(lcd, power: FB_BLANK_POWERDOWN); |
548 | } |
549 | |
550 | static struct spi_driver ams369fg06_driver = { |
551 | .driver = { |
552 | .name = "ams369fg06" , |
553 | .pm = &ams369fg06_pm_ops, |
554 | }, |
555 | .probe = ams369fg06_probe, |
556 | .remove = ams369fg06_remove, |
557 | .shutdown = ams369fg06_shutdown, |
558 | }; |
559 | |
560 | module_spi_driver(ams369fg06_driver); |
561 | |
562 | MODULE_AUTHOR("Jingoo Han <jg1.han@samsung.com>" ); |
563 | MODULE_DESCRIPTION("ams369fg06 LCD Driver" ); |
564 | MODULE_LICENSE("GPL" ); |
565 | |