summaryrefslogtreecommitdiff
path: root/drivers/gpu/drm/sitronix/st7920.c
blob: 18fe7a28ed07308e16d1d6c737db856e0a490236 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
// SPDX-License-Identifier: GPL-2.0-only
/*
 * DRM driver for Sitronix ST7920 LCD displays
 *
 * Copyright 2025 Iker Pedrosa <ikerpedrosam@gmail.com>
 *
 */

#include <linux/bitrev.h>
#include <linux/delay.h>
#include <linux/gpio/consumer.h>
#include <linux/module.h>
#include <linux/regmap.h>
#include <linux/spi/spi.h>

#include <drm/clients/drm_client_setup.h>
#include <drm/drm_atomic.h>
#include <drm/drm_atomic_helper.h>
#include <drm/drm_crtc_helper.h>
#include <drm/drm_damage_helper.h>
#include <drm/drm_drv.h>
#include <drm/drm_fbdev_shmem.h>
#include <drm/drm_framebuffer.h>
#include <drm/drm_gem_atomic_helper.h>
#include <drm/drm_gem_framebuffer_helper.h>
#include <drm/drm_gem_shmem_helper.h>
#include <drm/drm_plane.h>
#include <drm/drm_print.h>
#include <drm/drm_probe_helper.h>

#define DRIVER_NAME	"sitronix_st7920"
#define DRIVER_DESC	"DRM driver for Sitronix ST7920 LCD displays"
#define DRIVER_MAJOR	1
#define DRIVER_MINOR	0

/* Display organization */
#define ST7920_PITCH		16
#define ST7920_SCANLINES	64
#define BYTES_IN_DISPLAY	(ST7920_PITCH * ST7920_SCANLINES)
#define BYTES_IN_SEGMENT	2
#define PIXELS_PER_SEGMENT	(BYTES_IN_SEGMENT * 8)
#define ST7920_DEFAULT_WIDTH	128
#define ST7920_DEFAULT_HEIGHT	64

/* Sync sequence */
#define SYNC_BITS			0xF8
#define RW_HIGH				0x04
#define RS_HIGH				0x02

/* Commands */
#define SET_DISPLAY_ON			0x0C
#define SET_DISPLAY_OFF			0x08
#define SET_DISPLAY_CLEAR		0x01
#define SET_BASIC_INSTRUCTION_SET	0x30
#define SET_EXT_INSTRUCTION_SET		0x34
#define SET_GRAPHICS_DISPLAY		0x36
#define SET_GDRAM_ADDRESS		0x80
#define SET_GDRAM_DATA			0xFF /* Driver internal command */

/* Masks */
#define HIGH_DATA_MASK			0xF0
#define LOW_DATA_MASK			0x0F
#define TOP_VERTICAL_ADDRESS		0x80
#define BOTTOM_VERTICAL_ADDRESS		0x60
#define TOP_HORIZONTAL_ADDRESS		0x00
#define BOTTOM_HORIZONTAL_ADDRESS	0x80

#define CMD_SIZE			35

struct spi7920_error {
	int errno;
};

struct st7920_device {
	struct drm_device drm;
	struct drm_display_mode mode;
	struct drm_plane primary_plane;
	struct drm_crtc crtc;
	struct drm_encoder encoder;
	struct drm_connector connector;
	struct spi_device *spi;

	struct regmap *regmap;

	struct gpio_desc *reset_gpio;

	u32 height;
	u32 width;
};

struct st7920_plane_state {
	struct drm_shadow_plane_state base;
	/* Intermediate buffer to convert pixels from XRGB8888 to HW format */
	u8 *buffer;
};

struct st7920_crtc_state {
	struct drm_crtc_state base;
	/* Buffer to store pixels in HW format and written to the panel */
	u8 *data_array;
};

static inline struct st7920_plane_state *to_st7920_plane_state(struct drm_plane_state *state)
{
	return container_of(state, struct st7920_plane_state, base.base);
}

static inline struct st7920_crtc_state *to_st7920_crtc_state(struct drm_crtc_state *state)
{
	return container_of(state, struct st7920_crtc_state, base);
}

static inline struct st7920_device *drm_to_st7920(struct drm_device *drm)
{
	return container_of(drm, struct st7920_device, drm);
}

