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> |
10 | |
11 | #include <linux/gpio/consumer.h> |
12 | #include <linux/regulator/consumer.h> |
13 | #include <linux/delay.h> |
14 | #include <linux/mod_devicetable.h> |
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, |
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) { |
57 | dev_err(s6->dev, "failed to enter sleep mode (%d)\n" , ret); |
58 | return ret; |
59 | } |
60 | |
61 | /* Assert RESET */ |
62 | gpiod_set_value_cansleep(desc: s6->reset_gpio, value: 1); |
63 | regulator_disable(regulator: 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(regulator: s6->supply); |
75 | if (ret) { |
76 | dev_err(s6->dev, "failed to enable supply (%d)\n" , ret); |
77 | return ret; |
78 | } |
79 | |
80 | /* Assert RESET */ |
81 | gpiod_set_value_cansleep(desc: s6->reset_gpio, value: 1); |
82 | udelay(10); |
83 | /* De-assert RESET */ |
84 | gpiod_set_value_cansleep(desc: s6->reset_gpio, value: 0); |
85 | msleep(msecs: 120); |
86 | |
87 | /* Enabe tearing mode: send TE (tearing effect) at VBLANK */ |
88 | ret = mipi_dsi_dcs_set_tear_on(dsi, |
89 | mode: MIPI_DSI_DCS_TEAR_MODE_VBLANK); |
90 | if (ret) { |
91 | dev_err(s6->dev, "failed to enable vblank TE (%d)\n" , ret); |
92 | return ret; |
93 | } |
94 | /* Exit sleep mode and power on */ |
95 | ret = mipi_dsi_dcs_exit_sleep_mode(dsi); |
96 | if (ret) { |
97 | dev_err(s6->dev, "failed to exit sleep mode (%d)\n" , ret); |
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) { |
112 | dev_err(s6->dev, "failed to turn display on (%d)\n" , ret); |
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) { |
127 | dev_err(s6->dev, "failed to turn display off (%d)\n" , ret); |
128 | return ret; |
129 | } |
130 | |
131 | return 0; |
132 | } |
133 | |
134 | static int s6d16d0_get_modes(struct drm_panel *panel, |
135 | struct drm_connector *connector) |
136 | { |
137 | struct drm_display_mode *mode; |
138 | |
139 | mode = drm_mode_duplicate(dev: connector->dev, mode: &samsung_s6d16d0_mode); |
140 | if (!mode) { |
141 | dev_err(panel->dev, "bad mode or failed to add mode\n" ); |
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, size: sizeof(struct s6d16d0), GFP_KERNEL); |
170 | if (!s6) |
171 | return -ENOMEM; |
172 | |
173 | mipi_dsi_set_drvdata(dsi, data: 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 | */ |
187 | dsi->mode_flags = MIPI_DSI_CLOCK_NON_CONTINUOUS; |
188 | |
189 | s6->supply = devm_regulator_get(dev, id: "vdd1" ); |
190 | if (IS_ERR(ptr: s6->supply)) |
191 | return PTR_ERR(ptr: s6->supply); |
192 | |
193 | /* This asserts RESET by default */ |
194 | s6->reset_gpio = devm_gpiod_get_optional(dev, con_id: "reset" , |
195 | flags: GPIOD_OUT_HIGH); |
196 | if (IS_ERR(ptr: s6->reset_gpio)) { |
197 | ret = PTR_ERR(ptr: s6->reset_gpio); |
198 | if (ret != -EPROBE_DEFER) |
199 | dev_err(dev, "failed to request GPIO (%d)\n" , ret); |
200 | return ret; |
201 | } |
202 | |
203 | drm_panel_init(panel: &s6->panel, dev, funcs: &s6d16d0_drm_funcs, |
204 | DRM_MODE_CONNECTOR_DSI); |
205 | |
206 | drm_panel_add(panel: &s6->panel); |
207 | |
208 | ret = mipi_dsi_attach(dsi); |
209 | if (ret < 0) |
210 | drm_panel_remove(panel: &s6->panel); |
211 | |
212 | return ret; |
213 | } |
214 | |
215 | static void s6d16d0_remove(struct mipi_dsi_device *dsi) |
216 | { |
217 | struct s6d16d0 *s6 = mipi_dsi_get_drvdata(dsi); |
218 | |
219 | mipi_dsi_detach(dsi); |
220 | drm_panel_remove(panel: &s6->panel); |
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" ); |
242 | |