给泰山派 1M 风扇做「自动调速」
给泰山派 1M 风扇做「自动调速」
记录日期:2026-10-09
板子:泰山派 1M(芯片 RK3566)
读完你会明白:这个功能是怎么做的、为什么之前一直没做好、以及每一个命令到底在干什么。
第 0 章 一句话说清这件事
这块主板跑起来会发热,我们在上面装了个小风扇吹它。
但风扇不能一直傻转 —— 一直全速又吵又费电。我们希望它自己看温度决定转速:
| 主板温度 | 风扇力度 | 听感 |
|---|---|---|
| 45 °C 以下 | 35% | 慢悠悠,几乎听不见 |
| 45 ~ 50 °C | 55% | 有点声音 |
| 50 ~ 55 °C | 75% | 明显在吹 |
| 55 °C 以上 | 90% | 全力吹 |
这个"自己看温度调转速"的活儿,就是温控调速。
这次要解决的问题是:之前风扇不管多凉都快转(一直全速),上面的表格根本没生效。我们把它修好了。
第 1 章 先搞懂 7 个词
不用背,看不懂就跳过去,后面遇到再回来查。
① 开发板 / 主板
一台小电脑。我们这台用的是泰山派 1M,芯片是瑞芯微的 RK3566。
② PWM(脉宽调制)
这是全文最重要的概念。
想象你要控制水龙头的水流大小,但水龙头只有"全开"和"全关"两个状态,没有中间档。
怎么办?快速地开、关、开、关。开的时间占的比例越大,平均水流就越大。
一秒钟开 1000 次,其中 350 次是开的 → 平均水流 35%
一秒钟开 1000 次,其中 1000 次都是开的 → 平均水流 100%
这个"开的时间占的比例"就叫 占空比(duty cycle)。
"一秒钟开关多少次"就叫 频率。
占空比 = 力度(35% 55% 75% / 90%)
频率 = 开关的快慢(这次的关键就是它)
③ 设备树(dts / dtb)
主板上的硬件那么多,内核怎么知道"哪个引脚接了什么"?
靠一张说明书,就叫设备树。
.dts= 人看的说明书(纯文本,能直接改).dtb= 机器看的说明书(把上面的翻译成二进制)
改硬件配置 = 改 .dts → 翻译成 .dtb。
④ boot.img 和 boot 分区
主板开机时,会先从一个叫 boot 分区 的小区域里读一个 boot.img 包裹。
这个包裹里装着:内核(系统的心脏)+ 设备树(硬件说明书)。
所以"改硬件配置"的完整链条是:
⑤ /sys —— 内核的"控制面板"
Linux 里有个特殊的文件夹 /sys,里面的东西看起来是文件,其实不是文件,而是内核开出来的控制面板:
读它 = 问内核"现在什么情况?"
写它 = 命令内核"改成这样!"
比如:
(cat 是"显示内容",echo xxx > 是"把 xxx 写进去",下面会细讲)
⑥ 调速器(thermal governor)
内核里管温控的"大脑",有好几种性格:
power_allocator(功耗分配型):按功耗预算分配降温资源step_wise(逐级型):超温就一档一档往上加user_space:内核不管,让用户自己控制
这次出事的正是这个"大脑"。
⑦ 占空比 vs 频率 —— 用一句话记住
占空比决定"多大劲",频率决定"开关多快"。
这次的毛病是"开关太快",不是"劲不对"。
第 2 章 硬件是怎么接的
风扇不是直接插在主板上的,中间加了一小块电路。用户提供的电路图我们读出来是这样:
+5V 电源
│
┌────┴────┐
│ R298 │ 20kΩ(把开关默认拉到"关")
│ 20k │
└────┬────┘
│
+5V ───┼──── Q9 (AO3407,P沟道MOS管,当"电子开关"用)
│ │
│ └──── 风扇正极 (CN15 第1脚)
│ │
│ 风扇
│ │
│ GND 地
│
Q10 (NPN三极管,负责"把开关拉下来打开")
│
R299 1kΩ ── PWM0_M0(主板引脚 GPIO0_B7)
│
R300 10kΩ ── GND
用大白话说这套电路在干嘛:
主板引脚
PWM0_M0输出"开/关"信号(就是 PWM 波)引脚高电平 → 三极管 Q10 导通 → 把 MOS 管 Q9 的"闸门"拉低 → Q9 打开 → 风扇通电
引脚低电平 → Q10 关闭 → 电阻 R298 把"闸门"拉回 +5V → Q9 关闭 → 风扇断电
所以:引脚高电平的时间比例 = 风扇通电的时间比例 = 风扇力度。逻辑是对的。
顺带说:AO3407 是 P 沟道 MOS 管,做"高边开关"(控制电源正极)时必须源极接 +5V、漏极接风扇。如果装反了,它内部那个"寄生二极管"会一直给风扇供电,怎么调都没用。这一点我们后来专门验证过,没装反。第 3 章 症状:风扇一直全速
用户的原话是"一直全速"。我们上板子一看:
36.7°C 却全力吹 —— 明显不正常,凉快的时候应该是 0 档才对。
第 4 章 排查过程:一步一步找出真凶
排查思路是"从软件到硬件,一层层往下剥"。
第 1 步:确认软件链路有没有问题
先证明"主板引脚确实在输出 PWM 波"。 查了三个地方:
① 引脚有没有被正确"复用"成 PWM 功能
芯片的引脚是多功能复用的,得先设置成"PWM 模式"。
翻译:15 号引脚(就是 GPIO0_B7)现在归 fdd70000.pwm 这个 PWM 控制器管,工作模式是 pwm0m0。
✅ 引脚复用是对的。
② PWM 控制器的时钟有没有开
设备没时钟就等于没通电,什么都不会输出。
翻译:clk_pwm0 这个时钟被启用了(第一个数字 1 = 启用次数),频率 12000000 = 12 MHz。
✅ 时钟是开着的。
③ 直接看 PWM 控制器的寄存器(最硬的证据)
寄存器就是芯片内部的小格子,里面存着"周期设成多少、占空比设成多少、开没开"。
这 4 个数按顺序是:计数器 周期 占空比 / 控制字
周期 =
0x1e0= 480占空比 =
0xa9= 169控制字 =
0x0b= 二进制1011→ 最低位是 1,表示已使能
换算一下:时钟 12 MHz ÷ 480 = 25 kHz(一秒开关 25000 次);169 ÷ 480 = 35.2%。
0x开头表示这是十六进制(电脑里常用的一种计数法)。0x1e0换成我们熟悉的十进制就是 480。
✅ 结论:主板这边完全正确。 芯片确实在老老实实输出一个 25 kHz、35% 的波形。
所以问题不在软件,在别处。
第 2 步:那是不是"温控大脑"没在管风扇?
查了一下当前用的"大脑":
而 dmesg(内核的"工作日志")里赫然写着:
翻译:power_allocator 这个"大脑"看了风扇一眼,说"你不是我的菜",然后完全不管它。
真相浮出水面:
主板开机默认用
power_allocator这个大脑拒绝管理风扇
于是风扇就一直保持驱动自己开机时设的初始值 —— 100% 满速
再也没有任何东西去把它调下来
前任同事以为"power_allocator 会把风扇推到满速",其实更简单也更荒诞:它压根不碰风扇。顺带发现一个更离谱的:之前有人写了个开机脚本想修这个问题,脚本里写的是:
但板子上那个"温度区"的真名是 soc-thermal(中间是横杠),脚本里写的是下划线。
→ 条件永远不成立 → 脚本三个多月来一次都没真正执行过。
更坑的是脚本最后写了 exit 0(表示"我成功了"),于是系统还显示服务正常运行,属于"无声失败" —— 看日志一切正常,实际啥也没干。
第 3 步:找到正确的做法
翻内核源码时发现这个 SDK(芯片厂商给的开发套件)的 pwm-fan 驱动有个隐藏功能:
翻译成人话:只要在设备树里写上 rockchip,temp-trips(一张"多少度对应几档"的表),
驱动就会自己盯着温度调风扇,完全绕过那个不靠谱的"大脑"。
而前任同事删掉的那个旧节点,用的恰恰就是这个写法 —— 方向本来就是对的。
第 4 步:软件都对了,为什么风扇还是不变速?
按上面的思路改完,理论上应该好了。但用户反馈:测的时候风扇没有一档一档变速。
于是我们做了两个"运行时实验"(不用刷机、随时能改回来)。
实验 A:静态开关测试 —— 判断 MOS 开关有没有坏
思路:把风扇变成"一直开"和"一直关"两个极端状态。如果"一直关"的时候风扇还在转,说明电路有旁路。
同时看寄存器:
| 状态 | 周期 | 占空比 | 控制字 | 结果 |
|---|---|---|---|---|
| 一直开 | 480 | 480 | 0x0b(使能) | 风扇全速 |
| 一直关 | 480 | 0 | 0x08(使能位=0) | 风扇停了 ✅ |
结论:MOS 开关通路完全正常,源漏没装反,没有旁路。问题只在"动态"上。
实验 B:扫频测试 —— 换个开关速度试试
思路:把风扇驱动"请走",我们手动接管 PWM,用不同频率试。
结果:
| 频率 | 35% vs 100% 的听感 |
|---|---|
| 25 kHz(原来) | 听不出差别 |
| 1 kHz(新试的) | 能清楚听出分档 ✅ |
| 250 Hz | 也能,但 1 kHz 已经够了 |
真凶找到了。
第 5 步:为什么会这样?两个原因叠加
原因一:开关的"手"太慢
控制 MOS 管开关的"闸门"(栅极)靠一个 20 kΩ 电阻充电。算一下:
闸门电容约 650 pF,电阻 20 kΩ
充电时间常数 = 20000 × 0.00000000065 ≈ 13 微秒
而 25 kHz 下一个完整周期只有 40 微秒。35% 档的"关"的时间只有 26 微秒(才 2 倍时间常数)。
结果:闸门电压从 0 只爬到 4.2 V(本该爬到 5 V),MOS 管停在"半开半关"的线性区。
更糟的是,从"全开"退到"关"需要 10~22 微秒,这段时间里管子还是通的。
算总账:你要 35%,管子实际通了 62%~91%。
→ 想要 35% 和想要 100%,实际差别很小 → 听不出来。
原因二:风扇自己有个"小水桶"
风扇内部有个小电容(可以理解成蓄水池)。25 kHz 时"关"只有 26 微秒,
池子里的水(电)根本来不及放掉,风扇看到的电压几乎还是满的。
这就好比你想用快速开关水龙头控制水流,但水管中间有个大水桶把水存着 ——
水龙头开关得再快,水桶里的水也不少。
降到 1 kHz 后:
"关"的时间从 26 微秒变成 650 微秒,是充电时间常数的 50 倍 → 闸门有充足时间完全关严
风扇那个"水桶"有 650 微秒放水 → 真正放到空 → 电压真的降下来了
一个数字改动,两个问题同时解决。
第 5 章 最终改了什么
改动只有一个文件
改前(有问题):
&pwm0 {
status = "okay";
};
/ {
pwm0_fan: pwm-fan {
compatible = "pwm-fan";
#cooling-cells = <2>;
pwms = <&pwm0 0 40000 0>; /* 40000 纳秒 = 25 kHz */
cooling-levels = <90 140 190 230 255>;
};
};
&soc_thermal { /* 这一大段是前任加的,基于错误推断 */
/delete-property/ sustainable-power;
trips { fan_trip: trip-point-fan { temperature = <45000>; ... }; };
cooling-maps { fan_map0 { ... }; };
};
改后(正确):
&pwm0 {
status = "okay";
};
/ {
pwm0_fan: pwm-fan {
compatible = "pwm-fan";
pwms = <&pwm0 0 1000000 0>; /* 1000000 纳秒 = 1 kHz ← 关键改动 */
cooling-levels = <90 140 190 230 255>; /* 五档力度:35% 55% 75% 90% 100% */
rockchip,temp-trips = < /* 多少度 → 用第几档 */
45000 1
50000 2
55000 3
60000 4
>;
};
};
具体改了三件事:
40000→1000000:PWM 频率从 25 kHz 降到 1 kHz(最关键的一改)删掉
#cooling-cells,加上rockchip,temp-trips:让风扇驱动自己管温度,不再交给那个不靠谱的"大脑"整段删掉
&soc_thermal { ... }:这段是前任基于错误推断加的。删掉后主板自动恢复出厂设置(CPU/内存的温控回到原厂行为),风扇和它们彻底解耦
第 6 章 操作全流程(每个命令都讲解)
用到的三台机器
| 名字 | 是什么 | 怎么连 |
|---|---|---|
| 开发板 | 要动手术的对象 | adb 连 192.168.137.130:5555 |
| 编译服务器 | 干重活的地方(编译很吃 CPU) | ssh liguoyi@192.168.165.66 |
| 本机(Windows) | 你坐的地方,当"中转站" | — |
0. 准备知识:adb 是什么
adb = Android Debug Bridge,安卓调试桥。虽然板子跑的是 Linux,但这类芯片板子一般都用它来远程操作。
| 命令 | 什么意思 |
|---|---|
adb devices | 列出都连上了哪些设备。看到 192.168.137.130:5555 device 就说明连上了 |
adb connect 192.168.137.130:5555 | 主动连接板子。:5555 是端口号(像门牌号) |
adb shell | 在板子上开个命令行。后面跟着的命令都是在板子上跑的 |
adb push 本地文件 板上路径 | 把文件从电脑传到板子上 |
adb pull 板上路径 本地路径 | 把文件从板子取回电脑 |
adb reboot | 重启板子 |
1. 改设备树
在编译服务器上操作:
| 命令 | 讲解 |
|---|---|
cp A B | 复制文件。把 A 复制成 B。这里就是备份 |
文件名里的 .bak-260909-fan-v2 | 我们自己起的后缀,内容就是原文件,方便以后找回 |
2. 编译 + 打包
⚠️ 这一步有个大坑,必须用 ./build.sh,不能直接 make。
| 命令 | 讲解 |
|---|---|
cd 目录 | 切换当前所在目录(change directory) |
./build.sh kernel | 运行 SDK 自带的构建脚本,参数 kernel 表示"构建内核+boot.img" |
为什么不能直接 make?
编译要用交叉编译器(在 x86 电脑上编出 ARM 板子能跑的代码)。这个工具链藏在 SDK 的 prebuilts/gcc/... 目录里,要由 build.sh 把它加到 PATH 里。
我中途偷懒直接跑了 make,结果系统找不到交叉编译器,用了电脑自己的 gcc,把配置文件的编译器信息写错了。虽然最后恢复好了,但绕了一大圈 —— 记住这个教训。
PATH 是什么?可以理解成"电脑去哪些文件夹里找命令"。没加到 PATH 里,电脑就找不到那个交叉编译器。
跑完会看到什么?
看到 succeeded 就成了。产物在 kernel-6.1/boot.img。
3. 验证新固件(刷之前一定要验)
输出里会列出三样东西:fdt(设备树)、kernel(内核)、resource(资源)。
最关键的验证:确认新设备树真的装进去了。
把这个值和 mkimage -l 输出里 Image 0 (fdt) 的 Hash value 对一下。两个一样就说明装对了。
| 命令 | 讲解 |
|---|---|
sha256sum 文件 | 算文件的"指纹"。内容只要差一个字节,指纹就完全不同。用来判断两个文件是不是一模一样 |
md5sum 文件 | 同上,另一种指纹算法,更快但稍弱 |
mkimage -l 文件 | -l = list,列出这个镜像的清单 |
4. 传到板子上并写入
① 先把镜像从服务器拿到本机
| 命令 | 讲解 |
|---|---|
scp | 安全复制(secure copy),通过网络在两台电脑间传文件 |
② 从本机传到板子
/root/T16/是板子上专门放东西的目录。千万别用/tmp—— 那是内存盘,一重启就全没了(这个项目已经因此丢过数据)。
③ 备份板子现在的 boot 分区(最重要的一步!)
| 命令 | 讲解 |
|---|---|
dd | 原样搬运数据(disk dump)。最危险的命令之一,if 和 of 写反了会毁数据 |
if=... | input file,从哪读。这里是分区 /dev/mmcblk0p3 |
of=... | output file,写到哪。这里是备份文件 |
bs=1M | 每次搬 1 兆字节(block size)。越大越快 |
count=64 | 搬 64 次。64 × 1M = 64MB,正好是整个分区大小 |
/dev/mmcblk0p3 | 板子上的第 3 个分区,也就是 boot 分区 |
mmcblk0是板子上的 eMMC 存储芯片,p3表示第 3 个分区。
用cat /proc/partitions可以列出所有分区。
④ 写入新镜像
| 命令 | 讲解 |
|---|---|
sync | 强制把缓存里的数据真正写到磁盘上。不敲这个,数据可能还在内存里,一断电就没了 |
⑤ 回读校验(确认真的写进去了)
把新镜像的指纹和回读的指纹对比:一样就说明写入成功。
为什么是 count=81169?因为镜像正好 41558528 字节,除以 512 正好等于 81169。这只是为了让回读长度和镜像一样,多读的尾部是旧数据不用管。⑥ 重启
重启后 adb 会断开,等大约 45 秒再 adb connect 重连。
5. 验证效果
# 内核版本变了吗?(应该看到新日期)
adb shell cat /proc/version
# 新配置进去了吗?
adb shell ls /proc/device-tree/pwm-fan/
# 风扇现在多大力度?
adb shell cat /sys/class/hwmon/hwmon2/pwm1
# 实际占空比(看寄存器)
adb shell cat /sys/kernel/debug/fdd70000.pwm
我们的实测结果:
| 检查项 | 结果 |
|---|---|
| 内核 | #8 SMP Fri Oct 9 11:05:55(原来是 #7 … Oct 8 13:38)✅ |
| 设备树 | 有 rockchip,temp-trips ✅ |
| PWM 周期 | 0x2ee0 = 12000,12000 ÷ 12MHz = 1 kHz ✅ |
| 36°C 时的力度 | pwm1 = 90(35%),不再满速 ✅ |
| 加载后的分档 | 45°C→55%,50°C→75%,55°C→90%,停载后自动回落 ✅ |
6. 加载测试(看它真的会自己加速)
我们开了 8 个"死循环"把 CPU 跑满,让板子热起来:
实测记录(挑几行):
温度一档档上去,风扇一档档跟上,凉下来自动回落。 ✅
| 命令 | 讲解 |
|---|---|
cat | 显示文件内容(concatenate 的缩写,这里当"查看"用) |
ls | 列出目录里有什么 |
grep 关键词 文件 | 在文件里搜关键词,只显示匹配的行 |
dmesg | 显示内核的工作日志,排查驱动问题的第一去处 |
echo xxx > 文件 | 把 xxx 写进文件。> 是"覆盖写入" |
systemctl is-active 服务名 | 看某个服务在不在运行 |
第 7 章 出问题了怎么回滚
一条命令写回备份:
| 符号 | 讲解 |
|---|---|
&& | 前一个命令成功了,才执行下一个 |
为什么不会变砖? 因为 boot 分区只是"启动包裹",坏了最多是开不了机,
用上面这条命令把备份写回去就能恢复,主板本身的底层固件没动。
设备树也回滚:
第 8 章 名词速查表
| 名词 | 大白话解释 |
|---|---|
| PWM | 用"快速开关"模拟"中间档位"的技术 |
| 占空比 | 一秒钟里"开"占的比例 → 决定力度 |
| 频率 | 一秒钟开关多少次 → 这次的关键 |
| 设备树 dts dtb | 给内核看的"硬件接线说明书"(文本版 / 二进制版) |
| boot 分区 / boot.img | 存"启动包裹"的地方 / 包裹本身(内核+说明书) |
| PWM 寄存器 | 芯片里存"周期设多少、占空比设多少"的小格子 |
| 调速器 / governor | 内核里管温控的"大脑",有不同性格 |
| cooling_device | "降温设备",比如风扇、CPU 降频,统一登记在册 |
| hwmon | "硬件监控"子系统,风扇在这里登记,露出 pwm1 等接口 |
| MOS 管 | 电子开关,用电压控制通断 |
| 栅极 | MOS 管的"闸门",给它电压就能开/关 |
| 时间常数 τ | 电容充电快慢的指标。充到 63% 需要 1 个 τ |
| dmesg | 内核的工作日志 |
sysfs / /sys | 内核开出来的"控制面板",读=问情况,写=下命令 |
| 交叉编译 | 在 A 种电脑上编出 B 种电脑能跑的程序 |
第 9 章 这次的三个"坑",记住能省很多时间
坑 1:soc_thermal 和 soc-thermal 差一个字符
脚本里判断"温度区叫什么名字"时用了下划线,实际是横杠。
条件永远不成立,脚本啥也没干,但因为写了 exit 0,系统还显示"服务正常"。
教训:这类脚本不要无脑 exit 0,匹配失败时应该报错或打日志,否则就是无声失败。
坑 2:以为 power_allocator 会推满速,其实它压根不管
前任的推断是"这个大脑会把风扇推到最大",实际是"这个大脑拒绝管理风扇"。
结果一样(都是满速),但原因完全不同,导致修的方向也完全不同。
教训:查问题要看证据(dmesg 原话),不要靠推理猜。
坑 3:想控制"电压型负载",频率不能太高
25 kHz 是四线风扇的标准控制频率(那是数字信号,直接进风扇内部芯片)。
我们用的是两线风扇,靠"降低平均电压"调速,标准做法是 几十 Hz ~ 1 kHz。
用了 25 kHz,风扇内部的电容把斩波全平滑掉了,等于没调。
教训:同一个技术(PWM),用在不同的负载上,参数要求完全不一样。
第 10 章 还留下的两个小尾巴
尾巴 1:45°C 附近会来回跳档
空载时主板温度就在 41~46°C,正好压在第一个触发点上。
而这套"温度→档位"的映射是纯阈值判断,没有滞环(就是"过了 45 就升,低于 45 就降",没有缓冲带)。
所以风扇会在 35% 和 55% 之间来回切。
可以怎么调(改哪一项都行):
把第一个触发点从 45°C 抬到 50°C
或者把最低档从 35% 提高到 47%(让空载更凉,离开触发点)
尾巴 2:那个没用的开机脚本
thermal-governor-fix.service 现在已经完全没用了(风扇不再走温控框架那条路),
但它还是开机自启。建议关掉:
| 命令 | 讲解 |
|---|---|
systemctl disable 服务名 | 取消开机自启(不会立刻停掉,只是下次开机不启动) |
尾巴 3(可选):一个硬件小改进
给 MOS 管栅极充电的那个电阻 R298 是 20 kΩ,偏大。
改成 1 kΩ 会让开关关得更干脆。1 kHz 下不改也能用,但改了以后即使以后想提高频率也站得住。
附录:常用命令一句话速查
| 命令 | 一句话 |
|---|---|
adb devices | 看连上了哪些设备 |
adb connect IP:端口 | 主动连板子 |
adb shell '命令' | 在板子上执行命令 |
adb push 本地 板上 | 电脑 → 板子 |
adb pull 板上 本地 | 板子 → 电脑 |
adb reboot | 重启板子 |
ls 目录 | 列出目录内容 |
cat 文件 | 显示文件内容 |
cd 目录 | 切换目录 |
cp A B | 复制 A 成 B |
grep 关键词 文件 | 在文件里搜关键词 |
echo 值 > 文件 | 把值写进文件 |
dmesg | 看内核日志 |
md5sum / sha256sum | 算文件指纹,比对是否一致 |
dd if=源 of=目标 bs=块大小 count=次数 | 原样搬运数据(危险,别写反) |
sync | 强制把数据写到磁盘 |
ssh 用户@主机 | 远程登录另一台电脑 |
scp 源 目标 | 两台电脑间传文件 |
./build.sh kernel | 编译内核并打包 boot.img |
systemctl is-active 服务 | 看服务在不在跑 |
systemctl disable 服务 | 取消开机自启 |
一句话总结
风扇之前一直全速,是因为内核的温控"大脑"根本不管它,
而唯一想管它的那个脚本写错了字、从来没生效过。
换用驱动自带的温度联动、并把 PWM 频率从 25 kHz 降到 1 kHz 之后,
风扇终于能按温度一档一档地自己变速了。


评论