blob: 7808d943ac0d1a5d17bde43ece1650316fc14c90 [file] [log] [blame]
/*
* 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);
}