--- /dev/null
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (c) 2023 Qualcomm Innovation Center, Inc. All rights reserved. */
+
+#include <linux/io.h>
+#include <linux/kernel.h>
+#include <linux/math64.h>
+#include <linux/mhi.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/time64.h>
+#include <linux/timer.h>
+
+#include "qaic.h"
+#include "qaic_timesync.h"
+
+#define QTIMER_REG_OFFSET                      0xa28
+#define QAIC_TIMESYNC_SIGNATURE                        0x55aa
+#define QAIC_CONV_QTIMER_TO_US(qtimer)         (mul_u64_u32_div(qtimer, 10, 192))
+
+static unsigned int timesync_delay_ms = 1000; /* 1 sec default */
+module_param(timesync_delay_ms, uint, 0600);
+MODULE_PARM_DESC(timesync_delay_ms, "Delay in ms between two consecutive timesync operations");
+
+enum qts_msg_type {
+       QAIC_TS_SYNC_REQ = 1,
+       QAIC_TS_ACK_TO_HOST,
+       QAIC_TS_MSG_TYPE_MAX
+};
+
+/**
+ * struct qts_hdr - Timesync message header structure.
+ * @signature: Unique signature to identify the timesync message.
+ * @reserved_1: Reserved for future use.
+ * @reserved_2: Reserved for future use.
+ * @msg_type: sub-type of the timesync message.
+ * @reserved_3: Reserved for future use.
+ */
+struct qts_hdr {
+       __le16  signature;
+       __le16  reserved_1;
+       u8      reserved_2;
+       u8      msg_type;
+       __le16  reserved_3;
+} __packed;
+
+/**
+ * struct qts_timeval - Structure to carry time information.
+ * @tv_sec: Seconds part of the time.
+ * @tv_usec: uS (microseconds) part of the time.
+ */
+struct qts_timeval {
+       __le64  tv_sec;
+       __le64  tv_usec;
+} __packed;
+
+/**
+ * struct qts_host_time_sync_msg_data - Structure to denote the timesync message.
+ * @header: Header of the timesync message.
+ * @data: Time information.
+ */
+struct qts_host_time_sync_msg_data {
+       struct  qts_hdr header;
+       struct  qts_timeval data;
+} __packed;
+
+/**
+ * struct mqts_dev - MHI QAIC Timesync Control device.
+ * @qdev: Pointer to the root device struct driven by QAIC driver.
+ * @mhi_dev: Pointer to associated MHI device.
+ * @timer: Timer handle used for timesync.
+ * @qtimer_addr: Device QTimer register pointer.
+ * @buff_in_use: atomic variable to track if the sync_msg buffer is in use.
+ * @dev: Device pointer to qdev->pdev->dev stored for easy access.
+ * @sync_msg: Buffer used to send timesync message over MHI.
+ */
+struct mqts_dev {
+       struct qaic_device *qdev;
+       struct mhi_device *mhi_dev;
+       struct timer_list timer;
+       void __iomem *qtimer_addr;
+       atomic_t buff_in_use;
+       struct device *dev;
+       struct qts_host_time_sync_msg_data *sync_msg;
+};
+
+#ifdef readq
+static u64 read_qtimer(const volatile void __iomem *addr)
+{
+       return readq(addr);
+}
+#else
+static u64 read_qtimer(const volatile void __iomem *addr)
+{
+       u64 low, high;
+
+       low = readl(addr);
+       high = readl(addr + sizeof(u32));
+       return low | (high << 32);
+}
+#endif
+
+static void qaic_timesync_ul_xfer_cb(struct mhi_device *mhi_dev, struct mhi_result *mhi_result)
+{
+       struct mqts_dev *mqtsdev = dev_get_drvdata(&mhi_dev->dev);
+
+       dev_dbg(mqtsdev->dev, "%s status: %d xfer_len: %zu\n", __func__,
+               mhi_result->transaction_status, mhi_result->bytes_xferd);
+
+       atomic_set(&mqtsdev->buff_in_use, 0);
+}
+
+static void qaic_timesync_dl_xfer_cb(struct mhi_device *mhi_dev, struct mhi_result *mhi_result)
+{
+       struct mqts_dev *mqtsdev = dev_get_drvdata(&mhi_dev->dev);
+
+       dev_err(mqtsdev->dev, "%s no data expected on dl channel\n", __func__);
+}
+
+static void qaic_timesync_timer(struct timer_list *t)
+{
+       struct mqts_dev *mqtsdev = from_timer(mqtsdev, t, timer);
+       struct qts_host_time_sync_msg_data *sync_msg;
+       u64 device_qtimer_us;
+       u64 device_qtimer;
+       u64 host_time_us;
+       u64 offset_us;
+       u64 host_sec;
+       int ret;
+
+       if (atomic_read(&mqtsdev->buff_in_use)) {
+               dev_dbg(mqtsdev->dev, "%s buffer not free, schedule next cycle\n", __func__);
+               goto mod_timer;
+       }
+       atomic_set(&mqtsdev->buff_in_use, 1);
+
+       sync_msg = mqtsdev->sync_msg;
+       sync_msg->header.signature = cpu_to_le16(QAIC_TIMESYNC_SIGNATURE);
+       sync_msg->header.msg_type = QAIC_TS_SYNC_REQ;
+       /* Read host UTC time and convert to uS*/
+       host_time_us = div_u64(ktime_get_real_ns(), NSEC_PER_USEC);
+       device_qtimer = read_qtimer(mqtsdev->qtimer_addr);
+       device_qtimer_us = QAIC_CONV_QTIMER_TO_US(device_qtimer);
+       /* Offset between host UTC and device time */
+       offset_us = host_time_us - device_qtimer_us;
+
+       host_sec = div_u64(offset_us, USEC_PER_SEC);
+       sync_msg->data.tv_usec = cpu_to_le64(offset_us - host_sec * USEC_PER_SEC);
+       sync_msg->data.tv_sec = cpu_to_le64(host_sec);
+       ret = mhi_queue_buf(mqtsdev->mhi_dev, DMA_TO_DEVICE, sync_msg, sizeof(*sync_msg), MHI_EOT);
+       if (ret && (ret != -EAGAIN)) {
+               dev_err(mqtsdev->dev, "%s unable to queue to mhi:%d\n", __func__, ret);
+               return;
+       } else if (ret == -EAGAIN) {
+               atomic_set(&mqtsdev->buff_in_use, 0);
+       }
+
+mod_timer:
+       ret = mod_timer(t, jiffies + msecs_to_jiffies(timesync_delay_ms));
+       if (ret)
+               dev_err(mqtsdev->dev, "%s mod_timer error:%d\n", __func__, ret);
+}
+
+static int qaic_timesync_probe(struct mhi_device *mhi_dev, const struct mhi_device_id *id)
+{
+       struct qaic_device *qdev = pci_get_drvdata(to_pci_dev(mhi_dev->mhi_cntrl->cntrl_dev));
+       struct mqts_dev *mqtsdev;
+       struct timer_list *timer;
+       int ret;
+
+       mqtsdev = kzalloc(sizeof(*mqtsdev), GFP_KERNEL);
+       if (!mqtsdev) {
+               ret = -ENOMEM;
+               goto out;
+       }
+
+       timer = &mqtsdev->timer;
+       mqtsdev->mhi_dev = mhi_dev;
+       mqtsdev->qdev = qdev;
+       mqtsdev->dev = &qdev->pdev->dev;
+
+       mqtsdev->sync_msg = kzalloc(sizeof(*mqtsdev->sync_msg), GFP_KERNEL);
+       if (!mqtsdev->sync_msg) {
+               ret = -ENOMEM;
+               goto free_mqts_dev;
+       }
+       atomic_set(&mqtsdev->buff_in_use, 0);
+
+       ret = mhi_prepare_for_transfer(mhi_dev);
+       if (ret)
+               goto free_sync_msg;
+
+       /* Qtimer register pointer */
+       mqtsdev->qtimer_addr = qdev->bar_0 + QTIMER_REG_OFFSET;
+       timer_setup(timer, qaic_timesync_timer, 0);
+       timer->expires = jiffies + msecs_to_jiffies(timesync_delay_ms);
+       add_timer(timer);
+       dev_set_drvdata(&mhi_dev->dev, mqtsdev);
+
+       return 0;
+
+free_sync_msg:
+       kfree(mqtsdev->sync_msg);
+free_mqts_dev:
+       kfree(mqtsdev);
+out:
+       return ret;
+};
+
+static void qaic_timesync_remove(struct mhi_device *mhi_dev)
+{
+       struct mqts_dev *mqtsdev = dev_get_drvdata(&mhi_dev->dev);
+
+       del_timer_sync(&mqtsdev->timer);
+       mhi_unprepare_from_transfer(mqtsdev->mhi_dev);
+       kfree(mqtsdev->sync_msg);
+       kfree(mqtsdev);
+}
+
+static const struct mhi_device_id qaic_timesync_match_table[] = {
+       { .chan = "QAIC_TIMESYNC_PERIODIC"},
+       {},
+};
+
+MODULE_DEVICE_TABLE(mhi, qaic_timesync_match_table);
+
+static struct mhi_driver qaic_timesync_driver = {
+       .id_table = qaic_timesync_match_table,
+       .remove = qaic_timesync_remove,
+       .probe = qaic_timesync_probe,
+       .ul_xfer_cb = qaic_timesync_ul_xfer_cb,
+       .dl_xfer_cb = qaic_timesync_dl_xfer_cb,
+       .driver = {
+               .name = "qaic_timesync_periodic",
+       },
+};
+
+int qaic_timesync_init(void)
+{
+       return mhi_driver_register(&qaic_timesync_driver);
+}
+
+void qaic_timesync_deinit(void)
+{
+       mhi_driver_unregister(&qaic_timesync_driver);
+}