virtio: Implement a proper sandbox emulator

The existing sandbox implementation of virtio only tests the basic API.
It is not able to provide a block device, for example.

Add a new implementation which operations at a higher level. It makes
use of the existing MMIO driver to perform virtio operations.

This emulator-device should be the parent of a function-specific
emulator. That emulator uses this MMIO transport to communicate with the
controller:

	virtio-blk {
		compatible = "sandbox,virtio-blk-emul";

		mmio {
			compatible = "sandbox,virtio-emul";
		};
	};

A new UCLASS_VIRTIO_EMUL uclass is created for the child devices, which
implement the actual function (block device, random-number generator,
etc.)

Signed-off-by: Simon Glass <sjg@chromium.org>
This commit is contained in:
Simon Glass
2025-06-27 18:28:49 -06:00
parent 9377438d02
commit 3092ddbc88
7 changed files with 435 additions and 1 deletions

View File

@@ -220,6 +220,7 @@ config SANDBOX
imply VIRTIO_MMIO
imply VIRTIO_PCI
imply VIRTIO_SANDBOX
imply VIRTIO_SANDBOX_EMUL
# Re-enable this when fully implemented
# imply VIRTIO_BLK
imply VIRTIO_NET

View File

@@ -9,7 +9,6 @@ CONFIG_PCI=y
CONFIG_ANDROID_BOOT_IMAGE=y
CONFIG_TIMESTAMP=y
CONFIG_FIT=y
CONFIG_FIT_SIGNATURE=y
# CONFIG_BOOTSTD_FULL is not set
# CONFIG_BOOTMETH_CROS is not set
# CONFIG_BOOTMETH_VBE is not set
@@ -38,5 +37,6 @@ CONFIG_TIMER=y
# CONFIG_VIRTIO_MMIO is not set
# CONFIG_VIRTIO_PCI is not set
# CONFIG_VIRTIO_SANDBOX is not set
# CONFIG_VIRTIO_SANDBOX_EMUL is not set
# CONFIG_GENERATE_ACPI_TABLE is not set
CONFIG_TOOLS_MKEFICAPSULE=y

View File

@@ -54,6 +54,14 @@ config VIRTIO_SANDBOX
This driver provides support for Sandbox implementation of virtio
transport driver which is used for testing purpose only.
config VIRTIO_SANDBOX_EMUL
bool "Sandbox MMIO emulator for virtio devices"
depends on SANDBOX
select VIRTIO
help
This driver provides an MMIO interface to an emulation of a block
device. It is used for testing purpose only.
config VIRTIO_NET
bool "virtio net driver"
depends on VIRTIO && NETDEVICES

View File

@@ -8,6 +8,7 @@ obj-$(CONFIG_VIRTIO_MMIO) += virtio_mmio.o
obj-$(CONFIG_VIRTIO_PCI) += virtio_pci_modern.o
obj-$(CONFIG_VIRTIO_PCI_LEGACY) += virtio_pci_legacy.o
obj-$(CONFIG_VIRTIO_SANDBOX) += virtio_sandbox.o
obj-$(CONFIG_VIRTIO_SANDBOX_EMUL) += sandbox_emul.o
obj-$(CONFIG_VIRTIO_NET) += virtio_net.o
obj-$(CONFIG_VIRTIO_BLK) += virtio_blk.o
obj-$(CONFIG_VIRTIO_RNG) += virtio_rng.o

View File

