Ramfb

From OSDev Wiki
Jump to navigation Jump to search

Introduction

ramfb is a simple way to get graphics on QEMU via a framebuffer in memory on embedded platforms like ARM or riscv. It works by adding -device ramfb to your QEMU command line and then configuring ramfb via fw_cfg. ramfb requires QEMU DMA support which should be available on platforms that have fw_cfg as MMIO (as opposed to x86 IO ports).

Explanation

To initialize the ramfb device we need to write the following structure to the fw_cfg entry with the name etc/ramfb using QEMU_fw_cfg#DMA:

struct RAMFBCfg {
    uint64_t addr;
    uint32_t fourcc;
    uint32_t flags;
    uint32_t width;
    uint32_t height;
    uint32_t stride;
};

The field fourcc is a 4 letter code that identifies the pixelformat expected by the framebuffer. The available fourcc codes can be found in the qemu source code

After that the framebuffer will be mapped at the address specified in the addr field. Writing pixel data to it will immediately show the pixels on screen.

Example

A minimal rust example would be:

use core::ffi::CStr;
use core::ptr::addr_of;
use core::mem;

#[repr(C)]
struct FWCfgFile {
    size: u32,
    select: u16,
    reserved: u16,
    name: [u8; 56]
}

#[repr(C, packed)]
struct FWCfgDmaAccess {
    control: u32,
    len: u32,
    addr: u64
}

#[repr(C, packed)]
struct RamFBCfg {
    addr: u64,
    fmt: u32,
    flags: u32,
    w: u32,
    h: u32,
    st: u32
}

const QEMU_CFG_DMA_CTL_READ:   u32 = 0x02;
const QEMU_CFG_DMA_CTL_SELECT: u32 = 0x08;
const QEMU_CFG_DMA_CTL_WRITE:  u32 = 0x10;

unsafe fn qemu_dma_transfer (control: u32, len: u32, addr: u64) {
    // Address of the DMA register on the aarch64 virt board
    let fw_cfg_dma: *mut u64 = 0x9020010 as *mut u64; 
    let dma = FWCfgDmaAccess {
        control: control.to_be(),
        len: len.to_be(),
        addr: addr.to_be()
    };
    unsafe {
        fw_cfg_dma.write_volatile((addr_of!(dma) as u64).to_be());
    }
    // Wait until DMA completed or error bit set
}

pub fn setup_ramfb(fb_addr: *mut u8, width: u32, height: u32) {
    let mut num_entries: u32 = 0;
    let fw_cfg_file_directory = 0x19;
    unsafe {
        qemu_dma_transfer((fw_cfg_file_directory << 16
                        | QEMU_CFG_DMA_CTL_SELECT 
                        | QEMU_CFG_DMA_CTL_READ) as u32,
                        mem::size_of::<u32>(),
                        addr_of!(num_entries) as u64);
    }

    // QEMU DMA is BE so need to byte swap arguments and results on LE
    num_entries = num_entries.to_be();

    let ramfb = FWCfgFile {
        size: 0,
        select: 0,
        reserved: 0,
        name: [0; 56]
    };

    for _ in 0..num_entries {
        unsafe {
            qemu_dma_transfer(QEMU_CFG_DMA_CTL_READ,
                            mem::size_of::<FWCfgFile>() as u32,
                            addr_of!(ramfb) as u64);
        }
        let entry = CStr::from_bytes_until_nul(&ramfb.name).unwrap();
        let entry = entry.to_str().unwrap();
        if entry == "etc/ramfb" {
            break;
        }
    }

    // See fourcc:
    // https://github.com/qemu/qemu/blob/54294b23e16dfaeb72e0ffa8b9f13ca8129edfce/include/standard-headers/drm/drm_fourcc.h#L188
    let pixel_format = ('R' as u32) | (('G' as u32) << 8) | 
    (('2' as u32) << 16) | (('4' as u32) << 24);

    // Stride 0 means QEMU calculates from bpp_of_format*width:
    // https://github.com/qemu/qemu/blob/54294b23e16dfaeb72e0ffa8b9f13ca8129edfce/hw/display/ramfb.c#L60
    let ramfb_cfg = RamFBCfg {
        addr: (fb_addr as u64).to_be(),
        fmt: (pixel_format).to_be(),
        flags: (0 as u32).to_be(),
        w: (width as u32).to_be(),
        h: (height as u32).to_be(),
        st: (0 as u32).to_be()
    };

    unsafe {
        qemu_dma_transfer((ramfb.select.to_be() as u32) << 16 
      | QEMU_CFG_DMA_CTL_SELECT 
      | QEMU_CFG_DMA_CTL_WRITE, mem::size_of::<RamFBCfg>() as u32,
        addr_of!(ramfb_cfg) as u64);
    }
}

Now you should be able to write to the address you passed as a framebuffer and see the result on screen:

for x in 0..(stride*height) {
    fb_addr.add(x as usize).write_volatile(0xFF);
}

This should produce a white screen.

See Also

QEMU_fw_cfg#DMA

External Links

QEMU fw_cfg Documentation

QEMU fourcc source

Example code in C

Example code in rust