Problem measuring 4-pin fan RPM

Hello, everyone.

I've used search and found several similar topics, but was not able to find something that would help me.

I understand electrics at a basic level, but am quite a newbie at electronics. This is my first real Arduino project. I'm, though, a software developer (currently web development, did C/C++ some 15+ years ago), so hopefully will have less trouble in the programming department.

I'm working on a temperature-sensitive automatic fan speed controller for a fan that is cooling my lathe's motor. I decided to approach this step-by-step, so first thing I'd like to do is to be able to control the fan speed using a potentiometer, and also read the actual RPM reported by the fan's tachometer. I will deal with the lathe motor temperature measurement later.

The fan is a 4-pin Noctua NF-A14 industrialPPC-2000 IP67 PWM (basically a fancy 140mm PC fan).
Here's the link for Noctua technical info on their fans.

After hooking up a potentiometer to the Arduino, I followed this article. I've set the PWM frequency to 25 KHz as mentioned in Noctua's pdf. This worked without any issue, the fan can be controlled with the pot from almost 0 to max RPM.

I wanted to be able to measure the fan's RPM, mostly to be able to tell if the fan is not working properly for some reason. So I tried to hook up the tachometer output, based mostly on this instructable. I didn't quite understand why the tachometer signal is pulled up to the 3.3V pin. I modified the code based on other snippets I found, I also tried the original code and several different variants.

The problem is, I'm getting really strange readings. Only with PWM set to 100% duty, I'm getting something that seems somewhat reasonable - around 1760 RPM (the fan is rated at 2000 +/- 10% RPM). At 0% duty I'm getting 0 RPM, and everything is between displays around 20000-30000. It seems that the interrupt is firing really often for some reason. The fan is new so I'm guessing something is wrong either with my code, or with the wiring of the circuit.

Connections:
Arduino UNO, powered via USB
Fan power connected to a 12V bench power supply.
Fan PWM connected to pin 9 on the Arduino.
Fan tachometer connected to pin 2 on the Arduino (directly, internal pull-up resistor enabled).
Power supply's ground connected to Arduino ground.

Attached is a screenshot from Noctua's pdf. It mentions "Vcc for 12V fans: 13V", while I have 5V from the Arduino. I am wondering if this 13V is just the max allowed Vcc. If I understand Arduino's INPUT_PULLUP pin mode correctly, I should have the same circuit as prescribed by the picture.

Code:

const byte OC1A_PIN = 9;
//const byte OC1B_PIN = 10;
const byte TACHO_PIN = 2;
const byte POT_PIN = A0;

const word PWM_FREQ_HZ = 25e3; // Adjust this value to adjust the frequency
const word TCNT1_TOP = 16e6 / (2 * PWM_FREQ_HZ);

volatile unsigned int tachoPulses = 0;
unsigned int rpm = 0;
unsigned long lastRpmMillis = 0;

void setup() {
  Serial.begin(9600);

  pinMode(OC1A_PIN, OUTPUT);
  pinMode(TACHO_PIN, INPUT_PULLUP);
  attachInterrupt(0, countTachoPulse, RISING);

  // Clear Timer1 control and count registers
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;

  // Set Timer1 configuration
  // COM1A(1:0) = 0b10   (Output A clear rising/set falling)
  // COM1B(1:0) = 0b00   (Output B normal operation)
  // WGM(13:10) = 0b1010 (Phase correct PWM)
  // ICNC1      = 0b0    (Input capture noise canceler disabled)
  // ICES1      = 0b0    (Input capture edge select disabled)
  // CS(12:10)  = 0b001  (Input clock select = clock/1)
  
  TCCR1A |= (1 << COM1A1) | (1 << WGM11);
  TCCR1B |= (1 << WGM13) | (1 << CS10);
  ICR1 = TCNT1_TOP;
}

void countTachoPulse() {
  ++tachoPulses;
}

void loop() {
  int potValue = analogRead(POT_PIN);
  
  setPwmDuty(map(potValue, 0, 1023, 0, 100));

  unsigned long elapsedTime = millis() - lastRpmMillis;
  if (elapsedTime >= 1000) {
    noInterrupts();
    rpm = 30e3 / elapsedTime * tachoPulses;
    tachoPulses = 0;
    interrupts();
    lastRpmMillis = millis();
    Serial.println(rpm, DEC);
  }
  
  delay(50);
}

