/* Copyright 2015 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.
 */

/* PECI interface for Chrome EC */

#include "chipset.h"
#include "clock.h"
#include "common.h"
#include "console.h"
#include "gpio.h"
#include "hooks.h"
#include "peci.h"
#include "registers.h"
#include "temp_sensor.h"
#include "util.h"
#include "timer.h"
#include "task.h"

#define TEMP_AVG_LENGTH 4  /* Should be power of 2 */
static int temp_vals[TEMP_AVG_LENGTH];
static int temp_idx;

#define PECI_TARGET_ADDRESS         0x30
#define PECI_WRITE_DATA_FIFO_SIZE   15
#define PECI_READ_DATA_FIFO_SIZE    16

#define PECI_GET_TEMP_READ_LENGTH   2
#define PECI_GET_TEMP_WRITE_LENGTH  0
#define PECI_GET_TEMP_TIMEOUT_US    200

/* PECI Command Code */
enum peci_command_code {
	PECI_CMD_PING             = 0x00,
	PECI_CMD_GET_DIB          = 0xF7,
	PECI_CMD_GET_TEMP         = 0x01,
	PECI_CMD_RD_PKG_CFG       = 0xA1,
	PECI_CMD_WR_PKG_CFG       = 0xA5,
	PECI_CMD_RD_IAMSR         = 0xB1,
	PECI_CMD_WR_IAMSR         = 0xB5,
	PECI_CMD_RD_PCI_CFG       = 0x61,
	PECI_CMD_WR_PCI_CFG       = 0x65,
	PECI_CMD_RD_PCI_CFG_LOCAL = 0xE1,
	PECI_CMD_WR_PCI_CFG_LOCAL = 0xE5,
};

enum peci_status {
	PECI_STATUS_NO_ERR        = 0x00,
	PECI_STATUS_HOBY          = 0x01,
	PECI_STATUS_FINISH        = 0x02,
	PECI_STATUS_RD_FCS_ERR    = 0x04,
	PECI_STATUS_WR_FCS_ERR    = 0x08,
	PECI_STATUS_EXTERR        = 0x20,
	PECI_STATUS_BUSERR        = 0x40,
	PECI_STATUS_RCV_ERRCODE   = 0x80,
	PECI_STATUS_ERR_NEED_RST  = (PECI_STATUS_BUSERR | PECI_STATUS_EXTERR),
	PECI_STATUS_ANY_ERR       = (PECI_STATUS_RCV_ERRCODE |
					PECI_STATUS_BUSERR |
					PECI_STATUS_EXTERR |
					PECI_STATUS_WR_FCS_ERR |
					PECI_STATUS_RD_FCS_ERR),
	PECI_STATUS_ANY_BIT       = 0xFE,
	PECI_STATUS_TIMEOUT       = 0xFF,
};

static task_id_t peci_current_task;

static void peci_init_vtt_freq(void)
{
	/*
	 * bit2, enable the PECI interrupt generated by data valid event
	 * from PECI.
	 *
	 * bit[1-0], these bits are used to set PECI VTT level.
	 * 00b: 1.10v
	 * 01b: 1.05v
	 * 10b: 1.00v
	 */
	IT83XX_PECI_PADCTLR = 0x06;

	/*
	 * bit[2-0], these bits are used to set PECI host's optimal
	 * transfer rate.
	 * 000b: 2.0 MHz
	 * 001b: 1.0 MHz
	 * 100b: 1.6 MHz
	 */
	IT83XX_PECI_HOCTL2R = 0x01;
}

static void peci_reset(void)
{
	/* Reset PECI */
	IT83XX_GCTRL_RSTC4 |= 0x10;

	/* short delay */
	udelay(15);

	peci_init_vtt_freq();
}

/**
 * Start a PECI transaction
 *
 * @param  addr       client address
 * @param  w_len      write length (no include [Cmd Code] and [AW FCS])
 * @param  r_len      read length (no include [FCS])
 * @param  cmd_code   command code
 * @param  *w_buf How buffer pointer of write data
 * @param  *r_buf How buffer pointer of read data
 * @param  timeout_us transaction timeout unit:us
 *
 * @return zero if successful, non-zero if error
 */
