/* Copyright 2018 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.
 */
/*
 * Transport using the Servo V2 SPI1 interface through the FT4232 MPSSE
 * hardware engine (driven by libftdi) in order to send host commands V3
 * directly to a MCU slave SPI controller.
 *
 * It allows to drive a MCU with the cros_ec host SPI interface directly from
 * a developer workstation or another test system.
 *
 * The USB serial number of the servo board can be passed in the 'name'
 * parameter, e.g. :
 * sudo ectool_servo --name=905537-00474 version
 */

#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include <libftdi1/ftdi.h>

#include "comm-host.h"
#include "cros_ec_dev.h"

/* Servo V2 SPI1 interface identifiers */
#define SERVO_V2_USB_VID 0x18d1
#define SERVO_V2_USB_PID 0x5003
#define SERVO_V2_USB_SPI1_INTERFACE INTERFACE_B

/* SPI clock frequency in Hz */
#define SPI_CLOCK_FREQ 1000000

#define FTDI_LATENCY_1MS 2

/* Timeout when waiting for the EC answer to our request */
#define RESP_TIMEOUT 2 /* second */

#ifdef DEBUG
#define debug(format, arg...) printf(format, ##arg)
#else
#define debug(...)
#endif

/* Communication context */
static struct ftdi_context ftdi;

/* Size of a MPSSE command packet */
#define MPSSE_CMD_SIZE 3

enum mpsse_commands {
	ENABLE_ADAPTIVE_CLOCK = 0x96,
	DISABLE_ADAPTIVE_CLOCK = 0x97,
	TCK_X5 = 0x8A,
	TCK_D5 = 0x8B,
	TRISTATE_IO = 0x9E,
};

enum mpsse_pins {
	SCLK = 1,
	MOSI = 2,
	MISO = 4,
	CS_L = 8,
};
/* SCLK/MOSI/CS_L are outputs, MISO is an input */
#define PINS_DIR (SCLK | MOSI | CS_L)

/* SPI mode 0:
 * propagates data on the falling edge
 * and reads data on the rising edge of the clock.
 */
#define SPI_CMD_TX   (MPSSE_DO_WRITE | MPSSE_WRITE_NEG)
#define SPI_CMD_RX   (MPSSE_DO_READ)
#define SPI_CMD_TXRX (MPSSE_DO_WRITE | MPSSE_DO_READ | MPSSE_WRITE_NEG)

static int raw_read(uint8_t *buf, int size)
{
	int rlen;

	while (size) {
		rlen = ftdi_read_data(&ftdi, buf, size);
		if (rlen < 0)
			break;
		buf += rlen;
		size -= rlen;
	}
	return !!size;
}

static int mpsse_set_pins(uint8_t levels)
{
	uint8_t buf[MPSSE_CMD_SIZE] = {0};

	buf[0] = SET_BITS_LOW;
	buf[1] = levels;
	buf[2] = PINS_DIR;

	return ftdi_write_data(&ftdi, buf, sizeof(buf)) != sizeof(buf);
}

static int send_request(int cmd, int version,
			const uint8_t *outdata, size_t outsize)
{
	uint8_t *txbuf;
	struct ec_host_request *request;
	size_t i;
	int ret = -EC_RES_ERROR;
	uint8_t csum = 0;
	size_t block_size = sizeof(struct ec_host_request) + outsize;
	size_t total_len = MPSSE_CMD_SIZE + block_size;

	txbuf = calloc(1, total_len);
	if (!txbuf)
		return -ENOMEM;

	/* MPSSE block size is the full command minus 1 byte */
	txbuf[0] = SPI_CMD_TXRX;
	txbuf[1] = ((block_size - 1) & 0xFF);
	txbuf[2] = (((block_size - 1) >> 8) & 0xFF);

	/* Command header first */
	request = (struct ec_host_request *)(txbuf + MPSSE_CMD_SIZE);
	request->struct_version = EC_HOST_REQUEST_VERSION;
	request->checksum = 0;
	request->command = cmd;
	request->command_version = version;
	request->reserved = 0;
	request->data_len = outsize;

	/* copy the data to transmit after the command header */
	memcpy(txbuf + MPSSE_CMD_SIZE + sizeof(struct ec_host_request),
	       outdata, outsize);

	/* Compute the checksum */
	for (i = MPSSE_CMD_SIZE; i < total_len; i++)
		csum += txbuf[i];
	request->checksum = -csum;

	if (ftdi_write_data(&ftdi, txbuf, total_len) != total_len)
		goto free_request;

	if (raw_read(txbuf, block_size) != 0)
		goto free_request;

	/* Make sure the EC was listening */
	ret = 0;
	for (i = 0; i < block_size; i++) {
		switch (txbuf[i]) {
		case EC_SPI_PAST_END:
		case EC_SPI_RX_BAD_DATA:
		case EC_SPI_NOT_READY:
			ret = txbuf[i];
			/* Fall-through */
		default:
			break;
		}
		if (ret)
			break;
	}

free_request:
	free(txbuf);
	return ret;
}

static int spi_read(void *buf, size_t size)
{
	uint8_t cmd[MPSSE_CMD_SIZE];

	cmd[0] = SPI_CMD_RX;
	cmd[1] = ((size - 1) & 0xFF);
	cmd[2] = (((size - 1) >> 8) & 0xFF);

	if (ftdi_write_data(&ftdi, cmd, sizeof(cmd)) != sizeof(cmd))
		return -EC_RES_ERROR;

	return raw_read(buf, size) != 0;
}