void setPwmDuty(byte duty) {
  OCR1A = (word) (duty * TCNT1_TOP) / 100;
}

Appreciate your help in advance :slight_smile:
--Gene

Is there some good reason to specify an integer value in floating point, scientific notation?

const word PWM_FREQ_HZ = 25e3; // Adjust this value to adjust the frequency

This is the problem with copy and paste coding. I can't ask you because only the author can answer. It's really amateurish code, which is on par for instructables.

aarg:
Is there some good reason to specify an integer value in floating point, scientific notation?

This is the problem with copy and paste coding. I can't ask you because only the author can answer. It's really amateurish code, which is on par for instructables.

Is this how you greet first-time posters on this forum, by nitpicking some minor detail, and offering nothing truly helpful on the actual topic? I've had some coworkers like that, who would write comments about some code style issues during code review, but not take the time to look into the substance regarding architecture, possible bugs etc. I thought Arduino was created with beginners, students and hobbyists in mind, for many it can be a first start in programming. Many (most?) tutorials I've seen (including official ones) have rather primitive code which could be called amateurish.

I thought that by reading the "How to use this forum" topic and following the advice in there, I would get some friendly help. I didn't think I would find some professional snobbery attitude as my first experience on this forum.

I see that you have a lot of posts in here, 4 stars (whatever that means), and a lot of karma points. So it seems you are a knowledgeable and helpful person. But I can see that you didn't actually check the links I've posted, or compared my code to the code in the links. Instead, you jumped to conclusions and undeservedly criticised instructables, and accused me of copy&paste coding.

The PWM-related code comes from the first link (create.arduino.cc project hub), not from the instructable. And, the frequency there is defined as 20000. I changed it to scientific notation myself, because I don't like to see a lot of zeros, it's easy to make a mistake counting them. Like I said, my day job is web development, we actually use a statically typed language called Haxe (which is then compiled to JavaScript to run in the browsers), in Haxe indeed such code won't fly - var i:Int = 3e3; wouldn't compile (Float should be Int). I wondered about this when changing 20000 to 25e3, like I mentioned, the last time I did C/C++ was around 15 years ago, so forgive me if my C is a bit rusty. But I tried it, and found out that assigning a float to an int implicitly truncates the float, so it does compile and work (confirmed later by checking the topic). I kept it like this because for me, readable code can sometimes trump other concerns or principles. I will change it back to 25000 because I certainly want to avoid this sort of nitpicking in the future.

Like I mentioned in my first post, the PWM control already works perfectly. I'm having trouble counting the tachometer pulses. Will appreciate first and foremost suggestions on this topic, and then on improving the code.

TL;DR :sunglasses:

Paul__B:
TL;DR :sunglasses:

Mmm, the "How to use this forum" thread says "Describe your problem in detail."
That's what I did. If I would say "I'm trying to read a fan's tachometer but for some reason am getting an unreasonable number of interrupts", I would probably hear there is not enough detail.

wondering if the issue may be what amounts to noise on the sensor, thinking being if when the blade doesn't turn you (correctly) read zero, and at max you get pretty much the correct answer (I suspect you may actually be slightly over reading) and lots of over-reads in between I'm wondering if you have more errors at lower speed.

in effect is the sensor gittering across its threshold for a time, a time which is lower the faster you go.

easy to test, turn the fan blade very slowly by hand and have the interrupt set a flag so you get a serial write when (or shortly after) it fires - if you get multiple triggers as the blade with the magnet approaches the sensor this could be the issue.

not hard to solve, stick a Schmitt trigger or similar fast acting de-bounce type circuit in.

have had similar with photo-interruptors that are meant to be a hard yes/no output and things moving into the field of view at lower speeds, sometimes its better to run a linear output sensor and put the hysteresis in yourself to tune to the application.

just a thought?

curious as I'm currently doing something pretty similar (measuring fan rotation as a proxy for air flow), planning to use IR LED/Phototransistors as I have them, I do have a hall sensor and the fan actually has a magnet & hall speed sensor built in - which I'm testing the function of.

have had a look at the fan data sheet, in theory you have a square wave output from the sensor, however in practice it will have a rise and fall time - fans I'm looking at just give an output when the speed exceeds a set value, which is annoying.

its worth testing that though, turning slowly by hand will show you exactly whats happening and if you are getting false trigger pulses - the other thing is to make sure of what the sensor is actually spitting out as its high & low states, its possible if they are close to the threshold value of the input the gitter is the Arduino itself?

