PID controlled PWM fan humidity circuit

Hi all, I am working on a PID controlled humidity system. The aim is to take the humidity input from a DHT11 sensor and reduce the humidity using a PWM enabled Fan.

I would like the system to be controlled by a PID controller to ensure fast response when high humidity is detected.

The board I'm using is a mega 2560. The fan is a 12V fan that will connected to a battery with the PWM input coming from pin 5. ( I believe the entire battery and 2560 need to share a ground.)

My code is below, but I can't seem to get it to work correctly. The fan will spin at maximum speed when the PWM wire is 0V.

I am still learning so much of the code is cobbled together from other projects I have read through.

#include <PID_v1.h>
#include <dht.h>
#include <LiquidCrystal.h>

#define DHTPIN 4 // Pin connected to DHT sensor
#define DHTTYPE DHT11 // Defines DHT type so board can read output
#define FAN_PIN 5 // Pin connected to PWM pin on the fan
#define PIN_SENSE 2 // Pin where we connected the fan sense pin. Must be an interrupt capable pin (2 or 3 on Arduino Uno)
#define DEBOUNCE 0 // 0 is fine for most fans, some fans may require 10 or 20 to filter out noise
#define FANSTUCK_THRESHOLD 500 // If no interrupts were received for 500ms, consider the fan as stopped and report 0 RPM

dht DHT;
const int RS = 7, EN = 8, D4 = 9, D5 = 10, D6 = 11, D7 = 12; // Assign pins to LCD pads

LiquidCrystal lcd(RS, EN, D4, D5, D6, D7); // Set pins that are connected to LCD, 4-bit mode

// PID Constants
double Kp = 1.0;        // Proportional constant
double Ki = 0.5;        // Integral constant
double Kd = 0.2;        // Derivative constant

// Setpoint (desired humidity)
double setpoint = 65;   // Target humidity

// Variables for PID control
double input, output;
double humidity, lastHumidity;
double dt = 1.2;        // Time interval for PID calculation

// PWM Duty Cycle Settings
const int MIN_DUTY_CYCLE = 51;  // Minimum duty cycle (20% of 255)
const int MAX_DUTY_CYCLE = 255; // Maximum duty cycle (100% of 255)

// Create PID controller object
PID pid(&humidity, &output, &setpoint, Kp, Ki, Kd, DIRECT);

// Interrupt handler. Stores the timestamps of the last 2 interrupts and handles debouncing
volatile unsigned long ts1 = 0, ts2 = 0;

// Calculates the RPM based on the timestamps of the last 2 interrupts. Can be called at any time.
unsigned long calcRPM() {
    if (millis() - ts2 < FANSTUCK_THRESHOLD && ts2 != 0) {
        return (60000 / (ts2 - ts1)) / 2;
    }
    else {
        return 0;
    }
}

void tachISR() {
    unsigned long m = millis();
    if ((m - ts2) > DEBOUNCE) {
        ts1 = ts2;
        ts2 = m;
    }
}

void setup() {
    Serial.begin(9600);
    pinMode(FAN_PIN, OUTPUT);
    delay(1000);
    lcd.begin(16, 2);   // Set 16 columns and 2 rows of 16x2 LCD display

    // Initialize humidity and PID variables
    input = DHT.humidity;  // Initial humidity reading
    lastHumidity = input;
    humidity = input;

    // Set PID parameters
    pid.SetMode(AUTOMATIC);
    pid.SetSampleTime(dt * 1000); // Convert dt to milliseconds for PID sample time

    pinMode(PIN_SENSE, INPUT_PULLUP); // Set the sense pin as input with pullup resistor
    attachInterrupt(digitalPinToInterrupt(PIN_SENSE), tachISR, FALLING); // Set tachISR to be triggered when the signal on the sense pin goes low

    setupTimer1(); // Initialize Timer 1 for PWM
}