@@ -0,0 +1,313 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* VirtIO Sandbox emulator, for testing purpose only. This emulates the QEMU
* side of virtio, using the MMIO driver and handling any accesses
*
* This handles traffic from the virtio_ring
*
* Copyright 2025 Simon Glass <sjg@chromium.org>
*/
#define LOG_CATEGORY UCLASS_VIRTIO
#include <dm.h>
#include <malloc.h>
#include <virtio.h>
#include <asm/io.h>
#include <dt-bindings/virtio.h>
#include <asm/state.h>
#include <linux/sizes.h>
#include "sandbox_emul.h"
#include "virtio_types.h"
#include "virtio_blk.h"
#include "virtio_internal.h"
#include "virtio_mmio.h"
#include "virtio_ring.h"
enum {
MMIO_SIZE = 0x200,
VENDOR_ID = 0xf003,
DEVICE_ID = VIRTIO_ID_BLOCK,
DISK_SIZE_MB = 16,
};
void process_queue(struct udevice *emul_dev, struct sandbox_emul_priv *priv,
uint32_t queue_idx)
{
struct virtio_emul_ops *ops = virtio_emul_get_ops(emul_dev);
bool processed_something = false;
struct virtio_emul_queue *q;
struct vring_avail *avail;
struct vring_desc *desc;
struct vring_used *used;
uint old_used_idx;
if (queue_idx >= priv->num_queues)
return;
log_debug("Notified on queue %u\n", queue_idx);
q = &priv->queues[queue_idx];
if (!q->ready)
return;
desc = (struct vring_desc *)q->desc_addr;
avail = (struct vring_avail *)q->avail_addr;
used = (struct vring_used *)q->used_addr;
old_used_idx = used->idx;
while (q->last_avail_idx != avail->idx) {
processed_something = true;
uint ring_idx = q->last_avail_idx % q->num;
uint desc_head_idx = avail->ring[ring_idx];
uint used_ring_idx;
int written;
int ret;
log_debug("Found request at avail ring index %u (desc head %u)\n",
ring_idx, desc_head_idx);
ret = ops->process_request(emul_dev, desc, desc_head_idx,
&written);
if (ret)
log_warning("Failed to process request (err=%dE)\n",
ret);
used_ring_idx = used->idx % q->num;
used->ring[used_ring_idx].id = desc_head_idx;
used->ring[used_ring_idx].len = written;
used->idx++;
q->last_avail_idx++;
}
if (processed_something) {
bool needs_interrupt = true;
log_debug("finished processing, new used_idx is %d.\n",
used->idx);
if (priv->driver_features & BIT(VIRTIO_RING_F_EVENT_IDX)) {
struct {
struct vring_avail *avail;
unsigned int num;
} vr;
vr.avail = avail;
vr.num = q->num;
needs_interrupt =
vring_need_event(vring_used_event((&vr)),
used->idx, old_used_idx);
log_debug("EVENT_IDX is enabled; driver wants event "
"at %u needs_interrupt %d\n",
vring_used_event(&vr), needs_interrupt);
}
if (needs_interrupt) {
log_debug("sending VRING interrupt\n");
priv->interrupt_status |= VIRTIO_MMIO_INT_VRING;
}
}
}
long h_read(void *ctx, const void *addr, enum sandboxio_size_t size)
{
struct udevice *dev = ctx;
struct udevice *emul_dev = dev_get_parent(dev);
struct sandbox_emul_priv *priv = dev_get_priv(dev);
ulong offset = (ulong)addr - (ulong)priv->mmio.base;
struct virtio_emul_ops *ops = virtio_emul_get_ops(emul_dev);
struct virtio_emul_queue *q;
u32 val = 0;
if (offset >= VIRTIO_MMIO_CONFIG) {
ulong config_offset = offset - VIRTIO_MMIO_CONFIG;
int ret;
ret = ops->get_config(emul_dev, config_offset, &val, size);
if (ret)
log_warning("Failed to process request (err=%dE)\n",
ret);
return val;
}
if (priv->queue_sel >= priv->num_queues) {
log_debug("invalid queue_sel %d\n", priv->queue_sel);
return 0;
}
q = &priv->queues[priv->queue_sel];
switch (offset) {
case VIRTIO_MMIO_MAGIC_VALUE:
return ('v' | 'i' << 8 | 'r' << 16 | 't' << 24);
case VIRTIO_MMIO_VERSION:
return 2;
case VIRTIO_MMIO_DEVICE_ID:
return ops->get_device_id(emul_dev);
case VIRTIO_MMIO_VENDOR_ID:
return VENDOR_ID;
case VIRTIO_MMIO_DEVICE_FEATURES:
return !priv->features_sel ?
(priv->features & 0xffffffff) :
(priv->features >> 32);
case VIRTIO_MMIO_QUEUE_NUM_MAX:
return QUEUE_MAX_SIZE;
case VIRTIO_MMIO_QUEUE_READY:
return q->ready;
case VIRTIO_MMIO_INTERRUPT_STATUS:
return priv->interrupt_status;
case VIRTIO_MMIO_STATUS:
return priv->status;
case VIRTIO_MMIO_QUEUE_DESC_LOW:
return q->desc_addr & 0xffffffff;
case VIRTIO_MMIO_QUEUE_DESC_HIGH:
return q->desc_addr >> 32;
case VIRTIO_MMIO_QUEUE_AVAIL_LOW:
return q->avail_addr & 0xffffffff;
case VIRTIO_MMIO_QUEUE_AVAIL_HIGH:
return q->avail_addr >> 32;
case VIRTIO_MMIO_QUEUE_USED_LOW:
return q->used_addr & 0xffffffff;
case VIRTIO_MMIO_QUEUE_USED_HIGH:
return q->used_addr >> 32;
case VIRTIO_MMIO_CONFIG_GENERATION:
return priv->config_generation;
default:
log_debug("unhandled read from offset 0x%lx\n", offset);
return 0;
}
}
void h_write(void *ctx, void *addr, unsigned int val,
enum sandboxio_size_t size)
{
struct udevice *dev = ctx;
struct udevice *emul_dev = dev_get_parent(dev);
struct sandbox_emul_priv *priv = dev_get_priv(dev);
ulong offset = (ulong)addr - (ulong)priv->mmio.base;
struct virtio_emul_queue *q;
if (offset >= VIRTIO_MMIO_CONFIG)
return;
if (priv->queue_sel >= priv->num_queues && offset != VIRTIO_MMIO_QUEUE_SEL)
return;
q = &priv->queues[priv->queue_sel];
switch (offset) {
case VIRTIO_MMIO_DEVICE_FEATURES_SEL:
priv->features_sel = val;
break;
case VIRTIO_MMIO_DRIVER_FEATURES:
if (priv->features_sel == 0)
priv->driver_features = (priv->driver_features &
0xffffffff00000000) | val;
else
priv->driver_features = (priv->driver_features &
0xffffffff) | ((u64)val << 32);
break;
case VIRTIO_MMIO_DRIVER_FEATURES_SEL:
priv->features_sel = val;
break;
case VIRTIO_MMIO_QUEUE_SEL:
if (val < priv->num_queues)
priv->queue_sel = val;
else
log_debug("tried to select invalid queue %u\n", val);
break;
case VIRTIO_MMIO_QUEUE_NUM:
q->num = (val > 0 && val <= QUEUE_MAX_SIZE) ? val : 0;
break;
case VIRTIO_MMIO_QUEUE_READY:
q->ready = val & 0x1;
break;
case VIRTIO_MMIO_QUEUE_NOTIFY:
process_queue(emul_dev, priv, val);
break;
case VIRTIO_MMIO_INTERRUPT_ACK:
priv->interrupt_status &= ~val;
break;
case VIRTIO_MMIO_STATUS:
priv->status = val;
break;
case VIRTIO_MMIO_QUEUE_DESC_LOW:
q->desc_addr = (q->desc_addr & 0xffffffff00000000) | val;
break;
case VIRTIO_MMIO_QUEUE_DESC_HIGH:
q->desc_addr = (q->desc_addr & 0xffffffff) | ((u64)val << 32);
break;
case VIRTIO_MMIO_QUEUE_AVAIL_LOW:
q->avail_addr = (q->avail_addr & 0xffffffff00000000) | val;
break;
case VIRTIO_MMIO_QUEUE_AVAIL_HIGH:
q->avail_addr = (q->avail_addr & 0xffffffff) | ((u64)val << 32);
break;
case VIRTIO_MMIO_QUEUE_USED_LOW:
q->used_addr = (q->used_addr & 0xffffffff00000000) | val;
break;
case VIRTIO_MMIO_QUEUE_USED_HIGH:
q->used_addr = (q->used_addr & 0xffffffff) | ((u64)val << 32);
break;
default:
log_debug("unhandled write to offset 0x%lx\n", offset);
break;
}
}
static int sandbox_emul_of_to_plat(struct udevice *dev)
{
struct udevice *emul_dev = dev_get_parent(dev);
struct virtio_emul_ops *ops = virtio_emul_get_ops(emul_dev);
struct sandbox_emul_priv *priv = dev_get_priv(dev);
int ret;
/* set up the MMIO base so that virtio_mmio_probe() can find it */
priv->mmio.base = memalign(SZ_4K, MMIO_SIZE);
if (!priv->mmio.base)
return -ENOMEM;
ret = sandbox_mmio_add(priv->mmio.base, MMIO_SIZE, h_read, h_write,
dev);
if (ret) {
free(priv->mmio.base);
return log_msg_ret("sep", ret);
}
priv->num_queues = MAX_VIRTIO_QUEUES;
priv->features = BIT(VIRTIO_F_VERSION_1) |
BIT(VIRTIO_RING_F_EVENT_IDX) |
ops->get_features(emul_dev);
log_debug("sandbox virtio emulator, mmio %p\n", priv->mmio.base);
return 0;
}
static int sandbox_emul_remove(struct udevice *dev)
{
sandbox_mmio_remove(dev);
return 0;
}
static const struct udevice_id virtio_sandbox2_ids[] = {
{ .compatible = "sandbox,virtio-emul" },
{ }
};
U_BOOT_DRIVER(virtio_emul) = {
.name = "virtio-emul",
.id = UCLASS_VIRTIO,
.of_match = virtio_sandbox2_ids,
.probe = virtio_mmio_probe,
.remove = sandbox_emul_remove,
.ops = &virtio_mmio_ops,
.of_to_plat = sandbox_emul_of_to_plat,
.priv_auto = sizeof(struct sandbox_emul_priv),
};
UCLASS_DRIVER(virtio_emul) = {
.name = "virtio_emul",
.id = UCLASS_VIRTIO_EMUL,
#if CONFIG_IS_ENABLED(OF_REAL)
.post_bind = dm_scan_fdt_dev,
#endif
};