So I tried to hook up the tachometer output, based mostly on this instructable. I didn't quite understand why the tachometer signal is pulled up to the 3.3V pin.

Should be pulled up to 5v. for a 5v Arduno. INPUT_PULLUP on the interrupt pin should work with short wires. 560ohms is a very strong pullup and may be drawing more current than the tacho electronics can provide.

int potValue = analogRead(POT_PIN);
 setPwmDuty(map(potValue, 0, 1023, 0, 100));

Reading the pot and making adjustments every pass through loop can create problems. I would put the pot reading on a millis() timer of at least a second.

Fan PWM connected to pin 2 on the Arduino.
Fan tachometer connected to pin 9 on the Arduino (directly, internal pull-up resistor enabled).

The code would appear to be written for the tacho input to pin2 and the fan output on pin 9.
How do you really have this wired?

This would be a perfect project for the Input Capture Pin on Timer1. You can use the 8 bit timer0 to generate your 25 kHz PWM (pre-scale 8/16MHz CPU clock to 4MHz, and use TOP value of 160), and use timer1 instead for capturing the edges on the tach signal pin. Use ICP interrupt to count, and timer1 overflow interrupt to do the calculation (RPM/etc)

Maybe I am misunderstanding but looking at this:

Connections:
Arduino UNO, powered via USB
Fan power connected to a 12V bench power supply.
Fan PWM connected to pin 2 on the Arduino.
Fan tachometer connected to pin 9 on the Arduino (directly, internal pull-up resistor enabled).
Power supply's ground connected to Arduino ground.{quote}

FAN PWM connected to pin 2? I think on an Arduino Uno for example you can get a PWM output from pin 2. That said you can read a tachometer pulse signal on pin 9. The fan tachometer out does use an open collector output so as you see on page 3 of the data sheet (Customer Circuit) you need a pull up resistance. I am not saying an internal pull up won't work but trying a 10K external pull up won't hurt.

I am not going to nit-pik the code but in the interest of keeping things simple to see if the fan is outputting a tach signal I would just try some very basic code and with the fan running full tilt (PWM Input High) run a sample code like this:

unsigned long highTime;    //integer for storing high time

unsigned long lowTime;    //integer for storing low time
float period;    //integer for storing period
float freq;      //intefer for storing frequency
float RPM;      //storing or calculating RPM

void setup() {
   
    Serial.begin(9600);
    pinMode(5,INPUT);  //Setting pin as input
}

void loop() {
    highTime=pulseIn(5,HIGH);  //read high time
    lowTime=pulseIn(5,LOW);    //read low time
    period = highTime+lowTime; // Period = Ton + Toff
    freq=1000000/period;      //getting frequency with totalTime is in Micro seconds
    RPM=(freq/2)*60;          //we div by 2 since the fan tach outputs 2 pulses per revolution
   
    //Serial Print Data
    Serial.print("Frequency = ");
    Serial.print(freq);
    Serial.println("  Hertz");
    Serial.print("RPM = ");
    Serial.println(RPM);
    Serial.println("");

delay(1000);

}




There is no reason to float Period, Frequency and RPM since fractional parts of any of them are useless but it's in there. There is also no reason to use an interrupt. Pin 5 is used as the input on an Arduino Uno but change it to test your needs. Pin 9 if you like. I used it measuring 80mm PC and for an experiment and it does well enough to get you basic results. Measure pulse high and low times, add together to get period. Take the reciprocal of the period to get frequency. Divide the frequency by two pulses per revolution and multiply times 60 to end up with RPM. 

If this reads your fan then work things from there with your code.

Ron

dale_needham:
wondering if the issue may be what amounts to noise on the sensor, thinking being if when the blade doesn't turn you (correctly) read zero, and at max you get pretty much the correct answer (I suspect you may actually be slightly over reading) and lots of over-reads in between I'm wondering if you have more errors at lower speed.

I was also wondering if it can be EMI/RFI induced by the PWM signal. I don't have much knowledge on the topic, but for example setting up a VFD (variable frequency drive) for my lathe's electric motor involves a lot of EMI precautions: I have an EMI filter on the VFD input, a ferrite core on the output wires, the motor cable and signal/control cables are of a shielded variety. So I'm wondering if it's something similar here and the PWM signal is causing the interference.

