Build fan controller for Orange PI PC

In this example we are going build fan controller for Orange PI PC.

By: Björn Eiríksson and Hörður Kristjánsson.

Disclaimer:
We do not take any responsibility for possible errors in the guide or errors that you might do wiring it up. Incorrect wiring can result in damaged sensor or damaged Raspberry PI.

The project:
This started out as project where me and a guy name Hörður Kristjánson (he is a great developer) were building Owncloud servers from cheap Orange PI PC machines. This guide will not handle installing or setting up the Owncloud, but more of dealing with heat problems that we encountered.

On the first machine we set up then machine seemed to be running Owncloud stable at 49 – 50 C on the CPU, but on 2nd day we noticed it going to 75 C and everything went very slow due to CPU core shut downs. Reason for the spike is known, generation of Previews on images was turned on. Even if turning that feature off which we did then there will always come peek times where the CPU needs to be able to take load. This guide also applies for other Orange PI projects, basically if you are doing something that needs CPU load then the CPU on Orange PI will heat up really fast. Of course we already had heat sinks on the CPU and memory chips so it was clear that fans would be needed to ensure stability and performance.

Fans are loud!
Loud fans was not exactly something that we were prepared to swallow as those servers are being built as home servers where they need to be quiet. So after some thinking and discussion then this plan was made:

1. GPIO controlled fan. (We will be taking 3 GPIO pins for this though a slightly more complex circuiut can do 3 speeds with just 2 GPIO pins)
2. C++ daemon would be written that reads the CPU temp and turns on the fan, Slow at 60 C, faster at 65 C and full speed at 70 C.
3. Cut hole in the top of the boxes for the fan, and 3D print fan brackets that will also store the fan controller circuit. (We do not have the same Orange PI PC cases so we will try to make our fan bracket generic so it can work on both cases)

So in short if the plan works then the Fan is supposed to be silent 99% of the time.


The circuit:
This is the circuit we came up with:

FanControllerCircuitScheme


Selecting the components:
When looking for fans that are 5V and suitable for the task then it soon was clear that fan would take from about 145 mA to 200 mA.

In the end we took 5V fan, that takes 200 mA and is 5 x 5 cm.

We thought we would be using resistors for R4 and R5 with higher values so were concerned about the wattage on them but the fan does not run at lower voltage than 3,6 V at all. So doing the math on R4 and R5:

R4 and R5 values were selected based on what gave good speeds on the fan in actual tests, 8.2 Ohm drops the voltage on the fan to about 3,6 V (measured with multimeter) Then to verify the wattage for them:

Original fan voltage: 5V

Original fan current: 0.2A

V=IR (Ohm’s Law), so 5 = 0.2R   =>   R = 5 / 0.2   =   25

Fan resistance: 25 Ω

Target voltage: 3.6V

V=IR (Ohm’s Law), so 3.6 = 25I   =>   I = 3.6 / 25   =  0.144

Target current: 0.144A

Voltage drop across resistor: 5V – 3.6V  =  1.4V

V=IR (Ohm’s Law), so 1.4 = 0.144R    =>    R = 1.4 / 0.144    =  9.722

Target resistance:  9.722 Ω

Power dissipated by resistor:  1.4V * 0.144A  =  0.202W

The actual power dispatched is a little less than shown in the math above as going through the transistor does not deliver 5V, So in the end 1/4 W resistors were enough.

The hunt for transistor that would be able to take this amount of current and possible spikes started and we in the end decided on BC-337-25 (note the 25 variation) which is NPN transistor and has current rating of 0.8 A in the collector.

For the current then we need to apply the following formula: I(B) = I(C) / β.  (The β will usually be marked as hfc in the transistors specs) You google the spec for your transistor and find the Beta Value, it will have some range depending on current, it will be marked as On characteristics in the spec. Mine has the β spec of 160 to 400 at 100 mA. Since we are using 200 mA then we will choose β of 250.
Which means I(B) will be: 0,2 / 250 = 0,0008 A (This is how little we will be taking from the GPIO pin)

Now that we have the voltage and current then R1, R2 and R3 = 2,6 / 0,0008 A = 3250 Ω  (2,6 since  base of the transistor wants 0.7 V)

Manual tweaking of the speed on the fan controller ended us then with 3,6kΩ on R1 and R3 and 4,7 kΩ on R2.


After tweaking and testing it on the breadboard for a while then it was time to put the circuit on final board:

Measured final values were:

