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

/* Fan control module. */

#include "clock.h"
#include "fan.h"
#include "gpio.h"
#include "hooks.h"
#include "hwtimer_chip.h"
#include "math_util.h"
#include "pwm.h"
#include "pwm_chip.h"
#include "registers.h"
#include "system.h"
#include "task.h"
#include "util.h"

#define TACH_EC_FREQ                8000000
#define FAN_CTRL_BASED_MS           10
#define FAN_CTRL_INTERVAL_MAX_MS    60

/* The sampling rate (fs) is FreqEC / 128 */
#define TACH_DATA_VALID_TIMEOUT_MS  (0xFFFF * 128 / (TACH_EC_FREQ / 1000))

/*
 * Fan Speed (RPM) = 60 / (1/fs sec * {FnTMRR, FnTLRR} * P)
 * n denotes 1 or 2.
 * P denotes the numbers of square pulses per revolution.
 * And {FnTMRR, FnTLRR} = 0000h denotes Fan Speed is zero.
 * The sampling rate (fs) is FreqEC / 128.
 */
/* pulse, the numbers of square pulses per revolution. */
#define TACH0_TO_RPM(pulse, raw) (60 * TACH_EC_FREQ / 128 / pulse / raw)
#define TACH1_TO_RPM(pulse, raw) (raw * 120 / (pulse * 2))

enum fan_output_s {
	FAN_DUTY_I  = 0x01,
	FAN_DUTY_R  = 0x02,
	FAN_DUTY_OV = 0x03,
	FAN_DUTY_DONE = 0x04,
};

struct fan_info {
	unsigned int    flags;
	int             fan_mode;
	int             fan_p;
	int             rpm_target;
	int             rpm_actual;
	int             tach_valid_ms;
	int             rpm_re;
	int             fan_ms;
	int             fan_ms_idx;
	int             startup_duty;
	enum fan_status fan_sts;
	int             enabled;
};
static struct fan_info fan_info_data[TACH_CH_COUNT];

static enum tach_ch_sel tach_bind(int ch)
{
	return fan_tach[pwm_channels[ch].channel].ch_tach;
}

static void fan_set_interval(int ch)
{
	int diff, fan_ms;
	enum tach_ch_sel tach_ch;

	tach_ch = tach_bind(ch);

	diff = ABS(fan_info_data[tach_ch].rpm_target -
		fan_info_data[tach_ch].rpm_actual) / 100;

	fan_ms = FAN_CTRL_INTERVAL_MAX_MS;

	fan_ms -= diff;
	if (fan_ms < FAN_CTRL_BASED_MS)
		fan_ms = FAN_CTRL_BASED_MS;

	fan_info_data[tach_ch].fan_ms = fan_ms;
}

static void fan_init_start(int ch)
{
	enum tach_ch_sel tach_ch;

	tach_ch = tach_bind(ch);

	if (tach_ch < TACH_CH_COUNT)
		fan_set_duty(ch, fan_info_data[tach_ch].startup_duty);
}

static int fan_all_disabled(void)
{
	int fan, all_disabled = 0;

	for (fan = 0; fan < CONFIG_FANS; fan++) {
		if (!fan_get_enabled(fans[fan].ch))
			all_disabled++;
	}

	if (all_disabled >= CONFIG_FANS)
		return 1;

	return 0;
}

