1 | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | /* |
3 | * Driver for the Solomon SSD1307 OLED controller |
4 | * |
5 | * Copyright 2012 Free Electrons |
6 | */ |
7 | |
8 | #include <linux/backlight.h> |
9 | #include <linux/delay.h> |
10 | #include <linux/fb.h> |
11 | #include <linux/gpio/consumer.h> |
12 | #include <linux/i2c.h> |
13 | #include <linux/kernel.h> |
14 | #include <linux/module.h> |
15 | #include <linux/property.h> |
16 | #include <linux/pwm.h> |
17 | #include <linux/uaccess.h> |
18 | #include <linux/regulator/consumer.h> |
19 | |
20 | #define SSD1307FB_DATA 0x40 |
21 | #define SSD1307FB_COMMAND 0x80 |
22 | |
23 | #define SSD1307FB_SET_ADDRESS_MODE 0x20 |
24 | #define SSD1307FB_SET_ADDRESS_MODE_HORIZONTAL (0x00) |
25 | #define SSD1307FB_SET_ADDRESS_MODE_VERTICAL (0x01) |
26 | #define SSD1307FB_SET_ADDRESS_MODE_PAGE (0x02) |
27 | #define SSD1307FB_SET_COL_RANGE 0x21 |
28 | #define SSD1307FB_SET_PAGE_RANGE 0x22 |
29 | #define SSD1307FB_CONTRAST 0x81 |
30 | #define SSD1307FB_SET_LOOKUP_TABLE 0x91 |
31 | #define SSD1307FB_CHARGE_PUMP 0x8d |
32 | #define SSD1307FB_SEG_REMAP_ON 0xa1 |
33 | #define SSD1307FB_DISPLAY_OFF 0xae |
34 | #define SSD1307FB_SET_MULTIPLEX_RATIO 0xa8 |
35 | #define SSD1307FB_DISPLAY_ON 0xaf |
36 | #define SSD1307FB_START_PAGE_ADDRESS 0xb0 |
37 | #define SSD1307FB_SET_DISPLAY_OFFSET 0xd3 |
38 | #define SSD1307FB_SET_CLOCK_FREQ 0xd5 |
39 | #define SSD1307FB_SET_AREA_COLOR_MODE 0xd8 |
40 | #define SSD1307FB_SET_PRECHARGE_PERIOD 0xd9 |
41 | #define SSD1307FB_SET_COM_PINS_CONFIG 0xda |
42 | #define SSD1307FB_SET_VCOMH 0xdb |
43 | |
44 | #define MAX_CONTRAST 255 |
45 | |
46 | #define REFRESHRATE 1 |
47 | |
48 | static u_int refreshrate = REFRESHRATE; |
49 | module_param(refreshrate, uint, 0); |
50 | |
51 | struct ssd1307fb_deviceinfo { |
52 | u32 default_vcomh; |
53 | u32 default_dclk_div; |
54 | u32 default_dclk_frq; |
55 | bool need_pwm; |
56 | bool need_chargepump; |
57 | }; |
58 | |
59 | struct ssd1307fb_par { |
60 | unsigned area_color_enable : 1; |
61 | unsigned com_invdir : 1; |
62 | unsigned com_lrremap : 1; |
63 | unsigned com_seq : 1; |
64 | unsigned lookup_table_set : 1; |
65 | unsigned low_power : 1; |
66 | unsigned seg_remap : 1; |
67 | u32 com_offset; |
68 | u32 contrast; |
69 | u32 dclk_div; |
70 | u32 dclk_frq; |
71 | const struct ssd1307fb_deviceinfo *device_info; |
72 | struct i2c_client *client; |
73 | u32 height; |
74 | struct fb_info *info; |
75 | u8 lookup_table[4]; |
76 | u32 page_offset; |
77 | u32 col_offset; |
78 | u32 prechargep1; |
79 | u32 prechargep2; |
80 | struct pwm_device *pwm; |
81 | struct gpio_desc *reset; |
82 | struct regulator *vbat_reg; |
83 | u32 vcomh; |
84 | u32 width; |
85 | /* Cached address ranges */ |
86 | u8 col_start; |
87 | u8 col_end; |
88 | u8 page_start; |
89 | u8 page_end; |
90 | }; |
91 | |
92 | struct ssd1307fb_array { |
93 | u8 type; |
94 | u8 data[]; |
95 | }; |
96 | |
97 | static const struct fb_fix_screeninfo ssd1307fb_fix = { |
98 | .id = "Solomon SSD1307" , |
99 | .type = FB_TYPE_PACKED_PIXELS, |
100 | .visual = FB_VISUAL_MONO10, |
101 | .xpanstep = 0, |
102 | .ypanstep = 0, |
103 | .ywrapstep = 0, |
104 | .accel = FB_ACCEL_NONE, |
105 | }; |
106 | |
107 | static const struct fb_var_screeninfo ssd1307fb_var = { |
108 | .bits_per_pixel = 1, |
109 | .red = { .length = 1 }, |
110 | .green = { .length = 1 }, |
111 | .blue = { .length = 1 }, |
112 | }; |
113 | |
114 | static struct ssd1307fb_array *ssd1307fb_alloc_array(u32 len, u8 type) |
115 | { |
116 | struct ssd1307fb_array *array; |
117 | |
118 | array = kzalloc(size: sizeof(struct ssd1307fb_array) + len, GFP_KERNEL); |
119 | if (!array) |
120 | return NULL; |
121 | |
122 | array->type = type; |
123 | |
124 | return array; |
125 | } |
126 | |
127 | static int ssd1307fb_write_array(struct i2c_client *client, |
128 | struct ssd1307fb_array *array, u32 len) |
129 | { |
130 | int ret; |
131 | |
132 | len += sizeof(struct ssd1307fb_array); |
133 | |
134 | ret = i2c_master_send(client, buf: (u8 *)array, count: len); |
135 | if (ret != len) { |
136 | dev_err(&client->dev, "Couldn't send I2C command.\n" ); |
137 | return ret; |
138 | } |
139 | |
140 | return 0; |
141 | } |
142 | |
143 | static inline int ssd1307fb_write_cmd(struct i2c_client *client, u8 cmd) |
144 | { |
145 | struct ssd1307fb_array *array; |
146 | int ret; |
147 | |
148 | array = ssd1307fb_alloc_array(len: 1, SSD1307FB_COMMAND); |
149 | if (!array) |
150 | return -ENOMEM; |
151 | |
152 | array->data[0] = cmd; |
153 | |
154 | ret = ssd1307fb_write_array(client, array, len: 1); |
155 | kfree(objp: array); |
156 | |
157 | return ret; |
158 | } |
159 | |
160 | static int ssd1307fb_set_col_range(struct ssd1307fb_par *par, u8 col_start, |
161 | u8 cols) |
162 | { |
163 | u8 col_end = col_start + cols - 1; |
164 | int ret; |
165 | |
166 | if (col_start == par->col_start && col_end == par->col_end) |
167 | return 0; |
168 | |
169 | ret = ssd1307fb_write_cmd(client: par->client, SSD1307FB_SET_COL_RANGE); |
170 | if (ret < 0) |
171 | return ret; |
172 | |
173 | ret = ssd1307fb_write_cmd(client: par->client, cmd: col_start); |
174 | if (ret < 0) |
175 | return ret; |
176 | |
177 | ret = ssd1307fb_write_cmd(client: par->client, cmd: col_end); |
178 | if (ret < 0) |
179 | return ret; |
180 | |
181 | par->col_start = col_start; |
182 | par->col_end = col_end; |
183 | return 0; |
184 | } |
185 | |
186 | static int ssd1307fb_set_page_range(struct ssd1307fb_par *par, u8 page_start, |
187 | u8 pages) |
188 | { |
189 | u8 page_end = page_start + pages - 1; |
190 | int ret; |
191 | |
192 | if (page_start == par->page_start && page_end == par->page_end) |
193 | return 0; |
194 | |
195 | ret = ssd1307fb_write_cmd(client: par->client, SSD1307FB_SET_PAGE_RANGE); |
196 | if (ret < 0) |
197 | return ret; |
198 | |
199 | ret = ssd1307fb_write_cmd(client: par->client, cmd: page_start); |
200 | if (ret < 0) |
201 | return ret; |
202 | |
203 | ret = ssd1307fb_write_cmd(client: par->client, cmd: page_end); |
204 | if (ret < 0) |
205 | return ret; |
206 | |
207 | par->page_start = page_start; |
208 | par->page_end = page_end; |
209 | return 0; |
210 | } |
211 | |
212 | static int ssd1307fb_update_rect(struct ssd1307fb_par *par, unsigned int x, |
213 | unsigned int y, unsigned int width, |
214 | unsigned int height) |
215 | { |
216 | struct ssd1307fb_array *array; |
217 | u8 *vmem = par->info->screen_buffer; |
218 | unsigned int line_length = par->info->fix.line_length; |
219 | unsigned int pages = DIV_ROUND_UP(y % 8 + height, 8); |
220 | u32 array_idx = 0; |
221 | int ret, i, j, k; |
222 | |
223 | array = ssd1307fb_alloc_array(len: width * pages, SSD1307FB_DATA); |
224 | if (!array) |
225 | return -ENOMEM; |
226 | |
227 | /* |
228 | * The screen is divided in pages, each having a height of 8 |
229 | * pixels, and the width of the screen. When sending a byte of |
230 | * data to the controller, it gives the 8 bits for the current |
231 | * column. I.e, the first byte are the 8 bits of the first |
232 | * column, then the 8 bits for the second column, etc. |
233 | * |
234 | * |
235 | * Representation of the screen, assuming it is 5 bits |
236 | * wide. Each letter-number combination is a bit that controls |
237 | * one pixel. |
238 | * |
239 | * A0 A1 A2 A3 A4 |
240 | * B0 B1 B2 B3 B4 |
241 | * C0 C1 C2 C3 C4 |
242 | * D0 D1 D2 D3 D4 |
243 | * E0 E1 E2 E3 E4 |
244 | * F0 F1 F2 F3 F4 |
245 | * G0 G1 G2 G3 G4 |
246 | * H0 H1 H2 H3 H4 |
247 | * |
248 | * If you want to update this screen, you need to send 5 bytes: |
249 | * (1) A0 B0 C0 D0 E0 F0 G0 H0 |
250 | * (2) A1 B1 C1 D1 E1 F1 G1 H1 |
251 | * (3) A2 B2 C2 D2 E2 F2 G2 H2 |
252 | * (4) A3 B3 C3 D3 E3 F3 G3 H3 |
253 | * (5) A4 B4 C4 D4 E4 F4 G4 H4 |
254 | */ |
255 | |
256 | ret = ssd1307fb_set_col_range(par, col_start: par->col_offset + x, cols: width); |
257 | if (ret < 0) |
258 | goto out_free; |
259 | |
260 | ret = ssd1307fb_set_page_range(par, page_start: par->page_offset + y / 8, pages); |
261 | if (ret < 0) |
262 | goto out_free; |
263 | |
264 | for (i = y / 8; i < y / 8 + pages; i++) { |
265 | int m = 8; |
266 | |
267 | /* Last page may be partial */ |
268 | if (8 * (i + 1) > par->height) |
269 | m = par->height % 8; |
270 | for (j = x; j < x + width; j++) { |
271 | u8 data = 0; |
272 | |
273 | for (k = 0; k < m; k++) { |
274 | u8 byte = vmem[(8 * i + k) * line_length + |
275 | j / 8]; |
276 | u8 bit = (byte >> (j % 8)) & 1; |
277 | data |= bit << k; |
278 | } |
279 | array->data[array_idx++] = data; |
280 | } |
281 | } |
282 | |
283 | ret = ssd1307fb_write_array(client: par->client, array, len: width * pages); |
284 | |
285 | out_free: |
286 | kfree(objp: array); |
287 | return ret; |
288 | } |
289 | |
290 | static int ssd1307fb_update_display(struct ssd1307fb_par *par) |
291 | { |
292 | return ssd1307fb_update_rect(par, x: 0, y: 0, width: par->width, height: par->height); |
293 | } |
294 | |
295 | static int ssd1307fb_blank(int blank_mode, struct fb_info *info) |
296 | { |
297 | struct ssd1307fb_par *par = info->par; |
298 | |
299 | if (blank_mode != FB_BLANK_UNBLANK) |
300 | return ssd1307fb_write_cmd(client: par->client, SSD1307FB_DISPLAY_OFF); |
301 | else |
302 | return ssd1307fb_write_cmd(client: par->client, SSD1307FB_DISPLAY_ON); |
303 | } |
304 | |
305 | static void ssd1307fb_defio_damage_range(struct fb_info *info, off_t off, size_t len) |
306 | { |
307 | struct ssd1307fb_par *par = info->par; |
308 | |
309 | ssd1307fb_update_display(par); |
310 | } |
311 | |
312 | static void ssd1307fb_defio_damage_area(struct fb_info *info, u32 x, u32 y, |
313 | u32 width, u32 height) |
314 | { |
315 | struct ssd1307fb_par *par = info->par; |
316 | |
317 | ssd1307fb_update_rect(par, x, y, width, height); |
318 | } |
319 | |
320 | FB_GEN_DEFAULT_DEFERRED_SYSMEM_OPS(ssd1307fb, |
321 | ssd1307fb_defio_damage_range, |
322 | ssd1307fb_defio_damage_area) |
323 | |
324 | static const struct fb_ops ssd1307fb_ops = { |
325 | .owner = THIS_MODULE, |
326 | FB_DEFAULT_DEFERRED_OPS(ssd1307fb), |
327 | .fb_blank = ssd1307fb_blank, |
328 | }; |
329 | |
330 | static void ssd1307fb_deferred_io(struct fb_info *info, struct list_head *) |
331 | { |
332 | ssd1307fb_update_display(par: info->par); |
333 | } |
334 | |
335 | static int ssd1307fb_init(struct ssd1307fb_par *par) |
336 | { |
337 | struct pwm_state pwmstate; |
338 | int ret; |
339 | u32 precharge, dclk, com_invdir, compins; |
340 | |
341 | if (par->device_info->need_pwm) { |
342 | par->pwm = pwm_get(dev: &par->client->dev, NULL); |
343 | if (IS_ERR(ptr: par->pwm)) { |
344 | dev_err(&par->client->dev, "Could not get PWM from device tree!\n" ); |
345 | return PTR_ERR(ptr: par->pwm); |
346 | } |
347 | |
348 | pwm_init_state(pwm: par->pwm, state: &pwmstate); |
349 | pwm_set_relative_duty_cycle(state: &pwmstate, duty_cycle: 50, scale: 100); |
350 | pwm_apply_might_sleep(pwm: par->pwm, state: &pwmstate); |
351 | |
352 | /* Enable the PWM */ |
353 | pwm_enable(pwm: par->pwm); |
354 | |
355 | dev_dbg(&par->client->dev, "Using PWM %s with a %lluns period.\n" , |
356 | par->pwm->label, pwm_get_period(par->pwm)); |
357 | } |
358 | |
359 | /* Set initial contrast */ |
360 | ret = ssd1307fb_write_cmd(client: par->client, SSD1307FB_CONTRAST); |
361 | if (ret < 0) |
362 | return ret; |
363 | |
364 | ret = ssd1307fb_write_cmd(client: par->client, cmd: par->contrast); |
365 | if (ret < 0) |
366 | return ret; |
367 | |
368 | /* Set segment re-map */ |
369 | if (par->seg_remap) { |
370 | ret = ssd1307fb_write_cmd(client: par->client, SSD1307FB_SEG_REMAP_ON); |
371 | if (ret < 0) |
372 | return ret; |
373 | } |
374 | |
375 | /* Set COM direction */ |
376 | com_invdir = 0xc0 | par->com_invdir << 3; |
377 | ret = ssd1307fb_write_cmd(client: par->client, cmd: com_invdir); |
378 | if (ret < 0) |
379 | return ret; |
380 | |
381 | /* Set multiplex ratio value */ |
382 | ret = ssd1307fb_write_cmd(client: par->client, SSD1307FB_SET_MULTIPLEX_RATIO); |
383 | if (ret < 0) |
384 | return ret; |
385 | |
386 | ret = ssd1307fb_write_cmd(client: par->client, cmd: par->height - 1); |
387 | if (ret < 0) |
388 | return ret; |
389 | |
390 | /* set display offset value */ |
391 | ret = ssd1307fb_write_cmd(client: par->client, SSD1307FB_SET_DISPLAY_OFFSET); |
392 | if (ret < 0) |
393 | return ret; |
394 | |
395 | ret = ssd1307fb_write_cmd(client: par->client, cmd: par->com_offset); |
396 | if (ret < 0) |
397 | return ret; |
398 | |
399 | /* Set clock frequency */ |
400 | ret = ssd1307fb_write_cmd(client: par->client, SSD1307FB_SET_CLOCK_FREQ); |
401 | if (ret < 0) |
402 | return ret; |
403 | |
404 | dclk = ((par->dclk_div - 1) & 0xf) | (par->dclk_frq & 0xf) << 4; |
405 | ret = ssd1307fb_write_cmd(client: par->client, cmd: dclk); |
406 | if (ret < 0) |
407 | return ret; |
408 | |
409 | /* Set Area Color Mode ON/OFF & Low Power Display Mode */ |
410 | if (par->area_color_enable || par->low_power) { |
411 | u32 mode; |
412 | |
413 | ret = ssd1307fb_write_cmd(client: par->client, |
414 | SSD1307FB_SET_AREA_COLOR_MODE); |
415 | if (ret < 0) |
416 | return ret; |
417 | |
418 | mode = (par->area_color_enable ? 0x30 : 0) | |
419 | (par->low_power ? 5 : 0); |
420 | ret = ssd1307fb_write_cmd(client: par->client, cmd: mode); |
421 | if (ret < 0) |
422 | return ret; |
423 | } |
424 | |
425 | /* Set precharge period in number of ticks from the internal clock */ |
426 | ret = ssd1307fb_write_cmd(client: par->client, SSD1307FB_SET_PRECHARGE_PERIOD); |
427 | if (ret < 0) |
428 | return ret; |
429 | |
430 | precharge = (par->prechargep1 & 0xf) | (par->prechargep2 & 0xf) << 4; |
431 | ret = ssd1307fb_write_cmd(client: par->client, cmd: precharge); |
432 | if (ret < 0) |
433 | return ret; |
434 | |
435 | /* Set COM pins configuration */ |
436 | ret = ssd1307fb_write_cmd(client: par->client, SSD1307FB_SET_COM_PINS_CONFIG); |
437 | if (ret < 0) |
438 | return ret; |
439 | |
440 | compins = 0x02 | !par->com_seq << 4 | par->com_lrremap << 5; |
441 | ret = ssd1307fb_write_cmd(client: par->client, cmd: compins); |
442 | if (ret < 0) |
443 | return ret; |
444 | |
445 | /* Set VCOMH */ |
446 | ret = ssd1307fb_write_cmd(client: par->client, SSD1307FB_SET_VCOMH); |
447 | if (ret < 0) |
448 | return ret; |
449 | |
450 | ret = ssd1307fb_write_cmd(client: par->client, cmd: par->vcomh); |
451 | if (ret < 0) |
452 | return ret; |
453 | |
454 | /* Turn on the DC-DC Charge Pump */ |
455 | ret = ssd1307fb_write_cmd(client: par->client, SSD1307FB_CHARGE_PUMP); |
456 | if (ret < 0) |
457 | return ret; |
458 | |
459 | ret = ssd1307fb_write_cmd(client: par->client, |
460 | BIT(4) | (par->device_info->need_chargepump ? BIT(2) : 0)); |
461 | if (ret < 0) |
462 | return ret; |
463 | |
464 | /* Set lookup table */ |
465 | if (par->lookup_table_set) { |
466 | int i; |
467 | |
468 | ret = ssd1307fb_write_cmd(client: par->client, |
469 | SSD1307FB_SET_LOOKUP_TABLE); |
470 | if (ret < 0) |
471 | return ret; |
472 | |
473 | for (i = 0; i < ARRAY_SIZE(par->lookup_table); ++i) { |
474 | u8 val = par->lookup_table[i]; |
475 | |
476 | if (val < 31 || val > 63) |
477 | dev_warn(&par->client->dev, |
478 | "lookup table index %d value out of range 31 <= %d <= 63\n" , |
479 | i, val); |
480 | ret = ssd1307fb_write_cmd(client: par->client, cmd: val); |
481 | if (ret < 0) |
482 | return ret; |
483 | } |
484 | } |
485 | |
486 | /* Switch to horizontal addressing mode */ |
487 | ret = ssd1307fb_write_cmd(client: par->client, SSD1307FB_SET_ADDRESS_MODE); |
488 | if (ret < 0) |
489 | return ret; |
490 | |
491 | ret = ssd1307fb_write_cmd(client: par->client, |
492 | SSD1307FB_SET_ADDRESS_MODE_HORIZONTAL); |
493 | if (ret < 0) |
494 | return ret; |
495 | |
496 | /* Clear the screen */ |
497 | ret = ssd1307fb_update_display(par); |
498 | if (ret < 0) |
499 | return ret; |
500 | |
501 | /* Turn on the display */ |
502 | ret = ssd1307fb_write_cmd(client: par->client, SSD1307FB_DISPLAY_ON); |
503 | if (ret < 0) |
504 | return ret; |
505 | |
506 | return 0; |
507 | } |
508 | |
509 | static int ssd1307fb_update_bl(struct backlight_device *bdev) |
510 | { |
511 | struct ssd1307fb_par *par = bl_get_data(bl_dev: bdev); |
512 | int ret; |
513 | int brightness = bdev->props.brightness; |
514 | |
515 | par->contrast = brightness; |
516 | |
517 | ret = ssd1307fb_write_cmd(client: par->client, SSD1307FB_CONTRAST); |
518 | if (ret < 0) |
519 | return ret; |
520 | ret = ssd1307fb_write_cmd(client: par->client, cmd: par->contrast); |
521 | if (ret < 0) |
522 | return ret; |
523 | return 0; |
524 | } |
525 | |
526 | static int ssd1307fb_get_brightness(struct backlight_device *bdev) |
527 | { |
528 | struct ssd1307fb_par *par = bl_get_data(bl_dev: bdev); |
529 | |
530 | return par->contrast; |
531 | } |
532 | |
533 | static int ssd1307fb_check_fb(struct backlight_device *bdev, |
534 | struct fb_info *info) |
535 | { |
536 | return (info->bl_dev == bdev); |
537 | } |
538 | |
539 | static const struct backlight_ops ssd1307fb_bl_ops = { |
540 | .options = BL_CORE_SUSPENDRESUME, |
541 | .update_status = ssd1307fb_update_bl, |
542 | .get_brightness = ssd1307fb_get_brightness, |
543 | .check_fb = ssd1307fb_check_fb, |
544 | }; |
545 | |
546 | static struct ssd1307fb_deviceinfo ssd1307fb_ssd1305_deviceinfo = { |
547 | .default_vcomh = 0x34, |
548 | .default_dclk_div = 1, |
549 | .default_dclk_frq = 7, |
550 | }; |
551 | |
552 | static struct ssd1307fb_deviceinfo ssd1307fb_ssd1306_deviceinfo = { |
553 | .default_vcomh = 0x20, |
554 | .default_dclk_div = 1, |
555 | .default_dclk_frq = 8, |
556 | .need_chargepump = 1, |
557 | }; |
558 | |
559 | static struct ssd1307fb_deviceinfo ssd1307fb_ssd1307_deviceinfo = { |
560 | .default_vcomh = 0x20, |
561 | .default_dclk_div = 2, |
562 | .default_dclk_frq = 12, |
563 | .need_pwm = 1, |
564 | }; |
565 | |
566 | static struct ssd1307fb_deviceinfo ssd1307fb_ssd1309_deviceinfo = { |
567 | .default_vcomh = 0x34, |
568 | .default_dclk_div = 1, |
569 | .default_dclk_frq = 10, |
570 | }; |
571 | |
572 | static const struct of_device_id ssd1307fb_of_match[] = { |
573 | { |
574 | .compatible = "solomon,ssd1305fb-i2c" , |
575 | .data = (void *)&ssd1307fb_ssd1305_deviceinfo, |
576 | }, |
577 | { |
578 | .compatible = "solomon,ssd1306fb-i2c" , |
579 | .data = (void *)&ssd1307fb_ssd1306_deviceinfo, |
580 | }, |
581 | { |
582 | .compatible = "solomon,ssd1307fb-i2c" , |
583 | .data = (void *)&ssd1307fb_ssd1307_deviceinfo, |
584 | }, |
585 | { |
586 | .compatible = "solomon,ssd1309fb-i2c" , |
587 | .data = (void *)&ssd1307fb_ssd1309_deviceinfo, |
588 | }, |
589 | {}, |
590 | }; |
591 | MODULE_DEVICE_TABLE(of, ssd1307fb_of_match); |
592 | |
593 | static int ssd1307fb_probe(struct i2c_client *client) |
594 | { |
595 | struct device *dev = &client->dev; |
596 | struct backlight_device *bl; |
597 | char bl_name[12]; |
598 | struct fb_info *info; |
599 | struct fb_deferred_io *ssd1307fb_defio; |
600 | u32 vmem_size; |
601 | struct ssd1307fb_par *par; |
602 | void *vmem; |
603 | int ret; |
604 | |
605 | info = framebuffer_alloc(size: sizeof(struct ssd1307fb_par), dev); |
606 | if (!info) |
607 | return -ENOMEM; |
608 | |
609 | par = info->par; |
610 | par->info = info; |
611 | par->client = client; |
612 | |
613 | par->device_info = device_get_match_data(dev); |
614 | |
615 | par->reset = devm_gpiod_get_optional(dev, con_id: "reset" , flags: GPIOD_OUT_LOW); |
616 | if (IS_ERR(ptr: par->reset)) { |
617 | ret = dev_err_probe(dev, err: PTR_ERR(ptr: par->reset), |
618 | fmt: "failed to get reset gpio\n" ); |
619 | goto fb_alloc_error; |
620 | } |
621 | |
622 | par->vbat_reg = devm_regulator_get_optional(dev, id: "vbat" ); |
623 | if (IS_ERR(ptr: par->vbat_reg)) { |
624 | ret = PTR_ERR(ptr: par->vbat_reg); |
625 | if (ret == -ENODEV) { |
626 | par->vbat_reg = NULL; |
627 | } else { |
628 | dev_err_probe(dev, err: ret, fmt: "failed to get VBAT regulator\n" ); |
629 | goto fb_alloc_error; |
630 | } |
631 | } |
632 | |
633 | if (device_property_read_u32(dev, propname: "solomon,width" , val: &par->width)) |
634 | par->width = 96; |
635 | |
636 | if (device_property_read_u32(dev, propname: "solomon,height" , val: &par->height)) |
637 | par->height = 16; |
638 | |
639 | if (device_property_read_u32(dev, propname: "solomon,page-offset" , val: &par->page_offset)) |
640 | par->page_offset = 1; |
641 | |
642 | if (device_property_read_u32(dev, propname: "solomon,col-offset" , val: &par->col_offset)) |
643 | par->col_offset = 0; |
644 | |
645 | if (device_property_read_u32(dev, propname: "solomon,com-offset" , val: &par->com_offset)) |
646 | par->com_offset = 0; |
647 | |
648 | if (device_property_read_u32(dev, propname: "solomon,prechargep1" , val: &par->prechargep1)) |
649 | par->prechargep1 = 2; |
650 | |
651 | if (device_property_read_u32(dev, propname: "solomon,prechargep2" , val: &par->prechargep2)) |
652 | par->prechargep2 = 2; |
653 | |
654 | if (!device_property_read_u8_array(dev, propname: "solomon,lookup-table" , |
655 | val: par->lookup_table, |
656 | ARRAY_SIZE(par->lookup_table))) |
657 | par->lookup_table_set = 1; |
658 | |
659 | par->seg_remap = !device_property_read_bool(dev, propname: "solomon,segment-no-remap" ); |
660 | par->com_seq = device_property_read_bool(dev, propname: "solomon,com-seq" ); |
661 | par->com_lrremap = device_property_read_bool(dev, propname: "solomon,com-lrremap" ); |
662 | par->com_invdir = device_property_read_bool(dev, propname: "solomon,com-invdir" ); |
663 | par->area_color_enable = |
664 | device_property_read_bool(dev, propname: "solomon,area-color-enable" ); |
665 | par->low_power = device_property_read_bool(dev, propname: "solomon,low-power" ); |
666 | |
667 | par->contrast = 127; |
668 | par->vcomh = par->device_info->default_vcomh; |
669 | |
670 | /* Setup display timing */ |
671 | if (device_property_read_u32(dev, propname: "solomon,dclk-div" , val: &par->dclk_div)) |
672 | par->dclk_div = par->device_info->default_dclk_div; |
673 | if (device_property_read_u32(dev, propname: "solomon,dclk-frq" , val: &par->dclk_frq)) |
674 | par->dclk_frq = par->device_info->default_dclk_frq; |
675 | |
676 | vmem_size = DIV_ROUND_UP(par->width, 8) * par->height; |
677 | |
678 | vmem = (void *)__get_free_pages(GFP_KERNEL | __GFP_ZERO, |
679 | order: get_order(size: vmem_size)); |
680 | if (!vmem) { |
681 | dev_err(dev, "Couldn't allocate graphical memory.\n" ); |
682 | ret = -ENOMEM; |
683 | goto fb_alloc_error; |
684 | } |
685 | |
686 | ssd1307fb_defio = devm_kzalloc(dev, size: sizeof(*ssd1307fb_defio), |
687 | GFP_KERNEL); |
688 | if (!ssd1307fb_defio) { |
689 | dev_err(dev, "Couldn't allocate deferred io.\n" ); |
690 | ret = -ENOMEM; |
691 | goto fb_alloc_error; |
692 | } |
693 | |
694 | ssd1307fb_defio->delay = HZ / refreshrate; |
695 | ssd1307fb_defio->deferred_io = ssd1307fb_deferred_io; |
696 | |
697 | info->fbops = &ssd1307fb_ops; |
698 | info->fix = ssd1307fb_fix; |
699 | info->fix.line_length = DIV_ROUND_UP(par->width, 8); |
700 | info->fbdefio = ssd1307fb_defio; |
701 | |
702 | info->var = ssd1307fb_var; |
703 | info->var.xres = par->width; |
704 | info->var.xres_virtual = par->width; |
705 | info->var.yres = par->height; |
706 | info->var.yres_virtual = par->height; |
707 | |
708 | info->screen_buffer = vmem; |
709 | info->fix.smem_start = __pa(vmem); |
710 | info->fix.smem_len = vmem_size; |
711 | |
712 | fb_deferred_io_init(info); |
713 | |
714 | i2c_set_clientdata(client, data: info); |
715 | |
716 | if (par->reset) { |
717 | /* Reset the screen */ |
718 | gpiod_set_value_cansleep(desc: par->reset, value: 1); |
719 | udelay(4); |
720 | gpiod_set_value_cansleep(desc: par->reset, value: 0); |
721 | udelay(4); |
722 | } |
723 | |
724 | if (par->vbat_reg) { |
725 | ret = regulator_enable(regulator: par->vbat_reg); |
726 | if (ret) { |
727 | dev_err(dev, "failed to enable VBAT: %d\n" , ret); |
728 | goto reset_oled_error; |
729 | } |
730 | } |
731 | |
732 | ret = ssd1307fb_init(par); |
733 | if (ret) |
734 | goto regulator_enable_error; |
735 | |
736 | ret = register_framebuffer(fb_info: info); |
737 | if (ret) { |
738 | dev_err(dev, "Couldn't register the framebuffer\n" ); |
739 | goto panel_init_error; |
740 | } |
741 | |
742 | snprintf(buf: bl_name, size: sizeof(bl_name), fmt: "ssd1307fb%d" , info->node); |
743 | bl = backlight_device_register(name: bl_name, dev, devdata: par, ops: &ssd1307fb_bl_ops, |
744 | NULL); |
745 | if (IS_ERR(ptr: bl)) { |
746 | ret = PTR_ERR(ptr: bl); |
747 | dev_err(dev, "unable to register backlight device: %d\n" , ret); |
748 | goto bl_init_error; |
749 | } |
750 | |
751 | bl->props.brightness = par->contrast; |
752 | bl->props.max_brightness = MAX_CONTRAST; |
753 | info->bl_dev = bl; |
754 | |
755 | dev_info(dev, "fb%d: %s framebuffer device registered, using %d bytes of video memory\n" , info->node, info->fix.id, vmem_size); |
756 | |
757 | return 0; |
758 | |
759 | bl_init_error: |
760 | unregister_framebuffer(fb_info: info); |
761 | panel_init_error: |
762 | pwm_disable(pwm: par->pwm); |
763 | pwm_put(pwm: par->pwm); |
764 | regulator_enable_error: |
765 | if (par->vbat_reg) |
766 | regulator_disable(regulator: par->vbat_reg); |
767 | reset_oled_error: |
768 | fb_deferred_io_cleanup(info); |
769 | fb_alloc_error: |
770 | framebuffer_release(info); |
771 | return ret; |
772 | } |
773 | |
774 | static void ssd1307fb_remove(struct i2c_client *client) |
775 | { |
776 | struct fb_info *info = i2c_get_clientdata(client); |
777 | struct ssd1307fb_par *par = info->par; |
778 | |
779 | ssd1307fb_write_cmd(client: par->client, SSD1307FB_DISPLAY_OFF); |
780 | |
781 | backlight_device_unregister(bd: info->bl_dev); |
782 | |
783 | unregister_framebuffer(fb_info: info); |
784 | pwm_disable(pwm: par->pwm); |
785 | pwm_put(pwm: par->pwm); |
786 | if (par->vbat_reg) |
787 | regulator_disable(regulator: par->vbat_reg); |
788 | fb_deferred_io_cleanup(info); |
789 | __free_pages(__va(info->fix.smem_start), order: get_order(size: info->fix.smem_len)); |
790 | framebuffer_release(info); |
791 | } |
792 | |
793 | static const struct i2c_device_id ssd1307fb_i2c_id[] = { |
794 | { "ssd1305fb" , 0 }, |
795 | { "ssd1306fb" , 0 }, |
796 | { "ssd1307fb" , 0 }, |
797 | { "ssd1309fb" , 0 }, |
798 | { } |
799 | }; |
800 | MODULE_DEVICE_TABLE(i2c, ssd1307fb_i2c_id); |
801 | |
802 | static struct i2c_driver ssd1307fb_driver = { |
803 | .probe = ssd1307fb_probe, |
804 | .remove = ssd1307fb_remove, |
805 | .id_table = ssd1307fb_i2c_id, |
806 | .driver = { |
807 | .name = "ssd1307fb" , |
808 | .of_match_table = ssd1307fb_of_match, |
809 | }, |
810 | }; |
811 | |
812 | module_i2c_driver(ssd1307fb_driver); |
813 | |
814 | MODULE_DESCRIPTION("FB driver for the Solomon SSD1307 OLED controller" ); |
815 | MODULE_AUTHOR("Maxime Ripard <maxime.ripard@free-electrons.com>" ); |
816 | MODULE_LICENSE("GPL" ); |
817 | |