Skip to content

Commit

Permalink
hw/display: Implement a virtual RGB screen for Espressif targets
Browse files Browse the repository at this point in the history
The framebuffer emulated by this new component is fake in the sense that the
real targets don't have it, but this does make it possible to have a GUI
for programs that will run on the ESP32 or ESP32-C3 targets.
  • Loading branch information
o-marshmallow authored and igrr committed Dec 5, 2023
1 parent 4512cda commit 04b3574
Show file tree
Hide file tree
Showing 8 changed files with 433 additions and 4 deletions.
308 changes: 308 additions & 0 deletions hw/display/esp_rgb.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
/*
* ESP Display RGB emulation
*
* Copyright (c) 2023 Espressif Systems (Shanghai) Co. Ltd.
*
* This implementation overrides the ESP32 UARt one, check it out first.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 or
* (at your option) any later version.
*/

#include "qemu/osdep.h"
#include "qemu/module.h"
#include "qapi/error.h"
#include "sysemu/sysemu.h"
#include "hw/sysbus.h"
#include "hw/irq.h"
#include "hw/display/esp_rgb.h"
#include "ui/console.h"
#include "qemu/error-report.h"
#include "sysemu/dma.h"

#define RGB_WARNING 1
#define RGB_DEBUG 0

#define RGB_VERSION_MAJOR 0
#define RGB_VERSION_MINOR 1

static uint64_t esp_rgb_read(void *opaque, hwaddr addr, unsigned int size)
{
ESPRgbState *s = ESP_RGB(opaque);
uint32_t r = 0;

/* Only accept 32-bit transactions */
if (size != sizeof(uint32_t)) {
return r;
}

switch(addr) {
case A_RGB_VERSION:
r = FIELD_DP32(r, RGB_VERSION, MAJOR, RGB_VERSION_MAJOR);
r = FIELD_DP32(r, RGB_VERSION, MINOR, RGB_VERSION_MINOR);
break;

case A_RGB_WIN_SIZE:
r = FIELD_DP32(r, RGB_WIN_SIZE, WIDTH, s->width);
r = FIELD_DP32(r, RGB_WIN_SIZE, HEIGHT, s->height);
break;

case A_RGB_UPDATE_FROM:
r = FIELD_DP32(r, RGB_UPDATE_FROM, X, s->from_x);
r = FIELD_DP32(r, RGB_UPDATE_FROM, Y, s->from_y);
break;

case A_RGB_UPDATE_TO:
r = FIELD_DP32(r, RGB_UPDATE_TO, X, s->to_x);
r = FIELD_DP32(r, RGB_UPDATE_TO, Y, s->to_y);
break;

case A_RGB_UPDATE_CONTENT:
r = s->color_content;
break;

case A_RGB_UPDATE_STATUS:
r = s->update_area;
break;

default:
#if RGB_WARNING
warn_report("[ESP RGB] Unsupported read to 0x%lx", addr);
#endif
break;
}

#if RGB_DEBUG
info_report("[ESP RGB] Reading 0x%lx (0x%x)", addr, r);
#endif

return r;
}


