/* Copyright (c) 2016 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

/* IPC module for ISH */

/**
 * IPC - Inter Processor Communication
 * -----------------------------------
 *
 * IPC is a bi-directional doorbell based message passing interface sans
 * session and transport layers, between hardware blocks. ISH uses IPC to
 * communicate with the Host, PMC (Power Management Controller), CSME
 * (Converged Security and  Manageability Engine), Audio, Graphics and ISP.
 *
 * Both the initiator and target ends each have a 32-bit doorbell register and
 * 128-byte message regions. In addition, the following register pairs help in
 * synchronizing IPC.
 *
 *  - Peripheral Interrupt Status Register (PISR)
 *  - Peripheral Interrupt Mask Register (PIMR)
 *  - Doorbell Clear Status Register (DB CSR)
 */

#include "registers.h"
#include "console.h"
#include "hooks.h"
#include "host_command.h"
#include "lpc.h"
#include "task.h"
#include "timer.h"
#include "util.h"
#include "ipc.h"

#define CPUTS(outstr) cputs(CC_LPC, outstr)
#define CPRINTS(format, args...) cprints(CC_LPC, format, ## args)
#define CPRINTF(format, args...) cprintf(CC_LPC, format, ## args)

static struct host_packet ipc_packet;	/* For host command processing */
static struct host_cmd_handler_args host_cmd_args;
static uint8_t host_cmd_flags;	/* Flags from host command */
static uint8_t params_copy[EC_LPC_HOST_PACKET_SIZE] __aligned(4);
static uint8_t mem_mapped[0x200] __attribute__ ((section(".bss.big_align")));
static struct ec_lpc_host_args *const ipc_host_args =
	(struct ec_lpc_host_args *)mem_mapped;

/* Array of peer contexts */
struct ipc_if_ctx ipc_peer_ctxs[IPC_PEERS_COUNT] = {
	[IPC_PEER_HOST_ID] = {
		.in_msg_reg = IPC_HOST2ISH_MSG_REGS,
		.out_msg_reg = IPC_ISH2HOST_MSG_REGS,
		.in_drbl_reg = IPC_HOST2ISH_DOORBELL,
		.out_drbl_reg = IPC_ISH2HOST_DOORBELL,
		.clr_bit = IPC_INT_ISH2HOST_CLR_BIT,
		.irq_in = ISH_IPC_HOST2ISH_IRQ,
		.irq_clr = ISH_IPC_ISH2HOST_CLR_IRQ,
	},
	/* Other peers (PMC, CSME, etc) to be added when required */
};

/* Peripheral Interrupt Mask Register bits */
static uint8_t pimr_bit_array[IPC_PEERS_COUNT][3] = {
	{
		IPC_PIMR_HOST2ISH_OFFS,
		IPC_PIMR_HOST2ISH_OFFS,
		IPC_PIMR_ISH2HOST_CLR_OFFS
	},
};

/* Get protocol information */
static int ipc_get_protocol_info(struct host_cmd_handler_args *args)
{
	struct ec_response_get_protocol_info *r = args->response;

	memset(r, 0, sizeof(*r));
	r->protocol_versions = (1 << 3);
	r->max_request_packet_size = EC_LPC_HOST_PACKET_SIZE;
	r->max_response_packet_size = EC_LPC_HOST_PACKET_SIZE;
	r->flags = 0;

	args->response_size = sizeof(*r);

	return EC_SUCCESS;
}
DECLARE_HOST_COMMAND(EC_CMD_GET_PROTOCOL_INFO, ipc_get_protocol_info,
		     EC_VER_MASK(0));

/* Set/un-set PIMR bits */
static void ipc_set_pimr(uint8_t peer_id, int set,
			 enum pimr_signal_type signal_type)
{
	uint32_t new_pimr_val;

	new_pimr_val = (1 << (pimr_bit_array[peer_id][signal_type]));

	interrupt_disable();

	if (set)
		REG32(IPC_PIMR) |= new_pimr_val;
	else
		REG32(IPC_PIMR) &= ~new_pimr_val;

	interrupt_enable();
}

/**
 * ipc_read: Host -> ISH communication
 *
 * 1. Host SW checks HOST2ISH doorbell bit[31] to ensure it is cleared.
 * 2. Host SW writes data to HOST2ISH message registers (upto 128 bytes).
 * 3. Host SW writes to HOST2ISH doorbell register, setting bit [31].
 * 4. ISH FW recieves interrupt, checks PISR[0] to realize the event.
 * 5. After reading data, ISH FW clears HOST2ISH DB bit[31].
 * 6. Host SW recieves interrupt, reads Host PISR bit[8] to realize
 *    the message was consumed by ISH FW.
 */
