1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* |
3 | * tdo24m - SPI-based drivers for Toppoly TDO24M series LCD panels |
4 | * |
5 | * Copyright (C) 2008 Marvell International Ltd. |
6 | * Eric Miao <eric.miao@marvell.com> |
7 | */ |
8 | |
9 | #include <linux/module.h> |
10 | #include <linux/kernel.h> |
11 | #include <linux/init.h> |
12 | #include <linux/device.h> |
13 | #include <linux/spi/spi.h> |
14 | #include <linux/spi/tdo24m.h> |
15 | #include <linux/fb.h> |
16 | #include <linux/lcd.h> |
17 | #include <linux/slab.h> |
18 | |
19 | #define POWER_IS_ON(pwr) ((pwr) <= FB_BLANK_NORMAL) |
20 | |
21 | #define TDO24M_SPI_BUFF_SIZE (4) |
22 | #define MODE_QVGA 0 |
23 | #define MODE_VGA 1 |
24 | |
25 | struct tdo24m { |
26 | struct spi_device *spi_dev; |
27 | struct lcd_device *lcd_dev; |
28 | |
29 | struct spi_message msg; |
30 | struct spi_transfer xfer; |
31 | uint8_t *buf; |
32 | |
33 | int (*adj_mode)(struct tdo24m *lcd, int mode); |
34 | int color_invert; |
35 | |
36 | int power; |
37 | int mode; |
38 | }; |
39 | |
40 | /* use bit 30, 31 as the indicator of command parameter number */ |
41 | #define CMD0(x) ((0 << 30) | (x)) |
42 | #define CMD1(x, x1) ((1 << 30) | ((x) << 9) | 0x100 | (x1)) |
43 | #define CMD2(x, x1, x2) ((2 << 30) | ((x) << 18) | 0x20000 |\ |
44 | ((x1) << 9) | 0x100 | (x2)) |
45 | #define CMD_NULL (-1) |
46 | |
47 | static const uint32_t lcd_panel_reset[] = { |
48 | CMD0(0x1), /* reset */ |
49 | CMD0(0x0), /* nop */ |
50 | CMD0(0x0), /* nop */ |
51 | CMD0(0x0), /* nop */ |
52 | CMD_NULL, |
53 | }; |
54 | |
55 | static const uint32_t lcd_panel_on[] = { |
56 | CMD0(0x29), /* Display ON */ |
57 | CMD2(0xB8, 0xFF, 0xF9), /* Output Control */ |
58 | CMD0(0x11), /* Sleep out */ |
59 | CMD1(0xB0, 0x16), /* Wake */ |
60 | CMD_NULL, |
61 | }; |
62 | |
63 | static const uint32_t lcd_panel_off[] = { |
64 | CMD0(0x28), /* Display OFF */ |
65 | CMD2(0xB8, 0x80, 0x02), /* Output Control */ |
66 | CMD0(0x10), /* Sleep in */ |
67 | CMD1(0xB0, 0x00), /* Deep stand by in */ |
68 | CMD_NULL, |
69 | }; |
70 | |
71 | static const uint32_t lcd_vga_pass_through_tdo24m[] = { |
72 | CMD1(0xB0, 0x16), |
73 | CMD1(0xBC, 0x80), |
74 | CMD1(0xE1, 0x00), |
75 | CMD1(0x36, 0x50), |
76 | CMD1(0x3B, 0x00), |
77 | CMD_NULL, |
78 | }; |
79 | |
80 | static const uint32_t lcd_qvga_pass_through_tdo24m[] = { |
81 | CMD1(0xB0, 0x16), |
82 | CMD1(0xBC, 0x81), |
83 | CMD1(0xE1, 0x00), |
84 | CMD1(0x36, 0x50), |
85 | CMD1(0x3B, 0x22), |
86 | CMD_NULL, |
87 | }; |
88 | |
89 | static const uint32_t lcd_vga_transfer_tdo24m[] = { |
90 | CMD1(0xcf, 0x02), /* Blanking period control (1) */ |
91 | CMD2(0xd0, 0x08, 0x04), /* Blanking period control (2) */ |
92 | CMD1(0xd1, 0x01), /* CKV timing control on/off */ |
93 | CMD2(0xd2, 0x14, 0x00), /* CKV 1,2 timing control */ |
94 | CMD2(0xd3, 0x1a, 0x0f), /* OEV timing control */ |
95 | CMD2(0xd4, 0x1f, 0xaf), /* ASW timing control (1) */ |
96 | CMD1(0xd5, 0x14), /* ASW timing control (2) */ |
97 | CMD0(0x21), /* Invert for normally black display */ |
98 | CMD0(0x29), /* Display on */ |
99 | CMD_NULL, |
100 | }; |
101 | |
102 | static const uint32_t lcd_qvga_transfer[] = { |
103 | CMD1(0xd6, 0x02), /* Blanking period control (1) */ |
104 | CMD2(0xd7, 0x08, 0x04), /* Blanking period control (2) */ |
105 | CMD1(0xd8, 0x01), /* CKV timing control on/off */ |
106 | CMD2(0xd9, 0x00, 0x08), /* CKV 1,2 timing control */ |
107 | CMD2(0xde, 0x05, 0x0a), /* OEV timing control */ |
108 | CMD2(0xdf, 0x0a, 0x19), /* ASW timing control (1) */ |
109 | CMD1(0xe0, 0x0a), /* ASW timing control (2) */ |
110 | CMD0(0x21), /* Invert for normally black display */ |
111 | CMD0(0x29), /* Display on */ |
112 | CMD_NULL, |
113 | }; |
114 | |
115 | static const uint32_t lcd_vga_pass_through_tdo35s[] = { |
116 | CMD1(0xB0, 0x16), |
117 | CMD1(0xBC, 0x80), |
118 | CMD1(0xE1, 0x00), |
119 | CMD1(0x3B, 0x00), |
120 | CMD_NULL, |
121 | }; |
122 | |
123 | static const uint32_t lcd_qvga_pass_through_tdo35s[] = { |
124 | CMD1(0xB0, 0x16), |
125 | CMD1(0xBC, 0x81), |
126 | CMD1(0xE1, 0x00), |
127 | CMD1(0x3B, 0x22), |
128 | CMD_NULL, |
129 | }; |
130 | |
131 | static const uint32_t lcd_vga_transfer_tdo35s[] = { |
132 | CMD1(0xcf, 0x02), /* Blanking period control (1) */ |
133 | CMD2(0xd0, 0x08, 0x04), /* Blanking period control (2) */ |
134 | CMD1(0xd1, 0x01), /* CKV timing control on/off */ |
135 | CMD2(0xd2, 0x00, 0x1e), /* CKV 1,2 timing control */ |
136 | CMD2(0xd3, 0x14, 0x28), /* OEV timing control */ |
137 | CMD2(0xd4, 0x28, 0x64), /* ASW timing control (1) */ |
138 | CMD1(0xd5, 0x28), /* ASW timing control (2) */ |
139 | CMD0(0x21), /* Invert for normally black display */ |
140 | CMD0(0x29), /* Display on */ |
141 | CMD_NULL, |
142 | }; |
143 | |
144 | static const uint32_t lcd_panel_config[] = { |
145 | CMD2(0xb8, 0xff, 0xf9), /* Output control */ |
146 | CMD0(0x11), /* sleep out */ |
147 | CMD1(0xba, 0x01), /* Display mode (1) */ |
148 | CMD1(0xbb, 0x00), /* Display mode (2) */ |
149 | CMD1(0x3a, 0x60), /* Display mode 18-bit RGB */ |
150 | CMD1(0xbf, 0x10), /* Drive system change control */ |
151 | CMD1(0xb1, 0x56), /* Booster operation setup */ |
152 | CMD1(0xb2, 0x33), /* Booster mode setup */ |
153 | CMD1(0xb3, 0x11), /* Booster frequency setup */ |
154 | CMD1(0xb4, 0x02), /* Op amp/system clock */ |
155 | CMD1(0xb5, 0x35), /* VCS voltage */ |
156 | CMD1(0xb6, 0x40), /* VCOM voltage */ |
157 | CMD1(0xb7, 0x03), /* External display signal */ |
158 | CMD1(0xbd, 0x00), /* ASW slew rate */ |
159 | CMD1(0xbe, 0x00), /* Dummy data for QuadData operation */ |
160 | CMD1(0xc0, 0x11), /* Sleep out FR count (A) */ |
161 | CMD1(0xc1, 0x11), /* Sleep out FR count (B) */ |
162 | CMD1(0xc2, 0x11), /* Sleep out FR count (C) */ |
163 | CMD2(0xc3, 0x20, 0x40), /* Sleep out FR count (D) */ |
164 | CMD2(0xc4, 0x60, 0xc0), /* Sleep out FR count (E) */ |
165 | CMD2(0xc5, 0x10, 0x20), /* Sleep out FR count (F) */ |
166 | CMD1(0xc6, 0xc0), /* Sleep out FR count (G) */ |
167 | CMD2(0xc7, 0x33, 0x43), /* Gamma 1 fine tuning (1) */ |
168 | CMD1(0xc8, 0x44), /* Gamma 1 fine tuning (2) */ |
169 | CMD1(0xc9, 0x33), /* Gamma 1 inclination adjustment */ |
170 | CMD1(0xca, 0x00), /* Gamma 1 blue offset adjustment */ |
171 | CMD2(0xec, 0x01, 0xf0), /* Horizontal clock cycles */ |
172 | CMD_NULL, |
173 | }; |
174 | |
175 | static int tdo24m_writes(struct tdo24m *lcd, const uint32_t *array) |
176 | { |
177 | struct spi_transfer *x = &lcd->xfer; |
178 | const uint32_t *p = array; |
179 | uint32_t data; |
180 | int nparams, err = 0; |
181 | |
182 | for (; *p != CMD_NULL; p++) { |
183 | if (!lcd->color_invert && *p == CMD0(0x21)) |
184 | continue; |
185 | |
186 | nparams = (*p >> 30) & 0x3; |
187 | |
188 | data = *p << (7 - nparams); |
189 | switch (nparams) { |
190 | case 0: |
191 | lcd->buf[0] = (data >> 8) & 0xff; |
192 | lcd->buf[1] = data & 0xff; |
193 | break; |
194 | case 1: |
195 | lcd->buf[0] = (data >> 16) & 0xff; |
196 | lcd->buf[1] = (data >> 8) & 0xff; |
197 | lcd->buf[2] = data & 0xff; |
198 | break; |
199 | case 2: |
200 | lcd->buf[0] = (data >> 24) & 0xff; |
201 | lcd->buf[1] = (data >> 16) & 0xff; |
202 | lcd->buf[2] = (data >> 8) & 0xff; |
203 | lcd->buf[3] = data & 0xff; |
204 | break; |
205 | default: |
206 | continue; |
207 | } |
208 | x->len = nparams + 2; |
209 | err = spi_sync(spi: lcd->spi_dev, message: &lcd->msg); |
210 | if (err) |
211 | break; |
212 | } |
213 | |
214 | return err; |
215 | } |
216 | |
217 | static int tdo24m_adj_mode(struct tdo24m *lcd, int mode) |
218 | { |
219 | switch (mode) { |
220 | case MODE_VGA: |
221 | tdo24m_writes(lcd, array: lcd_vga_pass_through_tdo24m); |
222 | tdo24m_writes(lcd, array: lcd_panel_config); |
223 | tdo24m_writes(lcd, array: lcd_vga_transfer_tdo24m); |
224 | break; |
225 | case MODE_QVGA: |
226 | tdo24m_writes(lcd, array: lcd_qvga_pass_through_tdo24m); |
227 | tdo24m_writes(lcd, array: lcd_panel_config); |
228 | tdo24m_writes(lcd, array: lcd_qvga_transfer); |
229 | break; |
230 | default: |
231 | return -EINVAL; |
232 | } |
233 | |
234 | lcd->mode = mode; |
235 | return 0; |
236 | } |
237 | |
238 | static int tdo35s_adj_mode(struct tdo24m *lcd, int mode) |
239 | { |
240 | switch (mode) { |
241 | case MODE_VGA: |
242 | tdo24m_writes(lcd, array: lcd_vga_pass_through_tdo35s); |
243 | tdo24m_writes(lcd, array: lcd_panel_config); |
244 | tdo24m_writes(lcd, array: lcd_vga_transfer_tdo35s); |
245 | break; |
246 | case MODE_QVGA: |
247 | tdo24m_writes(lcd, array: lcd_qvga_pass_through_tdo35s); |
248 | tdo24m_writes(lcd, array: lcd_panel_config); |
249 | tdo24m_writes(lcd, array: lcd_qvga_transfer); |
250 | break; |
251 | default: |
252 | return -EINVAL; |
253 | } |
254 | |
255 | lcd->mode = mode; |
256 | return 0; |
257 | } |
258 | |
259 | static int tdo24m_power_on(struct tdo24m *lcd) |
260 | { |
261 | int err; |
262 | |
263 | err = tdo24m_writes(lcd, array: lcd_panel_on); |
264 | if (err) |
265 | goto out; |
266 | |
267 | err = tdo24m_writes(lcd, array: lcd_panel_reset); |
268 | if (err) |
269 | goto out; |
270 | |
271 | err = lcd->adj_mode(lcd, lcd->mode); |
272 | out: |
273 | return err; |
274 | } |
275 | |
276 | static int tdo24m_power_off(struct tdo24m *lcd) |
277 | { |
278 | return tdo24m_writes(lcd, array: lcd_panel_off); |
279 | } |
280 | |
281 | static int tdo24m_power(struct tdo24m *lcd, int power) |
282 | { |
283 | int ret = 0; |
284 | |
285 | if (POWER_IS_ON(power) && !POWER_IS_ON(lcd->power)) |
286 | ret = tdo24m_power_on(lcd); |
287 | else if (!POWER_IS_ON(power) && POWER_IS_ON(lcd->power)) |
288 | ret = tdo24m_power_off(lcd); |
289 | |
290 | if (!ret) |
291 | lcd->power = power; |
292 | |
293 | return ret; |
294 | } |
295 | |
296 | |
297 | static int tdo24m_set_power(struct lcd_device *ld, int power) |
298 | { |
299 | struct tdo24m *lcd = lcd_get_data(ld_dev: ld); |
300 | |
301 | return tdo24m_power(lcd, power); |
302 | } |
303 | |
304 | static int tdo24m_get_power(struct lcd_device *ld) |
305 | { |
306 | struct tdo24m *lcd = lcd_get_data(ld_dev: ld); |
307 | |
308 | return lcd->power; |
309 | } |
310 | |
311 | static int tdo24m_set_mode(struct lcd_device *ld, struct fb_videomode *m) |
312 | { |
313 | struct tdo24m *lcd = lcd_get_data(ld_dev: ld); |
314 | int mode = MODE_QVGA; |
315 | |
316 | if (m->xres == 640 || m->xres == 480) |
317 | mode = MODE_VGA; |
318 | |
319 | if (lcd->mode == mode) |
320 | return 0; |
321 | |
322 | return lcd->adj_mode(lcd, mode); |
323 | } |
324 | |
325 | static struct lcd_ops tdo24m_ops = { |
326 | .get_power = tdo24m_get_power, |
327 | .set_power = tdo24m_set_power, |
328 | .set_mode = tdo24m_set_mode, |
329 | }; |
330 | |
331 | static int tdo24m_probe(struct spi_device *spi) |
332 | { |
333 | struct tdo24m *lcd; |
334 | struct spi_message *m; |
335 | struct spi_transfer *x; |
336 | struct tdo24m_platform_data *pdata; |
337 | enum tdo24m_model model; |
338 | int err; |
339 | |
340 | pdata = dev_get_platdata(dev: &spi->dev); |
341 | if (pdata) |
342 | model = pdata->model; |
343 | else |
344 | model = TDO24M; |
345 | |
346 | spi->bits_per_word = 8; |
347 | spi->mode = SPI_MODE_3; |
348 | err = spi_setup(spi); |
349 | if (err) |
350 | return err; |
351 | |
352 | lcd = devm_kzalloc(dev: &spi->dev, size: sizeof(struct tdo24m), GFP_KERNEL); |
353 | if (!lcd) |
354 | return -ENOMEM; |
355 | |
356 | lcd->spi_dev = spi; |
357 | lcd->power = FB_BLANK_POWERDOWN; |
358 | lcd->mode = MODE_VGA; /* default to VGA */ |
359 | |
360 | lcd->buf = devm_kzalloc(dev: &spi->dev, TDO24M_SPI_BUFF_SIZE, GFP_KERNEL); |
361 | if (lcd->buf == NULL) |
362 | return -ENOMEM; |
363 | |
364 | m = &lcd->msg; |
365 | x = &lcd->xfer; |
366 | |
367 | spi_message_init(m); |
368 | |
369 | x->cs_change = 0; |
370 | x->tx_buf = &lcd->buf[0]; |
371 | spi_message_add_tail(t: x, m); |
372 | |
373 | switch (model) { |
374 | case TDO24M: |
375 | lcd->color_invert = 1; |
376 | lcd->adj_mode = tdo24m_adj_mode; |
377 | break; |
378 | case TDO35S: |
379 | lcd->adj_mode = tdo35s_adj_mode; |
380 | lcd->color_invert = 0; |
381 | break; |
382 | default: |
383 | dev_err(&spi->dev, "Unsupported model" ); |
384 | return -EINVAL; |
385 | } |
386 | |
387 | lcd->lcd_dev = devm_lcd_device_register(dev: &spi->dev, name: "tdo24m" , parent: &spi->dev, |
388 | devdata: lcd, ops: &tdo24m_ops); |
389 | if (IS_ERR(ptr: lcd->lcd_dev)) |
390 | return PTR_ERR(ptr: lcd->lcd_dev); |
391 | |
392 | spi_set_drvdata(spi, data: lcd); |
393 | err = tdo24m_power(lcd, power: FB_BLANK_UNBLANK); |
394 | if (err) |
395 | return err; |
396 | |
397 | return 0; |
398 | } |
399 | |
400 | static void tdo24m_remove(struct spi_device *spi) |
401 | { |
402 | struct tdo24m *lcd = spi_get_drvdata(spi); |
403 | |
404 | tdo24m_power(lcd, power: FB_BLANK_POWERDOWN); |
405 | } |
406 | |
407 | #ifdef CONFIG_PM_SLEEP |
408 | static int tdo24m_suspend(struct device *dev) |
409 | { |
410 | struct tdo24m *lcd = dev_get_drvdata(dev); |
411 | |
412 | return tdo24m_power(lcd, power: FB_BLANK_POWERDOWN); |
413 | } |
414 | |
415 | static int tdo24m_resume(struct device *dev) |
416 | { |
417 | struct tdo24m *lcd = dev_get_drvdata(dev); |
418 | |
419 | return tdo24m_power(lcd, power: FB_BLANK_UNBLANK); |
420 | } |
421 | #endif |
422 | |
423 | static SIMPLE_DEV_PM_OPS(tdo24m_pm_ops, tdo24m_suspend, tdo24m_resume); |
424 | |
425 | /* Power down all displays on reboot, poweroff or halt */ |
426 | static void tdo24m_shutdown(struct spi_device *spi) |
427 | { |
428 | struct tdo24m *lcd = spi_get_drvdata(spi); |
429 | |
430 | tdo24m_power(lcd, power: FB_BLANK_POWERDOWN); |
431 | } |
432 | |
433 | static struct spi_driver tdo24m_driver = { |
434 | .driver = { |
435 | .name = "tdo24m" , |
436 | .pm = &tdo24m_pm_ops, |
437 | }, |
438 | .probe = tdo24m_probe, |
439 | .remove = tdo24m_remove, |
440 | .shutdown = tdo24m_shutdown, |
441 | }; |
442 | |
443 | module_spi_driver(tdo24m_driver); |
444 | |
445 | MODULE_AUTHOR("Eric Miao <eric.miao@marvell.com>" ); |
446 | MODULE_DESCRIPTION("Driver for Toppoly TDO24M LCD Panel" ); |
447 | MODULE_LICENSE("GPL" ); |
448 | MODULE_ALIAS("spi:tdo24m" ); |
449 | |