static enum peci_status peci_transaction(uint8_t addr,
				uint8_t w_len,
				uint8_t r_len,
				enum peci_command_code cmd_code,
				uint8_t *w_buf,
				uint8_t *r_buf,
				int timeout_us)
{
	uint8_t status;
	int index;

	/* To enable PECI function pin */
	IT83XX_GPIO_GPCRF6 = 0x00;

	/*
	 * bit5, Both write and read data FIFO pointers will be cleared.
	 *
	 * bit4, This bit enables the PECI host to abort the transaction
	 *       when FCS error occurs.
	 *
	 * bit2, This bit enables the contention mechanism of the PECI bus.
	 *       When this bit is set, the host will abort the transaction
	 *       if the PECI bus is contentious.
	 */
	IT83XX_PECI_HOCTLR |= 0x34;

	/* This register is the target address field of the PECI protocol. */
	IT83XX_PECI_HOTRADDR = addr;

	/* This register is the write length field of the PECI protocol. */
	ASSERT(w_len <= PECI_WRITE_DATA_FIFO_SIZE);

	if (cmd_code == PECI_CMD_PING) {
		/* write length is 0 */
		IT83XX_PECI_HOWRLR = 0x00;
	} else {
		if ((cmd_code == PECI_CMD_WR_PKG_CFG) ||
			(cmd_code == PECI_CMD_WR_IAMSR) ||
			(cmd_code == PECI_CMD_WR_PCI_CFG) ||
			(cmd_code == PECI_CMD_WR_PCI_CFG_LOCAL)) {

			/* write length include Cmd Code + AW FCS */
			IT83XX_PECI_HOWRLR = w_len + 2;

			/* bit1, The bit enables the AW_FCS hardwired mechanism
			 * based on the PECI command. This bit is functional
			 * only when the AW_FCS supported command of
			 * PECI 2.0/3.0/3.1 is issued.
			 * When this bit is set, the hardware will handle the
			 * calculation of AW_FCS.
			 */
			IT83XX_PECI_HOCTLR |= 0x02;
		} else {
			/* write length include Cmd Code */
			IT83XX_PECI_HOWRLR = w_len + 1;

			IT83XX_PECI_HOCTLR &= ~0x02;
		}
	}

	/* This register is the read length field of the PECI protocol. */
	ASSERT(r_len <= PECI_READ_DATA_FIFO_SIZE);
	IT83XX_PECI_HORDLR = r_len;

	/* This register is the command field of the PECI protocol. */
	IT83XX_PECI_HOCMDR = cmd_code;

	/* The write data field of the PECI protocol. */
	for (index = 0x00; index < w_len; index++)
		IT83XX_PECI_HOWRDR = w_buf[index];

	peci_current_task = task_get_current();
	task_clear_pending_irq(IT83XX_IRQ_PECI);
	task_enable_irq(IT83XX_IRQ_PECI);

	/* start */
	IT83XX_PECI_HOCTLR |= 0x01;

	/* pre-set timeout */
	index = timeout_us;
	if (task_wait_event(timeout_us) != TASK_EVENT_TIMER)
		index = 0;

	task_disable_irq(IT83XX_IRQ_PECI);

	peci_current_task = TASK_ID_INVALID;

	if (index < timeout_us) {

		status = IT83XX_PECI_HOSTAR;

		/* any error */
		if (IT83XX_PECI_HOSTAR & PECI_STATUS_ANY_ERR) {

			if (IT83XX_PECI_HOSTAR & PECI_STATUS_ERR_NEED_RST)
				peci_reset();

		} else if (IT83XX_PECI_HOSTAR & PECI_STATUS_FINISH) {

			/* The read data field of the PECI protocol. */
			for (index = 0x00; index < r_len; index++)
				r_buf[index] = IT83XX_PECI_HORDDR;

			/* W/C */
			IT83XX_PECI_HOSTAR = PECI_STATUS_FINISH;
			status = IT83XX_PECI_HOSTAR;
		}
	} else {
		/* transaction timeout */
		status = PECI_STATUS_TIMEOUT;
	}

	/* Don't disable PECI host controller if controller already enable. */
	IT83XX_PECI_HOCTLR = 0x08;

	/* W/C */
	IT83XX_PECI_HOSTAR = PECI_STATUS_ANY_BIT;

	/* Disable PECI function pin */
	IT83XX_GPIO_GPCRF6 = 0x80;

	return status;
}

int peci_get_cpu_temp(void)
{
	uint8_t r_buf[PECI_GET_TEMP_READ_LENGTH] = {0};
	int cpu_temp = -1;

	if (peci_transaction(PECI_TARGET_ADDRESS,
				PECI_GET_TEMP_WRITE_LENGTH,
				PECI_GET_TEMP_READ_LENGTH,
				PECI_CMD_GET_TEMP,
				NULL,
				r_buf,
				PECI_GET_TEMP_TIMEOUT_US) ==
				PECI_STATUS_NO_ERR) {

		/* Get relative raw data of temperature. */
		cpu_temp = (r_buf[1] << 8) | r_buf[0];

#ifdef CONFIG_PECI_TJMAX
		/* Convert relative raw data to degrees C. */
		cpu_temp = ((cpu_temp ^ 0xFFFF) + 1) >> 6;
		/* temperature in K */
		cpu_temp = (CONFIG_PECI_TJMAX - cpu_temp) + 273;
#endif
	}

	return cpu_temp;
}

