犀牛派 X1 驯板记:修复风扇啸叫与开机断网两个坑

犀牛派 X1 驯板记:修复风扇啸叫与开机断网两个坑

平台:犀牛派 X1(QCS8550 / kalama,6 核 aarch64,16G 内存,AidLux Ubuntu 22.04)
工具:一台 Linux PC、一根 USB 线(adb)、一颗不放弃的心
结论先行:两个问题的修复都只有几行配置/一个脚本,但找到它们的过程值得记录。

背景

犀牛派 X1 到手,QCS8550 的算力在这个价位很香(Hexagon NPU 约 45 TOPS,QNN SDK 预装),
但开箱就有两个影响体验的问题:

  1. 风扇啸叫:开机后风扇恒速运转,发出固定音调的嗡鸣,放在桌上非常恼人
  2. 开机断网:某次重启后,SSH、ping 全部不通,板子仿佛从网络上消失了

这两个问题一个是硬件驱动设计缺陷,一个是出厂网络配置与家用环境的冲突。
下面分别讲排查过程和修复方法,所有脚本附在文末。

问题一:重启后整机断网

症状

重启后 PC 无法 ping 通板子的 192.168.1.177,SSH 超时。
有线无线都连不上——对于一块打算 headless 使用的板子,这是致命的。

救命通道:adb

好在犀牛派的 USB 口直接提供 adb,而且 adbd 是 root 免授权(开发固件的好处):

adb shell   # 直接拿到 root shell

经验:玩开发板,永远先确认带外通道(adb/串口)可用,它是网络挂掉时唯一的入口。
PC 上如果 adb 提示 no permissions,补一条 udev 规则即可(脚本见文末)。

根因:br-lan 抢了网关的 IP

adb 进去看路由表,矛盾一目了然:

default via 192.168.1.1 dev wlan0            ← 默认网关是 192.168.1.1
192.168.1.0/24 dev br-lan src 192.168.1.1    ← br-lan 自己就是 192.168.1.1?!

板子有个网桥 br-lan(板载网口的 LAN 侧,路由器式设计),重启后它把自己配成了
192.168.1.1——和我家路由器的网关地址一模一样,ARP 冲突,整机网络瘫痪。

继续追:谁给 br-lan 配的这个地址?NetworkManager、netplan、init.d 全部排除,
最后在进程列表里发现:

ps -ef | grep dhcp
# udhcpd -S /etc/lan_wan_cfg/udhcpd.conf

顺藤摸瓜到 /etc/lan_wan_cfg/qcassdk.conf

lan_wan_mode=1
gateway_ip=192.168.1.1    # ← 元凶,开机初始化按它设置 br-lan

厂商做网口路由功能时假设"上游网络不会是 192.168.1.0/24"——
可这恰恰是全球家用路由器最普遍的网段。出厂默认值与家用环境必然冲突。

修复

运行时应急(立即恢复网络):

ip addr del 192.168.1.1/24 dev br-lan
ip addr add 192.168.100.1/24 dev br-lan

持久化(改厂商配置,把 LAN 侧网段挪到 192.168.100.0/24):

cp /etc/lan_wan_cfg/qcassdk.conf /etc/lan_wan_cfg/qcassdk.conf.bak
cp /etc/lan_wan_cfg/udhcpd.conf /etc/lan_wan_cfg/udhcpd.conf.bak
sed -i 's/gateway_ip=192.168.1.1/gateway_ip=192.168.100.1/' /etc/lan_wan_cfg/qcassdk.conf
sed -i 's/192\.168\.1\./192.168.100./g' /etc/lan_wan_cfg/udhcpd.conf

重启验证:br-lan 稳定在 192.168.100.1,WiFi/SSH 一切正常。
(完整应急脚本 fix-brlan-runtime.sh 见文末。)

问题二:风扇啸叫

症状与第一层排查

开机即转、恒速、固定音调。先看 Linux thermal 框架里风扇归谁管:

cat /sys/class/thermal/cooling_device7/type        # pwm-fan
cat /sys/class/thermal/cooling_device7/cur_state   # 1(共 0~5 档)

