PID Control of Fan With Temperature Sensor

First and foremost, I’ve been reading a bunch about Arduino for the past few months. Delved into PID control for a fan on an amp rack in my truck. Mostly straightforward but for the life of me cannot get the PWM for the fan to work.
Components:
Arduino UNO
DHT22 temp sensor
BLEXAR app
HM10 BT transmitter/receiver
Noctura 4 wire PWM fan (NF-A8)
2N3904 BJTs to drive fan relay and PWM to fan
IN4001 diode around the relay coil
Status LED (flashes every 5 seconds)

Libraries:
PID from Dave
Software Serial
DHT

I would like to have the relay open when the duty cycle is below Noctura’s min duty cycle (approx 20%) and closed when above 20%. As written, nothing happens. If I force the RELAYPIN 8 to HIGH the relay closes, fan starts but still no change in speed based on temp.

Any help would be appreciated.

Mike

// Sketch uses DHT22 temp sensor with BlEXAR BT to display temperature. Also uses PID to control Noctura 4 wire fan.

#include <DHT.h>
#include <SoftwareSerial.h>
#include <PID_v1.h>


// -----PINS **cannot use PIN 3**
//PIN 0 - RX
//PIN 1 - TX
const int RELAYPIN = 8;            // pin number for fan control relay 
const int LEDPIN = 9;              // pin number for the running LED (on side of the amp rack) CHANGE PIN NUMBER
const int DHTPIN = 2;              // pin number for temp sensor
#define PWM11        OCR2B         // pin number for 25kHz PWM to fan on Timer 2 (pins 3 and ll only)

// -----CONSTANTS
const int BLINKDURATION = 500;    // number of millisecs the running LED is on 
const int LEDINTERVAL = 5000;     // number of millisecs between blinks
const int TEMPINTERVAL = 2000;    // number of millisecs between temp data from DHT22
#define DHTTYPE DHT22             // DHT 22  (AM2302), AM2321
const int KP = 0.4;               //PID constants - evaluate adding aggressive numbers per example
const int KI = 0.4;
const int KD = 0.05;
const byte TIMER2_CONSTANT = 79;  // set Timer2 to 25kHz output
const int MIN_DUTY_CYCLE = 15;    // minimum duty cycle for fan (is about 20% or 450 rpm) 15/79 for OCR2B @ 25kHz
      

//----- VARIABLES 

byte LED_STATE = LOW;                    // record whether the LED is on or off
double T = 0;                            // current temp from DH22
double TEMP_SETPOINT = 88;               // temperature setpoint 88F
double DUTY_CYCLE = 0;                   // PID output to control duty cycle of fan; set to zero at setup
unsigned long currentMillis = 0;         // stores the value of millis() in each iteration of loop()
unsigned long previousLedMillis = 0;     // will store last time the LED was updated
unsigned long previousTempMillis = 0;    // will store last time temp data sent to Arduino

//Initialize libraries
DHT dht(DHTPIN, DHTTYPE); 
SoftwareSerial ble_device(0,1);           // HM-10 TX/RX pins
PID FAN_PID(&T, &DUTY_CYCLE, &TEMP_SETPOINT, KP, KI, KD, REVERSE); //INPUT, OUTPUT, TARGET TEMP, CONS1, CONS2, CONS3, MODE

//========

void setup() {
                                          
    pinMode(PWM11, OUTPUT);                     //set PWMPIN as output

    pwm25kHzBegin();                            //Set timer2 for 25kHz PWM signal with 0% duty cycle on startup 

    pinMode(LEDPIN, OUTPUT);                    // set the Led pin as output:

    pinMode(RELAYPIN, OUTPUT);                  //set the fan relay pin as output
    
    pinMode(DHTPIN, INPUT);                     //set the DHT pin as input

    dht.begin();                                //start DHT sensor
 
    ble_device.begin(9600);                     //prepare BLE module      

    FAN_PID.SetMode(AUTOMATIC);                 //set PID calc to automatic

    //fanPID.SetSampleTime();                   // IS THIS NEEDED?
    
    FAN_PID.SetOutputLimits(0,255);             //min and max duty output
        
}

