PWM散热风扇设置—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
看门狗会喂了。今天让风扇转起来:系统不会上电自动出波形,要自己打开通道、设周期和占空比。
风扇座有电、PWM 脚也 okay,扇叶却一动不动。ls /sys/class/pwm 能看见 pwmchipN,但下面没有 pwm0/。PWM 子系统不会因为设备树 status = "okay" 就自动出波形。用户态要自己 export,再写 period、duty_cycle,最后 enable。少一步都是直流 0 或高阻,风扇当没接到信号。
第一次占空比先拉到 100%。很多 12 V 风扇有起转阈值,20% 占空比在桌上转、在风道里不转,你会误判脚号或极性。100% 转起来之后再往下降,才知道调速是不是这条通路。
产品 rk3568_evb,风扇走 pwm0:
device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts
&pwm0 {
status = "okay";
};
不要去使能 pwm5。pwm5 在这块板上和相机电源脚打架,板级是 disabled。风扇不是 pwm5。
内核:
CONFIG_PWM=y
CONFIG_PWM_ROCKCHIP=y
不要开 SENSORS_PWM_FAN。那是 hwmon 温控,会按温度自动改占空比。演示和验收用手写 sysfs 就够,自动温控会把你刚写的 100% 改掉,看起来像「写了没生效」。