Measured voltage on the fan
Pins active Voltage
No pin 0 V
1 3,64 V
2 3,79 V
1 and 3 3,89 V
1 and 2 4,34 V
1,2 and 3 4,56 V

The daemon will then be using those pin combinations to control the speed of the fan.

Fan controller soldered
Fan controller soldered

We squeezed it a bit n the board to not let it take a lot of space once it would be put in the Orange PI PC machine.

(See further down in the article how we printed a case for the controller and the fan to mount on top of the Orange PI PC)

Pins on the soldered board above are:

V: Pin 1
U: Pin 2
T:  Pin 3
Z: 5V
1: GND (bottom right)
5: Fan output GND
6: Fan output VCC  (top right)


 

<< Putting it all together:

Making the daemon:

In order to control the fan automatically based on cpu temperature we had to make it run in the background. This meant creating a daemon, something that neither of us had any experience doing.

We ended up writing a pretty basic c++ daemon that performs the following tasks:

  • Monitors cpu temperature on a set interval
  • Depending on the current cpu temperature the fan speed is set
  • When the cpu temperature drops below the lowest temperature level, the fan keeps spinning for a set amount of time to keep cooling the cpu. This is done to prevent the fan from stopping for a short while and the spin right back up again.

Another feature we wanted to add was to make it configurable. To this end we added some arguments so that you can configure the daemon settings on startup. The available parameters are:

  • Setting the temperature levels. You can override the 5 temperature levels individually.
  • Set the cpu temperature check interval. This controls how aggresively the daemon checks the cpu temperature.
  • Set the fan shutdown time. This controls for how long the daemon waits until the fan is completely shut down after the temperature drops below the lowest temperature threshold.
  • Set a flag con indicate wether this is an upstart daemon that forks or not. We’re not sure if forking is needed  but the tutorials used that so we decided not to change it up too much.

The final code for the daemon can be seen below:

// A simple daemon that monitors CPU temperature and controls a 5v fan
// Based on code from http://shahmirj.com/blog/beginners-guide-to-creating-a-daemon-in-linux
 
#include &lt;sys/types.h&gt;
#include &lt;sys/stat.h&gt;
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
using namespace std;
using namespace chrono;
 
#define DAEMON_NAME "pi-fan-controller"
 
// Fan state
#define FAN_STOPPED 0
#define FAN_STOPPING 1 
#define FAN_SPEED_1 2
#define FAN_SPEED_2 3
#define FAN_SPEED_3 4
#define FAN_SPEED_4 5
#define FAN_SPEED_5 6
 
char* message;
 
// GPIO pins to use
// Current implementation uses BCM numbers
int _pin1 = 2;
int _pin2 = 3;
int _pin3 = 4;
 
// Temperature thresholds
int _temp1 = 55;
int _temp2 = 60;
int _temp3 = 65;
int _temp4 = 70;
int _temp5 = 75;
 
// How often (in seconds) to check for cpu temp
int _checkInterval = 5;
 
int _logLevel = LOG_NOTICE;
 
int _stopWaitTime = 120;
steady_clock::time_point _stopWaitTimeStart;
 
int _currentState = FAN_STOPPED;
 
// Indicates wether this program should only stop the fans. This needs to be set with the -stopfan flag
// at startup which will stop the fan based on the given GPIO pin numbers
bool _stopFanOnly = false;
 
// Initializes the daemon settings based on the startup parameters given
void init(int argc, char *argv[])
{
    for(int i = 1; i &lt; argc; i++) { if(i + 1 != argc) { // GPIO pins if(std::string(argv[i]) == "-p1") { _pin1 = atoi(argv[i + 1]); } else if (std::string(argv[i]) == "-p2") { _pin2 = atoi(argv[i + 1]); } else if (std::string(argv[i]) == "-p3") { _pin3 = atoi(argv[i +1]); } // Interval else if (std::string(argv[i]) == "-i") { _checkInterval = atoi(argv[i + 1]); } // Temperature thresholds else if (std::string(argv[i]) == "-t1") { _temp1 = atoi(argv[i + 1]); } else if (std::string(argv[i]) == "-t2") { _temp2 = atoi(argv[i + 1]); } else if (std::string(argv[i]) == "-t3") { _temp3 = atoi(argv[i + 1]); } else if (std::string(argv[i]) == "-t4") { _temp4 = atoi(argv[i + 1]); } else if (std::string(argv[i]) == "-t5") { _temp5 = atoi(argv[i + 1]); } else if (std::string(argv[i]) == "-st") { _stopWaitTime = atoi(argv[i + 1]); } else if (std::string(argv[i]) == "-stopfan") { _stopFanOnly = true; } else if (std::string(argv[i]) == "-l") { int logLevel = atoi(argv[i + 1]); if(logLevel &gt;= 0 &amp;&amp; logLevel &lt;= 7)
                {
                    _logLevel = logLevel;                
                }
                else
                {
                    _logLevel = LOG_NOTICE;
                }
            }
        }
    }    
}
 