//=======

void pwm25kHzBegin() {
  TCCR2A = 0;                                                 // TC2 Control Register A
  TCCR2B = 0;                                                 // TC2 Control Register B
  TIMSK2 = 0;                                                 // TC2 Interrupt Mask Register
  TIFR2 = 0;                                                  // TC2 Interrupt Flag Register
  TCCR2A |= (1 << COM2B1) | (1 << WGM21) | (1 << WGM20);      // OC2B cleared/set on match when 
                                                                                                    // up/down counting, fast PWM
  TCCR2B |= (1 << WGM22) | (1 << CS21);                       // prescaler 8
  OCR2A = 79;                                                 // TOP overflow value (Hz)
  OCR2B = 0;
}

void loop() {

 currentMillis = millis();                                      // latest value of millis() for all calculations
 
// update LED_STATE: determine if need to turn LED on or off      
                    
 if (LED_STATE == LOW) {                                        // if the Led is off, must wait for LEDINTERVAL to 
                                                                               //  expire to turn on LED 
   if (currentMillis - previousLedMillis >= LEDINTERVAL) {      // LEDINTERVAL is up, so change to HIGH
      LED_STATE = HIGH;                                         // change LED_STATE to HIGH
      previousLedMillis += LEDINTERVAL;                         // save the time when we made the change      
   }
 }
 else {                                                         // if LED is on, turn off if BLINKDURATION expired
   if (currentMillis - previousLedMillis >= BLINKDURATION) {    // BLINKDURATION expired, turn LED off
      LED_STATE = LOW;                                          // change LED_STATE to LOW
      previousLedMillis += BLINKDURATION;                       // save the time when we made the change
   }
 }

  digitalWrite(LEDPIN, LED_STATE);                              // switch LED on or off
  
// update temperaure from DHT22, convert and send to BLE device

if (currentMillis - previousTempMillis >= TEMPINTERVAL)  {       // TEMPINTERVAL exceeded; get temp 
                                                                                            // data, convert and send to BLE device
  T = dht.readTemperature(true);                                 // get temperature data from DHT in degree F
  char T_str[6];                                                           // prepare character array to send
  dtostrf(T,2,1,T_str);                                                  // format data into char array
  ble_device.write(T_str);                                            // send to BLExAR
  previousTempMillis += TEMPINTERVAL;                     // save the time temp data sent to BT device to allow for delay
}                                                                              // otherwise do nothing
  
  FAN_PID.Compute();                                             // determine duty cycle
   
// Turn the fans ON/OFF if duty cycle too low ~ 20% per Noctura 
// Arduino puts out small duty cycle even at zero
 
    if (round(DUTY_CYCLE) < MIN_DUTY_CYCLE)  {                    // calculated duty cycle < min duty cycle
      PWM11 = 0;                                                  // set DUTY_CYCLE to 0
      digitalWrite(RELAYPIN, LOW);                                // turn off power to BJT to fan relay
      }
    else  {
      PWM11 = (DUTY_CYCLE / 255) * 79;                            // output of PID 0- 255; Timer2 is 0 - 79
      digitalWrite(RELAYPIN, HIGH);                               // turn on power to BJT on fan relay
      }
}

How about a schematic (even a hand drawn one) so we can see how you have this all wired up?

; is not needed when declaring a function.

{} are needed to encase the function.

Where in your code are you calling this function from?

FAN_PID.Compute();                                             // determine duty cycle
   
// Turn the fans ON/OFF if duty cycle too low ~ 20% per Noctura
// Arduino puts out small duty cycle even at zero
 
    if (round(DUTY_CYCLE) < MIN_DUTY_CYCLE)  {                    // calculated duty cycle < min duty cycle
      PWM11 = 0;                                                  // set DUTY_CYCLE to 0
      digitalWrite(RELAYPIN, LOW);                                // turn off power to BJT to fan relay
      }
    else  {
      PWM11 = (DUTY_CYCLE / 255) * 79;                            // output of PID 0- 255; Timer2 is 0 - 79
      digitalWrite(RELAYPIN, HIGH);                               // turn on power to BJT on fan relay
      }

