| /* 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 < fan_get_count(); fan++) { |
| if (!fan_get_enabled(FAN_CH(fan))) |
| all_disabled++; |
| } |
| |
| if (all_disabled >= fan_get_count()) |
| 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 < fan_get_count(); fan++) { |
| if (fan_get_enabled(FAN_CH(fan))) { |
| proc_tach(FAN_CH(fan)); |
| fan_ctrl(FAN_CH(fan)); |
| } |
| } |
| } |
| |
| static void fan_init(void) |
| { |
| int ch, rpm_re, fan_p, s_duty; |
| enum tach_ch_sel tach_ch; |
| |
| for (ch = 0; ch < fan_get_count(); ch++) { |
| |
| rpm_re = fan_tach[pwm_channels[FAN_CH(ch)].channel].rpm_re; |
| fan_p = fan_tach[pwm_channels[FAN_CH(ch)].channel].fan_p; |
| s_duty = fan_tach[pwm_channels[FAN_CH(ch)].channel].s_duty; |
| tach_ch = tach_bind(FAN_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); |