// Toggles pin state based on the given state
void togglePinState(int state)
{
    switch(state)
    {
        case FAN_STOPPED :
            syslog (LOG_INFO, ("Stoppin fan..."));
            digitalWrite(_pin1, LOW);
            digitalWrite(_pin2, LOW);
            digitalWrite(_pin3, LOW);        
            break;
 
        case FAN_SPEED_1 :
            syslog (LOG_INFO, ("Setting fan to speed 1..."));
            digitalWrite(_pin1, HIGH);
            digitalWrite(_pin2, LOW);
            digitalWrite(_pin3, LOW);
            break;
 
        case FAN_SPEED_2 :
            syslog (LOG_INFO, ("Setting fan to speed 2..."));
            digitalWrite(_pin1, LOW);
            digitalWrite(_pin2, HIGH);
            digitalWrite(_pin3, LOW);
            break;
 
        case FAN_SPEED_3 :
            syslog (LOG_INFO, ("Setting fan to speed 3..."));
            digitalWrite(_pin1, HIGH);
            digitalWrite(_pin2, LOW);
            digitalWrite(_pin3, HIGH);
            break;
 
        case FAN_SPEED_4 :
            syslog (LOG_INFO, ("Setting fan to speed 4..."));
            digitalWrite(_pin1, HIGH);
            digitalWrite(_pin2, HIGH);
            digitalWrite(_pin3, LOW);
            break;
 
        case FAN_SPEED_5 :
            syslog (LOG_INFO, ("Setting fan to speed 5..."));
            digitalWrite(_pin1, HIGH);
            digitalWrite(_pin2, HIGH);
            digitalWrite(_pin3, HIGH);
            break;
    }
}
 
// Executes a console command and returns the value
std::string exec(const char* cmd)
{
    std::shared_ptr pipe(popen(cmd, "r"), pclose);
    if (!pipe)
    {
        return "ERROR";
    }
 
    char buffer[128];
    std::string result = "";
 
    while (!feof(pipe.get()))
    {
        if (fgets(buffer, 128, pipe.get()) != NULL)
        {
            result += buffer;
        }
    }
    return result;
}
 
//
void process()
{
 
    syslog (LOG_DEBUG, ("CPU temp: " + exec("cat /sys/class/thermal/thermal_zone0/temp")).c_str());
 
    const char* tempString = exec("cat /sys/class/thermal/thermal_zone0/temp").c_str();
    int curTemp = atoi(tempString);
 
    int nextState = _currentState;
 
    // Turn off the fan
    if(curTemp &lt; _temp1) { // Check if we are about to enter the waiting period or if we can simply turn off the fan if(_currentState &gt; FAN_STOPPING)
        {
            nextState = FAN_STOPPING;
            _stopWaitTimeStart = steady_clock::now();
 
            // Set the speed to the lowest setting while winding down
            togglePinState(FAN_SPEED_1);
        }
        else if(_currentState == FAN_STOPPING)
        {
            // Check if the wait period has expired            
            duration time_span = duration_cast&lt;duration&gt;(steady_clock::now() - _stopWaitTimeStart);
 
            if(time_span.count() &gt;= _stopWaitTime)
            {
                nextState = FAN_STOPPED;
            }
        }
        else
        {
            nextState = FAN_STOPPED;
        }
    }
    else if (curTemp &gt;= _temp1 &amp;&amp; curTemp &lt; _temp2) { nextState = FAN_SPEED_1; } else if (curTemp &gt;= _temp2 &amp;&amp; curTemp &lt; _temp3) { nextState = FAN_SPEED_2; } else if (curTemp &gt;= _temp3 &amp;&amp; curTemp &lt; _temp4) { nextState = FAN_SPEED_3; } else if (curTemp &gt;= _temp4 &amp;&amp; curTemp &lt; _temp5) { nextState = FAN_SPEED_4; } else if (curTemp &gt;= _temp5)
    {
        nextState = FAN_SPEED_5;
    }
 
    // State changed so we need to change the GPIO pin output
    if (nextState != _currentState)
    {
        _currentState = nextState;
 
        // No need to toggle fan if we are still waiting
        if(_currentState != FAN_STOPPING)
        {
            togglePinState(_currentState);
        }
    }
}
 
