From a9a915685c51343d6aae0881ec0e195e59759068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Gi=C4=8D?= Date: Sat, 25 May 2024 16:51:03 +0200 Subject: [PATCH 1/8] Rename constants to Screaming snake case --- rpi-pwmfan.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/rpi-pwmfan.py b/rpi-pwmfan.py index 732dcc1..759e4da 100755 --- a/rpi-pwmfan.py +++ b/rpi-pwmfan.py @@ -4,14 +4,15 @@ import time from time import sleep -pwmPin = 12 #HW PWM works on GPIO 12, 13, 18 and 19 on RPi4B -pwmRange = 5000 -tachoPin = 6 -lowTemp = 55 # Lowest temperature, if lowest of this, the FAN is Off -maxTemp = 60 # Higher than it, the FAN is on full speed -check_sec = 2 #Time to check temperature and set FAN speed +PWM_PIN = 12 # HW PWM works on GPIO 12, 13, 18 and 19 on RPi4B +RPM_MAX = 5000 # Noctua Specs: Max=5000 +RPM_MIN = 1500 # Noctua Specs: Min=1000 +TACHO_PIN = 6 +MAX_TEMP = 60 # Above this temperature, the FAN is at max speed +LOW_TEMP = 55 # Lowest temperature, if lowest of this, the FAN is Off +WAIT = 2 # Interval before adjusting RPM (seconds) -percentTemp = (maxTemp-lowTemp)/100.0 +PERCENT_TEMP = (MAX_TEMP - LOW_TEMP) / 100.0 rpmChkStartTime=None rpmPulse = 0 @@ -33,7 +34,7 @@ def __init__(self, kp, ki, kd, tau, limMin, limMax): self.tau=tau self.limMin=limMin self.limMax=limMax - self.time=check_sec + self.time=WAIT self.integrator=0 self.prevError=0 self.differentiator=0 @@ -95,11 +96,11 @@ def setupTacho(): print("Setting up Tacho input pin") wiringpi.wiringPiSetupGpio() - wiringpi.pinMode(tachoPin,wiringpi.INPUT) - wiringpi.pullUpDnControl(tachoPin,wiringpi.PUD_UP) + wiringpi.pinMode(TACHO_PIN, wiringpi.INPUT) + wiringpi.pullUpDnControl(TACHO_PIN, wiringpi.PUD_UP) rpmChkStartTime=time.time() #print("{:4d}".format(wiringpi.INT_EDGE_FALLING)) - wiringpi.wiringPiISR(tachoPin,wiringpi.INT_EDGE_FALLING,tachoISR) + wiringpi.wiringPiISR(TACHO_PIN, wiringpi.INT_EDGE_FALLING, tachoISR) return def readRPM(): @@ -118,12 +119,12 @@ def readRPM(): return ret def fanOn(): - wiringpi.pwmWrite(pwmPin,pwmRange) + wiringpi.pwmWrite(PWM_PIN, RPM_MAX) return def updateFanSpeed(): temp=getCPUTemp() - myPID.update(lowTemp,temp) + myPID.update(LOW_TEMP, temp) #percentDiff = 45 @@ -141,22 +142,22 @@ def updateFanSpeed(): #percentDiff = 0 #if diff > 0: # percentDiff=diff/percentTemp - pwmDuty=int(percentDiff*pwmRange/100.0) + pwmDuty=int(percentDiff * RPM_MAX / 100.0) print(myPID.out) - wiringpi.pwmWrite(pwmPin, pwmDuty) + wiringpi.pwmWrite(PWM_PIN, pwmDuty) #print("currTemp {:4.2f} tempDiff {:4.2f} percentDiff {:4.2f} pwmDuty {:5.0f}".format(temp, diff, percentDiff, pwmDuty)) return def setup(): wiringpi.wiringPiSetupGpio() #wiringpi.pinMode(pwmPin, 2) #HW PWM works on GPIO 12, 13, 18 and 19 on RPi4B - wiringpi.pinMode(pwmPin,wiringpi.PWM_OUTPUT) + wiringpi.pinMode(PWM_PIN, wiringpi.PWM_OUTPUT) wiringpi.pwmSetClock(768) #Set PWM divider of base clock 19.2Mhz to 25Khz (Intel's recommendation for PWM FANs) - wiringpi.pwmSetRange(pwmRange) #Range setted + wiringpi.pwmSetRange(RPM_MAX) #Range setted - wiringpi.pwmWrite(pwmPin, pwmRange) # Setting to the max PWM + wiringpi.pwmWrite(PWM_PIN, RPM_MAX) # Setting to the max PWM return def main(): @@ -169,7 +170,7 @@ def main(): try: updateFanSpeed() readRPM() - sleep(check_sec) + sleep(WAIT) except KeyboardInterrupt: fanOn() break From 3adcb241074052900c9abe8dfa2221fe183af501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Gi=C4=8D?= Date: Sat, 25 May 2024 17:04:37 +0200 Subject: [PATCH 2/8] Reformat code using .editorconfig --- .editorconfig | 16 +++ rpi-pwmfan.py | 290 ++++++++++++++++++++++++++------------------------ 2 files changed, 166 insertions(+), 140 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7a52181 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +insert_final_newline = true + +[*.{ini,py,py.tpl,rst}] +indent_style = space +indent_size = 4 + +[*.{sh,bat.tpl,Makefile.tpl}] +indent_style = tab +indent_size = 4 + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/rpi-pwmfan.py b/rpi-pwmfan.py index 759e4da..852335a 100755 --- a/rpi-pwmfan.py +++ b/rpi-pwmfan.py @@ -2,19 +2,19 @@ import wiringpi as wiringpi import time -from time import sleep +from time import sleep -PWM_PIN = 12 # HW PWM works on GPIO 12, 13, 18 and 19 on RPi4B +PWM_PIN = 12 # HW PWM works on GPIO 12, 13, 18 and 19 on RPi4B RPM_MAX = 5000 # Noctua Specs: Max=5000 RPM_MIN = 1500 # Noctua Specs: Min=1000 TACHO_PIN = 6 -MAX_TEMP = 60 # Above this temperature, the FAN is at max speed -LOW_TEMP = 55 # Lowest temperature, if lowest of this, the FAN is Off -WAIT = 2 # Interval before adjusting RPM (seconds) +MAX_TEMP = 60 # Above this temperature, the FAN is at max speed +LOW_TEMP = 55 # Lowest temperature, if lowest of this, the FAN is Off +WAIT = 2 # Interval before adjusting RPM (seconds) PERCENT_TEMP = (MAX_TEMP - LOW_TEMP) / 100.0 -rpmChkStartTime=None +rpmChkStartTime = None rpmPulse = 0 ###PID Parameters### @@ -27,158 +27,168 @@ class PID_Controller: - def __init__(self, kp, ki, kd, tau, limMin, limMax): - self.kp=kp - self.ki=ki - self.kd=kd - self.tau=tau - self.limMin=limMin - self.limMax=limMax - self.time=WAIT - self.integrator=0 - self.prevError=0 - self.differentiator=0 - self.prevMeasure=0 - self.out=0 - def update(self, setpoint, measure): - error=setpoint-measure - #error=measure-setpoint - #Proportional gain - proportional=self.kp*error - #Integral gain - self.integrator=self.integrator+0.5*self.ki*self.time*(error+self.prevError) - #Anti-wind-up - if self.limMax>proportional: - intLimMax=self.limMax-proportional - else: - intLimMax=0 - if self.limMinintLimMax: - self.integrator=intLimMax - else: - self.integrator=intLimMin - #Differentiator gain - self.differentiator=(2*self.kd*measure-self.prevMeasure)+(2*self.tau-self.time)*self.differentiator/(2*self.tau+self.time) - #Calculate output - self.out=proportional+self.integrator+self.differentiator - #Apply limits - if self.out > self.limMax: - self.out=self.limMax - elif self.out < self.limMin: - self.out=self.limMin - #Store data - print(self.prevError) - self.prevError=error - print(self.prevError) - self.prevMeasure=measure - -myPID=PID_Controller(KP,KI,KD,TAU,PID_MIN,PID_MAX) + def __init__(self, kp, ki, kd, tau, limMin, limMax): + self.kp = kp + self.ki = ki + self.kd = kd + self.tau = tau + self.limMin = limMin + self.limMax = limMax + self.time = WAIT + self.integrator = 0 + self.prevError = 0 + self.differentiator = 0 + self.prevMeasure = 0 + self.out = 0 + + def update(self, setpoint, measure): + error = setpoint - measure + # error=measure-setpoint + # Proportional gain + proportional = self.kp * error + # Integral gain + self.integrator = self.integrator + 0.5 * self.ki * self.time * (error + self.prevError) + # Anti-wind-up + if self.limMax > proportional: + intLimMax = self.limMax - proportional + else: + intLimMax = 0 + if self.limMin < proportional: + intLimMin = self.limMin - proportional + else: + intLimMin = 0 + # Clamp integrator + if self.integrator > intLimMax: + self.integrator = intLimMax + else: + self.integrator = intLimMin + # Differentiator gain + self.differentiator = (2 * self.kd * measure - self.prevMeasure) + ( + 2 * self.tau - self.time) * self.differentiator / (2 * self.tau + self.time) + # Calculate output + self.out = proportional + self.integrator + self.differentiator + # Apply limits + if self.out > self.limMax: + self.out = self.limMax + elif self.out < self.limMin: + self.out = self.limMin + # Store data + print(self.prevError) + self.prevError = error + print(self.prevError) + self.prevMeasure = measure + + +myPID = PID_Controller(KP, KI, KD, TAU, PID_MIN, PID_MAX) + def getCPUTemp(): - f=open('/sys/class/thermal/thermal_zone0/temp', 'r') - temp=f.readline() - f.close() - ret=float(temp)/1000 - return ret + f = open('/sys/class/thermal/thermal_zone0/temp', 'r') + temp = f.readline() + f.close() + ret = float(temp) / 1000 + return ret + def tachoISR(): - global rpmPulse - #print("interruption!!!") - rpmPulse+=1 - return + global rpmPulse + # print("interruption!!!") + rpmPulse += 1 + return + def setupTacho(): - global rpmChkStartTime + global rpmChkStartTime + + print("Setting up Tacho input pin") + wiringpi.wiringPiSetupGpio() + wiringpi.pinMode(TACHO_PIN, wiringpi.INPUT) + wiringpi.pullUpDnControl(TACHO_PIN, wiringpi.PUD_UP) + rpmChkStartTime = time.time() + # print("{:4d}".format(wiringpi.INT_EDGE_FALLING)) + wiringpi.wiringPiISR(TACHO_PIN, wiringpi.INT_EDGE_FALLING, tachoISR) + return - print("Setting up Tacho input pin") - wiringpi.wiringPiSetupGpio() - wiringpi.pinMode(TACHO_PIN, wiringpi.INPUT) - wiringpi.pullUpDnControl(TACHO_PIN, wiringpi.PUD_UP) - rpmChkStartTime=time.time() - #print("{:4d}".format(wiringpi.INT_EDGE_FALLING)) - wiringpi.wiringPiISR(TACHO_PIN, wiringpi.INT_EDGE_FALLING, tachoISR) - return def readRPM(): - global rpmPulse, rpmChkStartTime - fanPulses=2 - - duration=time.time()-rpmChkStartTime - frequency=rpmPulse/duration - ret=int(frequency*60/fanPulses) - rpmChkStartTime=time.time() - rpmPulse=0 - print("Frequency {:3.2f} | RPM:{:4d}".format(frequency,ret)) -# with open('/tmp/adf-fanspeed', 'w') as f: -# f.write(str(ret)+'\n') -# f.close(); - return ret - -def fanOn(): - wiringpi.pwmWrite(PWM_PIN, RPM_MAX) - return - -def updateFanSpeed(): - temp=getCPUTemp() - myPID.update(LOW_TEMP, temp) - #percentDiff = 45 + global rpmPulse, rpmChkStartTime + fanPulses = 2 + + duration = time.time() - rpmChkStartTime + frequency = rpmPulse / duration + ret = int(frequency * 60 / fanPulses) + rpmChkStartTime = time.time() + rpmPulse = 0 + print("Frequency {:3.2f} | RPM:{:4d}".format(frequency, ret)) + # with open('/tmp/adf-fanspeed', 'w') as f: + # f.write(str(ret)+'\n') + # f.close(); + return ret - if myPID.out < 0: - percentDiff = 0 - else: - percentDiff = myPID.out +def fanOn(): + wiringpi.pwmWrite(PWM_PIN, RPM_MAX) + return - with open('/tmp/adf-fanspeed', 'w') as f: - f.write(str(percentDiff)+'\n') - f.close(); - #percentDiff = 100-myPID.out - #diff=temp-lowTemp - #percentDiff = 0 - #if diff > 0: - # percentDiff=diff/percentTemp - pwmDuty=int(percentDiff * RPM_MAX / 100.0) +def updateFanSpeed(): + temp = getCPUTemp() + myPID.update(LOW_TEMP, temp) + # percentDiff = 45 + + if myPID.out < 0: + percentDiff = 0 + else: + percentDiff = myPID.out + + with open('/tmp/adf-fanspeed', 'w') as f: + f.write(str(percentDiff) + '\n') + f.close(); + + # percentDiff = 100-myPID.out + # diff=temp-lowTemp + # percentDiff = 0 + # if diff > 0: + # percentDiff=diff/percentTemp + pwmDuty = int(percentDiff * RPM_MAX / 100.0) + + print(myPID.out) + wiringpi.pwmWrite(PWM_PIN, pwmDuty) + # print("currTemp {:4.2f} tempDiff {:4.2f} percentDiff {:4.2f} pwmDuty {:5.0f}".format(temp, diff, percentDiff, pwmDuty)) + return - print(myPID.out) - wiringpi.pwmWrite(PWM_PIN, pwmDuty) - #print("currTemp {:4.2f} tempDiff {:4.2f} percentDiff {:4.2f} pwmDuty {:5.0f}".format(temp, diff, percentDiff, pwmDuty)) - return def setup(): - wiringpi.wiringPiSetupGpio() - #wiringpi.pinMode(pwmPin, 2) #HW PWM works on GPIO 12, 13, 18 and 19 on RPi4B - wiringpi.pinMode(PWM_PIN, wiringpi.PWM_OUTPUT) + wiringpi.wiringPiSetupGpio() + # wiringpi.pinMode(pwmPin, 2) #HW PWM works on GPIO 12, 13, 18 and 19 on RPi4B + wiringpi.pinMode(PWM_PIN, wiringpi.PWM_OUTPUT) + + wiringpi.pwmSetClock(768) # Set PWM divider of base clock 19.2Mhz to 25Khz (Intel's recommendation for PWM FANs) + wiringpi.pwmSetRange(RPM_MAX) # Range setted + + wiringpi.pwmWrite(PWM_PIN, RPM_MAX) # Setting to the max PWM + return - wiringpi.pwmSetClock(768) #Set PWM divider of base clock 19.2Mhz to 25Khz (Intel's recommendation for PWM FANs) - wiringpi.pwmSetRange(RPM_MAX) #Range setted - - wiringpi.pwmWrite(PWM_PIN, RPM_MAX) # Setting to the max PWM - return def main(): - print("PWM FAN control starting") - setup() - setupTacho() - #fanOn() - - while True: - try: - updateFanSpeed() - readRPM() - sleep(WAIT) - except KeyboardInterrupt: - fanOn() - break - except e: - print("Something went wrong") - print(e) - fanOn() + print("PWM FAN control starting") + setup() + setupTacho() + # fanOn() + + while True: + try: + updateFanSpeed() + readRPM() + sleep(WAIT) + except KeyboardInterrupt: + fanOn() + break + except e: + print("Something went wrong") + print(e) + fanOn() -if __name__ == "__main__": - main() +if __name__ == "__main__": + main() From 76dd27b36ebaa74826ba6ffd6bafb7587483969c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Gi=C4=8D?= Date: Sat, 25 May 2024 17:08:16 +0200 Subject: [PATCH 3/8] Add .idea to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b6e4761..6c49222 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# IDE's +.idea From 470e17998d22aaed31b8ee3e4014d0b54df83b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Gi=C4=8D?= Date: Sat, 25 May 2024 17:08:35 +0200 Subject: [PATCH 4/8] Remove commented-out code --- rpi-pwmfan.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/rpi-pwmfan.py b/rpi-pwmfan.py index 852335a..054235c 100755 --- a/rpi-pwmfan.py +++ b/rpi-pwmfan.py @@ -12,8 +12,6 @@ LOW_TEMP = 55 # Lowest temperature, if lowest of this, the FAN is Off WAIT = 2 # Interval before adjusting RPM (seconds) -PERCENT_TEMP = (MAX_TEMP - LOW_TEMP) / 100.0 - rpmChkStartTime = None rpmPulse = 0 @@ -92,7 +90,6 @@ def getCPUTemp(): def tachoISR(): global rpmPulse - # print("interruption!!!") rpmPulse += 1 return @@ -105,7 +102,6 @@ def setupTacho(): wiringpi.pinMode(TACHO_PIN, wiringpi.INPUT) wiringpi.pullUpDnControl(TACHO_PIN, wiringpi.PUD_UP) rpmChkStartTime = time.time() - # print("{:4d}".format(wiringpi.INT_EDGE_FALLING)) wiringpi.wiringPiISR(TACHO_PIN, wiringpi.INT_EDGE_FALLING, tachoISR) return @@ -120,9 +116,6 @@ def readRPM(): rpmChkStartTime = time.time() rpmPulse = 0 print("Frequency {:3.2f} | RPM:{:4d}".format(frequency, ret)) - # with open('/tmp/adf-fanspeed', 'w') as f: - # f.write(str(ret)+'\n') - # f.close(); return ret @@ -134,7 +127,6 @@ def fanOn(): def updateFanSpeed(): temp = getCPUTemp() myPID.update(LOW_TEMP, temp) - # percentDiff = 45 if myPID.out < 0: percentDiff = 0 @@ -145,22 +137,15 @@ def updateFanSpeed(): f.write(str(percentDiff) + '\n') f.close(); - # percentDiff = 100-myPID.out - # diff=temp-lowTemp - # percentDiff = 0 - # if diff > 0: - # percentDiff=diff/percentTemp pwmDuty = int(percentDiff * RPM_MAX / 100.0) print(myPID.out) wiringpi.pwmWrite(PWM_PIN, pwmDuty) - # print("currTemp {:4.2f} tempDiff {:4.2f} percentDiff {:4.2f} pwmDuty {:5.0f}".format(temp, diff, percentDiff, pwmDuty)) return def setup(): wiringpi.wiringPiSetupGpio() - # wiringpi.pinMode(pwmPin, 2) #HW PWM works on GPIO 12, 13, 18 and 19 on RPi4B wiringpi.pinMode(PWM_PIN, wiringpi.PWM_OUTPUT) wiringpi.pwmSetClock(768) # Set PWM divider of base clock 19.2Mhz to 25Khz (Intel's recommendation for PWM FANs) @@ -174,7 +159,6 @@ def main(): print("PWM FAN control starting") setup() setupTacho() - # fanOn() while True: try: From 8f17906e0889a73126d575f2fac42940ac3eb71a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Gi=C4=8D?= Date: Sat, 25 May 2024 18:44:36 +0200 Subject: [PATCH 5/8] Rewrite script using pigpio's HW PWM --- README.md | 1 - requirement.txt | 1 - requirements.txt | 1 + rpi-pwmfan.py | 241 ++++++++++++++++++++--------------------------- 4 files changed, 104 insertions(+), 140 deletions(-) delete mode 100644 requirement.txt create mode 100644 requirements.txt diff --git a/README.md b/README.md index 5acef0c..2c33aa9 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ Corporation September 2005, revision 1.3), to read the FAN speed using the tacho ## Dependencies * [Python 3](https://www.python.org/download/releases/3.0/) - The script interpreter -* [WiringPi-Python](https://github.com/WiringPi/WiringPi-Python) - Control Hardware features of Rasbberry Pi ## Documentations * [Noctua white paper](https://noctua.at/pub/media/wysiwyg/Noctua_PWM_specifications_white_paper.pdf) - Noctua PWM specifications white paper diff --git a/requirement.txt b/requirement.txt deleted file mode 100644 index de8ed7a..0000000 --- a/requirement.txt +++ /dev/null @@ -1 +0,0 @@ -wiringpi==2.60.0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4f03ea2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pigpio==1.78 diff --git a/rpi-pwmfan.py b/rpi-pwmfan.py index 054235c..b24b1e7 100755 --- a/rpi-pwmfan.py +++ b/rpi-pwmfan.py @@ -1,177 +1,142 @@ #!/usr/bin/python3 -import wiringpi as wiringpi import time -from time import sleep +import pigpio -PWM_PIN = 12 # HW PWM works on GPIO 12, 13, 18 and 19 on RPi4B +# Pin configuration +PWM_PIN = 12 # Pin to drive PWM fan - HW PWM works on GPIO 12, 13, 18 and 19 on RPi4B +TACH_PIN = 24 # Fan's tachometer output pin + +# Temperature thresholds +MAX_TEMP = 70 # [°C] Above this temperature, the fan is at max speed +MIN_TEMP = 45 # [°C] Above this temperature, the fan starts +OFF_TEMP = 40 # [°C] Below this temperature, the fan is off + +# Fan settings +PWM_FREQ = 25000 # [kHZ] Noctua Specs: Target_Frequency=25kHz +PULSE = 2 # Noctua Specs: Noctua fans put out two pulses per revolution + +# Fan speed settings RPM_MAX = 5000 # Noctua Specs: Max=5000 RPM_MIN = 1500 # Noctua Specs: Min=1000 -TACHO_PIN = 6 -MAX_TEMP = 60 # Above this temperature, the FAN is at max speed -LOW_TEMP = 55 # Lowest temperature, if lowest of this, the FAN is Off -WAIT = 2 # Interval before adjusting RPM (seconds) - -rpmChkStartTime = None -rpmPulse = 0 - -###PID Parameters### -KP = 2 -KI = 1 -KD = 1 -TAU = 1 -PID_MIN = 0 -PID_MAX = 100 - - -class PID_Controller: - def __init__(self, kp, ki, kd, tau, limMin, limMax): - self.kp = kp - self.ki = ki - self.kd = kd - self.tau = tau - self.limMin = limMin - self.limMax = limMax - self.time = WAIT - self.integrator = 0 - self.prevError = 0 - self.differentiator = 0 - self.prevMeasure = 0 - self.out = 0 - - def update(self, setpoint, measure): - error = setpoint - measure - # error=measure-setpoint - # Proportional gain - proportional = self.kp * error - # Integral gain - self.integrator = self.integrator + 0.5 * self.ki * self.time * (error + self.prevError) - # Anti-wind-up - if self.limMax > proportional: - intLimMax = self.limMax - proportional - else: - intLimMax = 0 - if self.limMin < proportional: - intLimMin = self.limMin - proportional - else: - intLimMin = 0 - # Clamp integrator - if self.integrator > intLimMax: - self.integrator = intLimMax - else: - self.integrator = intLimMin - # Differentiator gain - self.differentiator = (2 * self.kd * measure - self.prevMeasure) + ( - 2 * self.tau - self.time) * self.differentiator / (2 * self.tau + self.time) - # Calculate output - self.out = proportional + self.integrator + self.differentiator - # Apply limits - if self.out > self.limMax: - self.out = self.limMax - elif self.out < self.limMin: - self.out = self.limMin - # Store data - print(self.prevError) - self.prevError = error - print(self.prevError) - self.prevMeasure = measure - - -myPID = PID_Controller(KP, KI, KD, TAU, PID_MIN, PID_MAX) - - -def getCPUTemp(): - f = open('/sys/class/thermal/thermal_zone0/temp', 'r') - temp = f.readline() - f.close() - ret = float(temp) / 1000 - return ret - - -def tachoISR(): - global rpmPulse - rpmPulse += 1 - return +RPM_OFF = 0 +# Timing +WAIT = 2 # [s] Interval before adjusting RPM + +# Initialize pigpio +pi = pigpio.pi() + +# Remember pin modes +orig_pwm_pin_mode = -1 +orig_tach_pin_mode = -1 + +# Global variables +t = time.time() +rpm_pulse = 0 +tach_pin_callback = None + + +def getCpuTemperature(): + with open('/sys/class/thermal/thermal_zone0/temp') as f: + return float(f.read()) / 1000 -def setupTacho(): - global rpmChkStartTime - - print("Setting up Tacho input pin") - wiringpi.wiringPiSetupGpio() - wiringpi.pinMode(TACHO_PIN, wiringpi.INPUT) - wiringpi.pullUpDnControl(TACHO_PIN, wiringpi.PUD_UP) - rpmChkStartTime = time.time() - wiringpi.wiringPiISR(TACHO_PIN, wiringpi.INT_EDGE_FALLING, tachoISR) - return +def handleTachometerPulse(gpio, level, tick): + """Handle the interrupt generated by the falling edge of the tachometer pulse.""" + global rpm_pulse + rpm_pulse += 1 # Increment pulse count when a pulse is detected -def readRPM(): - global rpmPulse, rpmChkStartTime - fanPulses = 2 + +def getFanRPM(): + global t, rpm_pulse + + dt = time.time() - t + if dt < 0.002: + return # Reject spuriously short pulses + + frequency = rpm_pulse / dt + rpm = (frequency / PULSE) * 60 - duration = time.time() - rpmChkStartTime - frequency = rpmPulse / duration - ret = int(frequency * 60 / fanPulses) - rpmChkStartTime = time.time() - rpmPulse = 0 - print("Frequency {:3.2f} | RPM:{:4d}".format(frequency, ret)) - return ret + # Reset pulse counter and timestamp + rpm_pulse = 0 + t = time.time() + + return rpm -def fanOn(): - wiringpi.pwmWrite(PWM_PIN, RPM_MAX) - return +def setFanRPM(rpm): + duty_cycle = int((rpm / RPM_MAX) * 1000000) + pi.hardware_PWM(PWM_PIN, PWM_FREQ, duty_cycle) -def updateFanSpeed(): - temp = getCPUTemp() - myPID.update(LOW_TEMP, temp) +def setup(): + global orig_pwm_pin_mode, orig_tach_pin_mode, tach_pin_callback - if myPID.out < 0: - percentDiff = 0 - else: - percentDiff = myPID.out + print("Setting up...") - with open('/tmp/adf-fanspeed', 'w') as f: - f.write(str(percentDiff) + '\n') - f.close(); + orig_pwm_pin_mode = pi.get_mode(PWM_PIN) + orig_tach_pin_mode = pi.get_mode(TACH_PIN) - pwmDuty = int(percentDiff * RPM_MAX / 100.0) + # Set pin modes + pi.set_mode(PWM_PIN, pigpio.ALT5) # ALT5 mode for hardware PWM + pi.set_mode(TACH_PIN, pigpio.INPUT) + + # Add event to detect tachometer pulse + tach_pin_callback = pi.callback(TACH_PIN, pigpio.FALLING_EDGE, handleTachometerPulse) + + setFanRPM(RPM_OFF) # Set fan speed to off initially - print(myPID.out) - wiringpi.pwmWrite(PWM_PIN, pwmDuty) return -def setup(): - wiringpi.wiringPiSetupGpio() - wiringpi.pinMode(PWM_PIN, wiringpi.PWM_OUTPUT) +def cleanup(): + # Turn off the fan + setFanRPM(RPM_OFF) + + # Pin mode cleanup + if orig_pwm_pin_mode != -1: + pi.set_mode(PWM_PIN, orig_pwm_pin_mode) + + if orig_tach_pin_mode != -1: + pi.set_mode(TACH_PIN, orig_tach_pin_mode) - wiringpi.pwmSetClock(768) # Set PWM divider of base clock 19.2Mhz to 25Khz (Intel's recommendation for PWM FANs) - wiringpi.pwmSetRange(RPM_MAX) # Range setted + if tach_pin_callback is not None: + tach_pin_callback.cancel() # Remove the callback associated with the tachometer pin - wiringpi.pwmWrite(PWM_PIN, RPM_MAX) # Setting to the max PWM + pi.stop() return def main(): - print("PWM FAN control starting") + print("PWM FAN control starting...") setup() - setupTacho() while True: try: - updateFanSpeed() - readRPM() - sleep(WAIT) + temp = getCpuTemperature() + print(f"CPU Temperature: {temp:.1f}") + + if temp >= MAX_TEMP: + setFanRPM(RPM_MAX) + elif temp >= MIN_TEMP: + delta = temp - MIN_TEMP + rpm = min(RPM_MAX, max(RPM_MIN, int(RPM_MIN + delta))) + setFanRPM(rpm) + elif temp < OFF_TEMP: + setFanRPM(RPM_OFF) + + rpm = getFanRPM() + print(f"Fan RPM: {rpm:.2f}") + time.sleep(WAIT) + except KeyboardInterrupt: - fanOn() + cleanup() break - except e: + except Exception as e: print("Something went wrong") print(e) - fanOn() + cleanup() if __name__ == "__main__": From c091e61618dd5a18ee0f2574f49f169ecd85a8f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Gi=C4=8D?= Date: Sat, 25 May 2024 18:45:35 +0200 Subject: [PATCH 6/8] Add option to run the script as a systemd service --- README.md | 18 +++++++++++++++++- rpi-pwm-fan-control.service | 9 +++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 rpi-pwm-fan-control.service diff --git a/README.md b/README.md index 2c33aa9..038818b 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,28 @@ Corporation September 2005, revision 1.3), to read the FAN speed using the tacho ## How to use ### Get repository -```sh +```shell $ git clone git@github.com:alexfukahori/rpi-pwm-fan-control.git $ cd rpi-pwm-fan-control $ pip3 install -r requirements.txt +``` + +### Run the script +```shell $ python3 ./rpi-pwmfan.py ``` +### Use the script as a background service +Please edit the `rpi-pwm-fan-control.service` file and replace the `absolute_path_to_this_repo` with your path. +E.g.: `ExecStart=python3 /home/pi/scripts/rpi-pwm-fan-control/rpi-pwmfan.py` + +Then run following commands: + +```shell +$ sudo cp rpi-pwm-fan-control.service /etc/systemd/system +$ sudo systemctl daemon-reload +$ sudo systemctl enable rpi-pwm-fan-control +``` + ## TODO List * Accept suggestion! ;-) diff --git a/rpi-pwm-fan-control.service b/rpi-pwm-fan-control.service new file mode 100644 index 0000000..07de751 --- /dev/null +++ b/rpi-pwm-fan-control.service @@ -0,0 +1,9 @@ +[Unit] +Description=Service for RPi PWM Fan Control + +[Service] +ExecStart=python3 //rpi-pwm-fan-control/rpi-pwmfan.py +Restart=on-failure + +[Install] +WantedBy=default.target From ed44ceb508404b35ba253645c559d415f49cfba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Gi=C4=8D?= Date: Sat, 25 May 2024 18:51:08 +0200 Subject: [PATCH 7/8] Add documentation about setting the GPIO pins --- README.md | 3 +++ rpi-pwmfan.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 038818b..32e77d0 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,9 @@ $ cd rpi-pwm-fan-control $ pip3 install -r requirements.txt ``` +### Set GPIO pins +Edit the `PWM_PIN` and `TACH_PIN` values in [rpi-pwmfan.py](./rpi-pwmfan.py) to match the pins used by your Pi. + ### Run the script ```shell $ python3 ./rpi-pwmfan.py diff --git a/rpi-pwmfan.py b/rpi-pwmfan.py index b24b1e7..1ddda8c 100755 --- a/rpi-pwmfan.py +++ b/rpi-pwmfan.py @@ -5,7 +5,7 @@ # Pin configuration PWM_PIN = 12 # Pin to drive PWM fan - HW PWM works on GPIO 12, 13, 18 and 19 on RPi4B -TACH_PIN = 24 # Fan's tachometer output pin +TACH_PIN = 6 # Fan's tachometer output pin # Temperature thresholds MAX_TEMP = 70 # [°C] Above this temperature, the fan is at max speed From 88ae8bb01656294392af699b2dcceb7d8212006f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Gi=C4=8D?= Date: Sat, 25 May 2024 19:57:16 +0200 Subject: [PATCH 8/8] Remove dollar signs from documentation to easy the copying --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 32e77d0..bae2a37 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ Corporation September 2005, revision 1.3), to read the FAN speed using the tacho ## How to use ### Get repository ```shell -$ git clone git@github.com:alexfukahori/rpi-pwm-fan-control.git -$ cd rpi-pwm-fan-control -$ pip3 install -r requirements.txt +git clone git@github.com:alexfukahori/rpi-pwm-fan-control.git +cd rpi-pwm-fan-control +pip3 install -r requirements.txt ``` ### Set GPIO pins @@ -29,7 +29,7 @@ Edit the `PWM_PIN` and `TACH_PIN` values in [rpi-pwmfan.py](./rpi-pwmfan.py) to ### Run the script ```shell -$ python3 ./rpi-pwmfan.py +python3 ./rpi-pwmfan.py ``` ### Use the script as a background service @@ -39,9 +39,9 @@ E.g.: `ExecStart=python3 /home/pi/scripts/rpi-pwm-fan-control/rpi-pwmfan.py` Then run following commands: ```shell -$ sudo cp rpi-pwm-fan-control.service /etc/systemd/system -$ sudo systemctl daemon-reload -$ sudo systemctl enable rpi-pwm-fan-control +sudo cp rpi-pwm-fan-control.service /etc/systemd/system +sudo systemctl daemon-reload +sudo systemctl enable rpi-pwm-fan-control ``` ## TODO List