1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* |
3 | * ld9040 AMOLED LCD drm_panel driver. |
4 | * |
5 | * Copyright (c) 2014 Samsung Electronics Co., Ltd |
6 | * Derived from drivers/video/backlight/ld9040.c |
7 | * |
8 | * Andrzej Hajda <a.hajda@samsung.com> |
9 | */ |
10 | |
11 | #include <linux/backlight.h> |
12 | #include <linux/delay.h> |
13 | #include <linux/gpio/consumer.h> |
14 | #include <linux/module.h> |
15 | #include <linux/of.h> |
16 | #include <linux/regulator/consumer.h> |
17 | #include <linux/spi/spi.h> |
18 | |
19 | #include <video/mipi_display.h> |
20 | #include <video/of_videomode.h> |
21 | #include <video/videomode.h> |
22 | |
23 | #include <drm/drm_modes.h> |
24 | #include <drm/drm_panel.h> |
25 | |
26 | /* Manufacturer Command Set */ |
27 | #define MCS_MANPWR 0xb0 |
28 | #define MCS_ELVSS_ON 0xb1 |
29 | #define MCS_USER_SETTING 0xf0 |
30 | #define MCS_DISPCTL 0xf2 |
31 | #define MCS_POWER_CTRL 0xf4 |
32 | #define MCS_GTCON 0xf7 |
33 | #define MCS_PANEL_CONDITION 0xf8 |
34 | #define MCS_GAMMA_SET1 0xf9 |
35 | #define MCS_GAMMA_CTRL 0xfb |
36 | |
37 | /* array of gamma tables for gamma value 2.2 */ |
38 | static u8 const ld9040_gammas[25][22] = { |
39 | { 0xf9, 0x00, 0x13, 0xb2, 0xba, 0xd2, 0x00, 0x30, 0x00, 0xaf, 0xc0, |
40 | 0xb8, 0xcd, 0x00, 0x3d, 0x00, 0xa8, 0xb8, 0xb7, 0xcd, 0x00, 0x44 }, |
41 | { 0xf9, 0x00, 0x13, 0xb9, 0xb9, 0xd0, 0x00, 0x3c, 0x00, 0xaf, 0xbf, |
42 | 0xb6, 0xcb, 0x00, 0x4b, 0x00, 0xa8, 0xb9, 0xb5, 0xcc, 0x00, 0x52 }, |
43 | { 0xf9, 0x00, 0x13, 0xba, 0xb9, 0xcd, 0x00, 0x41, 0x00, 0xb0, 0xbe, |
44 | 0xb5, 0xc9, 0x00, 0x51, 0x00, 0xa9, 0xb9, 0xb5, 0xca, 0x00, 0x57 }, |
45 | { 0xf9, 0x00, 0x13, 0xb9, 0xb8, 0xcd, 0x00, 0x46, 0x00, 0xb1, 0xbc, |
46 | 0xb5, 0xc8, 0x00, 0x56, 0x00, 0xaa, 0xb8, 0xb4, 0xc9, 0x00, 0x5d }, |
47 | { 0xf9, 0x00, 0x13, 0xba, 0xb8, 0xcb, 0x00, 0x4b, 0x00, 0xb3, 0xbc, |
48 | 0xb4, 0xc7, 0x00, 0x5c, 0x00, 0xac, 0xb8, 0xb4, 0xc8, 0x00, 0x62 }, |
49 | { 0xf9, 0x00, 0x13, 0xbb, 0xb7, 0xca, 0x00, 0x4f, 0x00, 0xb4, 0xbb, |
50 | 0xb3, 0xc7, 0x00, 0x60, 0x00, 0xad, 0xb8, 0xb4, 0xc7, 0x00, 0x67 }, |
51 | { 0xf9, 0x00, 0x47, 0xba, 0xb6, 0xca, 0x00, 0x53, 0x00, 0xb5, 0xbb, |
52 | 0xb3, 0xc6, 0x00, 0x65, 0x00, 0xae, 0xb8, 0xb3, 0xc7, 0x00, 0x6c }, |
53 | { 0xf9, 0x00, 0x71, 0xbb, 0xb5, 0xc8, 0x00, 0x57, 0x00, 0xb5, 0xbb, |
54 | 0xb0, 0xc5, 0x00, 0x6a, 0x00, 0xae, 0xb9, 0xb1, 0xc6, 0x00, 0x70 }, |
55 | { 0xf9, 0x00, 0x7b, 0xbb, 0xb4, 0xc8, 0x00, 0x5b, 0x00, 0xb5, 0xba, |
56 | 0xb1, 0xc4, 0x00, 0x6e, 0x00, 0xae, 0xb9, 0xb0, 0xc5, 0x00, 0x75 }, |
57 | { 0xf9, 0x00, 0x82, 0xba, 0xb4, 0xc7, 0x00, 0x5f, 0x00, 0xb5, 0xba, |
58 | 0xb0, 0xc3, 0x00, 0x72, 0x00, 0xae, 0xb8, 0xb0, 0xc3, 0x00, 0x7a }, |
59 | { 0xf9, 0x00, 0x89, 0xba, 0xb3, 0xc8, 0x00, 0x62, 0x00, 0xb6, 0xba, |
60 | 0xaf, 0xc3, 0x00, 0x76, 0x00, 0xaf, 0xb7, 0xae, 0xc4, 0x00, 0x7e }, |
61 | { 0xf9, 0x00, 0x8b, 0xb9, 0xb3, 0xc7, 0x00, 0x65, 0x00, 0xb7, 0xb8, |
62 | 0xaf, 0xc3, 0x00, 0x7a, 0x00, 0x80, 0xb6, 0xae, 0xc4, 0x00, 0x81 }, |
63 | { 0xf9, 0x00, 0x93, 0xba, 0xb3, 0xc5, 0x00, 0x69, 0x00, 0xb8, 0xb9, |
64 | 0xae, 0xc1, 0x00, 0x7f, 0x00, 0xb0, 0xb6, 0xae, 0xc3, 0x00, 0x85 }, |
65 | { 0xf9, 0x00, 0x97, 0xba, 0xb2, 0xc5, 0x00, 0x6c, 0x00, 0xb8, 0xb8, |
66 | 0xae, 0xc1, 0x00, 0x82, 0x00, 0xb0, 0xb6, 0xae, 0xc2, 0x00, 0x89 }, |
67 | { 0xf9, 0x00, 0x9a, 0xba, 0xb1, 0xc4, 0x00, 0x6f, 0x00, 0xb8, 0xb8, |
68 | 0xad, 0xc0, 0x00, 0x86, 0x00, 0xb0, 0xb7, 0xad, 0xc0, 0x00, 0x8d }, |
69 | { 0xf9, 0x00, 0x9c, 0xb9, 0xb0, 0xc4, 0x00, 0x72, 0x00, 0xb8, 0xb8, |
70 | 0xac, 0xbf, 0x00, 0x8a, 0x00, 0xb0, 0xb6, 0xac, 0xc0, 0x00, 0x91 }, |
71 | { 0xf9, 0x00, 0x9e, 0xba, 0xb0, 0xc2, 0x00, 0x75, 0x00, 0xb9, 0xb8, |
72 | 0xab, 0xbe, 0x00, 0x8e, 0x00, 0xb0, 0xb6, 0xac, 0xbf, 0x00, 0x94 }, |
73 | { 0xf9, 0x00, 0xa0, 0xb9, 0xaf, 0xc3, 0x00, 0x77, 0x00, 0xb9, 0xb7, |
74 | 0xab, 0xbe, 0x00, 0x90, 0x00, 0xb0, 0xb6, 0xab, 0xbf, 0x00, 0x97 }, |
75 | { 0xf9, 0x00, 0xa2, 0xb9, 0xaf, 0xc2, 0x00, 0x7a, 0x00, 0xb9, 0xb7, |
76 | 0xaa, 0xbd, 0x00, 0x94, 0x00, 0xb0, 0xb5, 0xab, 0xbf, 0x00, 0x9a }, |
77 | { 0xf9, 0x00, 0xa4, 0xb9, 0xaf, 0xc1, 0x00, 0x7d, 0x00, 0xb9, 0xb6, |
78 | 0xaa, 0xbb, 0x00, 0x97, 0x00, 0xb1, 0xb5, 0xaa, 0xbf, 0x00, 0x9d }, |
79 | { 0xf9, 0x00, 0xa4, 0xb8, 0xb0, 0xbf, 0x00, 0x80, 0x00, 0xb8, 0xb6, |
80 | 0xaa, 0xbc, 0x00, 0x9a, 0x00, 0xb0, 0xb5, 0xab, 0xbd, 0x00, 0xa0 }, |
81 | { 0xf9, 0x00, 0xa8, 0xb8, 0xae, 0xbe, 0x00, 0x84, 0x00, 0xb9, 0xb7, |
82 | 0xa8, 0xbc, 0x00, 0x9d, 0x00, 0xb2, 0xb5, 0xaa, 0xbc, 0x00, 0xa4 }, |
83 | { 0xf9, 0x00, 0xa9, 0xb6, 0xad, 0xbf, 0x00, 0x86, 0x00, 0xb8, 0xb5, |
84 | 0xa8, 0xbc, 0x00, 0xa0, 0x00, 0xb3, 0xb3, 0xa9, 0xbc, 0x00, 0xa7 }, |
85 | { 0xf9, 0x00, 0xa9, 0xb7, 0xae, 0xbd, 0x00, 0x89, 0x00, 0xb7, 0xb6, |
86 | 0xa8, 0xba, 0x00, 0xa4, 0x00, 0xb1, 0xb4, 0xaa, 0xbb, 0x00, 0xaa }, |
87 | { 0xf9, 0x00, 0xa7, 0xb4, 0xae, 0xbf, 0x00, 0x91, 0x00, 0xb2, 0xb4, |
88 | 0xaa, 0xbb, 0x00, 0xac, 0x00, 0xb3, 0xb1, 0xaa, 0xbc, 0x00, 0xb3 }, |
89 | }; |
90 | |
91 | struct ld9040 { |
92 | struct device *dev; |
93 | struct drm_panel panel; |
94 | |
95 | struct regulator_bulk_data supplies[2]; |
96 | struct gpio_desc *reset_gpio; |
97 | u32 power_on_delay; |
98 | u32 reset_delay; |
99 | struct videomode vm; |
100 | u32 width_mm; |
101 | u32 height_mm; |
102 | |
103 | int brightness; |
104 | |
105 | /* This field is tested by functions directly accessing bus before |
106 | * transfer, transfer is skipped if it is set. In case of transfer |
107 | * failure or unexpected response the field is set to error value. |
108 | * Such construct allows to eliminate many checks in higher level |
109 | * functions. |
110 | */ |
111 | int error; |
112 | }; |
113 | |
114 | static inline struct ld9040 *panel_to_ld9040(struct drm_panel *panel) |
115 | { |
116 | return container_of(panel, struct ld9040, panel); |
117 | } |
118 | |
119 | static int ld9040_clear_error(struct ld9040 *ctx) |
120 | { |
121 | int ret = ctx->error; |
122 | |
123 | ctx->error = 0; |
124 | return ret; |
125 | } |
126 | |
127 | static int ld9040_spi_write_word(struct ld9040 *ctx, u16 data) |
128 | { |
129 | struct spi_device *spi = to_spi_device(dev: ctx->dev); |
130 | struct spi_transfer xfer = { |
131 | .len = 2, |
132 | .tx_buf = &data, |
133 | }; |
134 | struct spi_message msg; |
135 | |
136 | spi_message_init(m: &msg); |
137 | spi_message_add_tail(t: &xfer, m: &msg); |
138 | |
139 | return spi_sync(spi, message: &msg); |
140 | } |
141 | |
142 | static void ld9040_dcs_write(struct ld9040 *ctx, const u8 *data, size_t len) |
143 | { |
144 | int ret = 0; |
145 | |
146 | if (ctx->error < 0 || len == 0) |
147 | return; |
148 | |
149 | dev_dbg(ctx->dev, "writing dcs seq: %*ph\n" , (int)len, data); |
150 | ret = ld9040_spi_write_word(ctx, data: *data); |
151 | |
152 | while (!ret && --len) { |
153 | ++data; |
154 | ret = ld9040_spi_write_word(ctx, data: *data | 0x100); |
155 | } |
156 | |
157 | if (ret) { |
158 | dev_err(ctx->dev, "error %d writing dcs seq: %*ph\n" , ret, |
159 | (int)len, data); |
160 | ctx->error = ret; |
161 | } |
162 | |
163 | usleep_range(min: 300, max: 310); |
164 | } |
165 | |
166 | #define ld9040_dcs_write_seq_static(ctx, seq...) \ |
167 | ({\ |
168 | static const u8 d[] = { seq };\ |
169 | ld9040_dcs_write(ctx, d, ARRAY_SIZE(d));\ |
170 | }) |
171 | |
172 | static void ld9040_brightness_set(struct ld9040 *ctx) |
173 | { |
174 | ld9040_dcs_write(ctx, data: ld9040_gammas[ctx->brightness], |
175 | ARRAY_SIZE(ld9040_gammas[ctx->brightness])); |
176 | |
177 | ld9040_dcs_write_seq_static(ctx, MCS_GAMMA_CTRL, 0x02, 0x5a); |
178 | } |
179 | |
180 | static void ld9040_init(struct ld9040 *ctx) |
181 | { |
182 | ld9040_dcs_write_seq_static(ctx, MCS_USER_SETTING, 0x5a, 0x5a); |
183 | ld9040_dcs_write_seq_static(ctx, MCS_PANEL_CONDITION, |
184 | 0x05, 0x5e, 0x96, 0x6b, 0x7d, 0x0d, 0x3f, 0x00, |
185 | 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
186 | 0x07, 0x05, 0x1f, 0x1f, 0x1f, 0x00, 0x00); |
187 | ld9040_dcs_write_seq_static(ctx, MCS_DISPCTL, |
188 | 0x02, 0x06, 0x0a, 0x10, 0x10); |
189 | ld9040_dcs_write_seq_static(ctx, MCS_MANPWR, 0x04); |
190 | ld9040_dcs_write_seq_static(ctx, MCS_POWER_CTRL, |
191 | 0x0a, 0x87, 0x25, 0x6a, 0x44, 0x02, 0x88); |
192 | ld9040_dcs_write_seq_static(ctx, MCS_ELVSS_ON, 0x0f, 0x00, 0x16); |
193 | ld9040_dcs_write_seq_static(ctx, MCS_GTCON, 0x09, 0x00, 0x00); |
194 | ld9040_brightness_set(ctx); |
195 | ld9040_dcs_write_seq_static(ctx, MIPI_DCS_EXIT_SLEEP_MODE); |
196 | ld9040_dcs_write_seq_static(ctx, MIPI_DCS_SET_DISPLAY_ON); |
197 | } |
198 | |
199 | static int ld9040_power_on(struct ld9040 *ctx) |
200 | { |
201 | int ret; |
202 | |
203 | ret = regulator_bulk_enable(ARRAY_SIZE(ctx->supplies), consumers: ctx->supplies); |
204 | if (ret < 0) |
205 | return ret; |
206 | |
207 | msleep(msecs: ctx->power_on_delay); |
208 | gpiod_set_value(desc: ctx->reset_gpio, value: 0); |
209 | msleep(msecs: ctx->reset_delay); |
210 | gpiod_set_value(desc: ctx->reset_gpio, value: 1); |
211 | msleep(msecs: ctx->reset_delay); |
212 | |
213 | return 0; |
214 | } |
215 | |
216 | static int ld9040_power_off(struct ld9040 *ctx) |
217 | { |
218 | return regulator_bulk_disable(ARRAY_SIZE(ctx->supplies), consumers: ctx->supplies); |
219 | } |
220 | |
221 | static int ld9040_disable(struct drm_panel *panel) |
222 | { |
223 | return 0; |
224 | } |
225 | |
226 | static int ld9040_unprepare(struct drm_panel *panel) |
227 | { |
228 | struct ld9040 *ctx = panel_to_ld9040(panel); |
229 | |
230 | msleep(msecs: 120); |
231 | ld9040_dcs_write_seq_static(ctx, MIPI_DCS_SET_DISPLAY_OFF); |
232 | ld9040_dcs_write_seq_static(ctx, MIPI_DCS_ENTER_SLEEP_MODE); |
233 | msleep(msecs: 40); |
234 | |
235 | ld9040_clear_error(ctx); |
236 | |
237 | return ld9040_power_off(ctx); |
238 | } |
239 | |
240 | static int ld9040_prepare(struct drm_panel *panel) |
241 | { |
242 | struct ld9040 *ctx = panel_to_ld9040(panel); |
243 | int ret; |
244 | |
245 | ret = ld9040_power_on(ctx); |
246 | if (ret < 0) |
247 | return ret; |
248 | |
249 | ld9040_init(ctx); |
250 | |
251 | ret = ld9040_clear_error(ctx); |
252 | |
253 | if (ret < 0) |
254 | ld9040_unprepare(panel); |
255 | |
256 | return ret; |
257 | } |
258 | |
259 | static int ld9040_enable(struct drm_panel *panel) |
260 | { |
261 | return 0; |
262 | } |
263 | |
264 | static int ld9040_get_modes(struct drm_panel *panel, |
265 | struct drm_connector *connector) |
266 | { |
267 | struct ld9040 *ctx = panel_to_ld9040(panel); |
268 | struct drm_display_mode *mode; |
269 | |
270 | mode = drm_mode_create(dev: connector->dev); |
271 | if (!mode) { |
272 | dev_err(panel->dev, "failed to create a new display mode\n" ); |
273 | return 0; |
274 | } |
275 | |
276 | drm_display_mode_from_videomode(vm: &ctx->vm, dmode: mode); |
277 | mode->width_mm = ctx->width_mm; |
278 | mode->height_mm = ctx->height_mm; |
279 | connector->display_info.width_mm = mode->width_mm; |
280 | connector->display_info.height_mm = mode->height_mm; |
281 | |
282 | mode->type = DRM_MODE_TYPE_DRIVER | DRM_MODE_TYPE_PREFERRED; |
283 | drm_mode_probed_add(connector, mode); |
284 | |
285 | return 1; |
286 | } |
287 | |
288 | static const struct drm_panel_funcs ld9040_drm_funcs = { |
289 | .disable = ld9040_disable, |
290 | .unprepare = ld9040_unprepare, |
291 | .prepare = ld9040_prepare, |
292 | .enable = ld9040_enable, |
293 | .get_modes = ld9040_get_modes, |
294 | }; |
295 | |
296 | static int ld9040_parse_dt(struct ld9040 *ctx) |
297 | { |
298 | struct device *dev = ctx->dev; |
299 | struct device_node *np = dev->of_node; |
300 | int ret; |
301 | |
302 | ret = of_get_videomode(np, vm: &ctx->vm, index: 0); |
303 | if (ret < 0) |
304 | return ret; |
305 | |
306 | of_property_read_u32(np, propname: "power-on-delay" , out_value: &ctx->power_on_delay); |
307 | of_property_read_u32(np, propname: "reset-delay" , out_value: &ctx->reset_delay); |
308 | of_property_read_u32(np, propname: "panel-width-mm" , out_value: &ctx->width_mm); |
309 | of_property_read_u32(np, propname: "panel-height-mm" , out_value: &ctx->height_mm); |
310 | |
311 | return 0; |
312 | } |
313 | |
314 | static int ld9040_bl_update_status(struct backlight_device *dev) |
315 | { |
316 | struct ld9040 *ctx = bl_get_data(bl_dev: dev); |
317 | |
318 | ctx->brightness = backlight_get_brightness(bd: dev); |
319 | ld9040_brightness_set(ctx); |
320 | |
321 | return 0; |
322 | } |
323 | |
324 | static const struct backlight_ops ld9040_bl_ops = { |
325 | .update_status = ld9040_bl_update_status, |
326 | }; |
327 | |
328 | static const struct backlight_properties ld9040_bl_props = { |
329 | .type = BACKLIGHT_RAW, |
330 | .scale = BACKLIGHT_SCALE_NON_LINEAR, |
331 | .max_brightness = ARRAY_SIZE(ld9040_gammas) - 1, |
332 | .brightness = ARRAY_SIZE(ld9040_gammas) - 1, |
333 | }; |
334 | |
335 | static int ld9040_probe(struct spi_device *spi) |
336 | { |
337 | struct backlight_device *bldev; |
338 | struct device *dev = &spi->dev; |
339 | struct ld9040 *ctx; |
340 | int ret; |
341 | |
342 | ctx = devm_kzalloc(dev, size: sizeof(struct ld9040), GFP_KERNEL); |
343 | if (!ctx) |
344 | return -ENOMEM; |
345 | |
346 | spi_set_drvdata(spi, data: ctx); |
347 | |
348 | ctx->dev = dev; |
349 | ctx->brightness = ld9040_bl_props.brightness; |
350 | |
351 | ret = ld9040_parse_dt(ctx); |
352 | if (ret < 0) |
353 | return ret; |
354 | |
355 | ctx->supplies[0].supply = "vdd3" ; |
356 | ctx->supplies[1].supply = "vci" ; |
357 | ret = devm_regulator_bulk_get(dev, ARRAY_SIZE(ctx->supplies), |
358 | consumers: ctx->supplies); |
359 | if (ret < 0) |
360 | return ret; |
361 | |
362 | ctx->reset_gpio = devm_gpiod_get(dev, con_id: "reset" , flags: GPIOD_OUT_HIGH); |
363 | if (IS_ERR(ptr: ctx->reset_gpio)) { |
364 | dev_err(dev, "cannot get reset-gpios %ld\n" , |
365 | PTR_ERR(ctx->reset_gpio)); |
366 | return PTR_ERR(ptr: ctx->reset_gpio); |
367 | } |
368 | |
369 | spi->bits_per_word = 9; |
370 | ret = spi_setup(spi); |
371 | if (ret < 0) { |
372 | dev_err(dev, "spi setup failed.\n" ); |
373 | return ret; |
374 | } |
375 | |
376 | drm_panel_init(panel: &ctx->panel, dev, funcs: &ld9040_drm_funcs, |
377 | DRM_MODE_CONNECTOR_DPI); |
378 | |
379 | bldev = devm_backlight_device_register(dev, name: dev_name(dev), parent: dev, |
380 | devdata: ctx, ops: &ld9040_bl_ops, |
381 | props: &ld9040_bl_props); |
382 | if (IS_ERR(ptr: bldev)) |
383 | return PTR_ERR(ptr: bldev); |
384 | |
385 | drm_panel_add(panel: &ctx->panel); |
386 | |
387 | return 0; |
388 | } |
389 | |
390 | static void ld9040_remove(struct spi_device *spi) |
391 | { |
392 | struct ld9040 *ctx = spi_get_drvdata(spi); |
393 | |
394 | ld9040_power_off(ctx); |
395 | drm_panel_remove(panel: &ctx->panel); |
396 | } |
397 | |
398 | static const struct of_device_id ld9040_of_match[] = { |
399 | { .compatible = "samsung,ld9040" }, |
400 | { } |
401 | }; |
402 | MODULE_DEVICE_TABLE(of, ld9040_of_match); |
403 | |
404 | static const struct spi_device_id ld9040_ids[] = { |
405 | { "ld9040" , }, |
406 | { /* sentinel */ } |
407 | }; |
408 | MODULE_DEVICE_TABLE(spi, ld9040_ids); |
409 | |
410 | static struct spi_driver ld9040_driver = { |
411 | .probe = ld9040_probe, |
412 | .remove = ld9040_remove, |
413 | .id_table = ld9040_ids, |
414 | .driver = { |
415 | .name = "panel-samsung-ld9040" , |
416 | .of_match_table = ld9040_of_match, |
417 | }, |
418 | }; |
419 | module_spi_driver(ld9040_driver); |
420 | |
421 | MODULE_AUTHOR("Andrzej Hajda <a.hajda@samsung.com>" ); |
422 | MODULE_DESCRIPTION("ld9040 LCD Driver" ); |
423 | MODULE_LICENSE("GPL v2" ); |
424 | |