--- /dev/null
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2023, Intel Corporation.
+ * Intel Visual Sensing Controller Interface Linux driver
+ */
+
+#include <linux/align.h>
+#include <linux/cache.h>
+#include <linux/cleanup.h>
+#include <linux/iopoll.h>
+#include <linux/list.h>
+#include <linux/mei.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/overflow.h>
+#include <linux/platform_device.h>
+#include <linux/pm_runtime.h>
+#include <linux/timekeeping.h>
+#include <linux/types.h>
+
+#include <asm-generic/bug.h>
+#include <asm-generic/unaligned.h>
+
+#include "mei_dev.h"
+#include "vsc-tp.h"
+
+#define MEI_VSC_DRV_NAME               "intel_vsc"
+
+#define MEI_VSC_MAX_MSG_SIZE           512
+
+#define MEI_VSC_POLL_DELAY_US          (50 * USEC_PER_MSEC)
+#define MEI_VSC_POLL_TIMEOUT_US                (200 * USEC_PER_MSEC)
+
+#define mei_dev_to_vsc_hw(dev)         ((struct mei_vsc_hw *)((dev)->hw))
+
+struct mei_vsc_host_timestamp {
+       u64 realtime;
+       u64 boottime;
+};
+
+struct mei_vsc_hw {
+       struct vsc_tp *tp;
+
+       bool fw_ready;
+       bool host_ready;
+
+       atomic_t write_lock_cnt;
+
+       u32 rx_len;
+       u32 rx_hdr;
+
+       /* buffer for tx */
+       char tx_buf[MEI_VSC_MAX_MSG_SIZE + sizeof(struct mei_msg_hdr)] ____cacheline_aligned;
+       /* buffer for rx */
+       char rx_buf[MEI_VSC_MAX_MSG_SIZE + sizeof(struct mei_msg_hdr)] ____cacheline_aligned;
+};
+
+static int mei_vsc_read_helper(struct mei_vsc_hw *hw, u8 *buf,
+                              u32 max_len)
+{
+       struct mei_vsc_host_timestamp ts = {
+               .realtime = ktime_to_ns(ktime_get_real()),
+               .boottime = ktime_to_ns(ktime_get_boottime()),
+       };
+
+       return vsc_tp_xfer(hw->tp, VSC_TP_CMD_READ, &ts, sizeof(ts),
+                          buf, max_len);
+}
+
+static int mei_vsc_write_helper(struct mei_vsc_hw *hw, u8 *buf, u32 len)
+{
+       u8 status;
+
+       return vsc_tp_xfer(hw->tp, VSC_TP_CMD_WRITE, buf, len, &status,
+                          sizeof(status));
+}
+
+static int mei_vsc_fw_status(struct mei_device *mei_dev,
+                            struct mei_fw_status *fw_status)
+{
+       if (!fw_status)
+               return -EINVAL;
+
+       fw_status->count = 0;
+
+       return 0;
+}
+
+static inline enum mei_pg_state mei_vsc_pg_state(struct mei_device *mei_dev)
+{
+       return MEI_PG_OFF;
+}
+
+static void mei_vsc_intr_enable(struct mei_device *mei_dev)
+{
+       struct mei_vsc_hw *hw = mei_dev_to_vsc_hw(mei_dev);
+
+       vsc_tp_intr_enable(hw->tp);
+}
+
+static void mei_vsc_intr_disable(struct mei_device *mei_dev)
+{
+       struct mei_vsc_hw *hw = mei_dev_to_vsc_hw(mei_dev);
+
+       vsc_tp_intr_disable(hw->tp);
+}
+
+/* mei framework requires this ops */
+static void mei_vsc_intr_clear(struct mei_device *mei_dev)
+{
+}
+
+/* wait for pending irq handler */
+static void mei_vsc_synchronize_irq(struct mei_device *mei_dev)
+{
+       struct mei_vsc_hw *hw = mei_dev_to_vsc_hw(mei_dev);
+
+       vsc_tp_intr_synchronize(hw->tp);
+}
+
+static int mei_vsc_hw_config(struct mei_device *mei_dev)
+{
+       return 0;
+}
+
+static bool mei_vsc_host_is_ready(struct mei_device *mei_dev)
+{
+       struct mei_vsc_hw *hw = mei_dev_to_vsc_hw(mei_dev);
+
+       return hw->host_ready;
+}
+
+static bool mei_vsc_hw_is_ready(struct mei_device *mei_dev)
+{
+       struct mei_vsc_hw *hw = mei_dev_to_vsc_hw(mei_dev);
+
+       return hw->fw_ready;
+}
+
+static int mei_vsc_hw_start(struct mei_device *mei_dev)
+{
+       struct mei_vsc_hw *hw = mei_dev_to_vsc_hw(mei_dev);
+       int ret, rlen;
+       u8 buf;
+
+       hw->host_ready = true;
+
+       vsc_tp_intr_enable(hw->tp);
+
+       ret = read_poll_timeout(mei_vsc_read_helper, rlen,
+                               rlen >= 0, MEI_VSC_POLL_DELAY_US,
+                               MEI_VSC_POLL_TIMEOUT_US, true,
+                               hw, &buf, sizeof(buf));
+       if (ret) {
+               dev_err(mei_dev->dev, "wait fw ready failed: %d\n", ret);
+               return ret;
+       }
+
+       hw->fw_ready = true;
+
+       return 0;
+}
+
+static bool mei_vsc_hbuf_is_ready(struct mei_device *mei_dev)
+{
+       struct mei_vsc_hw *hw = mei_dev_to_vsc_hw(mei_dev);
+
+       return atomic_read(&hw->write_lock_cnt) == 0;
+}
+
+static int mei_vsc_hbuf_empty_slots(struct mei_device *mei_dev)
+{
+       return MEI_VSC_MAX_MSG_SIZE / MEI_SLOT_SIZE;
+}
+
+static u32 mei_vsc_hbuf_depth(const struct mei_device *mei_dev)
+{
+       return MEI_VSC_MAX_MSG_SIZE / MEI_SLOT_SIZE;
+}
+
+static int mei_vsc_write(struct mei_device *mei_dev,
+                        const void *hdr, size_t hdr_len,
+                        const void *data, size_t data_len)
+{
+       struct mei_vsc_hw *hw = mei_dev_to_vsc_hw(mei_dev);
+       char *buf = hw->tx_buf;
+       int ret;
+
+       if (WARN_ON(!hdr || !IS_ALIGNED(hdr_len, 4)))
+               return -EINVAL;
+
+       if (!data || data_len > MEI_VSC_MAX_MSG_SIZE)
+               return -EINVAL;
+
+       atomic_inc(&hw->write_lock_cnt);
+
+       memcpy(buf, hdr, hdr_len);
+       memcpy(buf + hdr_len, data, data_len);
+
+       ret = mei_vsc_write_helper(hw, buf, hdr_len + data_len);
+
+       atomic_dec_if_positive(&hw->write_lock_cnt);
+
+       return ret < 0 ? ret : 0;
+}
+
+static inline u32 mei_vsc_read(const struct mei_device *mei_dev)
+{
+       struct mei_vsc_hw *hw = mei_dev_to_vsc_hw(mei_dev);
+       int ret;
+
+       ret = mei_vsc_read_helper(hw, hw->rx_buf, sizeof(hw->rx_buf));
+       if (ret < 0 || ret < sizeof(u32))
+               return 0;
+       hw->rx_len = ret;
+
+       hw->rx_hdr = get_unaligned_le32(hw->rx_buf);
+
+       return hw->rx_hdr;
+}
+
+static int mei_vsc_count_full_read_slots(struct mei_device *mei_dev)
+{
+       return MEI_VSC_MAX_MSG_SIZE / MEI_SLOT_SIZE;
+}
+
+static int mei_vsc_read_slots(struct mei_device *mei_dev, unsigned char *buf,
+                             unsigned long len)
+{
+       struct mei_vsc_hw *hw = mei_dev_to_vsc_hw(mei_dev);
+       struct mei_msg_hdr *hdr;
+
+       hdr = (struct mei_msg_hdr *)&hw->rx_hdr;
+       if (len != hdr->length || hdr->length + sizeof(*hdr) != hw->rx_len)
+               return -EINVAL;
+
+       memcpy(buf, hw->rx_buf + sizeof(*hdr), len);
+
+       return 0;
+}
+
+static bool mei_vsc_pg_in_transition(struct mei_device *mei_dev)
+{
+       return mei_dev->pg_event >= MEI_PG_EVENT_WAIT &&
+              mei_dev->pg_event <= MEI_PG_EVENT_INTR_WAIT;
+}
+
+static bool mei_vsc_pg_is_enabled(struct mei_device *mei_dev)
+{
+       return false;
+}
+
+static int mei_vsc_hw_reset(struct mei_device *mei_dev, bool intr_enable)
+{
+       struct mei_vsc_hw *hw = mei_dev_to_vsc_hw(mei_dev);
+
+       vsc_tp_reset(hw->tp);
+
+       vsc_tp_intr_disable(hw->tp);
+
+       return vsc_tp_init(hw->tp, mei_dev->dev);
+}
+
+static const struct mei_hw_ops mei_vsc_hw_ops = {
+       .fw_status = mei_vsc_fw_status,
+       .pg_state = mei_vsc_pg_state,
+
+       .host_is_ready = mei_vsc_host_is_ready,
+       .hw_is_ready = mei_vsc_hw_is_ready,
+       .hw_reset = mei_vsc_hw_reset,
+       .hw_config = mei_vsc_hw_config,
+       .hw_start = mei_vsc_hw_start,
+
+       .pg_in_transition = mei_vsc_pg_in_transition,
+       .pg_is_enabled = mei_vsc_pg_is_enabled,
+
+       .intr_clear = mei_vsc_intr_clear,
+       .intr_enable = mei_vsc_intr_enable,
+       .intr_disable = mei_vsc_intr_disable,
+       .synchronize_irq = mei_vsc_synchronize_irq,
+
+       .hbuf_free_slots = mei_vsc_hbuf_empty_slots,
+       .hbuf_is_ready = mei_vsc_hbuf_is_ready,
+       .hbuf_depth = mei_vsc_hbuf_depth,
+       .write = mei_vsc_write,
+
+       .rdbuf_full_slots = mei_vsc_count_full_read_slots,
+       .read_hdr = mei_vsc_read,
+       .read = mei_vsc_read_slots,
+};
+
+static void mei_vsc_event_cb(void *context)
+{
+       struct mei_device *mei_dev = context;
+       struct mei_vsc_hw *hw = mei_dev_to_vsc_hw(mei_dev);
+       struct list_head cmpl_list;
+       s32 slots;
+       int ret;
+
+       if (mei_dev->dev_state == MEI_DEV_RESETTING ||
+           mei_dev->dev_state == MEI_DEV_INITIALIZING)
+               return;
+
+       INIT_LIST_HEAD(&cmpl_list);
+
+       guard(mutex)(&mei_dev->device_lock);
+
+       while (vsc_tp_need_read(hw->tp)) {
+               /* check slots available for reading */
+               slots = mei_count_full_read_slots(mei_dev);
+
+               ret = mei_irq_read_handler(mei_dev, &cmpl_list, &slots);
+               if (ret) {
+                       if (ret != -ENODATA) {
+                               if (mei_dev->dev_state != MEI_DEV_RESETTING &&
+                                   mei_dev->dev_state != MEI_DEV_POWER_DOWN)
+                                       schedule_work(&mei_dev->reset_work);
+                       }
+
+                       return;
+               }
+       }
+
+       mei_dev->hbuf_is_ready = mei_hbuf_is_ready(mei_dev);
+       ret = mei_irq_write_handler(mei_dev, &cmpl_list);
+       if (ret)
+               dev_err(mei_dev->dev, "dispatch write request failed: %d\n", ret);
+
+       mei_dev->hbuf_is_ready = mei_hbuf_is_ready(mei_dev);
+       mei_irq_compl_handler(mei_dev, &cmpl_list);
+}
+
+static int mei_vsc_probe(struct platform_device *pdev)
+{
+       struct device *dev = &pdev->dev;
+       struct mei_device *mei_dev;
+       struct mei_vsc_hw *hw;
+       struct vsc_tp *tp;
+       int ret;
+
+       tp = *(struct vsc_tp **)dev_get_platdata(dev);
+       if (!tp)
+               return dev_err_probe(dev, -ENODEV, "no platform data\n");
+
+       mei_dev = devm_kzalloc(dev, size_add(sizeof(*mei_dev), sizeof(*hw)),
+                              GFP_KERNEL);
+       if (!mei_dev)
+               return -ENOMEM;
+
+       mei_device_init(mei_dev, dev, false, &mei_vsc_hw_ops);
+       mei_dev->fw_f_fw_ver_supported = 0;
+       mei_dev->kind = "ivsc";
+
+       hw = mei_dev_to_vsc_hw(mei_dev);
+       atomic_set(&hw->write_lock_cnt, 0);
+       hw->tp = tp;
+
+       platform_set_drvdata(pdev, mei_dev);
+
+       vsc_tp_register_event_cb(tp, mei_vsc_event_cb, mei_dev);
+
+       ret = mei_start(mei_dev);
+       if (ret) {
+               dev_err_probe(dev, ret, "init hw failed\n");
+               goto err_cancel;
+       }
+
+       ret = mei_register(mei_dev, dev);
+       if (ret)
+               goto err_stop;
+
+       pm_runtime_enable(mei_dev->dev);
+
+       return 0;
+
+err_stop:
+       mei_stop(mei_dev);
+
+err_cancel:
+       mei_cancel_work(mei_dev);
+
+       mei_disable_interrupts(mei_dev);
+
+       return ret;
+}
+
+static int mei_vsc_remove(struct platform_device *pdev)
+{
+       struct mei_device *mei_dev = platform_get_drvdata(pdev);
+
+       pm_runtime_disable(mei_dev->dev);
+
+       mei_stop(mei_dev);
+
+       mei_disable_interrupts(mei_dev);
+
+       mei_deregister(mei_dev);
+
+       return 0;
+}
+
+static int mei_vsc_suspend(struct device *dev)
+{
+       struct mei_device *mei_dev = dev_get_drvdata(dev);
+
+       mei_stop(mei_dev);
+
+       return 0;
+}
+
+static int mei_vsc_resume(struct device *dev)
+{
+       struct mei_device *mei_dev = dev_get_drvdata(dev);
+       int ret;
+
+       ret = mei_restart(mei_dev);
+       if (ret)
+               return ret;
+
+       /* start timer if stopped in suspend */
+       schedule_delayed_work(&mei_dev->timer_work, HZ);
+
+       return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(mei_vsc_pm_ops, mei_vsc_suspend, mei_vsc_resume);
+
+static const struct platform_device_id mei_vsc_id_table[] = {
+       { MEI_VSC_DRV_NAME },
+       { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(platform, mei_vsc_id_table);
+
+static struct platform_driver mei_vsc_drv = {
+       .probe = mei_vsc_probe,
+       .remove = mei_vsc_remove,
+       .id_table = mei_vsc_id_table,
+       .driver = {
+               .name = MEI_VSC_DRV_NAME,
+               .pm = &mei_vsc_pm_ops,
+               .probe_type = PROBE_PREFER_ASYNCHRONOUS,
+       },
+};
+module_platform_driver(mei_vsc_drv);
+
+MODULE_AUTHOR("Wentong Wu <wentong.wu@intel.com>");
+MODULE_AUTHOR("Zhifeng Wang <zhifeng.wang@intel.com>");
+MODULE_DESCRIPTION("Intel Visual Sensing Controller Interface");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS(VSC_TP);