Computer controlled electric fan setup for the breadbox American

Hot-rodding the Nash/Rambler 195.6 overhead valve six

24 May 2010

Since I'd ditched the boat anchor generator in favor of a sweet little Mitsubishi 35-ampere alternator, I actually had the juice to run electric fans, and gain a half a precious horsepower in the bargain. Every bit helps in this car!

You can't really appreciate just how small this car is until you attempt to do something like fit electric fans in it. There's simply no room!

But I managed to fit a pair of 11" fans I got from Summit between the radiator and grille, using some simple fabricated brackets. The fans mount to the radiator support at each side, and anchor to the front valance/hood latch brace in the center. I bought the thinnest fans I could find, Summit part number PRO-67012, 2.5" thick.

The controller

I've never been happy with the way aftermarket electric fans behave. The thermostatic switches are hard to set to the right set point, and they run on too long after the car is shut down and cause huge surges and load dumps and are hard on wiring.

Over the last year, contemplating this hot rod engine build, I designed my ideal controller, using an Arduino microcontroller and a couple of solid-state relays (automotive application "high side drivers", VN05) chips. Since I'd ditched the genny I built the controller into the old voltage regulator case and mounted it in it's original location.

The controller uses the Stewart Warner aftermarket electric temperature gauge sender as it's sole source of information on engine state: on/off and temperature. (It would not be hard to make it work with factory on/off type senders and gauge regulators, but I wanted a more accurate controller). Fan motor speed varies slowly over time (as does engine temperature), so there is no huge surge as the fan clicks on. There is a simple calibrated knob on the box, marked in degrees F, that is the "set point"; the temperature at which the thermostat begins to open. From the set point to 15 degrees over the set point, fan speed varies from minimum (a quiet hum) to full blast.

When the car is turned off, the algorithm chooses a "cool down cycle" fan speed based upon the most recent temperature reading. If the engine was below the set point, the fans simply turn off, slowly. Otherwise the fan is set to half the engine-on speed (with a minumum speed) for five minutes, then the fans are again turned off slowly. Quiet, simple minimal stress.

Fan Controller schematic and code

you can find the schematic here. the code refers to a VN02, but the schematic shows a VN920. These are "high side drivers" made by ST Microelectronics. there are a lot of work-alikes that can be used instead of this increasingly hard to find part. For some reason the VN02's (lesser current capacity) are easier to get and you can parallel two of them; simply connect each chip pin to each other. in any case you should heatsink it well!

high side drivers are incredibly useful and solve all of the problems dealing with large, nasty, high-current loads like electric motors; load-dump, huge back-EMF spikes, etc that easily ruin electronics. the heatsink is necessary not so much for the heat produced when the chip is ON, but for the back EMF the chip is absorbing when energy to the motor is cut off (due to pulse-width modulation). the diode and resistor in the ground leg (pin 1) is for load dump. the diode is any "small signal" type eg. 1N4148. it's an incomplete design; there needs to be a resistor in series with the arduino output pin (pin 11) to current-limit in case of a load dump (say, 1000 ohms) and also a resistor from the VNxx IN (pin 2) to ground, something like 47K, for the unlikely case where the arduino output "floats" or is disconnected; this will ensure the VNxx will remain off when not specifically turned on. (this last resistor is actually in my design but left off accidentally from the schematic here.)

the controller has been installed and operating continuously since the may 2012 revision. the revision involved two changes: one, to remove the set-point pot (silly, since the thermostat is fixed at 180 degrees F) and two, to move the box from the engine compartment to inside the car, under the dash. the latter change solved a problem where the controller would overheat from under-hood temperatures (it was mounted up high where the old generator voltage regulator was) and need a reset.

