Files
u-boot/drivers/virtio/sandbox_emul.c
Simon Glass 3092ddbc88 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>
2025-06-28 06:36:09 -06:00

314 lines
8.1 KiB
C

// 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
};