风扇绑定在 CPU 温度区(thermal_zone2, cpuss-1)上,触发温度为
35/60/70/80/90°C 对应 1~5 档。而待机温度就在 34~37°C——起转阈值 35°C
比待机温度还低,风扇当然永远常转。这解释了"恒速",但"啸叫"呢?

根因:64Hz 的软件 PWM

风扇驱动的参数在设备树里(/proc/device-tree/soc/pwm-fan/)。
注意 设备树是大端字节序,直接 od -t u4 读出来要做字节交换:

od -A n -t u4 /proc/device-tree/soc/pwm-fan/pwms
# 原始字节 00 EF 41 40 → 大端 0x00EF4140 = 15,655,232 ns ≈ 63.9Hz

PWM 载波频率约 64Hz——正好落在人耳最敏感的可闻频段。

继续深挖发现更有意思的事实:风扇的驱动线根本不在硬件 PWM 输出上,
而是接在 TLMM GPIO9,厂商用软件定时器翻转 GPIO(bit-bang)来模拟 PWM。
三条独立证据:

  1. 设备树里风扇引脚组 fan_pwm_activecompany_fan_gpio 都指向 GPIO9
  2. SoC TLMM 的引脚复用功能表里没有任何风扇可用的硬件 PWM 功能
  3. 63.9Hz 这个频率,PM8550 电源芯片的 LPG 硬件 PWM(19.2MHz 固定分频)合成不出来,
    只有软件定时器能产生这种"任意"频率

为什么 64Hz 会啸叫?每秒 64 次通断,电机绕组电流阶跃产生电磁振动,
转矩以 64Hz 脉动冲击转子,风扇内部驱动板的电容也被周期性大电流冲击
(压电效应发声)。基频 64Hz 加上一串谐波,全是可闻的。

实验验证:只有 0% 和 100% 占空比是干净的

理论预测:占空比两端没有开关动作,应该安静;中间档全是 64Hz 脉冲,必叫。
逐档实测(每档 8 秒,人耳验收):

档位 占空比 听感
0 0% 无声
1 20% 明显啸叫
2 40% 明显啸叫
3 60% 有啸叫
4 80% 有啸叫
5 100% 平稳风声,几乎无啸叫

完全吻合。中间档位在硬件层面无药可救(想静音需要 ≥20kHz 的硬件 PWM,
而 GPIO 翻转做不到 kHz 级),所以软件策略只剩一条路:
只用"停"和"满速"两个状态,做开关式(bang-bang)温控

修复:开关式温控服务

带迟滞的 bang-bang 控制:≥58°C 满速,≤50°C 停转,中间保持原状防抖。
脚本 x1-fanctl.sh 完整内容如下(也是本文的核心交付物):

#!/bin/sh
# RhinoPi X1 静音风扇控制(开关式)
# 背景:板载风扇由 ~64Hz 低频 PWM 斩波驱动,中间档位(1-4)啸叫明显,
# 只有 0(停)和 5(满速,恒定导通)安静。本服务采用带迟滞的开关控制。
# 安全:服务退出(含 systemctl stop)时恢复内核自动温控兜底。

ZONE=/sys/class/thermal/thermal_zone2        # cpuss-1
CDEV=/sys/class/thermal/cooling_device7      # pwm-fan
ON_TEMP=58000    # 满速阈值(毫摄氏度)
OFF_TEMP=50000   # 停转阈值
FULL=5

echo disabled > "$ZONE/mode"

restore() {
    echo enabled > "$ZONE/mode" 2>/dev/null
    exit 0
}
trap restore TERM INT

state=0
echo 0 > "$CDEV/cur_state"

while true; do
    t=$(cat "$ZONE/temp" 2>/dev/null)
    case "$t" in
        ''|*[!0-9]*) t=99999 ;;  # 读数异常按过热处理,风扇拉满
    esac
    if [ "$t" -ge "$ON_TEMP" ] && [ "$state" -ne "$FULL" ]; then
        state=$FULL
        echo $FULL > "$CDEV/cur_state"
    elif [ "$t" -le "$OFF_TEMP" ] && [ "$state" -ne 0 ]; then
        state=0
        echo 0 > "$CDEV/cur_state"
    fi
    sleep 2
done

配套 systemd 单元 x1-fanctl.service

