@samitouri / QOSamiQemu / commits / 5ef60a3787

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 */