1. chip 号不要写死
RK3568 有多路 PWM 控制器。pwm0 常见地址 fdd70000(PMU 侧)。pwmchip 的编号按 probe 顺序,不一定是 0。
hdc shell "ls -l /sys/class/pwm"
hdc shell "ls -l /sys/class/pwm/pwmchip*/device"
看软链指向谁。名字或 uevent 里带 fdd70000.pwm / pwm0 的那颗,才是风扇。下面用 $CHIP 指它,例子里写 pwmchip0 只是常见情况。
for c in /sys/class/pwm/pwmchip*; do
echo -n "$c -> "
readlink $c/device
done
npwm 告诉你这个控制器有几路通道。pwm0 节点通常 npwm=1,export 的是通道 0,目录名仍叫 pwm0(相对这个 chip)。
hdc shell "cat /sys/class/pwm/pwmchip0/npwm"
2. export 之后三步:period、duty、enable
单位是 纳秒。period=1000000 是 1 kHz;风扇更常见 20~25 kHz,听不见啸叫:period=40000 是 25 kHz。
必须先写 period,再写 duty_cycle。duty 不能大于当前 period,否则 -EINVAL。第一次 100% 就是 duty_cycle = period。
CHIP=/sys/class/pwm/pwmchip0
# 前置:export 通道 0(已经 export 过会 Device or resource busy,可忽略)
echo 0 > $CHIP/export
# 三步
echo 40000 > $CHIP/pwm0/period
echo 40000 > $CHIP/pwm0/duty_cycle # 先 100%
echo 1 > $CHIP/pwm0/enable
降速:
echo 20000 > $CHIP/pwm0/duty_cycle # 50%
echo 10000 > $CHIP/pwm0/duty_cycle # 25%
停:
echo 0 > $CHIP/pwm0/enable
不要 unexport 来停,下次还得再 export。enable=0 即可。
极性:
hdc shell "cat /sys/class/pwm/pwmchip0/pwm0/polarity"
normal:duty 越大,高电平越宽,多数风扇越快。inversed:100% 反而停转、0% 满转。现场「100% 不动、接近 0 狂转」先看 polarity,再怀疑脚。
有的内核 polarity 只读。那就在软件里把「速度 100」映射成 duty=0。
3. 为什么只 okay 控制器不够
PWM 控制器 probe 成功,只是寄存器能访问。sysfs 的 pwmN 是通道对象,export 才申请这一路、配 pinctrl、让脚从 GPIO 变成 PWM 功能。没 export 时,脚可能仍是 GPIO 或高阻。
时序:
dts &pwm0 okay → pwmchip 出现
export 0 → pwm0/ 目录、脚切到 PWM
period → 计数器周期
duty_cycle → 比较值
enable=1 → 脚上真正出波
dmesg 里 pwm-rockchip probe 成功但风扇不转,停在后四格的某一格。
电源:PWM 是控制线,不是供电。12 V 风扇要风扇座的 12 V。PWM 脚是 3.3 V 逻辑,座上通常有三极管或专用驱动把 3.3 V 波变成风扇能认的电平。量 SoC 脚有 25 kHz、座上没有,查驱动管和座供电,不要改 dts。
1 kHz 能转,但能听见啸叫。验收用 25 kHz(period=40000)。示波器时基不够时,先用 1000000 看有没有波,再改回 40000。
4. 设备树和 pinctrl
板级常常只开控制器,pinctrl 用 SoC dtsi 默认的 pwm0m0_pins。若风扇不转且 SoC 脚上也没有波,把 pinctrl 写明白:
&pwm0 {
status = "okay";
pinctrl-names = "active";
pinctrl-0 = <&pwm0m0_pins>;
};
具体脚以 dtsi 的 pwm0m0_pins 为准,不要和 uart、gpio-leds 抢。改之前:
dtc -I dtb -O dts /sys/firmware/fdt | grep -A20 "pwm@fdd70000"
pwm5 保持 disabled。相机那路如果误 okay,会和风扇无关的脚冲突,表现为相机不上电,不是风扇问题。
改 dts 刷 resource + boot_linux:
hdc shell "cat /proc/partitions"
hdc shell "dd if=/data/resource.img of=/dev/block/mmcblk1p4 bs=4M conv=fsync"
hdc shell "dd if=/data/boot_linux.img of=/dev/block/mmcblk1p5 bs=4M conv=fsync"
5. 完整 C
/* pwm_fan.c
* armv7-unknown-linux-ohos-clang --target=arm-linux-ohos \
* --sysroot=${OHOS_NATIVE}/sysroot \
* -o pwm_fan pwm_fan.c \
* -Wl,--dynamic-linker=/system/lib/ld-musl-arm.so.1
*
* ./pwm_fan # 100%
* ./pwm_fan 50 # 50%
* ./pwm_fan 0 # enable=0
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
static int wr(const char *path, const char *s)
{
int fd = open(path, O_WRONLY);
if (fd < 0) {
fprintf(stderr, "open %s: %s\n", path, strerror(errno));
return -1;
}
int n = (int)write(fd, s, strlen(s));
close(fd);
return n < 0 ? -1 : 0;
}
static int find_chip(char *out, int len)
{
DIR *d = opendir("/sys/class/pwm");
struct dirent *e;
if (!d) {
return -1;
}
while ((e = readdir(d)) != NULL) {
char link[192], tgt[192];
if (strncmp(e->d_name, "pwmchip", 7) != 0) {
continue;
}
snprintf(link, sizeof(link), "/sys/class/pwm/%s/device", e->d_name);
int n = (int)readlink(link, tgt, sizeof(tgt) - 1);
if (n < 0) {
continue;
}
tgt[n] = 0;
if (strstr(tgt, "fdd70000") || strstr(tgt, "pwm0")) {
snprintf(out, len, "/sys/class/pwm/%s", e->d_name);
closedir(d);
return 0;
}
if (out[0] == 0) {
snprintf(out, len, "/sys/class/pwm/%s", e->d_name);
}
}
closedir(d);
return out[0] ? 0 : -1;
}
int main(int argc, char **argv)
{
char chip[128] = {0}, p[192];
int percent = (argc > 1) ? atoi(argv[1]) : 100;
const char *period = "40000";
if (find_chip(chip, sizeof(chip)) < 0) {
fprintf(stderr, "没有 pwmchip。CONFIG_PWM_ROCKCHIP / &pwm0 okay?\n");
return 1;
}
printf("chip %s percent=%d\n", chip, percent);
snprintf(p, sizeof(p), "%s/export", chip);
wr(p, "0");
snprintf(p, sizeof(p), "%s/pwm0/period", chip);
if (wr(p, period) < 0) {
return 2;
}
if (percent <= 0) {
snprintf(p, sizeof(p), "%s/pwm0/enable", chip);
return wr(p, "0") == 0 ? 0 : 3;
}
if (percent > 100) {
percent = 100;
}
{
int duty = 40000 / 100 * percent;
char ds[16];
snprintf(ds, sizeof(ds), "%d", duty);
snprintf(p, sizeof(p), "%s/pwm0/duty_cycle", chip);
if (wr(p, ds) < 0) {
return 4;
}
}
snprintf(p, sizeof(p), "%s/pwm0/enable", chip);
if (wr(p, "1") < 0) {
return 5;
}
printf("enabled. first 100%%, then drop percent.\n");
return 0;
}
HAP 写这些节点要 chmod。export 之后才有 pwm0 目录,开机脚本里要先 export 再 chmod duty/enable:
CHIP=/sys/class/pwm/pwmchip0
echo 0 > $CHIP/export 2>/dev/null
chmod 0666 $CHIP/export
chmod 0666 $CHIP/pwm0/period $CHIP/pwm0/duty_cycle $CHIP/pwm0/enable
6. ArkTS
import evbio from 'libevbio.so';
@Entry
@Component
struct PwmFan {
@State percent: number = 100;
@State log: string = '先 100%,转起来再往下';
private chip: string = '';
build() {
Column() {
Text('PWM 风扇').fontSize(22).fontWeight(FontWeight.Bold)
Text(this.percent.toString() + '%').fontSize(28).margin({ top: 8 })
Row() {
Button('100%').onClick(() => { this.set(100); }).margin(6)
Button('50%').onClick(() => { this.set(50); }).margin(6)
Button('25%').onClick(() => { this.set(25); }).margin(6)
Button('停').onClick(() => { this.set(0); }).margin(6)
}
Text(this.log).margin({ top: 12 }).fontSize(14)
}.padding(20)
}
aboutToAppear() {
this.chip = evbio.findPwmChip();
if (this.chip.length === 0) {
this.log = '没有 pwmchip,查 &pwm0';
return;
}
evbio.writeText(this.chip + '/export', '0');
this.set(100);
}
set(pct: number) {
if (this.chip.length === 0) {
return;
}
let base: string = this.chip + '/pwm0';
if (pct <= 0) {
evbio.writeText(base + '/enable', '0');
this.percent = 0;
this.log = 'enable=0';
return;
}
let r1: string = evbio.writeText(base + '/period', '40000');
let duty: number = Math.floor(40000 * pct / 100);
let r2: string = evbio.writeText(base + '/duty_cycle', duty.toString());
let r3: string = evbio.writeText(base + '/enable', '1');
this.percent = pct;
this.log = 'period ' + r1 + ' duty ' + r2 + ' en ' + r3;
}
}
第一次进页面就 100%。不要默认 10% 然后抱怨「风扇坏了」。
7. 现象对照
| 现象 | 原因 | 处理 |
|---|---|---|
| 有 pwmchip 无 pwm0/ | 没 export | `echo 0 > export` |
| duty 写入 EINVAL | 先写了 duty 或 duty>period | 先 period,duty≤period |
| 100% 不转、0% 狂转 | polarity inversed | 读 polarity,软件反着映射 |
| 25% 不转、100% 转 | 起转阈值 | 先 100% 再降 |
| SoC 脚有波、风扇不转 | 12 V 或驱动管 | 量座供电,别改 dts |
| 写了被改回去 | 开了 pwm-fan 温控 | 演示不要 SENSORS_PWM_FAN |
| 相机同时挂了 | 动了 pwm5 | pwm5 保持 disabled |
| 啸叫 | period 太大(1 kHz) | 改 40000(25 kHz) |
flowchart TD
A[风扇不转] --> B{pwmchip 在?}
B -->|无| C[&pwm0 okay / PWM_ROCKCHIP]
B -->|有| D{pwm0/ 目录在?}
D -->|无| E[export 0]
D -->|有| F{enable=1 且 duty=period?}
F -->|否| G[先 100%]
F -->|是仍不转| H{脚上有 25kHz?}
H -->|无| I[pinctrl / 写错 chip]
H -->|有| J[12V 和座上驱动管]
export 之后才有通道。period → duty → enable,第一次 duty 等于 period。chip 按控制器地址找,不要写死编号。
系列第 28 篇 · 芯片:瑞芯微 RK3568 · OpenHarmony 4.1(API 11) · Linux 5.10
更多推荐




所有评论(0)