diff options
Diffstat (limited to 'drivers/misc/issei/main.c')
| -rw-r--r-- | drivers/misc/issei/main.c | 296 |
1 files changed, 296 insertions, 0 deletions
diff --git a/drivers/misc/issei/main.c b/drivers/misc/issei/main.c new file mode 100644 index 000000000..5987fb340 --- /dev/null +++ b/drivers/misc/issei/main.c @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright (C) 2023-2026 Intel Corporation */ +#include <linux/atomic.h> +#include <linux/device.h> +#include <linux/dev_printk.h> +#include <linux/err.h> +#include <linux/export.h> +#include <linux/jiffies.h> +#include <linux/kthread.h> +#include <linux/slab.h> +#include <linux/wait.h> + +#include "cdev.h" +#include "fw_client.h" +#include "host_client.h" +#include "ham.h" +#include "issei_dev.h" + +static void issei_rst_state_set(struct issei_device *idev, enum issei_rst_state state) +{ + idev->rst_state = state; + /* wake up the thread */ + if (waitqueue_active(&idev->wait_rst_state)) + wake_up(&idev->wait_rst_state); +} + +static int issei_reset(struct issei_device *idev) +{ + int ret; + + idev->ops->irq_clear(idev); + + issei_cl_all_disconnect(idev); + issei_fw_cl_remove_all(idev); + /* No need to check for overflow here, the counter is used only for info */ + idev->all_reset_count++; + ret = idev->ops->hw_reset(idev, !idev->power_down); + issei_dmam_setup(idev); + if (ret) { + dev_err(&idev->dev, "hw_reset failed ret = %d\n", ret); + return ret; + } + + if (idev->power_down) { + dev_dbg(&idev->dev, "powering down: end of reset\n"); + issei_rst_state_set(idev, ISSEI_RST_STATE_DISABLED); + return -ENODEV; + } + return 0; +} + +static int issei_process_read_msg(struct issei_device *idev) +{ + struct issei_dma_data data = {}; + int ret; + + ret = issei_dma_read(idev, &data); + if (ret) + return ret; + + dev_dbg(&idev->dev, "Processing response %u %u %u %u\n", data.fw_id, data.host_id, + data.status, data.length); + if (data.status != HAMS_SUCCESS) { + dev_err(&idev->dev, "Command failed with status 0x%02X", data.status); + kfree(data.buf); + ret = -EIO; + } else { + if (issei_is_ham_rsp(data.fw_id, data.host_id)) + ret = issei_ham_process_ham_rsp(idev, data.buf, data.length); + else + ret = issei_cl_read_buf(idev, data.fw_id, data.host_id, + data.buf, data.length); + } + idev->ops->irq_write_generate(idev); + return ret; +} + +static int issei_process_write_msg(struct issei_device *idev) +{ + if (idev->rst_state != ISSEI_RST_STATE_DONE) + return 0; + + return issei_cl_write_from_queue(idev); +} + +static int issei_process_thread(void *_dev) +{ + long timeout, old_timeout = MAX_SCHEDULE_TIMEOUT; + struct issei_device *idev = _dev; + int ret; + + while (!kthread_should_stop()) { + dev_dbg(&idev->dev, "process_work in %d\n", idev->rst_state); + if (!idev->ops->hw_is_ready(idev) && idev->rst_state > ISSEI_RST_STATE_HW_READY) { + if (!idev->power_down) + dev_dbg(&idev->dev, "HW not ready, resetting\n"); + idev->rst_state = ISSEI_RST_STATE_INIT; + } + if (idev->power_down) + idev->rst_state = ISSEI_RST_STATE_INIT; + WRITE_ONCE(idev->has_data, false); + dev_dbg(&idev->dev, "reset_step in %d\n", idev->rst_state); + timeout = MAX_SCHEDULE_TIMEOUT; + ret = 0; + switch (idev->rst_state) { + case ISSEI_RST_STATE_DISABLED: + if (idev->power_down) { + dev_dbg(&idev->dev, "Interrupt in power down?\n"); + break; + } + idev->rst_state = ISSEI_RST_STATE_INIT; + fallthrough; + + case ISSEI_RST_STATE_INIT: + idev->ops->irq_clear(idev); + idev->ops->irq_sync(idev); + + if (!idev->power_down) { + idev->reset_count++; + if (idev->reset_count > ISSEI_MAX_CONSEC_RESET) { + dev_err(&idev->dev, "reset: reached maximal consecutive resets: disabling the device\n"); + issei_rst_state_set(idev, ISSEI_RST_STATE_DISABLED); + break; + } + } + + ret = issei_reset(idev); + if (idev->power_down) { + dev_dbg(&idev->dev, "Powering down\n"); + return 0; + } + if (ret) + break; + + idev->rst_state = ISSEI_RST_STATE_HW_READY; + timeout = msecs_to_jiffies(ISSEI_RST_HW_READY_TIMEOUT_MSEC); + break; + + case ISSEI_RST_STATE_HW_READY: + if (!idev->ops->hw_is_ready(idev)) { + dev_dbg(&idev->dev, "HW is not ready?\n"); + timeout = old_timeout; + break; + } + + dev_dbg(&idev->dev, "HW is ready\n"); + idev->ops->hw_reset_release(idev); + idev->ops->host_set_ready(idev); + ret = idev->ops->setup_message_send(idev); + if (ret) + break; + + idev->rst_state = ISSEI_RST_STATE_SETUP; + timeout = msecs_to_jiffies(ISSEI_RST_STEP_TIMEOUT_MSEC); + break; + + case ISSEI_RST_STATE_SETUP: + ret = idev->ops->setup_message_recv(idev); + if (ret) { + if (ret == -ENODATA) { + ret = 0; + timeout = old_timeout; + } + } else { + timeout = msecs_to_jiffies(ISSEI_RST_STEP_TIMEOUT_MSEC); + ret = issei_ham_send_start_req(idev); + idev->rst_state = ISSEI_RST_STATE_START; + } + break; + + case ISSEI_RST_STATE_START: + ret = issei_process_read_msg(idev); + if (!ret) { + timeout = msecs_to_jiffies(ISSEI_RST_STEP_TIMEOUT_MSEC); + idev->rst_state = ISSEI_RST_STATE_CLIENT_ENUM; + } else if (ret == -ENODATA) { + ret = 0; + timeout = old_timeout; + } + break; + + case ISSEI_RST_STATE_CLIENT_ENUM: + ret = issei_process_read_msg(idev); + if (ret) { + if (ret == -ENODATA) { + ret = 0; + timeout = old_timeout; + } + } else { + idev->reset_count = 0; + idev->rst_state = ISSEI_RST_STATE_DONE; + dev_dbg(&idev->dev, "Reset finished successfully\n"); + } + break; + + case ISSEI_RST_STATE_DONE: + ret = issei_process_read_msg(idev); + if (ret != 0 && ret != -ENODATA) + break; + + ret = issei_process_write_msg(idev); + break; + } + + if (ret) { + dev_warn(&idev->dev, "Process failed ret = %d\n", ret); + idev->rst_state = ISSEI_RST_STATE_INIT; + continue; + } + + /* + * Every thread that has data to process sets the 'has_data' flag and + * triggers the wait queue. + * The processing thread, in each loop iteration, resets 'has_data' + * and processes all available data. + * + * After processing, the thread waits for 'has_data' to be set again. + * + * If the wait function times out but 'has_data' becomes 1 before + * the subsequent atomic read check, this is acceptable from a flow + * perspective - the thread will continue processing the data. + * + * The 'has_data' flag cannot become 0 between the wait function and + * the atomic read check, since only this thread is allowed to reset it to 0. + */ + + old_timeout = wait_event_interruptible_timeout(idev->wait_has_data, + READ_ONCE(idev->has_data), + timeout); + if (idev->rst_state == ISSEI_RST_STATE_DISABLED) + continue; + + if (!READ_ONCE(idev->has_data)) { + dev_warn(&idev->dev, "Timed out at state %d, resetting\n", + idev->rst_state); + idev->rst_state = ISSEI_RST_STATE_INIT; + } + } + + return 0; +} + +/** + * issei_start - configure HW device and start processing thread. + * @idev: the device structure + * + * Return: 0 on success, < 0 on failure + */ +int issei_start(struct issei_device *idev) +{ + int ret; + + idev->power_down = false; + + ret = issei_dmam_setup(idev); + if (ret) + return ret; + + idev->ops->irq_clear(idev); + + ret = idev->ops->hw_config(idev); + if (ret) + return ret; + + idev->process_thread = kthread_run(issei_process_thread, idev, + "kisseiprocess/%s", dev_name(&idev->dev)); + if (IS_ERR(idev->process_thread)) { + ret = PTR_ERR(idev->process_thread); + dev_err(&idev->dev, "unable to create process thread. ret = %d\n", ret); + return ret; + } + + issei_poke_process_thread(idev); + return 0; +} +EXPORT_SYMBOL_GPL(issei_start); + +/** + * issei_stop - stop interrupts and processing thread. + * @idev: the device structure + */ +void issei_stop(struct issei_device *idev) +{ + idev->power_down = true; + + idev->ops->irq_clear(idev); + idev->ops->irq_sync(idev); + + issei_poke_process_thread(idev); + + wait_event_timeout(idev->wait_rst_state, + (idev->rst_state == ISSEI_RST_STATE_DISABLED), + msecs_to_jiffies(ISSEI_STOP_TIMEOUT_MSEC)); + kthread_stop(idev->process_thread); +} +EXPORT_SYMBOL_GPL(issei_stop); |
