What's new

Open Source PWM Fan Controller

The manually creating PWM via one of the digital pins seems interesting. The button pushes are doing a digitalRead() on the button state, and then initiating a counter, so count 0 is one screen, count 1, is the second screen, and cound 2 is the third screen. I'm not sure if that would cause conflicts?
It would not cause an issue with the screen counter. It may change how you detect button release.

There are 2 ways of doing manually timed. For both you need the time high and time low. The first approach is:

Code:
initialization stuff

loop:
    sensor reading, math, actions
    
    calculate time high and time low
    switch high
    delay time high
    switch low
    delay time low

The second approach, setup for 100hz is:
Code:
Initialization stuff

counter = 0
state = 0

loop
    sensor reading, math, actions
    

    calculate time high calculate time low in ms
    time = get time function
    time offset = 10 * round down (time / 10)
    
    if current run time-time offset  <= time high
        if state = 0
            set DO to high
            state = 1
    else
        if state = 1
            set DO to low
            state = 0

The inner ifs that check for the current state are not necessary. They can also be part of the outer if statement using and for state = 0, and else if for state = 1. There are ways of doing it without needing the counter

The second approach is probably the better one, since it will be more accurate and won't cause the code to run at 100hz.
 
Ok very cool stuff there. So in the first example, by using the delays, you're actually slowing the entire code actually runs down to 100 hz? I could see why that's less desireable, because it's slowing everything including the other functions down to that speed as well?

The second one doesn't actually slow down the entire code, but repeatedly checks the high/low conditions, against the internal timers? If I'm understanding that correctly, I definitely see the appeal of the second version, and would prefer that option. I appreciate the hell out of your input, and articulating the logic. If I can pester you a bit more, could you possibly articulate the logic of the second approach in a little more detail? I know you were just wording it from a theory standpoint and not code -

"calculate time high calculate time low in ms" - I get that you're determining duty cycle here, and they will be variables defined by the other conditions like temperature, so I think I can piece that part together

"time = get time function" - is this checking time against the internal timers? Is it defining the 10ms interval?

"time offset = 10 * round down (time / 10)" - What's being accomplished here? Haven't quite pieced this part together yet

"
if current run time-time offset <= time high
if state = 0
set DO to high
state = 1
else
if state = 1
set DO to low
state = 0
"

This part seems pretty intuitive, just checking against the internal timers and setting the pin high or low depending on the conditions and duty cycle.

Is that somewhat along the right lines? I'll post up the code I have here soon, even though it doesn't currently have any sort of output language for whatever PWM pin I chose
 
Ok very cool stuff there. So in the first example, by using the delays, you're actually slowing the entire code actually runs down to 100 hz? I could see why that's less desireable, because it's slowing everything including the other functions down to that speed as well?
Yes.
The second one doesn't actually slow down the entire code, but repeatedly checks the high/low conditions, against the internal timers? If I'm understanding that correctly, I definitely see the appeal of the second version, and would prefer that option. I appreciate the hell out of your input, and articulating the logic. If I can pester you a bit more, could you possibly articulate the logic of the second approach in a little more detail? I know you were just wording it from a theory standpoint and not code -

"calculate time high calculate time low in ms" - I get that you're determining duty cycle here, and they will be variables defined by the other conditions like temperature, so I think I can piece that part together
Converting duty cycle to times.
"time = get time function" - is this checking time against the internal timers? Is it defining the 10ms interval?
Pretty much. Aurdiuos have a function that gets time running.
"time offset = 10 * round down (time / 10)" - What's being accomplished here? Haven't quite pieced this part together yet
In this case round to the nearest 10. It is part of getting the time in the range for the frequency. I changed it in the code below to properly using the remainder.
"
if current run time-time offset <= time high
if state = 0
set DO to high
state = 1
else
if state = 1
set DO to low
state = 0
"

This part seems pretty intuitive, just checking against the internal timers and setting the pin high or low depending on the conditions and duty cycle.

Is that somewhat along the right lines? I'll post up the code I have here soon, even though it doesn't currently have any sort of output language for whatever PWM pin I chose
Yep. Along the right lines.

I've been messing with a pwm generator today. Code below. It is tested. I did my best to comment the parts that may seem odd.

C-like:
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels

// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

int dutyCycle = 0;
int previousDutyCycle = 1;
unsigned long previousTime = 0;
unsigned long currentMil = 0;
bool valuesChanged = true;

unsigned long currentTime = 0;
float timePercent = 0;
bool state = false;

const int freq = 100; // PWM frequency

void setup() {
  Serial.begin(9600);           //  setup serial
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64
    Serial.println(F("SSD1306 allocation failed"));
    for(;;);
  }
  display.setTextColor(WHITE);
}

