年初购入一个树莓派 4B后,立刻遇到了散热问题,毕竟性能提升和大内存都是有代价的。散热如果不解决好,降频就很难用了。不过好在现在风扇价格都很便宜,尝试使用开放四面的层叠板外壳 + 4mm 直流风扇后发现,风扇本身还算静音,但是外壳会有轻微共振,在夜间尤其吵闹。于是自然想到通过芯片温度来控制风扇启动。

硬件 & 原理

普通直流风扇是不支持通过信号触发启动、关断的,所以我们要做的就是加上一个简单的电子开关。材料:

  • 5V 直流风扇一个,3mm 或 4mm,根据你机器外壳的开孔尺寸选择。注意最好买透明外壳的,有部分劣质塑料外壳会散发强烈刺激性气味。
  • 2N2222 三极管一个
  • 680Ω 电阻一个
  • 一小块 PCB 和三孔(2.54mm 间距)的插座,用于焊接上面那些元件,并方便接入树莓派的 GPIO 引脚。如果没有的话,自己动脑想办法咯。

按下图连接,或者参考这一段

电路

基本原理就是,当将引脚 1 拉高时,三极管 Q1 会导通,风扇开始运转;反之则三极管断开,风扇停止。

因为我选择的是 GPIO 14 引脚作为控制线,相邻的 5V 和 GND 作为电源,因此后续代码都使用这个配置。

Python 版本

按照温度来控制一个 GPIO 引脚的高低是一个比较简单的工作,因此我最开始就是花两分钟写了个 Python 脚本然后安装成 systemd 服务来跑的。脚本大致是这样子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# coding: utf-8
from atexit import register
import sys
from threading import Event

from gpiozero import OutputDevice
from loguru import logger

from vcgencmd import measure_temp

THRESHOLD_ON = 60
THRESHOLD_OFF = 45
INTERVAL_SLEEP = 5
PIN_FAN = 14


if __name__ == '__main__':
    wakeup_event = Event()
    fan = OutputDevice(PIN_FAN)
    register(fan.close)

    # Init logger
    logger.remove()
    logger.add(
        sys.stdout,
        format='<green>{time:YYYY-MM-DD HH:mm:ss}</green> <level>{level: <8}</level> <level>{message}</level>',
        level='INFO',
    )

    while 1:
        temp = measure_temp()
        if temp > THRESHOLD_ON and not fan.is_active:
            logger.info('Temp={}°C, fan start', temp)
            fan.on()

        elif fan.is_active and temp < THRESHOLD_OFF:
            logger.info('Temp={}°C, fan stop', temp)
            fan.off()

        wakeup_event.wait(timeout=INTERVAL_SLEEP)

使用 pip3 安装 gpiozero / loguru / vcgencmd,然后直接运行这个脚本即可。

因为去年8月 Wiring Pi 作者宣布项目不再维护,所以这里没有使用 Wiring Pi 的 binding,而是使用的 gpiozero。此外,这里读取温度使用了 vcgencmd,它是通过开启子进程调用 vcgencmd measure_temp 来查询芯片温度的,这是我偏好的获取温度数据的方式。

不过这样一个简单的工作,交给 Python 脚本来做,需要占用 15MB 内存,空闲 CPU 占用 0.1%;并且通过子进程调用外部命令来查询温度十分不雅,因此我改为使用 C 编写相同的逻辑。

C 语言版本

源代码我发布在了这里。主要依赖 MRAA 库userland 仓库中与 VideoCore 芯片通信相关的内容。构建依赖 cmake 和 gcc。VideoCore 相关库和头文件在 Raspbian 系统上预装在 /opt/vc,其他系统可能需要自行解决。

安装依赖

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 首先更新 apt 缓存
sudo apt update
# 安装构建相关的工具
sudo apt install build-essential gcc git
# 下载 MRAA 库代码
git clone https://github.com/eclipse/mraa.git
# 进入 MRAA 库代码所在目录
cd mraa
# 准备编译
mkdir build && cd build
# 生成 makefile
cmake ..
# 编译
make
# 安装编译后的库和工具
sudo make instalal
# 刷新库文件信息
sudo ldconfig -v

运行 mraa-gpio version,如果打印出 MRAA 版本号和树莓派的型号信息,则说明安装完成。

编译安装 fanctl 服务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 下载项目源代码
git clone https://github.com/JokerQyou/fanctl.git
# 进入源代码目录
cd fanctl
# 准备编译
mkdir build && cd build
# 生成 makefile
cmake ..
# 编译
make
# 安装为服务
sudo make install

注意:如果要使用不同的 GPIO 引脚来控制风扇,请在编译之前修改 fanctl.c 文件中 FAN_VCC_PIN 的值。你可以在 Pinout! 查阅各个引脚的编号,注意 MRAA 库使用的是物理编号。

配置和启动服务

安装后,工具会被安装在 /usr/local/bin/fanctl,systemd 服务配置在 /etc/systemd/system/fanctl.service。在服务配置中 [Service] 段内,通过环境变量 THRESHOLD_ONTHRESHOLD_OFF 可以配置风扇启动和停止的温度(以摄氏度计)。例如以下配置表示在温度超过 48 摄氏度时启动,当温度降至 38 摄氏度时停止。

1
2
3
4
5
6
[Service]
Type=idle
ExecStart=/usr/local/bin/fanctl
KillSignal=SIGINT
Environment=THRESHOLD_ON=48
Environment=THRESHOLD_OFF=38

修改完成后,需要重新加载 systemd 服务配置才能启动服务:

1
2
3
4
5
6
# 重新加载服务配置
sudo systemctl daemon-reload
# 启动服务
sudo systemctl start fanctl
# 查看实时的服务日志
sudo journalctl -u fanctl -f

如何读取温度

眼尖的读者可能已经发现了,Linux 下其实可以直接读取 /sys/class/thermal/thermal_zone0/temp 文件来获取当前 CPU 的温度。树莓派上 CPU 与 GPU 是同一颗芯片,并且这块芯片只有一个温度传感器,所以其实我们不需要使用 vcgencmd 命令,也不需要依赖 VideoCore 相关的库?

树莓派官方文档的一个角落中是这样解释的:

Due to the architecture of the SoCs used on the Raspberry Pi range, and the use of the upstream temperature monitoring code in the Raspberry Pi OS distribution, Linux-based temperature measurements can be inaccurate. There is a command that can provide an accurate and instantaneous reading of the current SoC temperature, as it communicates with the GPU directly:

vcgencmd measure_temp

vcgencmd 是与 VideoCore 芯片进行直接通信的工具,因此 vcgencmd measure 的结果是最精确的,Linux 通用的温度测量方法则可能不是那么精确。