Ramfb
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.