// SPDX-License-Identifier: GPL-2.0
// Copyright (C) 2013 - 2024 Intel Corporation

#include <asm/cacheflush.h>

#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/dma-mapping.h>

#include "ipu.h"
#include "ipu-trace.h"
#include "ipu-fw-com.h"
#include "ipu-bus.h"

/*
 * FWCOM layer is a shared resource between FW and driver. It consist
 * of token queues to both send and receive directions. Queue is simply
 * an array of structures with read and write indexes to the queue.
 * There are 1...n queues to both directions. Queues locates in
 * system ram and are mapped to ISP MMU so that both CPU and ISP can
 * see the same buffer. Indexes are located in ISP DMEM so that FW code
 * can poll those with very low latency and cost. CPU access to indexes is
 * more costly but that happens only at message sending time and
 * interrupt trigged message handling. CPU doesn't need to poll indexes.
 * wr_reg / rd_reg are offsets to those dmem location. They are not
 * the indexes itself.
 */

/* Shared structure between driver and FW - do not modify */
struct ipu_fw_sys_queue {
	u64 host_address;
	u32 vied_address;
	u32 size;
	u32 token_size;
	u32 wr_reg;	/* reg no in subsystem's regmem */
	u32 rd_reg;
	u32 _align;
};

struct ipu_fw_sys_queue_res {
	u64 host_address;
	u32 vied_address;
	u32 reg;
};

enum syscom_state {
	/* Program load or explicit host setting should init to this */
	SYSCOM_STATE_UNINIT = 0x57A7E000,
	/* SP Syscom sets this when it is ready for use */
	SYSCOM_STATE_READY = 0x57A7E001,
	/* SP Syscom sets this when no more syscom accesses will happen */
	SYSCOM_STATE_INACTIVE = 0x57A7E002
};

enum syscom_cmd {
	/* Program load or explicit host setting should init to this */
	SYSCOM_COMMAND_UNINIT = 0x57A7F000,
	/* Host Syscom requests syscom to become inactive */
	SYSCOM_COMMAND_INACTIVE = 0x57A7F001
};

/* firmware config: data that sent from the host to SP via DDR */
/* Cell copies data into a context */

struct ipu_fw_syscom_config {
	u32 firmware_address;

	u32 num_input_queues;
	u32 num_output_queues;

	/* ISP pointers to an array of ipu_fw_sys_queue structures */
	u32 input_queue;
	u32 output_queue;

	/* ISYS / PSYS private data */
	u32 specific_addr;
	u32 specific_size;
};

/* End of shared structures / data */

struct ipu_fw_com_context {
	struct ipu_bus_device *adev;
	void __iomem *dmem_addr;
	int (*cell_ready)(struct ipu_bus_device *adev);
	void (*cell_start)(struct ipu_bus_device *adev);

	void *dma_buffer;
	dma_addr_t dma_addr;
	unsigned int dma_size;
	unsigned long attrs;

	unsigned int num_input_queues;
	unsigned int num_output_queues;

	struct ipu_fw_sys_queue *input_queue;	/* array of host to SP queues */
	struct ipu_fw_sys_queue *output_queue;	/* array of SP to host */

	void *config_host_addr;
	void *specific_host_addr;
	u64 ibuf_host_addr;
	u64 obuf_host_addr;

	u32 config_vied_addr;
	u32 input_queue_vied_addr;
	u32 output_queue_vied_addr;
	u32 specific_vied_addr;
	u32 ibuf_vied_addr;
	u32 obuf_vied_addr;

	unsigned int buttress_boot_offset;
	void __iomem *base_addr;
};

#define FW_COM_WR_REG 0
#define FW_COM_RD_REG 4

#define REGMEM_OFFSET 0
#define TUNIT_MAGIC_PATTERN 0x5a5a5a5a

enum regmem_id {
	/* pass pkg_dir address to SPC in non-secure mode */
	PKG_DIR_ADDR_REG = 0,
	/* Tunit CFG blob for secure - provided by host.*/
	TUNIT_CFG_DWR_REG = 1,
	/* syscom commands - modified by the host */
	SYSCOM_COMMAND_REG = 2,
	/* Store interrupt status - updated by SP */
	SYSCOM_IRQ_REG = 3,
	/* first syscom queue pointer register */
	SYSCOM_QPR_BASE_REG = 4
};

enum message_direction {
	DIR_RECV = 0,
	DIR_SEND
};

#define BUTRESS_FW_BOOT_PARAMS_0 0x4000
#define BUTTRESS_FW_BOOT_PARAM_REG(base, offset, id) ((base) \
	+ BUTRESS_FW_BOOT_PARAMS_0 + ((offset) + (id)) * 4)