void fan_set_enabled(int ch, int enabled)
{
	enum tach_ch_sel tach_ch;

	tach_ch = tach_bind(ch);

	/* enable */
	if (enabled) {
		if (tach_ch < TACH_CH_COUNT)
			fan_info_data[tach_ch].fan_sts = FAN_STATUS_CHANGING;

		disable_sleep(SLEEP_MASK_FAN);
		/* enable timer interrupt for fan control */
		ext_timer_start(FAN_CTRL_EXT_TIMER, 1);
	/* disable */
	} else {
		fan_set_duty(ch, 0);

		if (tach_ch < TACH_CH_COUNT) {
			fan_info_data[tach_ch].rpm_actual = 0;
			fan_info_data[tach_ch].fan_sts = FAN_STATUS_STOPPED;
		}
	}

	/* on/off */
	if (tach_ch < TACH_CH_COUNT) {
		fan_info_data[tach_ch].enabled = enabled;
		fan_info_data[tach_ch].tach_valid_ms = 0;
	}

	pwm_enable(ch, enabled);

	if (!enabled) {
		/* disable timer interrupt if all fan off. */
		if (fan_all_disabled()) {
			ext_timer_stop(FAN_CTRL_EXT_TIMER, 1);
			enable_sleep(SLEEP_MASK_FAN);
		}
	}
}

int fan_get_enabled(int ch)
{
	enum tach_ch_sel tach_ch;

	tach_ch = tach_bind(ch);

	if (tach_ch < TACH_CH_COUNT)
		return pwm_get_enabled(ch) && fan_info_data[tach_ch].enabled;
	else
		return 0;
}

void fan_set_duty(int ch, int percent)
{
	pwm_set_duty(ch, percent);
}

int fan_get_duty(int ch)
{
	return pwm_get_duty(ch);
}

int fan_get_rpm_mode(int ch)
{
	enum tach_ch_sel tach_ch;

	tach_ch = tach_bind(ch);

	if (tach_ch < TACH_CH_COUNT)
		return fan_info_data[tach_ch].fan_mode;
	else
		return EC_ERROR_UNKNOWN;
}

void fan_set_rpm_mode(int ch, int rpm_mode)
{
	enum tach_ch_sel tach_ch;

	tach_ch = tach_bind(ch);

	if (tach_ch < TACH_CH_COUNT)
		fan_info_data[tach_ch].fan_mode = rpm_mode;
}

int fan_get_rpm_actual(int ch)
{
	enum tach_ch_sel tach_ch;

	tach_ch = tach_bind(ch);

	if (tach_ch < TACH_CH_COUNT)
		return fan_info_data[tach_ch].rpm_actual;
	else
		return EC_ERROR_UNKNOWN;
}

int fan_get_rpm_target(int ch)
{
	enum tach_ch_sel tach_ch;

	tach_ch = tach_bind(ch);

	if (tach_ch < TACH_CH_COUNT)
		return fan_info_data[tach_ch].rpm_target;
	else
		return EC_ERROR_UNKNOWN;
}

test_mockable void fan_set_rpm_target(int ch, int rpm)
{
	enum tach_ch_sel tach_ch;

	tach_ch = tach_bind(ch);

	if (tach_ch < TACH_CH_COUNT)
		fan_info_data[tach_ch].rpm_target = rpm;
}

enum fan_status fan_get_status(int ch)
{
	enum tach_ch_sel tach_ch;

	tach_ch = tach_bind(ch);

	if (tach_ch < TACH_CH_COUNT)
		return fan_info_data[tach_ch].fan_sts;
	else
		return FAN_STATUS_STOPPED;
}

/**
 * Return non-zero if fan is enabled but stalled.
 */
int fan_is_stalled(int ch)
{
	/* Must be enabled with non-zero target to stall */
	if (!fan_get_enabled(ch) ||
		fan_get_rpm_target(ch) == 0 ||
		!fan_get_duty(ch))
		return 0;

	/* Check for stall condition */
	return fan_get_status(ch) == FAN_STATUS_STOPPED;
}

void fan_channel_setup(int ch, unsigned int flags)
{
	enum tach_ch_sel tach_ch;

	tach_ch = tach_bind(ch);

	if (tach_ch < TACH_CH_COUNT)
		fan_info_data[tach_ch].flags = flags;
}

