aboutsummaryrefslogtreecommitdiffstats
path: root/roms/openbios/drivers/virtio.c
diff options
context:
space:
mode:
Diffstat (limited to 'roms/openbios/drivers/virtio.c')
-rw-r--r--roms/openbios/drivers/virtio.c530
1 files changed, 530 insertions, 0 deletions
diff --git a/roms/openbios/drivers/virtio.c b/roms/openbios/drivers/virtio.c
new file mode 100644
index 000000000..7808d943a
--- /dev/null
+++ b/roms/openbios/drivers/virtio.c
@@ -0,0 +1,530 @@
+/*
+ * OpenBIOS virtio-1.0 virtio-blk driver
+ *
+ * Copyright (c) 2013 Alexander Graf <agraf@suse.de>
+ * Copyright (c) 2018 Mark Cave-Ayland <mark.cave-ayland@ilande.co.uk>
+ *
+ * This work is licensed under the terms of the GNU GPL, version 2 or (at
+ * your option) any later version. See the COPYING file in the top-level
+ * directory.
+ */
+
+#include "config.h"
+#include "libc/byteorder.h"
+#include "libc/vsprintf.h"
+#include "libopenbios/bindings.h"
+#include "libopenbios/ofmem.h"
+#include "kernel/kernel.h"
+#include "drivers/drivers.h"
+
+#include "virtio.h"
+
+#define VRING_WAIT_REPLY_TIMEOUT 10000
+
+static uint8_t virtio_cfg_read8(uint64_t cfg_addr, int addr)
+{
+ return in_8((uint8_t *)(uintptr_t)(cfg_addr + addr));
+}
+
+static void virtio_cfg_write8(uint64_t cfg_addr, int addr, uint8_t value)
+{
+ out_8((uint8_t *)(uintptr_t)(cfg_addr + addr), value);
+}
+
+static uint16_t virtio_cfg_read16(uint64_t cfg_addr, int addr)
+{
+ return in_le16((uint16_t *)(uintptr_t)(cfg_addr + addr));
+}
+
+static void virtio_cfg_write16(uint64_t cfg_addr, int addr, uint16_t value)
+{
+ out_le16((uint16_t *)(uintptr_t)(cfg_addr + addr), value);
+}
+
+static uint32_t virtio_cfg_read32(uint64_t cfg_addr, int addr)
+{
+ return in_le32((uint32_t *)(uintptr_t)(cfg_addr + addr));
+}
+
+static void virtio_cfg_write32(uint64_t cfg_addr, int addr, uint32_t value)
+{
+ out_le32((uint32_t *)(uintptr_t)(cfg_addr + addr), value);
+}
+
+static uint64_t virtio_cfg_read64(uint64_t cfg_addr, int addr)
+{
+ uint64_t q = ((uint64_t)virtio_cfg_read32(cfg_addr + 4, addr) << 32);
+ q |= virtio_cfg_read32(cfg_addr, addr);
+
+ return q;
+}
+
+static void virtio_cfg_write64(uint64_t cfg_addr, int addr, uint64_t value)
+{
+ virtio_cfg_write32(cfg_addr, addr, (value & 0xffffffff));
+ virtio_cfg_write32(cfg_addr, addr + 4, ((value >> 32) & 0xffffffff));
+}
+
+static long virtio_notify(VDev *vdev, int vq_idx, long cookie)
+{
+ uint16_t notify_offset = virtio_cfg_read16(vdev->common_cfg, VIRTIO_PCI_COMMON_Q_NOFF);
+
+ virtio_cfg_write16(vdev->notify_base, notify_offset +
+ vq_idx * vdev->notify_mult, vq_idx);
+
+ return 0;
+}
+
+/***********************************************
+ * Virtio functions *
+ ***********************************************/
+
+static void vring_init(VRing *vr, VqInfo *info)
+{
+ void *p = (void *) (uintptr_t)info->queue;
+
+ vr->id = info->index;
+ vr->num = info->num;
+ vr->desc = p;
+ vr->avail = (void *)((uintptr_t)p + info->num * sizeof(VRingDesc));
+ vr->used = (void *)(((unsigned long)&vr->avail->ring[info->num]
+ + info->align - 1) & ~(info->align - 1));
+
+ /* Zero out all relevant field */
+ vr->avail->flags = __cpu_to_le16(0);
+ vr->avail->idx = __cpu_to_le16(0);
+
+ /* We're running with interrupts off anyways, so don't bother */
+ vr->used->flags = __cpu_to_le16(VRING_USED_F_NO_NOTIFY);
+ vr->used->idx = __cpu_to_le16(0);
+ vr->used_idx = 0;
+ vr->next_idx = 0;
+ vr->cookie = 0;
+}
+
+static int vring_notify(VDev *vdev, VRing *vr)
+{
+ return virtio_notify(vdev, vr->id, vr->cookie);
+}
+
+static void vring_send_buf(VRing *vr, uint64_t p, int len, int flags)
+{
+ /* For follow-up chains we need to keep the first entry point */
+ if (!(flags & VRING_HIDDEN_IS_CHAIN)) {
+ vr->avail->ring[__le16_to_cpu(vr->avail->idx) % vr->num] = __cpu_to_le16(vr->next_idx);
+ }
+
+ vr->desc[vr->next_idx].addr = __cpu_to_le64(p);
+ vr->desc[vr->next_idx].len = __cpu_to_le32(len);
+ vr->desc[vr->next_idx].flags = __cpu_to_le16(flags & ~VRING_HIDDEN_IS_CHAIN);
+ vr->desc[vr->next_idx].next = __cpu_to_le16(vr->next_idx);
+ vr->desc[vr->next_idx].next = __cpu_to_le16(__le16_to_cpu(vr->desc[vr->next_idx].next) + 1);
+ vr->next_idx++;
+
+ /* Chains only have a single ID */
+ if (!(flags & VRING_DESC_F_NEXT)) {
+ vr->avail->idx = __cpu_to_le16(__le16_to_cpu(vr->avail->idx) + 1);
+ }
+}
+
+static int vr_poll(VDev *vdev, VRing *vr)
+{
+ if (__le16_to_cpu(vr->used->idx) == vr->used_idx) {
+ vring_notify(vdev, vr);
+ return 0;
+ }
+
+ vr->used_idx = __le16_to_cpu(vr->used->idx);
+ vr->next_idx = 0;
+ vr->desc[0].len = __cpu_to_le32(0);
+ vr->desc[0].flags = __cpu_to_le16(0);
+ return 1; /* vr has been updated */
+}
+
+/*
+ * Wait for the host to reply.
+ *
+ * timeout is in msecs if > 0.
+ *
+ * Returns 0 on success, 1 on timeout.
+ */
+static int vring_wait_reply(VDev *vdev)
+{
+ ucell target_ms, get_ms;
+
+ fword("get-msecs");
+ target_ms = POP();
+ target_ms += vdev->wait_reply_timeout;
+
+ /* Wait for any queue to be updated by the host */
+ do {
+ int i, r = 0;
+
+ for (i = 0; i < vdev->nr_vqs; i++) {
+ r += vr_poll(vdev, &vdev->vrings[i]);
+ }
+
+ if (r) {
+ return 0;
+ }
+
+ fword("get-msecs");
+ get_ms = POP();
+
+ } while (!vdev->wait_reply_timeout || (get_ms < target_ms));
+
+ return 1;
+}
+
+static uint64_t vring_addr_translate(VDev *vdev, void *p)
+{
+ ucell mode;
+ uint64_t iova;
+
+ iova = ofmem_translate(pointer2cell(p), &mode);
+ return iova;
+}
+
+/***********************************************
+ * Virtio block *
+ ***********************************************/
+
+static int virtio_blk_read_many(VDev *vdev,
+ uint64_t offset, void *load_addr, int len)
+{
+ VirtioBlkOuthdr out_hdr;
+ u8 status;
+ VRing *vr = &vdev->vrings[vdev->cmd_vr_idx];
+ uint8_t discard[VIRTIO_SECTOR_SIZE];
+
+ uint64_t start_sector = offset / virtio_get_block_size(vdev);
+ int head_len = offset & (virtio_get_block_size(vdev) - 1);
+ uint64_t end_sector = (offset + len + virtio_get_block_size(vdev) - 1) /
+ virtio_get_block_size(vdev);
+ int tail_len = end_sector * virtio_get_block_size(vdev) - (offset + len);
+
+ /* Tell the host we want to read */
+ out_hdr.type = __cpu_to_le32(VIRTIO_BLK_T_IN);
+ out_hdr.ioprio = __cpu_to_le32(99);
+ out_hdr.sector = __cpu_to_le64(virtio_sector_adjust(vdev, start_sector));
+
+ vring_send_buf(vr, vring_addr_translate(vdev, &out_hdr), sizeof(out_hdr),
+ VRING_DESC_F_NEXT);
+
+ /* Discarded head */
+ if (head_len) {
+ vring_send_buf(vr, vring_addr_translate(vdev, &discard), head_len,
+ VRING_DESC_F_WRITE | VRING_HIDDEN_IS_CHAIN |
+ VRING_DESC_F_NEXT);
+ }
+
+ /* This is where we want to receive data */
+ vring_send_buf(vr, vring_addr_translate(vdev, load_addr), len,
+ VRING_DESC_F_WRITE | VRING_HIDDEN_IS_CHAIN |
+ VRING_DESC_F_NEXT);
+
+ /* Discarded tail */
+ if (tail_len) {
+ vring_send_buf(vr, vring_addr_translate(vdev, &discard), tail_len,
+ VRING_DESC_F_WRITE | VRING_HIDDEN_IS_CHAIN |
+ VRING_DESC_F_NEXT);
+ }
+
+ /* status field */
+ vring_send_buf(vr, vring_addr_translate(vdev, &status), sizeof(u8),
+ VRING_DESC_F_WRITE | VRING_HIDDEN_IS_CHAIN);
+
+ /* Now we can tell the host to read */
+ vring_wait_reply(vdev);
+
+ return status;
+}
+
+int virtio_read_many(VDev *vdev, uint64_t offset, void *load_addr, int len)
+{
+ switch (vdev->senseid) {
+ case VIRTIO_ID_BLOCK:
+ return virtio_blk_read_many(vdev, offset, load_addr, len);
+ }
+ return -1;
+}
+
+static int virtio_read(VDev *vdev, uint64_t offset, void *load_addr, int len)
+{
+ return virtio_read_many(vdev, offset, load_addr, len);
+}
+
+int virtio_get_block_size(VDev *vdev)
+{
+ switch (vdev->senseid) {
+ case VIRTIO_ID_BLOCK:
+ return vdev->config.blk.blk_size << vdev->config.blk.physical_block_exp;
+ }
+ return 0;
+}
+
+static void
+ob_virtio_configure_device(VDev *vdev)
+{
+ uint32_t feature;
+ uint8_t status;
+ int i;
+
+ /* Indicate we recognise the device */
+ status = virtio_cfg_read8(vdev->common_cfg, VIRTIO_PCI_COMMON_STATUS);
+ status |= VIRTIO_CONFIG_S_ACKNOWLEDGE | VIRTIO_CONFIG_S_DRIVER;
+ virtio_cfg_write8(vdev->common_cfg, VIRTIO_PCI_COMMON_STATUS, status);
+
+ /* Negotiate features: acknowledge VIRTIO_F_VERSION_1 for 1.0 specification
+ little-endian access */
+ virtio_cfg_write32(vdev->common_cfg, VIRTIO_PCI_COMMON_DFSELECT, 0x1);
+ virtio_cfg_write32(vdev->common_cfg, VIRTIO_PCI_COMMON_GFSELECT, 0x1);
+ feature = virtio_cfg_read32(vdev->common_cfg, VIRTIO_PCI_COMMON_DF);
+ feature &= (1ULL << (VIRTIO_F_VERSION_1 - 32));
+ virtio_cfg_write32(vdev->common_cfg, VIRTIO_PCI_COMMON_GF, feature);
+
+ status = virtio_cfg_read8(vdev->common_cfg, VIRTIO_PCI_COMMON_STATUS);
+ status |= VIRTIO_CONFIG_S_FEATURES_OK;
+ virtio_cfg_write8(vdev->common_cfg, VIRTIO_PCI_COMMON_STATUS, status);
+
+ vdev->senseid = VIRTIO_ID_BLOCK;
+ vdev->nr_vqs = 1;
+ vdev->cmd_vr_idx = 0;
+ vdev->wait_reply_timeout = VRING_WAIT_REPLY_TIMEOUT;
+ vdev->scsi_block_size = VIRTIO_SCSI_BLOCK_SIZE;
+ vdev->blk_factor = 1;
+
+ for (i = 0; i < vdev->nr_vqs; i++) {
+ VqInfo info = {
+ .queue = (uintptr_t) vdev->ring_area + (i * VIRTIO_RING_SIZE),
+ .align = VIRTIO_PCI_VRING_ALIGN,
+ .index = i,
+ .num = 0,
+ };
+
+ virtio_cfg_write16(vdev->common_cfg, VIRTIO_PCI_COMMON_Q_SELECT, i);
+
+ info.num = virtio_cfg_read16(vdev->common_cfg, VIRTIO_PCI_COMMON_Q_SIZE);
+ if (info.num > VIRTIO_MAX_RING_ENTRIES) {
+ info.num = VIRTIO_MAX_RING_ENTRIES;
+ virtio_cfg_write16(vdev->common_cfg, VIRTIO_PCI_COMMON_Q_SIZE, info.num);
+ }
+
+ vring_init(&vdev->vrings[i], &info);
+
+ /* Set block information */
+ vdev->guessed_disk_nature = VIRTIO_GDN_NONE;
+ vdev->config.blk.blk_size = VIRTIO_SECTOR_SIZE;
+ vdev->config.blk.physical_block_exp = 0;
+
+ /* Read sectors */
+ vdev->config.blk.capacity = virtio_cfg_read64(vdev->device_cfg, 0);
+
+ /* Set queue addresses */
+ virtio_cfg_write64(vdev->common_cfg, VIRTIO_PCI_COMMON_Q_DESCLO,
+ vring_addr_translate(vdev, &vdev->vrings[i].desc[0]));
+ virtio_cfg_write64(vdev->common_cfg, VIRTIO_PCI_COMMON_Q_AVAILLO,
+ vring_addr_translate(vdev, &vdev->vrings[i].avail[0]));
+ virtio_cfg_write64(vdev->common_cfg, VIRTIO_PCI_COMMON_Q_USEDLO,
+ vring_addr_translate(vdev, &vdev->vrings[i].used[0]));
+
+ /* Enable queue */
+ virtio_cfg_write16(vdev->common_cfg, VIRTIO_PCI_COMMON_Q_ENABLE, 1);
+ }
+
+ /* Initialisation complete */
+ status |= VIRTIO_CONFIG_S_DRIVER_OK;
+ virtio_cfg_write8(vdev->common_cfg, VIRTIO_PCI_COMMON_STATUS, status);
+
+ vdev->configured = 1;
+}
+
+static void
+ob_virtio_disk_open(VDev **_vdev)
+{
+ VDev *vdev;
+ phandle_t ph;
+
+ PUSH(find_ih_method("vdev", my_self()));
+ fword("execute");
+ *_vdev = cell2pointer(POP());
+ vdev = *_vdev;
+
+ vdev->pos = 0;
+
+ if (!vdev->configured) {
+ ob_virtio_configure_device(vdev);
+ }
+
+ /* interpose disk-label */
+ ph = find_dev("/packages/disk-label");
+ fword("my-args");
+ PUSH_ph( ph );
+ fword("interpose");
+
+ RET(-1);
+}
+
+static void
+ob_virtio_disk_close(VDev **_vdev)
+{
+ return;
+}
+
+/* ( pos.d -- status ) */
+static void
+ob_virtio_disk_seek(VDev **_vdev)
+{
+ VDev *vdev = *_vdev;
+ uint64_t pos;
+
+ pos = ((uint64_t)POP()) << 32;
+ pos |= POP();
+
+ /* Make sure we are within the physical limits */
+ if (pos < (vdev->config.blk.capacity * virtio_get_block_size(vdev))) {
+ vdev->pos = pos;
+ PUSH(0);
+ } else {
+ PUSH(1);
+ }
+
+ return;
+}
+
+/* ( addr len -- actual ) */
+static void
+ob_virtio_disk_read(VDev **_vdev)
+{
+ VDev *vdev = *_vdev;
+ ucell len = POP();
+ uint8_t *addr = (uint8_t *)POP();
+
+ virtio_read(vdev, vdev->pos, addr, len);
+
+ vdev->pos += len;
+
+ PUSH(len);
+}
+
+static void set_virtio_alias(const char *path, int idx)
+{
+ phandle_t aliases;
+ char name[9];
+
+ aliases = find_dev("/aliases");
+
+ snprintf(name, sizeof(name), "virtio%d", idx);
+
+ set_property(aliases, name, path, strlen(path) + 1);
+}
+
+DECLARE_UNNAMED_NODE(ob_virtio_disk, 0, sizeof(VDev *));
+
+NODE_METHODS(ob_virtio_disk) = {
+ { "open", ob_virtio_disk_open },
+ { "close", ob_virtio_disk_close },
+ { "seek", ob_virtio_disk_seek },
+ { "read", ob_virtio_disk_read },
+};
+
+static void
+ob_virtio_open(VDev **_vdev)
+{
+ PUSH(-1);
+}
+
+static void
+ob_virtio_close(VDev **_vdev)
+{
+ return;
+}
+
+static void
+ob_virtio_dma_alloc(__attribute__((unused)) VDev **_vdev)
+{
+ call_parent_method("dma-alloc");
+}
+
+static void
+ob_virtio_dma_free(__attribute__((unused)) VDev **_vdev)
+{
+ call_parent_method("dma-free");
+}
+
+static void
+ob_virtio_dma_map_in(__attribute__((unused)) VDev **_vdev)
+{
+ call_parent_method("dma-map-in");
+}
+
+static void
+ob_virtio_dma_map_out(__attribute__((unused)) VDev **_vdev)
+{
+ call_parent_method("dma-map-out");
+}
+
+static void
+ob_virtio_dma_sync(__attribute__((unused)) VDev **_vdev)
+{
+ call_parent_method("dma-sync");
+}
+
+DECLARE_UNNAMED_NODE(ob_virtio, 0, sizeof(VDev *));
+
+NODE_METHODS(ob_virtio) = {
+ { "open", ob_virtio_open },
+ { "close", ob_virtio_close },
+ { "dma-alloc", ob_virtio_dma_alloc },
+ { "dma-free", ob_virtio_dma_free },
+ { "dma-map-in", ob_virtio_dma_map_in },
+ { "dma-map-out", ob_virtio_dma_map_out },
+ { "dma-sync", ob_virtio_dma_sync },
+};
+
+void ob_virtio_init(const char *path, const char *dev_name, uint64_t common_cfg,
+ uint64_t device_cfg, uint64_t notify_base, uint32_t notify_mult,
+ int idx)
+{
+ char buf[256];
+ ucell addr;
+ VDev *vdev;
+
+ /* Open ob_virtio */
+ BIND_NODE_METHODS(get_cur_dev(), ob_virtio);
+
+ vdev = malloc(sizeof(VDev));
+ vdev->common_cfg = common_cfg;
+ vdev->device_cfg = device_cfg;
+ vdev->notify_base = notify_base;
+ vdev->notify_mult = notify_mult;
+ vdev->configured = 0;
+
+ PUSH(pointer2cell(vdev));
+ feval("value vdev");
+
+ PUSH(sizeof(VRing) * VIRTIO_MAX_VQS);
+ feval("dma-alloc");
+ addr = POP();
+ vdev->vrings = cell2pointer(addr);
+
+ PUSH((VIRTIO_RING_SIZE * 2 + VIRTIO_PCI_VRING_ALIGN) * VIRTIO_MAX_VQS);
+ feval("dma-alloc");
+ addr = POP();
+ vdev->ring_area = cell2pointer(addr);
+
+ fword("new-device");
+ push_str("disk");
+ fword("device-name");
+ push_str("block");
+ fword("device-type");
+
+ PUSH(pointer2cell(vdev));
+ feval("value vdev");
+
+ BIND_NODE_METHODS(get_cur_dev(), ob_virtio_disk);
+ fword("finish-device");
+
+ snprintf(buf, sizeof(buf), "%s/disk", path);
+ set_virtio_alias(buf, idx);
+}