enum buttress_syscom_id {
	/* pass syscom configuration to SPC */
	SYSCOM_CONFIG_ID		= 0,
	/* syscom state - modified by SP */
	SYSCOM_STATE_ID			= 1,
	/* syscom vtl0 addr mask */
	SYSCOM_VTL0_ADDR_MASK_ID	= 2,
	SYSCOM_ID_MAX
};

static unsigned int num_messages(unsigned int wr, unsigned int rd,
				 unsigned int size)
{
	if (wr < rd)
		wr += size;
	return wr - rd;
}

static unsigned int num_free(unsigned int wr, unsigned int rd,
			     unsigned int size)
{
	return size - num_messages(wr, rd, size);
}

static unsigned int curr_index(void __iomem *q_dmem,
			       enum message_direction dir)
{
	return readl(q_dmem +
			 (dir == DIR_RECV ? FW_COM_RD_REG : FW_COM_WR_REG));
}

static unsigned int inc_index(void __iomem *q_dmem, struct ipu_fw_sys_queue *q,
			      enum message_direction dir)
{
	unsigned int index;

	index = curr_index(q_dmem, dir) + 1;
	return index >= q->size ? 0 : index;
}

static unsigned int ipu_sys_queue_buf_size(unsigned int size,
					   unsigned int token_size)
{
	return (size + 1) * token_size;
}

static void ipu_sys_queue_init(struct ipu_fw_sys_queue *q, unsigned int size,
			       unsigned int token_size,
			       struct ipu_fw_sys_queue_res *res)
{
	unsigned int buf_size;

	q->size = size + 1;
	q->token_size = token_size;
	buf_size = ipu_sys_queue_buf_size(size, token_size);

	/* acquire the shared buffer space */
	q->host_address = res->host_address;
	res->host_address += buf_size;
	q->vied_address = res->vied_address;
	res->vied_address += buf_size;

	/* acquire the shared read and writer pointers */
	q->wr_reg = res->reg;
	res->reg++;
	q->rd_reg = res->reg;
	res->reg++;
}

void *ipu_fw_com_prepare(struct ipu_fw_com_cfg *cfg,
			 struct ipu_bus_device *adev, void __iomem *base)
{
	struct ipu_fw_com_context *ctx;
	struct ipu_fw_syscom_config *fw_cfg;
	unsigned int i;
	unsigned int sizeall, offset;
	unsigned int sizeinput = 0, sizeoutput = 0;
	unsigned long attrs = 0;
	struct ipu_fw_sys_queue_res res;

	/* error handling */
	if (!cfg || !cfg->cell_start || !cfg->cell_ready)
		return NULL;

	ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
	if (!ctx)
		return NULL;
	ctx->dmem_addr = base + cfg->dmem_addr + REGMEM_OFFSET;
	ctx->adev = adev;
	ctx->cell_start = cfg->cell_start;
	ctx->cell_ready = cfg->cell_ready;
	ctx->buttress_boot_offset = cfg->buttress_boot_offset;
	ctx->base_addr  = base;

	ctx->num_input_queues = cfg->num_input_queues;
	ctx->num_output_queues = cfg->num_output_queues;

	/*
	 * Allocate DMA mapped memory. Allocate one big chunk.
	 */
	sizeall =
	    /* Base cfg for FW */
	    roundup(sizeof(struct ipu_fw_syscom_config), 8) +
	    /* Descriptions of the queues */
	    cfg->num_input_queues * sizeof(struct ipu_fw_sys_queue) +
	    cfg->num_output_queues * sizeof(struct ipu_fw_sys_queue) +
	    /* FW specific information structure */
	    roundup(cfg->specific_size, 8);

	for (i = 0; i < cfg->num_input_queues; i++)
		sizeinput += ipu_sys_queue_buf_size(cfg->input[i].queue_size,
						cfg->input[i].token_size);

	for (i = 0; i < cfg->num_output_queues; i++)
		sizeoutput += ipu_sys_queue_buf_size(cfg->output[i].queue_size,
						 cfg->output[i].token_size);

	sizeall += sizeinput + sizeoutput;

	ctx->dma_buffer = dma_alloc_attrs(&ctx->adev->dev, sizeall,
					  &ctx->dma_addr, GFP_KERNEL,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0)
					  attrs);
	ctx->attrs = attrs;
#else
					  NULL);