static void fan_ctrl(int ch)
{
	int status = -1, adjust = 0;
	int rpm_actual, rpm_target, rpm_re, duty;
	enum tach_ch_sel tach_ch;

	tach_ch = tach_bind(ch);

	fan_info_data[tach_ch].fan_ms_idx += FAN_CTRL_BASED_MS;
	if (fan_info_data[tach_ch].fan_ms_idx >
		fan_info_data[tach_ch].fan_ms) {
		fan_info_data[tach_ch].fan_ms_idx = 0x00;
		adjust = 1;
	}

	if (adjust) {
		/* get current pwm output duty */
		duty = fan_get_duty(ch);

		/* rpm mode */
		if (fan_info_data[tach_ch].fan_mode) {
			rpm_actual = fan_info_data[tach_ch].rpm_actual;
			rpm_target = fan_info_data[tach_ch].rpm_target;
			rpm_re = fan_info_data[tach_ch].rpm_re;

			if (rpm_actual < (rpm_target - rpm_re)) {
				if (duty == 100) {
					status = FAN_DUTY_OV;
				} else {
					if (duty == 0)
						fan_init_start(ch);

					pwm_duty_inc(ch);
					status = FAN_DUTY_I;
				}
			} else if (rpm_actual > (rpm_target + rpm_re)) {
				if (duty == 0) {
					status = FAN_DUTY_OV;
				} else {
					pwm_duty_reduce(ch);
					status = FAN_DUTY_R;
				}
			} else {
				status = FAN_DUTY_DONE;
			}
		} else {
			fan_info_data[tach_ch].fan_sts = FAN_STATUS_LOCKED;
		}

		if (status == FAN_DUTY_DONE) {
			fan_info_data[tach_ch].fan_sts = FAN_STATUS_LOCKED;
		} else if ((status == FAN_DUTY_I) || (status == FAN_DUTY_R)) {
			fan_info_data[tach_ch].fan_sts = FAN_STATUS_CHANGING;
		} else if (status == FAN_DUTY_OV) {
			fan_info_data[tach_ch].fan_sts = FAN_STATUS_FRUSTRATED;

			if (!fan_info_data[tach_ch].rpm_actual && duty)
				fan_info_data[tach_ch].fan_sts =
					FAN_STATUS_STOPPED;
		}
	}
}

static int tach_ch_valid(enum tach_ch_sel tach_ch)
{
	int valid = 0;

	switch (tach_ch) {
	case TACH_CH_TACH0A:
		if ((IT83XX_PWM_TSWCTRL & 0x0C) == 0x08)
			valid = 1;
		break;
	case TACH_CH_TACH1A:
		if ((IT83XX_PWM_TSWCTRL & 0x03) == 0x02)
			valid = 1;
		break;
	case TACH_CH_TACH0B:
		if ((IT83XX_PWM_TSWCTRL & 0x0C) == 0x0C)
			valid = 1;
		break;
	case TACH_CH_TACH1B:
		if ((IT83XX_PWM_TSWCTRL & 0x03) == 0x03)
			valid = 1;
		break;
	default:
		break;
	}

	return valid;
}

static int get_tach0_rpm(int fan_p)
{
	uint16_t rpm;

	/* TACH0A / TACH0B data is valid */
	if (IT83XX_PWM_TSWCTRL & 0x08) {
		rpm = (IT83XX_PWM_F1TMRR << 8) | IT83XX_PWM_F1TLRR;

		if (rpm)
			rpm = TACH0_TO_RPM(fan_p, rpm);

		/* W/C */
		IT83XX_PWM_TSWCTRL |= 0x08;
		return rpm;
	}
	return -1;
}

static int get_tach1_rpm(int fan_p)
{
	uint16_t rpm;

	/* TACH1A / TACH1B data is valid */
	if (IT83XX_PWM_TSWCTRL & 0x02) {
		rpm = (IT83XX_PWM_F2TMRR << 8) | IT83XX_PWM_F2TLRR;

		if (rpm)
			rpm = TACH1_TO_RPM(fan_p, rpm);

		/* W/C */
		IT83XX_PWM_TSWCTRL |= 0x02;
		return rpm;
	}
	return -1;
}

