PWM Regulated Fan Based on CPU Temperature for Raspberry Pi


17,763

53

36

Student in engineering

Many cases for Raspberry Pi come with a little 5V fan in order to help cooling the CPU. However, these fans are usually pretty noisy and many people plug it on the 3V3 pin to reduce the noise. These fans are usually rated for 200mA which is pretty high for the 3V3 regulator on the RPi. This project will teach you how to regulate the fan speed based on CPU temperature. Unlike most of tutorials covering this subject, we won't only turn on or off the fan, but will control its speed like it's done on mainstream PC, using Python.

Teacher Notes

Teachers! Did you use this instructable in your classroom?
Add a Teacher Note to share how you incorporated it into your lesson.

Step 1: Parts Needed

For this project, we will use only a few components that are usually included in electronics kits for hobbyist that you can find on Amazon, like this one.

  • Raspberry Pi running Raspbian (but should work with other distribs).
  • 5V Fan (but a 12V fan could be used with an adapted transistor and a 12V power supply).
  • NPN transistor that supports at least 300mA, like a 2N2222A.
  • 1K resistor.
  • 1 diode.

Optional, to put the components inside the case (but not done yet):

  • A little piece of protoboard, to solder the components.
  • Large heat shrink, to protect the board.

Step 2: Electrical Connections

Resistor can be plug in either way, but be careful about transistor's and diode's direction. Diode's cathode must be connected to the +5V (red) wire, and anode must be connected to the GND (black) wire. Check your transistor doc for Emitter, Base and Collector pins. Fan's ground must be connected to the Collector, and Rpi's ground must be connected to Emitter.

In order to control the fan, we need to use a transistor that will be used inopen collector configuration. By doing this, we have a switch that will connect or disconnect the ground wire from the fan to the ground of the raspberry pi.

A NPN BJT transistor conducts depending on the current that flows in its gate. The current that will be allowed to flow from the collector (C) to the emitter (E) is:

Ic = B * Ib

Ic is the current that flows through the collector the emitter, Ib is the current that flows through the base to the emitter, and B (beta) is a value depending on each transistor. We approximate B = 100.

As our fan is rated as 200mA, we need at least 2mA through the base of the transistor. The tension between the base and the emitter (Vbe) is considered constant and Vbe = 0,7V. This means that when the GPIO is on, we have 3.3 - 0.7 = 2.6V at the resistor. To have 2mA through that resistor, we need a resistor of, maximum, 2.6 / 0.002 = 1300 ohm. We use a resistor of 1000 ohm to simplify and keep a margin of error. We will have 2.6mA through the GPIO pin which is totally safe.

As a fan is basically an electrical motor, it is an inductive charge. This means when the transistor stops conducting, the current in the fan will continue flowing as an inductive charge tries to keep the current constant. This would results in a high voltage on the ground pin of the fan and could damage the transistor. That's why we need a diode in parallel with the fan which will make the current flow constantly through the motor. This type of diode setup is called a Flywheel diode

Step 3: Program to Control the Fan Speed

To control fan speed, we use a software PWM signal from the RPi.GPIO library. A PWM Signal is well adapted to drive electric motors, as their reaction time is very high compared to the PWM frequency.

Use the calib_fan.py program to find the FAN_MIN value by running in the terminal:

python calib_fan.py

Check several values between 0 and 100% (should be around 20%) and see what is the minimum value for your fan to turn on.

You can change the correspondence between temperature and fan speed at the beginning of the code. There must be as many tempSteps as speedSteps values. This is the method that is generally used in PC motherboards, moving points on a Temp / Speed 2-axis graph.

Step 4: Run the Program at Startup

To run the program automatically at startup, I made a bash script where I put all the programs I want to launch, and then I launch this bash script at startup with rc.locale

  1. Create a directory /home/pi/Scripts/ and place the ctrl_fan.py file inside that directory.
  2. In the same directory, create a file named launcher.sh and copy the script bellow.
  3. Edit the /etc/rc.locale file and add a new line before the "exit 0": sudo sh '/home/pi/Scripts/launcher.sh'

launcher.sh script:

#!/bin/sh
#launcher.sh # navigate to home directory, then to this directory, then execute python script, then back home
locale
cd /
cd /home/pi/Scripts/
sudo python3 ./fan_ctrl.py &
cd /

If you want to use it with OSMC for example, you need to start it as a service with systemd.

  1. Download the fanctrl.service file
  2. Check the path to your python file,
  3. Place fanctrl.service in /lib/systemd/system
  4. Finally, enable the service with sudo systemctl enable fanctrl.service