void loop() {
    delay(100);
    Serial.print("RPM: ");
    Serial.println(calcRPM());

    // Read humidity from DHT11 sensor
    DHT.read11(DHTPIN);
    input = DHT.humidity;

    // Calculate PID output
    pid.Compute();

    // Map the PID output to the duty cycle range
    int pwmDutyCycle = map(output, -100, 100, MIN_DUTY_CYCLE, MAX_DUTY_CYCLE);

    // Apply PWM to the output pin
    analogWrite(FAN_PIN, pwmDutyCycle);

    // Print humidity and output values
    Serial.print("Humidity: ");
    Serial.print(input);
    Serial.print("%, Output PWM: ");
    Serial.print(pwmDutyCycle * 100 / MAX_DUTY_CYCLE);
    Serial.println("%");

    // Display temperature, humidity, RPM, and PWM on LCD display
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Temp: ");
    lcd.print(DHT.temperature);
    lcd.print("C");
    lcd.setCursor(0, 1);
    lcd.print("Humidity: ");
    lcd.print(DHT.humidity);
    lcd.print("%");

    delay(2000);  // Wait for 2 seconds before displaying RPM and PWM

    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("RPM: ");
    lcd.print(calcRPM());
    lcd.setCursor(0, 1);
    lcd.print("PWM: ");
    lcd.print(pwmDutyCycle * 100 / MAX_DUTY_CYCLE);
    lcd.print("%");

    delay(1000);  // Wait for 1 second before switching back to temperature and humidity
}

void setupTimer1() {
    // Set PWM frequency to about 25kHz on pin 5 (Timer 1, phase correct PWM, prescale 1)
    TCCR1A = 0;
    TCCR1B = 0;
    TCCR1A = (1 << WGM11);
    TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS10);
    ICR1 = 400;
}

If you disconnect the fan PWM wire from pin 5 and connect it to GND, does the fan still run?

If so then write a test program that takes the duty cycle from the PC or an analog joystick. Find out how the fan reacts on varying duty cycle. Full speed at 0V input may be a reaction on a broken or missing control signal.

Hi Jim,

Yes if the PWM pin is connected to ground the fun runs at full speed.

It would appear that is the case.

I now have a different issue in that I am struggling to get the PWM pin to rise above 20%. and when the humidity is at 95% (target is set to 60%) the PWM drops to 0. My updated code is below

// Libraries to include
#include <PID_v1.h>
#include <dht.h>
#include <LiquidCrystal.h>

// Defines pins
#define DHTPIN 4                // Pin connected to DHT sensor
#define DHTTYPE DHT11           // Defines DHT type so board can read output
#define FAN_PIN 5               // Pin connected to PWM pin on the fan
#define PIN_SENSE 2             // Pin where we connected the fan sense pin. Must be an interrupt capable pin (2 or 3 on Arduino Uno)
#define DEBOUNCE 0              // 0 is fine for most fans, some fans may require 10 or 20 to filter out noise
#define FANSTUCK_THRESHOLD 500  // If no interrupts were received for 500ms, consider the fan as stopped and report 0 RPM

dht DHT;
const int RS = 7, EN = 8, D4 = 9, D5 = 10, D6 = 11, D7 = 12;  // Assign pins to LCD pads
LiquidCrystal lcd(RS, EN, D4, D5, D6, D7);                    // Set pins that are connected to LCD, 4-bit mode

// RPM Sensing
// Interrupt handler. Stores the timestamps of the last 2 interrupts and handles debouncing
volatile unsigned long ts1 = 0, ts2 = 0;

// Define Variables we'll be connecting to
double TargetHumidity = 65;   // Target humidity
double Input, Output;

// Define the aggressive and conservative Tuning Parameters
double aggKp = 4, aggKi = 0.2, aggKd = 1;
double consKp = 1, consKi = 0.05, consKd = 0.25;

// Specify the links and initial tuning parameters
PID myPID(&Input, &Output, &TargetHumidity, consKp, consKi, consKd, DIRECT);

void setup() {
  Serial.begin(9600);
  lcd.begin(16, 2);  // Set 16 columns and 2 rows of 16x2 LCD display
  pinMode(FAN_PIN, OUTPUT);
  pinMode(DHTPIN, INPUT);
  pinMode(PIN_SENSE, INPUT_PULLUP);                                 // Set the sense pin as Input with pullup resistor
  attachInterrupt(digitalPinToInterrupt(PIN_SENSE), tachISR, FALLING);  // Set tachISR to be triggered when the signal on the sense pin goes low

  // Initialize PID controller
  Input = DHT.humidity;
  TargetHumidity = TargetHumidity;
  myPID.SetMode(AUTOMATIC);
  myPID.SetOutputLimits(0, 255);  // Adjust the Output limits according to the desired PWM range (0-100%)
}