dale_needham:
its worth testing that though, turning slowly by hand will show you exactly whats happening and if you are getting false trigger pulses - the other thing is to make sure of what the sensor is actually spitting out as its high & low states, its possible if they are close to the threshold value of the input the gitter is the Arduino itself?

I did what you proposed (test code below), but I'm not getting any interrupts when turning the fan by hand. The only connection I changed was to remove the wire going to fan's +12V. I also tried the test code + the original PWM code, pot set to 0, fan's +12V connected. No interrupts when turning the fan by hand. When I turn the pot higher, the interrupts start coming, as soon as I back the pot to 0, the interrupts stop, even though it takes a few seconds for the fan to spin down to a stop. It seems strange and I don't really understand why would it behave like this.

const byte TACHO_PIN = 2;

void setup() {
  Serial.begin(9600);

  pinMode(TACHO_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(TACHO_PIN), onTachoSignal, RISING);
}

volatile bool tachoState = false;
bool lastTachoState = tachoState;

void onTachoSignal() {
  tachoState = !tachoState;
}

void loop() {
  noInterrupts();
  if (tachoState != lastTachoState) {
    Serial.println(tachoState ? 1 : 0, DEC);
    lastTachoState = tachoState;
  }
  interrupts();
}

hzrnbgy:
This would be a perfect project for the Input Capture Pin on Timer1. You can use the 8 bit timer0 to generate your 25 kHz PWM (pre-scale 8/16MHz CPU clock to 4MHz, and use TOP value of 160), and use timer1 instead for capturing the edges on the tach signal pin. Use ICP interrupt to count, and timer1 overflow interrupt to do the calculation (RPM/etc)

This sounds interesting but a bit too advanced for my knowledge at the moment (I will have to read more in-depth info about using timers later). I wonder if this solution would also be susceptible to the unknown problem I'm currently facing. I would like to figure out what's wrong with the current solution first, if possible.

cattledog:
Should be pulled up to 5v. for a 5v Arduno. INPUT_PULLUP on the interrupt pin should work with short wires. 560ohms is a very strong pullup and may be drawing more current than the tacho electronics can provide.

Reading the pot and making adjustments every pass through loop can create problems. I would put the pot reading on a millis() timer of at least a second.

The code would appear to be written for the tacho input to pin2 and the fan output on pin 9.
How do you really have this wired?

The wire is fan's own wire (around 45 cm) plus a regular Dupont male-male wire. I've also just tried wiring a 3.3KOhm pull-up resistor (and changed INPUT_PULLUP to INPUT for the interrupt pin) - no difference from using the internal pull-up.

What kind of problem could come from adjusting the PWM duty cycle every 50 ms? I thought it would be a pleasant user experience, if the fan's speed reacts quickly when fiddling with the pot. I did the change you suggested, this didn't solve/improve the tachometer problem.

Sorry, my original post had the pin numbers wrong. They are connected the same way as the code expects. I've fixed this in the original post, and also took a few photos of the connections (attached), maybe you or other smart people here can see something obviously wrong. The first two photos show the original connections (using INPUT_PULLUP mode for the interrupt pin), in the last photo I've added an external pull-up resistor (interrupt pin mode changed to INPUT).

Current code (with external pull-up resistor):

const byte OC1A_PIN = 9;
//const byte OC1B_PIN = 10;
const byte TACHO_PIN = 2;
const byte POT_PIN = A0;

const unsigned long PWM_UPDATE_PERIOD = 1000;
const unsigned long RPM_CALC_PERIOD = 1000;

const word PWM_FREQ_HZ = 25000; // Adjust this value to adjust the frequency
const word TCNT1_TOP = 16000000 / (2 * PWM_FREQ_HZ);

unsigned long lastPWMUpdateTime = 0;
unsigned long lastRPMCalcTime = 0;

volatile unsigned int tachoPulses = 0;
unsigned int rpm = 0;

void setup() {
  Serial.begin(9600);

  pinMode(OC1A_PIN, OUTPUT);
  //pinMode(TACHO_PIN, INPUT_PULLUP);
  pinMode(TACHO_PIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(TACHO_PIN), countTachoPulse, RISING);

  // Clear Timer1 control and count registers
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;

  // Set Timer1 configuration
  // COM1A(1:0) = 0b10   (Output A clear rising/set falling)
  // COM1B(1:0) = 0b00   (Output B normal operation)
  // WGM(13:10) = 0b1010 (Phase correct PWM)
  // ICNC1      = 0b0    (Input capture noise canceler disabled)
  // ICES1      = 0b0    (Input capture edge select disabled)
  // CS(12:10)  = 0b001  (Input clock select = clock/1)
  
  TCCR1A |= (1 << COM1A1) | (1 << WGM11);
  TCCR1B |= (1 << WGM13) | (1 << CS10);
  ICR1 = TCNT1_TOP;

  setPwmDuty(50);
}