static void esp_rgb_write(void *opaque, hwaddr addr,
uint64_t value, unsigned int size)
{
ESPRgbState *s = ESP_RGB(opaque);
uint32_t width = 0;
uint32_t height = 0;

/* Only accept 32-bit transactions */
if (size != sizeof(uint32_t)) {
return;
}

#if RGB_DEBUG
info_report("[ESP RGB] Writing 0x%lx = %08lx", addr, value);
#endif

switch(addr) {
case A_RGB_WIN_SIZE:
width = FIELD_EX32(value, RGB_WIN_SIZE, WIDTH);
height = FIELD_EX32(value, RGB_WIN_SIZE, HEIGHT);

/* Check the bounds of the new window size */
s->width = MIN(width, ESP_RGB_MAX_WIDTH);
s->height = MIN(height, ESP_RGB_MAX_HEIGHT);
/* Make sure it's bigger than 10x10 */
s->width = MAX(s->width, 10);
s->height = MAX(s->height, 10);

/* Update the window size */
if (s->con) {
qemu_console_resize(s->con, s->width, s->height);
}
break;

case A_RGB_UPDATE_STATUS:
s->update_area = FIELD_EX32(value, RGB_UPDATE_STATUS, ENA) != 0;
break;

case A_RGB_UPDATE_FROM:
s->from_x = FIELD_EX32(value, RGB_UPDATE_FROM, X);
s->from_y = FIELD_EX32(value, RGB_UPDATE_FROM, Y);
break;

case A_RGB_UPDATE_TO:
s->to_x = FIELD_EX32(value, RGB_UPDATE_TO, X);
s->to_y = FIELD_EX32(value, RGB_UPDATE_TO, Y);
break;

case A_RGB_UPDATE_CONTENT:
s->color_content = (uint32_t) value;
break;

default:
#if RGB_WARNING
warn_report("[ESP RGB] Unsupported write to 0x%lx (%08lx)", addr, value);
#endif
break;
}

}


static void rgb_update(void* opaque)
{
ESPRgbState* s = (ESPRgbState*) opaque;

if (s->con && s->update_area) {
uint32_t src = s->color_content;
AddressSpace* src_as = NULL;

/* Since we are in a 32bpp configuration, it's enough to cast the framebuffer
* as a uint32_t pointer */
uint32_t* data = surface_data(qemu_console_surface(s->con));

/* Width and height of the area to update */
const int width = s->to_x - s->from_x;
const int height = s->to_y - s->from_y;
const int bytes_per_pixel = sizeof(uint32_t);
const int total_bytes = width * height * bytes_per_pixel;

if (address_space_access_valid(&s->vram_as, src, total_bytes, false, MEMTXATTRS_UNSPECIFIED)) {
src_as = &s->vram_as;
} else if (address_space_access_valid(&s->intram_as, src, total_bytes, false, MEMTXATTRS_UNSPECIFIED)) {
src_as = &s->intram_as;
} else {
s->update_area = false;
#if RGB_WARNING
warn_report("[ESP RGB] Invalid color content address or length");
#endif
return;
}

/* Only perform the copy if the area is valid */
if (width > 0 && height > 0 &&
(s->from_x + width) <= s->width && (s->from_y + height) <= s->height) {

uint32_t* dest = &data[s->from_y * s->width + s->from_x];

/* Copy the pixels to the framebuffer */
for (int i = 0; i < height; i++) {
dma_memory_read(src_as, src, dest, width * bytes_per_pixel, MEMTXATTRS_UNSPECIFIED);
/* Go to the next line in the destination */
dest += s->width;
/* Same goes for the source */
src += width * bytes_per_pixel;
}

dpy_gfx_update(s->con, s->from_x, s->from_y, width, height);
}
#if RGB_WARNING
else {
warn_report("[ESP RGB] Invalid drawing area");
}
#endif

/* Automatically clear the update flag, the guest can re-use the given color_content buffer.
* It must set it again to trigger another update. */
s->update_area = false;
}
}


static void rgb_invalidate(void *opaque)
{
ESPRgbState* s = (ESPRgbState*) opaque;

if (s->con) {
uint32_t* data = surface_data(qemu_console_surface(s->con));

/* On invalidate, reset the display */
memset(data, 0, s->width * s->height * 4);
}
}


static const GraphicHwOps fb_ops = {
.invalidate = rgb_invalidate,
.gfx_update = rgb_update
};


static void esp_rgb_realize(DeviceState *dev, Error **errp)
{
ESPRgbState* s = ESP_RGB(dev);

assert(s->intram != NULL);
/* Create an address space for internal RAM so that we can read data from it on GUI update */
address_space_init(&s->intram_as, s->intram, "esp32c3.rgb.intram_as");
}


