Jump to content

DIY fan controller with Arduino

Olaf6541
Go to solution Solved by Olaf6541,

Last update of this topic

I've installed the fan controller in the pc:

 

vRxiivS.jpg

 

And a comparison of the LEDs off vs on, I also placed the temp sensor into the loop:

 

3MQ3gqo.jpg

 

UImJYgq.jpg

 

Currently tweaking the fan curves for the fans, everything seems to work as intended. ?

Thanks everyone for the help, you guys were very helpful!

On 1/25/2019 at 8:59 PM, Unimportant said:

Can you post the code you wrote for this aswell ? So we can check if it does not contain any of the same atomicity errors.

  

This is really the part where you ought to get a 'scope. Even an cheap, fourth hand, ancient analog scope would be better then shooting in the dark. (Don't be tempted to buy one of those toy ebay kits tough).

Alright here's the code. I removed the entire rpm section and didn't have a backup so I quickly added it back. I haven't been able to physically test it again but this should be exactly what I had before (with the interrupts, not the sampling). Here I had problems when physically attaching both input pins (roughly halved each input) and when adding in the PWM mode (shoots up to 20k-30k RPM).

 

Spoiler


//---------------------- Fan Controller ----------------------//

// This fan controller is designed to control two temperature dependant PWM 4 pin outputs (A and B) and an external 12V output (C).
// The Arduino Uno is used in combination with a splitted MOLEX 12V connector to create two 4 pin PWM output headers.
// The extrnal 12V is regulated by a BJT controlled by the Arduino.

//------------------------ Temperature ------------------------//

// The fan controller measures temperature using a thermistor in a voltage divider setup.
// The current setup is configured for use with a 10k NTC temperature sensor.
// The thermistor has to be calibrated by determining the Steinhart-Hart coefficients.

//------------------------ PWM control ------------------------//

// The fan controller is able to generate two independant PWM duty cycles.
// It utilizes timer1 to produce a 25kHz square wave on pins 9 and 10.
// Duty cycle determination can be fully customised as desired, in this setup the fan curve function 1/(1 + e^-x) is used.

//------------------------- Tachometer ------------------------//

// The RPM wire is monitored with interrupts to measure the amount of pulses per second.
// This is extrapolated to RPM every loop. Internal pullup resistors are used.

//------------------------- Fan curve -------------------------//

// The fan curves can be modified using the fan curve coefficients Ta/a1/a2/a3/Tb/b1/b2/b3; 
// Coefficients Ta and Tb are used to create a zero RPM fan mode: when T<Ta, PWM duty cycle is set to 0%.
// To disable the zero RPM mode, simply set Ta/Tb to a low temperature (e.g. 0.0).
// Coefficients a1/b1 (scalar) are used to set to slope of the fan curve, pwm(a1*T).
// If a fixed RPM is desired you must set a1=0.0, pwmA(T) becomes 50%+a3.
// Coefficients a2/b2 (C) are used to shift the fan curve along the x-axis, pwm(T-a2).
// Coefficients a3/b3 (%) are used to shift the fan curve along the y-axis, pwm(T)+a3.
// For this PWM control the assumption is made that fans are turned off at 0% duty cycle.
// Some fans are designed to stay at a minimum pwm duty and cannot be shutdown with PWM control.
// The controller will always check and prevent PWM duty cycles from leaving the 0-100% range.
// This makes it easier to use a linear fan curve and allows easy shifting along the y-axis.

//------------------------- 12V output -------------------------//

// The third output is designed to power LEDs inside the computer case controlled by Serial monitor input.
// To turn on the LEDs, a "1" must be sent to the Arduino.
// to turn off the LEDS, a "0" must be sent to the Arduino.
// (Any character that is not "1" is treated as "0")

//----------------------- Initialisation -----------------------//

// Initialisation of the system requires constants:
//  - Resistance R1
//  - Thermistor Steinhart-Hart coefficients c1/c2/c3
//  - Fan curve coefficients Ta/a1/a2/a3/Tb/b1/b2/b3
//  - I/O pins (Temp, rpmA/B, pwmA/B, outputC)

