图 1

先前为树莓派淘了个好看又便宜的外壳,11块包邮还送了个简易小风扇。小风扇上岗之后就开始不眠不休的全速狂转,最后可能是磨损严重,嘎嘎响,只能淘汰掉换新的。

兼顾 变速、颜值、价格,新风扇选了 25块不包邮的 Argon Mini Fan

硬件安装

按照说明书图示,断电,依次 垫上包装内附送的导热硅胶、插入预留针脚即可。通电,开关拨到 ON 测试硬件没问题就可拨到 PWM (pulse width controllable mode)。 图 3

软件配置

Raspberry Pi OS 系统 - 官方驱动

官方系统软件配置 :

The instructions below will start the Mini Fan at CPU Temp 55 degrees (tempt 55000). You may set your desired fan initiation temperature as per your requirements:

  1. In Raspberry Pi OS, open a new terminal window

  2. Enter sudo nano /boot/config.txt

  3. Add the following line:

    dtoverlay=gpio-fan,gpiopin=18,temp=55000

  4. Save changes and exit by pressing Ctrl+X

Ubuntu 系统 - Python脚本

Google 一番,原理大概是:

常开一个监测程序,根据不同的 CPU 温度,gpio 输出不同的档位的电信号,从而控制风扇转速。从上方官方配置中可知,风扇连接的 gpio 控制针脚为 18 。

预设 温度达到 45度 风扇慢转,温度达到 55度 风扇满转。分步调试如下:

  1. 安装依赖 pip install RPi.GPIO

  2. 单步验证 ,在Python终端交互模式下依次执行:

    import RPi.GPIO as GPIO
    GPIO.setwarnings(False)
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(18, GPIO.OUT)
    fan = GPIO.PWM(18,50)
    fan.start(100) # 风扇开转
    fan.stop() # 风扇挺转
    
  3. 功能定制: 单步验证通过后,便可开始展开调试 定时读取温度、循环判断等逻辑。github 上也有大量的现成代码,挑选个最近比较新的为模版魔改定制(试过两个star多的旧项目不兼容)。

    结合下一节系统服务管理模块,配置 开机自启、通过服务名启动、停止等。但是调试时发现通过systemctl stop fan停掉服务时,python进程已经没了,风扇却还在转。应该是服务stop时,进程直接被杀掉了,PWM端口的高电位未被重置。Google一番,确实。如下是魔改后的完整代码,引入了 signal ,对关停信号做出响应:

/apps/gpio/fan.py

展开/折叠 代码 :
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import RPi.GPIO as GPIO ### https://pypi.org/project/RPi.GPIO/
import time
import signal #### 处理 systemmd stop https://stackoverflow.com/questions/18499497/how-to-process-sigterm-signal-gracefully
import sys
import os
import atexit

# Configuration
FAN_PIN = 18            # BCM pin used to drive PWM fan
WAIT_TIME = 10          # [s] Time to wait between each refresh
PWM_FREQ = 50           # [kHz] 25kHz for Noctua PWM control

# Configurable temperature and fan speed
MIN_TEMP = 45
MIN_TEMP_DEAD_BAND = 5
MAX_TEMP = 55
FAN_LOW = 1
FAN_HIGH = 100
FAN_OFF = 0
FAN_MAX = 100

# Variable definition
outside_dead_band_higher = True

# Get CPU's temperature
def getCpuTemperature():
    res = os.popen('cat /sys/class/thermal/thermal_zone0/temp').readline()
    temp = float(res)/1000
    #print("temp is {0}".format(temp)) # Uncomment for testing
    return temp

# Set fan speed
def setFanSpeed(speed):
    fan.start(speed)
    return()

