HP Proliant PWM fan reader - controller

Hello Everyone,

I'm working on arduino pwn fan "controller", but it won't be a typical solution.

I have a proliant server, running with lower TDP cpus, and I will watercool it anyway. But still - I want to retain fans to some extend (there's a lot of other components to cool, not only cpus) and want also to retain original fan steering - especially in case of malfunction.

My idea was to use arduino to basically two things: 1. Read original PWM controlling signal value off 4-pin (well six in that case, but that's another story) 2. send new PWM signal to the fan, using original as a reference.

For now I want to focus on point 1. - reading original PWM value off the motherboard.

6-pin PWM fan headers on Proliant have basically same pinout as standard 4 pin, but two fans can be connected to one header (+12V1, GND, +12V2, Tacho1, PWM, Tacho2)

According to wiki https://en.wikipedia.org/wiki/Computer_fan_control#Pulse-width_modulation PWM signal is 25kHz:

"control pin is an open-drain or open-collector output, which requires a pull-up to 5 V or 3.3 V in the fan"

If I understand that correctly, voltage is delivered on the fan (arduino) side, with GND on the motherboard? Would it mean that it will be enough to define the pin as INPUT_PULLUP, and then follow http://www.benripley.com/diy/arduino/three-ways-to-read-a-pwm-signal-with-arduino/ ?

Ground lines must be connected, for a common signal voltage reference level.

Open collector outputs act like mechanical switches, which can sink but not source current. They require a pulllup resistor or other load, connected to some voltage source. This allows to use the Vcc of the input device (3.3V or 5V) to generate just the expected input signal levels - as far as supported by the output transistor.

Thanks for information, I’m connecting PWM signal from the motherboard to arduino pin set as INPUT_PULLUP and it seem to work.

Stumbled upon different issue though:

While I was working along with Three Ways To Read A PWM Signal With Arduino | BenRipley.com first method works, added some “smoothing” to the result, but overall it reads similar PWM duty cycle as show by iLO monitoring.

byte PWM_PIN = 3;

const int iterations = 100; 

int high_value;
int low_value;
int duty_cycle;
int highValues[iterations];
int lowValues[iterations];
 
void setup() {
  pinMode(PWM_PIN, INPUT_PULLUP);
  for (int i = 0; i < iterations; i++) {
    highValues[i] = 0;
    lowValues[i] = 0;
  }
  Serial.begin(115200);
}
 
void loop() {
  for (int currentIteration = 0; currentIteration < iterations; currentIteration++){
    highValues[currentIteration] = pulseIn(PWM_PIN, HIGH);
    lowValues[currentIteration] = pulseIn(PWM_PIN, LOW);
    Serial.println(highValues[currentIteration]);
    Serial.println(lowValues[currentIteration]);
    }
  int sumHigh = 0;
  int sumLow = 0;
  for (int i = 0; i < iterations; i++){
    sumHigh = sumHigh + highValues[i];
    sumLow = sumLow + lowValues[i];
  }
  int averageHigh = sumHigh / iterations;
  int averageLow = sumLow / iterations;
  duty_cycle = 100 * averageHigh / ( averageHigh + averageLow );
  Serial.println(duty_cycle);
  delay(200);
}

I take time in state HIGH as a signal, time in HIGH + LOW as a PWM frequency interval (which gets measured about right for 25kHz PWM - 40 microseconds). The calculated PWM duty cycle value is within 1% of what ilo shows.

Unfortunately this changed when I moved to method 2 of reading PWM, external interrupts…

 int fallTime = 0;
 int raiseTime = 0;
volatile int timeLow = 0;
volatile int timeHigh = 0;
 
void setup() {
  Serial.begin(115200);
  pinMode(3, INPUT_PULLUP);
  // when pin 3 goes high, call the rising function
  attachInterrupt(0, rising, RISING); // interrupt 0 -> pin 3 (3,2,0,1 -> 0,1,2,3)
}
 
void loop() {
  Serial.print("PWM Duty Cycle: ");
  Serial.print(100 * timeHigh / (timeLow + timeHigh));
  Serial.print(" %         ");
  delay(500);
  }
 
void rising() {
  raiseTime = micros();
  timeLow = micros() - fallTime;
  attachInterrupt(0, falling, FALLING);
}
 
void falling() {
  fallTime = micros();
  timeHigh = micros()-raiseTime;
  attachInterrupt(0, rising, RISING);
}

With code above, second way from the link listed in the beginning of this post, I get reading 10-20% different than what iLo2 and method 1 returns. Did I do something wrong?

First of all you should disable interrupts while accessing the volatile variables outside an ISR.

Next you can try to add averaging to the interrupt version, as used in the pulseIn version. Arrays are not required, unless you want to plot curves, else you can sum up the low and high times immediately.

How accurate is the PWM signal, does it jitter?

AFAIR micros() increments only in steps of 4µs, so that you won't get more than 10 distinct duty cycle steps in a 40µs cycle, or 10% error. Do corresponding high and low times sum up to the expected 40µs?