1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* |
3 | * Power control for Samsung LTV350QV Quarter VGA LCD Panel |
4 | * |
5 | * Copyright (C) 2006, 2007 Atmel Corporation |
6 | */ |
7 | #include <linux/delay.h> |
8 | #include <linux/err.h> |
9 | #include <linux/fb.h> |
10 | #include <linux/init.h> |
11 | #include <linux/lcd.h> |
12 | #include <linux/module.h> |
13 | #include <linux/slab.h> |
14 | #include <linux/spi/spi.h> |
15 | |
16 | #include "ltv350qv.h" |
17 | |
18 | #define POWER_IS_ON(pwr) ((pwr) <= FB_BLANK_NORMAL) |
19 | |
20 | struct ltv350qv { |
21 | struct spi_device *spi; |
22 | u8 *buffer; |
23 | int power; |
24 | struct lcd_device *ld; |
25 | }; |
26 | |
27 | /* |
28 | * The power-on and power-off sequences are taken from the |
29 | * LTV350QV-F04 data sheet from Samsung. The register definitions are |
30 | * taken from the S6F2002 command list also from Samsung. |
31 | * |
32 | * There's still some voodoo going on here, but it's a lot better than |
33 | * in the first incarnation of the driver where all we had was the raw |
34 | * numbers from the initialization sequence. |
35 | */ |
36 | static int ltv350qv_write_reg(struct ltv350qv *lcd, u8 reg, u16 val) |
37 | { |
38 | struct spi_message msg; |
39 | struct spi_transfer index_xfer = { |
40 | .len = 3, |
41 | .cs_change = 1, |
42 | }; |
43 | struct spi_transfer value_xfer = { |
44 | .len = 3, |
45 | }; |
46 | |
47 | spi_message_init(m: &msg); |
48 | |
49 | /* register index */ |
50 | lcd->buffer[0] = LTV_OPC_INDEX; |
51 | lcd->buffer[1] = 0x00; |
52 | lcd->buffer[2] = reg & 0x7f; |
53 | index_xfer.tx_buf = lcd->buffer; |
54 | spi_message_add_tail(t: &index_xfer, m: &msg); |
55 | |
56 | /* register value */ |
57 | lcd->buffer[4] = LTV_OPC_DATA; |
58 | lcd->buffer[5] = val >> 8; |
59 | lcd->buffer[6] = val; |
60 | value_xfer.tx_buf = lcd->buffer + 4; |
61 | spi_message_add_tail(t: &value_xfer, m: &msg); |
62 | |
63 | return spi_sync(spi: lcd->spi, message: &msg); |
64 | } |
65 | |
66 | /* The comments are taken straight from the data sheet */ |
67 | static int ltv350qv_power_on(struct ltv350qv *lcd) |
68 | { |
69 | int ret; |
70 | |
71 | /* Power On Reset Display off State */ |
72 | if (ltv350qv_write_reg(lcd, LTV_PWRCTL1, val: 0x0000)) |
73 | goto err; |
74 | usleep_range(min: 15000, max: 16000); |
75 | |
76 | /* Power Setting Function 1 */ |
77 | if (ltv350qv_write_reg(lcd, LTV_PWRCTL1, LTV_VCOM_DISABLE)) |
78 | goto err; |
79 | if (ltv350qv_write_reg(lcd, LTV_PWRCTL2, LTV_VCOML_ENABLE)) |
80 | goto err_power1; |
81 | |
82 | /* Power Setting Function 2 */ |
83 | if (ltv350qv_write_reg(lcd, LTV_PWRCTL1, |
84 | LTV_VCOM_DISABLE | LTV_DRIVE_CURRENT(5) |
85 | | LTV_SUPPLY_CURRENT(5))) |
86 | goto err_power2; |
87 | |
88 | msleep(msecs: 55); |
89 | |
90 | /* Instruction Setting */ |
91 | ret = ltv350qv_write_reg(lcd, LTV_IFCTL, |
92 | LTV_NMD | LTV_REV | LTV_NL(0x1d)); |
93 | ret |= ltv350qv_write_reg(lcd, LTV_DATACTL, |
94 | LTV_DS_SAME | LTV_CHS_480 |
95 | | LTV_DF_RGB | LTV_RGB_BGR); |
96 | ret |= ltv350qv_write_reg(lcd, LTV_ENTRY_MODE, |
97 | LTV_VSPL_ACTIVE_LOW |
98 | | LTV_HSPL_ACTIVE_LOW |
99 | | LTV_DPL_SAMPLE_RISING |
100 | | LTV_EPL_ACTIVE_LOW |
101 | | LTV_SS_RIGHT_TO_LEFT); |
102 | ret |= ltv350qv_write_reg(lcd, LTV_GATECTL1, LTV_CLW(3)); |
103 | ret |= ltv350qv_write_reg(lcd, LTV_GATECTL2, |
104 | LTV_NW_INV_1LINE | LTV_FWI(3)); |
105 | ret |= ltv350qv_write_reg(lcd, LTV_VBP, val: 0x000a); |
106 | ret |= ltv350qv_write_reg(lcd, LTV_HBP, val: 0x0021); |
107 | ret |= ltv350qv_write_reg(lcd, LTV_SOTCTL, LTV_SDT(3) | LTV_EQ(0)); |
108 | ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(0), val: 0x0103); |
109 | ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(1), val: 0x0301); |
110 | ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(2), val: 0x1f0f); |
111 | ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(3), val: 0x1f0f); |
112 | ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(4), val: 0x0707); |
113 | ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(5), val: 0x0307); |
114 | ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(6), val: 0x0707); |
115 | ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(7), val: 0x0000); |
116 | ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(8), val: 0x0004); |
117 | ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(9), val: 0x0000); |
118 | if (ret) |
119 | goto err_settings; |
120 | |
121 | /* Wait more than 2 frames */ |
122 | msleep(msecs: 20); |
123 | |
124 | /* Display On Sequence */ |
125 | ret = ltv350qv_write_reg(lcd, LTV_PWRCTL1, |
126 | LTV_VCOM_DISABLE | LTV_VCOMOUT_ENABLE |
127 | | LTV_POWER_ON | LTV_DRIVE_CURRENT(5) |
128 | | LTV_SUPPLY_CURRENT(5)); |
129 | ret |= ltv350qv_write_reg(lcd, LTV_GATECTL2, |
130 | LTV_NW_INV_1LINE | LTV_DSC | LTV_FWI(3)); |
131 | if (ret) |
132 | goto err_disp_on; |
133 | |
134 | /* Display should now be ON. Phew. */ |
135 | return 0; |
136 | |
137 | err_disp_on: |
138 | /* |
139 | * Try to recover. Error handling probably isn't very useful |
140 | * at this point, just make a best effort to switch the panel |
141 | * off. |
142 | */ |
143 | ltv350qv_write_reg(lcd, LTV_PWRCTL1, |
144 | LTV_VCOM_DISABLE | LTV_DRIVE_CURRENT(5) |
145 | | LTV_SUPPLY_CURRENT(5)); |
146 | ltv350qv_write_reg(lcd, LTV_GATECTL2, |
147 | LTV_NW_INV_1LINE | LTV_FWI(3)); |
148 | err_settings: |
149 | err_power2: |
150 | err_power1: |
151 | ltv350qv_write_reg(lcd, LTV_PWRCTL2, val: 0x0000); |
152 | usleep_range(min: 1000, max: 1100); |
153 | err: |
154 | ltv350qv_write_reg(lcd, LTV_PWRCTL1, LTV_VCOM_DISABLE); |
155 | return -EIO; |
156 | } |
157 | |
158 | static int ltv350qv_power_off(struct ltv350qv *lcd) |
159 | { |
160 | int ret; |
161 | |
162 | /* Display Off Sequence */ |
163 | ret = ltv350qv_write_reg(lcd, LTV_PWRCTL1, |
164 | LTV_VCOM_DISABLE |
165 | | LTV_DRIVE_CURRENT(5) |
166 | | LTV_SUPPLY_CURRENT(5)); |
167 | ret |= ltv350qv_write_reg(lcd, LTV_GATECTL2, |
168 | LTV_NW_INV_1LINE | LTV_FWI(3)); |
169 | |
170 | /* Power down setting 1 */ |
171 | ret |= ltv350qv_write_reg(lcd, LTV_PWRCTL2, val: 0x0000); |
172 | |
173 | /* Wait at least 1 ms */ |
174 | usleep_range(min: 1000, max: 1100); |
175 | |
176 | /* Power down setting 2 */ |
177 | ret |= ltv350qv_write_reg(lcd, LTV_PWRCTL1, LTV_VCOM_DISABLE); |
178 | |
179 | /* |
180 | * No point in trying to recover here. If we can't switch the |
181 | * panel off, what are we supposed to do other than inform the |
182 | * user about the failure? |
183 | */ |
184 | if (ret) |
185 | return -EIO; |
186 | |
187 | /* Display power should now be OFF */ |
188 | return 0; |
189 | } |
190 | |
191 | static int ltv350qv_power(struct ltv350qv *lcd, int power) |
192 | { |
193 | int ret = 0; |
194 | |
195 | if (POWER_IS_ON(power) && !POWER_IS_ON(lcd->power)) |
196 | ret = ltv350qv_power_on(lcd); |
197 | else if (!POWER_IS_ON(power) && POWER_IS_ON(lcd->power)) |
198 | ret = ltv350qv_power_off(lcd); |
199 | |
200 | if (!ret) |
201 | lcd->power = power; |
202 | |
203 | return ret; |
204 | } |
205 | |
206 | static int ltv350qv_set_power(struct lcd_device *ld, int power) |
207 | { |
208 | struct ltv350qv *lcd = lcd_get_data(ld_dev: ld); |
209 | |
210 | return ltv350qv_power(lcd, power); |
211 | } |
212 | |
213 | static int ltv350qv_get_power(struct lcd_device *ld) |
214 | { |
215 | struct ltv350qv *lcd = lcd_get_data(ld_dev: ld); |
216 | |
217 | return lcd->power; |
218 | } |
219 | |
220 | static struct lcd_ops ltv_ops = { |
221 | .get_power = ltv350qv_get_power, |
222 | .set_power = ltv350qv_set_power, |
223 | }; |
224 | |
225 | static int ltv350qv_probe(struct spi_device *spi) |
226 | { |
227 | struct ltv350qv *lcd; |
228 | struct lcd_device *ld; |
229 | int ret; |
230 | |
231 | lcd = devm_kzalloc(dev: &spi->dev, size: sizeof(struct ltv350qv), GFP_KERNEL); |
232 | if (!lcd) |
233 | return -ENOMEM; |
234 | |
235 | lcd->spi = spi; |
236 | lcd->power = FB_BLANK_POWERDOWN; |
237 | lcd->buffer = devm_kzalloc(dev: &spi->dev, size: 8, GFP_KERNEL); |
238 | if (!lcd->buffer) |
239 | return -ENOMEM; |
240 | |
241 | ld = devm_lcd_device_register(dev: &spi->dev, name: "ltv350qv" , parent: &spi->dev, devdata: lcd, |
242 | ops: <v_ops); |
243 | if (IS_ERR(ptr: ld)) |
244 | return PTR_ERR(ptr: ld); |
245 | |
246 | lcd->ld = ld; |
247 | |
248 | ret = ltv350qv_power(lcd, power: FB_BLANK_UNBLANK); |
249 | if (ret) |
250 | return ret; |
251 | |
252 | spi_set_drvdata(spi, data: lcd); |
253 | |
254 | return 0; |
255 | } |
256 | |
257 | static void ltv350qv_remove(struct spi_device *spi) |
258 | { |
259 | struct ltv350qv *lcd = spi_get_drvdata(spi); |
260 | |
261 | ltv350qv_power(lcd, power: FB_BLANK_POWERDOWN); |
262 | } |
263 | |
264 | #ifdef CONFIG_PM_SLEEP |
265 | static int ltv350qv_suspend(struct device *dev) |
266 | { |
267 | struct ltv350qv *lcd = dev_get_drvdata(dev); |
268 | |
269 | return ltv350qv_power(lcd, power: FB_BLANK_POWERDOWN); |
270 | } |
271 | |
272 | static int ltv350qv_resume(struct device *dev) |
273 | { |
274 | struct ltv350qv *lcd = dev_get_drvdata(dev); |
275 | |
276 | return ltv350qv_power(lcd, power: FB_BLANK_UNBLANK); |
277 | } |
278 | #endif |
279 | |
280 | static SIMPLE_DEV_PM_OPS(ltv350qv_pm_ops, ltv350qv_suspend, ltv350qv_resume); |
281 | |
282 | /* Power down all displays on reboot, poweroff or halt */ |
283 | static void ltv350qv_shutdown(struct spi_device *spi) |
284 | { |
285 | struct ltv350qv *lcd = spi_get_drvdata(spi); |
286 | |
287 | ltv350qv_power(lcd, power: FB_BLANK_POWERDOWN); |
288 | } |
289 | |
290 | static struct spi_driver ltv350qv_driver = { |
291 | .driver = { |
292 | .name = "ltv350qv" , |
293 | .pm = <v350qv_pm_ops, |
294 | }, |
295 | |
296 | .probe = ltv350qv_probe, |
297 | .remove = ltv350qv_remove, |
298 | .shutdown = ltv350qv_shutdown, |
299 | }; |
300 | |
301 | module_spi_driver(ltv350qv_driver); |
302 | |
303 | MODULE_AUTHOR("Haavard Skinnemoen (Atmel)" ); |
304 | MODULE_DESCRIPTION("Samsung LTV350QV LCD Driver" ); |
305 | MODULE_LICENSE("GPL" ); |
306 | MODULE_ALIAS("spi:ltv350qv" ); |
307 | |