530 lines
14 KiB
C
530 lines
14 KiB
C
/*
|
|
* 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);
|
|
}
|