Should be like this.

FAN_PID.Compute()                                          // determine duty cycle
   {
// Turn the fans ON/OFF if duty cycle too low ~ 20% per Noctura
// Arduino puts out small duty cycle even at zero
 
    if (round(DUTY_CYCLE) < MIN_DUTY_CYCLE)  {                    // calculated duty cycle < min duty cycle
      PWM11 = 0;                                                  // set DUTY_CYCLE to 0
      digitalWrite(RELAYPIN, LOW);                                // turn off power to BJT to fan relay
      }
    else  {
      PWM11 = (DUTY_CYCLE / 255) * 79;                            // output of PID 0- 255; Timer2 is 0 - 79
      digitalWrite(RELAYPIN, HIGH);                               // turn on power to BJT on fan relay
      }
    }

Please see attached schematic.

All grounds are shared. 5V to Uno via USB wall charger. 5V to DHT22 from separate 5V power supply (on amp rack). 12V from wall charger/transformer.

Should have provided schematic up front; sorry about that.

Mike

Schematic.jpg|0x0

Ray_Richter:
; is not needed when declaring a function.

{} are needed to encase the function.

Where in your code are you calling this function from?

FAN_PID is called up top when all libraries are initialized. It wasn’t intended to be a function in of itself; PID is already a function from PID library. The if statement is looking at the output of FAN_PID which is DUTY_CYCLE.

What am I missing here?

Mike

FAN_PID.Compute();                                             // determine duty cycle

// Turn the fans ON/OFF if duty cycle too low ~ 20% per Noctura
// Arduino puts out small duty cycle even at zero

if (round(DUTY_CYCLE) < MIN_DUTY_CYCLE)  {                    // calculated duty cycle < min duty cycle
      PWM11 = 0;                                                  // set DUTY_CYCLE to 0
      digitalWrite(RELAYPIN, LOW);                                // turn off power to BJT to fan relay
      }
    else  {
      PWM11 = (DUTY_CYCLE / 255) * 79;                            // output of PID 0- 255; Timer2 is 0 - 79
      digitalWrite(RELAYPIN, HIGH);                              // turn on power to BJT on fan relay
      }





Should be like this.



FAN_PID.Compute()                                          // determine duty cycle
  {
// Turn the fans ON/OFF if duty cycle too low ~ 20% per Noctura
// Arduino puts out small duty cycle even at zero

if (round(DUTY_CYCLE) < MIN_DUTY_CYCLE)  {                    // calculated duty cycle < min duty cycle
      PWM11 = 0;                                                  // set DUTY_CYCLE to 0
      digitalWrite(RELAYPIN, LOW);                                // turn off power to BJT to fan relay
      }
    else  {
      PWM11 = (DUTY_CYCLE / 255) * 79;                            // output of PID 0- 255; Timer2 is 0 - 79
      digitalWrite(RELAYPIN, HIGH);                              // turn on power to BJT on fan relay
      }
    }

Tried to quote but didn't work out so well.

FAN_PID is initialized with all other libraries in setup. PID is already a defined function in PID library (just renamed it FAN_PID). The if statement is looking at the output of FAN_PID which is DUTY_CYCLE.

What am I missing here?

And I'm limited to posting every 5 minutes so might take a bit to work through....

Mike

This does not make any sense

#define PWM11        OCR2B         // pin number for 25kHz PWM to fan on Timer 2 (pins 3 and ll only)
//...
void setup() {

  pinMode(PWM11, OUTPUT);                     //set PWMPIN as output
//...

You are trying to set OCR2B to OUTPUT? Did you mean to use 3 or 11?

Previously I had the following as a standalone function:

void pwmDuty(DUTY_CYCLE) {
  OCR2B = (DUTY_CYCLE) / 255 * 79;    // PWM Width (duty cycle)
 }                                                        // PID output 0 - 255
                                                           // TImer2 0 - 79

Then had this bit of code in the main loop:

FAN_PID.Compute();                                                    // get the duty cycle

[code](round(DUTY_CYCLE) < MIN_DUTY_CYCLE)  {     // calculated duty cycle < min duty cycle
      DUTY_CYCLE = 0;                                                  // set DUTY_CYCLE to 0
      digitalWrite(RELAYPIN, LOW);                                // turn off power to BJT to fan relay
      }
    else  {
      digitalWrite(RELAYPIN, HIGH);                               // turn on power to BJT on fan relay
      }

pwmDuty();                                                               // set PWM output based on duty cycle

}