void countTachoPulse() {
  ++tachoPulses;
}

void loop() {
  unsigned long currentTime = millis();

  if (currentTime - lastRPMCalcTime >= RPM_CALC_PERIOD) {
    noInterrupts();
    rpm = 30000 / (currentTime - lastRPMCalcTime) * tachoPulses;
    tachoPulses = 0;
    interrupts();
    lastRPMCalcTime = currentTime;
    Serial.println(rpm, DEC);
  }

  if (lastPWMUpdateTime == 0 || currentTime - lastPWMUpdateTime >= PWM_UPDATE_PERIOD) {
    setPwmDuty(map(analogRead(POT_PIN), 0, 1023, 0, 100));
    lastPWMUpdateTime = currentTime;
  }
}

void setPwmDuty(byte duty) {
  OCR1A = (word) (duty * TCNT1_TOP) / 100;
}

Ron_Blain:
FAN PWM connected to pin 2? I think on an Arduino Uno for example you can get a PWM output from pin 2. That said you can read a tachometer pulse signal on pin 9. The fan tachometer out does use an open collector output so as you see on page 3 of the data sheet (Customer Circuit) you need a pull up resistance. I am not saying an internal pull up won't work but trying a 10K external pull up won't hurt.

I am not going to nit-pik the code but in the interest of keeping things simple to see if the fan is outputting a tach signal I would just try some very basic code and with the fan running full tilt (PWM Input High) run a sample code like this:
...

Ron, somehow your replies ended up mixed together with your quote of my post. I was able to figure it out, but not the easiest to read :slight_smile:

I'm sorry, I made a mistake with the connection description in the original post. I have the PWM output on pin 9, and tacho input on pin 2.

Note: In the original post I was using 12V from a bench power supply. Currently I'm using 9V, supplied to Arduino via the barrel jack from a 9V power adapter that the postlady delivered just yesterday. I have the fan +12V wire connected to the VIN pin (as you can see on the photo in my previous post). The fan is obviously turning slower, but I don't suppose this could be causing any issues with regards to the tachometer?

I've just tried your code (changing the pin from 5 to 2), and, when fan is running at full tilt, it's producing correct results (RPM = 1319.15). My original code reports 1320 RPM at full speed. I then added PWM control code to your code, and set the duty cycle to 50%. Still works! I added the code that sets the duty cycle according to the pot value, and the measurements still work throughout the range, although when adjusting the pot, I sometimes see a spurious value (e.g. double from what is expected, or even a few times it shows "inf"), when I stop fiddling with the pot, the reading becomes stable.

For the sake of experiment, I removed my 3.3K pull-up resistor and used the internal pull-up (changed pin mode to INPUT_PULLUP), with this setup the readings became unstable. With duty cycle hardcoded to 50%, the successive readings alternate between around 740 (reasonable value), 1450 (double of actual value), and sometimes inf.

What conclusions can we draw from all of this? And I suppose something is wrong with my interrupt-based code, it would be nice to know what.

I would like to figure out what's wrong with the current solution first, if possible.

Good thinking. I don't think there are fundamental issues with the code. Forgetting about the fan, when I connect A0 to 3.3v and jumper the pwm output pin the tacho input pin, I can read the pulses.

What is your current problem statement?

cattledog:
Good thinking. I don't think there are fundamental issues with the code. Forgetting about the fan, when I connect A0 to 3.3v and jumper the pwm output pin the tacho input pin, I can read the pulses.

What is your current problem statement?

I don't really understand what you're saying with A0, 3.3v and jumper.

Now for the problem. The code based on pulseIn does seem to work (even though it sometimes gives a few spurious readings while adjusting the pot). I could just use that code and consider it done.

However, I thought the interrupt-based code seems kinda neat. It would be nice if we could figure out why it only works at 0% and 100% duty cycles. I think that somehow it's caused by the PWM signal. I've just tried another experiment. I hardcoded the duty cycle, and then tried different PWM frequencies, all in the acceptable range of 21KHz to 28KHz. The reported RPM varied widely, depending on the PWM frequency.
PWM Hz - RPM average, readings fluctuate by around +/- 500
21000 - 42000
22000 - 6500
23000 - 37000
25000 - 30000
27000 - 25000
28000 - 56000

