Add a basic i.MX6UL LCDIF device model, hook it into the i.MX6UL SoC, and replace the previous unimplemented LCDIF stub. The model exposes MMIO registers, frame-done IRQ behavior, and framebuffer-backed display updates for RGB565/XRGB8888 formats.
Signed-off-by: Yucai Liu <[email protected]> --- MAINTAINERS | 2 + hw/arm/fsl-imx6ul.c | 9 +- hw/arm/imx6ul_lcdif.c | 409 ++++++++++++++++++++++++++++++++++ hw/arm/meson.build | 5 +- include/hw/arm/fsl-imx6ul.h | 2 +- include/hw/arm/imx6ul_lcdif.h | 37 +++ 6 files changed, 460 insertions(+), 4 deletions(-) create mode 100644 hw/arm/imx6ul_lcdif.c create mode 100644 include/hw/arm/imx6ul_lcdif.h diff --git a/MAINTAINERS b/MAINTAINERS index 4918f41ec4..11c16ff387 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -895,8 +895,10 @@ L: [email protected] S: Odd Fixes F: hw/arm/mcimx6ul-evk.c F: hw/arm/fsl-imx6ul.c +F: hw/arm/imx6ul_lcdif.c F: hw/misc/imx6ul_ccm.c F: include/hw/arm/fsl-imx6ul.h +F: include/hw/arm/imx6ul_lcdif.h F: include/hw/misc/imx6ul_ccm.h F: docs/system/arm/mcimx6ul-evk.rst diff --git a/hw/arm/fsl-imx6ul.c b/hw/arm/fsl-imx6ul.c index 225e179126..419dba5b56 100644 --- a/hw/arm/fsl-imx6ul.c +++ b/hw/arm/fsl-imx6ul.c @@ -19,6 +19,7 @@ #include "qemu/osdep.h" #include "qapi/error.h" #include "hw/arm/fsl-imx6ul.h" +#include "hw/arm/imx6ul_lcdif.h" #include "hw/misc/unimp.h" #include "hw/usb/imx-usb-phy.h" #include "hw/core/boards.h" @@ -163,6 +164,7 @@ static void fsl_imx6ul_realize(DeviceState *dev, Error **errp) DeviceState *gic; SysBusDevice *gicsbd; DeviceState *cpu; + DeviceState *lcdif; if (ms->smp.cpus > 1) { error_setg(errp, "%s: Only a single CPU is supported (%d requested)", @@ -656,8 +658,11 @@ static void fsl_imx6ul_realize(DeviceState *dev, Error **errp) /* * LCD */ - create_unimplemented_device("lcdif", FSL_IMX6UL_LCDIF_ADDR, - FSL_IMX6UL_LCDIF_SIZE); + lcdif = qdev_new(TYPE_IMX6UL_LCDIF); + sysbus_realize_and_unref(SYS_BUS_DEVICE(lcdif), &error_abort); + sysbus_mmio_map(SYS_BUS_DEVICE(lcdif), 0, FSL_IMX6UL_LCDIF_ADDR); + sysbus_connect_irq(SYS_BUS_DEVICE(lcdif), 0, + qdev_get_gpio_in(gic, FSL_IMX6UL_LCDIF_IRQ)); /* * CSU diff --git a/hw/arm/imx6ul_lcdif.c b/hw/arm/imx6ul_lcdif.c new file mode 100644 index 0000000000..e2e42fbc26 --- /dev/null +++ b/hw/arm/imx6ul_lcdif.c @@ -0,0 +1,409 @@ +/* + * SPDX-License-Identifier: GPL-2.0-or-later + * + * i.MX6UL LCDIF controller + * + * Copyright (c) 2026 + */ + +#include "qemu/osdep.h" +#include "hw/arm/imx6ul_lcdif.h" +#include "hw/core/irq.h" +#include "hw/display/framebuffer.h" +#include "system/address-spaces.h" +#include "qemu/bitops.h" +#include "qemu/log.h" +#include "qemu/module.h" +#include "ui/pixel_ops.h" + +#define LCDIF_MMIO_SIZE 0x1000 + +#define LCDC_CTRL 0x00 +#define LCDC_CTRL1 0x10 +#define LCDC_V4_TRANSFER_COUNT 0x30 +#define LCDC_V4_CUR_BUF 0x40 +#define LCDC_V4_NEXT_BUF 0x50 +#define LCDC_AS_NEXT_BUF 0x230 + +#define REG_SET 0x4 +#define REG_CLR 0x8 +#define REG_TOG 0xc + +#define CTRL_RUN BIT(0) +#define CTRL_WORD_LENGTH_MASK (0x3 << 8) +#define CTRL_WORD_LENGTH_16 (0x0 << 8) +#define CTRL_WORD_LENGTH_24 (0x3 << 8) +#define CTRL1_CUR_FRAME_DONE_IRQ_EN BIT(13) +#define CTRL1_CUR_FRAME_DONE_IRQ BIT(9) +#define FRAME_PERIOD_NS (16 * 1000 * 1000ULL) + +static inline uint32_t imx6ul_lcdif_reg_idx(hwaddr offset) +{ + return (offset & ~0xf) >> 2; +} + +static inline uint32_t imx6ul_lcdif_reg_read(IMX6ULLCDIFState *s, hwaddr offset) +{ + return s->regs[imx6ul_lcdif_reg_idx(offset)]; +} + +static inline void imx6ul_lcdif_reg_write(IMX6ULLCDIFState *s, hwaddr offset, + uint32_t value) +{ + s->regs[imx6ul_lcdif_reg_idx(offset)] = value; +} + +static inline bool imx6ul_lcdif_is_running(IMX6ULLCDIFState *s) +{ + return imx6ul_lcdif_reg_read(s, LCDC_CTRL) & CTRL_RUN; +} + +static void imx6ul_lcdif_schedule_frame(IMX6ULLCDIFState *s) +{ + int64_t now = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL); + + timer_mod(s->frame_timer, now + FRAME_PERIOD_NS); +} + +static void imx6ul_lcdif_update_irq(IMX6ULLCDIFState *s) +{ + uint32_t ctrl1 = imx6ul_lcdif_reg_read(s, LCDC_CTRL1); + bool level = (ctrl1 & CTRL1_CUR_FRAME_DONE_IRQ_EN) && + (ctrl1 & CTRL1_CUR_FRAME_DONE_IRQ); + + qemu_set_irq(s->irq, level); +} + +static void imx6ul_lcdif_frame_done(IMX6ULLCDIFState *s) +{ + uint32_t ctrl1 = imx6ul_lcdif_reg_read(s, LCDC_CTRL1); + + imx6ul_lcdif_reg_write(s, LCDC_CTRL1, ctrl1 | CTRL1_CUR_FRAME_DONE_IRQ); + imx6ul_lcdif_update_irq(s); +} + +static void imx6ul_lcdif_write_pixel(uint8_t **dst, uint8_t r, + uint8_t g, uint8_t b, int dst_bpp) +{ + uint32_t rgb888; + + switch (dst_bpp) { + case 8: + **dst = rgb_to_pixel8(r, g, b); + *dst += 1; + break; + case 15: + *(uint16_t *)*dst = rgb_to_pixel15(r, g, b); + *dst += 2; + break; + case 16: + *(uint16_t *)*dst = rgb_to_pixel16(r, g, b); + *dst += 2; + break; + case 24: + rgb888 = rgb_to_pixel24(r, g, b); + (*dst)[0] = rgb888 & 0xff; + (*dst)[1] = (rgb888 >> 8) & 0xff; + (*dst)[2] = (rgb888 >> 16) & 0xff; + *dst += 3; + break; + case 32: + *(uint32_t *)*dst = rgb_to_pixel32(r, g, b); + *dst += 4; + break; + default: + break; + } +} + +static void imx6ul_lcdif_draw_line_rgb565(void *opaque, uint8_t *dst, + const uint8_t *src, int width, + int dststep) +{ + IMX6ULLCDIFState *s = opaque; + uint16_t pixel; + uint8_t r, g, b; + int i; + + for (i = 0; i < width; i++) { + pixel = lduw_le_p((void *)src); + r = ((pixel >> 11) & 0x1f) << 3; + g = ((pixel >> 5) & 0x3f) << 2; + b = (pixel & 0x1f) << 3; + imx6ul_lcdif_write_pixel(&dst, r, g, b, s->dst_bpp); + src += 2; + } +} + +static void imx6ul_lcdif_draw_line_xrgb8888(void *opaque, uint8_t *dst, + const uint8_t *src, int width, + int dststep) +{ + IMX6ULLCDIFState *s = opaque; + uint32_t pixel; + uint8_t r, g, b; + int i; + + for (i = 0; i < width; i++) { + pixel = ldl_le_p((void *)src); + r = (pixel >> 16) & 0xff; + g = (pixel >> 8) & 0xff; + b = pixel & 0xff; + imx6ul_lcdif_write_pixel(&dst, r, g, b, s->dst_bpp); + src += 4; + } +} + +static void imx6ul_lcdif_update_display(void *opaque) +{ + IMX6ULLCDIFState *s = opaque; + DisplaySurface *surface = qemu_console_surface(s->con); + uint32_t transfer_count = imx6ul_lcdif_reg_read(s, LCDC_V4_TRANSFER_COUNT); + uint32_t width = transfer_count & 0xffff; + uint32_t height = (transfer_count >> 16) & 0xffff; + uint32_t ctrl = imx6ul_lcdif_reg_read(s, LCDC_CTRL); + uint32_t frame_base = imx6ul_lcdif_reg_read(s, LCDC_V4_CUR_BUF); + drawfn fn; + int first = 0, last = 0; + int src_width; + + if (!imx6ul_lcdif_is_running(s) || width == 0 || height == 0) { + return; + } + + s->dst_bpp = surface_bits_per_pixel(surface); + if (s->dst_bpp == 0) { + return; + } + + switch (ctrl & CTRL_WORD_LENGTH_MASK) { + case CTRL_WORD_LENGTH_16: + s->src_bpp = 2; + fn = imx6ul_lcdif_draw_line_rgb565; + break; + case CTRL_WORD_LENGTH_24: + s->src_bpp = 4; + fn = imx6ul_lcdif_draw_line_xrgb8888; + break; + default: + return; + } + + if (surface_width(surface) != width || surface_height(surface) != height) { + qemu_console_resize(s->con, width, height); + surface = qemu_console_surface(s->con); + s->invalidate = true; + } + + src_width = width * s->src_bpp; + if (s->invalidate || s->fb_base != frame_base || + s->src_width != src_width || s->rows != height) { + framebuffer_update_memory_section(&s->fbsection, get_system_memory(), + frame_base, height, src_width); + s->fb_base = frame_base; + s->src_width = src_width; + s->rows = height; + } + + framebuffer_update_display(surface, &s->fbsection, width, height, + src_width, surface_stride(surface), 0, + s->invalidate, fn, s, &first, &last); + if (first >= 0) { + dpy_gfx_update(s->con, 0, first, width, last - first + 1); + } + + s->invalidate = false; +} + +static void imx6ul_lcdif_invalidate_display(void *opaque) +{ + IMX6ULLCDIFState *s = opaque; + + s->invalidate = true; +} + +static const GraphicHwOps imx6ul_lcdif_graphic_ops = { + .invalidate = imx6ul_lcdif_invalidate_display, + .gfx_update = imx6ul_lcdif_update_display, +}; + +static void imx6ul_lcdif_frame_timer_cb(void *opaque) +{ + IMX6ULLCDIFState *s = opaque; + + if (!imx6ul_lcdif_is_running(s)) { + return; + } + + imx6ul_lcdif_frame_done(s); + imx6ul_lcdif_schedule_frame(s); +} + +static uint64_t imx6ul_lcdif_read(void *opaque, hwaddr offset, unsigned size) +{ + IMX6ULLCDIFState *s = opaque; + uint32_t idx; + + if (size != 4 || (offset & 0x3) || offset >= LCDIF_MMIO_SIZE) { + qemu_log_mask(LOG_GUEST_ERROR, + "%s: bad read offset=0x%" HWADDR_PRIx " size=%u\n", + TYPE_IMX6UL_LCDIF, offset, size); + return 0; + } + + idx = imx6ul_lcdif_reg_idx(offset); + return s->regs[idx]; +} + +static void imx6ul_lcdif_write(void *opaque, hwaddr offset, + uint64_t value, unsigned size) +{ + IMX6ULLCDIFState *s = opaque; + hwaddr reg = offset & ~0xf; + uint32_t idx, oldv; + + if (size != 4 || (offset & 0x3) || offset >= LCDIF_MMIO_SIZE) { + qemu_log_mask(LOG_GUEST_ERROR, + "%s: bad write offset=0x%" HWADDR_PRIx " size=%u\n", + TYPE_IMX6UL_LCDIF, offset, size); + return; + } + + idx = imx6ul_lcdif_reg_idx(reg); + oldv = s->regs[idx]; + + switch (offset & 0xf) { + case REG_SET: + s->regs[idx] |= (uint32_t)value; + break; + case REG_CLR: + s->regs[idx] &= ~(uint32_t)value; + break; + case REG_TOG: + s->regs[idx] ^= (uint32_t)value; + break; + default: + s->regs[idx] = (uint32_t)value; + break; + } + + switch (reg) { + case LCDC_CTRL: + if (!(oldv & CTRL_RUN) && (s->regs[idx] & CTRL_RUN)) { + imx6ul_lcdif_frame_done(s); + imx6ul_lcdif_schedule_frame(s); + s->invalidate = true; + graphic_hw_invalidate(s->con); + return; + } + if ((oldv & CTRL_RUN) && !(s->regs[idx] & CTRL_RUN)) { + timer_del(s->frame_timer); + } + break; + case LCDC_V4_TRANSFER_COUNT: + s->invalidate = true; + graphic_hw_invalidate(s->con); + break; + case LCDC_V4_CUR_BUF: + s->invalidate = true; + graphic_hw_invalidate(s->con); + break; + case LCDC_V4_NEXT_BUF: + imx6ul_lcdif_reg_write(s, LCDC_V4_CUR_BUF, s->regs[idx]); + imx6ul_lcdif_frame_done(s); + if (imx6ul_lcdif_is_running(s)) { + imx6ul_lcdif_schedule_frame(s); + } + s->invalidate = true; + graphic_hw_invalidate(s->con); + return; + case LCDC_AS_NEXT_BUF: + imx6ul_lcdif_frame_done(s); + if (imx6ul_lcdif_is_running(s)) { + imx6ul_lcdif_schedule_frame(s); + } + return; + default: + break; + } + + imx6ul_lcdif_update_irq(s); +} + +static const MemoryRegionOps imx6ul_lcdif_ops = { + .read = imx6ul_lcdif_read, + .write = imx6ul_lcdif_write, + .endianness = DEVICE_LITTLE_ENDIAN, + .valid = { + .min_access_size = 4, + .max_access_size = 4, + .unaligned = false, + }, +}; + +static void imx6ul_lcdif_reset(DeviceState *dev) +{ + IMX6ULLCDIFState *s = IMX6UL_LCDIF(dev); + + memset(s->regs, 0, sizeof(s->regs)); + s->fb_base = 0; + s->src_width = 0; + s->rows = 0; + s->src_bpp = 0; + s->invalidate = true; + timer_del(s->frame_timer); + qemu_set_irq(s->irq, 0); +} + +static void imx6ul_lcdif_realize(DeviceState *dev, Error **errp) +{ + IMX6ULLCDIFState *s = IMX6UL_LCDIF(dev); + + s->frame_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, + imx6ul_lcdif_frame_timer_cb, s); + s->invalidate = true; + memory_region_init_io(&s->iomem, OBJECT(dev), &imx6ul_lcdif_ops, s, + TYPE_IMX6UL_LCDIF, LCDIF_MMIO_SIZE); + sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->iomem); + sysbus_init_irq(SYS_BUS_DEVICE(dev), &s->irq); + s->con = graphic_console_init(dev, 0, &imx6ul_lcdif_graphic_ops, s); +} + +static void imx6ul_lcdif_finalize(Object *obj) +{ + IMX6ULLCDIFState *s = IMX6UL_LCDIF(obj); + + if (s->fbsection.mr) { + memory_region_set_log(s->fbsection.mr, false, DIRTY_MEMORY_VGA); + memory_region_unref(s->fbsection.mr); + s->fbsection.mr = NULL; + } + if (s->con) { + graphic_console_close(s->con); + } + timer_free(s->frame_timer); +} + +static void imx6ul_lcdif_class_init(ObjectClass *klass, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + + dc->realize = imx6ul_lcdif_realize; + device_class_set_legacy_reset(dc, imx6ul_lcdif_reset); + dc->desc = "i.MX6UL LCDIF"; +} + +static const TypeInfo imx6ul_lcdif_info = { + .name = TYPE_IMX6UL_LCDIF, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(IMX6ULLCDIFState), + .instance_finalize = imx6ul_lcdif_finalize, + .class_init = imx6ul_lcdif_class_init, +}; + +static void imx6ul_lcdif_register_types(void) +{ + type_register_static(&imx6ul_lcdif_info); +} + +type_init(imx6ul_lcdif_register_types) diff --git a/hw/arm/meson.build b/hw/arm/meson.build index 47cdc51d13..afcaca55ba 100644 --- a/hw/arm/meson.build +++ b/hw/arm/meson.build @@ -87,7 +87,10 @@ arm_common_ss.add(when: 'CONFIG_FSL_IMX8MP', if_true: files('fsl-imx8mp.c')) arm_common_ss.add(when: 'CONFIG_FSL_IMX8MP_EVK', if_true: files('imx8mp-evk.c')) arm_ss.add(when: 'CONFIG_ARM_SMMUV3', if_true: files('smmuv3.c')) arm_ss.add(when: 'CONFIG_ARM_SMMUV3_ACCEL', if_true: files('smmuv3-accel.c')) -arm_common_ss.add(when: 'CONFIG_FSL_IMX6UL', if_true: files('fsl-imx6ul.c', 'mcimx6ul-evk.c')) +arm_common_ss.add(when: 'CONFIG_FSL_IMX6UL', + if_true: files('fsl-imx6ul.c', + 'imx6ul_lcdif.c', + 'mcimx6ul-evk.c')) arm_common_ss.add(when: 'CONFIG_NRF51_SOC', if_true: files('nrf51_soc.c')) arm_common_ss.add(when: 'CONFIG_XEN', if_true: files( 'xen-stubs.c', diff --git a/include/hw/arm/fsl-imx6ul.h b/include/hw/arm/fsl-imx6ul.h index 4e3209b25b..cc41c2dbab 100644 --- a/include/hw/arm/fsl-imx6ul.h +++ b/include/hw/arm/fsl-imx6ul.h @@ -143,7 +143,7 @@ enum FslIMX6ULMemoryMap { FSL_IMX6UL_PXP_SIZE = (16 * KiB), FSL_IMX6UL_LCDIF_ADDR = 0x021C8000, - FSL_IMX6UL_LCDIF_SIZE = 0x100, + FSL_IMX6UL_LCDIF_SIZE = 0x1000, FSL_IMX6UL_CSI_ADDR = 0x021C4000, FSL_IMX6UL_CSI_SIZE = 0x100, diff --git a/include/hw/arm/imx6ul_lcdif.h b/include/hw/arm/imx6ul_lcdif.h new file mode 100644 index 0000000000..86da972d06 --- /dev/null +++ b/include/hw/arm/imx6ul_lcdif.h @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: GPL-2.0-or-later + * + * i.MX6UL LCDIF controller + * + * Copyright (c) 2026 + */ + +#ifndef IMX6UL_LCDIF_H +#define IMX6UL_LCDIF_H + +#include "hw/core/sysbus.h" +#include "qom/object.h" +#include "qemu/timer.h" +#include "ui/console.h" + +#define TYPE_IMX6UL_LCDIF "imx6ul-lcdif" +OBJECT_DECLARE_SIMPLE_TYPE(IMX6ULLCDIFState, IMX6UL_LCDIF) + +struct IMX6ULLCDIFState { + SysBusDevice parent_obj; + + MemoryRegion iomem; + MemoryRegionSection fbsection; + qemu_irq irq; + QemuConsole *con; + QEMUTimer *frame_timer; + uint32_t fb_base; + uint32_t src_width; + uint32_t rows; + uint8_t src_bpp; + uint8_t dst_bpp; + bool invalidate; + uint32_t regs[0x1000 / sizeof(uint32_t)]; +}; + +#endif /* IMX6UL_LCDIF_H */ -- 2.53.0