Didn’t work either. I know the sketch needs to modify the square wave on Pin 11 using the output of the PID calculation. I feel like I’m missing something obvious…

Mike

Found the problem. PWM isn’t the issue. Doesn’t appear I’m getting any output from the PID function. If I manually input a value and use analogWrite to Pin 3 the fan responds accordingly.

I’m uploading the complete sketch. If someone could help out, would be much appreciated.

Mike

// Sketch uses DHT22 temp sensor with BlEXAR BT to display temperature. Also uses PID to control Noctura 4 wire fan.

#include <DHT.h>
#include <SoftwareSerial.h>
#include <PID_v1.h>


// -----PINS 
//PIN 0 - RX
//PIN 1 - TX
const int RELAYPIN = 8;            // pin number for fan control relay 
const int LEDPIN = 9;              // pin number for the running LED (on side of the amp rack) CHANGE PIN NUMBER
const int DHTPIN = 2;              // pin number for temp sensor
const int PWMPIN = 3;              // pin number for 25kHz PWM (Timer 2 OC2B output)

// -----CONSTANTS
const int BLINKDURATION = 500;    // number of millisecs the running LED is on 
const int LEDINTERVAL = 5000;     // number of millisecs between blinks
const int TEMPINTERVAL = 2000;    // number of millisecs between temp data from DHT22
#define DHTTYPE DHT22             // DHT 22  (AM2302), AM2321
const int KP = 3;               //PID constants - evaluate adding aggressive numbers per example
const int KI = .5;
const int KD = .5;
const int MIN_DUTY_CYCLE = 15;    // minimum duty cycle for fan (is about 20% or 450 rpm) 15/79 for OCR2B @ 25kHz
      

//----- VARIABLES 

byte LED_STATE = LOW;                    // record whether the LED is on or off
double TEMP;                            // current temp from DH22
double CURRENT_TEMP;                    // copy of temp from DH22
double TEMP_SETPOINT = 78;               // temperature setpoint 88F
double DUTY_CYCLE;                   // PID output to control duty cycle of fan; set to zero at setup
unsigned long currentMillis = 0;         // stores the value of millis() in each iteration of loop()
unsigned long previousLedMillis = 0;     // will store last time the LED was updated
unsigned long previousTempMillis = 0;    // will store last time temp data sent to Arduino

//Initialize libraries
DHT dht(DHTPIN, DHTTYPE); 
SoftwareSerial ble_device(0,1);           // HM-10 TX/RX pins
PID FAN_PID(&CURRENT_TEMP, &DUTY_CYCLE, &TEMP_SETPOINT, KP, KI, KD, REVERSE); //INPUT, OUTPUT, TARGET TEMP, CONS1, CONS2, CONS3, MODE

//========

void setup() {
                                          
    pinMode(PWMPIN, OUTPUT);                     //set PWMPIN as output

    pwm25kHzBegin();                            //Set timer2 for 25kHz PWM signal with 0% duty cycle on startup of board 

    pinMode(LEDPIN, OUTPUT);                    // set the Led pin as output:

    pinMode(RELAYPIN, OUTPUT);                  //set the fan relay pin as output
    
    pinMode(DHTPIN, INPUT);                     //set the DHT pin as input

    dht.begin();                                //start DHT sensor
 
    ble_device.begin(9600);                     //prepare BLE module      

    FAN_PID.SetMode(AUTOMATIC);                 //set PID calc to automatic
        
}

//=======