/*

 Smart controller for an automotive cooling fan.
 
 Tom Jennings

 27 Jul 2012 Increased RANGE from 15 to 20.
 19 Jul 2012 Added low-power-state code found at http://interface.khm.de/index.php/lab/experiments/sleep_watchdog_battery/
 NOTE: the temp sensor reading is wildly variable, correlating to voltage; reads high (190)
 at idle then cool/cold at 1400rpm (150). Needs looking at.
 (almost certainly due to poor alternator voltage regulation)
 14 Jul 2012 Minor tweaks.
 04 Jun 2012 Dropped cool down to 3 min
 23 May 2012 New hardware, removed external setpoint pot, 
             fixed at 190F.
 07 Jun 2010 Formalized fan-off-below-setpoint (COLDOFF)
 04 May 2010 fixed calibration, cold-then-off bug 
 29 Mar 2010 working
 25 Feb 2010
 
 based upon WPS/Cars/Fan Controller/fan.c, 27 Apr 2007
 
 This is code for an automotive cooling fan controller that
 adjusts motor speed dynamically to correspond to heat load.
 
 Requires the installation of a Stewart Warner DLX series electric 
 100 - 280 degree electric temperature guage. IMPORTANT NOTE: the sensor/gauge uses
 an internal balanced bridge that makes it insensitive to battery
 voltage; having access to only one leg of the bridge means the
 sensed voltage is proportional to battery voltage. For our purposes
 however it's accurate enough.
 
 NOTE AGAIN: the above is not true; the indicated temperature is very voltage-sensitive
 wen taking from one leg (the sensor) as used here. the side effect is that at idle,
 when the alternator output is lower, this controller thinks the engine is hot.
 otherwise it works fine. the "real" solution is to simply use a dedicated
 sensor for this controller, feed it from a regulated voltage (eg. 5V) and
 work up the correct polynomial to linearize it.
 
 NOTE: Could certainly be made to work with the factory make-break
 gauge system, but would require a long time constant filter. Probably
 just making the averaging array large (Nyquist) relative to make/break
 time would work.
 

OPERATING CHARACTERISTICS

 NOTE: names in (PARENS) refer to definitions within the code.
 
 The SW sensor is the sole sensor for this controller, both engine on/off
 and current temperature. [23 May 2012 NOT: There is a single potentiometer, 
 called SET POINT, that sets the temperature that the controller begins to 
 run the fan motor. The set point pot runs from 160 degrees (CCW) to 
 205 degrees (CW).]
 
 The controller monitors engine status and temperature continuously.
 The fan is only run when it is needed, and fan speed always changes
 slowly, avoiding large electrical system current excursions and load dumps,
 and overall consumes less energy.
 
 The SET POINT pot sets the temperature at which the controller centers
 its operation. This should be the installed thermostat temperature, more
 or less.
 
 At (COLDOFF) degrees below the set point, the fan operates at its
 lowest possible speed (SLOW) (about 25%). (Below this many fans will stall
 and consume current but not operate.)
 
 Engine temperature above the set point causes the fan speed to increase.
 Fan speed is maximum at (RANGE) degrees over the set point. The fan speed
 varies linearly between the set point and set point + (RANGE).
 
 
 When the engine is turned off, the controller uses the last-known
 temperature to determine what to do. If the temperature was at or above
 the set point, the fan will run for N minutes after engine-off.
 The fan speed will be (SLOW) (sufficient, because the engine is producing
 no additional heat and water is no longer circulating) unless the
 fan speed had been (FAST), in which case the fan will run (HALF).
 

TEMPERATURE AVERAGING, AND LOOP TIME

 The main loop and all functions are based upon a very slow (ITER)
 loop time; it's a 500lb block of cast iron, temperature changes slowly!
 This code was originally going to use the factory temp sensor but
 they seem to vary too widely and the make-break dash voltage regulator
 is a PITA so I went with the predictably accurate SW system.
 
 There is an array that generates an average over (AVG * ITER) seconds,
 if this were 30 or 45 seconds and had 100 samples it would probably work
 with the factory sensor with no electronic changes.
 
HARDWARE

 As little as possible! An arduino controller, two VN05 (high side driver)
 chips, and a resistive divider (55% reduction) to scale the 12V sensor to the 
 Arduino's 5VDC in limit.
 
 

 See #define for SLOPE for cal process.


EXTERNAL CONNECTIONS

 GROUND
 BATTERY
 TEMPERATURE SENSOR
 FAN

 
 */


#include 
#include 

#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif

 
/* Fixed temperature set point. */
#define SETPOINT 190

/* Inputs. */
//#define CAL 5
#define SENS 0

/* Outputs. */
#define FAN 11
#define LED 13

/* Fan runs after ignition off, iterations (1 sec each iteration). */
#define COOLTIME (3L*60L)

/* Full scale temperature; beyond this the
engine is assumed to be off. */
#define FS 340

/* The fan will begin to run at minimum speed at
temperature-COLDOFF degrees. */
#define COLDOFF 5

/* Temperature return value that means car is off. */
#define CAROFF -1

/* Fan speeds. */
#define OFF (0)
#define SLOWEST (20)
#define SLOW (70)
#define HALF (130)
#define FAST (255)

