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