ISR only called when PWM duty cycle > 0

Hi all,

I'm trying to control a 4-pin PC fan based on data from a humidity sensor using an Arduino Micro.
I have everything in place. I even read the fan's RPM, and that's where I'm confused.
When I set the PWM duty cycle to 0, it is still spinning because it has a minimum speed. That's not great, but I can always add a relay if I want to switch it off entirely. I read the fan's RPM by connecting its tacho pin to pin 7 on the Micro and attaching an interrupt on the falling edge.
The weird thing is that the ISR is only called when I set the PWM duty cycle to a value greater than zero. I don't understand why that is. The pin I use to set the fan's PWM (pin 9) and the pin to read the fan's RPM (pin 7) are completely independent and the fan is still spinning when the PWM duty cycle is set to 0, so in my opinion the ISR to read the RPM should be called, but it isn't. What can I do to fix this?

Here's my sketch:

#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <TimerOne.h>

#define SEALEVELPRESSURE_HPA (1013.25)
#define pwmPin 9
#define rpmPin 7

Adafruit_BME280 bme;  // I2C

/* RPM calculation */
volatile unsigned long duration = 0; // accumulates pulse width
volatile unsigned int pulseCount = 0;
volatile unsigned long previousMicros = 0;

/* Called when hall sensor pulses */
//TODO why is this only called when the pwmValue is > 0 even though the fan is spinning with pwmValue == 0?
void pickRPM ()
{
  volatile unsigned long currentMicros = micros();

  if (currentMicros - previousMicros > 20000) // Prevent pulses less than 20k micros far.
  {
    duration += currentMicros - previousMicros;
    previousMicros = currentMicros;
    pulseCount++;
  }
}

void setup()
{

  pinMode(rpmPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(rpmPin), pickRPM, FALLING);

  Serial.begin(9600);
  while (!Serial)
    ;  // time to get serial running
  Serial.println(F("fan control based on humidity"));

  unsigned status;

  status = bme.begin(0x76);
  if (!status) {
    Serial.println("Could not find a valid BME280 sensor, check wiring, address, sensor ID!");
    Serial.print("SensorID was: 0x");
    Serial.println(bme.sensorID(), 16);
    Serial.print("        ID of 0xFF probably means a bad address, a BMP 180 or BMP 085\n");
    Serial.print("   ID of 0x56-0x58 represents a BMP 280,\n");
    Serial.print("        ID of 0x60 represents a BME 280.\n");
    Serial.print("        ID of 0x61 represents a BME 680.\n");
    while (1) delay(10);
  }

  Timer1.initialize(40);  // 40 us = 25 kHz

  Serial.println();
}


void loop() {
  Serial.print("Temperature = ");
  Serial.print(bme.readTemperature());
  Serial.println(" °C");

  Serial.print("Pressure = ");

  Serial.print(bme.readPressure() / 100.0F);
  Serial.println(" hPa");

  Serial.print("Approx. Altitude = ");
  Serial.print(bme.readAltitude(SEALEVELPRESSURE_HPA));
  Serial.println(" m");

  Serial.print("Humidity = ");
  Serial.print(bme.readHumidity());
  Serial.println(" %");

  Serial.println();

  float humidity = bme.readHumidity();
  float pwmValue = max(0, (humidity - 80.0f)) * (100 / 20);
  Timer1.pwm(pwmPin, (pwmValue / 100) * 1023);
  Serial.print("PWM Value = ");
  Serial.print(pwmValue);
  Serial.println(" %");

  // Calculate fan speed
  float Freq = (1e6 / float(duration) * pulseCount) / 2;
  int speed = Freq * 60;
  Serial.print("fan speed = ");
  Serial.print(speed);
  Serial.println(" rpm");

  duration = 0;
  pulseCount = 0;

  Serial.println("==================================");
 
  delay(1000);
}

That's because duty cycle values below zero are treated as zero, with no pulses generated.

Some fans do stop, but most have a minimum rpm, which is an Intel specification.
The Noctua NF-F12 for example has this in the datasheet: " Stops at 0% PWM".

This pinMode(rpmPin, INPUT); should be pinMode(rpmPin, INPUT_PULLUP);
Leo..

Okay, maybe I didn't make myself clear enough.
The question wasn't why the fan is still running with the duty cycle set to 0. I even wrote that it's because it has a minimum speed. The code also never sets the duty cycle to a value below 0.
The question is why I can't read the fan's RPM even though it is spinning. pickRPM() simply isn't called when pwmValue is 0. I'd like it to be called whenever the fan is spinning, no matter what I set the PWM duty cycle to.

I'll try that out.

If no pulses are generated then no FALLING edge occurs.

See my #2 and try to understand PWM generation.

I think we're still misunderstanding each other. The Fan is spinning at its minimum speed, so pulses are generated by the hall effect sensor inside the fan. The fan simply receives no PWM signal when the duty cycle is set to zero. These are two different pins, though - on the Arduino as well as on the fan. So in my opinion if the fan is spinning, pickRPM() should be called no matter what I set the PWM duty cycle to.

Just to make it clear, this is my current setup:

Ok, my bad :frowning:

Then the guilty sits in the fan and suppresses RPM pulses when PWM duty cycle is zero.

So my code is correct?

How would that make any sense? I'll try with a different fan to see whether that shows the same behavior.

Your issue may be caused by the following. (in fact chances are more issues may be caused by it)

/* RPM calculation */
volatile unsigned long duration = 0; // accumulates pulse width
volatile unsigned int pulseCount = 0;
volatile unsigned long previousMicros = 0;

/* Called when hall sensor pulses */
//TODO why is this only called when the pwmValue is > 0 even though the fan is spinning with pwmValue == 0?
void pickRPM ()
{
  volatile unsigned long currentMicros = micros();

  if (currentMicros - previousMicros > 20000) // Prevent pulses less than 20k micros far.
  {
    duration += currentMicros - previousMicros;
    previousMicros = currentMicros;
    pulseCount++;
  }
}

Within an ISR micros is not updated, but that isn't relevant here. Also the local variable doesn't need to be volatile, but the global variables which you correctly made volatile are 32-bit, while you are working on an 8-bit MCU.
Before you start to use that variable outside of the ISR, you need to disable interrupts, copy the value to a different variable (which isn't used inside the ISR) and then do the processing with that variable, to prevent the variable being modified while it is being read.

Also it may be better to get rid of all of those excessive Serial messages, since they do influence the ISR's simply by invoking an ISR themselves, which in turn blocks the execution of an ISR.

Thanks for the hint, I'll try that out.

I'll do that, but for now I need those for debugging purposes.

At this point, you do NOT know if the interrupt code is executed or not if your length test fails. Comment out the length test and see if the count increases during a test. If it does, your interrupt is actually running.

while at the same time micros may become garbage if the low 16 (hardware) bits overflow and the high 16 (software) bits are not updated. Exact effects depend on the firmware implementation.

You could try pulseIn() to see if you get the same result.