static int st7920_store_gdram_address(const void *data, u8 *reg)
{
	const u8 y_addr = *(const u8 *)data;
	bool bottom_screen = (y_addr >= 32);
	int i = 0;

	reg[i++] = SYNC_BITS;
	/* Set vertical address */
	if (!bottom_screen)
		reg[i++] = TOP_VERTICAL_ADDRESS + (*(uint8_t *)data & HIGH_DATA_MASK);
	else
		reg[i++] = BOTTOM_VERTICAL_ADDRESS + (*(uint8_t *)data & HIGH_DATA_MASK);

	reg[i++] = *(uint8_t *)data << 4;
	/* Set horizontal address */
	reg[i++] = SET_GDRAM_ADDRESS;
	if (!bottom_screen)
		reg[i++] = TOP_HORIZONTAL_ADDRESS;
	else
		reg[i++] = BOTTOM_HORIZONTAL_ADDRESS;

	return i;
}

static int st7920_store_gdram_data(const void *data, u8 *reg)
{
	const u8 *line_data = data;
	int i = 0, j = 0;

	reg[i++] = SYNC_BITS | RS_HIGH;

	for (j = 0; j < 16; j++) {
		reg[i++] = line_data[j] & 0xF0;
		reg[i++] = (line_data[j] << 4) & 0xF0;
	}

	return i;
}

static int st7920_store_others(int cmd, const void *data, u8 *reg)
{
	int i = 0;

	reg[i++] = SYNC_BITS;
	reg[i++] = cmd & HIGH_DATA_MASK;
	reg[i++] = (cmd & LOW_DATA_MASK) << 4;

	return i;
}

static void st7920_spi_write(struct spi_device *spi, int cmd, const void *data,
			     int delay_us, struct spi7920_error *err)
{
	u8 reg[CMD_SIZE] = {0};
	int size = 0;
	int ret;

	if (err->errno)
		return;

	/*
	 * First the sync bits are sent: 11111WS0.
	 * Where W is the read/write (RW) bit and S is the register/data (RS) bit.
	 * Then, every 8 bits instruction/data will be separated into 2 groups.
	 * Higher 4 bits (DB7~DB4) will be placed in the first section followed by
	 * 4 '0's. And lower 4 bits (DB3~DB0) will be placed in the second section
	 * followed by 4 '0's.
	 */
	if (cmd == SET_GDRAM_ADDRESS)
		size = st7920_store_gdram_address(data, reg);
	else if (cmd == SET_GDRAM_DATA)
		size = st7920_store_gdram_data(data, reg);
	else
		size = st7920_store_others(cmd, data, reg);

	ret = spi_write(spi, reg, size);
	if (ret) {
		err->errno = ret;
		return;
	}

	if (delay_us)
		udelay(delay_us);
}

static const struct regmap_config st7920_spi_regmap_config = {
	.reg_bits = 8,
	.val_bits = 8,
};