/* Arduino A/D, volts per bit. */
/* Temperature sensor input range, A/D 0 - 1023. */
#define VPB ((float)(5.0/1024))

/* CALIBRATION: The input runs through a voltage divider with a
for a scale factor of 0.55;  9.0V at the sensor is A/D full scale.

From here calculate m and Y intercept:

http://www.mathsisfun.com/straight-line-graph-calculate.html 

X,Y pairs are scaled sensor voltage, temperature.

*/

#define SENSSLOPE -139.4
#define SENSINTERCEPT 726.6


/* Set point pot limits, A/D 0 - 1023. */
#define SETMIN 160
#define SETMAX 205

/* Temperature range over which we servo. */
#define RANGE 20

/* Fan speed rate-of-change, in units per iteration. */
#define FROC 5

unsigned char state = 0; 	/* state machine */
int sensor;                     // raw analog sensor reading
int temp;                       // calculated temperature
int setpt = SETPOINT;
char engineoff;                 // true when engine is off
int fanspeed;                   // current (changing) fan speed
int fangoal;                    // desired (goal) fan speed
long coolT;                     // iterations we spend in cooldown state

#define AVG 5
int avg[AVG], a;                // average sensor reading junk

#define LOWPASS 7
unsigned char offcount, oncount; 

volatile boolean f_wdt=1;



void setup() {

  analogWrite (FAN, fanspeed= 0); // stop fan immediately.
  pinMode (LED, OUTPUT);

  Serial.begin (9600);
  Serial.println ("Hello.");

  for (int i= 3; i--;) {
    digitalWrite (LED, 1);         // blink LED at startup
    delay (75);
    digitalWrite (LED, 0);
    delay (300);
  }
  state= 0;
  engineoff= 1;
  info ("Off");
  delay (100);         // (allows info() output to drain...)  
  
  // CPU Sleep Modes 
  // SM2 SM1 SM0 Sleep Mode
  // 0    0  0 Idle
  // 0    0  1 ADC Noise Reduction
  // 0    1  0 Power-down
  // 0    1  1 Power-save
  // 1    0  0 Reserved
  // 1    0  1 Reserved
  // 1    1  0 Standby(1)

  cbi( SMCR,SE );      // sleep enable, power down mode
  cbi( SMCR,SM0 );     // power down mode
  sbi( SMCR,SM1 );     // power down mode
  cbi( SMCR,SM2 );     // power down mode

  setup_watchdog(7);   // watchdog timer fires every N seconds
}

// The main loop stays in low-power mode as much as possible; for maximum
// car-off energy savings, don't do anything in state 0.

void loop() {
int d;
float f;

  gettemp();                          // read the temperature sensor,

  switch (state) {

 // ENGINE OFF. Anything done in this state draws current from the battery
 // when the car is off. Not good. 
 // Lowe power mode and watchdog timer are used here as timer; we wake
 // up periodically so that the temp sensor is read and to notice if 
 // the car is turned on again.
  case 0:
//    if (f_wdt != 1) return;             // wait for watchdog timer
//  f_wdt=0;                            // reset it

    digitalWrite (LED, 1);            // blink the LED briefly,
    delay (75);
    digitalWrite (LED, 0);
 
    if (! engineoff) state= 1;        // watch for engine on
    system_sleep();                   // sleep here, for N seconds
    analogWrite (FAN, fanspeed= 0);   // ensure fan is off
    Serial.begin (9600);
    
    return;                           // does not execute code below!
    break;

// ENGINE RUNNING. Calc fan speed goal.
  case 1:
    info ("Run");
    // From set point to RANGE degrees over set point, fan speed varies
    // SLOW to FAST.
    f= (temp - setpt) / (float)RANGE; // fraction 0 - 1  within RANGE
    if (f < 0.0) f= 0.0;              // bound it
    if (f > 1.0) f= 1.0;
    fangoal= f * (FAST - SLOW) + SLOW;

    // No sense running the fan when the engine is cold.
    if (temp <= setpt - COLDOFF) fangoal= 0;

    // If we are going to run the fan, then set the
    // minimum speed; below that the motor sits and hums.
    if ((fanspeed < SLOW) && (fangoal >= SLOW))
      fanspeed= SLOW;                 // minimum motor speed
      
    if (engineoff) state= 2;          // engine off, go cooldown
    break;

// JUST TURNED OFF. If engine is hot, run the cooldown cycle.
  case 2:
    info ("Engine off");
    if (fangoal > HALF)       fangoal= HALF;
    else if (fangoal > SLOW)  fangoal= SLOW; // but not SLOW
    else                      fangoal= OFF;  // no need to run fan
    coolT= COOLTIME;                  // how long to spend in cooldown
    state= (fangoal < SLOW) ? 4 : 3;  // run cooldown if engine hot
    if (! engineoff) state= 1;        // car turned back on
    break;
 
// RUN THE COOLDOWN CYCLE.
  case 3:
    info ("Cool down");
    if (fanspeed <= 1) state= 4;      // fan speed zero
    if (--coolT <= 0) state= 4;       // timed cooldown cycle
    if (! engineoff) state= 1;        // car turned back on
    break;

// WAIT FOR FAN TO STOP. Since it ramps slowly it may take
// some seconds.
  case 4:
    info ("Fan off");
    fangoal= 0;                       // now fan off
    if (fanspeed <= 1) state= 0;      // wait for fan off
    if (! engineoff) state= 1;        // car turned back on
    break;
  }

// Here we adjust the actual fan speed, slowly, to match the
// fan speed goal.

  d= fangoal - fanspeed;              // speed difference
  if (d < -FROC) d= -FROC;
  if (d > FROC) d= FROC;              // limit rate of change
  fanspeed += d;
  if (fanspeed < SLOWEST) fanspeed= 0;// slowest possible speed
  analogWrite (FAN, fanspeed);
  
// Since we do nothing but wait, anyway, this suffices, when the car is on.
  delay (1000);
}


