PWM Regulated Fan Based on CPU Temperature for Raspberry Pi

12,009

43

31

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

Supplies:

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.

5 People Made This Project!

Recommendations

  • Colors of the Rainbow Contest

    Colors of the Rainbow Contest
  • Arduino Contest 2019

    Arduino Contest 2019
  • Woodworking Contest

    Woodworking Contest

31 Discussions

0
None
kdmag88

Question 2 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
tmntnpizza

4 weeks 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()

0
None
JimF143

3 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

4 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 3 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 3 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 3 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

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

3 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 12 months 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 4 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 12 months 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!

0
None
WalterC77mitchellw34

Answer 4 months ago

It wouldn't, as long as the PSU could provide enough current to the fan. 100mA is a relatively low current for a fan (old PS3 systems had a 2,650mA 12V fan for example) but if your PSU can power up a 200mA fan it surely can power up a 100mA one. You don't have to put a resistor in series with the fan, nonetheless the flyback diode is pretty much mandatory in designs like this (as specified in the instructable).

0
None
jhonfreddo

Question 8 months ago

Hi Aerandir14, this instructable is awesome! Do you have a GitHub page where you store the Python code posted above? (I'd like to fork it in order to adapt it to my needs). Thank you!

0
None
The Lost Puppy

11 months ago

Hello. I am curious as to what the diode is used for? I've seen several sets of instructions that don't use them.

1 reply
1
None
Aerandir14The Lost Puppy

Reply 11 months ago

Hi,

The diode is used to protect the transistor. When the PWM signal is low, the fan works as a generator due to it's inertia. This raises the voltage at the transistor's collector and can damage it. The diode allows the current to pass through it and go back to the positive terminal of the fan and pass through it again.