| /* Copyright 2013 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. |
| */ |
| |
| /* Basic Chrome OS fan control */ |
| |
| #include "assert.h" |
| #include "chipset.h" |
| #include "common.h" |
| #include "console.h" |
| #include "fan.h" |
| #include "gpio.h" |
| #include "hooks.h" |
| #include "host_command.h" |
| #include "printf.h" |
| #include "system.h" |
| #include "util.h" |
| |
| /* True if we're listening to the thermal control task. False if we're setting |
| * things manually. */ |
| static int thermal_control_enabled[CONFIG_FANS]; |
| |
| int is_thermal_control_enabled(int idx) |
| { |
| return thermal_control_enabled[idx]; |
| } |
| |
| #ifdef CONFIG_FAN_UPDATE_PERIOD |
| /* Should we ignore the fans for a while? */ |
| static int fan_update_counter[CONFIG_FANS]; |
| #endif |
| |
| /* |
| * Number of fans. |
| * |
| * Use fan_get_count and fan_set_count to access it. It should be set only |
| * before HOOK_INIT/HOOK_PRIO_DEFAULT. |
| */ |
| static int fan_count = CONFIG_FANS; |
| |
| int fan_get_count(void) |
| { |
| return fan_count; |
| } |
| |
| void fan_set_count(int count) |
| { |
| /* You can only decrease the count. */ |
| assert(count <= CONFIG_FANS); |
| fan_count = count; |
| } |
| |
| #ifndef CONFIG_FAN_RPM_CUSTOM |
| /* This is the default implementation. It's only called over [0,100]. |
| * Convert the percentage to a target RPM. We can't simply scale all |
| * the way down to zero because most fans won't turn that slowly, so |
| * we'll map [1,100] => [FAN_MIN,FAN_MAX], and [0] => "off". |
| */ |
| int fan_percent_to_rpm(int fan, int pct) |
| { |
| int rpm, max, min; |
| |
| if (!pct) { |
| rpm = 0; |
| } else { |
| min = fans[fan].rpm->rpm_min; |
| max = fans[fan].rpm->rpm_max; |
| rpm = ((pct - 1) * max + (100 - pct) * min) / 99; |
| } |
| |
| return rpm; |
| } |
| #endif /* CONFIG_FAN_RPM_CUSTOM */ |
| |
| /* The thermal task will only call this function with pct in [0,100]. */ |
| test_mockable void fan_set_percent_needed(int fan, int pct) |
| { |
| int actual_rpm, new_rpm; |
| |
| if (!is_thermal_control_enabled(fan)) |
| return; |
| |
| #ifdef CONFIG_FAN_UPDATE_PERIOD |
| /* Only set each fan every so often, to avoid rapid changes. */ |
| fan_update_counter[fan] %= CONFIG_FAN_UPDATE_PERIOD; |
| if (fan_update_counter[fan]++) |
| return; |
| #endif |
| |
| new_rpm = fan_percent_to_rpm(fan, pct); |
| actual_rpm = fan_get_rpm_actual(FAN_CH(fan)); |
| |
| /* If we want to turn and the fans are currently significantly below |
| * the minimum turning speed, we should turn at least as fast as the |
| * necessary start speed instead. */ |
| if (new_rpm && |
| actual_rpm < fans[fan].rpm->rpm_min * 9 / 10 && |
| new_rpm < fans[fan].rpm->rpm_start) |
| new_rpm = fans[fan].rpm->rpm_start; |
| |
| fan_set_rpm_target(FAN_CH(fan), new_rpm); |
| } |
| |
| static void set_enabled(int fan, int enable) |
| { |
| fan_set_enabled(FAN_CH(fan), enable); |
| |
| if (fans[fan].conf->enable_gpio >= 0) |
| gpio_set_level(fans[fan].conf->enable_gpio, enable); |
| } |
| |
| test_export_static void set_thermal_control_enabled(int fan, int enable) |
| { |
| thermal_control_enabled[fan] = enable; |
| |
| /* If controlling the fan, need it in RPM-control mode */ |
| if (enable) |
| fan_set_rpm_mode(FAN_CH(fan), 1); |
| } |
| |
| static void set_duty_cycle(int fan, int percent) |
| { |
| /* Move the fan to manual control */ |
| fan_set_rpm_mode(FAN_CH(fan), 0); |
| |
| /* enable the fan when non-zero duty */ |
| set_enabled(fan, (percent > 0) ? 1 : 0); |
| |
| /* Disable thermal engine automatic fan control. */ |
| set_thermal_control_enabled(fan, 0); |
| |
| /* Set the duty cycle */ |
| fan_set_duty(FAN_CH(fan), percent); |
| } |
| |
| /*****************************************************************************/ |
| /* Console commands */ |
| |
| static int cc_fanauto(int argc, char **argv) |
| { |
| char *e; |
| int fan = 0; |
| |
| if (fan_count > 1) { |
| if (argc < 2) { |
| ccprintf("fan number is required as the first arg\n"); |
| return EC_ERROR_PARAM_COUNT; |
| } |
| fan = strtoi(argv[1], &e, 0); |
| if (*e || fan >= fan_count) |
| return EC_ERROR_PARAM1; |
| argc--; |
| argv++; |
| } |
| |
| set_thermal_control_enabled(fan, 1); |
| return EC_SUCCESS; |
| } |
| DECLARE_CONSOLE_COMMAND(fanauto, cc_fanauto, |
| "{fan}", |
| "Enable thermal fan control"); |
| |
| /* Return 0 for off, 1 for on, -1 for unknown */ |
| static int is_powered(int fan) |
| { |
| int is_pgood = -1; |
| |
| /* If we have an enable output, see if it's on or off. */ |
| if (fans[fan].conf->enable_gpio >= 0) |
| is_pgood = gpio_get_level(fans[fan].conf->enable_gpio); |
| /* If we have a pgood input, it overrides any enable output. */ |
| if (fans[fan].conf->pgood_gpio >= 0) |
| is_pgood = gpio_get_level(fans[fan].conf->pgood_gpio); |
| |
| return is_pgood; |
| } |
| |
| static int cc_faninfo(int argc, char **argv) |
| { |
| static const char * const human_status[] = { |
| "not spinning", "changing", "locked", "frustrated" |
| }; |
| int tmp, is_pgood; |
| int fan; |
| char leader[20] = ""; |
| for (fan = 0; fan < fan_count; fan++) { |
| if (fan_count > 1) |
| snprintf(leader, sizeof(leader), "Fan %d ", fan); |
| if (fan) |
| ccprintf("\n"); |
| ccprintf("%sActual: %4d rpm\n", leader, |
| fan_get_rpm_actual(FAN_CH(fan))); |
| ccprintf("%sTarget: %4d rpm\n", leader, |
| fan_get_rpm_target(FAN_CH(fan))); |
| ccprintf("%sDuty: %d%%\n", leader, |
| fan_get_duty(FAN_CH(fan))); |
| tmp = fan_get_status(FAN_CH(fan)); |
| ccprintf("%sStatus: %d (%s)\n", leader, |
| tmp, human_status[tmp]); |
| ccprintf("%sMode: %s\n", leader, |
| fan_get_rpm_mode(FAN_CH(fan)) ? "rpm" : "duty"); |
| ccprintf("%sAuto: %s\n", leader, |
| is_thermal_control_enabled(fan) ? "yes" : "no"); |
| ccprintf("%sEnable: %s\n", leader, |
| fan_get_enabled(FAN_CH(fan)) ? "yes" : "no"); |
| is_pgood = is_powered(fan); |
| if (is_pgood >= 0) |
| ccprintf("%sPower: %s\n", leader, |
| is_pgood ? "yes" : "no"); |
| } |
| |
| return EC_SUCCESS; |
| } |
| DECLARE_CONSOLE_COMMAND(faninfo, cc_faninfo, |
| NULL, |
| "Print fan info"); |
| |
| static int cc_fanset(int argc, char **argv) |
| { |
| int rpm; |
| char *e; |
| int fan = 0; |
| |
| if (fan_count == 0) { |
| ccprintf("Fan count is zero\n"); |
| return EC_ERROR_INVAL; |
| } |
| |
| if (fan_count > 1) { |
| if (argc < 2) { |
| ccprintf("fan number is required as the first arg\n"); |
| return EC_ERROR_PARAM_COUNT; |
| } |
| fan = strtoi(argv[1], &e, 0); |
| if (*e || fan >= fan_count) |
| return EC_ERROR_PARAM1; |
| argc--; |
| argv++; |
| } |
| |
| if (argc < 2) |
| return EC_ERROR_PARAM_COUNT; |
| |
| rpm = strtoi(argv[2], &e, 0); |
| if (*e == '%') { /* Wait, that's a percentage */ |
| ccprintf("Fan rpm given as %d%%\n", rpm); |
| if (rpm < 0) |
| rpm = 0; |
| else if (rpm > 100) |
| rpm = 100; |
| rpm = fan_percent_to_rpm(fan, rpm); |
| } else if (*e) { |
| return EC_ERROR_PARAM1; |
| } |
| |
| /* Move the fan to automatic control */ |
| fan_set_rpm_mode(FAN_CH(fan), 1); |
| |
| /* enable the fan when non-zero rpm */ |
| set_enabled(fan, (rpm > 0) ? 1 : 0); |
| |
| /* Disable thermal engine automatic fan control. */ |
| set_thermal_control_enabled(fan, 0); |
| |
| fan_set_rpm_target(FAN_CH(fan), rpm); |
| |
| ccprintf("Setting fan %d rpm target to %d\n", fan, rpm); |
| |
| return EC_SUCCESS; |
| } |
| DECLARE_CONSOLE_COMMAND(fanset, cc_fanset, |
| "{fan} (rpm | pct%)", |
| "Set fan speed"); |
| |
| static int cc_fanduty(int argc, char **argv) |
| { |
| int percent = 0; |
| char *e; |
| int fan = 0; |
| |
| if (fan_count == 0) { |
| ccprintf("Fan count is zero\n"); |
| return EC_ERROR_INVAL; |
| } |
| |
| if (fan_count > 1) { |
| if (argc < 2) { |
| ccprintf("fan number is required as the first arg\n"); |
| return EC_ERROR_PARAM_COUNT; |
| } |
| fan = strtoi(argv[1], &e, 0); |
| if (*e || fan >= fan_count) |
| return EC_ERROR_PARAM1; |
| argc--; |
| argv++; |
| } |
| |
| if (argc < 2) |
| return EC_ERROR_PARAM_COUNT; |
| |
| percent = strtoi(argv[1], &e, 0); |
| if (*e) |
| return EC_ERROR_PARAM1; |
| |
| ccprintf("Setting fan %d duty cycle to %d%%\n", fan, percent); |
| set_duty_cycle(fan, percent); |
| |
| return EC_SUCCESS; |
| } |
| DECLARE_CONSOLE_COMMAND(fanduty, cc_fanduty, |
| "{fan} percent", |
| "Set fan duty cycle"); |
| |
| /*****************************************************************************/ |
| /* DPTF interface functions */ |
| |
| /* 0-100% if in duty mode. -1 if not */ |
| int dptf_get_fan_duty_target(void) |
| { |
| int fan = 0; /* TODO(crosbug.com/p/23803) */ |
| |
| if (fan_count == 0) |
| return -1; |
| |
| if (is_thermal_control_enabled(fan) || fan_get_rpm_mode(FAN_CH(fan))) |
| return -1; |
| |
| return fan_get_duty(FAN_CH(fan)); |
| } |
| |
| /* 0-100% sets duty, out of range means let the EC drive */ |
| void dptf_set_fan_duty_target(int pct) |
| { |
| int fan; |
| |
| if (pct < 0 || pct > 100) { |
| /* TODO(crosbug.com/p/23803) */ |
| for (fan = 0; fan < fan_count; fan++) |
| set_thermal_control_enabled(fan, 1); |
| } else { |
| /* TODO(crosbug.com/p/23803) */ |
| for (fan = 0; fan < fan_count; fan++) |
| set_duty_cycle(fan, pct); |
| } |
| } |
| |
| /*****************************************************************************/ |
| /* Host commands */ |
| |
| static enum ec_status |
| hc_pwm_get_fan_target_rpm(struct host_cmd_handler_args *args) |
| { |
| struct ec_response_pwm_get_fan_rpm *r = args->response; |
| |
| if (fan_count == 0) |
| return EC_RES_ERROR; |
| |
| /* TODO(crosbug.com/p/23803) */ |
| r->rpm = fan_get_rpm_target(FAN_CH(0)); |
| args->response_size = sizeof(*r); |
| |
| return EC_RES_SUCCESS; |
| } |
| DECLARE_HOST_COMMAND(EC_CMD_PWM_GET_FAN_TARGET_RPM, |
| hc_pwm_get_fan_target_rpm, |
| EC_VER_MASK(0)); |
| |
| static enum ec_status |
| hc_pwm_set_fan_target_rpm(struct host_cmd_handler_args *args) |
| { |
| const struct ec_params_pwm_set_fan_target_rpm_v1 *p_v1 = args->params; |
| const struct ec_params_pwm_set_fan_target_rpm_v0 *p_v0 = args->params; |
| int fan; |
| |
| if (args->version == 0) { |
| for (fan = 0; fan < fan_count; fan++) { |
| /* enable the fan if rpm is non-zero */ |
| set_enabled(fan, (p_v0->rpm > 0) ? 1 : 0); |
| |
| set_thermal_control_enabled(fan, 0); |
| fan_set_rpm_mode(FAN_CH(fan), 1); |
| fan_set_rpm_target(FAN_CH(fan), p_v0->rpm); |
| } |
| |
| return EC_RES_SUCCESS; |
| } |
| |
| fan = p_v1->fan_idx; |
| if (fan >= fan_count) |
| return EC_RES_ERROR; |
| |
| /* enable the fan if rpm is non-zero */ |
| set_enabled(fan, (p_v1->rpm > 0) ? 1 :0); |
| |
| set_thermal_control_enabled(fan, 0); |
| fan_set_rpm_mode(FAN_CH(fan), 1); |
| fan_set_rpm_target(FAN_CH(fan), p_v1->rpm); |
| |
| return EC_RES_SUCCESS; |
| } |
| DECLARE_HOST_COMMAND(EC_CMD_PWM_SET_FAN_TARGET_RPM, |
| hc_pwm_set_fan_target_rpm, |
| EC_VER_MASK(0) | EC_VER_MASK(1)); |
| |
| static enum ec_status hc_pwm_set_fan_duty(struct host_cmd_handler_args *args) |
| { |
| const struct ec_params_pwm_set_fan_duty_v1 *p_v1 = args->params; |
| const struct ec_params_pwm_set_fan_duty_v0 *p_v0 = args->params; |
| int fan; |
| |
| if (args->version == 0) { |
| for (fan = 0; fan < fan_count; fan++) |
| set_duty_cycle(fan, p_v0->percent); |
| |
| return EC_RES_SUCCESS; |
| } |
| |
| fan = p_v1->fan_idx; |
| if (fan >= fan_count) |
| return EC_RES_ERROR; |
| |
| set_duty_cycle(fan, p_v1->percent); |
| |
| return EC_RES_SUCCESS; |
| } |
| DECLARE_HOST_COMMAND(EC_CMD_PWM_SET_FAN_DUTY, |
| hc_pwm_set_fan_duty, |
| EC_VER_MASK(0) | EC_VER_MASK(1)); |
| |
| static enum ec_status |
| hc_thermal_auto_fan_ctrl(struct host_cmd_handler_args *args) |
| { |
| int fan; |
| const struct ec_params_auto_fan_ctrl_v1 *p_v1 = args->params; |
| |
| if (args->version == 0) { |
| for (fan = 0; fan < fan_count; fan++) |
| set_thermal_control_enabled(fan, 1); |
| |
| return EC_RES_SUCCESS; |
| } |
| |
| fan = p_v1->fan_idx; |
| if (fan >= fan_count) |
| return EC_RES_ERROR; |
| |
| set_thermal_control_enabled(fan, 1); |
| |
| return EC_RES_SUCCESS; |
| } |
| DECLARE_HOST_COMMAND(EC_CMD_THERMAL_AUTO_FAN_CTRL, |
| hc_thermal_auto_fan_ctrl, |
| EC_VER_MASK(0)|EC_VER_MASK(1)); |
| |
| |
| /*****************************************************************************/ |
| /* Hooks */ |
| |
| /* We only have a limited number of memory-mapped slots to report fan speed to |
| * the AP. If we have more fans than that, some will be inaccessible. But |
| * if we're using that many fans, we probably have bigger problems. |
| */ |
| BUILD_ASSERT(CONFIG_FANS <= EC_FAN_SPEED_ENTRIES); |
| |
| #define PWMFAN_SYSJUMP_TAG 0x5046 /* "PF" */ |
| #define PWM_HOOK_VERSION 1 |
| /* Saved PWM state across sysjumps */ |
| struct pwm_fan_state { |
| /* TODO(crosbug.com/p/23530): Still treating all fans as one. */ |
| uint16_t rpm; |
| uint8_t flag; /* FAN_STATE_FLAG_* */ |
| }; |
| |
| /* For struct pwm_fan_state.flag */ |
| #define FAN_STATE_FLAG_ENABLED BIT(0) |
| #define FAN_STATE_FLAG_THERMAL BIT(1) |
| |
| static void pwm_fan_init(void) |
| { |
| const struct pwm_fan_state *prev; |
| struct pwm_fan_state state; |
| uint16_t *mapped; |
| int version, size; |
| int i; |
| int fan; |
| |
| if (fan_count == 0) |
| return; |
| |
| for (fan = 0; fan < fan_count; fan++) |
| fan_channel_setup(FAN_CH(fan), fans[fan].conf->flags); |
| |
| /* Restore previous state. */ |
| prev = (const struct pwm_fan_state *) |
| system_get_jump_tag(PWMFAN_SYSJUMP_TAG, &version, &size); |
| if (prev && version == PWM_HOOK_VERSION && size == sizeof(*prev)) { |
| memcpy(&state, prev, sizeof(state)); |
| } else { |
| memset(&state, 0, sizeof(state)); |
| } |
| |
| for (fan = 0; fan < fan_count; fan++) { |
| fan_set_enabled(FAN_CH(fan), |
| state.flag & FAN_STATE_FLAG_ENABLED); |
| fan_set_rpm_target(FAN_CH(fan), state.rpm); |
| set_thermal_control_enabled( |
| fan, state.flag & FAN_STATE_FLAG_THERMAL); |
| } |
| |
| /* Initialize memory-mapped data */ |
| mapped = (uint16_t *)host_get_memmap(EC_MEMMAP_FAN); |
| for (i = 0; i < EC_FAN_SPEED_ENTRIES; i++) |
| mapped[i] = EC_FAN_SPEED_NOT_PRESENT; |
| } |
| DECLARE_HOOK(HOOK_INIT, pwm_fan_init, HOOK_PRIO_DEFAULT); |
| |
| static void pwm_fan_second(void) |
| { |
| uint16_t *mapped = (uint16_t *)host_get_memmap(EC_MEMMAP_FAN); |
| uint16_t rpm; |
| int stalled = 0; |
| int fan; |
| |
| for (fan = 0; fan < fan_count; fan++) { |
| if (fan_is_stalled(FAN_CH(fan))) { |
| rpm = EC_FAN_SPEED_STALLED; |
| stalled = 1; |
| cprints(CC_PWM, "Fan %d stalled!", fan); |
| } else { |
| rpm = fan_get_rpm_actual(FAN_CH(fan)); |
| } |
| |
| mapped[fan] = rpm; |
| } |
| |
| /* |
| * Issue warning. As we have thermal shutdown |
| * protection, issuing warning here should be enough. |
| */ |
| if (stalled) |
| host_set_single_event(EC_HOST_EVENT_THERMAL); |
| } |
| DECLARE_HOOK(HOOK_SECOND, pwm_fan_second, HOOK_PRIO_DEFAULT); |
| |
| static void pwm_fan_preserve_state(void) |
| { |
| struct pwm_fan_state state = {0}; |
| int fan = 0; |
| |
| if (fan_count == 0) |
| return; |
| |
| /* TODO(crosbug.com/p/23530): Still treating all fans as one. */ |
| if (fan_get_enabled(FAN_CH(fan))) |
| state.flag |= FAN_STATE_FLAG_ENABLED; |
| if (is_thermal_control_enabled(fan)) |
| state.flag |= FAN_STATE_FLAG_THERMAL; |
| state.rpm = fan_get_rpm_target(FAN_CH(fan)); |
| |
| system_add_jump_tag(PWMFAN_SYSJUMP_TAG, PWM_HOOK_VERSION, |
| sizeof(state), &state); |
| } |
| DECLARE_HOOK(HOOK_SYSJUMP, pwm_fan_preserve_state, HOOK_PRIO_DEFAULT); |
| |
| static void pwm_fan_control(int enable) |
| { |
| int fan; |
| |
| /* TODO(crosbug.com/p/23530): Still treating all fans as one. */ |
| for (fan = 0; fan < fan_count; fan++) { |
| set_thermal_control_enabled(fan, enable); |
| fan_set_rpm_target(FAN_CH(fan), enable ? |
| fan_percent_to_rpm(FAN_CH(fan), CONFIG_FAN_INIT_SPEED) : |
| 0); |
| set_enabled(fan, enable); |
| } |
| } |
| |
| static void pwm_fan_stop(void) |
| { |
| /* |
| * There is no need to cool CPU in S3 or S5. We currently don't |
| * have fans for battery or charger chip. Battery systems will |
| * control charge current based on its own temperature readings. |
| * Thus, we do not need to keep fans running in S3 or S5. |
| * |
| * Even with a fan on charging system, it's questionable to run |
| * a fan in S3/S5. Under an extreme heat condition, spinning a |
| * fan would create more heat as it draws current from a |
| * battery and heat would come from ambient air instead of CPU. |
| * |
| * Thermal control may be already disabled if DPTF is used. |
| */ |
| pwm_fan_control(0); /* crosbug.com/p/8097 */ |
| } |
| DECLARE_HOOK(HOOK_CHIPSET_SUSPEND, pwm_fan_stop, HOOK_PRIO_DEFAULT); |
| DECLARE_HOOK(HOOK_CHIPSET_SHUTDOWN, pwm_fan_stop, HOOK_PRIO_DEFAULT); |
| |
| static void pwm_fan_start(void) |
| { |
| /* |
| * Even if the DPTF is enabled, enable thermal control here. |
| * Upon booting to S0, if needed AP will disable/throttle it using |
| * host commands. |
| */ |
| if (chipset_in_or_transitioning_to_state(CHIPSET_STATE_ON)) |
| pwm_fan_control(1); |
| } |
| /* On Fizz, CHIPSET_RESUME isn't triggered when AP warm resets. |
| * So we hook CHIPSET_RESET instead. |
| */ |
| DECLARE_HOOK(HOOK_CHIPSET_RESET, pwm_fan_start, HOOK_PRIO_FIRST); |
| DECLARE_HOOK(HOOK_CHIPSET_RESUME, pwm_fan_start, HOOK_PRIO_DEFAULT); |