1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * Panel driver for the WideChips WS2401 480x800 DPI RGB panel, used in |
4 | * the Samsung Mobile Display (SMD) LMS380KF01. |
5 | * Found in the Samsung Galaxy Ace 2 GT-I8160 mobile phone. |
6 | * Linus Walleij <linus.walleij@linaro.org> |
7 | * Inspired by code and know-how in the vendor driver by Gareth Phillips. |
8 | */ |
9 | #include <drm/drm_mipi_dbi.h> |
10 | #include <drm/drm_modes.h> |
11 | #include <drm/drm_panel.h> |
12 | |
13 | #include <linux/backlight.h> |
14 | #include <linux/delay.h> |
15 | #include <linux/gpio/consumer.h> |
16 | #include <linux/init.h> |
17 | #include <linux/kernel.h> |
18 | #include <linux/media-bus-format.h> |
19 | #include <linux/module.h> |
20 | #include <linux/regulator/consumer.h> |
21 | #include <linux/spi/spi.h> |
22 | |
23 | #include <video/mipi_display.h> |
24 | |
25 | #define WS2401_RESCTL 0xb8 /* Resolution select control */ |
26 | #define WS2401_PSMPS 0xbd /* SMPS positive control */ |
27 | #define WS2401_NSMPS 0xbe /* SMPS negative control */ |
28 | #define WS2401_SMPS 0xbf |
29 | #define WS2401_BCMODE 0xc1 /* Backlight control mode */ |
30 | #define WS2401_WRBLCTL 0xc3 /* Backlight control */ |
31 | #define WS2401_WRDISBV 0xc4 /* Write manual brightness */ |
32 | #define WS2401_WRCTRLD 0xc6 /* Write BL control */ |
33 | #define WS2401_WRMIE 0xc7 /* Write MIE mode */ |
34 | #define WS2401_READ_ID1 0xda /* Read panel ID 1 */ |
35 | #define WS2401_READ_ID2 0xdb /* Read panel ID 2 */ |
36 | #define WS2401_READ_ID3 0xdc /* Read panel ID 3 */ |
37 | #define WS2401_GAMMA_R1 0xe7 /* Gamma red 1 */ |
38 | #define WS2401_GAMMA_G1 0xe8 /* Gamma green 1 */ |
39 | #define WS2401_GAMMA_B1 0xe9 /* Gamma blue 1 */ |
40 | #define WS2401_GAMMA_R2 0xea /* Gamma red 2 */ |
41 | #define WS2401_GAMMA_G2 0xeb /* Gamma green 2 */ |
42 | #define WS2401_GAMMA_B2 0xec /* Gamma blue 2 */ |
43 | #define WS2401_PASSWD1 0xf0 /* Password command for level 2 */ |
44 | #define WS2401_DISCTL 0xf2 /* Display control */ |
45 | #define WS2401_PWRCTL 0xf3 /* Power control */ |
46 | #define WS2401_VCOMCTL 0xf4 /* VCOM control */ |
47 | #define WS2401_SRCCTL 0xf5 /* Source control */ |
48 | #define WS2401_PANELCTL 0xf6 /* Panel control */ |
49 | |
50 | static const u8 ws2401_dbi_read_commands[] = { |
51 | WS2401_READ_ID1, |
52 | WS2401_READ_ID2, |
53 | WS2401_READ_ID3, |
54 | 0, /* sentinel */ |
55 | }; |
56 | |
57 | /** |
58 | * struct ws2401 - state container for a panel controlled by the WS2401 |
59 | * controller |
60 | */ |
61 | struct ws2401 { |
62 | /** @dev: the container device */ |
63 | struct device *dev; |
64 | /** @dbi: the DBI bus abstraction handle */ |
65 | struct mipi_dbi dbi; |
66 | /** @panel: the DRM panel instance for this device */ |
67 | struct drm_panel panel; |
68 | /** @width: the width of this panel in mm */ |
69 | u32 width; |
70 | /** @height: the height of this panel in mm */ |
71 | u32 height; |
72 | /** @reset: reset GPIO line */ |
73 | struct gpio_desc *reset; |
74 | /** @regulators: VCCIO and VIO supply regulators */ |
75 | struct regulator_bulk_data regulators[2]; |
76 | /** @internal_bl: If using internal backlight */ |
77 | bool internal_bl; |
78 | }; |
79 | |
80 | static const struct drm_display_mode lms380kf01_480_800_mode = { |
81 | /* |
82 | * The vendor driver states that the "SMD panel" has a clock |
83 | * frequency of 49920000 Hz / 2 = 24960000 Hz. |
84 | */ |
85 | .clock = 24960, |
86 | .hdisplay = 480, |
87 | .hsync_start = 480 + 8, |
88 | .hsync_end = 480 + 8 + 10, |
89 | .htotal = 480 + 8 + 10 + 8, |
90 | .vdisplay = 800, |
91 | .vsync_start = 800 + 8, |
92 | .vsync_end = 800 + 8 + 2, |
93 | .vtotal = 800 + 8 + 2 + 18, |
94 | .width_mm = 50, |
95 | .height_mm = 84, |
96 | .flags = DRM_MODE_FLAG_NVSYNC | DRM_MODE_FLAG_NHSYNC, |
97 | }; |
98 | |
99 | static inline struct ws2401 *to_ws2401(struct drm_panel *panel) |
100 | { |
101 | return container_of(panel, struct ws2401, panel); |
102 | } |
103 | |
104 | static void ws2401_read_mtp_id(struct ws2401 *ws) |
105 | { |
106 | struct mipi_dbi *dbi = &ws->dbi; |
107 | u8 id1, id2, id3; |
108 | int ret; |
109 | |
110 | ret = mipi_dbi_command_read(dbi, WS2401_READ_ID1, val: &id1); |
111 | if (ret) { |
112 | dev_err(ws->dev, "unable to read MTP ID 1\n" ); |
113 | return; |
114 | } |
115 | ret = mipi_dbi_command_read(dbi, WS2401_READ_ID2, val: &id2); |
116 | if (ret) { |
117 | dev_err(ws->dev, "unable to read MTP ID 2\n" ); |
118 | return; |
119 | } |
120 | ret = mipi_dbi_command_read(dbi, WS2401_READ_ID3, val: &id3); |
121 | if (ret) { |
122 | dev_err(ws->dev, "unable to read MTP ID 3\n" ); |
123 | return; |
124 | } |
125 | dev_info(ws->dev, "MTP ID: %02x %02x %02x\n" , id1, id2, id3); |
126 | } |
127 | |
128 | static int ws2401_power_on(struct ws2401 *ws) |
129 | { |
130 | struct mipi_dbi *dbi = &ws->dbi; |
131 | int ret; |
132 | |
133 | /* Power up */ |
134 | ret = regulator_bulk_enable(ARRAY_SIZE(ws->regulators), |
135 | consumers: ws->regulators); |
136 | if (ret) { |
137 | dev_err(ws->dev, "failed to enable regulators: %d\n" , ret); |
138 | return ret; |
139 | } |
140 | msleep(msecs: 10); |
141 | |
142 | /* Assert reset >=1 ms */ |
143 | gpiod_set_value_cansleep(desc: ws->reset, value: 1); |
144 | usleep_range(min: 1000, max: 5000); |
145 | /* De-assert reset */ |
146 | gpiod_set_value_cansleep(desc: ws->reset, value: 0); |
147 | /* Wait >= 10 ms */ |
148 | msleep(msecs: 10); |
149 | dev_dbg(ws->dev, "de-asserted RESET\n" ); |
150 | |
151 | /* |
152 | * Exit sleep mode and initialize display - some hammering is |
153 | * necessary. |
154 | */ |
155 | mipi_dbi_command(dbi, MIPI_DCS_EXIT_SLEEP_MODE); |
156 | mipi_dbi_command(dbi, MIPI_DCS_EXIT_SLEEP_MODE); |
157 | msleep(msecs: 50); |
158 | |
159 | /* Magic to unlock level 2 control of the display */ |
160 | mipi_dbi_command(dbi, WS2401_PASSWD1, 0x5a, 0x5a); |
161 | /* Configure resolution to 480RGBx800 */ |
162 | mipi_dbi_command(dbi, WS2401_RESCTL, 0x12); |
163 | /* Set addressing mode Flip V(d0), Flip H(d1) RGB/BGR(d3) */ |
164 | mipi_dbi_command(dbi, MIPI_DCS_SET_ADDRESS_MODE, 0x01); |
165 | /* Set pixel format: 24 bpp */ |
166 | mipi_dbi_command(dbi, MIPI_DCS_SET_PIXEL_FORMAT, 0x70); |
167 | mipi_dbi_command(dbi, WS2401_SMPS, 0x00, 0x0f); |
168 | mipi_dbi_command(dbi, WS2401_PSMPS, 0x06, 0x03, /* DDVDH: 4.6v */ |
169 | 0x7e, 0x03, 0x12, 0x37); |
170 | mipi_dbi_command(dbi, WS2401_NSMPS, 0x06, 0x03, /* DDVDH: -4.6v */ |
171 | 0x7e, 0x02, 0x15, 0x37); |
172 | mipi_dbi_command(dbi, WS2401_SMPS, 0x02, 0x0f); |
173 | mipi_dbi_command(dbi, WS2401_PWRCTL, 0x10, 0xA9, 0x00, 0x01, 0x44, |
174 | 0xb4, /* VGH:16.1v, VGL:-13.8v */ |
175 | 0x50, /* GREFP:4.2v (default) */ |
176 | 0x50, /* GREFN:-4.2v (default) */ |
177 | 0x00, |
178 | 0x44); /* VOUTL:-10v (default) */ |
179 | mipi_dbi_command(dbi, WS2401_DISCTL, 0x01, 0x00, 0x00, 0x00, 0x14, |
180 | 0x16); |
181 | mipi_dbi_command(dbi, WS2401_VCOMCTL, 0x30, 0x53, 0x53); |
182 | mipi_dbi_command(dbi, WS2401_SRCCTL, 0x03, 0x0C, 0x00, 0x00, 0x00, |
183 | 0x01, /* 2 dot inversion */ |
184 | 0x01, 0x06, 0x03); |
185 | mipi_dbi_command(dbi, WS2401_PANELCTL, 0x14, 0x00, 0x80, 0x00); |
186 | mipi_dbi_command(dbi, WS2401_WRMIE, 0x01); |
187 | |
188 | /* Set up gamma, probably these are P-gamma and N-gamma for each color */ |
189 | mipi_dbi_command(dbi, WS2401_GAMMA_R1, 0x00, |
190 | 0x5b, 0x42, 0x41, 0x3f, 0x42, 0x3d, 0x38, 0x2e, |
191 | 0x2b, 0x2a, 0x27, 0x22, 0x27, 0x0f, 0x00, 0x00); |
192 | mipi_dbi_command(dbi, WS2401_GAMMA_R2, 0x00, |
193 | 0x5b, 0x42, 0x41, 0x3f, 0x42, 0x3d, 0x38, 0x2e, |
194 | 0x2b, 0x2a, 0x27, 0x22, 0x27, 0x0f, 0x00, 0x00); |
195 | mipi_dbi_command(dbi, WS2401_GAMMA_G1, 0x00, |
196 | 0x59, 0x40, 0x3f, 0x3e, 0x41, 0x3d, 0x39, 0x2f, |
197 | 0x2c, 0x2b, 0x29, 0x25, 0x29, 0x19, 0x08, 0x00); |
198 | mipi_dbi_command(dbi, WS2401_GAMMA_G2, 0x00, |
199 | 0x59, 0x40, 0x3f, 0x3e, 0x41, 0x3d, 0x39, 0x2f, |
200 | 0x2c, 0x2b, 0x29, 0x25, 0x29, 0x19, 0x08, 0x00); |
201 | mipi_dbi_command(dbi, WS2401_GAMMA_B1, 0x00, |
202 | 0x57, 0x3b, 0x3a, 0x3b, 0x3f, 0x3b, 0x38, 0x27, |
203 | 0x38, 0x2a, 0x26, 0x22, 0x34, 0x0c, 0x09, 0x00); |
204 | mipi_dbi_command(dbi, WS2401_GAMMA_B2, 0x00, |
205 | 0x57, 0x3b, 0x3a, 0x3b, 0x3f, 0x3b, 0x38, 0x27, |
206 | 0x38, 0x2a, 0x26, 0x22, 0x34, 0x0c, 0x09, 0x00); |
207 | |
208 | if (ws->internal_bl) { |
209 | mipi_dbi_command(dbi, WS2401_WRCTRLD, 0x2c); |
210 | } else { |
211 | mipi_dbi_command(dbi, WS2401_WRCTRLD, 0x00); |
212 | /* |
213 | * When not using internal backlight we do not need any further |
214 | * L2 accesses to the panel so we close the door on our way out. |
215 | * Otherwise we need to leave the L2 door open. |
216 | */ |
217 | mipi_dbi_command(dbi, WS2401_PASSWD1, 0xa5, 0xa5); |
218 | } |
219 | |
220 | return 0; |
221 | } |
222 | |
223 | static int ws2401_power_off(struct ws2401 *ws) |
224 | { |
225 | /* Go into RESET and disable regulators */ |
226 | gpiod_set_value_cansleep(desc: ws->reset, value: 1); |
227 | return regulator_bulk_disable(ARRAY_SIZE(ws->regulators), |
228 | consumers: ws->regulators); |
229 | } |
230 | |
231 | static int ws2401_unprepare(struct drm_panel *panel) |
232 | { |
233 | struct ws2401 *ws = to_ws2401(panel); |
234 | struct mipi_dbi *dbi = &ws->dbi; |
235 | |
236 | /* Make sure we disable backlight, if any */ |
237 | if (ws->internal_bl) |
238 | mipi_dbi_command(dbi, WS2401_WRCTRLD, 0x00); |
239 | mipi_dbi_command(dbi, MIPI_DCS_ENTER_SLEEP_MODE); |
240 | msleep(msecs: 120); |
241 | return ws2401_power_off(ws: to_ws2401(panel)); |
242 | } |
243 | |
244 | static int ws2401_disable(struct drm_panel *panel) |
245 | { |
246 | struct ws2401 *ws = to_ws2401(panel); |
247 | struct mipi_dbi *dbi = &ws->dbi; |
248 | |
249 | mipi_dbi_command(dbi, MIPI_DCS_SET_DISPLAY_OFF); |
250 | msleep(msecs: 25); |
251 | |
252 | return 0; |
253 | } |
254 | |
255 | static int ws2401_prepare(struct drm_panel *panel) |
256 | { |
257 | return ws2401_power_on(ws: to_ws2401(panel)); |
258 | } |
259 | |
260 | static int ws2401_enable(struct drm_panel *panel) |
261 | { |
262 | struct ws2401 *ws = to_ws2401(panel); |
263 | struct mipi_dbi *dbi = &ws->dbi; |
264 | |
265 | mipi_dbi_command(dbi, MIPI_DCS_SET_DISPLAY_ON); |
266 | |
267 | return 0; |
268 | } |
269 | |
270 | /** |
271 | * ws2401_get_modes() - return the mode |
272 | * @panel: the panel to get the mode for |
273 | * @connector: reference to the central DRM connector control structure |
274 | */ |
275 | static int ws2401_get_modes(struct drm_panel *panel, |
276 | struct drm_connector *connector) |
277 | { |
278 | struct ws2401 *ws = to_ws2401(panel); |
279 | struct drm_display_mode *mode; |
280 | static const u32 bus_format = MEDIA_BUS_FMT_RGB888_1X24; |
281 | |
282 | /* |
283 | * We just support the LMS380KF01 so far, if we implement more panels |
284 | * this mode, the following connector display_info settings and |
285 | * probably the custom DCS sequences needs to selected based on what |
286 | * the target panel needs. |
287 | */ |
288 | mode = drm_mode_duplicate(dev: connector->dev, mode: &lms380kf01_480_800_mode); |
289 | if (!mode) { |
290 | dev_err(ws->dev, "failed to add mode\n" ); |
291 | return -ENOMEM; |
292 | } |
293 | |
294 | connector->display_info.bpc = 8; |
295 | connector->display_info.width_mm = mode->width_mm; |
296 | connector->display_info.height_mm = mode->height_mm; |
297 | connector->display_info.bus_flags = |
298 | DRM_BUS_FLAG_PIXDATA_DRIVE_NEGEDGE; |
299 | drm_display_info_set_bus_formats(info: &connector->display_info, |
300 | formats: &bus_format, num_formats: 1); |
301 | |
302 | drm_mode_set_name(mode); |
303 | mode->type = DRM_MODE_TYPE_DRIVER | DRM_MODE_TYPE_PREFERRED; |
304 | |
305 | drm_mode_probed_add(connector, mode); |
306 | |
307 | return 1; |
308 | } |
309 | |
310 | static const struct drm_panel_funcs ws2401_drm_funcs = { |
311 | .disable = ws2401_disable, |
312 | .unprepare = ws2401_unprepare, |
313 | .prepare = ws2401_prepare, |
314 | .enable = ws2401_enable, |
315 | .get_modes = ws2401_get_modes, |
316 | }; |
317 | |
318 | static int ws2401_set_brightness(struct backlight_device *bl) |
319 | { |
320 | struct ws2401 *ws = bl_get_data(bl_dev: bl); |
321 | struct mipi_dbi *dbi = &ws->dbi; |
322 | u8 brightness = backlight_get_brightness(bd: bl); |
323 | |
324 | if (backlight_is_blank(bd: bl)) { |
325 | mipi_dbi_command(dbi, WS2401_WRCTRLD, 0x00); |
326 | } else { |
327 | mipi_dbi_command(dbi, WS2401_WRCTRLD, 0x2c); |
328 | mipi_dbi_command(dbi, WS2401_WRDISBV, brightness); |
329 | } |
330 | |
331 | return 0; |
332 | } |
333 | |
334 | static const struct backlight_ops ws2401_bl_ops = { |
335 | .update_status = ws2401_set_brightness, |
336 | }; |
337 | |
338 | static const struct backlight_properties ws2401_bl_props = { |
339 | .type = BACKLIGHT_PLATFORM, |
340 | .brightness = 120, |
341 | .max_brightness = U8_MAX, |
342 | }; |
343 | |
344 | static int ws2401_probe(struct spi_device *spi) |
345 | { |
346 | struct device *dev = &spi->dev; |
347 | struct ws2401 *ws; |
348 | int ret; |
349 | |
350 | ws = devm_kzalloc(dev, size: sizeof(*ws), GFP_KERNEL); |
351 | if (!ws) |
352 | return -ENOMEM; |
353 | ws->dev = dev; |
354 | |
355 | /* |
356 | * VCI is the analog voltage supply |
357 | * VCCIO is the digital I/O voltage supply |
358 | */ |
359 | ws->regulators[0].supply = "vci" ; |
360 | ws->regulators[1].supply = "vccio" ; |
361 | ret = devm_regulator_bulk_get(dev, |
362 | ARRAY_SIZE(ws->regulators), |
363 | consumers: ws->regulators); |
364 | if (ret) |
365 | return dev_err_probe(dev, err: ret, fmt: "failed to get regulators\n" ); |
366 | |
367 | ws->reset = devm_gpiod_get(dev, con_id: "reset" , flags: GPIOD_OUT_HIGH); |
368 | if (IS_ERR(ptr: ws->reset)) { |
369 | ret = PTR_ERR(ptr: ws->reset); |
370 | return dev_err_probe(dev, err: ret, fmt: "no RESET GPIO\n" ); |
371 | } |
372 | |
373 | ret = mipi_dbi_spi_init(spi, dbi: &ws->dbi, NULL); |
374 | if (ret) |
375 | return dev_err_probe(dev, err: ret, fmt: "MIPI DBI init failed\n" ); |
376 | ws->dbi.read_commands = ws2401_dbi_read_commands; |
377 | |
378 | ws2401_power_on(ws); |
379 | ws2401_read_mtp_id(ws); |
380 | ws2401_power_off(ws); |
381 | |
382 | drm_panel_init(panel: &ws->panel, dev, funcs: &ws2401_drm_funcs, |
383 | DRM_MODE_CONNECTOR_DPI); |
384 | |
385 | ret = drm_panel_of_backlight(panel: &ws->panel); |
386 | if (ret) |
387 | return dev_err_probe(dev, err: ret, |
388 | fmt: "failed to get external backlight device\n" ); |
389 | |
390 | if (!ws->panel.backlight) { |
391 | dev_dbg(dev, "no external backlight, using internal backlight\n" ); |
392 | ws->panel.backlight = |
393 | devm_backlight_device_register(dev, name: "ws2401" , parent: dev, devdata: ws, |
394 | ops: &ws2401_bl_ops, props: &ws2401_bl_props); |
395 | if (IS_ERR(ptr: ws->panel.backlight)) |
396 | return dev_err_probe(dev, err: PTR_ERR(ptr: ws->panel.backlight), |
397 | fmt: "failed to register backlight device\n" ); |
398 | } else { |
399 | dev_dbg(dev, "using external backlight\n" ); |
400 | } |
401 | |
402 | spi_set_drvdata(spi, data: ws); |
403 | |
404 | drm_panel_add(panel: &ws->panel); |
405 | dev_dbg(dev, "added panel\n" ); |
406 | |
407 | return 0; |
408 | } |
409 | |
410 | static void ws2401_remove(struct spi_device *spi) |
411 | { |
412 | struct ws2401 *ws = spi_get_drvdata(spi); |
413 | |
414 | drm_panel_remove(panel: &ws->panel); |
415 | } |
416 | |
417 | /* |
418 | * Samsung LMS380KF01 is the one instance of this display controller that we |
419 | * know about, but if more are found, the controller can be parameterized |
420 | * here and used for other configurations. |
421 | */ |
422 | static const struct of_device_id ws2401_match[] = { |
423 | { .compatible = "samsung,lms380kf01" , }, |
424 | {}, |
425 | }; |
426 | MODULE_DEVICE_TABLE(of, ws2401_match); |
427 | |
428 | static const struct spi_device_id ws2401_ids[] = { |
429 | { "lms380kf01" }, |
430 | { }, |
431 | }; |
432 | MODULE_DEVICE_TABLE(spi, ws2401_ids); |
433 | |
434 | static struct spi_driver ws2401_driver = { |
435 | .probe = ws2401_probe, |
436 | .remove = ws2401_remove, |
437 | .id_table = ws2401_ids, |
438 | .driver = { |
439 | .name = "ws2401-panel" , |
440 | .of_match_table = ws2401_match, |
441 | }, |
442 | }; |
443 | module_spi_driver(ws2401_driver); |
444 | |
445 | MODULE_AUTHOR("Linus Walleij <linus.walleij@linaro.org>" ); |
446 | MODULE_DESCRIPTION("Samsung WS2401 panel driver" ); |
447 | MODULE_LICENSE("GPL v2" ); |
448 | |