void loop() {
  dutyCycle = analogRead(A0) *0.1745; // Calculate duty cycle, in this case its based on a voltage input
  if (dutyCycle > 100) dutyCycle = 100; // Upper limit of duty cycle
  else if (dutyCycle < 0) dutyCycle = 0;  // Lower limit of duty cycle

  // check if the screen needs to update by checking if displayed value is diffrent than current value
  valuesChanged = (dutyCycle != previousDutyCycle);
  previousDutyCycle = dutyCycle;

  // Display Section

  currentMil = millis();
  if ((currentMil/200 != previousTime) && valuesChanged){ // the code pauses for the time it takes to write the screen, so only update if needed and at 5 fps
    display.clearDisplay();
    display.setTextSize(2);
    display.setCursor(0,0);
    display.println(dutyCycle);
    display.display();
  }
  previousTime = currentMil/200; // For getting 5 fps, divide milliseconds by 200

  // PWM Generator

  currentTime = micros() % (1000000/freq); // Get remainder remainder of current runtime divided by period, 1000000 is microseconds per second
  timePercent = 100.0*currentTime / (1000000.0/freq);
 
  if (dutyCycle == 100){
    if (!state){
      digitalWrite(2,HIGH);
      state = true;
    }
  }
  else if (dutyCycle == 0){
    if (state){
      digitalWrite(2,LOW);
      state = false;
    }
  }
  else{
    if ((timePercent < dutyCycle) && !state){
      // High
      digitalWrite(2,HIGH);
      state = true;
    }
    else if ((timePercent > dutyCycle) && state){
      // Low
      digitalWrite(2,LOW);
      state = false;
    }
  }
}
 
Alright I've gotta thank ya for sticking with me through my own learning here, and appreciate the extra confirmation that I'm properly picking up what you're putting down. And the icing of the cake of that code for creating a PWM generator from scratch. The logic of it all is making more and more sense as I go along. I do have one question, what's that *0.1745 multiplier on the analogRead of pin A0?


I hate to admit it, but I did indeed discover I was overcomplicating something that people already had a solution for. Live and learn :laughing:

I actually also found out there are some PWM libraries that look like they may help accomplish this as well, it would be excellent if there was a solution this easy!

PWM frequency library

PWM.h

The "PWM frequency library" seems to be the OG library that people used for creating software PWM on these things, then someone came along and gave it some small improvements to create the "PWM.h" library. The links above have details on the different functions they can perform, but even at the most basic level they seem well suited for my needs. The use of them is way easier than I even could have asked, per this video:

 
So with that said, here is my (junk) code as of now. I have not yet tested the PWM output so I'm still assuming it to be very broken, but at least before this mornings modifications this was functional as far as calculating temperatures, potentiometer values, and the screen displays.

NOT THE FINAL CODE, DON'T ATTEMPT USE THIS VERSION BESIDES DEBUGGING
EDIT - Bummer, I was hoping to get the colors to carry over as you did, this was the "copy for forum" function in the Arduino, but the "copy for HTML" has too many characters

Code:
// Include required libraries
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <PWM.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels

// Initialize OLED display object
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// Define constants for pins and values
const int FAN_PIN = 9;               //Fan output pin
const int TEMP_SENSOR_PIN = A1;      //Temp sensor pin
const int POT_PIN = A2;              //Potentiometer pin
const int buttonPin = 11;            //Momentary switch pin
const int MIN_TEMP = 160;            //Minimum duty cycle temperature
const int MAX_TEMP = 210;            //Maximum duty cycle temperature
const int MIN_DUTY = 89;             //Minimum duty cycle percentage
const int MAX_DUTY = 9;             //Maximum duty cycle percentage
const int DUTY_INCREMENT = 1;        //Duty cycle percentage increments
const int POT_RANGE = 10000;         //Potentiometer resistance

// Define variables for current values
int current_duty = 0;                //Current duty cycle
int current_screen = 1;              //Current screen (may be deleted)

int current_pot_value = 0;           //Current raw potentiometer value
int mapped_pot = 0;                  //Potentiometer values mapped to range of pMin to pMax
int pMin = -20;                      //Minimum temperature variation for minimum potentiometer value
int pMax = 20;                       //Maximum temperature variation for maximum potentiometer value

int current_temp_sensor = 0;         //Current raw temperature sensor output
int mapped_temp = 0;                 //Temperature sensor values mapped to Fahrenheit range
int tuned_MIN_TEMP = 0;              //Minimum temp range after modification by potentiometer value
int tuned_MAX_TEMP = 0;              //Maximum temp range after modification by potentiometer value

int buttonPushCounter = 0;           //Counter for the number of button presses
boolean buttonState = LOW;           //Current state of the button
boolean lastButtonState = LOW;       //Previous state of the button

// Set PWM Frequency
int32_t frequency = 100;             //Frequency (in Hz)

