树莓派 PWM散热风扇控制 (2024)

发布于 2024-04-27  6304 次阅读


请不要直接使用GPIO接口来控制风扇,风扇电流过大,会烧坏你的树莓派!!

之前想着树莓派就是图一乐,所以就用了额定功率的小风扇,怕太吵接到树莓派3.7V的接口上来散热,后来,树莓派就变成7*24小时运行,为了不被室友打省电 ,我又花了二十大洋买了(也可以自己手搓,但是会比较难看)一个PWM调频风扇。

小心翼翼的拔下之前的散热片,之后充分利用了一节晚上的课,把亚克力的树莓派外壳磨了磨(买的PWM散热器稍大,放不进外壳里),将散热风扇接好,拨到ON挡位,已经在正常转了。之后拨到PWM档位,打开树莓派,命令行: sudo raspi-config ,选择 Performance Options ,可以在 P3 Fan Set behaviour of GPIO case fan 里设置风扇的引脚(我的是18号引脚),以及风扇的起转温度。风扇起转温度最低可以设置的是 60 °C,显然有些偏高,但是我们要想调低也不是没有办法,使用

sudo nano /boot/firmware/config.txt   # 在旧版本的树莓派中,此文件位置在 /boot/config.txt

将其中有一行

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

其中 temp= 后的部分即为设定的温度(千分),你可以将此温度改为更低的温度,比如 42000

但是树莓派自带的温度控制总感觉不是特别舒服,不可以自己手动调,温度控制的逻辑也不清楚,不喜欢这种转不转不知道随缘的感觉。还能怎么办?自己手搓呗。

中间遇见的坑

网上好多都是用Python写的,之前虽然浅浅的入门过Python,但是想着C++运行效率要更高,所以我选择使用C++来写。

我先把具体实现的框架写好,并且把一些函数完善了一下,在我尝试写操作GPIO来控制风扇的函数时,问题来了,我根据其他教程,直接尝试用命令行来启动: system("echo 1 > /sys/class/gpio/gpio18/value"); ,结果当运行时候发生了报错。我发现是因为没有暴露 GPIO ,于是我尝试

qaq@qwq:~/tempC $ echo 18 > /sys/class/gpio/export
-bash: echo: 写入错误:无效的参数

意识到问题,我放下了C++的继续编写,开始用命令行来尝试操作 GPIO,一直找不到合适的办法,扒拉树莓派的文档也没有扒拉出来(如果你找到了,万分感谢能为我指明)。想到C++的一些库可能可以进行操作,我满怀希望的重新接起C++的编程。好不容易从GitHub安装 wiringPi.h (树莓派官方源已经弃用了,而你能找到的大部分的教程都是用的这个库)。将该库插入到我的代码中后,满怀希望的运行,然后Client_loop: send disconnect: Broken pipe Error 我成功的与我的树莓派断联了(要命的是我还在床上),内心做了巨大的心里斗争后,我起来重启了树莓派,改了其中可能死循环的代码,之后再次运行,不出意外又断联了。除了SSH,所有联网的服务都寄了,困得实在不想下床就直接睡了。第二天经过逐行的测试,我发现只要设置pinMode ,(pinMode(18, OUTPUT);),编译运行后就会处于这种假死的状态(后来发现一些跟网络无关的服务还在运行,只是网络没了)。

还是选Python了

花了点时间,把 RPi.GPIO 装上,(树莓派安装Python3后还需要单独安装pip,而安装玩pip不能直接导入,需要创建虚拟环境,直接搞机械学习时候用过,直接按照提示做就行了),之后,很容易的,我的PWM风扇就转了起来。将写好的C++丢给Kimi,让他帮我转为Python,之后把那些函数小修小改一下,之后开始手搓main函数,写了大概4个小时,将想实现的功能都实现了(用之前写C++的思路,Python我是真刚入门),比如手动切换启停状态,运行时可直接修改的温度阈值和刷新状态的间隔时间,尽量减少对GPIO操作的优化,还有差强人意的交互界面。

代码我放这了

import RPi.GPIO as GPIO
import os
import time

#   转载请注明出处
#     uulin.cn 

fanOn = False
# 添加计数器,如果不需要删除以下两行,以及 controlFan 函数,还有 main 函数中的对应变量
turn_on = 0
turn_off = 0
  
# 定义默认温度
defaultTemp = 45
# 定义默认监控间隔
defaultTime = 5

# 获取目标温度
def getWantTemp():
    try:
        with open("TempSet.txt", "r") as setFile:
            wantTemp = int(setFile.read().strip())
    except (FileNotFoundError, IOError, ValueError):
        with open("TempSet.txt", "w") as setFile:
            setFile.write(str(defaultTemp))
        print("没有设置文件,创建一个!")
        wantTemp = defaultTemp
    except Exception as e:
        print(f"文件无法写入!将使用默认值 {defaultTemp}")
        wantTemp = defaultTemp
    return wantTemp

# 获取温控延时
def getWantTime():
    try:
        with open("TimeSet.txt", "r") as setFile:
            wantTime = int(setFile.read().strip())
    except (FileNotFoundError, IOError, ValueError):
        with open("TimeSet.txt", "w") as setFile:
            setFile.write(str(defaultTime))
        print("没有设置文件,创建一个!")
        wantTime = defaultTime
    except Exception as e:
        print(f"文件无法写入!将使用默认值 {defaultTime}")
        wantTime = defaultTime
    return wantTime