#endif
	if (!ctx->dma_buffer) {
		dev_err(&ctx->adev->dev, "failed to allocate dma memory\n");
		kfree(ctx);
		return NULL;
	}

	ctx->dma_size = sizeall;

	/* This is the address where FW starts to parse allocations */
	ctx->config_host_addr = ctx->dma_buffer;
	ctx->config_vied_addr = ctx->dma_addr;
	fw_cfg = (struct ipu_fw_syscom_config *)ctx->config_host_addr;
	offset = roundup(sizeof(struct ipu_fw_syscom_config), 8);

	ctx->input_queue = ctx->dma_buffer + offset;
	ctx->input_queue_vied_addr = ctx->dma_addr + offset;
	offset += cfg->num_input_queues * sizeof(struct ipu_fw_sys_queue);

	ctx->output_queue = ctx->dma_buffer + offset;
	ctx->output_queue_vied_addr = ctx->dma_addr + offset;
	offset += cfg->num_output_queues * sizeof(struct ipu_fw_sys_queue);

	ctx->specific_host_addr = ctx->dma_buffer + offset;
	ctx->specific_vied_addr = ctx->dma_addr + offset;
	offset += roundup(cfg->specific_size, 8);

	ctx->ibuf_host_addr = (uintptr_t)(ctx->dma_buffer + offset);
	ctx->ibuf_vied_addr = ctx->dma_addr + offset;
	offset += sizeinput;

	ctx->obuf_host_addr = (uintptr_t)(ctx->dma_buffer + offset);
	ctx->obuf_vied_addr = ctx->dma_addr + offset;
	offset += sizeoutput;

	/* initialize input queues */
	res.reg = SYSCOM_QPR_BASE_REG;
	res.host_address = ctx->ibuf_host_addr;
	res.vied_address = ctx->ibuf_vied_addr;
	for (i = 0; i < cfg->num_input_queues; i++) {
		ipu_sys_queue_init(ctx->input_queue + i,
				   cfg->input[i].queue_size,
				   cfg->input[i].token_size, &res);
	}

	/* initialize output queues */
	res.host_address = ctx->obuf_host_addr;
	res.vied_address = ctx->obuf_vied_addr;
	for (i = 0; i < cfg->num_output_queues; i++) {
		ipu_sys_queue_init(ctx->output_queue + i,
				   cfg->output[i].queue_size,
				   cfg->output[i].token_size, &res);
	}

	/* copy firmware specific data */
	if (cfg->specific_addr && cfg->specific_size) {
		memcpy((void *)ctx->specific_host_addr,
		       cfg->specific_addr, cfg->specific_size);
	}

	fw_cfg->num_input_queues = cfg->num_input_queues;
	fw_cfg->num_output_queues = cfg->num_output_queues;
	fw_cfg->input_queue = ctx->input_queue_vied_addr;
	fw_cfg->output_queue = ctx->output_queue_vied_addr;
	fw_cfg->specific_addr = ctx->specific_vied_addr;
	fw_cfg->specific_size = cfg->specific_size;
	return ctx;
}
EXPORT_SYMBOL_GPL(ipu_fw_com_prepare);

int ipu_fw_com_open(struct ipu_fw_com_context *ctx)
{
	dma_addr_t trace_buff = TUNIT_MAGIC_PATTERN;

	/*
	 * Write trace buff start addr to tunit cfg reg.
	 * This feature is used to enable tunit trace in secure mode.
	 */
	ipu_trace_buffer_dma_handle(&ctx->adev->dev, &trace_buff);
	writel(trace_buff, ctx->dmem_addr + TUNIT_CFG_DWR_REG * 4);

	/* Check if SP is in valid state */
	if (!ctx->cell_ready(ctx->adev))
		return -EIO;

	/* store syscom uninitialized command */
	writel(SYSCOM_COMMAND_UNINIT,
	       ctx->dmem_addr + SYSCOM_COMMAND_REG * 4);

	/* store syscom uninitialized state */
	writel(SYSCOM_STATE_UNINIT,
	       BUTTRESS_FW_BOOT_PARAM_REG(ctx->base_addr,
					  ctx->buttress_boot_offset,
					  SYSCOM_STATE_ID));

	/* store firmware configuration address */
	writel(ctx->config_vied_addr,
	       BUTTRESS_FW_BOOT_PARAM_REG(ctx->base_addr,
					  ctx->buttress_boot_offset,
					  SYSCOM_CONFIG_ID));
	ctx->cell_start(ctx->adev);

	return 0;
}
EXPORT_SYMBOL_GPL(ipu_fw_com_open);