static const MemoryRegionOps esp_rgb_ops = {
.read = esp_rgb_read,
.write = esp_rgb_write,
.endianness = DEVICE_LITTLE_ENDIAN,
};


static void esp_rgb_init(Object *obj)
{
ESPRgbState* s = ESP_RGB(obj);
SysBusDevice *sbd = SYS_BUS_DEVICE(obj);

memory_region_init_io(&s->iomem, obj, &esp_rgb_ops, s,
TYPE_ESP_RGB, ESP_RGB_IO_SIZE);
sysbus_init_mmio(sbd, &s->iomem);

/* Default window size */
s->width = ESP_RGB_MAX_WIDTH;
s->height = ESP_RGB_MAX_HEIGHT;
s->update_area = false;

if (s->con == NULL) {
s->con = graphic_console_init(DEVICE(s), 0, &fb_ops, s);
qemu_console_resize(s->con, s->width, s->height);
uint32_t* data = surface_data(qemu_console_surface(s->con));
/* Initialize the window to black */
memset(data, 0, ESP_RGB_MAX_VRAM_SIZE);
}

/* Create a memory region that can be used as a framebuffer by the guest */
memory_region_init_ram(&s->vram, OBJECT(s), "esp32c3-rgb-vram", ESP_RGB_MAX_VRAM_SIZE, &error_abort);

/* Create an AddressSpace out of the MemoryRegion to be able to perform DMA */
address_space_init(&s->vram_as, &s->vram, "esp32c3.rgb.vram_as");
}


static void esp_rgb_reset(DeviceState *dev)
{
ESPRgbState* s = ESP_RGB(dev);

/* Default window size */
s->width = ESP_RGB_MAX_WIDTH;
s->height = ESP_RGB_MAX_HEIGHT;
s->update_area = false;
s->from_x = 0;
s->from_y = 0;
s->to_x = 0;
s->to_y = 0;
}


static void esp_rgb_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);

dc->reset = esp_rgb_reset;
dc->realize = esp_rgb_realize;
}

static const TypeInfo esp_rgb_info = {
.name = TYPE_ESP_RGB,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(ESPRgbState),
.instance_init = esp_rgb_init,
.class_init = esp_rgb_class_init,
};

static void esp_rgb_register_types(void)
{
type_register_static(&esp_rgb_info);
}

type_init(esp_rgb_register_types)
2 changes: 2 additions & 0 deletions hw/display/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ system_ss.add(when: 'CONFIG_TCX', if_true: files('tcx.c'))
system_ss.add(when: 'CONFIG_CG3', if_true: files('cg3.c'))
system_ss.add(when: 'CONFIG_MACFB', if_true: files('macfb.c'))
system_ss.add(when: 'CONFIG_NEXTCUBE', if_true: files('next-fb.c'))
system_ss.add(when: 'CONFIG_RISCV_ESP32C3', if_true: files('esp_rgb.c'))
system_ss.add(when: 'CONFIG_XTENSA_ESP32', if_true: files('esp_rgb.c'))

system_ss.add(when: 'CONFIG_VGA', if_true: files('vga.c'))

Expand Down
23 changes: 22 additions & 1 deletion hw/riscv/esp32c3.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
#include "hw/misc/esp32c3_ds.h"
#include "hw/misc/esp32c3_jtag.h"
#include "hw/dma/esp32c3_gdma.h"
#include "hw/display/esp_rgb.h"

#define ESP32C3_IO_WARNING 0

Expand Down Expand Up @@ -83,10 +84,13 @@ struct Esp32C3MachineState {
ESP32C3SpiState spi1;
ESP32C3RtcCntlState rtccntl;
ESP32C3UsbJtagState jtag;
ESPRgbState rgb;
};

/* Fake register used by ESP-IDF application to determine whether the code is running on real hardware or on QEMU */
#define A_SYSCON_ORIGIN_REG 0x3F8
/* Temporary macro for generating a random value from register SYSCON_RND_DATA_REG */
#define A_SYSCON_RND_DATA_REG 0x0B0
#define A_SYSCON_RND_DATA_REG 0x0B0

