From c76218b2e6142b29225deb5818dcfb81dc9c687d Mon Sep 17 00:00:00 2001 From: Tobias Daeullary Date: Tue, 25 Apr 2023 03:35:59 +0100 Subject: [PATCH 1/4] Add initial PWM --- fancontrol.py | 58 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/fancontrol.py b/fancontrol.py index 92f0b00..1bf248c 100755 --- a/fancontrol.py +++ b/fancontrol.py @@ -1,14 +1,19 @@ #!/usr/bin/env python3 import time +import logging -from gpiozero import OutputDevice +from gpiozero import PWMOutputDevice -ON_THRESHOLD = 65 # (degrees Celsius) Fan kicks on at this temperature. -OFF_THRESHOLD = 55 # (degress Celsius) Fan shuts off at this temperature. -SLEEP_INTERVAL = 5 # (seconds) How often we check the core temperature. -GPIO_PIN = 17 # Which GPIO pin you're using to control the fan. +FULL_ON_TEMP = 65. # [degC] temp for max PWM +OFF_TEMP = 55. # [degC] temp for min PWM +INTERVAL = 5. # [s] refresh interval +GPIO_PIN = 17 # Pin to connect transistor base +MAX_PWM = 1. # PWM at max on temp +MIN_PWM = .7 # PWM at lowest on temp +MIN_STARTUP_PWM = .9 # PWM to use on startup +PWM_FREQUENCY = 500 # [Hz] - with 100uF, and .7 min PWM def get_temp(): @@ -27,25 +32,34 @@ def get_temp(): except (IndexError, ValueError,) as e: raise RuntimeError('Could not parse temperature output.') from e -if __name__ == '__main__': - # Validate the on and off thresholds - if OFF_THRESHOLD >= ON_THRESHOLD: - raise RuntimeError('OFF_THRESHOLD must be less than ON_THRESHOLD') - fan = OutputDevice(GPIO_PIN) +def turn_on_or_off(pwm_device: PWMOutputDevice, temp: int): + if fan.is_active and temp < OFF_TEMP: + fan.off() + print("Turned off") + elif not fan.is_active and temp >= OFF_TEMP: + fan.on() + fan.value = MIN_STARTUP_PWM + print("Turned on") - while True: - temp = get_temp() - # Start the fan if the temperature has reached the limit and the fan - # isn't already running. - # NOTE: `fan.value` returns 1 for "on" and 0 for "off" - if temp > ON_THRESHOLD and not fan.value: - fan.on() +if __name__ == "__main__": + assert OFF_TEMP < FULL_ON_TEMP, "OFF_TEMP must be lower than FULL_ON_TEMP" + assert MIN_STARTUP_PWM >= MIN_PWM, "MIN_STARTUP_PWM must be at least MIN_PWM" + assert MAX_PWM > MIN_PWM, "MAX_PWM must be higher than MIN_PWM" + + fan = PWMOutputDevice(GPIO_PIN, frequency=PWM_FREQUENCY) + turn_on_or_off(fan, get_temp()) - # Stop the fan if the fan is running and the temperature has dropped - # to 10 degrees below the limit. - elif fan.value and temp < OFF_THRESHOLD: - fan.off() + pwm_range = MAX_PWM - MIN_PWM + temp_range = FULL_ON_TEMP - OFF_TEMP + + while True: + temp = get_temp() + pwm = pwm_range * (temp - OFF_TEMP) / temp_range + MIN_PWM + if fan.is_active: + fan.value = max(min(pwm, MAX_PWM), MIN_PWM) + turn_on_or_off(fan, temp) - time.sleep(SLEEP_INTERVAL) + print(f"Current temp|PWM: {temp:.2f}|{fan.value:.2f}") + time.sleep(INTERVAL) From faeb7908b87bd7c0662e2224048160837d988bed Mon Sep 17 00:00:00 2001 From: Tobias Daeullary Date: Sat, 13 May 2023 02:49:52 +0100 Subject: [PATCH 2/4] Add exponential fan curve, add ON_TEMP for better behaviour --- fancontrol.py | 56 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/fancontrol.py b/fancontrol.py index 1bf248c..027bfdf 100755 --- a/fancontrol.py +++ b/fancontrol.py @@ -5,15 +5,26 @@ from gpiozero import PWMOutputDevice - -FULL_ON_TEMP = 65. # [degC] temp for max PWM -OFF_TEMP = 55. # [degC] temp for min PWM +""" +ON_TEMP should be higher than the Raspi idle temperature (usually +between 50-70 degC, depending on the base load), otherwise the fan will +continuously switch ON and OFF. +OFF_TEMP should be reachable with the minimum fan speed in the idle state, +otherwise the fan will be continuously ON after triggered once (usually +around 50 degC). +""" + +FULL_ON_TEMP = 80. # [degC] temp for max PWM +ON_TEMP = 68. # [degC] temp to switch on +OFF_TEMP = 50. # [degC] temp for min PWM INTERVAL = 5. # [s] refresh interval GPIO_PIN = 17 # Pin to connect transistor base MAX_PWM = 1. # PWM at max on temp -MIN_PWM = .7 # PWM at lowest on temp +MIN_PWM = .64 # PWM at lowest on temp MIN_STARTUP_PWM = .9 # PWM to use on startup PWM_FREQUENCY = 500 # [Hz] - with 100uF, and .7 min PWM +FAN_MODES = ["linear", "exponential"] +FAN_MODE = "exponential" def get_temp(): @@ -32,34 +43,43 @@ def get_temp(): except (IndexError, ValueError,) as e: raise RuntimeError('Could not parse temperature output.') from e - -def turn_on_or_off(pwm_device: PWMOutputDevice, temp: int): - if fan.is_active and temp < OFF_TEMP: - fan.off() +def turn_on_or_off(pwm_device: PWMOutputDevice, temp: int) -> None: + if pwm_device.is_active and temp < OFF_TEMP: + pwm_device.off() print("Turned off") - elif not fan.is_active and temp >= OFF_TEMP: - fan.on() - fan.value = MIN_STARTUP_PWM + elif not pwm_device.is_active and temp >= ON_TEMP: + pwm_device.on() + pwm_device.value = MIN_STARTUP_PWM print("Turned on") +def calc_pwm(pwm_range: float, temp_range: float, temp: float) -> float: + if FAN_MODE == "linear": + return pwm_range * (temp - OFF_TEMP) / temp_range + MIN_PWM + elif FAN_MODE == "exponential": + return (1.001 ** (3.7 * (temp - OFF_TEMP) / temp_range) ** 5 - 1) * pwm_range + MIN_PWM -if __name__ == "__main__": - assert OFF_TEMP < FULL_ON_TEMP, "OFF_TEMP must be lower than FULL_ON_TEMP" - assert MIN_STARTUP_PWM >= MIN_PWM, "MIN_STARTUP_PWM must be at least MIN_PWM" - assert MAX_PWM > MIN_PWM, "MAX_PWM must be higher than MIN_PWM" - +def fancontrol_loop(): fan = PWMOutputDevice(GPIO_PIN, frequency=PWM_FREQUENCY) - turn_on_or_off(fan, get_temp()) pwm_range = MAX_PWM - MIN_PWM temp_range = FULL_ON_TEMP - OFF_TEMP while True: temp = get_temp() - pwm = pwm_range * (temp - OFF_TEMP) / temp_range + MIN_PWM if fan.is_active: + pwm = calc_pwm(pwm_range, temp_range, temp) fan.value = max(min(pwm, MAX_PWM), MIN_PWM) turn_on_or_off(fan, temp) print(f"Current temp|PWM: {temp:.2f}|{fan.value:.2f}") time.sleep(INTERVAL) + + +if __name__ == "__main__": + assert ON_TEMP > OFF_TEMP, "ON_TEMP must be higher than OFF_TEMP" + assert ON_TEMP < FULL_ON_TEMP, "ON_TEMP must be lower than FULL_ON_TEMP" + assert MIN_STARTUP_PWM >= MIN_PWM, "MIN_STARTUP_PWM must be at least MIN_PWM" + assert MAX_PWM > MIN_PWM, "MAX_PWM must be higher than MIN_PWM" + assert FAN_MODE in FAN_MODES, f"FAN_MODE must be one of {str(FAN_MODES)}" + + fancontrol_loop() From 4f4c0823cb52fcaa1e6c4276488e5e6d83bedf32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20D=C3=A4ullary?= Date: Sat, 13 May 2023 12:18:26 +1000 Subject: [PATCH 3/4] Update README.md: Add guide to select capacitance and MIN_PWM --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index e2c6a6e..111504a 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,24 @@ a certain threshold. To use this code, you'll have to install a fan. The full instructions can be found on our guide: [Control Your Raspberry Pi Fan (and Temperature) with Python](https://howchoo.com/g/ote2mjkzzta/control-raspberry-pi-fan-temperature-python). + +In addition to the guide above, in order to use soft PWM, you will need to add a capacitor to avoid audible noise induced by the fan at PWM frequency. +Values for the capacitor are highly dependent on PWM frequency; the higher the frequency, the less capacitance is needed. + +In addition, the values for minimum PWM for the fan will be influenced by PWM frequency as well. + +The default configuration uses a 100µF electrolytic capacitor between the two fan leads, and a 500Hz PWM frequency. + +In order to test out the minimum speed of a different fan manually, you can use the module interactively: + +```sh +$ python +``` +```python +from fancontrol import * + +fan = PWMOutputDevice(GPIO_PIN, frequency=PWM_FREQUENCY) +``` +Now `fan.value` can be set to values between `0.0` and `1.0` in order to find the minimum value that still allows the fan to spin reliably. + +Start with a high fan speed (such as `0.9`), and slowly decrease the speed until the fan stops. This value (in addition to a security margin) will be the `MIN_PWM` value for your specific PWM frequency, capacitance and fan type. From e6bb0156ff0b1945abb48c6aae640a6f9dfeed99 Mon Sep 17 00:00:00 2001 From: Tobias Daeullary Date: Sat, 14 Jun 2025 16:08:42 +0100 Subject: [PATCH 4/4] Update for more silent operation --- fancontrol.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fancontrol.py b/fancontrol.py index 027bfdf..3820139 100755 --- a/fancontrol.py +++ b/fancontrol.py @@ -14,9 +14,9 @@ around 50 degC). """ -FULL_ON_TEMP = 80. # [degC] temp for max PWM -ON_TEMP = 68. # [degC] temp to switch on -OFF_TEMP = 50. # [degC] temp for min PWM +FULL_ON_TEMP = 85. # [degC] temp for max PWM +ON_TEMP = 75. # [degC] temp to switch on +OFF_TEMP = 55. # [degC] temp for min PWM INTERVAL = 5. # [s] refresh interval GPIO_PIN = 17 # Pin to connect transistor base MAX_PWM = 1. # PWM at max on temp