hw/display: Add i.MX6UL LCDIF device model
Implement a basic i.MX6UL LCDIF controller model with MMIO registers, frame-done interrupt behavior, and framebuffer-backed display updates for RGB565 and XRGB8888 input formats. Place the LCDIF device under hw/display and build it via a dedicated CONFIG_IMX6UL_LCDIF symbol. Model register fields with registerfields.h helpers and provide migration support via vmstate. Signed-off-by: Yucai Liu <1486344514@qq.com> Message-id: 20260412110240.93116-2-yangyanglan718@gmail.com Reviewed-by: Peter Maydell <peter.maydell@linaro.org> Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
Yucai Liu committed
Apr 12, 2026 at 19:02 UTC
5ef60a3787e8e0dccf30dae3704d4f5249160e7d
5 files changed
+497
MAINTAINERS
+2
@@ -894,8 +894,10 @@ L: qemu-arm@nongnu.org
894
S: Odd Fixes
895
F: hw/arm/mcimx6ul-evk.c
896
F: hw/arm/fsl-imx6ul.c
897
+F: hw/display/imx6ul_lcdif.c
898
F: hw/misc/imx6ul_ccm.c
899
F: include/hw/arm/fsl-imx6ul.h
900
+F: include/hw/display/imx6ul_lcdif.h
901
F: include/hw/misc/imx6ul_ccm.h
902
F: docs/system/arm/mcimx6ul-evk.rst
903
hw/display/Kconfig
+4
@@ -25,6 +25,10 @@ config PL110
25
bool
26
select FRAMEBUFFER
27
28
+config IMX6UL_LCDIF
29
+ bool
30
+ select FRAMEBUFFER
31
+
32
config SII9022
33
bool
34
depends on I2C
hw/display/imx6ul_lcdif.c
new
+453
@@ -0,0 +1,453 @@
1
+/*
2
+ * SPDX-License-Identifier: GPL-2.0-or-later
3
+ *
4
+ * i.MX6UL LCDIF controller
5
+ *
6
+ * Copyright (c) 2026 Yucai Liu <1486344514@qq.com>
7
+ */
8
+
9
+#include "qemu/osdep.h"
10
+#include "hw/display/imx6ul_lcdif.h"
11
+#include "hw/core/irq.h"
12
+#include "hw/core/registerfields.h"
13
+#include "hw/display/framebuffer.h"
14
+#include "migration/vmstate.h"
15
+#include "system/address-spaces.h"
16
+#include "qemu/module.h"
17
+#include "qemu/units.h"
18
+#include "ui/pixel_ops.h"
19
+
20
+#define LCDIF_MMIO_SIZE (16 * KiB)
21
+#define LCDIF_RESET_CTRL1 0x000f0000
22
+
23
+REG32(CTRL, 0x00)
24
+ FIELD(CTRL, RUN, 0, 1)
25
+ FIELD(CTRL, WORD_LENGTH, 8, 2)
26
+REG32(CTRL1, 0x10)
27
+ FIELD(CTRL1, CUR_FRAME_DONE_IRQ, 9, 1)
28
+ FIELD(CTRL1, CUR_FRAME_DONE_IRQ_EN, 13, 1)
29
+ FIELD(CTRL1, BYTE_PACKING_FORMAT, 16, 4)
30
+REG32(V4_TRANSFER_COUNT, 0x30)
31
+ FIELD(V4_TRANSFER_COUNT, H_COUNT, 0, 16)
32
+ FIELD(V4_TRANSFER_COUNT, V_COUNT, 16, 16)
33
+REG32(V4_CUR_BUF, 0x40)
34
+REG32(V4_NEXT_BUF, 0x50)
35
+REG32(AS_NEXT_BUF, 0x230)
36
+
37
+#define REG_SET 0x4
38
+#define REG_CLR 0x8
39
+#define REG_TOG 0xc
40
+
41
+#define CTRL_WORD_LENGTH_16 0
42
+#define CTRL_WORD_LENGTH_24 3
43
+
44
+#define FRAME_PERIOD_NS (16 * 1000 * 1000ULL)
45
+
46
+enum IMX6ULLCDIFReg {
47
+ IMX6UL_LCDIF_REG_CTRL = A_CTRL >> 4,
48
+ IMX6UL_LCDIF_REG_CTRL1 = A_CTRL1 >> 4,
49
+ IMX6UL_LCDIF_REG_V4_TRANSFER_COUNT = A_V4_TRANSFER_COUNT >> 4,
50
+ IMX6UL_LCDIF_REG_V4_CUR_BUF = A_V4_CUR_BUF >> 4,
51
+ IMX6UL_LCDIF_REG_V4_NEXT_BUF = A_V4_NEXT_BUF >> 4,
52
+ IMX6UL_LCDIF_REG_AS_NEXT_BUF = A_AS_NEXT_BUF >> 4,
53
+};
54
+
55
+static inline bool imx6ul_lcdif_reg_exists(hwaddr reg)
56
+{
57
+ return (reg >> 4) < IMX6UL_LCDIF_REGS_NUM;
58
+}
59
+
60
+static inline bool imx6ul_lcdif_reg_has_setclr(hwaddr reg)
61
+{
62
+ switch (reg) {
63
+ case A_CTRL:
64
+ case A_CTRL1:
65
+ return true;
66
+ default:
67
+ return false;
68
+ }
69
+}
70
+
71
+static inline bool imx6ul_lcdif_is_running(IMX6ULLCDIFState *s)
72
+{
73
+ uint32_t ctrl = s->regs[IMX6UL_LCDIF_REG_CTRL];
74
+
75
+ return FIELD_EX32(ctrl, CTRL, RUN);
76
+}
77
+
78
+static inline bool imx6ul_lcdif_frame_done_pending(IMX6ULLCDIFState *s)
79
+{
80
+ uint32_t ctrl1 = s->regs[IMX6UL_LCDIF_REG_CTRL1];
81
+
82
+ return FIELD_EX32(ctrl1, CTRL1, CUR_FRAME_DONE_IRQ);
83
+}
84
+
85
+static void imx6ul_lcdif_schedule_frame(IMX6ULLCDIFState *s)
86
+{
87
+ int64_t now = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
88
+
89
+ timer_mod(s->frame_timer, now + FRAME_PERIOD_NS);
90
+}
91
+
92
+static void imx6ul_lcdif_maybe_schedule_frame(IMX6ULLCDIFState *s)
93
+{
94
+ if (imx6ul_lcdif_is_running(s) && !imx6ul_lcdif_frame_done_pending(s)) {
95
+ imx6ul_lcdif_schedule_frame(s);
96
+ } else {
97
+ timer_del(s->frame_timer);
98
+ }
99
+}
100
+
101
+static void imx6ul_lcdif_update_irq(IMX6ULLCDIFState *s)
102
+{
103
+ uint32_t ctrl1 = s->regs[IMX6UL_LCDIF_REG_CTRL1];
104
+ bool level = FIELD_EX32(ctrl1, CTRL1, CUR_FRAME_DONE_IRQ_EN) &&
105
+ FIELD_EX32(ctrl1, CTRL1, CUR_FRAME_DONE_IRQ);
106
+
107
+ qemu_set_irq(s->irq, level);
108
+}
109
+
110
+static void imx6ul_lcdif_frame_done(IMX6ULLCDIFState *s)
111
+{
112
+ uint32_t ctrl1 = s->regs[IMX6UL_LCDIF_REG_CTRL1];
113
+
114
+ ctrl1 = FIELD_DP32(ctrl1, CTRL1, CUR_FRAME_DONE_IRQ, 1);
115
+ s->regs[IMX6UL_LCDIF_REG_CTRL1] = ctrl1;
116
+ imx6ul_lcdif_update_irq(s);
117
+}
118
+
119
+static void imx6ul_lcdif_draw_line_rgb565(void *opaque, uint8_t *dst,
120
+ const uint8_t *src, int width,
121
+ int dststep)
122
+{
123
+ uint32_t *dst32 = (uint32_t *)dst;
124
+ int i;
125
+
126
+ for (i = 0; i < width; i++) {
127
+ uint16_t pixel = lduw_le_p(src);
128
+ uint8_t r = ((pixel >> 11) & 0x1f) << 3;
129
+ uint8_t g = ((pixel >> 5) & 0x3f) << 2;
130
+ uint8_t b = (pixel & 0x1f) << 3;
131
+
132
+ *dst32++ = rgb_to_pixel32(r, g, b);
133
+ src += 2;
134
+ }
135
+}
136
+
137
+static void imx6ul_lcdif_draw_line_xrgb8888(void *opaque, uint8_t *dst,
138
+ const uint8_t *src, int width,
139
+ int dststep)
140
+{
141
+ uint32_t *dst32 = (uint32_t *)dst;
142
+ int i;
143
+
144
+ for (i = 0; i < width; i++) {
145
+ uint32_t pixel = ldl_le_p(src);
146
+ uint8_t r = (pixel >> 16) & 0xff;
147
+ uint8_t g = (pixel >> 8) & 0xff;
148
+ uint8_t b = pixel & 0xff;
149
+
150
+ *dst32++ = rgb_to_pixel32(r, g, b);
151
+ src += 4;
152
+ }
153
+}
154
+
155
+static void imx6ul_lcdif_update_display(void *opaque)
156
+{
157
+ IMX6ULLCDIFState *s = opaque;
158
+ DisplaySurface *surface = qemu_console_surface(s->con);
159
+ uint32_t transfer_count = s->regs[IMX6UL_LCDIF_REG_V4_TRANSFER_COUNT];
160
+ uint32_t width = FIELD_EX32(transfer_count, V4_TRANSFER_COUNT, H_COUNT);
161
+ uint32_t height = FIELD_EX32(transfer_count, V4_TRANSFER_COUNT, V_COUNT);
162
+ uint32_t ctrl = s->regs[IMX6UL_LCDIF_REG_CTRL];
163
+ uint32_t frame_base = s->regs[IMX6UL_LCDIF_REG_V4_CUR_BUF];
164
+ drawfn fn;
165
+ int first = 0;
166
+ int last = 0;
167
+ int src_width;
168
+
169
+ if (!imx6ul_lcdif_is_running(s) || width == 0 || height == 0) {
170
+ return;
171
+ }
172
+
173
+ switch (FIELD_EX32(ctrl, CTRL, WORD_LENGTH)) {
174
+ case CTRL_WORD_LENGTH_16:
175
+ s->src_bpp = 2;
176
+ fn = imx6ul_lcdif_draw_line_rgb565;
177
+ break;
178
+ case CTRL_WORD_LENGTH_24:
179
+ s->src_bpp = 4;
180
+ fn = imx6ul_lcdif_draw_line_xrgb8888;
181
+ break;
182
+ default:
183
+ return;
184
+ }
185
+
186
+ if (surface_width(surface) != width || surface_height(surface) != height) {
187
+ qemu_console_resize(s->con, width, height);
188
+ surface = qemu_console_surface(s->con);
189
+ s->invalidate = true;
190
+ }
191
+
192
+ src_width = width * s->src_bpp;
193
+ if (s->invalidate || s->fb_base != frame_base ||
194
+ s->src_width != src_width || s->rows != height) {
195
+ framebuffer_update_memory_section(&s->fbsection, get_system_memory(),
196
+ frame_base, height, src_width);
197
+ s->fb_base = frame_base;
198
+ s->src_width = src_width;
199
+ s->rows = height;
200
+ }
201
+
202
+ framebuffer_update_display(surface, &s->fbsection, width, height,
203
+ src_width, surface_stride(surface), 0,
204
+ s->invalidate, fn, s, &first, &last);
205
+ if (first >= 0) {
206
+ dpy_gfx_update(s->con, 0, first, width, last - first + 1);
207
+ }
208
+
209
+ s->invalidate = false;
210
+}
211
+
212
+static void imx6ul_lcdif_invalidate_display(void *opaque)
213
+{
214
+ IMX6ULLCDIFState *s = opaque;
215
+
216
+ s->invalidate = true;
217
+}
218
+
219
+static const GraphicHwOps imx6ul_lcdif_graphic_ops = {
220
+ .invalidate = imx6ul_lcdif_invalidate_display,
221
+ .gfx_update = imx6ul_lcdif_update_display,
222
+};
223
+
224
+static void imx6ul_lcdif_frame_timer_cb(void *opaque)
225
+{
226
+ IMX6ULLCDIFState *s = opaque;
227
+
228
+ if (!imx6ul_lcdif_is_running(s) || imx6ul_lcdif_frame_done_pending(s)) {
229
+ return;
230
+ }
231
+
232
+ imx6ul_lcdif_frame_done(s);
233
+}
234
+
235
+static uint64_t imx6ul_lcdif_read(void *opaque, hwaddr offset, unsigned size)
236
+{
237
+ IMX6ULLCDIFState *s = opaque;
238
+ hwaddr reg = offset & ~0xf;
239
+ uint32_t idx;
240
+
241
+ assert(size == 4);
242
+ assert(!(offset & 0x3));
243
+ assert(offset < LCDIF_MMIO_SIZE);
244
+
245
+ idx = reg >> 4;
246
+ if (idx >= ARRAY_SIZE(s->regs)) {
247
+ return 0;
248
+ }
249
+
250
+ return s->regs[idx];
251
+}
252
+
253
+static void imx6ul_lcdif_write(void *opaque, hwaddr offset,
254
+ uint64_t value, unsigned size)
255
+{
256
+ IMX6ULLCDIFState *s = opaque;
257
+ hwaddr reg = offset & ~0xf;
258
+ uint32_t idx;
259
+ uint32_t oldv;
260
+
261
+ assert(size == 4);
262
+ assert(!(offset & 0x3));
263
+ assert(offset < LCDIF_MMIO_SIZE);
264
+
265
+ if (!imx6ul_lcdif_reg_exists(reg)) {
266
+ return;
267
+ }
268
+
269
+ idx = reg >> 4;
270
+ oldv = s->regs[idx];
271
+
272
+ switch (offset & 0xf) {
273
+ case 0:
274
+ s->regs[idx] = (uint32_t)value;
275
+ break;
276
+ case REG_SET:
277
+ if (!imx6ul_lcdif_reg_has_setclr(reg)) {
278
+ return;
279
+ }
280
+ s->regs[idx] = oldv | (uint32_t)value;
281
+ break;
282
+ case REG_CLR:
283
+ if (!imx6ul_lcdif_reg_has_setclr(reg)) {
284
+ return;
285
+ }
286
+ s->regs[idx] = oldv & ~(uint32_t)value;
287
+ break;
288
+ case REG_TOG:
289
+ if (!imx6ul_lcdif_reg_has_setclr(reg)) {
290
+ return;
291
+ }
292
+ s->regs[idx] = oldv ^ (uint32_t)value;
293
+ break;
294
+ default:
295
+ g_assert_not_reached();
296
+ }
297
+
298
+ switch (reg) {
299
+ case A_CTRL:
300
+ if (!FIELD_EX32(oldv, CTRL, RUN) &&
301
+ FIELD_EX32(s->regs[idx], CTRL, RUN)) {
302
+ s->invalidate = true;
303
+ graphic_hw_invalidate(s->con);
304
+ imx6ul_lcdif_maybe_schedule_frame(s);
305
+ break;
306
+ }
307
+ if (FIELD_EX32(oldv, CTRL, RUN) &&
308
+ !FIELD_EX32(s->regs[idx], CTRL, RUN)) {
309
+ timer_del(s->frame_timer);
310
+ }
311
+ break;
312
+ case A_CTRL1:
313
+ if (FIELD_EX32(oldv, CTRL1, CUR_FRAME_DONE_IRQ) &&
314
+ !FIELD_EX32(s->regs[idx], CTRL1, CUR_FRAME_DONE_IRQ)) {
315
+ imx6ul_lcdif_maybe_schedule_frame(s);
316
+ }
317
+ break;
318
+ case A_V4_TRANSFER_COUNT:
319
+ s->invalidate = true;
320
+ graphic_hw_invalidate(s->con);
321
+ break;
322
+ case A_V4_CUR_BUF:
323
+ s->invalidate = true;
324
+ graphic_hw_invalidate(s->con);
325
+ break;
326
+ case A_V4_NEXT_BUF:
327
+ s->regs[IMX6UL_LCDIF_REG_V4_CUR_BUF] = s->regs[idx];
328
+ imx6ul_lcdif_frame_done(s);
329
+ s->invalidate = true;
330
+ graphic_hw_invalidate(s->con);
331
+ imx6ul_lcdif_maybe_schedule_frame(s);
332
+ return;
333
+ case A_AS_NEXT_BUF:
334
+ imx6ul_lcdif_frame_done(s);
335
+ imx6ul_lcdif_maybe_schedule_frame(s);
336
+ return;
337
+ default:
338
+ break;
339
+ }
340
+
341
+ imx6ul_lcdif_update_irq(s);
342
+}
343
+
344
+static const MemoryRegionOps imx6ul_lcdif_ops = {
345
+ .read = imx6ul_lcdif_read,
346
+ .write = imx6ul_lcdif_write,
347
+ .endianness = DEVICE_LITTLE_ENDIAN,
348
+ .valid = {
349
+ .min_access_size = 4,
350
+ .max_access_size = 4,
351
+ .unaligned = false,
352
+ },
353
+};
354
+
355
+static void imx6ul_lcdif_reset(DeviceState *dev)
356
+{
357
+ IMX6ULLCDIFState *s = IMX6UL_LCDIF(dev);
358
+
359
+ memset(s->regs, 0, sizeof(s->regs));
360
+ s->regs[IMX6UL_LCDIF_REG_CTRL1] = LCDIF_RESET_CTRL1;
361
+ s->fb_base = 0;
362
+ s->src_width = 0;
363
+ s->rows = 0;
364
+ s->src_bpp = 0;
365
+ s->invalidate = true;
366
+ timer_del(s->frame_timer);
367
+ imx6ul_lcdif_update_irq(s);
368
+}
369
+
370
+static int imx6ul_lcdif_post_load(void *opaque, int version_id)
371
+{
372
+ IMX6ULLCDIFState *s = opaque;
373
+
374
+ s->fb_base = 0;
375
+ s->src_width = 0;
376
+ s->rows = 0;
377
+ s->src_bpp = 0;
378
+ s->invalidate = true;
379
+
380
+ imx6ul_lcdif_update_irq(s);
381
+ if (imx6ul_lcdif_is_running(s) &&
382
+ !imx6ul_lcdif_frame_done_pending(s) &&
383
+ !timer_pending(s->frame_timer)) {
384
+ imx6ul_lcdif_schedule_frame(s);
385
+ }
386
+
387
+ return 0;
388
+}
389
+
390
+static const VMStateDescription vmstate_imx6ul_lcdif = {
391
+ .name = TYPE_IMX6UL_LCDIF,
392
+ .version_id = 1,
393
+ .minimum_version_id = 1,
394
+ .post_load = imx6ul_lcdif_post_load,
395
+ .fields = (const VMStateField[]) {
396
+ VMSTATE_UINT32_ARRAY(regs, IMX6ULLCDIFState, IMX6UL_LCDIF_REGS_NUM),
397
+ VMSTATE_TIMER_PTR(frame_timer, IMX6ULLCDIFState),
398
+ VMSTATE_END_OF_LIST()
399
+ },
400
+};
401
+
402
+static void imx6ul_lcdif_realize(DeviceState *dev, Error **errp)
403
+{
404
+ IMX6ULLCDIFState *s = IMX6UL_LCDIF(dev);
405
+
406
+ s->frame_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL,
407
+ imx6ul_lcdif_frame_timer_cb, s);
408
+ s->invalidate = true;
409
+ memory_region_init_io(&s->iomem, OBJECT(dev), &imx6ul_lcdif_ops, s,
410
+ TYPE_IMX6UL_LCDIF, LCDIF_MMIO_SIZE);
411
+ sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->iomem);
412
+ sysbus_init_irq(SYS_BUS_DEVICE(dev), &s->irq);
413
+ s->con = graphic_console_init(dev, 0, &imx6ul_lcdif_graphic_ops, s);
414
+}
415
+
416
+static void imx6ul_lcdif_unrealize(DeviceState *dev)
417
+{
418
+ IMX6ULLCDIFState *s = IMX6UL_LCDIF(dev);
419
+
420
+ timer_del(s->frame_timer);
421
+ timer_free(s->frame_timer);
422
+ s->frame_timer = NULL;
423
+
424
+ if (s->con) {
425
+ graphic_console_close(s->con);
426
+ s->con = NULL;
427
+ }
428
+}
429
+
430
+static void imx6ul_lcdif_class_init(ObjectClass *klass, const void *data)
431
+{
432
+ DeviceClass *dc = DEVICE_CLASS(klass);
433
+
434
+ dc->realize = imx6ul_lcdif_realize;
435
+ dc->unrealize = imx6ul_lcdif_unrealize;
436
+ dc->vmsd = &vmstate_imx6ul_lcdif;
437
+ device_class_set_legacy_reset(dc, imx6ul_lcdif_reset);
438
+ dc->desc = "i.MX6UL LCDIF";
439
+}
440
+
441
+static const TypeInfo imx6ul_lcdif_info = {
442
+ .name = TYPE_IMX6UL_LCDIF,
443
+ .parent = TYPE_SYS_BUS_DEVICE,
444
+ .instance_size = sizeof(IMX6ULLCDIFState),
445
+ .class_init = imx6ul_lcdif_class_init,
446
+};
447
+
448
+static void imx6ul_lcdif_register_types(void)
449
+{
450
+ type_register_static(&imx6ul_lcdif_info);
451
+}
452
+
453
+type_init(imx6ul_lcdif_register_types)
hw/display/meson.build
+1
@@ -12,6 +12,7 @@ system_ss.add(when: ['CONFIG_VGA_CIRRUS', 'CONFIG_VGA_ISA'], if_true: files('cir
12
system_ss.add(when: 'CONFIG_G364FB', if_true: files('g364fb.c'))
13
system_ss.add(when: 'CONFIG_JAZZ_LED', if_true: files('jazz_led.c'))
14
system_ss.add(when: 'CONFIG_PL110', if_true: files('pl110.c'))
15
+system_ss.add(when: 'CONFIG_IMX6UL_LCDIF', if_true: files('imx6ul_lcdif.c'))
16
system_ss.add(when: 'CONFIG_SII9022', if_true: files('sii9022.c'))
17
system_ss.add(when: 'CONFIG_SSD0303', if_true: files('ssd0303.c'))
18
system_ss.add(when: 'CONFIG_SSD0323', if_true: files('ssd0323.c'))
include/hw/display/imx6ul_lcdif.h
new
+37
@@ -0,0 +1,37 @@
1
+/*
2
+ * SPDX-License-Identifier: GPL-2.0-or-later
3
+ *
4
+ * i.MX6UL LCDIF controller
5
+ *
6
+ * Copyright (c) 2026 Yucai Liu <1486344514@qq.com>
7
+ */
8
+
9
+#ifndef IMX6UL_LCDIF_H
10
+#define IMX6UL_LCDIF_H
11
+
12
+#include "hw/core/sysbus.h"
13
+#include "qom/object.h"
14
+#include "qemu/timer.h"
15
+#include "ui/console.h"
16
+
17
+#define TYPE_IMX6UL_LCDIF "imx6ul-lcdif"
18
+#define IMX6UL_LCDIF_REGS_NUM ((0x230 >> 4) + 1)
19
+OBJECT_DECLARE_SIMPLE_TYPE(IMX6ULLCDIFState, IMX6UL_LCDIF)
20
+
21
+struct IMX6ULLCDIFState {
22
+ SysBusDevice parent_obj;
23
+
24
+ MemoryRegion iomem;
25
+ MemoryRegionSection fbsection;
26
+ qemu_irq irq;
27
+ QemuConsole *con;
28
+ QEMUTimer *frame_timer;
29
+ uint32_t fb_base;
30
+ uint32_t src_width;
31
+ uint32_t rows;
32
+ uint8_t src_bpp;
33
+ bool invalidate;
34
+ uint32_t regs[IMX6UL_LCDIF_REGS_NUM];
35
+};
36
+
37
+#endif /* IMX6UL_LCDIF_H */