// Define function for updating the duty cycle based on temperature and potentiometer
void update_duty() {
  if (mapped_temp < MIN_TEMP) {
    current_duty = 0;
  } else if (mapped_temp > MAX_TEMP) {
    current_duty = MAX_DUTY;
  } else {
    int pot_adjust = (current_pot_value) / (POT_RANGE / 60);
    current_duty = MIN_DUTY + (MAX_DUTY - MIN_DUTY) * (mapped_temp - tuned_MIN_TEMP + pot_adjust) / (tuned_MAX_TEMP - tuned_MIN_TEMP);
    current_duty = round(current_duty / DUTY_INCREMENT) * DUTY_INCREMENT;
  }
  analogWrite(FAN_PIN, round(current_duty * 255.0 / 100.0));
}

void setup() {
  // Initialize serial communication
  Serial.begin(9600);

  // Set up momentary switch as input
  pinMode(buttonPin, INPUT_PULLUP);

  // Initialize OLED display
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(WHITE);

  //Prepare Timers for PWM frequency on FAN_PIN, activate onboard LED (pin 13) if successful
  InitTimersSafe();
  bool success = SetPinFrequencySafe(FAN_PIN, frequency);
  if(success) {
    pinMode(13, OUTPUT);
    digitalWrite(13, HIGH);
  }

  // *DELETE SOON* Set up fan output pin and set initial duty cycle
//  pinMode(FAN_PIN, OUTPUT);
//  update_duty();
}

void loop() {
  // Read current potentiometer value
  current_pot_value = analogRead(POT_PIN);

  //Map potentiometer data to a range from negative (-)30 to +30, a simple variable added or subtracted to the commanded temperature range
  mapped_pot = map(current_pot_value, 1023, 0, pMin, pMax);

  tuned_MIN_TEMP = (MIN_TEMP + mapped_pot);
  tuned_MAX_TEMP = (MAX_TEMP + mapped_pot);

  // Read current temperature sensor value
  current_temp_sensor = analogRead(TEMP_SENSOR_PIN); //Raw unmapped temp sensor data

  //Map voltages from temp sensor to Fahrenheit. First two numbers are ohms resistance, second two numbers are correlating temp in Fahrenheit
  mapped_temp = map(current_temp_sensor, 497, 78, 63, 212);

  //Output PWM to FAN_PIN based on calculated duty cycle
  pwmWrite(FAN_PIN, round(current_duty * 255.0 / 100.0));

  // Read the state of the pushbutton value:
  buttonState = digitalRead(buttonPin);

  Serial.println(current_temp_sensor);
  Serial.println(mapped_temp);
  switch (buttonPushCounter) // choose what to display based on buttonPushCounter value
  {
    case 0:

      display.setTextColor(WHITE);
      display.clearDisplay();
      display.setCursor(0, 10);
      display.print("RG:");
      display.print(tuned_MIN_TEMP) ;
      display.print("-") ;
      display.println(tuned_MAX_TEMP);
      display.display();
      break;


    case 1:

      display.setTextColor(WHITE);
      display.clearDisplay();
      display.setCursor(0, 10);
      // Display static text
      display.print("C.Temp:");
      display.println(mapped_temp);
      display.display();
      break;

    case 2:

      display.setTextColor(WHITE);
      display.clearDisplay();
      display.setCursor(0, 10);
      // Display static text
      display.print("C.Duty:");
      display.println(current_duty);
      display.display();
      break;

  }

  if (buttonState != lastButtonState)
  {
    if (buttonState == HIGH)
    {
      // if the current state is HIGH then the button
      // went from off to on:
      buttonPushCounter++;  // add one to counter
      display.clearDisplay();
      display.display();
      if (buttonPushCounter > 2)
      {
        buttonPushCounter = 0;
      }

    }
    // save the current state as the last state,
    //for next time through the loop
    lastButtonState = buttonState;
  }

}
 
I do have one question, what's that *0.1745 multiplier on the analogRead of pin A0?
I didn't feel like removing or changing the circuit I used for checking resistors and potentiometers, so I used a scaling factor to go from analog to percent in one step.
EDIT - Bummer, I was hoping to get the colors to carry over as you did, this was the "copy for forum" function in the Arduino, but the "copy for HTML" has too many characters
in the insert code option, change the code type. I used C-like.
 
Ahh interesting, thanks for the heads up. Here's a test, same exact code as above:

EDIT - Wow that's handy, looks great thanks for that!

And I see a flaw, Initially I had an analogWrite function for the FAN_PIN, but the new library gives me a pwmWrite function, I accidentally have both in the current code. I most likely won't have a chance to do any testing until later in the day, so this is kinda a reminder for myself

C-like:
// Include required libraries
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <PWM.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels

// Initialize OLED display object
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// Define constants for pins and values
const int FAN_PIN = 9;               //Fan output pin
const int TEMP_SENSOR_PIN = A1;      //Temp sensor pin
const int POT_PIN = A2;              //Potentiometer pin
const int buttonPin = 11;            //Momentary switch pin
const int MIN_TEMP = 160;            //Minimum duty cycle temperature
const int MAX_TEMP = 210;            //Maximum duty cycle temperature
const int MIN_DUTY = 89;             //Minimum duty cycle percentage
const int MAX_DUTY = 9;             //Maximum duty cycle percentage
const int DUTY_INCREMENT = 1;        //Duty cycle percentage increments
const int POT_RANGE = 10000;         //Potentiometer resistance

// Define variables for current values
int current_duty = 0;                //Current duty cycle
int current_screen = 1;              //Current screen (may be deleted)

int current_pot_value = 0;           //Current raw potentiometer value
int mapped_pot = 0;                  //Potentiometer values mapped to range of pMin to pMax
int pMin = -20;                      //Minimum temperature variation for minimum potentiometer value
int pMax = 20;                       //Maximum temperature variation for maximum potentiometer value

int current_temp_sensor = 0;         //Current raw temperature sensor output
int mapped_temp = 0;                 //Temperature sensor values mapped to Fahrenheit range
int tuned_MIN_TEMP = 0;              //Minimum temp range after modification by potentiometer value
int tuned_MAX_TEMP = 0;              //Maximum temp range after modification by potentiometer value

int buttonPushCounter = 0;           //Counter for the number of button presses
boolean buttonState = LOW;           //Current state of the button
boolean lastButtonState = LOW;       //Previous state of the button

// Set PWM Frequency
int32_t frequency = 100;             //Frequency (in Hz)

// Define function for updating the duty cycle based on temperature and potentiometer
void update_duty() {
  if (mapped_temp < MIN_TEMP) {
    current_duty = 0;
  } else if (mapped_temp > MAX_TEMP) {
    current_duty = MAX_DUTY;
  } else {
    int pot_adjust = (current_pot_value) / (POT_RANGE / 60);
    current_duty = MIN_DUTY + (MAX_DUTY - MIN_DUTY) * (mapped_temp - tuned_MIN_TEMP + pot_adjust) / (tuned_MAX_TEMP - tuned_MIN_TEMP);
    current_duty = round(current_duty / DUTY_INCREMENT) * DUTY_INCREMENT;
  }
  analogWrite(FAN_PIN, round(current_duty * 255.0 / 100.0));
}

void setup() {
  // Initialize serial communication
  Serial.begin(9600);

  // Set up momentary switch as input
  pinMode(buttonPin, INPUT_PULLUP);

  // Initialize OLED display
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(WHITE);

  //Prepare Timers for PWM frequency on FAN_PIN, activate onboard LED (pin 13) if successful
  InitTimersSafe();
  bool success = SetPinFrequencySafe(FAN_PIN, frequency);
  if(success) {
    pinMode(13, OUTPUT);
    digitalWrite(13, HIGH);
  }

  // *DELETE SOON* Set up fan output pin and set initial duty cycle
//  pinMode(FAN_PIN, OUTPUT);
//  update_duty();
}

void loop() {
  // Read current potentiometer value
  current_pot_value = analogRead(POT_PIN);

  //Map potentiometer data to a range from negative (-)30 to +30, a simple variable added or subtracted to the commanded temperature range
  mapped_pot = map(current_pot_value, 1023, 0, pMin, pMax);

  tuned_MIN_TEMP = (MIN_TEMP + mapped_pot);
  tuned_MAX_TEMP = (MAX_TEMP + mapped_pot);

  // Read current temperature sensor value
  current_temp_sensor = analogRead(TEMP_SENSOR_PIN); //Raw unmapped temp sensor data

  //Map voltages from temp sensor to Fahrenheit. First two numbers are ohms resistance, second two numbers are correlating temp in Fahrenheit
  mapped_temp = map(current_temp_sensor, 497, 78, 63, 212);

  //Output PWM to FAN_PIN based on calculated duty cycle
  pwmWrite(FAN_PIN, round(current_duty * 255.0 / 100.0));

  // Read the state of the pushbutton value:
  buttonState = digitalRead(buttonPin);

  Serial.println(current_temp_sensor);
  Serial.println(mapped_temp);
  switch (buttonPushCounter) // choose what to display based on buttonPushCounter value
  {
    case 0:

      display.setTextColor(WHITE);
      display.clearDisplay();
      display.setCursor(0, 10);
      display.print("RG:");
      display.print(tuned_MIN_TEMP) ;
      display.print("-") ;
      display.println(tuned_MAX_TEMP);
      display.display();
      break;


    case 1:

      display.setTextColor(WHITE);
      display.clearDisplay();
      display.setCursor(0, 10);
      // Display static text
      display.print("C.Temp:");
      display.println(mapped_temp);
      display.display();
      break;

    case 2:

      display.setTextColor(WHITE);
      display.clearDisplay();
      display.setCursor(0, 10);
      // Display static text
      display.print("C.Duty:");
      display.println(current_duty);
      display.display();
      break;

  }

  if (buttonState != lastButtonState)
  {
    if (buttonState == HIGH)
    {
      // if the current state is HIGH then the button
      // went from off to on:
      buttonPushCounter++;  // add one to counter
      display.clearDisplay();
      display.display();
      if (buttonPushCounter > 2)
      {
        buttonPushCounter = 0;
      }

    }
    // save the current state as the last state,
    //for next time through the loop
    lastButtonState = buttonState;
  }

}
 
