| // 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); |