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