1 | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | /* |
3 | * Cirrus Logic CLPS711X FB driver |
4 | * |
5 | * Copyright (C) 2014 Alexander Shiyan <shc_work@mail.ru> |
6 | * Based on driver by Russell King <rmk@arm.linux.org.uk> |
7 | */ |
8 | |
9 | #include <linux/clk.h> |
10 | #include <linux/fb.h> |
11 | #include <linux/io.h> |
12 | #include <linux/lcd.h> |
13 | #include <linux/module.h> |
14 | #include <linux/of.h> |
15 | #include <linux/platform_device.h> |
16 | #include <linux/regmap.h> |
17 | #include <linux/mfd/syscon.h> |
18 | #include <linux/mfd/syscon/clps711x.h> |
19 | #include <linux/regulator/consumer.h> |
20 | #include <video/of_display_timing.h> |
21 | |
22 | #define CLPS711X_FB_NAME "clps711x-fb" |
23 | #define CLPS711X_FB_BPP_MAX (4) |
24 | |
25 | /* Registers relative to LCDCON */ |
26 | #define CLPS711X_LCDCON (0x0000) |
27 | # define LCDCON_GSEN BIT(30) |
28 | # define LCDCON_GSMD BIT(31) |
29 | #define CLPS711X_PALLSW (0x0280) |
30 | #define CLPS711X_PALMSW (0x02c0) |
31 | #define CLPS711X_FBADDR (0x0d40) |
32 | |
33 | struct clps711x_fb_info { |
34 | struct clk *clk; |
35 | void __iomem *base; |
36 | struct regmap *syscon; |
37 | resource_size_t buffsize; |
38 | struct fb_videomode mode; |
39 | struct regulator *lcd_pwr; |
40 | u32 ac_prescale; |
41 | bool cmap_invert; |
42 | }; |
43 | |
44 | static int clps711x_fb_setcolreg(u_int regno, u_int red, u_int green, |
45 | u_int blue, u_int transp, struct fb_info *info) |
46 | { |
47 | struct clps711x_fb_info *cfb = info->par; |
48 | u32 level, mask, shift; |
49 | |
50 | if (regno >= BIT(info->var.bits_per_pixel)) |
51 | return -EINVAL; |
52 | |
53 | shift = 4 * (regno & 7); |
54 | mask = 0xf << shift; |
55 | /* gray = 0.30*R + 0.58*G + 0.11*B */ |
56 | level = (((red * 77 + green * 151 + blue * 28) >> 20) << shift) & mask; |
57 | if (cfb->cmap_invert) |
58 | level = 0xf - level; |
59 | |
60 | regno = (regno < 8) ? CLPS711X_PALLSW : CLPS711X_PALMSW; |
61 | |
62 | writel(val: (readl(addr: cfb->base + regno) & ~mask) | level, addr: cfb->base + regno); |
63 | |
64 | return 0; |
65 | } |
66 | |
67 | static int clps711x_fb_check_var(struct fb_var_screeninfo *var, |
68 | struct fb_info *info) |
69 | { |
70 | u32 val; |
71 | |
72 | if (var->bits_per_pixel < 1 || |
73 | var->bits_per_pixel > CLPS711X_FB_BPP_MAX) |
74 | return -EINVAL; |
75 | |
76 | if (!var->pixclock) |
77 | return -EINVAL; |
78 | |
79 | val = DIV_ROUND_UP(var->xres, 16) - 1; |
80 | if (val < 0x01 || val > 0x3f) |
81 | return -EINVAL; |
82 | |
83 | val = DIV_ROUND_UP(var->yres * var->xres * var->bits_per_pixel, 128); |
84 | val--; |
85 | if (val < 0x001 || val > 0x1fff) |
86 | return -EINVAL; |
87 | |
88 | var->transp.msb_right = 0; |
89 | var->transp.offset = 0; |
90 | var->transp.length = 0; |
91 | var->red.msb_right = 0; |
92 | var->red.offset = 0; |
93 | var->red.length = var->bits_per_pixel; |
94 | var->green = var->red; |
95 | var->blue = var->red; |
96 | var->grayscale = var->bits_per_pixel > 1; |
97 | |
98 | return 0; |
99 | } |
100 | |
101 | static int clps711x_fb_set_par(struct fb_info *info) |
102 | { |
103 | struct clps711x_fb_info *cfb = info->par; |
104 | resource_size_t size; |
105 | u32 lcdcon, pps; |
106 | |
107 | size = (info->var.xres * info->var.yres * info->var.bits_per_pixel) / 8; |
108 | if (size > cfb->buffsize) |
109 | return -EINVAL; |
110 | |
111 | switch (info->var.bits_per_pixel) { |
112 | case 1: |
113 | info->fix.visual = FB_VISUAL_MONO01; |
114 | break; |
115 | case 2: |
116 | case 4: |
117 | info->fix.visual = FB_VISUAL_PSEUDOCOLOR; |
118 | break; |
119 | default: |
120 | return -EINVAL; |
121 | } |
122 | |
123 | info->fix.line_length = info->var.xres * info->var.bits_per_pixel / 8; |
124 | info->fix.smem_len = size; |
125 | |
126 | lcdcon = (info->var.xres * info->var.yres * |
127 | info->var.bits_per_pixel) / 128 - 1; |
128 | lcdcon |= ((info->var.xres / 16) - 1) << 13; |
129 | lcdcon |= (cfb->ac_prescale & 0x1f) << 25; |
130 | |
131 | pps = clk_get_rate(clk: cfb->clk) / (PICOS2KHZ(info->var.pixclock) * 1000); |
132 | if (pps) |
133 | pps--; |
134 | lcdcon |= (pps & 0x3f) << 19; |
135 | |
136 | if (info->var.bits_per_pixel == 4) |
137 | lcdcon |= LCDCON_GSMD; |
138 | if (info->var.bits_per_pixel >= 2) |
139 | lcdcon |= LCDCON_GSEN; |
140 | |
141 | /* LCDCON must only be changed while the LCD is disabled */ |
142 | regmap_update_bits(map: cfb->syscon, SYSCON_OFFSET, SYSCON1_LCDEN, val: 0); |
143 | writel(val: lcdcon, addr: cfb->base + CLPS711X_LCDCON); |
144 | regmap_update_bits(map: cfb->syscon, SYSCON_OFFSET, |
145 | SYSCON1_LCDEN, SYSCON1_LCDEN); |
146 | |
147 | return 0; |
148 | } |
149 | |
150 | static int clps711x_fb_blank(int blank, struct fb_info *info) |
151 | { |
152 | /* Return happy */ |
153 | return 0; |
154 | } |
155 | |
156 | static const struct fb_ops clps711x_fb_ops = { |
157 | .owner = THIS_MODULE, |
158 | FB_DEFAULT_IOMEM_OPS, |
159 | .fb_setcolreg = clps711x_fb_setcolreg, |
160 | .fb_check_var = clps711x_fb_check_var, |
161 | .fb_set_par = clps711x_fb_set_par, |
162 | .fb_blank = clps711x_fb_blank, |
163 | }; |
164 | |
165 | static int clps711x_lcd_check_fb(struct lcd_device *lcddev, struct fb_info *fi) |
166 | { |
167 | struct clps711x_fb_info *cfb = dev_get_drvdata(dev: &lcddev->dev); |
168 | |
169 | return (!fi || fi->par == cfb) ? 1 : 0; |
170 | } |
171 | |
172 | static int clps711x_lcd_get_power(struct lcd_device *lcddev) |
173 | { |
174 | struct clps711x_fb_info *cfb = dev_get_drvdata(dev: &lcddev->dev); |
175 | |
176 | if (!IS_ERR_OR_NULL(ptr: cfb->lcd_pwr)) |
177 | if (!regulator_is_enabled(regulator: cfb->lcd_pwr)) |
178 | return FB_BLANK_NORMAL; |
179 | |
180 | return FB_BLANK_UNBLANK; |
181 | } |
182 | |
183 | static int clps711x_lcd_set_power(struct lcd_device *lcddev, int blank) |
184 | { |
185 | struct clps711x_fb_info *cfb = dev_get_drvdata(dev: &lcddev->dev); |
186 | |
187 | if (!IS_ERR_OR_NULL(ptr: cfb->lcd_pwr)) { |
188 | if (blank == FB_BLANK_UNBLANK) { |
189 | if (!regulator_is_enabled(regulator: cfb->lcd_pwr)) |
190 | return regulator_enable(regulator: cfb->lcd_pwr); |
191 | } else { |
192 | if (regulator_is_enabled(regulator: cfb->lcd_pwr)) |
193 | return regulator_disable(regulator: cfb->lcd_pwr); |
194 | } |
195 | } |
196 | |
197 | return 0; |
198 | } |
199 | |
200 | static struct lcd_ops clps711x_lcd_ops = { |
201 | .check_fb = clps711x_lcd_check_fb, |
202 | .get_power = clps711x_lcd_get_power, |
203 | .set_power = clps711x_lcd_set_power, |
204 | }; |
205 | |
206 | static int clps711x_fb_probe(struct platform_device *pdev) |
207 | { |
208 | struct device *dev = &pdev->dev; |
209 | struct device_node *disp, *np = dev->of_node; |
210 | struct clps711x_fb_info *cfb; |
211 | struct lcd_device *lcd; |
212 | struct fb_info *info; |
213 | struct resource *res; |
214 | int ret = -ENOENT; |
215 | u32 val; |
216 | |
217 | if (fb_get_options(CLPS711X_FB_NAME, NULL)) |
218 | return -ENODEV; |
219 | |
220 | info = framebuffer_alloc(size: sizeof(*cfb), dev); |
221 | if (!info) |
222 | return -ENOMEM; |
223 | |
224 | cfb = info->par; |
225 | platform_set_drvdata(pdev, data: info); |
226 | |
227 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
228 | if (!res) |
229 | goto out_fb_release; |
230 | cfb->base = devm_ioremap(dev, offset: res->start, size: resource_size(res)); |
231 | if (!cfb->base) { |
232 | ret = -ENOMEM; |
233 | goto out_fb_release; |
234 | } |
235 | |
236 | info->fix.mmio_start = res->start; |
237 | info->fix.mmio_len = resource_size(res); |
238 | |
239 | info->screen_base = devm_platform_get_and_ioremap_resource(pdev, index: 1, res: &res); |
240 | if (IS_ERR(ptr: info->screen_base)) { |
241 | ret = PTR_ERR(ptr: info->screen_base); |
242 | goto out_fb_release; |
243 | } |
244 | |
245 | /* Physical address should be aligned to 256 MiB */ |
246 | if (res->start & 0x0fffffff) { |
247 | ret = -EINVAL; |
248 | goto out_fb_release; |
249 | } |
250 | |
251 | cfb->buffsize = resource_size(res); |
252 | info->fix.smem_start = res->start; |
253 | |
254 | cfb->clk = devm_clk_get(dev, NULL); |
255 | if (IS_ERR(ptr: cfb->clk)) { |
256 | ret = PTR_ERR(ptr: cfb->clk); |
257 | goto out_fb_release; |
258 | } |
259 | |
260 | cfb->syscon = syscon_regmap_lookup_by_phandle(np, property: "syscon" ); |
261 | if (IS_ERR(ptr: cfb->syscon)) { |
262 | ret = PTR_ERR(ptr: cfb->syscon); |
263 | goto out_fb_release; |
264 | } |
265 | |
266 | disp = of_parse_phandle(np, phandle_name: "display" , index: 0); |
267 | if (!disp) { |
268 | dev_err(&pdev->dev, "No display defined\n" ); |
269 | ret = -ENODATA; |
270 | goto out_fb_release; |
271 | } |
272 | |
273 | ret = of_get_fb_videomode(np: disp, fb: &cfb->mode, OF_USE_NATIVE_MODE); |
274 | if (ret) { |
275 | of_node_put(node: disp); |
276 | goto out_fb_release; |
277 | } |
278 | |
279 | of_property_read_u32(np: disp, propname: "ac-prescale" , out_value: &cfb->ac_prescale); |
280 | cfb->cmap_invert = of_property_read_bool(np: disp, propname: "cmap-invert" ); |
281 | |
282 | ret = of_property_read_u32(np: disp, propname: "bits-per-pixel" , |
283 | out_value: &info->var.bits_per_pixel); |
284 | of_node_put(node: disp); |
285 | if (ret) |
286 | goto out_fb_release; |
287 | |
288 | /* Force disable LCD on any mismatch */ |
289 | if (info->fix.smem_start != (readb(addr: cfb->base + CLPS711X_FBADDR) << 28)) |
290 | regmap_update_bits(map: cfb->syscon, SYSCON_OFFSET, |
291 | SYSCON1_LCDEN, val: 0); |
292 | |
293 | ret = regmap_read(map: cfb->syscon, SYSCON_OFFSET, val: &val); |
294 | if (ret) |
295 | goto out_fb_release; |
296 | |
297 | if (!(val & SYSCON1_LCDEN)) { |
298 | /* Setup start FB address */ |
299 | writeb(val: info->fix.smem_start >> 28, addr: cfb->base + CLPS711X_FBADDR); |
300 | /* Clean FB memory */ |
301 | memset_io(info->screen_base, 0, cfb->buffsize); |
302 | } |
303 | |
304 | cfb->lcd_pwr = devm_regulator_get(dev, id: "lcd" ); |
305 | if (PTR_ERR(ptr: cfb->lcd_pwr) == -EPROBE_DEFER) { |
306 | ret = -EPROBE_DEFER; |
307 | goto out_fb_release; |
308 | } |
309 | |
310 | info->fbops = &clps711x_fb_ops; |
311 | info->var.activate = FB_ACTIVATE_FORCE | FB_ACTIVATE_NOW; |
312 | info->var.height = -1; |
313 | info->var.width = -1; |
314 | info->var.vmode = FB_VMODE_NONINTERLACED; |
315 | info->fix.type = FB_TYPE_PACKED_PIXELS; |
316 | info->fix.accel = FB_ACCEL_NONE; |
317 | strscpy(info->fix.id, CLPS711X_FB_NAME, sizeof(info->fix.id)); |
318 | fb_videomode_to_var(var: &info->var, mode: &cfb->mode); |
319 | |
320 | ret = fb_alloc_cmap(cmap: &info->cmap, BIT(CLPS711X_FB_BPP_MAX), transp: 0); |
321 | if (ret) |
322 | goto out_fb_release; |
323 | |
324 | ret = fb_set_var(info, var: &info->var); |
325 | if (ret) |
326 | goto out_fb_dealloc_cmap; |
327 | |
328 | ret = register_framebuffer(fb_info: info); |
329 | if (ret) |
330 | goto out_fb_dealloc_cmap; |
331 | |
332 | lcd = devm_lcd_device_register(dev, name: "clps711x-lcd" , parent: dev, devdata: cfb, |
333 | ops: &clps711x_lcd_ops); |
334 | if (!IS_ERR(ptr: lcd)) |
335 | return 0; |
336 | |
337 | ret = PTR_ERR(ptr: lcd); |
338 | unregister_framebuffer(fb_info: info); |
339 | |
340 | out_fb_dealloc_cmap: |
341 | regmap_update_bits(map: cfb->syscon, SYSCON_OFFSET, SYSCON1_LCDEN, val: 0); |
342 | fb_dealloc_cmap(cmap: &info->cmap); |
343 | |
344 | out_fb_release: |
345 | framebuffer_release(info); |
346 | |
347 | return ret; |
348 | } |
349 | |
350 | static void clps711x_fb_remove(struct platform_device *pdev) |
351 | { |
352 | struct fb_info *info = platform_get_drvdata(pdev); |
353 | struct clps711x_fb_info *cfb = info->par; |
354 | |
355 | regmap_update_bits(map: cfb->syscon, SYSCON_OFFSET, SYSCON1_LCDEN, val: 0); |
356 | |
357 | unregister_framebuffer(fb_info: info); |
358 | fb_dealloc_cmap(cmap: &info->cmap); |
359 | framebuffer_release(info); |
360 | } |
361 | |
362 | static const struct of_device_id clps711x_fb_dt_ids[] = { |
363 | { .compatible = "cirrus,ep7209-fb" , }, |
364 | { } |
365 | }; |
366 | MODULE_DEVICE_TABLE(of, clps711x_fb_dt_ids); |
367 | |
368 | static struct platform_driver clps711x_fb_driver = { |
369 | .driver = { |
370 | .name = CLPS711X_FB_NAME, |
371 | .of_match_table = clps711x_fb_dt_ids, |
372 | }, |
373 | .probe = clps711x_fb_probe, |
374 | .remove_new = clps711x_fb_remove, |
375 | }; |
376 | module_platform_driver(clps711x_fb_driver); |
377 | |
378 | MODULE_AUTHOR("Alexander Shiyan <shc_work@mail.ru>" ); |
379 | MODULE_DESCRIPTION("Cirrus Logic CLPS711X FB driver" ); |
380 | MODULE_LICENSE("GPL" ); |
381 | |