# Handle fan speed
def handleFanSpeed(temperature, outside_dead_band_higher):
    # Turn off the fan if lower than lower dead band 
    if outside_dead_band_higher == False:
        setFanSpeed(FAN_OFF)
        #print("Fan OFF") # Uncomment for testing
        return
    # Run fan at calculated speed if being in or above dead zone not having passed lower dead band    
    elif outside_dead_band_higher == True and temperature < MAX_TEMP:
        step = float(FAN_HIGH - FAN_LOW)/float(MAX_TEMP - MIN_TEMP)  
        temperature -= MIN_TEMP
        setFanSpeed(FAN_LOW + ( round(temperature) * step ))
        #print(FAN_LOW + ( round(temperature) * step )) # Uncomment for testing
        return
    # Set fan speed to MAXIMUM if the temperature is above MAX_TEMP
    elif temperature > MAX_TEMP:
        setFanSpeed(FAN_MAX)
        #print("Fan MAX") # Uncomment for testing
        return
    else:
        return

# Handle dead zone bool
def handleDeadZone(temperature):
    if temperature > (MIN_TEMP + MIN_TEMP_DEAD_BAND/2):
        return True
    elif temperature < (MIN_TEMP - MIN_TEMP_DEAD_BAND/2):
        return False

# Reset fan to 100% by cleaning GPIO ports
def resetFan():
    GPIO.cleanup() # resets all GPIO ports used by this function
### systemmd stop
class GracefulKiller:
  kill_now = False
  def __init__(self):
    signal.signal(signal.SIGINT, self.exit_gracefully)
    signal.signal(signal.SIGTERM, self.exit_gracefully)

  def exit_gracefully(self, *args):
    self.kill_now = True

###
try:
    # Setup GPIO pin
    GPIO.setwarnings(False)
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(FAN_PIN, GPIO.OUT, initial=GPIO.LOW)
    fan = GPIO.PWM(FAN_PIN,PWM_FREQ)
    # setFanSpeed(FAN_OFF)
    # Handle fan speed every WAIT_TIME sec
    killer = GracefulKiller() ###
    while not killer.kill_now:
        temp = float(getCpuTemperature())
        outside_dead_band_higher = handleDeadZone(temp)
        handleFanSpeed(temp, outside_dead_band_higher)
        time.sleep(WAIT_TIME)
    resetFan()
except KeyboardInterrupt: # trap a CTRL+C keyboard interrupt
    resetFan()

atexit.register(resetFan)

服务

接上一节,为实现 通过服务名启停风扇,定义如下服务:

/usr/lib/systemd/system/fan.service

[Unit]
Description=Fan
#Documentation=https://blog.driftking.tw/2019/11/Using-Raspberry-Pi-to-Control-a-PWM-Fan-and-Monitor-its-Speed/
#Documentation=https://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-part-two.html
# After=
# Requires=

[Service]
Type=simple
User=root
Group=root
ExecStart=/apps/gpio/fan.py
ExecReload=/apps/gpio/fan.py
KillMode=mixed

[Install]
WantedBy=multi-user.target

[Service]服务模块里ExecStartExecReload指定上一节的Python脚本绝对地址,重点在KillMode需指定为 mixed ,这样在服务 stop 时,不会直接 kill 进程,而是通知 进程,从而在 Python 脚本内重置PWM针脚电位,正常关停风扇。systemd 更多配置详情可参考 阮一峰老师的 systemd 入门教程:实战篇

最终效果就是通过如下命令来便捷操作风扇服务了:

systemctl start fan # 开启
systemctl stop fan # 停止
systemctl enable fan # 开机自启
systemctl disable fan # 禁用开机自启

总结

  • Python 语法已经忘得差不多了,主要是思路理清,语法照猫画虎。

  • 单步验证通很重要,最初尝试其他gpio库时没做验证也是走了一些弯路。

  • 按需开风扇好处多多:安静、省电、减少风扇损耗,还能根据风扇声音估计判断系统负载(比如风扇一旦狂转估计是 jellyfin 在转码 或 Mac SMB 在备份)。

后续

备忘:

Ubuntu 21.04 默认的 gcc 版本为 10

部分旧语法的C代码可能会报错,安装 指定版本的 gcc:

sudo apt install gcc-8 g++-8 gcc-9 g++-9 gcc-10 g++-10

使用指定版本的 gcc 编译:

gcc-9 -Wall -o pwmfan pwmfan.c -lwiringPi -lcrypt -lm -lrt -lpthread