int ipu_fw_com_close(struct ipu_fw_com_context *ctx)
{
	int state;

	state = readl(BUTTRESS_FW_BOOT_PARAM_REG(ctx->base_addr,
						 ctx->buttress_boot_offset,
						 SYSCOM_STATE_ID));
	if (state != SYSCOM_STATE_READY)
		return -EBUSY;

	/* set close request flag */
	writel(SYSCOM_COMMAND_INACTIVE, ctx->dmem_addr +
		   SYSCOM_COMMAND_REG * 4);

	return 0;
}
EXPORT_SYMBOL_GPL(ipu_fw_com_close);

int ipu_fw_com_release(struct ipu_fw_com_context *ctx, unsigned int force)
{
	/* check if release is forced, an verify cell state if it is not */
	if (!force && !ctx->cell_ready(ctx->adev))
		return -EBUSY;

	dma_free_attrs(&ctx->adev->dev, ctx->dma_size,
		       ctx->dma_buffer, ctx->dma_addr,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0)
		       ctx->attrs);
#else
		       NULL);
#endif
	kfree(ctx);
	return 0;
}
EXPORT_SYMBOL_GPL(ipu_fw_com_release);

int ipu_fw_com_ready(struct ipu_fw_com_context *ctx)
{
	int state;

	state = readl(BUTTRESS_FW_BOOT_PARAM_REG(ctx->base_addr,
						 ctx->buttress_boot_offset,
						 SYSCOM_STATE_ID));
	if (state != SYSCOM_STATE_READY)
		return -EBUSY;	/* SPC is not ready to handle messages yet */

	return 0;
}
EXPORT_SYMBOL_GPL(ipu_fw_com_ready);

static bool is_index_valid(struct ipu_fw_sys_queue *q, unsigned int index)
{
	if (index >= q->size)
		return false;
	return true;
}

void *ipu_send_get_token(struct ipu_fw_com_context *ctx, int q_nbr)
{
	struct ipu_fw_sys_queue *q = &ctx->input_queue[q_nbr];
	void __iomem *q_dmem = ctx->dmem_addr + q->wr_reg * 4;
	unsigned int wr, rd;
	unsigned int packets;
	unsigned int index;

	wr = readl(q_dmem + FW_COM_WR_REG);
	rd = readl(q_dmem + FW_COM_RD_REG);

	/* Catch indexes in dmem */
	if (!is_index_valid(q, wr) || !is_index_valid(q, rd)) {
		dev_err(&ctx->adev->dev, "invalid index\n");
		return NULL;
	}

	packets = num_free(wr + 1, rd, q->size);
	if (!packets) {
		dev_err(&ctx->adev->dev, "no packets available\n");
		return NULL;
	}

	index = curr_index(q_dmem, DIR_SEND);

	return (void *)(unsigned long)q->host_address + (index * q->token_size);
}
EXPORT_SYMBOL_GPL(ipu_send_get_token);

void ipu_send_put_token(struct ipu_fw_com_context *ctx, int q_nbr)
{
	struct ipu_fw_sys_queue *q = &ctx->input_queue[q_nbr];
	void __iomem *q_dmem = ctx->dmem_addr + q->wr_reg * 4;
	int index = curr_index(q_dmem, DIR_SEND);

	/* Increment index */
	index = inc_index(q_dmem, q, DIR_SEND);

	writel(index, q_dmem + FW_COM_WR_REG);
}
EXPORT_SYMBOL_GPL(ipu_send_put_token);

void *ipu_recv_get_token(struct ipu_fw_com_context *ctx, int q_nbr)
{
	struct ipu_fw_sys_queue *q = &ctx->output_queue[q_nbr];
	void __iomem *q_dmem = ctx->dmem_addr + q->wr_reg * 4;
	unsigned int wr, rd;
	unsigned int packets;
	void *addr;

	wr = readl(q_dmem + FW_COM_WR_REG);
	rd = readl(q_dmem + FW_COM_RD_REG);

	/* Catch indexes in dmem? */
	if (!is_index_valid(q, wr) || !is_index_valid(q, rd))
		return NULL;

	packets = num_messages(wr, rd, q->size);
	if (!packets)
		return NULL;

	addr = (void *)(unsigned long)q->host_address + (rd * q->token_size);

	return addr;
}
EXPORT_SYMBOL_GPL(ipu_recv_get_token);

void ipu_recv_put_token(struct ipu_fw_com_context *ctx, int q_nbr)
{
	struct ipu_fw_sys_queue *q = &ctx->output_queue[q_nbr];
	void __iomem *q_dmem = ctx->dmem_addr + q->wr_reg * 4;
	unsigned int rd = inc_index(q_dmem, q, DIR_RECV);

	/* Release index */
	writel(rd, q_dmem + FW_COM_RD_REG);
}
EXPORT_SYMBOL_GPL(ipu_recv_put_token);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Intel ipu fw comm library");
