blob: 5987fb340250b8c30af1772dba4e9e9873a830c9 [file] [edit]
// 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);