So, is PWM noise/EMI messing with reading the tacho signal? I looked up the Schmitt trigger that was suggested earlier, but I think I don't have the parts with which to try it out.

gene-pavlovsky:
Mmm, the "How to use this forum" thread says "Describe your problem in detail."
That's what I did.

As indeed you should.

I was commenting on the petulant whine in the first three paragraphs, occupying the body of your post which did not contribute usefully to solving your problem and dissuaded me for one from trying to help. :grinning:

Paul__B:
I was commenting on the petulant whine in the first three paragraphs, occupying the body of your post which did not contribute usefully to solving your problem and dissuaded me for one from trying to help. :grinning:

Yeah my wife told me I shouldn't have wasted my energy feeding the troll. Lesson learned - next time I'll consult my wife first :slight_smile:

Are you now ready to jump in and help?

With the help of my friend, who is not an Arduino guy per se, but knows a lot about electronics and robots, I came up with the following circuit (photo attached), that seems to result in correct RPM readings. I used the biggest ceramic capacitor I had on hand (0.1 uF) between tachometer signal and ground, a 2 KOhm resistor between tachometer signal and VCC, and a 10 KOhm series resistor to the Arduino's pin (mode set to INPUT). The circuit my friend suggested (a low-pass RC filter) actually has the capacitor on the other side of the series resistor. But for some reason that arrangement gave too high readings, and the one I ended up with just works.

Basically he thinks that the 25KHz PWM causes noise in the tachometer wire, so whenever the tachometer switches, multiple interrupts happen in a very short time due to this noise. This noise must somehow be filtered out or debounced (a Schmitt trigger was suggested for this earlier).

I wonder if this circuit is good enough, or should something be improved here.

I am also about to try my friend's suggestion - software debouncing. Will post code if it works.

1 Like

I had a problem with "glitchy" PWM before and it happens to be caused by changing the duty cycle/period mid signal/pulse. The way I solved it is by changing the PWM period/duty Cycle during the PWM overflow ISR.

Maybe this is something you can explore.

hzrnbgy:
I had a problem with "glitchy" PWM before and it happens to be caused by changing the duty cycle/period mid signal/pulse. The way I solved it is by changing the PWM period/duty Cycle during the PWM overflow ISR.

Maybe this is something you can explore.

The problems occur even if PWM duty cycle is fixed (e.g. at 50%) during setup.
Currently my code only changes the duty cycle only if the pot values changes by >=2%. So if I don't touch the pot, the duty cycle is not changed at all.

I've just tried to do some software debouncing, assuming that I'm getting close bursts of spurious interrupts when the tachometer signal changes. With 2 pulses per rev, my 2000 RPM fan should produce no more than 66.7 pulses per second, so in theory there should be 15 or more ms between interrupts. So I tried checking how many millis had elapsed since last interrupt, and if the value is less than 5, assume it's a "noise" interrupt and increment the noise counter instead of the regular counter. However it seems that my assumption is wrong. I've measured the min/max time between two successive interrupts (printed out and reset every second), and in reality I'm getting interrupts every 0 to 2 ms, if the circuit is wired as Noctua suggests (I tried both a 3.3 KOhm external pull-up resistor and the internal pull-up).

By the way, the modified circuit I attached a photo of in the last post works even without a capacitor (so, a 2 KOhm pull-up resistor and 10 KOhm series resistor seem to be doing the trick somehow). I wish I knew more about electronics and could understand why.

My friend suggested an oscilloscope could help investigate this further, but I don't have one.

I've just tried to do some software debouncing, assuming that I'm getting close bursts of spurious interrupts when the tachometer signal changes. With 2 pulses per rev, my 2000 RPM fan should produce no more than 66.7 pulses per second, so in theory there should be 15 or more ms between interrupts.

I don't know what you tried, but see what this simple lock out in the isr performs for you. Adjust the lock out time for your interrupt frequency.

void countTachoPulse() {
  static unsigned long interruptTime = micros();
  static unsigned long lastInterruptTime = 0;
  const unsigned long lockOutTime = 10000;//10ms
  if(interruptTime - lastInterruptTime >= lockOutTime)
  {
  ++tachoPulses;
  }
  lastInterruptTime = interruptTime;
}