[Unit]
Description=RhinoPi X1 quiet bang-bang fan control
After=multi-user.target

[Service]
Type=simple
ExecStart=/usr/local/bin/x1-fanctl.sh
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

部署:

sudo cp x1-fanctl.sh /usr/local/bin/ && sudo chmod +x /usr/local/bin/x1-fanctl.sh
sudo cp x1-fanctl.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now x1-fanctl.service

实测效果:待机 37~39°C 风扇全停、完全静音;6 核满载温度冲到 64.5°C 时
风扇满速介入(平稳风声),稳定压在 62°C 左右;负载撤走后温度回落自动停转。
重启后服务自启,行为一致。

安全性说明:服务停止时会自动把温控区交还内核(恢复厂商原策略);
温度读数异常时按过热处理拉满风扇;芯片硬件过热断电保护不受任何软件影响。

给厂商的建议(如果官方看到)

  1. 风扇:硬件改版把 PWM 控制线接到 PM8550 LPG 硬件 PWM 通道
    (注意 LPG 是电流阱输出,需上拉/电平转换),频率用行业标准 21~28kHz;
    现有硬件出货批次,建议固件采用开关式控制或把起转温度提到 45°C 以上。
    另外 pwm-fan 驱动的 remove 路径疑似泄漏 GPIO 请求(unbind 后 GPIO9 无法
    重新导出),请检查资源释放。
  2. 网络qcassdk.conf 的默认 gateway_ip 改成冷门网段,或开机时检测
    上联网段自动避让。192.168.1.0/24 是家用路由器的默认网段,这个冲突
    对每个家用用户都是必现的,且后果是整机失联。
  3. 文档:希望能提供引脚定义、风扇规格、lan_wan_cfg 配置说明。
    这次排查最大的成本是"无文档逆向"。

附:全部脚本

  • x1-fanctl.sh / x1-fanctl.service — 风扇开关式温控(正文已完整给出)
  • fix-brlan-runtime.sh — br-lan 运行时应急修复:
#!/bin/sh
# 犀牛派 X1 br-lan 抢网关 IP 应急修复(root 执行)
# 持久化修复:改 /etc/lan_wan_cfg/qcassdk.conf 和 udhcpd.conf(正文有命令)
ip addr del 192.168.1.1/24 dev br-lan 2>/dev/null
ip addr add 192.168.100.1/24 dev br-lan
ip link set br-lan up
  • 51-android-qualcomm.rules — PC 侧 adb 权限 udev 规则
    (放入 /etc/udev/rules.d/sudo udevadm control --reload && sudo udevadm trigger):
SUBSYSTEM=="usb", ATTR{idVendor}=="05c6", MODE="0666", GROUP="plugdev"

写在最后

这块板子的硬件素质(算力、散热余量、adb 开发体验)都对得起价格,
踩到的两个坑都属于"软件默认值没想周全"的范畴,修复成本都不高。
希望这篇文章能帮到同样入手犀牛派 X1 的朋友,也希望厂商下个固件版本
能把这两个默认值修掉。

有任何问题欢迎评论区交流。

2 个赞

哈哈你注意到细节了。我改了一版,你可以考虑使用哈哈

#!/bin/sh
# RhinoPi X1 静音风扇控制(开关式)
#
# 背景:板载风扇由 pwm-fan 驱动以 ~64Hz 低频 PWM 斩波驱动,
# 中间档位(1-4)会产生明显啸叫,只有 0(停)和 5(满速,恒定导通)安静。
# 因此本服务采用带迟滞的开关控制:>= ON_TEMP 满速,<= OFF_TEMP 停转。
#
# 安全:本服务退出(含 systemctl stop)时恢复内核自动温控兜底。

ZONE=/sys/class/thermal/thermal_zone2        # cpuss-1
CDEV=/sys/class/thermal/cooling_device7      # pwm-fan
ON_TEMP=58000    # 满速阈值(毫摄氏度)
OFF_TEMP=50000   # 停转阈值
FULL=5

# 接管前关闭内核对该温控区的自动调速(该区只绑定了 pwm-fan)
echo disabled > "$ZONE/mode"