Last edited:
Tried to verify/upload the code and got an error with PWM.h. Were you able to get it to verify/upload?

Tracked it to using a Nano Every. Guess I'll have an amazon package waiting for me in a few days.


edit: nano -> nano every
 
Last edited:
Tried to verify/upload the code and got an error with PWM.h. Were you able to get it to verify/upload?

Tracked it to using a Nano Every. Guess I'll have an amazon package waiting for me in a few days.


edit: nano -> nano every

Oh interesting, yeah I haven't uploaded it yet but did verify/compile successfully beforehand. If you end up with the same Nanos that I linked earlier in the thread, I will say I did have to select the old bootloader for uploading, but that has worked fine
 
Oh interesting, yeah I haven't uploaded it yet but did verify/compile successfully beforehand. If you end up with the same Nanos that I linked earlier in the thread, I will say I did have to select the old bootloader for uploading, but that has worked fine
I had ordered the newer generation of the nano, the nano every. It uses a different chipset. Was about to hit order on some of the nano clones, and realized I should check the chipset of the unos I have in storage. It's the right chipset, just a little bit annoying to use since it doesn't plug into a breadboard.

As for testing the code you posted, duty cycle is not updating. I copied the contents of the update_duty excluding the analogWrite to right above pwmWrite. Duty cycle was updating, and PWM was outputting the duty cycle at 100 hz.

There does seem to be an issue with duty cycle mapping. I'll try to post a video in a moment.
 
I had ordered the newer generation of the nano, the nano every. It uses a different chipset. Was about to hit order on some of the nano clones, and realized I should check the chipset of the unos I have in storage. It's the right chipset, just a little bit annoying to use since it doesn't plug into a breadboard.

As for testing the code you posted, duty cycle is not updating. I copied the contents of the update_duty excluding the analogWrite to right above pwmWrite. Duty cycle was updating, and PWM was outputting the duty cycle at 100 hz.

There does seem to be an issue with duty cycle mapping. I'll try to post a video in a moment.

Ahh I see what you were saying now, I wasn't familiar with the Nano Every but I get it.

Interesting notes on relocating the contents of update_duty making it functional as well! I did just notice that the "else" code introduces a "pot_adjust" integer using "current_pot_value", I believe that's remnant code that shouldn't be necessary, I don't believe there's a need for any raw potentiometer data in those if/else statements.

Very interesting observations on what the duty cycle is doing as you move past the upper limit of the range, hmm. Hard limits for duty cycle should be doable I think, I'll need to do a little tinkering later to see what I can do there.
 
Ahh I see what you were saying now, I wasn't familiar with the Nano Every but I get it.

Interesting notes on relocating the contents of update_duty making it functional as well! I did just notice that the "else" code introduces a "pot_adjust" integer using "current_pot_value", I believe that's remnant code that shouldn't be necessary, I don't believe there's a need for any raw potentiometer data in those if/else statements.

Very interesting observations on what the duty cycle is doing as you move past the upper limit of the range, hmm. Hard limits for duty cycle should be doable I think, I'll need to do a little tinkering later to see what I can do there.
I messed with it a little bit and end up with this before the pwmWrite:

C-like:
current_duty = MIN_DUTY + (MAX_DUTY - MIN_DUTY) * (mapped_temp - tuned_MIN_TEMP) / (tuned_MAX_TEMP - tuned_MIN_TEMP);
current_duty = round(current_duty / DUTY_INCREMENT) * DUTY_INCREMENT;

if (current_duty < MIN_DUTY) {current_duty = MIN_DUTY;}
else if (current_duty > MAX_DUTY) {current_duty = MAX_DUTY;}

Also noticed that the max and min duty cycle values needed flipping.
 
I messed with it a little bit and end up with this before the pwmWrite:

C-like:
current_duty = MIN_DUTY + (MAX_DUTY - MIN_DUTY) * (mapped_temp - tuned_MIN_TEMP) / (tuned_MAX_TEMP - tuned_MIN_TEMP);
current_duty = round(current_duty / DUTY_INCREMENT) * DUTY_INCREMENT;

if (current_duty < MIN_DUTY) {current_duty = MIN_DUTY;}
else if (current_duty > MAX_DUTY) {current_duty = MAX_DUTY;}

Also noticed that the max and min duty cycle values needed flipping.


Ohh very cool, that looks nice and efficient! The only condition I have a question about is that "If" statement. It looks like that makes the fan run at minimum speed at all temperatures below minimum temperature, I'd prefer to have the fan off for all temps below minimum which is where that zero came from in my code. But that may not have been the right way to solve it.

As far as the values needing flipping, I know I had the large number assigned to MIN_DUTY, and the small number assigned to MAX_DUTY, the thought there was a sloppy way to cure that inverted relationship where the fan is looking for percentage of time low, while usual PWM circuits look at percentage of time high. I'm definitely expecting that's the wrong way to go about it haha
 
Ohh very cool, that looks nice and efficient! The only condition I have a question about is that "If" statement. It looks like that makes the fan run at minimum speed at all temperatures below minimum temperature, I'd prefer to have the fan off for all temps below minimum which is where that zero came from in my code. But that may not have been the right way to solve it.
My bad, I was going of the stuff you had early on in this thread. Just replace min duty with 0.
As far as the values needing flipping, I know I had the large number assigned to MIN_DUTY, and the small number assigned to MAX_DUTY, the thought there was a sloppy way to cure that inverted relationship where the fan is looking for percentage of time low, while usual PWM circuits look at percentage of time high. I'm definitely expecting that's the wrong way to go about it haha
Subtracting the duty cycle from 100 will flip it.
 
Man, i seriously can't thank you enough. You have genuinely helped me push through the unknowns of this project, and make sense of some more daunting tasks. I think I can just about cross out the final puzzle pieces, I should have a fully compiled working code here soon (along with some thinning of leftover nonsense).

The only item left on the list is the latest addition of the additional bulkhead connection that forces max duty cycle when ground is completed. I can make sense of writing it on its own similar to the function of the momentary for the screen, the only thing I'm not sure about is how to make it override what is being done by all of the above conditions. Can I essentially make it an if condition as part of your above improved code? Basically something like:

if digital pin of choice = high, then current_duty = MAX_DUTY
followed by the other conditions
 
It may be best to put it as the last condition before pwm write.

I may have missed it, but do you know the voltage the fan excepts on the pwm wire?
 
It may be best to put it as the last condition before pwm write.

I may have missed it, but do you know the voltage the fan excepts on the pwm wire?

Will that cause any conflicts if the conditions set by that extra button conflict with the conditions dictated by temperature? Or will it automatically override by coming later in the code?

That's a damn good question on voltage, and oversight on my part. I haven't been testing the fan on low voltage. I suppose if it can't operate on the lower logic voltage from the arduino, could I have the arduino trigger a mosfet that's getting 12-14v?
 
Will that cause any conflicts if the conditions set by that extra button conflict with the conditions dictated by temperature? Or will it automatically override by coming later in the code?
That's actually what you want. If you have it above the temperature stuff, the temperature stuff will override it.
That's a damn good question on voltage, and oversight on my part. I haven't been testing the fan on low voltage.
You may have to use the arduino to drive a mosfet or something to get the right voltage. With the Uno, I'm measuring 5V output.
 
I would probably do something like this:
C-like:
current_duty = MIN_DUTY + (MAX_DUTY - MIN_DUTY) * (mapped_temp - tuned_MIN_TEMP) / (tuned_MAX_TEMP - tuned_MIN_TEMP);
current_duty = round(current_duty / DUTY_INCREMENT) * DUTY_INCREMENT;

if (MAX_PIN = TRUE) //insert correct code to read if the pin is "on" here
{current_duty = MAX_DUTY;}
else if (current_duty < MIN_DUTY) {current_duty = MIN_DUTY;}
else if (current_duty > MAX_DUTY) {current_duty = MAX_DUTY;}

Then if the max pin is triggered it will skip the rest and just put the fan on max.

Aaron Z
 
That's actually what you want. If you have it above the temperature stuff, the temperature stuff will override it.

You may have to use the arduino to drive a mosfet or something to get the right voltage. With the Uno, I'm measuring 5V output.

I just wasn't sure how it actually handled conflicts like that, if it was just a priority thing based on which code comes first, or it would would actually cause some sort of error/crash. Good to know it's just priority based though, makes life easy.

Ya caught that last part before my edit haha:lmao:. Mosfet should be no biggie if needed, and I do have raw vehicle voltage coming into the case before the converter so it should bean easy addition. Actually that reminds me, I still need to figure out if I need to add some sort of FET for reverse polarity protection brought up by rattle_snake early on, or if it needs something more robust for surge protection.

