WSL2-Linux-Kernel/drivers/misc/mei/hw-virtio.c

875 строки
20 KiB
C

// SPDX-License-Identifier: GPL-2.0
/*
* Intel Management Engine Interface (Intel MEI) Linux driver
* Copyright (c) 2018-2020, Intel Corporation.
*/
#include <linux/err.h>
#include <linux/module.h>
#include <linux/pm_runtime.h>
#include <linux/scatterlist.h>
#include <linux/spinlock.h>
#include <linux/slab.h>
#include <linux/virtio.h>
#include <linux/virtio_config.h>
#include <linux/virtio_ids.h>
#include <linux/atomic.h>
#include "mei_dev.h"
#include "hbm.h"
#include "client.h"
#define MEI_VIRTIO_RPM_TIMEOUT 500
/* ACRN virtio device types */
#ifndef VIRTIO_ID_MEI
#define VIRTIO_ID_MEI 0xFFFE /* virtio mei */
#endif
/**
* struct mei_virtio_cfg - settings passed from the virtio backend
* @buf_depth: read buffer depth in slots (4bytes)
* @hw_ready: hw is ready for operation
* @host_reset: synchronize reset with virtio backend
* @reserved: reserved for alignment
* @fw_status: FW status
*/
struct mei_virtio_cfg {
u32 buf_depth;
u8 hw_ready;
u8 host_reset;
u8 reserved[2];
u32 fw_status[MEI_FW_STATUS_MAX];
} __packed;
struct mei_virtio_hw {
struct mei_device mdev;
char name[32];
struct virtqueue *in;
struct virtqueue *out;
bool host_ready;
struct work_struct intr_handler;
u32 *recv_buf;
u8 recv_rdy;
size_t recv_sz;
u32 recv_idx;
u32 recv_len;
/* send buffer */
atomic_t hbuf_ready;
const void *send_hdr;
const void *send_buf;
struct mei_virtio_cfg cfg;
};
#define to_virtio_hw(_dev) container_of(_dev, struct mei_virtio_hw, mdev)
/**
* mei_virtio_fw_status() - read status register of mei
* @dev: mei device
* @fw_status: fw status register values
*
* Return: always 0
*/
static int mei_virtio_fw_status(struct mei_device *dev,
struct mei_fw_status *fw_status)
{
struct virtio_device *vdev = dev_to_virtio(dev->dev);
fw_status->count = MEI_FW_STATUS_MAX;
virtio_cread_bytes(vdev, offsetof(struct mei_virtio_cfg, fw_status),
fw_status->status, sizeof(fw_status->status));
return 0;
}
/**
* mei_virtio_pg_state() - translate internal pg state
* to the mei power gating state
* There is no power management in ACRN mode always return OFF
* @dev: mei device
*
* Return:
* * MEI_PG_OFF - if aliveness is on (always)
* * MEI_PG_ON - (never)
*/
static inline enum mei_pg_state mei_virtio_pg_state(struct mei_device *dev)
{
return MEI_PG_OFF;
}
/**
* mei_virtio_hw_config() - configure hw dependent settings
*
* @dev: mei device
*
* Return: always 0
*/
static int mei_virtio_hw_config(struct mei_device *dev)
{
return 0;
}
/**
* mei_virtio_hbuf_empty_slots() - counts write empty slots.
* @dev: the device structure
*
* Return: always return frontend buf size if buffer is ready, 0 otherwise
*/
static int mei_virtio_hbuf_empty_slots(struct mei_device *dev)
{
struct mei_virtio_hw *hw = to_virtio_hw(dev);
return (atomic_read(&hw->hbuf_ready) == 1) ? hw->cfg.buf_depth : 0;
}
/**
* mei_virtio_hbuf_is_ready() - checks if write buffer is ready
* @dev: the device structure
*
* Return: true if hbuf is ready
*/
static bool mei_virtio_hbuf_is_ready(struct mei_device *dev)
{
struct mei_virtio_hw *hw = to_virtio_hw(dev);
return atomic_read(&hw->hbuf_ready) == 1;
}
/**
* mei_virtio_hbuf_max_depth() - returns depth of FE write buffer.
* @dev: the device structure
*
* Return: size of frontend write buffer in bytes
*/
static u32 mei_virtio_hbuf_depth(const struct mei_device *dev)
{
struct mei_virtio_hw *hw = to_virtio_hw(dev);
return hw->cfg.buf_depth;
}
/**
* mei_virtio_intr_clear() - clear and stop interrupts
* @dev: the device structure
*/
static void mei_virtio_intr_clear(struct mei_device *dev)
{
/*
* In our virtio solution, there are two types of interrupts,
* vq interrupt and config change interrupt.
* 1) start/reset rely on virtio config changed interrupt;
* 2) send/recv rely on virtio virtqueue interrupts.
* They are all virtual interrupts. So, we don't have corresponding
* operation to do here.
*/
}
/**
* mei_virtio_intr_enable() - enables mei BE virtqueues callbacks
* @dev: the device structure
*/
static void mei_virtio_intr_enable(struct mei_device *dev)
{
struct mei_virtio_hw *hw = to_virtio_hw(dev);
struct virtio_device *vdev = dev_to_virtio(dev->dev);
virtio_config_enable(vdev);
virtqueue_enable_cb(hw->in);
virtqueue_enable_cb(hw->out);
}
/**
* mei_virtio_intr_disable() - disables mei BE virtqueues callbacks
*
* @dev: the device structure
*/
static void mei_virtio_intr_disable(struct mei_device *dev)
{
struct mei_virtio_hw *hw = to_virtio_hw(dev);
struct virtio_device *vdev = dev_to_virtio(dev->dev);
virtio_config_disable(vdev);
virtqueue_disable_cb(hw->in);
virtqueue_disable_cb(hw->out);
}
/**
* mei_virtio_synchronize_irq() - wait for pending IRQ handlers for all
* virtqueue
* @dev: the device structure
*/
static void mei_virtio_synchronize_irq(struct mei_device *dev)
{
struct mei_virtio_hw *hw = to_virtio_hw(dev);
/*
* Now, all IRQ handlers are converted to workqueue.
* Change synchronize irq to flush this work.
*/
flush_work(&hw->intr_handler);
}
static void mei_virtio_free_outbufs(struct mei_virtio_hw *hw)
{
kfree(hw->send_hdr);
kfree(hw->send_buf);
hw->send_hdr = NULL;
hw->send_buf = NULL;
}
/**
* mei_virtio_write_message() - writes a message to mei virtio back-end service.
* @dev: the device structure
* @hdr: mei header of message
* @hdr_len: header length
* @data: message payload will be written
* @data_len: message payload length
*
* Return:
* * 0: on success
* * -EIO: if write has failed
* * -ENOMEM: on memory allocation failure
*/
static int mei_virtio_write_message(struct mei_device *dev,
const void *hdr, size_t hdr_len,
const void *data, size_t data_len)
{
struct mei_virtio_hw *hw = to_virtio_hw(dev);
struct scatterlist sg[2];
const void *hbuf, *dbuf;
int ret;
if (WARN_ON(!atomic_add_unless(&hw->hbuf_ready, -1, 0)))
return -EIO;
hbuf = kmemdup(hdr, hdr_len, GFP_KERNEL);
hw->send_hdr = hbuf;
dbuf = kmemdup(data, data_len, GFP_KERNEL);
hw->send_buf = dbuf;
if (!hbuf || !dbuf) {
ret = -ENOMEM;
goto fail;
}
sg_init_table(sg, 2);
sg_set_buf(&sg[0], hbuf, hdr_len);
sg_set_buf(&sg[1], dbuf, data_len);
ret = virtqueue_add_outbuf(hw->out, sg, 2, hw, GFP_KERNEL);
if (ret) {
dev_err(dev->dev, "failed to add outbuf\n");
goto fail;
}
virtqueue_kick(hw->out);
return 0;
fail:
mei_virtio_free_outbufs(hw);
return ret;
}
/**
* mei_virtio_count_full_read_slots() - counts read full slots.
* @dev: the device structure
*
* Return: -EOVERFLOW if overflow, otherwise filled slots count
*/
static int mei_virtio_count_full_read_slots(struct mei_device *dev)
{
struct mei_virtio_hw *hw = to_virtio_hw(dev);
if (hw->recv_idx > hw->recv_len)
return -EOVERFLOW;
return hw->recv_len - hw->recv_idx;
}
/**
* mei_virtio_read_hdr() - Reads 32bit dword from mei virtio receive buffer
*
* @dev: the device structure
*
* Return: 32bit dword of receive buffer (u32)
*/
static inline u32 mei_virtio_read_hdr(const struct mei_device *dev)
{
struct mei_virtio_hw *hw = to_virtio_hw(dev);
WARN_ON(hw->cfg.buf_depth < hw->recv_idx + 1);
return hw->recv_buf[hw->recv_idx++];
}
static int mei_virtio_read(struct mei_device *dev, unsigned char *buffer,
unsigned long len)
{
struct mei_virtio_hw *hw = to_virtio_hw(dev);
u32 slots = mei_data2slots(len);
if (WARN_ON(hw->cfg.buf_depth < hw->recv_idx + slots))
return -EOVERFLOW;
/*
* Assumption: There is only one MEI message in recv_buf each time.
* Backend service need follow this rule too.
*/
memcpy(buffer, hw->recv_buf + hw->recv_idx, len);
hw->recv_idx += slots;
return 0;
}
static bool mei_virtio_pg_is_enabled(struct mei_device *dev)
{
return false;
}
static bool mei_virtio_pg_in_transition(struct mei_device *dev)
{
return false;
}
static void mei_virtio_add_recv_buf(struct mei_virtio_hw *hw)
{
struct scatterlist sg;
if (hw->recv_rdy) /* not needed */
return;
/* refill the recv_buf to IN virtqueue to get next message */
sg_init_one(&sg, hw->recv_buf, mei_slots2data(hw->cfg.buf_depth));
hw->recv_len = 0;
hw->recv_idx = 0;
hw->recv_rdy = 1;
virtqueue_add_inbuf(hw->in, &sg, 1, hw->recv_buf, GFP_KERNEL);
virtqueue_kick(hw->in);
}
/**
* mei_virtio_hw_is_ready() - check whether the BE(hw) has turned ready
* @dev: mei device
* Return: bool
*/
static bool mei_virtio_hw_is_ready(struct mei_device *dev)
{
struct mei_virtio_hw *hw = to_virtio_hw(dev);
struct virtio_device *vdev = dev_to_virtio(dev->dev);
virtio_cread(vdev, struct mei_virtio_cfg,
hw_ready, &hw->cfg.hw_ready);
dev_dbg(dev->dev, "hw ready %d\n", hw->cfg.hw_ready);
return hw->cfg.hw_ready;
}
/**
* mei_virtio_hw_reset - resets virtio hw.
*
* @dev: the device structure
* @intr_enable: virtio use data/config callbacks
*
* Return: 0 on success an error code otherwise
*/
static int mei_virtio_hw_reset(struct mei_device *dev, bool intr_enable)
{
struct mei_virtio_hw *hw = to_virtio_hw(dev);
struct virtio_device *vdev = dev_to_virtio(dev->dev);
dev_dbg(dev->dev, "hw reset\n");
dev->recvd_hw_ready = false;
hw->host_ready = false;
atomic_set(&hw->hbuf_ready, 0);
hw->recv_len = 0;
hw->recv_idx = 0;
hw->cfg.host_reset = 1;
virtio_cwrite(vdev, struct mei_virtio_cfg,
host_reset, &hw->cfg.host_reset);
mei_virtio_hw_is_ready(dev);
if (intr_enable)
mei_virtio_intr_enable(dev);
return 0;
}
/**
* mei_virtio_hw_reset_release() - release device from the reset
* @dev: the device structure
*/
static void mei_virtio_hw_reset_release(struct mei_device *dev)
{
struct mei_virtio_hw *hw = to_virtio_hw(dev);
struct virtio_device *vdev = dev_to_virtio(dev->dev);
dev_dbg(dev->dev, "hw reset release\n");
hw->cfg.host_reset = 0;
virtio_cwrite(vdev, struct mei_virtio_cfg,
host_reset, &hw->cfg.host_reset);
}
/**
* mei_virtio_hw_ready_wait() - wait until the virtio(hw) has turned ready
* or timeout is reached
* @dev: mei device
*
* Return: 0 on success, error otherwise
*/
static int mei_virtio_hw_ready_wait(struct mei_device *dev)
{
mutex_unlock(&dev->device_lock);
wait_event_timeout(dev->wait_hw_ready,
dev->recvd_hw_ready,
mei_secs_to_jiffies(MEI_HW_READY_TIMEOUT));
mutex_lock(&dev->device_lock);
if (!dev->recvd_hw_ready) {
dev_err(dev->dev, "wait hw ready failed\n");
return -ETIMEDOUT;
}
dev->recvd_hw_ready = false;
return 0;
}
/**
* mei_virtio_hw_start() - hw start routine
* @dev: mei device
*
* Return: 0 on success, error otherwise
*/
static int mei_virtio_hw_start(struct mei_device *dev)
{
struct mei_virtio_hw *hw = to_virtio_hw(dev);
int ret;
dev_dbg(dev->dev, "hw start\n");
mei_virtio_hw_reset_release(dev);
ret = mei_virtio_hw_ready_wait(dev);
if (ret)
return ret;
mei_virtio_add_recv_buf(hw);
atomic_set(&hw->hbuf_ready, 1);
dev_dbg(dev->dev, "hw is ready\n");
hw->host_ready = true;
return 0;
}
/**
* mei_virtio_host_is_ready() - check whether the FE has turned ready
* @dev: mei device
*
* Return: bool
*/
static bool mei_virtio_host_is_ready(struct mei_device *dev)
{
struct mei_virtio_hw *hw = to_virtio_hw(dev);
dev_dbg(dev->dev, "host ready %d\n", hw->host_ready);
return hw->host_ready;
}
/**
* mei_virtio_data_in() - The callback of recv virtqueue of virtio mei
* @vq: receiving virtqueue
*/
static void mei_virtio_data_in(struct virtqueue *vq)
{
struct mei_virtio_hw *hw = vq->vdev->priv;
/* disable interrupts (enabled again from in the interrupt worker) */
virtqueue_disable_cb(hw->in);
schedule_work(&hw->intr_handler);
}
/**
* mei_virtio_data_out() - The callback of send virtqueue of virtio mei
* @vq: transmitting virtqueue
*/
static void mei_virtio_data_out(struct virtqueue *vq)
{
struct mei_virtio_hw *hw = vq->vdev->priv;
schedule_work(&hw->intr_handler);
}
static void mei_virtio_intr_handler(struct work_struct *work)
{
struct mei_virtio_hw *hw =
container_of(work, struct mei_virtio_hw, intr_handler);
struct mei_device *dev = &hw->mdev;
LIST_HEAD(complete_list);
s32 slots;
int rets = 0;
void *data;
unsigned int len;
mutex_lock(&dev->device_lock);
if (dev->dev_state == MEI_DEV_DISABLED) {
dev_warn(dev->dev, "Interrupt in disabled state.\n");
mei_virtio_intr_disable(dev);
goto end;
}
/* check if ME wants a reset */
if (!mei_hw_is_ready(dev) && dev->dev_state != MEI_DEV_RESETTING) {
dev_warn(dev->dev, "BE service not ready: resetting.\n");
schedule_work(&dev->reset_work);
goto end;
}
/* check if we need to start the dev */
if (!mei_host_is_ready(dev)) {
if (mei_hw_is_ready(dev)) {
dev_dbg(dev->dev, "we need to start the dev.\n");
dev->recvd_hw_ready = true;
wake_up(&dev->wait_hw_ready);
} else {
dev_warn(dev->dev, "Spurious Interrupt\n");
}
goto end;
}
/* read */
if (hw->recv_rdy) {
data = virtqueue_get_buf(hw->in, &len);
if (!data || !len) {
dev_dbg(dev->dev, "No data %d", len);
} else {
dev_dbg(dev->dev, "data_in %d\n", len);
WARN_ON(data != hw->recv_buf);
hw->recv_len = mei_data2slots(len);
hw->recv_rdy = 0;
}
}
/* write */
if (!atomic_read(&hw->hbuf_ready)) {
if (!virtqueue_get_buf(hw->out, &len)) {
dev_warn(dev->dev, "Failed to getbuf\n");
} else {
mei_virtio_free_outbufs(hw);
atomic_inc(&hw->hbuf_ready);
}
}
/* check slots available for reading */
slots = mei_count_full_read_slots(dev);
while (slots > 0) {
dev_dbg(dev->dev, "slots to read = %08x\n", slots);
rets = mei_irq_read_handler(dev, &complete_list, &slots);
if (rets &&
(dev->dev_state != MEI_DEV_RESETTING &&
dev->dev_state != MEI_DEV_POWER_DOWN)) {
dev_err(dev->dev, "mei_irq_read_handler ret = %d.\n",
rets);
schedule_work(&dev->reset_work);
goto end;
}
}
dev->hbuf_is_ready = mei_hbuf_is_ready(dev);
mei_irq_write_handler(dev, &complete_list);
dev->hbuf_is_ready = mei_hbuf_is_ready(dev);
mei_irq_compl_handler(dev, &complete_list);
mei_virtio_add_recv_buf(hw);
end:
if (dev->dev_state != MEI_DEV_DISABLED) {
if (!virtqueue_enable_cb(hw->in))
schedule_work(&hw->intr_handler);
}
mutex_unlock(&dev->device_lock);
}
static void mei_virtio_config_changed(struct virtio_device *vdev)
{
struct mei_virtio_hw *hw = vdev->priv;
struct mei_device *dev = &hw->mdev;
virtio_cread(vdev, struct mei_virtio_cfg,
hw_ready, &hw->cfg.hw_ready);
if (dev->dev_state == MEI_DEV_DISABLED) {
dev_dbg(dev->dev, "disabled state don't start\n");
return;
}
/* Run intr handler once to handle reset notify */
schedule_work(&hw->intr_handler);
}
static void mei_virtio_remove_vqs(struct virtio_device *vdev)
{
struct mei_virtio_hw *hw = vdev->priv;
virtqueue_detach_unused_buf(hw->in);
hw->recv_len = 0;
hw->recv_idx = 0;
hw->recv_rdy = 0;
virtqueue_detach_unused_buf(hw->out);
mei_virtio_free_outbufs(hw);
vdev->config->del_vqs(vdev);
}
/*
* There are two virtqueues, one is for send and another is for recv.
*/
static int mei_virtio_init_vqs(struct mei_virtio_hw *hw,
struct virtio_device *vdev)
{
struct virtqueue *vqs[2];
vq_callback_t *cbs[] = {
mei_virtio_data_in,
mei_virtio_data_out,
};
static const char * const names[] = {
"in",
"out",
};
int ret;
ret = virtio_find_vqs(vdev, 2, vqs, cbs, names, NULL);
if (ret)
return ret;
hw->in = vqs[0];
hw->out = vqs[1];
return 0;
}
static const struct mei_hw_ops mei_virtio_ops = {
.fw_status = mei_virtio_fw_status,
.pg_state = mei_virtio_pg_state,
.host_is_ready = mei_virtio_host_is_ready,
.hw_is_ready = mei_virtio_hw_is_ready,
.hw_reset = mei_virtio_hw_reset,
.hw_config = mei_virtio_hw_config,
.hw_start = mei_virtio_hw_start,
.pg_in_transition = mei_virtio_pg_in_transition,
.pg_is_enabled = mei_virtio_pg_is_enabled,
.intr_clear = mei_virtio_intr_clear,
.intr_enable = mei_virtio_intr_enable,
.intr_disable = mei_virtio_intr_disable,
.synchronize_irq = mei_virtio_synchronize_irq,
.hbuf_free_slots = mei_virtio_hbuf_empty_slots,
.hbuf_is_ready = mei_virtio_hbuf_is_ready,
.hbuf_depth = mei_virtio_hbuf_depth,
.write = mei_virtio_write_message,
.rdbuf_full_slots = mei_virtio_count_full_read_slots,
.read_hdr = mei_virtio_read_hdr,
.read = mei_virtio_read,
};
static int mei_virtio_probe(struct virtio_device *vdev)
{
struct mei_virtio_hw *hw;
int ret;
hw = devm_kzalloc(&vdev->dev, sizeof(*hw), GFP_KERNEL);
if (!hw)
return -ENOMEM;
vdev->priv = hw;
INIT_WORK(&hw->intr_handler, mei_virtio_intr_handler);
ret = mei_virtio_init_vqs(hw, vdev);
if (ret)
goto vqs_failed;
virtio_cread(vdev, struct mei_virtio_cfg,
buf_depth, &hw->cfg.buf_depth);
hw->recv_buf = kzalloc(mei_slots2data(hw->cfg.buf_depth), GFP_KERNEL);
if (!hw->recv_buf) {
ret = -ENOMEM;
goto hbuf_failed;
}
atomic_set(&hw->hbuf_ready, 0);
virtio_device_ready(vdev);
mei_device_init(&hw->mdev, &vdev->dev, &mei_virtio_ops);
pm_runtime_get_noresume(&vdev->dev);
pm_runtime_set_active(&vdev->dev);
pm_runtime_enable(&vdev->dev);
ret = mei_start(&hw->mdev);
if (ret)
goto mei_start_failed;
pm_runtime_set_autosuspend_delay(&vdev->dev, MEI_VIRTIO_RPM_TIMEOUT);
pm_runtime_use_autosuspend(&vdev->dev);
ret = mei_register(&hw->mdev, &vdev->dev);
if (ret)
goto mei_failed;
pm_runtime_put(&vdev->dev);
return 0;
mei_failed:
mei_stop(&hw->mdev);
mei_start_failed:
mei_cancel_work(&hw->mdev);
mei_disable_interrupts(&hw->mdev);
kfree(hw->recv_buf);
hbuf_failed:
vdev->config->del_vqs(vdev);
vqs_failed:
return ret;
}
static int __maybe_unused mei_virtio_pm_runtime_idle(struct device *device)
{
struct virtio_device *vdev = dev_to_virtio(device);
struct mei_virtio_hw *hw = vdev->priv;
dev_dbg(&vdev->dev, "rpm: mei_virtio : runtime_idle\n");
if (!hw)
return -ENODEV;
if (mei_write_is_idle(&hw->mdev))
pm_runtime_autosuspend(device);
return -EBUSY;
}
static int __maybe_unused mei_virtio_pm_runtime_suspend(struct device *device)
{
return 0;
}
static int __maybe_unused mei_virtio_pm_runtime_resume(struct device *device)
{
return 0;
}
static int __maybe_unused mei_virtio_freeze(struct virtio_device *vdev)
{
struct mei_virtio_hw *hw = vdev->priv;
dev_dbg(&vdev->dev, "freeze\n");
if (!hw)
return -ENODEV;
mei_stop(&hw->mdev);
mei_disable_interrupts(&hw->mdev);
cancel_work_sync(&hw->intr_handler);
vdev->config->reset(vdev);
mei_virtio_remove_vqs(vdev);
return 0;
}
static int __maybe_unused mei_virtio_restore(struct virtio_device *vdev)
{
struct mei_virtio_hw *hw = vdev->priv;
int ret;
dev_dbg(&vdev->dev, "restore\n");
if (!hw)
return -ENODEV;
ret = mei_virtio_init_vqs(hw, vdev);
if (ret)
return ret;
virtio_device_ready(vdev);
ret = mei_restart(&hw->mdev);
if (ret)
return ret;
/* Start timer if stopped in suspend */
schedule_delayed_work(&hw->mdev.timer_work, HZ);
return 0;
}
static const struct dev_pm_ops mei_virtio_pm_ops = {
SET_RUNTIME_PM_OPS(mei_virtio_pm_runtime_suspend,
mei_virtio_pm_runtime_resume,
mei_virtio_pm_runtime_idle)
};
static void mei_virtio_remove(struct virtio_device *vdev)
{
struct mei_virtio_hw *hw = vdev->priv;
mei_stop(&hw->mdev);
mei_disable_interrupts(&hw->mdev);
cancel_work_sync(&hw->intr_handler);
mei_deregister(&hw->mdev);
vdev->config->reset(vdev);
mei_virtio_remove_vqs(vdev);
kfree(hw->recv_buf);
pm_runtime_disable(&vdev->dev);
}
static struct virtio_device_id id_table[] = {
{ VIRTIO_ID_MEI, VIRTIO_DEV_ANY_ID },
{ }
};
static struct virtio_driver mei_virtio_driver = {
.id_table = id_table,
.probe = mei_virtio_probe,
.remove = mei_virtio_remove,
.config_changed = mei_virtio_config_changed,
.driver = {
.name = KBUILD_MODNAME,
.owner = THIS_MODULE,
.pm = &mei_virtio_pm_ops,
},
#ifdef CONFIG_PM_SLEEP
.freeze = mei_virtio_freeze,
.restore = mei_virtio_restore,
#endif
};
module_virtio_driver(mei_virtio_driver);
MODULE_DEVICE_TABLE(virtio, id_table);
MODULE_DESCRIPTION("Virtio MEI frontend driver");
MODULE_LICENSE("GPL v2");