| // 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 <linux/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 (100 * USEC_PER_MSEC) | |
| #define MEI_VSC_POLL_TIMEOUT_US (400 * 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); | |
| if (!intr_enable) | |
| return 0; | |
| return vsc_tp_init(hw->tp, mei_dev->parent); | |
| } | |
| 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 = kzalloc(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 = 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_register(mei_dev, dev); | |
| if (ret) | |
| goto err; | |
| ret = mei_start(mei_dev); | |
| if (ret) { | |
| dev_err_probe(dev, ret, "init hw failed\n"); | |
| goto err; | |
| } | |
| pm_runtime_enable(mei_dev->parent); | |
| return 0; | |
| err: | |
| mei_cancel_work(mei_dev); | |
| vsc_tp_register_event_cb(tp, NULL, NULL); | |
| mei_disable_interrupts(mei_dev); | |
| mei_deregister(mei_dev); | |
| return ret; | |
| } | |
| static void mei_vsc_remove(struct platform_device *pdev) | |
| { | |
| struct mei_device *mei_dev = platform_get_drvdata(pdev); | |
| struct mei_vsc_hw *hw = mei_dev_to_vsc_hw(mei_dev); | |
| pm_runtime_disable(mei_dev->parent); | |
| mei_stop(mei_dev); | |
| vsc_tp_register_event_cb(hw->tp, NULL, NULL); | |
| mei_disable_interrupts(mei_dev); | |
| mei_deregister(mei_dev); | |
| } | |
| static int mei_vsc_suspend(struct device *dev) | |
| { | |
| struct mei_device *mei_dev; | |
| int ret = 0; | |
| mei_dev = dev_get_drvdata(dev); | |
| if (!mei_dev) | |
| return -ENODEV; | |
| mutex_lock(&mei_dev->device_lock); | |
| if (!mei_write_is_idle(mei_dev)) | |
| ret = -EAGAIN; | |
| mutex_unlock(&mei_dev->device_lock); | |
| return ret; | |
| } | |
| static int mei_vsc_resume(struct device *dev) | |
| { | |
| struct mei_device *mei_dev; | |
| mei_dev = dev_get_drvdata(dev); | |
| if (!mei_dev) | |
| return -ENODEV; | |
| 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"); |