static int get_response(uint8_t *bodydest, size_t bodylen)
{
	uint8_t sum = 0;
	size_t i;
	struct ec_host_response hdr;
	uint8_t status;
	time_t deadline = time(NULL) + RESP_TIMEOUT;

	/*
	 * Read a byte at a time until we see the start of the frame.
	 * This is slow, but often still faster than the EC.
	 */
	while (time(NULL) < deadline) {
		if (spi_read(&status, sizeof(status)))
			goto read_error;
		if (status == EC_SPI_FRAME_START)
			break;
	}
	if (status != EC_SPI_FRAME_START) {
		fprintf(stderr, "timeout wait for response\n");
		return -EC_RES_ERROR;
	}

	/* Now read the response header */
	if (spi_read(&hdr, sizeof(hdr)))
		goto read_error;

	/* Check the header */
	if (hdr.struct_version != EC_HOST_RESPONSE_VERSION) {
		fprintf(stderr, "response version %d (should be %d)\n",
			hdr.struct_version,
			EC_HOST_RESPONSE_VERSION);
		return -EC_RES_ERROR;
	}
	if (hdr.data_len > bodylen) {
		fprintf(stderr, "response data_len %d is > %zd\n",
			hdr.data_len, bodylen);
		return -EC_RES_ERROR;
	}

	/* Read the data if needed */
	if (hdr.data_len && spi_read(bodydest, hdr.data_len))
		goto read_error;

	/* Verify the checksum */
	for (i = 0; i < sizeof(struct ec_host_response); i++)
		sum += ((uint8_t *)&hdr)[i];
	for (i = 0; i < hdr.data_len; i++)
		sum += bodydest[i];
	if (sum) {
		fprintf(stderr, "Checksum invalid\n");
		return -EC_RES_ERROR;
	}

	return hdr.result ? -EECRESULT - hdr.result : 0;

read_error:
	fprintf(stderr, "Read failed: %s\n", ftdi_get_error_string(&ftdi));
	return -EC_RES_ERROR;
}

static int ec_command_servo_spi(int cmd, int version,
				const void *outdata, int outsize,
				void *indata, int insize)
{
	int ret = -EC_RES_ERROR;

	/* Set the chip select low */
	if (mpsse_set_pins(0) != 0) {
		fprintf(stderr, "Start failed: %s\n",
			ftdi_get_error_string(&ftdi));
		return -EC_RES_ERROR;
	}

	if (send_request(cmd, version, outdata, outsize) == 0)
		ret = get_response(indata, insize);

	if (mpsse_set_pins(CS_L) != 0) {
		fprintf(stderr, "Stop failed: %s\n",
			ftdi_get_error_string(&ftdi));
		return -EC_RES_ERROR;
	}
	/* SPI protocol gap ... */
	usleep(10);

	return ret;
}

static int mpsse_set_clock(uint32_t freq)
{
	uint32_t system_clock = 0;
	uint16_t divisor = 0;
	uint8_t buf[MPSSE_CMD_SIZE] = {0};

	if (freq > 6000000) {
		buf[0] = TCK_X5;
		system_clock = 60000000;
	} else {
		buf[0] = TCK_D5;
		system_clock = 12000000;
	}

	if (ftdi_write_data(&ftdi, buf, 1) != 1)
		return -EC_RES_ERROR;

	divisor = (((system_clock / freq) / 2) - 1);

	buf[0] = TCK_DIVISOR;
	buf[1] = (divisor & 0xFF);
	buf[2] = ((divisor >> 8) & 0xFF);

	return ftdi_write_data(&ftdi, buf, MPSSE_CMD_SIZE) != MPSSE_CMD_SIZE;
}

static void servo_spi_close(void)
{
	ftdi_set_bitmode(&ftdi, 0, BITMODE_RESET);
	ftdi_usb_close(&ftdi);
	ftdi_deinit(&ftdi);
}

int comm_init_servo_spi(const char *device_name)
{
	int status;
	uint8_t buf[MPSSE_CMD_SIZE] = {0};
	/* if the user mentioned a device name, use it as serial string */
	const char *serial = strcmp(CROS_EC_DEV_NAME, device_name) ?
		device_name : NULL;

	if (ftdi_init(&ftdi))
		return -EC_RES_ERROR;
	ftdi_set_interface(&ftdi, SERVO_V2_USB_SPI1_INTERFACE);

	status = ftdi_usb_open_desc(&ftdi, SERVO_V2_USB_VID, SERVO_V2_USB_PID,
				    NULL, serial);
	if (status) {
		debug("Can't find a Servo v2 USB device\n");
		return -EC_RES_ERROR;
	}

	status |= ftdi_usb_reset(&ftdi);
	status |= ftdi_set_latency_timer(&ftdi, FTDI_LATENCY_1MS);
	status |= ftdi_set_bitmode(&ftdi, 0, BITMODE_RESET);
	if (status)
		goto err_close;

	ftdi_set_bitmode(&ftdi, 0, BITMODE_MPSSE);
	if (mpsse_set_clock(SPI_CLOCK_FREQ))
		goto err_close;

	/* Disable FTDI internal loopback */
	buf[0] = LOOPBACK_END;
	if (ftdi_write_data(&ftdi, buf, 1) != 1)
		goto err_close;
	/* Ensure adaptive clock is disabled */
	buf[0] = DISABLE_ADAPTIVE_CLOCK;
	if (ftdi_write_data(&ftdi, buf, 1) != 1)
		goto err_close;
	/* Set the idle pin states */
	if (mpsse_set_pins(CS_L) != 0)
		goto err_close;

	ec_command_proto = ec_command_servo_spi;
	/* Set temporary size, will be updated later. */
	ec_max_outsize = EC_PROTO2_MAX_PARAM_SIZE - 8;
	ec_max_insize = EC_PROTO2_MAX_PARAM_SIZE;

	return 0;

err_close:
	servo_spi_close();
	return -EC_RES_ERROR;
}
