Introduction and Goal

Disclaimer: I do not have profound but basic knowledge in electronics and hardware and my python is a bit rusty. For tl;dr, jump to the problem section below.


I'm trying to build a standalone cooling system for my server closet (for educational purposes, mostly) which seemed easy enough to do but now I hit the first major obstacle.

The controller for the cooling system will be a Raspberry Pi Zero WH I had lying around and the used fans are 4-pin-PWM-fans Noctua's NF-P12 redux-1300 PWM. The RPi and fans then will be connected to an Arctic Case Fan Hub. Therefore, power will come from an external power supply while only the RPM/TACH and PWM pins will be connected to the RPi.

The goal is to read the RPM from the fan hub, in this case from the fan in slot 1, and, depending on temperature (will add temperature sensor later), daytime, and maybe some other factors, send a PWM signal accordingly.

Current Status

After some reading (page 3), I learned that the RPM pin is an open collector. I used this circuit as a reference

and connected my RPi as follows:



The fan spins joyfully according to the power applied.

But know to my first question: Is this circuit correct? If yes, the occurring problem is confusing me more and more.


On the software side, I am using gpiozero, to be exact the button input device. There are two functions which seem useful: when_pressed (calls function on press) and wait_for_press()/wait_for_release() (blocks code from continuing until button is pressed/released). Now the problem:

When testing with the following test script with wait_for_press()/wait_for_release()

import gpiozero

pin_rpm = gpiozero.Button(24)

while True:

everything works as expected.

  • Fan is spinning = Script outputs "Press" and "Release" alternating
  • Fan isn't spinning = Script waits for next press

BUT if I am using when_pressed

import gpiozero

pin_rpm = gpiozero.Button(24)

def i_am_lying():
    print("I aM deFiNItELy BEiNg PResSeD")
while True:
    pin_rpm.when_pressed = i_am_lying

it does not matter if the fan is spinning or not, the script is lying to me and claims the button is being pressed.

Why? Is the circuit incorrect (different wiring, different resistor, malfunctioning hardware)? Does when_pressed does not work as I expect or use it?


On a German RPi board, I found this script excerpt which seems to have a similar goal as me. I began to translate it and added some documentation and outputs for troubleshooting (see end of post). It has the same problem as the test script. Even without spinning fan, it calls the functions with when_pressed. But as long I am not certain that the hardware is correct, I do not see the point in extensively troubleshoot the script while the signal I am dealing with might be faulty.

pi@julius:~/fan-controller/scripts $ python3 fan-controller.py
Initalization: 12:28:18.382540
Tick Interval: -1.991448
Start Time: 12:28:18.394005
TPS: -0.5021471813474416
RPM: -15.064415440423248

Tick Interval: 0.04042900000000005
Start Time: 12:28:20.434028
TPS: 24.734720126641736
RPM: 742.0416037992521

Tick Interval: 0.03633100000000011
Start Time: 12:28:22.485537
TPS: 27.52470342132055
RPM: 825.7411026396164


In the end, I want the script to output the RPM every five seconds by stopping the time between to ticks and calculating the ticks per second and subsequently the rotation per minute.



import gpiozero
import time
import datetime
import warnings

As of gpiozero v1.5, setting a callback funciton to None 
(see stop_frequency_count()) will raise a CallbackSetToNone warning. If it is
intentional this can be surpressed with the warnings module. 
# Ignore CallbackSetToNone warning as it is intentional
warnings.filterwarnings("ignore", message='^.*was set to None.*$')

# Set pin GPIO24 (BCM) as a button-like input device with external pull-up 
# resistor and default HIGH
pin_rpm = gpiozero.Button(24)

class Tacho:
    def __init__(self):
        self.time_start= None
        self.tick_interval = 0
        self.tick_curr = 0
        self.tick_last = datetime.datetime.now()
        print(f"Initalization: {self.tick_last.time()}")

    def start_frequency_count(self):
        pin_rpm.when_pressed = self.increase_counter()
        self.time_start = datetime.datetime.now()
        print(f"Start Time: {self.time_start.time()}")

    def stop_frequency_count(self):
        pin_rpm.when_pressed = None
    def increase_counter(self):
        tick_curr = datetime.datetime.now()
        self.tick_interval = (tick_curr - self.tick_last).total_seconds() - 2
        print(f"Tick Interval: {self.tick_interval}")
        self.tick_last = tick_curr

    def evaluate_results(self):
        # Get ticks per second by dividing 1 by the time between two ticks
            ticks_per_second = 1 / self.tick_interval
        except ZeroDivisionError:
            ticks_per_second = 0
        print(f"TPS: {ticks_per_second}")
        # There are two ticks per rotation, therefore dividing by 2 and
        # multiplying with 60 will give the rotations per minute.
        rpm = ticks_per_second / 2 * 60
        print(f"RPM: {rpm}\n")

def main():
    tacho = Tacho()
    while True:
        while (datetime.datetime.now() - tacho.time_start).total_seconds() < 1:

if __name__ == "__main__":
  • Note that the Arduino uses 5V logic and the Pi 3.3V. If you are not aware of that/have not taken it into account, it could be important; what counts as high vs. low for a GPIO input is determined by voltage.
    – goldilocks
    Nov 22, 2023 at 14:34
  • Your Question(s) lack focus. Limit to one question at a time. You appear to want to see the frequency of pulses. I suggest, rather than the overly complex and convoluted code you have detect when a pulse occurs; record time; when the next pulse occurs calculate the elapsed time (invert to get frequency if desired).
    – Milliways
    Nov 22, 2023 at 23:44
  • NOTE DO NOT attempt to print ANYTHING in callbacks. If the fan is spinning at ~800 rpm there will only be 600µSec between pulses - NO time to print anything!
    – Milliways
    Nov 23, 2023 at 5:48


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.