int main(int argc, char *argv[])
{
 
    init(argc, argv);
 
    //Set our Logging Mask and open the Log    
    setlogmask(LOG_UPTO(_logLevel));
    openlog(DAEMON_NAME, LOG_CONS | LOG_NDELAY | LOG_PERROR | LOG_PID, LOG_USER);
 
    syslog(LOG_INFO, _stopFanOnly ? "Shutting down the fan only" : "Entering Daemon");
 
    //----------------
    //Main Process
    //----------------
 
    // Use BCM numbering
    wiringPiSetupGpio();
    pinMode(_pin1, OUTPUT);
    pinMode(_pin2, OUTPUT);
    pinMode(_pin3, OUTPUT);
 
    // This will shut down the fan
    togglePinState(_currentState);
 
    if (!_stopFanOnly)
    {
        while(true)
        {
            process();
            sleep(_checkInterval);
        }
    }
    //Close the log
    closelog ();
}

Daemonizing the executable:

Because we started writing the daemon for upstart (which was totally not the correct init system) we decided to have two versions: one for upstart and one for systemd.  In practice they are not that different and they can use the same compiled executable daemon.

We’ll go over both versions but we’ll only examine the systemd version in detail since that is the default init system on the current version of Armbian.

Upstart version:

For upstart we need to deploy our config file to /etc/init and the executable to /usr/bin. The config file was simply called pi-fan-controller.conf .

Systemd version:

The executable was again copied to /usr/bin. The unit file goes to /etc/systemd/system and the environment file to /etc.

The unit file(/etc/systemd/system/pi-fan-controller.service):

[Unit]
Description=pi fan controller daemon
 
[Service]
Type=simple
Restart=always
EnvironmentFile=/etc/pi-fan-controller.env
ExecStart=/usr/bin/pi-fan-controller -i $INTERVAL -p1 $PIN1 -p2 $PIN2 -p3 $PIN3 -l $LOGLEVEL -st $STOPWAITTIME -t1 $TEMP1 -t2 $TEMP2 -t3 $TEMP3 -t4 $TEMP4 -t5 $TEMP5
ExecStopPost=/usr/bin/pi-fan-controller -stopfan -p1 $PIN1 -p2 $PIN2 -p3 $PIN3 -l $LOGLEVEL
 
[Install]
WantedBy=multi-user.target

The environment file(/etc/pi-fan-controller.env):

# Which GPIO pins to use in BCM numbers
PIN1=2
PIN2=3
PIN3=4
 
# Set the CPU temperature thresholds
TEMP1=55
TEMP2=60
TEMP3=65
TEMP4=70
TEMP5=75
 
# Set the interval in seconds to read the CPU temperature
INTERVAL=5
 
# Default log level is LOG_NOTICE
# 0 Emergency
# 1 Alert
# 2 Critical
# 3 Error
# 4 Warning
# 5 Notice
# 6 Informational
# 7 Debug
LOGLEVEL=6
 
# Set the time in seconds for the fan stopping grace period. This means that the controller will wait 
# for the defined amount of time until it completely stops the fan.
STOPWAITTIME=120

You’ll notice that all of the settings that you can control are in the environment file, so you can control the daemon completely from there.

Installation guide for the pi-fan-controller daemon

  1. Install and compile the WiringOP library:  https://github.com/zhaolei/WiringOP
  2. Clone the pi-fan-controller repo:  https://github.com/Hoddikr/pi-fan-controller.git
  3. Navigate into the pi-fan-controller directory and run the following commands:
  • make
  • sudo make install.systemd
  • or
  • sudo make install.upstart depending on your init system.

Example:

cd pi-fan-controller
make
sudo make install.systemd

Now you should have the daemon up and running. For more information on the install/configure/uninstall process check out the readme on the pi-fan-controller github page.


From start then it was clear that we would need to put the circuit somewhere, and mount the fan on top of the boxes, and 5 cm fan is big for those boxes. 

We did not have the same cases, I had 3D printed case which I am not happy with do to design flaws in its model. And Hörður had a transparent bought case. So we set up with making generic design that can be mounted on top of either case and most cases.

3DDesignView1

So fan goes on left, and circuit in the box side by the fan. We did not put holes for the fan since, it may vary from fan to fan and drilling dow into the Orange PI box will be needed anyhow, so drilling through the print at same time should not be issue.