/* Display useful junk. */

void info (char *m) {
    Serial.print ("state: "); Serial.print (m);
//    Serial.print (" setpt="); Serial.print(setpt, DEC);
    Serial.print (" sens="); Serial.print (sensor, DEC);
    Serial.print (" temp="); Serial.print (temp, DEC);
    Serial.print (" fan="); Serial.print (fanspeed, DEC);
    Serial.print (" goal="); Serial.println (fangoal, DEC);
}




/*
Read the temperature sensor and return the reading, or -1
if the car is off. This calculates an average to filter out
spurious or momentary readings.
*/

void gettemp (void) {

int i;
unsigned long sum;

  // May 2012: fixed set point
  // Calc set point as degrees F.
  //  setpt= analogRead(CAL) * (float)((SETMAX - SETMIN) / 1024.0) + SETMIN;

  sensor= avg[a]= ((float)analogRead(SENS) * VPB) * SENSSLOPE + SENSINTERCEPT;
 
  // If a valid temperature, add to average temp.
  if (avg[a] < FS) {
    if (++a == AVG) a= 0;
    if (++oncount > LOWPASS) {           // if we have enough samples
      oncount= LOWPASS;
      offcount= 0;                       // car is not off
      engineoff= 0;

      for (i= 0, sum= 0L; i < AVG; i++)  // create average temp
        sum += avg[i];
      temp= sum / AVG;
    }
  // else engine is off.
  } else if (++offcount >= LOWPASS) {
    oncount= 0;                          // require refill of avg buff
    offcount= LOWPASS;                   // (might be off a long time)
    engineoff= 1;                        // (don't wrap around)
  }
}







//****************************************************************  
// set system into the sleep state 
// system wakes up when wtchdog is timed out
void system_sleep() {
  
  cbi(ADCSRA,ADEN);                    // switch Analog to Digital converter OFF
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); // sleep mode is set here
  sleep_enable();
  sleep_mode();                        // System sleeps here
  sleep_disable();                     // System continues execution here when watchdog timed out 
  sbi(ADCSRA,ADEN);                    // switch Analog to Digital converter ON
}

//****************************************************************
// 0=16ms, 1=32ms,2=64ms,3=128ms,4=250ms,5=500ms
// 6=1 sec,7=2 sec, 8=4 sec, 9= 8sec

void setup_watchdog(int ii) {
byte bb;
int ww;

  if (ii > 9) ii= 9;
  bb= ii & 7;
  if (ii > 7) bb |= (1 << 5);
  bb |= (1 << WDCE);
  ww= bb;

  MCUSR &= ~(1 << WDRF);
  // start timed sequence
  WDTCSR |= (1 << WDCE) | (1 << WDE);
  // set new watchdog timeout value
  WDTCSR = bb;
  WDTCSR |= _BV(WDIE);


}
//****************************************************************  
// Watchdog Interrupt Service / is executed when  watchdog timed out
ISR(WDT_vect) {
  
  f_wdt= 1;  // set global flag
}