Fan control speed control on Hackergadget board (CM5)?

hiyah folks.
Does anyone know how to control the fan speed on the new hackergadget mainboard?
Are there any scripts that can do it? sensors-detect doesn’t show anything.
maybe @vileer can comment?

The fan’s PWM control pin was connected to GPIO_3V3_PWM (pin 218).

Here is an AI-generated code for the Radxa CM5 that has been tested that works. Please ingore the Chinese comment.

#!/usr/bin/env python3
import os
import time
import threading
import select

# --- 配置区域 ---
GPIO_PWM = 125  # GPIO3_D5

# 温控配置 (温度: 占空比%)
# 格式: (阈值温度, 对应的风扇速度0-100)
TEMP_STEPS = [
    (40, 0),    # 低于40度停转
    (50, 30),   # 50度时 30%转速
    (60, 50),   # 60度时 50%转速
    (70, 80),   # 70度时 80%转速
    (80, 100)   # 80度以上全速
]

PWM_FREQ = 1000  # PWM频率 Hz (软件PWM建议不要太高,20-100Hz比较稳定,太高CPU占用大)
# 注意:普通4线风扇通常建议25kHz,但软件GPIO无法达到且稳定。
# 如果风扇有噪音,尝试将频率降低到 50 或 100。

# --- GPIO 工具函数 ---
def export_gpio(gpio):
    path = f"/sys/class/gpio/gpio{gpio}"
    if not os.path.exists(path):
        try:
            with open("/sys/class/gpio/export", "w") as f:
                f.write(str(gpio))
        except OSError:
            print(f"Warn: GPIO {gpio} already exported or busy.")

def set_direction(gpio, direction):
    # direction: "out" or "in"
    path = f"/sys/class/gpio/gpio{gpio}/direction"
    try:
        with open(path, "w") as f:
            f.write(direction)
    except OSError:
        pass

def set_edge(gpio, edge):
    # edge: "none", "rising", "falling", "both"
    path = f"/sys/class/gpio/gpio{gpio}/edge"
    try:
        with open(path, "w") as f:
            f.write(edge)
    except OSError:
        pass

# --- 功能类 ---

class FanController:
    def __init__(self):
        self.running = True
        self.current_duty = 0
        self.rpm = 0
        
        # 初始化GPIO
        export_gpio(GPIO_PWM)
        set_direction(GPIO_PWM, "out")
        

    def get_cpu_temp(self):
        try:
            with open("/sys/class/thermal/thermal_zone0/temp", "r") as f:
                return int(f.read()) / 1000.0
        except:
            return 50.0 # 默认安全值

    def pwm_loop(self):
        """软件PWM线程"""
        pwm_path = f"/sys/class/gpio/gpio{GPIO_PWM}/value"
        period = 1.0 / PWM_FREQ
        
        while self.running:
            duty = self.current_duty
            
            if duty >= 100:
                with open(pwm_path, "w") as f: f.write("1")
                time.sleep(0.1)
            elif duty <= 0:
                with open(pwm_path, "w") as f: f.write("0")
                time.sleep(0.1)
            else:
                on_time = period * (duty / 100.0)
                off_time = period - on_time
                # 简单的软件PWM循环
                try:
                    with open(pwm_path, "w") as f: f.write("1")
                    time.sleep(on_time)
                    with open(pwm_path, "w") as f: f.write("0")
                    time.sleep(off_time)
                except:
                    pass

    def update_speed_strategy(self):
        """根据温度更新目标转速"""
        temp = self.get_cpu_temp()
        target_duty = 0
        
        # 简单的阶梯控制
        for threshold, duty in TEMP_STEPS:
            if temp >= threshold:
                target_duty = duty
            else:
                break
        
        # 滞后处理(防止在临界点频繁跳动),这里简单直接赋值
        self.current_duty = target_duty
        return temp

    def start(self):
        t_pwm = threading.Thread(target=self.pwm_loop)
        
        t_pwm.start()
        
        print(f"Fan Control Started. PWM Pin: {GPIO_PWM}")
        
        try:
            while True:
                temp = self.update_speed_strategy()
                # 打印状态,使用 \r 覆盖当前行
                print(f"Temp: {temp:.1f}°C | Speed: {self.current_duty}%", end="\r")
                time.sleep(2)
        except KeyboardInterrupt:
            print("\nStopping...")
            self.running = False
            t_pwm.join()
            # 退出前关闭风扇或全开,视安全需求而定
            with open(f"/sys/class/gpio/gpio{GPIO_PWM}/value", "w") as f: f.write("0") 

if __name__ == "__main__":
    # 需要root权限运行
    if os.geteuid() != 0:
        print("Error: This script must be run as root (sudo).")
        exit(1)
        
    controller = FanController()
    controller.start()

Run it with sudo, and your fan will spin based on your CPU temperature.

4 Likes

thanks @vileer. i understand the comments in chinese and its not a problem :slight_smile:

However, when i try to run the code (root), pin 125 doesn’t seem to exist. Should i be changing that to 218? i tried either, but it doesn’t seem to make a difference.

Trying to manually export pin 125 or 218 also results in “write error: invalid argument”

if it helps, i’m on ubuntu. and my gpio directory shows the following.

export  gpiochip512  gpiochip527  gpiochip533  gpiochip565  gpiochip569  unexport

of all of these, only gpiochip569 has a different label of pinctrl-rp1. the others all have gpio-brcmstb@XXXXX

any thoughts?

Sorry, I thought you mentioned the Radxa CM5. On the Raspberry Pi CM5, you don’t need to do anything; the system controls the fan automatically. If you want to change the profile of it. Please check this link: https://raspberrypi.stackexchange.com/questions/145927/raspberry-pi-5-edit-modify-temperature-limit-for-the-active-cooler-fan.

2 Likes

Thank @vileer!

works great.
Added the following to my /boot/firmware/config.txt and it now maps the way i want it. Appreciate the quick response as always!

# PWM fan control
dtparam=fan_temp0=45000
dtparam=fan_temp0_hyst=1500
dtparam=fan_temp0_speed=75
dtparam=fan_temp1=53000
dtparam=fan_temp1_hyst=2500
dtparam=fan_temp1_speed=127
dtparam=fan_temp2=58000
dtparam=fan_temp2_hyst=3500
dtparam=fan_temp2_speed=191
dtparam=fan_temp3=63000
dtparam=fan_temp3_hyst=4500
dtparam=fan_temp3_speed=255
3 Likes