/* * OpenBIOS virtio-1.0 virtio-blk driver * * Copyright (c) 2013 Alexander Graf * Copyright (c) 2018 Mark Cave-Ayland * * 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); }