I would probably do something like this:
C-like:
current_duty = MIN_DUTY + (MAX_DUTY - MIN_DUTY) * (mapped_temp - tuned_MIN_TEMP) / (tuned_MAX_TEMP - tuned_MIN_TEMP);
current_duty = round(current_duty / DUTY_INCREMENT) * DUTY_INCREMENT;

if (MAX_PIN = TRUE) //insert correct code to read if the pin is "on" here
{current_duty = MAX_DUTY;}
else if (current_duty < MIN_DUTY) {current_duty = MIN_DUTY;}
else if (current_duty > MAX_DUTY) {current_duty = MAX_DUTY;}

Then if the max pin is triggered it will skip the rest and just put the fan on max.

Aaron Z
That sounds like about what's needed. If it's just priority based (as I'm learning), sounds like I have some flexibility in how to accomplish that extra override pin
 
I just wasn't sure how it actually handled conflicts like that, if it was just a priority thing based on which code comes first, or it would would actually cause some sort of error/crash. Good to know it's just priority based though, makes life easy.

Ya caught that last part before my edit haha:lmao:. Mosfet should be no biggie if needed, and I do have raw vehicle voltage coming into the case before the converter so it should bean easy addition. Actually that reminds me, I still need to figure out if I need to add some sort of FET for reverse polarity protection brought up by rattle_snake early on, or if it needs something more robust for surge protection.


That sounds like about what's needed. If it's just priority based (as I'm learning), sounds like I have some flexibility in how to accomplish that extra override pin

I was under the impression if we power the pin 30 (I think on the Nano) it will use the onboard regulation/protection to power the board.

Do we think that is not going to suffice? I don't mind wasting one board to test that but I'd rather not become a expert in fried boards.
 
Any one looking for gizmos but don't want to roll their own can check out this guys store.

 
I was under the impression if we power the pin 30 (I think on the Nano) it will use the onboard regulation/protection to power the board.

Do we think that is not going to suffice? I don't mind wasting one board to test that but I'd rather not become a expert in fried boards.

12v is right on the edge from what I’ve gathered, for some it works, some it fries immediately or soon after. 13.5-14v from the alternator is probably too much though. The second board in my enclosure is a voltage regulator so I can tune what the arduino is getting, pretty cheap insurance. There’s a link on the first or second page I think, when I was talking with Pablo (edit - went back and checked, post #21 has the links). But rattle brought up some concerns about reverse polarity/surges that can happen in automotive systems, which I haven’t addressed
 
Last edited:
Any one looking for gizmos but don't want to roll their own can check out this guys store.


That seems to be a pretty cool device, and reading what it offers introduced another idea for the force on pin. Instead of just forcing the fan straight to max speed, it would be easy enough to just force the duty cycle to a higher percentage. For example, when that pin is grounded have it add +20% duty cycle, or +50% duty cycle (within the preset limits). That may be fun to experiment with
 
12v is right on the edge from what I’ve gathered, for some it works, some it fries immediately or soon after. 13.5-14v from the alternator is probably too much though. The second board in my enclosure is a voltage regulator so I can tune what the arduino is getting, pretty cheap insurance. There’s a link on the first or second page I think, when I was talking with Pablo (edit - went back and checked, post #21 has the links). But rattle brought up some concerns about reverse polarity/surges that can happen in automotive systems, which I haven’t addressed
I thought the Nanos pin 30 was good up to 24+ volts? Maybe I misread that.
 
The Nano has an LM1117 linear regulator. It's going to dissipate a lot of heat for 12+v input and probably cook itself eventually. It's also not reverse-polarity protected or load-dump protected for automotive use. I happen to have a PWM fan controller I designed on my desk next to me right now that I've been polishing the code on. I can share code/schematics if you want, but read on:

When you "force" the fan to max, that's probably actually 95% duty cycle, not 100%. The designed range of DC for that fan is likely 5-95% or 10-90%. It's done like this so if there's a shorted wire or a broken wire the fan controller (in the fan itself) knows there's a problem and runs the fan at full speed. If you sent the PWM output to 100% you're telling the fan something is wrong and it might not react the way you want.

The input to the fan is probably one that wants a simple "switched ground" type output from your Nano. You would wire up a simple NPN transistor on your Arduino GPIO pin and between ground and the fan's input. PWM switches this ground on and off. This is why the voltage doesn't matter, too.

I think your control strategy lacks a bit... If you really just want the fan to come on at 160 and haul ass for no reason then so be it. But a better way is to set up a lookup table in your program that applies different DC's to different temp sensor values. At 155F you might need 30% to keep it cool. Put 5 degree steps in the table. At 170F it might need 60%, etc. You will have to run it a bit in-situ, but once you do this you'll know how much fan you really need at these different temps. Running the engine at 160F is a fruitless tree because you'll be fighting your thermostat all the time and running the fan way more than you need to. You want an engine thermostat that's 10-20F lower than your temp setpoint. That probably means you should run a 160F thermostat and set your coolant target temp to 180F, something like that. You will learn that the relationship between the fan speed and engine temp is pretty damned linear, actually. Simple, but took a while to learn this.

Also, you would be smart to switch to a two-wire temp sensor to avoid all the garbage that can come in from sharing a chassis ground with the single wire. The typical GM sensor is cheap and the datasheet has all the values already. I can share my lookup tables if you want.

You don't need the manual override switch or the pot. You are trying to deal with a situation that won't exist in real life. If you want the fan to run at full speed all the time just disconnect the PWM input and watch it fly.
 
I appreciate the knowledgeable input. Although I'm not positive, though it looks like you might be basing this response off the first couple posts and not how things have evolved over the past couple pages?

The Nano has an LM1117 linear regulator. It's going to dissipate a lot of heat for 12+v input and probably cook itself eventually. It's also not reverse-polarity protected or load-dump protected for automotive use. I happen to have a PWM fan controller I designed on my desk next to me right now that I've been polishing the code on. I can share code/schematics if you want, but read on:
This part might be directed toward CarterKraft, but I've got a voltage regulator in the case to step down the incoming voltage to a range the nano is more happy with. Though I still don't have the reverse polarity protection that rattle_snake mentioned early on, so it still needs some improvement. I'd be curious to see your schematic as well, the more the merrier imo!

When you "force" the fan to max, that's probably actually 95% duty cycle, not 100%. The designed range of DC for that fan is likely 5-95% or 10-90%. It's done like this so if there's a shorted wire or a broken wire the fan controller (in the fan itself) knows there's a problem and runs the fan at full speed. If you sent the PWM output to 100% you're telling the fan something is wrong and it might not react the way you want

The input to the fan is probably one that wants a simple "switched ground" type output from your Nano. You would wire up a simple NPN transistor on your Arduino GPIO pin and between ground and the fan's input. PWM switches this ground on and off. This is why the voltage doesn't matter, too.
87%-8% is the full range of the fan, I put up a short little video in post #28 where I also learned that it's inverted because the fan is looking for percentage low, vs the pwm generator looking at percentage high.

I think your control strategy lacks a bit... If you really just want the fan to come on at 160 and haul ass for no reason then so be it. But a better way is to set up a lookup table in your program that applies different DC's to different temp sensor values. At 155F you might need 30% to keep it cool. Put 5 degree steps in the table. At 170F it might need 60%, etc. You will have to run it a bit in-situ, but once you do this you'll know how much fan you really need at these different temps. Running the engine at 160F is a fruitless tree because you'll be fighting your thermostat all the time and running the fan way more than you need to. You want an engine thermostat that's 10-20F lower than your temp setpoint. That probably means you should run a 160F thermostat and set your coolant target temp to 180F, something like that. You will learn that the relationship between the fan speed and engine temp is pretty damned linear, actually. Simple, but took a while to learn this.

I have the duty cycle doing a linear sweep from the minimum defined temperature to the maximum defined temperature, so the fan only comes on at basically at an idle at the low value. A linear relationship might not be the most ideal, but it seems like it would be pretty sufficient without being too overkill. The starting range I chose (160-210) is completely arbitrary, they're just placeholder values that are somewhere in the ballpark for the sake of figuring out the code. I run a 195 tstat just for reference. But because my temp sensor is not near the thermostat (and the correlation between observed temp and temp at the tstat could vary across different engine configs), I figured would be wrong to assume a direct relationship there.

Also, you would be smart to switch to a two-wire temp sensor to avoid all the garbage that can come in from sharing a chassis ground with the single wire. The typical GM sensor is cheap and the datasheet has all the values already. I can share my lookup tables if you want.

You don't need the manual override switch or the pot. You are trying to deal with a situation that won't exist in real life. If you want the fan to run at full speed all the time just disconnect the PWM input and watch it fly.
Great point on the two wire temp sensor just to ensure a clean ground, that may happen.

The manual override isn't really there for "manual" control that I'm going to be managing, but to tell the fan to run faster when the AC is running so you can extract more heat from the condenser, similar to many standard fan controllers out there. The potentiometer may be overkill, but it's there for fine tuning the range when the temp sensor location doesn't see the same temperature as the thermostat. So people have a means of tuning without having to modify code if anyone else were to build one of these for themselves in the future. Obviously the code is easy to modify too, but not having to may have some additional appeal. The JL fan I'm using does not run at all with the PWM input disconnected
 
Top Back Refresh