This method is safer, as the program will be automatically restarted if killed by the user or the system.

Raspberry Pi Contest 2017

Participated in the
Raspberry Pi Contest 2017

First Time Author Contest 2018

Participated in the
First Time Author Contest 2018

7 People Made This Project!

Recommendations

  • Indoor Lighting Contest

    Indoor Lighting Contest
  • Make It Fly Challenge

    Make It Fly Challenge
  • Growing Beyond Earth Maker Contest

    Growing Beyond Earth Maker Contest

36 Discussions

0
None
direbearform

5 days ago

For those running this setup on Raspberry Pi 4, please note that the GPIO PIN specified in the code should be 21 (not the default 24 came with the provided source code) - if you connect transistor's base to the very end pin near Eithernet adapter on the outside row like the picture shown in this post. See https://www.raspberrypi.org/documentation/usage/gpio/ for the PIN number allocations for RPI 4.

0
None
tmntnpizza

3 months ago

I have modified your original script to better suite my intentions. I am sharing it, but if you don't like something I have done or preferred me not to share it feel free to delete and/or contact me. Great instructable! This prints out current speed(%) and cpu temp.

#!/usr/bin/python
# -*- coding: utf-8 -*-
import RPi.GPIO as GPIO
import time
import sys
import os
import signal
# Configuration
FAN_PIN = 14 # BCM pin used to drive transistor's base
WAIT_TIME = 5 # [s] Time to wait between each refresh
FAN_LSPD = 20 # [%] Fan minimum speed.
PWM_FREQ = 25 # [Hz] Change this value if fan has strange behavior
# Setup GPIO pin
GPIO.setmode(GPIO.BCM)
GPIO.setup(FAN_PIN, GPIO.OUT, initial=GPIO.LOW)
fan=GPIO.PWM(FAN_PIN,PWM_FREQ)
fan.start(0);
def getCPUtemperature():
res = os.popen('/opt/vc/bin/vcgencmd measure_temp').readline()
temp =(res.replace("temp=","").replace("'C\n",""))
print("Temp is {0}°C".format(temp)) #Uncomment here for testing
return temp
def getTEMP():
CPU_temp = float(getCPUtemperature())
return()
i = 0
hyst = 1 # Fan speed will change only of the difference of temperature is higher than hysteresis
tempSteps = [30, 35, 40, 45, 50, 60, 65, 70] # [°C] # Configurable temperature and fan speed steps
speedSteps = [0, 20, 40, 60, 80, 100, 100, 100] # [%]
cpuTempOld=0
fanSpeedOld=0
# We must set a speed value for each temperature step
if(len(speedSteps) != len(tempSteps)):
print("Numbers of temp steps and speed steps are different")
exit(0)
try:
while 1:
getTEMP()
time.sleep(5) # Read the temperature every 5 secs
cpuTempFile=open("/sys/class/thermal/thermal_zone0/temp","r")
cpuTemp=float(cpuTempFile.read())/1000
cpuTempFile.close() # Read CPU temperature
if(abs(cpuTemp-cpuTempOld > hyst)): # Calculate desired fan speed
if(cpuTemp < tempSteps[0]): # Below first value, fan will run at min speed.
fanSpeed = speedSteps[0]
elif(cpuTemp >= tempSteps[len(tempSteps)-1]): # Above last value, fan will run at max speed
fanSpeed = speedSteps[len(tempSteps)-1]
else: # If temperature is between 2 steps, fan speed is calculated by linear interpolation
for i in range(0,len(tempSteps)-1):
if((cpuTemp >= tempSteps[i]) and (cpuTemp < tempSteps[i+1])):
fanSpeed = round((speedSteps[i+1]-speedSteps[i])\
/(tempSteps[i+1]-tempSteps[i])\
*(cpuTemp-tempSteps[i])\
+speedSteps[i],1)
if((fanSpeed != fanSpeedOld) ):
if((fanSpeed != fanSpeedOld)\
and ((fanSpeed >= FAN_LSPD) or (fanSpeed == 0))):
fan.ChangeDutyCycle(fanSpeed)
fanSpeedOld = fanSpeed
print("Speed is {0}%".format(fanSpeed))
time.sleep(WAIT_TIME) # Wait until next refresh
except(KeyboardInterrupt): # If a keyboard interrupt occurs (ctrl + c), the GPIO is set to 0 and the program exits.
print("Cancelled... Fan OFF")
GPIO.cleanup()
sys.exit()

1 reply
0
None
highroller_ch

7 weeks ago

Hi, thank you for the nice tutorial.
I have a fan which is rated at 0.07A, 0,35W. I am not able to control the fan with your setup. Do I need to change the resistor? what am I doing wrong?

