Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite to pigpio instead of deprecated wiringpy #5

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ dmypy.json

# Pyre type checker
.pyre/

# IDE's
.idea
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,36 @@ 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

## How to use
### Get repository
```sh
$ git clone [email protected]:alexfukahori/rpi-pwm-fan-control.git
$ cd rpi-pwm-fan-control
$ pip3 install -r requirements.txt
$ python3 ./rpi-pwmfan.py
```shell
git clone [email protected]:alexfukahori/rpi-pwm-fan-control.git
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
```

### 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
Expand Down
1 change: 0 additions & 1 deletion requirement.txt

This file was deleted.

1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pigpio==1.78
9 changes: 9 additions & 0 deletions rpi-pwm-fan-control.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[Unit]
Description=Service for RPi PWM Fan Control

[Service]
ExecStart=python3 /<absolute_path_to_this_repo>/rpi-pwm-fan-control/rpi-pwmfan.py
Restart=on-failure

[Install]
WantedBy=default.target
306 changes: 133 additions & 173 deletions rpi-pwmfan.py
Original file line number Diff line number Diff line change
@@ -1,183 +1,143 @@
#!/usr/bin/python3

import wiringpi as wiringpi
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

percentTemp = (maxTemp-lowTemp)/100.0

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=check_sec
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
#print("interruption!!!")
rpmPulse+=1
return

def setupTacho():
global rpmChkStartTime

print("Setting up Tacho input pin")
wiringpi.wiringPiSetupGpio()
wiringpi.pinMode(tachoPin,wiringpi.INPUT)
wiringpi.pullUpDnControl(tachoPin,wiringpi.PUD_UP)
rpmChkStartTime=time.time()
#print("{:4d}".format(wiringpi.INT_EDGE_FALLING))
wiringpi.wiringPiISR(tachoPin,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(pwmPin,pwmRange)
return

def updateFanSpeed():
temp=getCPUTemp()
myPID.update(lowTemp,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*pwmRange/100.0)

print(myPID.out)
wiringpi.pwmWrite(pwmPin, pwmDuty)
#print("currTemp {:4.2f} tempDiff {:4.2f} percentDiff {:4.2f} pwmDuty {:5.0f}".format(temp, diff, percentDiff, pwmDuty))
return
import pigpio

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)
# Pin configuration
PWM_PIN = 12 # Pin to drive PWM fan - HW PWM works on GPIO 12, 13, 18 and 19 on RPi4B
TACH_PIN = 6 # 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
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

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.pwmWrite(pwmPin, pwmRange) # Setting to the max PWM
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 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

# Reset pulse counter and timestamp
rpm_pulse = 0
t = time.time()

return rpm


def setFanRPM(rpm):
duty_cycle = int((rpm / RPM_MAX) * 1000000)
pi.hardware_PWM(PWM_PIN, PWM_FREQ, duty_cycle)


def setup():
global orig_pwm_pin_mode, orig_tach_pin_mode, tach_pin_callback

print("Setting up...")

orig_pwm_pin_mode = pi.get_mode(PWM_PIN)
orig_tach_pin_mode = pi.get_mode(TACH_PIN)

# 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

return


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)

if tach_pin_callback is not None:
tach_pin_callback.cancel() # Remove the callback associated with the tachometer pin

pi.stop()
return


def main():
print("PWM FAN control starting")
setup()
setupTacho()
#fanOn()

while True:
try:
updateFanSpeed()
readRPM()
sleep(check_sec)
except KeyboardInterrupt:
fanOn()
break
except e:
print("Something went wrong")
print(e)
fanOn()
print("PWM FAN control starting...")
setup()

while True:
try:
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:
cleanup()
break
except Exception as e:
print("Something went wrong")
print(e)
cleanup()

if __name__ == "__main__":
main()

if __name__ == "__main__":
main()