void pwm25kHzBegin() {
  TCCR2A = 0;                                                 // TC2 Control Register A
  TCCR2B = 0;                                                 // TC2 Control Register B
  TIMSK2 = 0;                                                 // TC2 Interrupt Mask Register
  TIFR2 = 0;                                                  // TC2 Interrupt Flag Register
  TCCR2A |= (1 << COM2B1) | (1 << WGM21) | (1 << WGM20);      // OC2B cleared/set on match when up/down counting, fast PWM
  TCCR2B |= (1 << WGM22) | (1 << CS21);                       // prescaler 8
  OCR2A = 79;                                                 // TOP overflow value (Hz)
  OCR2B = 0;
}

void loop() {

 currentMillis = millis();                                      // latest value of millis() for all calculations
 
// update LED_STATE: determine if need to turn LED on or off      
                    
 if (LED_STATE == LOW) {                                        // if the Led is off, must wait for LEDINTERVAL to expire to turn LED 
   if (currentMillis - previousLedMillis >= LEDINTERVAL) {      // LEDINTERVAL is up, so change the state to HIGH
      LED_STATE = HIGH;                                         // change LED_STATE to HIGH
      previousLedMillis += LEDINTERVAL;                         // save the time when we made the change      
   }
 }
 else {                                                         // if LED is on, turn off if BLINKDURATION expired
   if (currentMillis - previousLedMillis >= BLINKDURATION) {    // BLINKDURATION expired, turn LED off
      LED_STATE = LOW;                                          // change LED_STATE to LOW
      previousLedMillis += BLINKDURATION;                       // save the time when we made the change
   }
 }

  digitalWrite(LEDPIN, LED_STATE);                              // switch LED on or off
  
// update temperaure from DHT22, convert and send to BLE device

if (currentMillis - previousTempMillis >= TEMPINTERVAL)  {       // TEMPINTERVAL exceeded; get temp data, convert and send to BLE device
      //float f = dht.readHumidity();                            // uncomment to send humidity
  TEMP = dht.readTemperature(true);                                 // get temperature data from DHT in degree F
  CURRENT_TEMP = TEMP;
  char TEMP_str[6];                                                 // prepare character array to send
  dtostrf(TEMP,2,1,TEMP_str);                                          // format data into char array
  ble_device.write(TEMP_str);                                       // send to BLExAR
  previousTempMillis += TEMPINTERVAL;                            // save the time temp data sent to BT device to allow for delay
}                                                                // otherwise do nothing
  
   FAN_PID.Compute();                                             // determine duty cycle
   
// Turn the fans ON/OFF if duty cycle too low ~ 20% per Noctura 
// Arduino puts out small duty cycle even at zero
   
    if (round(DUTY_CYCLE) < MIN_DUTY_CYCLE)  {                    // calculated duty cycle < min duty cycle
      DUTY_CYCLE = 0;                                                  // set DUTY_CYCLE to 0
      digitalWrite(RELAYPIN, LOW);                                // turn off power to BJT to fan relay
      }
    else  {
      digitalWrite(RELAYPIN, HIGH);                               // turn on power to BJT on fan relay
      }
      
  analogWrite(PWMPIN,DUTY_CYCLE);                                 // set the duty cycle on Pin 3
   
}

Article on Low-Pass Filter a PWM Signal into an Analog Voltage:

https://www.allaboutcircuits.com/technical-articles/low-pass-filter-a-pwm-signal-into-an-analog-voltage/

nuc1: const int KP = 3;              //PID constants - evaluate adding aggressive numbers per example const int KI = .5; const int KD = .5; const int MIN_DUTY_CYCLE = 15;    // minimum duty cycle for fan (is about 20% or 450 rpm) 15/79 for

Those PID variables are defined as int yet you are trying to put a floating point number into them. Since the value is less than 1, they get the value 0 so your PID won't work.

blh64: Those PID variables are defined as int yet you are trying to put a floating point number into them. Since the value is less than 1, they get the value 0 so your PID won't work.

I can't thank you enough! Such a simple mistake on my part. It works perfectly now.

Just playing around with the PID constants now - pretty interesting to watch how it works.

Mike