Thanks and best regards
Chris

0
None
Tcheetoz

Question 7 weeks ago

Great work!
Do I absolutely need the diode? If not, what shall I do to adapt the circuit properly?

Thanks,
K.

0
None
StevenB308

2 months ago

I made this! It worked fine for about a year, but now it fails to start fanctrl.service on a Raspberry Pi 3. The log shows a segmentation fault:
Main process exited, code=killed, status=11/SEGV
Running the command-line:
:~ $ sudo python /home/pi/Scripts/fan_ctrl.py
Segmentation fault
I've tried getting latest updates.
I'm at a loss as to how to proceed.

0
None
kdmag88

Question 5 months ago

Thank you! I had problems with a lot of other scripts I found. This one seems to work.

My question is, my fan doesn't seem to activate until about 60*, it does seem to regulate speed after it has started, and with the pi at idle the fan slows and stops at 50*. Is this normal? I have a slightly bigger fan(.25W, .05A), is it just that the 50-60 temps are enough power to keep it going but not enough to start it? fan min didn't seem to effect start temp.

thanks
Keith

1 answer
0
None
JimF143

6 months ago

Hi you mention we need at least 2mA through the base of the transistor. Why 2mA? Please and thanks for your help. Just trying to figure out the math

0
None
Jens-R

7 months ago

Thanks for the article, I used your design with a cheap metal rPi case with a built-in fan, and it worked like a charm. I used a 470k resistor instead of 1k, but otherwise everything is the same. However, my fan makes a clicking noise on any PWM duty cycle setting lower than ~85%, so in my case I had to configure it to run 0-or-100 instead of variable speed. From what I could find online it's down to some fan motors just not playing well with PWM control.

4 replies
0
None
JimF143Jens-R

Reply 6 months ago

Hi, looks great! Question; did you shrink the components and wires too? Of course in a way they aren’t touching.. I was thinking of doing this also but wasn’t sure if shrink over components is ok?

0
None
Jens-RJimF143

Reply 6 months ago

I did, it shouldn't get too hot - and it's right in the path of the airflow from the fan anyway

0
None
Aerandir14Jens-R

Reply 6 months ago

Hi! Thanks for your comment.
With a 470k resistor, you probably don't get enough current on the transistor's base to drive the fan current correctly.
About your clicking noise, try to ajust the PWM frequency. Try to lower it and see if it gets better :)

0
None
Aerandir14

6 months ago

Hi,
It should work well if the transistor allow it. What transistor will you be using ?
You should also lower a little bit the resistor value in order to get 300 mA through the transistor, but it also depends on the B factor of your transistor.

1 reply
0
None
JimF143Aerandir14

Reply 6 months ago

Hi I was planning to use the 2N2222A (800MA 40V) , that work? As far as the resistor I will go buy whatever I need, what do you suggest? Lastly the diode I have a 1N4001

0
None
Manitas10000

6 months ago

Thanks for the instructions

Had to change the code because the pin on my rapsberry pi that goes on the (BCM) pin shown in your pictures matches pin 21 and not 24 as in the code. I read on the reference that you can map pins using BOARD mapping, would that be an improvement to avoid people getting wrong pins like I did?

Also, made a systemd service for the whole think, something like this:

[Unit]
Description=Fan Speed Controller
After=multi-user.target


[Service]
Restart=always
RestartSec=15
Type=simple
ExecStart=/usr/local/sbin/fan_ctrl.py
User=pi

[Install]
WantedBy=multi-user.target

0
None
MattP186

Question 1 year ago

Can i ask why you didnt sudo python /home/pi/Scripts/fan_ctrl.py & in the rc.local file?

2 answers
0
None
WalterC77MattP186

Answer 7 months ago

I think it's cleaner and neater how the OP did it. This allows you to add a single hash character to the command line in /etc/rc.local instead of adding multiple ones to disable the command. It's a time saver and gives a tidier system. You can also edit the single config file without tampering with other settings, and enables non-root users to modify it as well.

On the downside, being GNU/Linux an operating system with a f**kton of config files scattered everywhere, this adds yet another one of them, so you could easily lose track of it if you accidentally move it elsewhere. Moreover, you get the same problems with the non-root user access, because this configuration could break the principle of minimal privilege if mishandled. By incorporating the code in /etc/rc.local this issue is avoided.

0
None
Aerandir14MattP186

Answer 1 year ago

You're right, this should work, but I prefer to edit the bash script to modify or add other programs that need to be launched at startup, than the rc.local file. But your method should work fine!