int peci_temp_sensor_get_val(int idx, int *temp_ptr)
{
	int sum = 0;
	int success_cnt = 0;
	int i;

	if (!chipset_in_state(CHIPSET_STATE_ON))
		return EC_ERROR_NOT_POWERED;

	for (i = 0; i < TEMP_AVG_LENGTH; ++i) {
		if (temp_vals[i] >= 0) {
			success_cnt++;
			sum += temp_vals[i];
		}
	}

	/*
	 * Require at least two valid samples. When the AP transitions into S0,
	 * it is possible, depending on the timing of the PECI sample, to read
	 * an invalid temperature. This is very rare, but when it does happen
	 * the temperature returned is CONFIG_PECI_TJMAX. Requiring two valid
	 * samples here assures us that one bad maximum temperature reading
	 * when entering S0 won't cause us to trigger an over temperature.
	 */
	if (success_cnt < 2)
		return EC_ERROR_UNKNOWN;

	*temp_ptr = sum / success_cnt;
	return EC_SUCCESS;
}

static void peci_temp_sensor_poll(void)
{
	temp_vals[temp_idx] = peci_get_cpu_temp();
	temp_idx = (temp_idx + 1) & (TEMP_AVG_LENGTH - 1);
}
DECLARE_HOOK(HOOK_TICK, peci_temp_sensor_poll, HOOK_PRIO_TEMP_SENSOR);

void peci_interrupt(void)
{
	task_clear_pending_irq(IT83XX_IRQ_PECI);
	task_disable_irq(IT83XX_IRQ_PECI);

	if (peci_current_task != TASK_ID_INVALID)
		task_wake(peci_current_task);
}

static void peci_init(void)
{
	int i;

	clock_enable_peripheral(CGC_OFFSET_PECI, 0, 0);
	peci_init_vtt_freq();

	/* bit3,this bit enables the PECI host controller. */
	IT83XX_PECI_HOCTLR |= 0x08;

	/* bit4, PECI enable */
	IT83XX_GPIO_GRC2 |= 0x10;

	/* Initialize temperature reading buffer to a sane value. */
	for (i = 0; i < TEMP_AVG_LENGTH; ++i)
		temp_vals[i] = 300; /* 27 C */
}
DECLARE_HOOK(HOOK_INIT, peci_init, HOOK_PRIO_DEFAULT);

/*****************************************************************************/
/* Console commands */

static int peci_cmd(int argc, char **argv)
{
	uint8_t r_buf[PECI_READ_DATA_FIFO_SIZE] = {0};
	uint8_t w_buf[PECI_WRITE_DATA_FIFO_SIZE] = {0};

	int addr, wlen, rlen, cmd, time_us, param;
	char *e;

	if ((argc < 6) || (argc > 8))
		return EC_ERROR_PARAM_COUNT;

	addr = strtoi(argv[1], &e, 0);
	if (*e)
		return EC_ERROR_PARAM1;

	wlen = strtoi(argv[2], &e, 0);
	if (*e)
		return EC_ERROR_PARAM2;

	rlen = strtoi(argv[3], &e, 0);
	if (*e)
		return EC_ERROR_PARAM3;

	cmd = strtoi(argv[4], &e, 0);
	if (*e)
		return EC_ERROR_PARAM4;

	time_us = strtoi(argv[5], &e, 0);
	if (*e)
		return EC_ERROR_PARAM5;

	if (argc > 6) {
		param = strtoi(argv[6], &e, 0);
		if (*e)
			return EC_ERROR_PARAM6;

		/* MSB of parameter */
		w_buf[3] = (uint8_t)(param >> 24);
		/* LSB of parameter */
		w_buf[2] = (uint8_t)(param >> 16);
		/* Index */
		w_buf[1] = (uint8_t)(param >> 8);
		/* Host ID[7:1] & Retry[0] */
		w_buf[0] = (uint8_t)(param >> 0);

		if (argc > 7) {
			param = strtoi(argv[7], &e, 0);
			if (*e)
				return EC_ERROR_PARAM7;

			/* Data (1, 2 or 4 bytes) */
			w_buf[7] = (uint8_t)(param >> 24);
			w_buf[6] = (uint8_t)(param >> 16);
			w_buf[5] = (uint8_t)(param >> 8);
			w_buf[4] = (uint8_t)(param >> 0);
		}
	} else {
		wlen = 0x00;
	}

	if (peci_transaction(addr, wlen, rlen, cmd, w_buf, r_buf, time_us)) {
		ccprintf("PECI transaction error\n");
		return EC_ERROR_UNKNOWN;
	}
	ccprintf("PECI read data: %.*h\n", rlen, r_buf);
	return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(peci, peci_cmd,
			"addr wlen rlen cmd timeout(us)",
			"PECI command");

static int command_peci_temp(int argc, char **argv)
{
	int t = peci_get_cpu_temp();
	if (t == -1) {
		ccprintf("PECI get cpu temp error\n");
		return EC_ERROR_UNKNOWN;
	}
	ccprintf("CPU temp = %d K = %d C\n", t, K_TO_C(t));
	return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(pecitemp, command_peci_temp,
			NULL,
			"Print CPU temperature");
