Commit | Line | Data |
---|---|---|
ac1d6d74 LW |
1 | // SPDX-License-Identifier: GPL-2.0+ |
2 | /* | |
3 | * MIPI-DSI Samsung s6d16d0 panel driver. This is a 864x480 | |
4 | * AMOLED panel with a command-only DSI interface. | |
5 | */ | |
6 | ||
7 | #include <drm/drm_modes.h> | |
8 | #include <drm/drm_mipi_dsi.h> | |
9 | #include <drm/drm_panel.h> | |
ac1d6d74 LW |
10 | |
11 | #include <linux/gpio/consumer.h> | |
12 | #include <linux/regulator/consumer.h> | |
13 | #include <linux/delay.h> | |
722d4f06 | 14 | #include <linux/mod_devicetable.h> |
ac1d6d74 LW |
15 | #include <linux/module.h> |
16 | ||
17 | struct s6d16d0 { | |
18 | struct device *dev; | |
19 | struct drm_panel panel; | |
20 | struct regulator *supply; | |
21 | struct gpio_desc *reset_gpio; | |
22 | }; | |
23 | ||
24 | /* | |
25 | * The timings are not very helpful as the display is used in | |
26 | * command mode. | |
27 | */ | |
28 | static const struct drm_display_mode samsung_s6d16d0_mode = { | |
29 | /* HS clock, (htotal*vtotal*vrefresh)/1000 */ | |
30 | .clock = 420160, | |
31 | .hdisplay = 864, | |
32 | .hsync_start = 864 + 154, | |
33 | .hsync_end = 864 + 154 + 16, | |
34 | .htotal = 864 + 154 + 16 + 32, | |
35 | .vdisplay = 480, | |
36 | .vsync_start = 480 + 1, | |
37 | .vsync_end = 480 + 1 + 1, | |
38 | .vtotal = 480 + 1 + 1 + 1, | |
ac1d6d74 LW |
39 | .width_mm = 84, |
40 | .height_mm = 48, | |
41 | }; | |
42 | ||
43 | static inline struct s6d16d0 *panel_to_s6d16d0(struct drm_panel *panel) | |
44 | { | |
45 | return container_of(panel, struct s6d16d0, panel); | |
46 | } | |
47 | ||
48 | static int s6d16d0_unprepare(struct drm_panel *panel) | |
49 | { | |
50 | struct s6d16d0 *s6 = panel_to_s6d16d0(panel); | |
51 | struct mipi_dsi_device *dsi = to_mipi_dsi_device(s6->dev); | |
52 | int ret; | |
53 | ||
54 | /* Enter sleep mode */ | |
55 | ret = mipi_dsi_dcs_enter_sleep_mode(dsi); | |
56 | if (ret) { | |
5936b3bd | 57 | dev_err(s6->dev, "failed to enter sleep mode (%d)\n", ret); |
ac1d6d74 LW |
58 | return ret; |
59 | } | |
60 | ||
61 | /* Assert RESET */ | |
62 | gpiod_set_value_cansleep(s6->reset_gpio, 1); | |
63 | regulator_disable(s6->supply); | |
64 | ||
65 | return 0; | |
66 | } | |
67 | ||
68 | static int s6d16d0_prepare(struct drm_panel *panel) | |
69 | { | |
70 | struct s6d16d0 *s6 = panel_to_s6d16d0(panel); | |
71 | struct mipi_dsi_device *dsi = to_mipi_dsi_device(s6->dev); | |
72 | int ret; | |
73 | ||
74 | ret = regulator_enable(s6->supply); | |
75 | if (ret) { | |
5936b3bd | 76 | dev_err(s6->dev, "failed to enable supply (%d)\n", ret); |
ac1d6d74 LW |
77 | return ret; |
78 | } | |
79 | ||
80 | /* Assert RESET */ | |
81 | gpiod_set_value_cansleep(s6->reset_gpio, 1); | |
82 | udelay(10); | |
83 | /* De-assert RESET */ | |
84 | gpiod_set_value_cansleep(s6->reset_gpio, 0); | |
85 | msleep(120); | |
86 | ||
87 | /* Enabe tearing mode: send TE (tearing effect) at VBLANK */ | |
88 | ret = mipi_dsi_dcs_set_tear_on(dsi, | |
89 | MIPI_DSI_DCS_TEAR_MODE_VBLANK); | |
90 | if (ret) { | |
5936b3bd | 91 | dev_err(s6->dev, "failed to enable vblank TE (%d)\n", ret); |
ac1d6d74 LW |
92 | return ret; |
93 | } | |
94 | /* Exit sleep mode and power on */ | |
95 | ret = mipi_dsi_dcs_exit_sleep_mode(dsi); | |
96 | if (ret) { | |
5936b3bd | 97 | dev_err(s6->dev, "failed to exit sleep mode (%d)\n", ret); |
ac1d6d74 LW |
98 | return ret; |
99 | } | |
100 | ||
101 | return 0; | |
102 | } | |
103 | ||
104 | static int s6d16d0_enable(struct drm_panel *panel) | |
105 | { | |
106 | struct s6d16d0 *s6 = panel_to_s6d16d0(panel); | |
107 | struct mipi_dsi_device *dsi = to_mipi_dsi_device(s6->dev); | |
108 | int ret; | |
109 | ||
110 | ret = mipi_dsi_dcs_set_display_on(dsi); | |
111 | if (ret) { | |
5936b3bd | 112 | dev_err(s6->dev, "failed to turn display on (%d)\n", ret); |
ac1d6d74 LW |
113 | return ret; |
114 | } | |
115 | ||
116 | return 0; | |
117 | } | |
118 | ||
119 | static int s6d16d0_disable(struct drm_panel *panel) | |
120 | { | |
121 | struct s6d16d0 *s6 = panel_to_s6d16d0(panel); | |
122 | struct mipi_dsi_device *dsi = to_mipi_dsi_device(s6->dev); | |
123 | int ret; | |
124 | ||
125 | ret = mipi_dsi_dcs_set_display_off(dsi); | |
126 | if (ret) { | |
5936b3bd | 127 | dev_err(s6->dev, "failed to turn display off (%d)\n", ret); |
ac1d6d74 LW |
128 | return ret; |
129 | } | |
130 | ||
131 | return 0; | |
132 | } | |
133 | ||
0ce8ddd8 SR |
134 | static int s6d16d0_get_modes(struct drm_panel *panel, |
135 | struct drm_connector *connector) | |
ac1d6d74 | 136 | { |
ac1d6d74 LW |
137 | struct drm_display_mode *mode; |
138 | ||
aa6c4364 | 139 | mode = drm_mode_duplicate(connector->dev, &samsung_s6d16d0_mode); |
ac1d6d74 | 140 | if (!mode) { |
5936b3bd | 141 | dev_err(panel->dev, "bad mode or failed to add mode\n"); |
ac1d6d74 LW |
142 | return -EINVAL; |
143 | } | |
144 | drm_mode_set_name(mode); | |
145 | mode->type = DRM_MODE_TYPE_DRIVER | DRM_MODE_TYPE_PREFERRED; | |
146 | ||
147 | connector->display_info.width_mm = mode->width_mm; | |
148 | connector->display_info.height_mm = mode->height_mm; | |
149 | ||
150 | drm_mode_probed_add(connector, mode); | |
151 | ||
152 | return 1; /* Number of modes */ | |
153 | } | |
154 | ||
155 | static const struct drm_panel_funcs s6d16d0_drm_funcs = { | |
156 | .disable = s6d16d0_disable, | |
157 | .unprepare = s6d16d0_unprepare, | |
158 | .prepare = s6d16d0_prepare, | |
159 | .enable = s6d16d0_enable, | |
160 | .get_modes = s6d16d0_get_modes, | |
161 | }; | |
162 | ||
163 | static int s6d16d0_probe(struct mipi_dsi_device *dsi) | |
164 | { | |
165 | struct device *dev = &dsi->dev; | |
166 | struct s6d16d0 *s6; | |
167 | int ret; | |
168 | ||
169 | s6 = devm_kzalloc(dev, sizeof(struct s6d16d0), GFP_KERNEL); | |
170 | if (!s6) | |
171 | return -ENOMEM; | |
172 | ||
173 | mipi_dsi_set_drvdata(dsi, s6); | |
174 | s6->dev = dev; | |
175 | ||
176 | dsi->lanes = 2; | |
177 | dsi->format = MIPI_DSI_FMT_RGB888; | |
178 | dsi->hs_rate = 420160000; | |
179 | dsi->lp_rate = 19200000; | |
180 | /* | |
181 | * This display uses command mode so no MIPI_DSI_MODE_VIDEO | |
182 | * or MIPI_DSI_MODE_VIDEO_SYNC_PULSE | |
183 | * | |
184 | * As we only send commands we do not need to be continuously | |
185 | * clocked. | |
186 | */ | |
d0c5ac04 | 187 | dsi->mode_flags = MIPI_DSI_CLOCK_NON_CONTINUOUS; |
ac1d6d74 LW |
188 | |
189 | s6->supply = devm_regulator_get(dev, "vdd1"); | |
190 | if (IS_ERR(s6->supply)) | |
191 | return PTR_ERR(s6->supply); | |
192 | ||
193 | /* This asserts RESET by default */ | |
194 | s6->reset_gpio = devm_gpiod_get_optional(dev, "reset", | |
195 | GPIOD_OUT_HIGH); | |
196 | if (IS_ERR(s6->reset_gpio)) { | |
197 | ret = PTR_ERR(s6->reset_gpio); | |
198 | if (ret != -EPROBE_DEFER) | |
5936b3bd | 199 | dev_err(dev, "failed to request GPIO (%d)\n", ret); |
ac1d6d74 LW |
200 | return ret; |
201 | } | |
202 | ||
9a2654c0 LP |
203 | drm_panel_init(&s6->panel, dev, &s6d16d0_drm_funcs, |
204 | DRM_MODE_CONNECTOR_DSI); | |
ac1d6d74 | 205 | |
c3ee8c65 | 206 | drm_panel_add(&s6->panel); |
ac1d6d74 LW |
207 | |
208 | ret = mipi_dsi_attach(dsi); | |
209 | if (ret < 0) | |
210 | drm_panel_remove(&s6->panel); | |
211 | ||
212 | return ret; | |
213 | } | |
214 | ||
79abca2b | 215 | static void s6d16d0_remove(struct mipi_dsi_device *dsi) |
ac1d6d74 LW |
216 | { |
217 | struct s6d16d0 *s6 = mipi_dsi_get_drvdata(dsi); | |
218 | ||
219 | mipi_dsi_detach(dsi); | |
220 | drm_panel_remove(&s6->panel); | |
ac1d6d74 LW |
221 | } |
222 | ||
223 | static const struct of_device_id s6d16d0_of_match[] = { | |
224 | { .compatible = "samsung,s6d16d0" }, | |
225 | { } | |
226 | }; | |
227 | MODULE_DEVICE_TABLE(of, s6d16d0_of_match); | |
228 | ||
229 | static struct mipi_dsi_driver s6d16d0_driver = { | |
230 | .probe = s6d16d0_probe, | |
231 | .remove = s6d16d0_remove, | |
232 | .driver = { | |
233 | .name = "panel-samsung-s6d16d0", | |
234 | .of_match_table = s6d16d0_of_match, | |
235 | }, | |
236 | }; | |
237 | module_mipi_dsi_driver(s6d16d0_driver); | |
238 | ||
239 | MODULE_AUTHOR("Linus Wallei <linus.walleij@linaro.org>"); | |
240 | MODULE_DESCRIPTION("MIPI-DSI s6d16d0 Panel Driver"); | |
241 | MODULE_LICENSE("GPL v2"); |