//------------------------- Constants -------------------------//

const int TempInput = A0;   //Pin for temperature input
const int rpmAinput = 2;    //Pin for RPM input A
const int rpmBinput = 3;    //Pin for RPM input B
const int outputC = 7;      //Pin for output C
const int pwmAoutput = 9;   //Pin for PWM output A
const int pwmBoutput = 10;  //Pin for PWM output B

const float R1 = 10000.0;           //Resistor R1 resistance
const float c1 = -1.850744540e-03;  //Steinhart-Hart coefficient of thermistor
const float c2 = 7.349456989e-04;   //Steinhart-Hart coefficient of thermistor
const float c3 = -20.02233861e-07;  //Steinhart-Hart coefficient of thermistor

const float Ta = 22.0;   //Fan curve 0 RPM temperature A (pwmA=0% when T<Ta)
const float a1 = 0.40;  //Fan curve coefficient a1 (slope, pwm(a1*T) )
const float a2 = 28.0;  //Fan curve coefficient a2 (x offset, pwm(T-a2) in C)
const float a3 = 0.0;   //Fan curve coefficient a3 (y offset, pwm(T)+a3 in %)

const float Tb = 0.0;   //Fan curve 0 RPM temperature B (pwmB=0% when T<Tb)
const float b1 = 0.40;  //Fan curve coefficient b1 (slope, pwm(b1*T) )
const float b2 = 38.0;  //Fan curve coefficient b2 (x offset, pwm(T-b2) in C)
const float b3 = 0.0;   //Fan curve coefficient b3 (y offset, pwm(T)+b3 in %)

//------------------------- Variables -------------------------//

int input = 0;  //Serial input C
int Tc = 0;     //Value output C

float V = 0;   //Thermistor voltage
float R2 = 0;  //Thermistor resistance
float T = 0;   //Thermistor temperature

word pwmA = 0;  //duty cycle (0-320 = 0-100% duty cycle)
word pwmB = 0;  //duty cycle (0-320 = 0-100% duty cycle)
word rpmA = 0;  //RPM of output A
word rpmB = 0;  //RPM of output B

volatile word counterA = 0; //Pulse counter rpmA
volatile word counterB = 0; //Pulse counter rpmB

//------------------------- Functions -------------------------//

void rpmCounterA(){  //Interrupt function for rpmA
  counterA++;        //Counts amount of pulses
}

void rpmCounterB(){  //Interrupt function for rpmB
  counterB++;        //Counts amount of pulses
}

void setup() {
  
  Serial.begin(9600);  //Serial data rate
  
  pinMode(pwmAoutput, OUTPUT);       //Output A (pwmA)
  pinMode(pwmBoutput, OUTPUT);       //Output B (pwmB)
  pinMode(rpmAinput, INPUT_PULLUP);  //Input A (rpmA)
  pinMode(rpmBinput, INPUT_PULLUP);  //Input B (rpmB)
  pinMode(outputC, OUTPUT);          //Output C (Tc)

  attachInterrupt(digitalPinToInterrupt(2), rpmCounterA, RISING);  //Interrupt for rpmA
  attachInterrupt(digitalPinToInterrupt(3), rpmCounterB, RISING);  //Interrupt for rpmB

  TCCR1A = 0;             //Clear timer registers
  TCCR1B = 0;
  TCNT1 = 0;
  
  TCCR1B |= _BV(CS10);    //No prescaler
  ICR1 = 320;             //PWM mode counts up 320 then down 320 counts (16MHz/640=25kHz)
  
  OCR1A = pwmA;           //0-320 = 0-100% duty cycle
  TCCR1A |= _BV(COM1A1);  //Output A clear rising/set falling
  
  OCR1B = pwmB;           //0-320 = 0-100% duty cycle
  TCCR1A |= _BV(COM1B1);  //Output B clear rising/set falling
  
  TCCR1B |= _BV(WGM13);   //PWM mode with ICR1 Mode 10
  TCCR1A |= _BV(WGM11);   //WGM13:WGM10 set 1010
  
}