static void proc_tach(int ch)
{
	int t_rpm;
	enum tach_ch_sel tach_ch;

	tach_ch = tach_bind(ch);

	/* tachometer data valid */
	if (tach_ch_valid(tach_ch)) {
		if ((tach_ch == TACH_CH_TACH0A) || (tach_ch == TACH_CH_TACH0B))
			t_rpm = get_tach0_rpm(fan_info_data[tach_ch].fan_p);
		else
			t_rpm = get_tach1_rpm(fan_info_data[tach_ch].fan_p);

		fan_info_data[tach_ch].rpm_actual = t_rpm;
		fan_set_interval(ch);
		fan_info_data[tach_ch].tach_valid_ms = 0;
	} else {
		fan_info_data[tach_ch].tach_valid_ms += FAN_CTRL_BASED_MS;
		if (fan_info_data[tach_ch].tach_valid_ms >
			TACH_DATA_VALID_TIMEOUT_MS)
			fan_info_data[tach_ch].rpm_actual = 0;
	}
}

void fan_ext_timer_interrupt(void)
{
	int fan;

	task_clear_pending_irq(et_ctrl_regs[FAN_CTRL_EXT_TIMER].irq);

	for (fan = 0; fan < CONFIG_FANS; fan++) {
		if (fan_get_enabled(fans[fan].ch)) {
			proc_tach(fans[fan].ch);
			fan_ctrl(fans[fan].ch);
		}
	}
}

static void fan_init(void)
{
	int ch, rpm_re, fan_p, s_duty;
	enum tach_ch_sel tach_ch;

	for (ch = 0; ch < CONFIG_FANS; ch++) {

		rpm_re = fan_tach[pwm_channels[fans[ch].ch].channel].rpm_re;
		fan_p = fan_tach[pwm_channels[fans[ch].ch].channel].fan_p;
		s_duty = fan_tach[pwm_channels[fans[ch].ch].channel].s_duty;
		tach_ch = tach_bind(fans[ch].ch);

		if (tach_ch < TACH_CH_COUNT) {

			if (tach_ch == TACH_CH_TACH0B) {
				/* GPJ2 will select TACH0B as its alt. */
				IT83XX_GPIO_GRC5 |= 0x01;
				/* bit2, to select TACH0B */
				IT83XX_PWM_TSWCTRL |= 0x04;
			} else if (tach_ch == TACH_CH_TACH1B) {
				/* GPJ3 will select TACH1B as its alt. */
				IT83XX_GPIO_GRC5 |= 0x02;
				/* bit0, to select TACH1B */
				IT83XX_PWM_TSWCTRL |= 0x01;
			}

			fan_info_data[tach_ch].flags = 0;
			fan_info_data[tach_ch].fan_mode = 0;
			fan_info_data[tach_ch].rpm_target = 0;
			fan_info_data[tach_ch].rpm_actual = 0;
			fan_info_data[tach_ch].tach_valid_ms = 0;
			fan_info_data[tach_ch].fan_ms_idx = 0;
			fan_info_data[tach_ch].enabled = 0;
			fan_info_data[tach_ch].fan_p = fan_p;
			fan_info_data[tach_ch].rpm_re = rpm_re;
			fan_info_data[tach_ch].fan_ms = FAN_CTRL_BASED_MS;
			fan_info_data[tach_ch].fan_sts = FAN_STATUS_STOPPED;
			fan_info_data[tach_ch].startup_duty = s_duty;
		}
	}

	/* init external timer for fan control */
	ext_timer_ms(FAN_CTRL_EXT_TIMER, EXT_PSR_32P768K_HZ, 0, 0,
			FAN_CTRL_BASED_MS, 1, 0);
}
DECLARE_HOOK(HOOK_INIT, fan_init, HOOK_PRIO_INIT_FAN);