Also we did not draw hole from the fan to the circuit box for the fan wire so support material when printing would not be needed, so small hole will need to be drilled there.

3DDesignView2

But there is a hole for wires to take from the circuit box down to the Orange PI GPIO header.

After this was done then we had a number of problems when assembling the cases and neither the bought case nor the printed case were going to work.

Keeping the 3D design for the fan and the circuit then we proceeded in changing the printed case.

Base of the case before mod comes from here.

This piece bellow is from the original case linked above where we have cut out rectangular hole at top. This was  the top piece in the original case but will now be the middle piece.

OrangePI_PC_Middle

And this piece here bellow will be the new top piece of the main case, then the fan mount and circuit box will be mounted on top of it.

OrangePI_PC_Top

Bottom piece of the case is the original bottom piece from the the Thingverse page here:

Note that we had some issues with the original design which is still in our mods. 1. Some plastic around the USB ports needed to be sanded down to get perfect fit, and 2. screws don’t get good grip. So what I do is I drill through the screw holes and put screws with nuts all the way through all the three pieces. And then just put rubber pad under the box. Its even more important to put screws with such nuts when the box is in 3 pieces than when it was in 2 parts like the original design.

One more piece was needed, a fan grill which we used this design here from Thingverse. (Note flip it upside down before printing it gives far better results)

And here bellow is the final printed result, with Orange PI own cloud server, case in 3 pieces, then on top of it the fan controller system printed in 3 more pieces. Note that we did put plastic nuts under the fan in case if it goes loose and falls down into the machine. The screws in the circuit box are just body screws at top and also body screws under going from the top piece of the case and down under into the circuit box.

FinalCase

We made accurate drilling scheme to help do the drilling:

DrillingSceme

Use the SVG file bellow to print the drilling scheme. Do not open the SVG in web browser and trust that the sizes are correct. We used free application on the Mac called EazyDraw. That gave accurate print of the drilling scheme.

The files:

   Base design of the case (from Thingverse)

   Middle part of the case, top part of the case and fan controller circuit box as well as drilling scheme

   3D design for the fan grill (from Thingverse)


Update 11. Sep .2016:

We have been working on making much smaller circuit with surface mounted components.  A PCB design has been made as shown bellow.

FanControllerFabricated

It is not as expensive as you might think to have such PCB fabricated. For 10 such board then expected cost is $10, so only $1 per board, and then some tiny shipping cost.

The board came like this:

fcfabricated fcfabricatedbottom

After soldering the board was like this:

fcsoldered

The board is small and intended to place inside some of the cases. Its only 25,9 mm x 19,9 mm.

Surface mount called for different transistor, and different transistor called for recalculating good values for all the resistors. So here is component list for use with this surface mount board:

Transistors: BC818 NPN SOT-23 (3x)
Resistors: R3 and R5: 1kΩ (SMD 1206) (1/4 W) – Note that for those then SMD 0805 (1/8 W) is all right too. 
Resistor R4: 1,5kΩ  (SMD 1206) (1/4 W) – Note that for this one then SMD 0805 (1/8 W) is all right too.
Resistor R2: 4,7Ω (SMD 1206) (1/4W). This one has to be SMD 1206 to have enough wattage.
Resistor R1: 1Ω (SMD 1206) (1/4W). This one has to be SMD 1206 to have enough wattage.

Pins are as follows:
F-: Fan ground
F+: Fan +
VCC: 5V
GND: Ground
1: Control pin 1 (See up top in the article)
2: Control pin 2 (See up top in the article)
3: Control pin 3 (See up top in the article)

   Gerber files to fabricate copy of the Fan Controller board

The boards were fabricated at AllPCB, if you use the link bellow to sign up there then both you and Einhugur should get discount. (Which of course would help support our effort here)

All PCB


If not wanting to use 3 GPIO pins then its perfectly valid to connect just pin 1 or just pin 2 and configure the daemon to use that pin as pin number 1 for speed step 1.


That it for now, but we are working on testing building it from much smaller surface mounted components where the idea is to see if we can fit it into normal Orange PI box.

Note that this guide will almost certainly work with all Orange PI machines and almost certainly will work with Raspberry PI as well. (of course the 3D models need to be changed depending on machine though)


AliExpress.com Product – HoldPeak HP-770D Multimeter

Leave a Reply

Einhugur technical blog that involves Xojo and Einhugur Xojo plugin related topics

%d bloggers like this: