| // SPDX-License-Identifier: GPL-2.0+ | |
| /* | |
| * Copyright (C) 2018, Tuomas Tynkkynen <tuomas.tynkkynen@iki.fi> | |
| * Copyright (C) 2018, Bin Meng <bmeng.cn@gmail.com> | |
| * | |
| * VirtIO memory-maped I/O transport driver | |
| * Ported from Linux drivers/virtio/virtio_mmio.c | |
| */ | |
| #include <dm.h> | |
| #include <log.h> | |
| #include <virtio_types.h> | |
| #include <virtio.h> | |
| #include <virtio_ring.h> | |
| #include <linux/bug.h> | |
| #include <linux/compat.h> | |
| #include <linux/err.h> | |
| #include <linux/io.h> | |
| #include "virtio_mmio.h" | |
| static int virtio_mmio_get_config(struct udevice *udev, unsigned int offset, | |
| void *buf, unsigned int len) | |
| { | |
| struct virtio_mmio_priv *priv = dev_get_priv(udev); | |
| void __iomem *base = priv->base + VIRTIO_MMIO_CONFIG; | |
| u8 b; | |
| __le16 w; | |
| __le32 l; | |
| if (priv->version == 1) { | |
| u8 *ptr = buf; | |
| int i; | |
| for (i = 0; i < len; i++) | |
| ptr[i] = readb(base + offset + i); | |
| return 0; | |
| } | |
| switch (len) { | |
| case 1: | |
| b = readb(base + offset); | |
| memcpy(buf, &b, sizeof(b)); | |
| break; | |
| case 2: | |
| w = cpu_to_le16(readw(base + offset)); | |
| memcpy(buf, &w, sizeof(w)); | |
| break; | |
| case 4: | |
| l = cpu_to_le32(readl(base + offset)); | |
| memcpy(buf, &l, sizeof(l)); | |
| break; | |
| case 8: | |
| l = cpu_to_le32(readl(base + offset)); | |
| memcpy(buf, &l, sizeof(l)); | |
| l = cpu_to_le32(readl(base + offset + sizeof(l))); | |
| memcpy(buf + sizeof(l), &l, sizeof(l)); | |
| break; | |
| default: | |
| WARN_ON(true); | |
| } | |
| return 0; | |
| } | |
| static int virtio_mmio_set_config(struct udevice *udev, unsigned int offset, | |
| const void *buf, unsigned int len) | |
| { | |
| struct virtio_mmio_priv *priv = dev_get_priv(udev); | |
| void __iomem *base = priv->base + VIRTIO_MMIO_CONFIG; | |
| u8 b; | |
| __le16 w; | |
| __le32 l; | |
| if (priv->version == 1) { | |
| const u8 *ptr = buf; | |
| int i; | |
| for (i = 0; i < len; i++) | |
| writeb(ptr[i], base + offset + i); | |
| return 0; | |
| } | |
| switch (len) { | |
| case 1: | |
| memcpy(&b, buf, sizeof(b)); | |
| writeb(b, base + offset); | |
| break; | |
| case 2: | |
| memcpy(&w, buf, sizeof(w)); | |
| writew(le16_to_cpu(w), base + offset); | |
| break; | |
| case 4: | |
| memcpy(&l, buf, sizeof(l)); | |
| writel(le32_to_cpu(l), base + offset); | |
| break; | |
| case 8: | |
| memcpy(&l, buf, sizeof(l)); | |
| writel(le32_to_cpu(l), base + offset); | |
| memcpy(&l, buf + sizeof(l), sizeof(l)); | |
| writel(le32_to_cpu(l), base + offset + sizeof(l)); | |
| break; | |
| default: | |
| WARN_ON(true); | |
| } | |
| return 0; | |
| } | |
| static int virtio_mmio_generation(struct udevice *udev, u32 *counter) | |
| { | |
| struct virtio_mmio_priv *priv = dev_get_priv(udev); | |
| if (priv->version == 1) | |
| *counter = 0; | |
| else | |
| *counter = readl(priv->base + VIRTIO_MMIO_CONFIG_GENERATION); | |
| return 0; | |
| } | |
| static int virtio_mmio_get_status(struct udevice *udev, u8 *status) | |
| { | |
| struct virtio_mmio_priv *priv = dev_get_priv(udev); | |
| *status = readl(priv->base + VIRTIO_MMIO_STATUS) & 0xff; | |
| return 0; | |
| } | |
| static int virtio_mmio_set_status(struct udevice *udev, u8 status) | |
| { | |
| struct virtio_mmio_priv *priv = dev_get_priv(udev); | |
| /* We should never be setting status to 0 */ | |
| WARN_ON(status == 0); | |
| writel(status, priv->base + VIRTIO_MMIO_STATUS); | |
| return 0; | |
| } | |
| static int virtio_mmio_reset(struct udevice *udev) | |
| { | |
| struct virtio_mmio_priv *priv = dev_get_priv(udev); | |
| /* 0 status means a reset */ | |
| writel(0, priv->base + VIRTIO_MMIO_STATUS); | |
| return 0; | |
| } | |
| static int virtio_mmio_get_features(struct udevice *udev, u64 *features) | |
| { | |
| struct virtio_mmio_priv *priv = dev_get_priv(udev); | |
| writel(1, priv->base + VIRTIO_MMIO_DEVICE_FEATURES_SEL); | |
| *features = readl(priv->base + VIRTIO_MMIO_DEVICE_FEATURES); | |
| *features <<= 32; | |
| writel(0, priv->base + VIRTIO_MMIO_DEVICE_FEATURES_SEL); | |
| *features |= readl(priv->base + VIRTIO_MMIO_DEVICE_FEATURES); | |
| return 0; | |
| } | |
| static int virtio_mmio_set_features(struct udevice *udev) | |
| { | |
| struct virtio_mmio_priv *priv = dev_get_priv(udev); | |
| struct virtio_dev_priv *uc_priv = dev_get_uclass_priv(udev); | |
| /* Make sure there is are no mixed devices */ | |
| if (priv->version == 2 && uc_priv->legacy) { | |
| debug("New virtio-mmio devices (version 2) must provide VIRTIO_F_VERSION_1 feature!\n"); | |
| return -EINVAL; | |
| } | |
| writel(1, priv->base + VIRTIO_MMIO_DRIVER_FEATURES_SEL); | |
| writel((u32)(uc_priv->features >> 32), | |
| priv->base + VIRTIO_MMIO_DRIVER_FEATURES); | |
| writel(0, priv->base + VIRTIO_MMIO_DRIVER_FEATURES_SEL); | |
| writel((u32)uc_priv->features, | |
| priv->base + VIRTIO_MMIO_DRIVER_FEATURES); | |
| return 0; | |
| } | |
| static struct virtqueue *virtio_mmio_setup_vq(struct udevice *udev, | |
| unsigned int index) | |
| { | |
| struct virtio_mmio_priv *priv = dev_get_priv(udev); | |
| struct virtqueue *vq; | |
| unsigned int num; | |
| int err; | |
| /* Select the queue we're interested in */ | |
| writel(index, priv->base + VIRTIO_MMIO_QUEUE_SEL); | |
| /* Queue shouldn't already be set up */ | |
| if (readl(priv->base + (priv->version == 1 ? | |
| VIRTIO_MMIO_QUEUE_PFN : VIRTIO_MMIO_QUEUE_READY))) { | |
| err = -ENOENT; | |
| goto error_available; | |
| } | |
| num = readl(priv->base + VIRTIO_MMIO_QUEUE_NUM_MAX); | |
| if (num == 0) { | |
| err = -ENOENT; | |
| goto error_new_virtqueue; | |
| } | |
| /* Create the vring */ | |
| vq = vring_create_virtqueue(index, num, VIRTIO_MMIO_VRING_ALIGN, udev); | |
| if (!vq) { | |
| err = -ENOMEM; | |
| goto error_new_virtqueue; | |
| } | |
| /* Activate the queue */ | |
| writel(virtqueue_get_vring_size(vq), | |
| priv->base + VIRTIO_MMIO_QUEUE_NUM); | |
| if (priv->version == 1) { | |
| u64 q_pfn = virtqueue_get_desc_addr(vq) >> PAGE_SHIFT; | |
| /* | |
| * virtio-mmio v1 uses a 32bit QUEUE PFN. If we have something | |
| * that doesn't fit in 32bit, fail the setup rather than | |
| * pretending to be successful. | |
| */ | |
| if (q_pfn >> 32) { | |
| debug("platform bug: legacy virtio-mmio must not be used with RAM above 0x%llxGB\n", | |
| 0x1ULL << (32 + PAGE_SHIFT - 30)); | |
| err = -E2BIG; | |
| goto error_bad_pfn; | |
| } | |
| writel(PAGE_SIZE, priv->base + VIRTIO_MMIO_QUEUE_ALIGN); | |
| writel(q_pfn, priv->base + VIRTIO_MMIO_QUEUE_PFN); | |
| } else { | |
| u64 addr; | |
| addr = virtqueue_get_desc_addr(vq); | |
| writel((u32)addr, priv->base + VIRTIO_MMIO_QUEUE_DESC_LOW); | |
| writel((u32)(addr >> 32), | |
| priv->base + VIRTIO_MMIO_QUEUE_DESC_HIGH); | |
| addr = virtqueue_get_avail_addr(vq); | |
| writel((u32)addr, priv->base + VIRTIO_MMIO_QUEUE_AVAIL_LOW); | |
| writel((u32)(addr >> 32), | |
| priv->base + VIRTIO_MMIO_QUEUE_AVAIL_HIGH); | |
| addr = virtqueue_get_used_addr(vq); | |
| writel((u32)addr, priv->base + VIRTIO_MMIO_QUEUE_USED_LOW); | |
| writel((u32)(addr >> 32), | |
| priv->base + VIRTIO_MMIO_QUEUE_USED_HIGH); | |
| writel(1, priv->base + VIRTIO_MMIO_QUEUE_READY); | |
| } | |
| return vq; | |
| error_bad_pfn: | |
| vring_del_virtqueue(vq); | |
| error_new_virtqueue: | |
| if (priv->version == 1) { | |
| writel(0, priv->base + VIRTIO_MMIO_QUEUE_PFN); | |
| } else { | |
| writel(0, priv->base + VIRTIO_MMIO_QUEUE_READY); | |
| WARN_ON(readl(priv->base + VIRTIO_MMIO_QUEUE_READY)); | |
| } | |
| error_available: | |
| return ERR_PTR(err); | |
| } | |
| static void virtio_mmio_del_vq(struct virtqueue *vq) | |
| { | |
| struct virtio_mmio_priv *priv = dev_get_priv(vq->vdev); | |
| unsigned int index = vq->index; | |
| /* Select and deactivate the queue */ | |
| writel(index, priv->base + VIRTIO_MMIO_QUEUE_SEL); | |
| if (priv->version == 1) { | |
| writel(0, priv->base + VIRTIO_MMIO_QUEUE_PFN); | |
| } else { | |
| writel(0, priv->base + VIRTIO_MMIO_QUEUE_READY); | |
| WARN_ON(readl(priv->base + VIRTIO_MMIO_QUEUE_READY)); | |
| } | |
| vring_del_virtqueue(vq); | |
| } | |
| static int virtio_mmio_del_vqs(struct udevice *udev) | |
| { | |
| struct virtio_dev_priv *uc_priv = dev_get_uclass_priv(udev); | |
| struct virtqueue *vq, *n; | |
| list_for_each_entry_safe(vq, n, &uc_priv->vqs, list) | |
| virtio_mmio_del_vq(vq); | |
| return 0; | |
| } | |
| static int virtio_mmio_find_vqs(struct udevice *udev, unsigned int nvqs, | |
| struct virtqueue *vqs[]) | |
| { | |
| int i; | |
| for (i = 0; i < nvqs; ++i) { | |
| vqs[i] = virtio_mmio_setup_vq(udev, i); | |
| if (IS_ERR(vqs[i])) { | |
| virtio_mmio_del_vqs(udev); | |
| return PTR_ERR(vqs[i]); | |
| } | |
| } | |
| return 0; | |
| } | |
| static int virtio_mmio_notify(struct udevice *udev, struct virtqueue *vq) | |
| { | |
| struct virtio_mmio_priv *priv = dev_get_priv(udev); | |
| /* | |
| * We write the queue's selector into the notification register | |
| * to signal the other end | |
| */ | |
| writel(vq->index, priv->base + VIRTIO_MMIO_QUEUE_NOTIFY); | |
| return 0; | |
| } | |
| static int virtio_mmio_of_to_plat(struct udevice *udev) | |
| { | |
| struct virtio_mmio_priv *priv = dev_get_priv(udev); | |
| priv->base = (void __iomem *)(ulong)dev_read_addr(udev); | |
| if (priv->base == (void __iomem *)FDT_ADDR_T_NONE) | |
| return -EINVAL; | |
| return 0; | |
| } | |
| static int virtio_mmio_probe(struct udevice *udev) | |
| { | |
| struct virtio_mmio_priv *priv = dev_get_priv(udev); | |
| struct virtio_dev_priv *uc_priv = dev_get_uclass_priv(udev); | |
| u32 magic; | |
| /* Check magic value */ | |
| magic = readl(priv->base + VIRTIO_MMIO_MAGIC_VALUE); | |
| if (magic != ('v' | 'i' << 8 | 'r' << 16 | 't' << 24)) { | |
| debug("(%s): wrong magic value 0x%08x!\n", udev->name, magic); | |
| return 0; | |
| } | |
| /* Check device version */ | |
| priv->version = readl(priv->base + VIRTIO_MMIO_VERSION); | |
| if (priv->version < 1 || priv->version > 2) { | |
| debug("(%s): version %d not supported!\n", | |
| udev->name, priv->version); | |
| return 0; | |
| } | |
| /* Check device ID */ | |
| uc_priv->device = readl(priv->base + VIRTIO_MMIO_DEVICE_ID); | |
| if (uc_priv->device == 0) { | |
| /* | |
| * virtio-mmio device with an ID 0 is a (dummy) placeholder | |
| * with no function. End probing now with no error reported. | |
| */ | |
| return 0; | |
| } | |
| uc_priv->vendor = readl(priv->base + VIRTIO_MMIO_VENDOR_ID); | |
| if (priv->version == 1) | |
| writel(PAGE_SIZE, priv->base + VIRTIO_MMIO_GUEST_PAGE_SIZE); | |
| debug("(%s): device (%d) vendor (%08x) version (%d)\n", udev->name, | |
| uc_priv->device, uc_priv->vendor, priv->version); | |
| return 0; | |
| } | |
| static const struct dm_virtio_ops virtio_mmio_ops = { | |
| .get_config = virtio_mmio_get_config, | |
| .set_config = virtio_mmio_set_config, | |
| .generation = virtio_mmio_generation, | |
| .get_status = virtio_mmio_get_status, | |
| .set_status = virtio_mmio_set_status, | |
| .reset = virtio_mmio_reset, | |
| .get_features = virtio_mmio_get_features, | |
| .set_features = virtio_mmio_set_features, | |
| .find_vqs = virtio_mmio_find_vqs, | |
| .del_vqs = virtio_mmio_del_vqs, | |
| .notify = virtio_mmio_notify, | |
| }; | |
| static const struct udevice_id virtio_mmio_ids[] = { | |
| { .compatible = "virtio,mmio" }, | |
| { } | |
| }; | |
| U_BOOT_DRIVER(virtio_mmio) = { | |
| .name = "virtio-mmio", | |
| .id = UCLASS_VIRTIO, | |
| .of_match = virtio_mmio_ids, | |
| .ops = &virtio_mmio_ops, | |
| .probe = virtio_mmio_probe, | |
| .of_to_plat = virtio_mmio_of_to_plat, | |
| .priv_auto = sizeof(struct virtio_mmio_priv), | |
| }; |