static int ipc_read(uint8_t peer_id, void *out_buff, uint32_t buff_size)
{
#ifdef ISH_DEBUG
	int i;
#endif
	struct ipc_if_ctx *ctx;
	int retval = EC_SUCCESS;

	ctx = &ipc_peer_ctxs[peer_id];

	if (buff_size > IPC_MSG_MAX_SIZE)
		retval = IPC_FAILURE;

	if (retval >= 0) {
		/* Copy message to out buffer. */
		memcpy(out_buff, (const uint32_t *)ctx->in_msg_reg, buff_size);
		retval = buff_size;

#ifdef ISH_DEBUG
		CPRINTF("ipc_read, len=0x%0x [", buff_size);
		for (i = 0; i < buff_size; i++)
			CPRINTF("0x%0x ", (uint8_t) ((char *)out_buff)[i]);
		CPUTS("]\n");
#endif
	}

	REG32(ctx->in_drbl_reg) = 0;
	ipc_set_pimr(peer_id, SET_PIMR, PIMR_SIGNAL_IN);

	return retval;
}

static int ipc_wait_until_msg_consumed(struct ipc_if_ctx *ctx, int timeout)
{
	int wait_sts = 0;
	uint32_t drbl;

	drbl = REG32(ctx->out_drbl_reg);
	if (!(drbl & IPC_DRBL_BUSY_BIT)) {
		/* doorbell is already cleared. we can continue */
		return 0;
	}

	while (1) {
		wait_sts = task_wait_event_mask(EVENT_FLAG_BIT_WRITE_IPC,
						timeout);
		drbl = REG32(ctx->out_drbl_reg);

		if (!(drbl & IPC_DRBL_BUSY_BIT)) {
			return 0;
		} else if (wait_sts != 0) {
			/* timeout */
			return wait_sts;
		}
	}
}

/**
 * ipc_write: ISH -> Host Communication
 *
 * 1. ISH FW ensures ISH2HOST doorbell busy bit [31] is cleared.
 * 2. ISH FW writes data (upto 128 bytes) to ISH2HOST message registers.
 * 3. ISH FW writes to ISH2HOST doorbell, busy bit (31) is set.
 * 4. Host SW receives interrupt, reads host PISR[0] to realize event.
 * 5. Upon reading data, Host driver clears ISH2HOST doorbell busy bit. This
 *    de-asserts the interrupt.
 * 6. ISH FW also receieves an interrupt for the clear event.
 */
static int ipc_write(uint8_t peer_id, void *buff, uint32_t buff_size)
{
	struct ipc_if_ctx *ctx;
	uint32_t drbl_val = 0;
#ifdef ISH_DEBUG
	int i;
#endif

	ctx = &ipc_peer_ctxs[peer_id];

	if (ipc_wait_until_msg_consumed(ctx, IPC_TIMEOUT)) {
		/* timeout */
		return IPC_FAILURE;
	}
#ifdef ISH_DEBUG
	CPRINTF("ipc_write, len=0x%0x [", buff_size);
	for (i = 0; i < buff_size; i++)
		CPRINTF("0x%0x ", (uint8_t) ((char *)buff)[i]);
	CPUTS("]\n");
#endif

	/* write message */
	if (buff_size <= IPC_MSG_MAX_SIZE) {
		/* write to message register */
		memcpy((uint32_t *) ctx->out_msg_reg, buff, buff_size);
		drbl_val = IPC_BUILD_HEADER(buff_size, IPC_PROTOCOL_ECP,
					    SET_BUSY);
	} else {
		return IPC_FAILURE;
	}

	/* write doorbell */
	REG32(ctx->out_drbl_reg) = drbl_val;

	return EC_SUCCESS;
}

uint8_t *lpc_get_memmap_range(void)
{
	return mem_mapped + 0x100;
}

static uint8_t *ipc_get_hostcmd_data_range(void)
{
	return mem_mapped;
}

static void ipc_send_response_packet(struct host_packet *pkt)
{
	ipc_write(IPC_PEER_HOST_ID, pkt->response, pkt->response_size);
}

void lpc_set_host_event_state(uint32_t mask)
{
}

void lpc_set_host_event_mask(enum lpc_host_event_type type, uint32_t mask)
{
}

uint32_t lpc_get_host_event_mask(enum lpc_host_event_type type)
{
	return 0;
}

void lpc_clear_acpi_status_mask(uint8_t mask)
{
}

/**
 * IPC interrupts are recieved by the FW when a) Host SW rings doorbell and
 * b) when Host SW clears doorbell busy bit [31].
 *
 * Doorbell Register (DB) bits
 * ----+-------+--------+-----------+--------+------------+--------------------
 *  31 | 30 29 |  28-20 |19 18 17 16| 15 14  | 13 12 11 10| 9 8 7 6 5 4 3 2 1 0
 * ----+-------+--------+-----------+--------+------------+--------------------
 * Busy|Options|Reserved|  Command  |Reserved|   Protocol |    Message Length
 * ----+-------+--------+-----------+--------+------------+--------------------
 *
 * ISH Peripheral Interrupt Status Register:
 *  Bit 0 - If set, indicates interrupt was caused by setting Host2ISH DB
 *
 * ISH Peripheral Interrupt Mask Register
 *  Bit 0 - If set, mask interrupt caused by Host2ISH DB
 *
 * ISH Peripheral DB Clear Status Register
 *  Bit 0 - If set, indicates interrupt was caused by clearing Host2ISH DB
 */
static void ipc_interrupt_handler(void)
{
	uint32_t pisr = REG32(IPC_PISR);
	uint32_t pimr = REG32(IPC_PIMR);
	uint32_t busy_clear = REG32(IPC_BUSY_CLEAR);
	uint32_t drbl = REG32(IPC_ISH2HOST_MSG_REGS);
	uint8_t proto, cmd;

	if ((pisr & IPC_PISR_HOST2ISH_BIT)
	    && (pimr & IPC_PIMR_HOST2ISH_BIT)) {

		/* New message arrived */
		ipc_set_pimr(IPC_PEER_HOST_ID, UNSET_PIMR, PIMR_SIGNAL_IN);
		task_set_event(TASK_ID_IPC_COMM, EVENT_FLAG_BIT_READ_IPC, 0);
		proto = IPC_HEADER_GET_PROTOCOL(drbl);
		cmd = IPC_HEADER_GET_MNG_CMD(drbl);

		if ((proto == IPC_PROTOCOL_MNG) && (cmd == MNG_TIME_UPDATE))
			/* Ignoring time update from host */
			;
	}

	if ((busy_clear & IPC_INT_ISH2HOST_CLR_BIT)
	    && (pimr & IPC_PIMR_ISH2HOST_CLR_MASK_BIT)) {
		/* Written message cleared */
		REG32(IPC_BUSY_CLEAR) = IPC_ISH_FWSTS;
		task_set_event(TASK_ID_IPC_COMM, EVENT_FLAG_BIT_WRITE_IPC, 0);
	}
}
DECLARE_IRQ(ISH_IPC_HOST2ISH_IRQ, ipc_interrupt_handler);

/* Task that listens for incomming IPC messages from Host and initiate host
 * command processing.
 */
void ipc_comm_task(void)
{
	int ret = 0;
	uint32_t out_drbl, pkt_len;

	for (;;) {

		ret = task_wait_event_mask(EVENT_FLAG_BIT_READ_IPC
					   | EVENT_FLAG_BIT_WRITE_IPC, -1);

		if ((ret & EVENT_FLAG_BIT_WRITE_IPC))
			continue;
		else if (!(ret & EVENT_FLAG_BIT_READ_IPC))
			continue;

		/* Read the command byte.  This clears the FRMH bit in
		 * the status byte.
		 */
		out_drbl = REG32(IPC_HOST2ISH_DOORBELL);
		pkt_len = out_drbl & IPC_HEADER_LENGTH_MASK;

		ret = ipc_read(IPC_PEER_HOST_ID, ipc_host_args, pkt_len);
		host_cmd_args.command = EC_COMMAND_PROTOCOL_3;

		host_cmd_args.result = EC_RES_SUCCESS;
		host_cmd_flags = ipc_host_args->flags;

		/* We only support new style command (v3) now */
		if (host_cmd_args.command == EC_COMMAND_PROTOCOL_3) {
			ipc_packet.send_response = ipc_send_response_packet;

			ipc_packet.request =
			    (const void *)ipc_get_hostcmd_data_range();
			ipc_packet.request_temp = params_copy;
			ipc_packet.request_max = sizeof(params_copy);
			/* Don't know the request size so pass in
			 * the entire buffer
			 */
			ipc_packet.request_size = EC_LPC_HOST_PACKET_SIZE;

			ipc_packet.response =
			    (void *)ipc_get_hostcmd_data_range();
			ipc_packet.response_max = EC_LPC_HOST_PACKET_SIZE;
			ipc_packet.response_size = 0;

			ipc_packet.driver_result = EC_RES_SUCCESS;
			host_packet_receive(&ipc_packet);
			usleep(10);	/* To force yield */

			continue;
		} else {
			/* Old style command unsupported */
			host_cmd_args.result = EC_RES_INVALID_COMMAND;
		}

		/* Hand off to host command handler */
		host_command_received(&host_cmd_args);
	}
}

static void setup_ipc(void)
{

	task_enable_irq(ISH_IPC_HOST2ISH_IRQ);
	task_enable_irq(ISH_IPC_ISH2HOST_CLR_IRQ);

	ipc_set_pimr(IPC_PEER_HOST_ID, SET_PIMR, PIMR_SIGNAL_IN);
	ipc_set_pimr(IPC_PEER_HOST_ID, SET_PIMR, PIMR_SIGNAL_CLR);
}
DECLARE_HOOK(HOOK_CHIPSET_STARTUP, setup_ipc, HOOK_PRIO_FIRST);

static void ipc_init(void)
{
	CPRINTS("ipc_init");

	/* Initialize host args and memory map to all zero */
	memset(ipc_host_args, 0, sizeof(*ipc_host_args));
	memset(lpc_get_memmap_range(), 0, EC_MEMMAP_SIZE);

	setup_ipc();
}
DECLARE_HOOK(HOOK_INIT, ipc_init, HOOK_PRIO_INIT_LPC);

/* On boards without a host, this command is used to set up IPC */
static int ipc_command_init(int argc, char **argv)
{
	ipc_init();
	return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(ipcinit, ipc_command_init, NULL, NULL);