# 控制风扇
def controlFan(on):
    global fanOn,turn_on,turn_off
    fanOn = GPIO.input(18)
    print(f"{on} {fanOn}")
    if fanOn != on:
        print(f"尝试将风扇状态更改为: {on}", end='')
        GPIO.output(18, on)
        fanOn = GPIO.input(18)  # 更新fanOn的状态
        if on == True:        # 从此向下四行为计数器内容
            turn_on+=1
        else:
            turn_off+=1
        print(f",完成,当前{fanOn}")
        

# 获取CPU温度
def getTemp():
    with open("/sys/class/thermal/thermal_zone0/temp", "r") as tempFile:
        temp = int(tempFile.read().strip())
    return temp / 1000

# GPIO设置
def setup_gpio():
    GPIO.setwarnings(False)
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(18, GPIO.OUT)


# 清理GPIO设置
def cleanup_gpio():
    GPIO.cleanup()

# 主程序
def main():
    setup_gpio()
    desired_temp = getWantTemp()
    current_temp =  getTemp()
    sleep_time = getWantTime()
    fanOn = GPIO.input(18)
    print("初始化完成,启用对风扇的自动控制,使用^C进入菜单")
    
    showMenu = False
    try:
       while True:
          if showMenu is not False:
             # 显示菜单
             current_temp =  getTemp()
             os.system('cls' if os.name == 'nt' else 'clear')
             print(f"当前风扇状态: {GPIO.input(18)},当前温度:{getWantTemp()}")
             print(f"开启: {turn_on}次,关闭:{turn_off}次")
             print("\ton:临时打开风扇\n\toff:临时关闭风扇\n\tea:启动对风扇的自动控制\n\t^C:关闭对风扇的自动控制\n\texit/^C:退出程序")
             user_input = input("请输入命令 (on/off/ea/exit): ").strip().lower()
             if user_input == 'on':
                  controlFan(True)
                  print(f"风扇暂时开启,当前温度: {current_temp}℃")
                  time.sleep(3)
                  continue
             elif user_input == 'off':
                  controlFan(False)
                  print(f"风扇暂时关闭,当前温度: {current_temp}℃")
                  time.sleep(3)
                  continue
             elif user_input == 'ea':
                  print("风扇控制已开启。")
             elif user_input == 'exit':
                  print("程序将退出。")
                  break
             else:
                  print("无效的命令,请输入 'start'、'stop' 或 'exit'。")
                  time.sleep(1)
                  continue

            # 如果风扇控制激活,根据温度控制风扇
          while True:
             try:
                desired_temp = getWantTemp()  # 为了能够实现实时的温度还有间隔时间的变化,此处两行将会在每次刷新时调用函数读取
                sleep_time = getWantTime()  #  TimeSet.txt 、TempSet.txt 文件中的内容,这可能对SD卡的寿命造成影响,你可以将这两行注释掉
                current_temp =  getTemp()
                # fanOn = GPIO.input(18)
                # print("数据更新完成")
                if current_temp is not None:
                  if current_temp > desired_temp:
                          print(f"当前温度:{current_temp}℃ ,设定温度:{desired_temp}℃ ", end='')
                          controlFan(True)
                          print(",风扇开启")
                  elif current_temp < (desired_temp-5):
                          print(f"当前温度:{current_temp}℃ ,设定温度:{desired_temp}℃ ", end='')
                          controlFan(False)
                          print(f",风扇关闭")
                  else:
                          print(f"当前温度:{current_temp}℃ ,在暂缓区间{desired_temp}℃ -{(desired_temp-5)}℃ 内,当前风扇 {GPIO.input(18)}")
                time.sleep(sleep_time)  # 简短的延时,以避免频繁读取温度
             except KeyboardInterrupt:
               print("\n风扇控制已停止。")
               showMenu = 1
               time.sleep(1)
               break

    except KeyboardInterrupt:
        print("\n程序被用户中断。")
        cleanup_gpio()
    

if __name__ == "__main__":
    main()

你可以通过调整目录下的 TempSet.txt 和 TimeSet.txt 文件来设置阈值温度和间隔时间,默认情况下阈值和时间会随文件实时变化(代价时如果你使用SD卡,这样会降低SD卡的使用寿命 (每次检测一次温度分别读取两个文件一次),代码中相应部分有说明,你可以将其注释掉)(我外接的固态没用SD卡啊)。中间调试用的输出较多,如果你想要用 systemctl 来实现开机启动,建议将其注释掉,否则日志输出可能会很多。

顺便来压测压测树莓派

为了验证效果,随手用C++写了一个用来测试的程序,该程序会读取当前的温度,当CPU超过设置的温度(70摄氏度)候将会停止。使用 ^c 和 kill命令一样可以将其停止。

#include <iostream>
#include <fstream>

int getTemp()
{
	std::ifstream tempFile;
	tempFile.open("/sys/class/thermal/thermal_zone0/temp");
	int temp;
	tempFile >> temp;
	tempFile.close();
	temp/=1000;
	return temp;
}

int main(){
while(1){
if (getTemp()>=70)
return 0;
}
}

自己编译 or

curl -O https://cdn.uulin.cn/Script/stresstest
chmod +x stresstest
./stresstest&  ./stresstest& ./stresstest& ./stresstest&

# 如果需要手动终止
pkill -f ./stresstest

备注

尽管目前较为完善,但是我依然会在以后想到什么或者遇见什么问题时候更改我本地的代码,因为懒惰不一定会在此处更新,并不一定适合所有人。你可以随意更改这份代码,并在本地使用,但是当你要发布修改的代码时,请注明出处。