void loop() {
  
  V = (float)analogRead(TempInput);                                     //Read out divider voltage
  R2 = R1 * (1023.0 / V - 1.0);                                         //Determine resistance of thermistor
  T = (1.0 / (c1 + c2*log(R2) + c3*log(R2)*log(R2)*log(R2))) - 273.15;  //Calculate temperature using Steinhart-Hart equation
  
  if (Serial.available() > 0){  //check for input for output C
    input = Serial.read();      //Retrieve the serial input
    if(input != 10){            //block 'line feed' as input
      Tc = input;               //Assign input to trigger value
    }
  }                     
  
  if (T > Ta) {
    pwmA = 3200*(1/(1+exp(a1*(-T+a2)))+a3/100.0);  //Determine pwmA(T) for output A
    if(pwmA < 1)  {pwmA = 1;}                     //Keep pwmA within 0-100% duty cycle
    if(pwmA > 319){pwmA = 319;}
    }
  else            {pwmA = 1;}                     //Output A to 0% pwm duty (Fan off)
  OCR1A = pwmA;                                   //Assign duty cycle A to register
  
  if (T > Tb) {
    pwmB = 320*(1/(1+exp(b1*(-T+b2)))+b3/100.0);  //Determine pwmB(T) for output B
    if(pwmB < 1)  {pwmB = 1;}                     //Keep pwmB within 0-100% duty cycle
    if(pwmB > 319){pwmB = 319;}   
    }
  else            {pwmB = 1;}                     //Output B to 0% pwm duty (Fan off)
  OCR1B = pwmB;                                   //Assign duty cycle B to register

  rpmA = counterA*30;  //Calculate RPM of output A
  counterA = 0;        //Reset counterA
  rpmB = counterB*30;  //Calculate RPM of output B
  counterB = 0;        //Reset counterB
  
  Serial.print("T: ");  //Display thermistor temperature
  Serial.print(T, 1);
  Serial.print("°C    ");
  
  Serial.print("A: ");  //Display rpm and duty cycle of output A
  Serial.print(rpmA);
  Serial.print("RPM, ");
  Serial.print(pwmA*10/32);
  Serial.print("%    ");
  
  Serial.print("B: ");  //Display rpm and duty cycle of output B
  Serial.print(rpmB);
  Serial.print("RPM, ");
  Serial.print(pwmB*10/32);
  Serial.print("%    ");

  if(Tc == 49){
    digitalWrite(outputC, HIGH);  //Set output C to HIGH (LEDs on)
    Serial.println("C: ON");      //Display state of output C
  }
  else{
    digitalWrite(outputC, LOW);   //Set output C to LOW (LEDs off)
    Serial.println("C: OFF");     //Display state of output C
  }
  
  delay(1000);  //Repeat every second
}

 

Also a picture of what I managed to put together so far, tbh I'm quite happy with the result:

 

mY3rDk7.jpg

 

As for the 'scope, I'll have to 'scope out my options ?

Link to comment
Share on other sites

Link to post
Share on other sites

You don't need pwm fans to send a pwm signal to them. You'd just need a fast switching mosfet to switch the 12V power line going to the fan. The fan is, in essence, a DC motor, and can be controlled as such. 

 

EDIT: Also, something to think about, serial connections with Atmel chips are super simple if you have the right adapter. Hell, you could probably just use the USB that's already connected (instead of using the pwm signal from the motherboard.) You could send/receive all necessary data over that including temps/rpms, etc. 

Link to comment
Share on other sites

Link to post
Share on other sites

4 hours ago, corrado33 said:

You don't need pwm fans to send a pwm signal to them. You'd just need a fast switching mosfet to switch the 12V power line going to the fan. The fan is, in essence, a DC motor, and can be controlled as such. 

The hall sensor is fed directly from the 12V source, feeding it with a chopped up PWM signal will only make it worse.

That's why you need a continuous 12V supply and a separate PWM input.

 

fan_speed_02.gif?la=en

4 hours ago, corrado33 said:

EDIT: Also, something to think about, serial connections with Atmel chips are super simple if you have the right adapter. Hell, you could probably just use the USB that's already connected (instead of using the pwm signal from the motherboard.) You could send/receive all necessary data over that including temps/rpms, etc. 

Yeah I'm already using the PWM signal generated from the Arduino itself. Only a molex connector for 12V and a USB connector for power and data to/from the Arduino are needed.

Link to comment
Share on other sites

Link to post
Share on other sites

@Olaf6541 So you've made 'CounterA' and 'CounterB' volatile, that's good, but you didn't do anything about the atomicity issue.

 

16 hours ago, Olaf6541 said:

rpmA = counterA*30; //Calculate RPM of output A 
counterA = 0; //Reset counterA 
rpmB = counterB*30; //Calculate RPM of output B 
counterB = 0; //Reset counterB

 

A single line of C code such as "rpmA = counterA * 30" can result in a whole lot of assembly instructions, especially something like multiplying a multi-byte variable on a tiny 8-bit micro-controller.  So this seemingly simple action could result in tens or hundreds of machine instructions. Ask yourself what happens when a interrupt occurs just when the controller is busy with this multiplication. The controller is busy working with 'counterA' or 'counterB', doing god knows what with it under the hood. So during the operation those variables could be in a invalid intermediate state, and then the interrupt code starts messing with it at the same time.

 

Such operations that are not completed in a single cycle and thus some asynchronous code could see things in a invalid intermediate state are called "non-atomic" operations. You should guard against those. In this case a possible solution could be:

  • disable interrupts
  • Copy counterA and counterB to temporary variables such as copyCounterA and copyCounterB. Since interrupts are disabled this non-atomic copy won't be interrupted.
  • Clear counterA and counterB.
  • re-enable interrupts.
  • do the multiplication with copyCounterA and copyCounterB. Leave counterA and counterB alone while interrupts are enabled.

Furthermore I'd try greatly simplifying the code for testing purposes, remove all that complex math and just control the fan manually (with a RPM+ and RPM- button for example) and then check if your pulses are coming in as they should. In the same vein as before, when you write something like:

16 hours ago, Olaf6541 said:

V = (float)analogRead(TempInput); //Read out divider voltage 
R2 = R1 * (1023.0 / V - 1.0); //Determine resistance of thermistor 
T = (1.0 / (c1 + c2*log(R2) + c3*log(R2)*log(R2)*log(R2))) - 273.15;

//and...

pwmA = 3200*(1/(1+exp(a1*(-T+a2)))+a3/100.0);

 

You should realize this too will probably result in many hundreds or even thousands of machine instructions. The AVR is a microcontroller, not a maths processor. It has to emulate floating point in software (so get rid of it, you can avoid floating point in nearly all cases), and functions like log and exp are also tough on the little thing. Add some slow serial comm's on top of that and you could easily be missing events because your seemingly simple C code is in fact bogging the AVR down.

Link to comment
Share on other sites

Link to post
Share on other sites

In addition to what Unimportant says, consider using bit shifts instead of multiplications where possible. The compiler will optimize as much as possible but it's still worth checking the generated code to see what the compiler generates.

For example, result = value * 30 could be rewritten as

result = value <<4 + value <<3 + value <<2 + value <<1  

or

result = value*(32-2)  = value * 32 - value * 2 = value * 25 - value * 21  = value << 5 - value <<1

 

Also, you have 2 KB of SRAM in the arduino uno, so if you lower the precision of the ADC to 8 bit, you get 256 possible values, so you could use 384 bytes of ram to store a table with precalculated values for temperature, 12 bits for every ADC result ... so you could store temperatures from 0c to 100c in 0.1 degree steps.

Do you need more than 0.1 degree steps for this application?

 

Link to comment
Share on other sites

Link to post
Share on other sites

2 hours ago, Unimportant said:

So you've made 'CounterA' and 'CounterB' volatile, that's good, but you didn't do anything about the atomicity issue.

 

1 hour ago, mariushm said:

In addition to what Unimportant says, consider using bit shifts instead of multiplications where possible. The compiler will optimize as much as possible but it's still worth checking the generated code to see what the compiler generates.

I think you guys are right, I removed as much code as I could and did some more testing, this is what I ended up with:

Spoiler

//------------------------- Constants -------------------------//

const int pwmAoutput = 9;  //Pin for PWM output A
const int rpmAInput = 2;   //Pin for RPM sensor A input

//------------------------- Variables -------------------------//

word pwmA = 319;     //Set duty cycle (0-320 = 0-100% duty cycle)
word rpmA = 0;       //Fan RPM of output A

volatile word counterA = 0;   //Counter rpmA

//------------------------- Functions -------------------------//

void rpmCounterA(){  //Interrupt function for rpmA
  counterA++;        //Counts amount of pulses
}

void setup() {
  Serial.begin(9600);  //Serial data rate
  
  pinMode(pwmAoutput, OUTPUT);       //Output A (pwmA)
  pinMode(rpmAInput, INPUT_PULLUP);  //Input rpm sensor (rpmA)

  attachInterrupt(digitalPinToInterrupt(2), rpmCounterA, RISING);  //Interrupt for rpmA
  
  TCCR1A = 0;            //Clear timer registers
  TCCR1B = 0;
  TCNT1 = 0;
  
  TCCR1B |= _BV(CS10);   //No prescaler
  ICR1 = 320;            //PWM mode counts up 320 then down 320 counts (25kHz)
  
  OCR1A = pwmA;          //0-320 = 0-100% duty cycle
  TCCR1A |= _BV(COM1A1); //Output A clear rising/set falling
  
  TCCR1B |= _BV(WGM13);  //PWM mode with ICR1 Mode 10
  TCCR1A |= _BV(WGM11);  //WGM13:WGM10 set 1010
  
}

void loop() {
  noInterrupts();
  rpmA = counterA*30;  //Calculate RPM of output A
  counterA = 0;        //Reset counterA
  interrupts();
  Serial.println(rpmA);
  
  delay(1000);

}

 

 

I measured RPM at 1/3 PWM, 2/3 PWM and full power:

OHtTyBn.png

Not completely linear but that looks pretty good.

With the RPM calculation moved to Serial.print(rpmA) I could even get 1 more pulse (+30RPM) an average so I guess it's just too much code around it, the full code doesn't go above 750RPM on full fan power.

 

Edit: made a mistake in the full code for setting the fan to full power, it was actually set at~2/3 PWM so I'll do some more testing, updates will follow...

 

Edit 2: Well, guess it completely works now, and it's probably due to bad solder/contact between pins (reapplied the solder and put tape over it) and that mistake of running 215/320 PWM instead of 319/320...

 

xXSn5pP.png

 

Also I realized that the Vardar 120s isn't actually 1200, it's 1150 RPM so its pretty accurate.

Link to comment
Share on other sites

Link to post
Share on other sites

Last update of this topic

I've installed the fan controller in the pc:

 

vRxiivS.jpg

 

And a comparison of the LEDs off vs on, I also placed the temp sensor into the loop:

 

3MQ3gqo.jpg

 

UImJYgq.jpg

 

Currently tweaking the fan curves for the fans, everything seems to work as intended. ?

Thanks everyone for the help, you guys were very helpful!

Link to comment
Share on other sites

Link to post
Share on other sites

3 hours ago, Olaf6541 said:

Last update of this topic

I've installed the fan controller in the pc:

 

vRxiivS.jpg

 

And a comparison of the LEDs off vs on, I also placed the temp sensor into the loop:

 

 

 

Currently tweaking the fan curves for the fans, everything seems to work as intended. ?

Thanks everyone for the help, you guys were very helpful!

RIP tiny heatsinks

ASUS X470-PRO • R7 1700 4GHz • Corsair H110i GT P/P • 2x MSI RX 480 8G • Corsair DP 2x8 @3466 • EVGA 750 G2 • Corsair 730T • Crucial MX500 250GB • WD 4TB

Link to comment
Share on other sites

Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×