static const struct of_device_id st7920_of_match[] = {
	/* st7920 family */
	{
		.compatible = "sitronix,st7920",
	},
	{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, st7920_of_match);

/*
 * The SPI core always reports a MODALIAS uevent of the form "spi:<dev>", even
 * if the device was registered via OF. This means that the module will not be
 * auto loaded, unless it contains an alias that matches the MODALIAS reported.
 *
 * To workaround this issue, add a SPI device ID table. Even when this should
 * not be needed for this driver to match the registered SPI devices.
 */
static const struct spi_device_id st7920_spi_id[] = {
	/* st7920 family */
	{ "st7920",  0 },
	{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(spi, st7920_spi_id);

static void st7920_power_on(struct st7920_device *st7920,
			    struct spi7920_error *err)
{
	st7920_spi_write(st7920->spi, SET_DISPLAY_ON, NULL, 72, err);
}

static void st7920_power_off(struct st7920_device *st7920,
			     struct spi7920_error *err)
{
	st7920_spi_write(st7920->spi, SET_DISPLAY_CLEAR, NULL, 1600, err);
	st7920_spi_write(st7920->spi, SET_DISPLAY_OFF, NULL, 72, err);
}

static void st7920_hw_reset(struct st7920_device *st7920)
{
	if (!st7920->reset_gpio)
		return;

	gpiod_set_value_cansleep(st7920->reset_gpio, 1);
	usleep_range(15, 20);
	gpiod_set_value_cansleep(st7920->reset_gpio, 0);
	msleep(40);
}

static int st7920_init(struct st7920_device *st7920)
{
	struct spi7920_error err = {0};

	st7920_spi_write(st7920->spi, SET_BASIC_INSTRUCTION_SET, NULL, 72, &err);
	st7920_power_on(st7920, &err);
	st7920_spi_write(st7920->spi, SET_GRAPHICS_DISPLAY, NULL, 72, &err);
	st7920_spi_write(st7920->spi, SET_DISPLAY_CLEAR, NULL, 1600, &err);

	return err.errno;
}

static int st7920_update_rect(struct st7920_device *st7920,
			      struct drm_rect *rect, u8 *buf,
			      u8 *data_array)
{
	struct spi7920_error err = {0};
	u32 array_idx = 0;
	int i, j;

	/*
	 * The screen is divided in 64(Y)x8(X) segments and each segment is
	 * further divided in 2 bytes (D15~D0).
	 *
	 * Segment 0x0 is in the top-right corner, while segment 63x15 is in the
	 * bottom-left. They would be displayed in the screen in the following way:
	 * 0x0  0x1  0x2  ... 0x15
	 * 1x0  1x1  1x2  ... 1x15
	 * ...
	 * 63x0 63x1 63x2 ... 63x15
	 *
	 * The data in each byte is big endian.
	 */

	for (i = 0; i < ST7920_SCANLINES; i++) {
		u8 *line_start = buf + (i * ST7920_PITCH);
		u8 line_buffer[ST7920_PITCH];

		for (j = 0; j < ST7920_PITCH; j++) {
			line_buffer[j] = bitrev8(line_start[j]);
			data_array[array_idx++] = line_buffer[j];
		}

		st7920_spi_write(st7920->spi, SET_GDRAM_ADDRESS, &i, 72, &err);
		st7920_spi_write(st7920->spi, SET_GDRAM_DATA, line_buffer, 72, &err);
	}

	return err.errno;
}

static void st7920_clear_screen(struct st7920_device *st7920, u8 *data_array)
{
	struct spi7920_error err = {0};

	memset(data_array, 0, BYTES_IN_DISPLAY);

	st7920_spi_write(st7920->spi, SET_DISPLAY_CLEAR, NULL, 1600, &err);
}

static int st7920_fb_blit_rect(struct drm_framebuffer *fb,
			       const struct iosys_map *vmap,
			       struct drm_rect *rect,
			       u8 *buf, u8 *data_array,
			       struct drm_format_conv_state *fmtcnv_state)
{
	struct st7920_device *st7920 = drm_to_st7920(fb->dev);
	struct iosys_map dst;
	unsigned int dst_pitch;
	int ret;

	/* Align y to display page boundaries */
	rect->y1 = round_down(rect->y1, PIXELS_PER_SEGMENT);
	rect->y2 = min_t(unsigned int, round_up(rect->y2, PIXELS_PER_SEGMENT), st7920->height);

	dst_pitch = DIV_ROUND_UP(drm_rect_width(rect), 8);

	iosys_map_set_vaddr(&dst, buf);
	drm_fb_xrgb8888_to_mono(&dst, &dst_pitch, vmap, fb, rect, fmtcnv_state);

	ret = st7920_update_rect(st7920, rect, buf, data_array);

	return ret;
}

static int st7920_primary_plane_atomic_check(struct drm_plane *plane,
					     struct drm_atomic_state *state)
{
	struct drm_device *drm = plane->dev;
	struct st7920_device *st7920 = drm_to_st7920(drm);
	struct drm_plane_state *plane_state = drm_atomic_get_new_plane_state(state, plane);
	struct st7920_plane_state *st7920_state = to_st7920_plane_state(plane_state);
	struct drm_shadow_plane_state *shadow_plane_state = &st7920_state->base;
	struct drm_crtc *crtc = plane_state->crtc;
	struct drm_crtc_state *crtc_state = NULL;
	const struct drm_format_info *fi;
	unsigned int pitch;
	int ret;

	if (crtc)
		crtc_state = drm_atomic_get_new_crtc_state(state, crtc);

	ret = drm_atomic_helper_check_plane_state(plane_state, crtc_state,
						  DRM_PLANE_NO_SCALING,
						  DRM_PLANE_NO_SCALING,
						  false, false);
	if (ret)
		return ret;
	else if (!plane_state->visible)
		return 0;

	fi = drm_format_info(DRM_FORMAT_R1);
	if (!fi)
		return -EINVAL;

	pitch = drm_format_info_min_pitch(fi, 0, st7920->width);

	if (plane_state->fb->format != fi) {
		void *buf;

		/* format conversion necessary; reserve buffer */
		buf = drm_format_conv_state_reserve(&shadow_plane_state->fmtcnv_state,
						    pitch, GFP_KERNEL);
		if (!buf)
			return -ENOMEM;
	}

	st7920_state->buffer = kcalloc(pitch, st7920->height, GFP_KERNEL);
	if (!st7920_state->buffer)
		return -ENOMEM;

	return 0;
}

static void st7920_primary_plane_atomic_update(struct drm_plane *plane,
					       struct drm_atomic_state *state)
{
	struct drm_plane_state *plane_state = drm_atomic_get_new_plane_state(state, plane);
	struct drm_plane_state *old_plane_state = drm_atomic_get_old_plane_state(state, plane);
	struct drm_shadow_plane_state *shadow_plane_state = to_drm_shadow_plane_state(plane_state);
	struct drm_crtc_state *crtc_state = drm_atomic_get_new_crtc_state(state, plane_state->crtc);
	struct st7920_crtc_state *st7920_crtc_state =  to_st7920_crtc_state(crtc_state);
	struct st7920_plane_state *st7920_plane_state = to_st7920_plane_state(plane_state);
	struct drm_framebuffer *fb = plane_state->fb;
	struct drm_atomic_helper_damage_iter iter;
	struct drm_device *drm = plane->dev;
	struct drm_rect dst_clip;
	struct drm_rect damage;
	int idx;
	int ret;

	if (!drm_dev_enter(drm, &idx))
		return;

	if (drm_gem_fb_begin_cpu_access(fb, DMA_FROM_DEVICE) == 0) {
		drm_atomic_helper_damage_iter_init(&iter, old_plane_state, plane_state);
		drm_atomic_for_each_plane_damage(&iter, &damage) {
			dst_clip = plane_state->dst;

			if (!drm_rect_intersect(&dst_clip, &damage))
				continue;

			ret = st7920_fb_blit_rect(fb, &shadow_plane_state->data[0], &dst_clip,
						  st7920_plane_state->buffer,
						  st7920_crtc_state->data_array,
						  &shadow_plane_state->fmtcnv_state);
			if (ret)
				drm_err_once(plane->dev, "Failed to write to device: %d.\n", ret);
		}

		drm_gem_fb_end_cpu_access(fb, DMA_FROM_DEVICE);
	}

	drm_dev_exit(idx);
}

static void st7920_primary_plane_atomic_disable(struct drm_plane *plane,
						struct drm_atomic_state *state)
{
	struct drm_device *drm = plane->dev;
	struct st7920_device *st7920 = drm_to_st7920(drm);
	struct drm_plane_state *plane_state = drm_atomic_get_new_plane_state(state, plane);
	struct drm_crtc_state *crtc_state;
	struct st7920_crtc_state *st7920_crtc_state;
	int idx;

	if (!plane_state->crtc)
		return;

	crtc_state = drm_atomic_get_new_crtc_state(state, plane_state->crtc);
	st7920_crtc_state = to_st7920_crtc_state(crtc_state);

	if (!drm_dev_enter(drm, &idx))
		return;

	st7920_clear_screen(st7920, st7920_crtc_state->data_array);

	drm_dev_exit(idx);
}

/* Called during init to allocate the plane's atomic state. */
static void st7920_primary_plane_reset(struct drm_plane *plane)
{
	struct st7920_plane_state *st7920_state;

	drm_WARN_ON_ONCE(plane->dev, plane->state);

	st7920_state = kzalloc_obj(*st7920_state);
	if (!st7920_state)
		return;

	__drm_gem_reset_shadow_plane(plane, &st7920_state->base);
}

static struct drm_plane_state *st7920_primary_plane_duplicate_state(struct drm_plane *plane)
{
	struct drm_shadow_plane_state *new_shadow_plane_state;
	struct st7920_plane_state *st7920_state;

	if (drm_WARN_ON_ONCE(plane->dev, !plane->state))
		return NULL;

	st7920_state = kzalloc_obj(*st7920_state);
	if (!st7920_state)
		return NULL;

	new_shadow_plane_state = &st7920_state->base;

	__drm_gem_duplicate_shadow_plane_state(plane, new_shadow_plane_state);

	return &new_shadow_plane_state->base;
}

static void st7920_primary_plane_destroy_state(struct drm_plane *plane,
					       struct drm_plane_state *state)
{
	struct st7920_plane_state *st7920_state = to_st7920_plane_state(state);

	kfree(st7920_state->buffer);

	__drm_gem_destroy_shadow_plane_state(&st7920_state->base);

	kfree(st7920_state);
}

static const struct drm_plane_helper_funcs st7920_primary_plane_helper_funcs = {
	DRM_GEM_SHADOW_PLANE_HELPER_FUNCS,
	.atomic_check = st7920_primary_plane_atomic_check,
	.atomic_update = st7920_primary_plane_atomic_update,
	.atomic_disable = st7920_primary_plane_atomic_disable,
};

static const struct drm_plane_funcs st7920_primary_plane_funcs = {
	.update_plane = drm_atomic_helper_update_plane,
	.disable_plane = drm_atomic_helper_disable_plane,
	.reset = st7920_primary_plane_reset,
	.atomic_duplicate_state = st7920_primary_plane_duplicate_state,
	.atomic_destroy_state = st7920_primary_plane_destroy_state,
	.destroy = drm_plane_cleanup,
};

static enum drm_mode_status st7920_crtc_mode_valid(struct drm_crtc *crtc,
						   const struct drm_display_mode *mode)
{
	struct st7920_device *st7920 = drm_to_st7920(crtc->dev);

	return drm_crtc_helper_mode_valid_fixed(crtc, mode, &st7920->mode);
}

static int st7920_crtc_atomic_check(struct drm_crtc *crtc,
				    struct drm_atomic_state *state)
{
	struct drm_crtc_state *crtc_state = drm_atomic_get_new_crtc_state(state, crtc);
	struct st7920_crtc_state *st7920_state = to_st7920_crtc_state(crtc_state);
	int ret;

	ret = drm_crtc_helper_atomic_check(crtc, state);
	if (ret)
		return ret;

	st7920_state->data_array = kmalloc(BYTES_IN_DISPLAY, GFP_KERNEL);
	if (!st7920_state->data_array)
		return -ENOMEM;

	return 0;
}

static void st7920_crtc_atomic_enable(struct drm_crtc *crtc,
				      struct drm_atomic_state *state)
{
	struct drm_device *drm = crtc->dev;
	struct st7920_device *st7920 = drm_to_st7920(drm);
	int idx;
	int ret;

	if (!drm_dev_enter(drm, &idx))
		return;

	st7920_hw_reset(st7920);

	ret = st7920_init(st7920);
	if (ret)
		drm_err(drm, "Failed to init hardware: %d\n", ret);

	drm_dev_exit(idx);
}

static void st7920_crtc_atomic_disable(struct drm_crtc *crtc,
				       struct drm_atomic_state *state)
{
	struct spi7920_error err = {0};
	struct drm_device *drm = crtc->dev;
	struct st7920_device *st7920 = drm_to_st7920(drm);
	int idx;

	drm_dev_enter(drm, &idx);

	st7920_power_off(st7920, &err);

	drm_dev_exit(idx);
}

/* Called during init to allocate the CRTC's atomic state. */
static void st7920_crtc_reset(struct drm_crtc *crtc)
{
	struct st7920_crtc_state *st7920_state;

	drm_WARN_ON_ONCE(crtc->dev, crtc->state);

	st7920_state = kzalloc_obj(*st7920_state);
	if (!st7920_state)
		return;

	__drm_atomic_helper_crtc_reset(crtc, &st7920_state->base);
}

static struct drm_crtc_state *st7920_crtc_duplicate_state(struct drm_crtc *crtc)
{
	struct st7920_crtc_state *st7920_state;

	if (drm_WARN_ON_ONCE(crtc->dev, !crtc->state))
		return NULL;

	st7920_state = kzalloc_obj(*st7920_state);
	if (!st7920_state)
		return NULL;

	__drm_atomic_helper_crtc_duplicate_state(crtc, &st7920_state->base);

	return &st7920_state->base;
}

static void st7920_crtc_destroy_state(struct drm_crtc *crtc,
				      struct drm_crtc_state *state)
{
	struct st7920_crtc_state *st7920_state = to_st7920_crtc_state(state);

	kfree(st7920_state->data_array);

	__drm_atomic_helper_crtc_destroy_state(state);

	kfree(st7920_state);
}

/*
 * The CRTC is always enabled. Screen updates are performed by
 * the primary plane's atomic_update function. Disabling clears
 * the screen in the primary plane's atomic_disable function.
 */
static const struct drm_crtc_helper_funcs st7920_crtc_helper_funcs = {
	.mode_valid = st7920_crtc_mode_valid,
	.atomic_check = st7920_crtc_atomic_check,
	.atomic_enable = st7920_crtc_atomic_enable,
	.atomic_disable = st7920_crtc_atomic_disable,
};

static const struct drm_crtc_funcs st7920_crtc_funcs = {
	.reset = st7920_crtc_reset,
	.destroy = drm_crtc_cleanup,
	.set_config = drm_atomic_helper_set_config,
	.page_flip = drm_atomic_helper_page_flip,
	.atomic_duplicate_state = st7920_crtc_duplicate_state,
	.atomic_destroy_state = st7920_crtc_destroy_state,
};

static const struct drm_encoder_funcs st7920_encoder_funcs = {
	.destroy = drm_encoder_cleanup,
};

static int st7920_connector_get_modes(struct drm_connector *connector)
{
	struct st7920_device *st7920 = drm_to_st7920(connector->dev);

	return drm_connector_helper_get_modes_fixed(connector, &st7920->mode);
}

static const struct drm_connector_helper_funcs st7920_connector_helper_funcs = {
	.get_modes = st7920_connector_get_modes,
};

static const struct drm_connector_funcs st7920_connector_funcs = {
	.reset = drm_atomic_helper_connector_reset,
	.fill_modes = drm_helper_probe_single_connector_modes,
	.destroy = drm_connector_cleanup,
	.atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
	.atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
};

static const struct drm_mode_config_funcs st7920_mode_config_funcs = {
	.fb_create = drm_gem_fb_create_with_dirty,
	.atomic_check = drm_atomic_helper_check,
	.atomic_commit = drm_atomic_helper_commit,
};

static const uint32_t st7920_formats[] = {
	DRM_FORMAT_XRGB8888,
};

DEFINE_DRM_GEM_FOPS(st7920_fops);

static const struct drm_driver st7920_drm_driver = {
	DRM_GEM_SHMEM_DRIVER_OPS,
	DRM_FBDEV_SHMEM_DRIVER_OPS,
	.name			= DRIVER_NAME,
	.desc			= DRIVER_DESC,
	.major			= DRIVER_MAJOR,
	.minor			= DRIVER_MINOR,
	.driver_features = DRIVER_ATOMIC | DRIVER_GEM | DRIVER_MODESET,
	.fops			= &st7920_fops,
};

static int st7920_init_modeset(struct st7920_device *st7920)
{
	struct drm_display_mode *mode = &st7920->mode;
	struct drm_device *drm = &st7920->drm;
	unsigned long max_width, max_height;
	struct drm_plane *primary_plane;
	struct drm_crtc *crtc;
	struct drm_encoder *encoder;
	struct drm_connector *connector;
	int ret;

	/*
	 * Modesetting
	 */

	ret = drmm_mode_config_init(drm);
	if (ret) {
		drm_err(drm, "DRM mode config init failed: %d\n", ret);
		return ret;
	}

	mode->type = DRM_MODE_TYPE_DRIVER;
	mode->clock = 30;
	mode->hdisplay = st7920->width;
	mode->htotal = st7920->width;
	mode->hsync_start = st7920->width;
	mode->hsync_end = st7920->width;
	mode->vdisplay = st7920->height;
	mode->vtotal = st7920->height;
	mode->vsync_start = st7920->height;
	mode->vsync_end = st7920->height;
	mode->width_mm = 27;
	mode->height_mm = 27;

	max_width = max_t(unsigned long, mode->hdisplay, DRM_SHADOW_PLANE_MAX_WIDTH);
	max_height = max_t(unsigned long, mode->vdisplay, DRM_SHADOW_PLANE_MAX_HEIGHT);

	drm->mode_config.min_width = mode->hdisplay;
	drm->mode_config.max_width = max_width;
	drm->mode_config.min_height = mode->vdisplay;
	drm->mode_config.max_height = max_height;
	drm->mode_config.preferred_depth = 24;
	drm->mode_config.funcs = &st7920_mode_config_funcs;

	/* Primary plane */

	primary_plane = &st7920->primary_plane;
	ret = drm_universal_plane_init(drm, primary_plane, 0, &st7920_primary_plane_funcs,
				       st7920_formats, ARRAY_SIZE(st7920_formats),
				       NULL, DRM_PLANE_TYPE_PRIMARY, NULL);
	if (ret) {
		drm_err(drm, "DRM primary plane init failed: %d\n", ret);
		return ret;
	}

	drm_plane_helper_add(primary_plane, &st7920_primary_plane_helper_funcs);

	drm_plane_enable_fb_damage_clips(primary_plane);

	/* CRTC */

	crtc = &st7920->crtc;
	ret = drm_crtc_init_with_planes(drm, crtc, primary_plane, NULL,
					&st7920_crtc_funcs, NULL);
	if (ret) {
		drm_err(drm, "DRM crtc init failed: %d\n", ret);
		return ret;
	}

	drm_crtc_helper_add(crtc, &st7920_crtc_helper_funcs);

	/* Encoder */

	encoder = &st7920->encoder;
	ret = drm_encoder_init(drm, encoder, &st7920_encoder_funcs,
			       DRM_MODE_ENCODER_NONE, NULL);
	if (ret) {
		drm_err(drm, "DRM encoder init failed: %d\n", ret);
		return ret;
	}

	encoder->possible_crtcs = drm_crtc_mask(crtc);

	/* Connector */

	connector = &st7920->connector;
	ret = drm_connector_init(drm, connector, &st7920_connector_funcs,
				 DRM_MODE_CONNECTOR_Unknown);
	if (ret) {
		drm_err(drm, "DRM connector init failed: %d\n", ret);
		return ret;
	}

	drm_connector_helper_add(connector, &st7920_connector_helper_funcs);

	ret = drm_connector_attach_encoder(connector, encoder);
	if (ret) {
		drm_err(drm, "DRM attach connector to encoder failed: %d\n", ret);
		return ret;
	}

	drm_mode_config_reset(drm);

	return 0;
}

static int st7920_probe(struct spi_device *spi)
{
	struct st7920_device *st7920;
	struct regmap *regmap;
	struct device *dev = &spi->dev;
	struct drm_device *drm;
	int ret;

	regmap = devm_regmap_init_spi(spi, &st7920_spi_regmap_config);
	if (IS_ERR(regmap))
		return PTR_ERR(regmap);

	st7920 = devm_drm_dev_alloc(dev, &st7920_drm_driver,
				    struct st7920_device, drm);
	if (IS_ERR(st7920))
		return PTR_ERR(st7920);

	drm = &st7920->drm;

	st7920->drm.dev = dev;
	st7920->regmap = regmap;
	st7920->spi = spi;
	st7920->width = ST7920_DEFAULT_WIDTH;
	st7920->height = ST7920_DEFAULT_HEIGHT;

	st7920->reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_LOW);
	if (IS_ERR(st7920->reset_gpio)) {
		ret = PTR_ERR(st7920->reset_gpio);
		return dev_err_probe(dev, ret, "Unable to retrieve reset GPIO\n");
	}

	spi_set_drvdata(spi, st7920);

	ret = st7920_init_modeset(st7920);
	if (ret)
		return ret;

	ret = drm_dev_register(drm, 0);
	if (ret)
		return dev_err_probe(dev, ret, "DRM device register failed\n");

	drm_client_setup(drm, NULL);

	return 0;
}

static void st7920_remove(struct spi_device *spi)
{
	struct st7920_device *st7920 = spi_get_drvdata(spi);

	drm_dev_unplug(&st7920->drm);
	drm_atomic_helper_shutdown(&st7920->drm);
}

static void st7920_shutdown(struct spi_device *spi)
{
	struct st7920_device *st7920 = spi_get_drvdata(spi);

	drm_atomic_helper_shutdown(&st7920->drm);
}

static struct spi_driver st7920_spi_driver = {
	.driver = {
		.name = DRIVER_NAME,
		.of_match_table = st7920_of_match,
	},
	.id_table = st7920_spi_id,
	.probe = st7920_probe,
	.remove = st7920_remove,
	.shutdown = st7920_shutdown,
};
module_spi_driver(st7920_spi_driver);

MODULE_DESCRIPTION(DRIVER_DESC);
MODULE_AUTHOR("Iker Pedrosa <ipedrosam@gmail.com>");
MODULE_LICENSE("GPL");