restore() {
    echo enabled > "$ZONE/mode" 2>/dev/null
    exit 0
}
trap restore TERM INT

state=0
echo 0 > "$CDEV/cur_state"

while true; do
    t=$(cat "$ZONE/temp" 2>/dev/null)
    t=`echo "scale=2; $t / 1000.0" |bc`
    echo -e "\e[32;40m 当前温度\e[0m\e[33;40m: $t\e[0m\e[32;40m摄氏度\e[0m"
    case "$t" in
        ''|*[!0-9]*) t=99999 ;;  # 读数异常时按过热处理,风扇拉满
    esac
    if [ "$t" -ge "$ON_TEMP" ] && [ "$state" -ne "$FULL" ]; then
        state=$FULL
        echo $FULL > "$CDEV/cur_state"
    elif [ "$t" -le "$OFF_TEMP" ] && [ "$state" -ne 0 ]; then
        state=0
        echo 0 > "$CDEV/cur_state"
    fi
    sleep 2
done

2 个赞

修复版本:

#!/bin/sh
# RhinoPi X1 静音风扇控制(开关式)
#
# 背景:板载风扇由 pwm-fan 驱动以 ~64Hz 低频 PWM 斩波驱动,
# 中间档位(1-4)会产生明显啸叫,只有 0(停)和 5(满速,恒定导通)安静。
# 因此本服务采用带迟滞的开关控制:>= ON_TEMP 满速,<= OFF_TEMP 停转。
#
# 安全:本服务退出(含 systemctl stop)时恢复内核自动温控兜底。

ZONE=/sys/class/thermal/thermal_zone2        # cpuss-1
CDEV=/sys/class/thermal/cooling_device7      # pwm-fan
ON_TEMP=58.00    # 满速阈值
OFF_TEMP=50.00   # 停转阈值
FULL=5

# 接管前关闭内核对该温控区的自动调速(该区只绑定了 pwm-fan)
echo disabled > "$ZONE/mode"

restore() {
    echo enabled > "$ZONE/mode" 2>/dev/null
    exit 0
}
trap restore TERM INT

state=0
echo 0 > "$CDEV/cur_state"

while true; do
    t_raw=$(cat "$ZONE/temp" 2>/dev/null)
    # 温度单位转换:毫摄氏度 → 摄氏度
    t=$(echo "scale=2; $t_raw / 1000.0" | bc)
    echo -e "\e[32;40m 当前温度\e[0m\e[33;40m: $t\e[0m\e[32;40m摄氏度\e[0m"

    # 校验温度是否合法数字,异常强制拉满风扇
    case "$t" in
        ''|*[!0-9.]*)
            t=$ON_TEMP
            ;;
    esac

    # bc 返回 1 代表条件成立,0 不成立
    # 温度 >= 开启阈值 且 当前不是满速
    if [ $(echo "$t >= $ON_TEMP" | bc) -eq 1 ] && [ "$state" -ne "$FULL" ]; then
        state=$FULL
        echo $FULL > "$CDEV/cur_state"
        echo "温度超标,风扇拉满全速"
    # 温度 <= 关闭阈值 且 当前不是停止状态
    elif [ $(echo "$t <= $OFF_TEMP" | bc) -eq 1 ] && [ "$state" -ne 0 ]; then
        state=0
        echo 0 > "$CDEV/cur_state"
        echo "温度回落,风扇停止运转"
    fi

    sleep 2
done

CPU满载测试指令

while :; do :; done

哈哈,再加点儿feature吧。

#!/bin/sh
# RhinoPi X1 静音风扇控制(开关式)- 可视化版
#
# 背景:板载风扇由 pwm-fan 驱动以 ~64Hz 低频 PWM 斩波驱动,
# 中间档位(1-4)会产生明显啸叫,只有 0(停)和 5(满速,恒定导通)安静。
# 因此本服务采用带迟滞的开关控制:>= ON_TEMP 满速,<= OFF_TEMP 停转。
#
# 安全:本服务退出(含 systemctl stop)时恢复内核自动温控兜底。

ZONE=/sys/class/thermal/thermal_zone2        # cpuss-1
CDEV=/sys/class/thermal/cooling_device7      # pwm-fan
ON_TEMP=58.00    # 满速阈值
OFF_TEMP=50.00   # 停转阈值
FULL=5

