Intro

Most of the stuff I do on computer don't consume much resource and the CPU rarely reaches high temperatures. Nevertheless, most firmware don't allow fan adjustments and have a minimum, always-on state even when the CPU is below 30°C. Since I like my computer/laptop to be silent in this case, I tend to open the computer, reroute the ground pin of the fan to the outside and add a switch, allowing me to turn on the fan whenever I like. However, this can be dangerous since the temperature can raise without me noticing. I would also need to make sure the fan is on if I leave the computer unsupervised for a longer time.

Thus, I decided to test addressing this issue in a different way by introducing a micro-controller, along with a relay inside the case to programatically control the fan state. In this case I used a RP2040-zero and a 5V relay (although it is controlled by the 3.3 V which is the RP2040 logic voltage) but other controllers or relays depending on fan voltage can be selected.

Mechanism

This will detect the PWM set by the firmware on one of the controller pins (here pin number 5) which is reflective of the current temperature and the amount of cooling the computer is trying to achieve. START_DUTY_CYCLE and STOP_DUTY_CYCLE then allow to control when to turn the fan on or off.

Circuit for RP2040 fan control

Software setup

In my case, I needed to download Pico SDK and import its cmake init file when running cmake from my project. The C code that handles the fan state is pwm_logger.c:

#include <stdio.h>

#include "pico/stdlib.h"
#include "hardware/pwm.h"
#include "hardware/clocks.h"

const uint OUTPUT_PIN = 2;
const uint MEASURE_PIN = 5;
const float START_DUTY_CYCLE = 0.70;
const float STOP_DUTY_CYCLE = 0.50;
bool is_on = false;

bool change_fan_state(bool state) {
    printf("fan state: %d\n", state);
    gpio_put(OUTPUT_PIN, state);
    return state;
}

float measure_duty_cycle(uint gpio) {
    // Only the PWM B pins can be used as inputs.
    assert(pwm_gpio_to_channel(gpio) == PWM_CHAN_B);
    uint slice_num = pwm_gpio_to_slice_num(gpio);

    // Count once for every 100 cycles the PWM B input is high
    pwm_config cfg = pwm_get_default_config();
    pwm_config_set_clkdiv_mode(&cfg, PWM_DIV_B_HIGH);
    pwm_config_set_clkdiv(&cfg, 100);
    pwm_init(slice_num, &cfg, false);
    gpio_set_function(gpio, GPIO_FUNC_PWM);

    pwm_set_enabled(slice_num, true);
    sleep_ms(10);
    pwm_set_enabled(slice_num, false);
    float counting_rate = clock_get_hz(clk_sys) / 100;
    float max_possible_count = counting_rate * 0.01;
    return pwm_get_counter(slice_num) / max_possible_count;
}

int main() {
    stdio_init_all();
    const uint count_top = 1000;
    pwm_config cfg = pwm_get_default_config();
    pwm_config_set_wrap(&cfg, count_top);
    sleep_ms(5000);

    gpio_init(OUTPUT_PIN);
    gpio_set_dir(OUTPUT_PIN, GPIO_OUT);

    is_on = change_fan_state(true);
    sleep_ms(20000);
    is_on = change_fan_state(false);

    while (true) {
        float measured_duty_cycle = measure_duty_cycle(MEASURE_PIN);
        printf("measured duty cycle = %.1f%%, fan state: %d\n", measured_duty_cycle * 100.f, is_on);
        if (is_on && measured_duty_cycle < STOP_DUTY_CYCLE) {
            is_on = change_fan_state(false);
        }
        if (!is_on && measured_duty_cycle > START_DUTY_CYCLE)
            is_on = change_fan_state(true);
        }
        sleep_ms(2000);
    }
}

You see that I wait 20 seconds in the beginning to turn the fan off. In my case, this was necessary for the mainboard internal tests once the computer turns on to avoid detecting the fan is off and throwing an error, prolonging the boot sequence unnecessarily.

We also need cmake file CMakeLists.txt:

cmake_minimum_required(VERSION 3.13)

set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
if (NOT PICO_SDK_PATH)
    message(FATAL_ERROR "PICO_SDK_PATH not set – export it first.")
endif()

include(${PICO_SDK_PATH}/pico_sdk_init.cmake)

project(pwm_reducer_zero C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

pico_sdk_init()

add_executable(pwm_logger_zero
    pwm_logger.c
)

target_link_libraries(pwm_logger_zero
    pico_stdlib
    hardware_pwm
    hardware_irq
)

pico_enable_stdio_usb(pwm_logger_zero 1)
pico_enable_stdio_uart(pwm_logger_zero 0)

pico_add_extra_outputs(pwm_logger_zero)
mkdir build && cd build
export PICO_SDK_PATH=/usr/src/pico-sdk cmake ..
make

# restart the RP2040 in boot mode by holding boot button, push reset button then let go of boot button
cp pwm_logger_zero.uf2 <mouted_rp2040_location>

Once the programming is done, the controller can be unplugged from the USB and tucked somewhere safe around the fan. That's it. This made my computer very silent most of the time and also cool when needed.