void loop() {
  // Read temperature and humidity from DHT sensor
  DHT.read11(DHTPIN);
  float temperature = DHT.temperature;
  float humidity = DHT.humidity;

  // Update the PID TargetHumidity
  TargetHumidity = TargetHumidity;

  // Compute PID Output
  Input = DHT.humidity;
  
  double gap = abs(TargetHumidity - Input); //distance away from TargetHumidity
  if (gap < 10) {
    // We're close to TargetHumidity, use conservative tuning parameters
    myPID.SetTunings(consKp, consKi, consKd);
  }
  else {
    // We're far from TargetHumidity, use aggressive tuning parameters
    myPID.SetTunings(aggKp, aggKi, aggKd);
  }

  myPID.Compute();

  // Map PID Output to the PWM range
  int dutyCycle = map(Output, 0, 255, 0, 100);  // Map Output range (0-255) to duty cycle range (0-100%)

  // Update PWM duty cycle based on mapped Output
  analogWrite(FAN_PIN, dutyCycle);  // Scale duty cycle to match the 0-255 PWM range

  // Print humidity value to serial monitor
  Serial.print("Humidity: ");
  Serial.print(humidity);
  Serial.println("%");

  // Display temperature, humidity, RPM, and PWM on LCD display
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Temp: ");
  lcd.print(temperature);
  lcd.print("C");
  lcd.setCursor(0, 1);
  lcd.print("Humidity: ");
  lcd.print(humidity);
  lcd.print("%");

  delay(2000);  // Wait for 2 seconds before displaying RPM and PWM

  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("RPM: ");
  unsigned long rpm = calcRPM();
  lcd.print(rpm);

  lcd.setCursor(0, 1);
  lcd.print("PWM: ");
  lcd.print(dutyCycle);

  // Print RPM and PWM values to serial monitor
  Serial.print("RPM: ");
  Serial.println(rpm);
  Serial.print("PWM: ");
  Serial.println(dutyCycle);

  delay(1000);  // Wait for 1 second before switching back to temperature and humidity
}

// Calculates the RPM based on the timestamps of the last 2 interrupts. Can be called at any time.
unsigned long calcRPM() {
  if (millis() - ts2 < FANSTUCK_THRESHOLD && ts2 != 0) {
    return (60000 / (ts2 - ts1)) / 2;
  } else {
    return 0;
  }
}

void tachISR() {
  unsigned long m = millis();
  if ((m - ts2) > DEBOUNCE) {
    ts1 = ts2;
    ts2 = m;
  }
}

Then it is not your code it is a hardware problem.
Do you have a datasheet for the fan or a link

https://noctua.at/en/nf-p12-redux-1700-pwm/specification

According to the specifications, 0V is 0 RPM and constant 5V is max RPM
Do you the fan ground wire connected to the mega GND?

The fan ground wire is connected to both the Arduino ground and the battery ground

Black: Connected to Mega GND and battery negative (-)
Yellow: Connected to battery Positive +12V
Blue: Connected to PWM
Green: No Connection

fan

That fan will not work with the Mega.
That fan requires a PWM frequency of 25kHz.
Mega PWM max freq is 980.

Yes that's correct.

I presume that because it is a case fan if there is 0v at the PWM the fan activates at full power to ensure the computer stays cool in case of a fault with the PWM signal.

See post # 11

As far as I am aware this can be updated by changing the timer3 prescaler.

Possible but I think your fan is broken.
If the PWM wire is left unconnected or connected to 5V it should run at full speed.
If connected to GND it should be stopped.

I have just tried it again with the code posted above and I can get it to turn at lower speeds using the PWM however the PWM output is only ever hitting 20-30%. I am assuming the code is restricting it in some way, the PID is also acting strangely in the fact the the with a large the gap between the measured and target humidity the pwm pin stops outputting.

Comment out the line:
int pwmDutyCycle = map(output, -100, 100, MIN_DUTY_CYCLE, MAX_DUTY_CYCLE);

and just set the duty to a fixed value

int pwmDutyCycle = 128;

see if it works

This seems to work as I am now getting 4.4v from the PWM pin.

AC or DC?

I only looked for DC voltage, not AC