View File

@@ -0,0 +1,110 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* VirtIO Sandbox emulator, for testing purpose only. This emulates the QEMU
* side of virtio, using the MMIO driver and handling any accesses
*
* This handles traffic from the virtio_ring
*
* Copyright 2025 Simon Glass <sjg@chromium.org>
*/
#ifndef __SANDBOX_EMUL_H
#define __SANDBOX_EMUL_H
#include "virtio_mmio.h"
#include "virtio_types.h"
enum sandboxio_size_t;
struct udevice;
struct vring_desc;
enum {
MAX_VIRTIO_QUEUES = 8,
QUEUE_MAX_SIZE = 256,
};
/**
* struct virtio_emul_queue - Emulator's state for a single virtqueue
*/
struct virtio_emul_queue {
__virtio32 num;
__virtio32 ready;
__virtio64 desc_addr;
__virtio64 avail_addr;
__virtio64 used_addr;
__virtio16 last_avail_idx; // Device's internal counter
};
/**
* struct sandbox_emul_priv - Private info for the emulator
*/
struct sandbox_emul_priv {
struct virtio_mmio_priv mmio;
int num_queues;
int queue_sel;
u32 status;
u64 features_sel;
u64 features;
u64 driver_features;
u32 interrupt_status;
u32 config_generation;
struct virtio_emul_queue queues[MAX_VIRTIO_QUEUES];
};
/**
* struct virtio_emul_ops - Operations for a virtio device emulator
*
* @process_request:
* @get_config: Reads from the device-specific configuration space
* @get_features: Returns the device-specific feature bits
*/
struct virtio_emul_ops {
/**
* process_request() - Handles a single request from the driver
*
* @dev: The emulator device
* @descs: Pointer to the virtqueue's descriptor table
* @head_idx: The index of the first descriptor in the chain for
* this request
* @writtenp: Returns the total number of bytes written by the
* device into the driver's buffers (e.g. for a read
* request and the status byte). This is what will be
* placed in the `len` field of the used ring element.
* @return 0 on success, negative on error.
*/
int (*process_request)(struct udevice *dev, struct vring_desc *descs,
u32 head_idx, int *writtenp);
/**
* get_config() - Reads from the device-specific configuration space
*
* @dev: The emulator device
* @offset: The byte offset into the configuration space to read from
* @buf: The buffer to copy the configuration data into
* @size: The number of bytes to read
* @return 0 on success, negative on error.
*/
int (*get_config)(struct udevice *dev, ulong offset, void *buf,
enum sandboxio_size_t size);
/**
* get_features() - Returns the device-specific feature bits
*
* @dev: The emulator device
* @return A bitmask of the device-specific features to be OR'd
* with the transport features.
*/
u64 (*get_features)(struct udevice *dev);
/**
* get_device_id() - Returns the virtio device ID
*
* @dev: The emulator device
* @return The virtio device ID for this emulator
*/
u32 (*get_device_id)(struct udevice *dev);
};
#define virtio_emul_get_ops(dev) ((struct virtio_emul_ops *)(dev)->driver->ops)
#endif

View File

@@ -35,6 +35,7 @@ enum uclass_id {
UCLASS_USB_EMUL, /* sandbox USB bus device emulator */
UCLASS_AXI_EMUL, /* sandbox AXI bus device emulator */
UCLASS_FFA_EMUL, /* sandbox FF-A device emulator */
UCLASS_VIRTIO_EMUL, /* Emulator for a virtIO transport device */
/* U-Boot uclasses start here - in alphabetical order */
UCLASS_ACPI_PMC, /* (x86) Power-management controller (PMC) */