/* Temporary macro to mark the CPU as in non-debugging mode */
#define A_ASSIST_DEBUG_CORE_0_DEBUG_MODE_REG 0x098
Expand All @@ -107,6 +111,7 @@ enum MemoryRegions {
ESP32C3_MEMREGION_RTCFAST,
ESP32C3_MEMREGION_DCACHE,
ESP32C3_MEMREGION_ICACHE,
ESP32C3_MEMREGION_FRAMEBUF,
};

#define ESP32C3_INTERNAL_SRAM0_SIZE (16*1024)
Expand All @@ -123,6 +128,8 @@ static const struct MemmapEntry {
[ESP32C3_MEMREGION_RTCFAST] = { 0x50000000, 0x2000 },
[ESP32C3_MEMREGION_DCACHE] = { 0x3c000000, 0x800000 },
[ESP32C3_MEMREGION_ICACHE] = { 0x42000000, 0x800000 },
/* Virtual Framebuffer, used for the graphical interface */
[ESP32C3_MEMREGION_FRAMEBUF] = { 0x20000000, ESP_RGB_MAX_VRAM_SIZE }
};


Expand All @@ -135,6 +142,9 @@ static uint64_t esp32c3_io_read(void *opaque, hwaddr addr, unsigned int size)
{
if (addr_in_range(addr + ESP32C3_IO_START_ADDR, DR_REG_RTC_I2C_BASE, DR_REG_RTC_I2C_BASE + 0x100)) {
return (uint32_t) 0xffffff;
} else if (addr + ESP32C3_IO_START_ADDR == DR_REG_SYSCON_BASE + A_SYSCON_ORIGIN_REG) {
/* Return "QEMU" as a 32-bit value */
return 0x51454d55;
} else if (addr + ESP32C3_IO_START_ADDR == DR_REG_SYSCON_BASE + A_SYSCON_RND_DATA_REG) {
/* Return a random 32-bit value */
static bool init = false;
Expand Down Expand Up @@ -343,6 +353,7 @@ static void esp32c3_machine_init(MachineState *machine)
object_initialize_child(OBJECT(machine), "spi1", &ms->spi1, TYPE_ESP32C3_SPI);
object_initialize_child(OBJECT(machine), "rtccntl", &ms->rtccntl, TYPE_ESP32C3_RTC_CNTL);
object_initialize_child(OBJECT(machine), "jtag", &ms->jtag, TYPE_ESP32C3_JTAG);
object_initialize_child(OBJECT(machine), "rgb", &ms->rgb, TYPE_ESP_RGB);

/* Realize all the I/O peripherals we depend on */

Expand Down Expand Up @@ -546,6 +557,16 @@ static void esp32c3_machine_init(MachineState *machine)
memory_region_add_subregion_overlap(sys_mem, DR_REG_DIGITAL_SIGNATURE_BASE, mr, 0);
}

/* RGB display realization */
{
/* Give the internal RAM memory region to the display */
ms->rgb.intram = dram;
sysbus_realize(SYS_BUS_DEVICE(&ms->rgb), &error_fatal);
MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(&ms->rgb), 0);
memory_region_add_subregion_overlap(sys_mem, DR_REG_FRAMEBUF_BASE, mr, 0);
memory_region_add_subregion_overlap(sys_mem, esp32c3_memmap[ESP32C3_MEMREGION_FRAMEBUF].base, &ms->rgb.vram, 0);
}

/* Open and load the "bios", which is the ROM binary, also named "first stage bootloader" */
char *rom_binary = qemu_find_file(QEMU_FILE_TYPE_BIOS, "esp32c3-rom.bin");
if (rom_binary == NULL) {
Expand Down
Loading

0 comments on commit 04b3574

Please sign in to comment.