# 可视化配置(可根据终端宽度调整 BAR_WIDTH)
BAR_WIDTH=30
TEMP_MIN=30      # 进度条起始温度(对应空条)
TEMP_MAX=80      # 进度条满格温度(对应满条)

# ANSI 颜色
C_RST="\033[0m"
C_DIM="\033[2m"
C_BLD="\033[1m"
C_GRN="\033[32m"
C_YLW="\033[33m"
C_RED="\033[31m"

# 接管前关闭内核对该温控区的自动调速(该区只绑定了 pwm-fan)
echo disabled > "$ZONE/mode"

restore() {
    printf "\n\033[K${C_RST}正在恢复内核自动温控...\n"
    echo enabled > "$ZONE/mode" 2>/dev/null
    exit 0
}
trap restore TERM INT

state=0
echo 0 > "$CDEV/cur_state"

# 清屏并打印静态标题(只执行一次)
printf "\033[2J\033[H"
printf "${C_BLD}RhinoPi X1 静音风扇控制器${C_RST}\n"
printf "${C_DIM}停转≤${OFF_TEMP}°C  |  满速≥${ON_TEMP}°C${C_RST}\n\n"

# 生成并打印进度条(原地刷新)
draw_bar() {
    _t="$1"

    # 计算填充长度(整数)
    _filled=$(echo "scale=0; (_t - $TEMP_MIN) * $BAR_WIDTH / ($TEMP_MAX - $TEMP_MIN)" | bc 2>/dev/null)
    case "$_filled" in
        ''|*[!0-9]*) _filled=0 ;;
    esac
    if [ "$_filled" -lt 0 ]; then _filled=0; fi
    if [ "$_filled" -gt "$BAR_WIDTH" ]; then _filled=$BAR_WIDTH; fi
    _empty=$((BAR_WIDTH - _filled))

    # 根据温度选择颜色与状态文字
    if [ $(echo "$_t >= $ON_TEMP" | bc) -eq 1 ]; then
        _color="$C_RED"
        _status="◈ FULL SPEED"
    elif [ $(echo "$_t >= $OFF_TEMP" | bc) -eq 1 ]; then
        _color="$C_YLW"
        _status="○ STANDBY   "
    else
        _color="$C_GRN"
        _status="○ OFF       "
    fi

    # 构建进度条字符串
    _bar=""
    i=0
    while [ $i -lt "$_filled" ]; do
        _bar="${_bar}█"
        i=$((i + 1))
    done
    i=0
    while [ $i -lt "$_empty" ]; do
        _bar="${_bar}░"
        i=$((i + 1))
    done

    # 原地刷新:\r 回到行首,\033[K 清到行尾
    printf "\r\033[K${_color}${C_BLD}${_t}°C${C_RST} ${_color}[${_bar}]${C_RST} ${_color}${_status}${C_RST}"
}

while true; do
    t_raw=$(cat "$ZONE/temp" 2>/dev/null)
    # 温度单位转换:毫摄氏度 → 摄氏度
    t=$(echo "scale=2; $t_raw / 1000.0" | bc)

    # 校验温度是否合法数字,异常强制拉满风扇
    case "$t" in
        ''|*[!0-9.]*)
            t=$ON_TEMP
            ;;
    esac

    # 绘制进度条(同一行原地更新,不刷屏)
    draw_bar "$t"

    # 带迟滞的开关控制
    if [ $(echo "$t >= $ON_TEMP" | bc) -eq 1 ] && [ "$state" -ne "$FULL" ]; then
        state=$FULL
        echo $FULL > "$CDEV/cur_state"
    elif [ $(echo "$t <= $OFF_TEMP" | bc) -eq 1 ] && [ "$state" -ne 0 ]; then
        state=0
        echo 0 > "$CDEV/cur_state"
    fi

    sleep 2
done

1 个赞

or



sudo apt -y install stress-ng

stress-ng -c $(nproc)
1 个赞

构建了一个项目仓库,大家都可以用,开源的:
https://github.com/yoyojacky/rhino_Pi_X1_FanControl

有兴趣可以加入哈哈