#树莓派驱动PWM 4pin 风扇,实现PWM调速及风扇转速测量
###起因
树莓派外壳自带散热风扇不带调速功能,噪音很吵,难以忍受。随后看到有三极管配合脚本开关风扇,亦可pwm调速。拆了充电器上的三极管,按图接线,没有设备工具只能把线绕上去,接上树莓派,可以工作,很是高兴。调低转速却听到明显的哒哒声,调高频率就是嗡嗡声音,作为强迫症患者很是难受,随在某宝入手了一个pwm风扇,连上后情况依旧。经过几天研究,发现原因所在,现分享给大家。
###噪音原因
风扇运转时的噪音主要有轴噪和风噪,运转速度越快越明显,而经改造后的风扇在低转速下的哒哒声和嗡嗡声是由pwm脉冲引起的,在中高转速下,轴噪和风噪会掩盖掉,所以在低转速下就很明显。
###解决方案
轴噪和风噪与风扇自身转轴类型和叶片结构有关,不可避免,只能在满足散热的情况下降低转速。或者实力雄厚的可使用磁悬浮轴承的静音风扇,而我只能在其他方面想办法了。
人耳能听到的声音频率为20-20kHz,那么想办法将pwm频率调到这个范围之外不就可以解决了吗,于是修改python脚本,调低频率,风扇运行顿挫感强烈,并伴随着震动及哒哒哒的低频噪音。调高频率,风扇运行良好,有明显的高频噪音,无论我频率调到多大都始终存在,且稳定在一个频率。后来发现python脚本gpio的pwm频率控制很是糟糕,最高只能到6kHz,且频率越高越与设定值偏差越大,随弃之。
后使用c语言使用wiringpi库来控制。wiringpi的pwm分为硬件pwm和软件pwm,软件pwm频率100Hz,也不满足要求,只有使用硬件pwm。
wiringpi库在官方原版系统上有附带,我使用的是树莓派4b ubuntu server arm64系统,需要安装,安装方法建议使用git克隆 github地址[https://github.com/WiringPi/WiringPi/](https://github.com/WiringPi/WiringPi/) 国内下载速度很慢请耐心等待。
wiringpi库官方已停止更新,最终版本2.50,对树莓派4b支持不是很好,该库是非官方镜像,已更新至2.60,在树莓派4b运行正常。
安装git命令
sudo apt update
sudo apt install git
git命令
`git clone https://github.com/WiringPi/WiringPi.git`
下载完后运行以下命令安装
cd WiringPi
./build
树莓派4b gcc10 安装过程中会报错,github评论区给出了解决方法 [https://github.com/WiringPi/WiringPi/issues/83](https://github.com/WiringPi/WiringPi/issues/83) 使用补丁程序修补文件就可正常安装。
文件名`0001-patch-to-build-under-gcc-10.x.patch`
补丁文件内容
From e198e4855660b5a8f5b8896c0f9e6753b798973e Mon Sep 17 00:00:00 2001
From: gearhead <ys3al35l@gmail.com>
Date: Sat, 26 Sep 2020 11:39:54 -0500
Subject: [PATCH] patch to build under gcc 10.x
https://gcc.gnu.org/gcc-10/porting_to.html
---
wiringPiD/drcNetCmd.h | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/wiringPiD/drcNetCmd.h b/wiringPiD/drcNetCmd.h
index 23f7dc1..3cbd639 100644
--- a/wiringPiD/drcNetCmd.h
+++ b/wiringPiD/drcNetCmd.h
@@ -35,7 +35,7 @@
#define DRCN_ANALOG_READ 9
-struct drcNetComStruct
+extern struct drcNetComStruct
{
uint32_t pin ;
uint32_t cmd ;
--
2.28.0
复制补丁文件内容 `vi 0001-patch-to-build-under-gcc-10.x.patch` 粘贴保存,运行下条命令修补文件,
`patch wiringPiD/drcNetCmd.h 0001-patch-to-build-under-gcc-10.x.patch`
然后运行 `./bulid` 命令就可正常安装,安装中有警告不用担心。安装完成运行 `gpio -v` 和 `gpio readall` 出现gpio接口表就表示安装成功。
`gpio -v`
gpio version: 2.60
Copyright (c) 2012-2018 Gordon Henderson
This is free software with ABSOLUTELY NO WARRANTY.
For details type: gpio -warranty
Raspberry Pi Details:
Type: Pi 4B, Revision: 02, Memory: 2048MB, Maker: Sony
* Device tree is enabled.
*--> Raspberry Pi 4 Model B Rev 1.2
* This Raspberry Pi supports user-level GPIO access.
`gpio readall`
+-----+-----+---------+------+---+---Pi 4B--+---+------+---------+-----+-----+
| BCM | wPi | Name | Mode | V | Physical | V | Mode | Name | wPi | BCM |
+-----+-----+---------+------+---+----++----+---+------+---------+-----+-----+
| | | 3.3v | | | 1 || 2 | | | 5v | | |
| 2 | 8 | SDA.1 | ALT0 | 1 | 3 || 4 | | | 5v | | |
| 3 | 9 | SCL.1 | ALT0 | 1 | 5 || 6 | | | 0v | | |
| 4 | 7 | GPIO. 7 | IN | 1 | 7 || 8 | 1 | ALT5 | TxD | 15 | 14 |
| | | 0v | | | 9 || 10 | 1 | IN | RxD | 16 | 15 |
| 17 | 0 | GPIO. 0 | IN | 0 | 11 || 12 | 0 | ALT5 | GPIO. 1 | 1 | 18 |
| 27 | 2 | GPIO. 2 | IN | 0 | 13 || 14 | | | 0v | | |
| 22 | 3 | GPIO. 3 | IN | 0 | 15 || 16 | 0 | IN | GPIO. 4 | 4 | 23 |
| | | 3.3v | | | 17 || 18 | 0 | IN | GPIO. 5 | 5 | 24 |
| 10 | 12 | MOSI | ALT0 | 0 | 19 || 20 | | | 0v | | |
| 9 | 13 | MISO | ALT0 | 0 | 21 || 22 | 0 | IN | GPIO. 6 | 6 | 25 |
| 11 | 14 | SCLK | ALT0 | 0 | 23 || 24 | 1 | OUT | CE0 | 10 | 8 |
| | | 0v | | | 25 || 26 | 1 | OUT | CE1 | 11 | 7 |
| 0 | 30 | SDA.0 | IN | 1 | 27 || 28 | 1 | IN | SCL.0 | 31 | 1 |
| 5 | 21 | GPIO.21 | IN | 1 | 29 || 30 | | | 0v | | |
| 6 | 22 | GPIO.22 | IN | 1 | 31 || 32 | 0 | IN | GPIO.26 | 26 | 12 |
| 13 | 23 | GPIO.23 | IN | 0 | 33 || 34 | | | 0v | | |
| 19 | 24 | GPIO.24 | IN | 0 | 35 || 36 | 0 | IN | GPIO.27 | 27 | 16 |
| 26 | 25 | GPIO.25 | IN | 0 | 37 || 38 | 0 | IN | GPIO.28 | 28 | 20 |
| | | 0v | | | 39 || 40 | 0 | IN | GPIO.29 | 29 | 21 |
+-----+-----+---------+------+---+----++----+---+------+---------+-----+-----+
| BCM | wPi | Name | Mode | V | Physical | V | Mode | Name | wPi | BCM |
+-----+-----+---------+------+---+---Pi 4B--+---+------+---------+-----+-----+
###硬件设备
* 风扇某宝购入的 4pin pwm 4010 5v 风扇
* 树莓派4b 2g版
* ubuntu server arm64 20.10
###接线方法
* 红色 5v电源正极
* 黑色 0v电源负极
* 黄色 10号针脚 (wPi16 BCM15) 风扇测速线
* 蓝色 12号针脚 (wPi1 BCM18) PWM控制线
不同风扇pwm和测速线颜色可能不同,请自己测试。同一个针脚不同标准编号不同,pwm只能接12号脚,电源、测速可接其他脚位,对应编号搞正确即可。
###所用函数介绍
这里只介绍几个下面需用到的函数,其他情自行搜索参考手册。
####初始化gpio引脚
`int wiringPiSetup (void) ` 使用wiringPi 编号初始化引脚
`int wiringPiSetupGpio (void)` 使用BCM 编号初始化引脚
####配置引脚
`void pinMode (int pin, int mode)`
pin:引脚号
mode:指定引脚的IO模式
可取的值:INPUT、OUTPUT、PWM_OUTPUT,GPIO_CLOCK (输入、输出、pwm输出、gpio时钟)
只有wiringPi 引脚编号下的1脚(BCM下的18脚)支持PWM硬件输出,只有wiringPi编号下的7(BCM下的4号)支持GPIO_CLOCK输出,所以风扇pwm线只能接1号针脚。
`void pullUpDnControl (int pin, int pud)`
对一个设置IO模式为 INPUT 的输入引脚设置拉电阻模式。
pud:拉电阻模式
可取的值:PUD_OFF 不启用任何拉电阻。关闭拉电阻
PUD_DOWN 启用下拉电阻,引脚电平拉到GND
PUD_UP 启用上拉电阻,引脚电平拉到3.3v
####pwm输出频率控制
`pwmSetRange(int value)` pwm脉宽
`pwmSetClock(int value)` 分频系数
`pwmSetMode (int mode)` pwm运行模式
pwm发生器可以运行在2种模式下,通过参数指定:
PWM_MODE_BAL :树莓派默认的PWM模式
PWM_MODE_MS :传统的pwm模式
pwm输出频率由分频系数和pwm脉宽共同决定,树莓派gpio时钟主频19.2MHz
主频震动分频系数次作为一个pwm最小调制单位,用分频系数重新划分时钟主频。
脉宽就是最大占空比,pwm频率为19.2M/分频系数/脉宽。
默认脉宽1024,默认分频系数32。pwm频率为 19.2*1000*1000/1024/32=585.9375Hz。驱动风扇的pwm频率在21k-25kHz,在这里可选择分频系数16 脉宽50、60,应频率24kHz,20kHz。分频系数8 脉宽100 120,对应频率24kHz,20kHz,具有更大的调控数
####pwm输出
`void pwmWrite (int pin, int value)`
输出一个值到PWM寄存器,控制PWM输出。
`void delay (unsigned int howLong)`
将当前执行流暂停 指定的毫秒数。因为Linux本身是多线程的,所以实际暂停时间可能会长一些。
`int wiringPiISR (int pin, int edgeType, void (*function)(void))`
一个中断处理注册函数,返回值:返回负数则代表注册失败
edgeType:触发的方式。
INT_EDGE_FALLING:下降沿触发
INT_EDGE_RISING:上升沿触发
INT_EDGE_BOTH :上下降都会触发
function:中断处理函数的指针,它是一个无返回值,无参数的函数。需写在中断处理前面,使用时&函数名即可。
注册的函数会在中断发生时执行 这个注册的中断处理函数会和main函数并发执行(同时执行,谁也不耽误谁)
当本次中断函数还未执行完毕,这个时候树莓派又触发了一个中断,那么这个后来的中断不会被丢弃,它仍然可以被执行。但是wiringPi最多可以跟踪并记录后来的仅仅1个中断,如果不止1个,则他们会被忽略,得不到执行。
###代码
c语言
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
#include<wiringPi.h>
// 控制风扇的GPIO接口 wiringPi标准
#define FAN_PWM 1
#define FAN_SPE 16
// 启动阈值 高于它则开启风扇
#define STA_TEMP 55
// 低温阈值 低于它则关闭风扇
#define MIN_TEMP 45
// 高温阈值 高于它则全速运转
#define MAX_TEMP 65
// 多长时间读取一次CPU温度 单位毫秒 总循环时间 加上测速时间 速度转换时间
#define SAMPLING 10000
// PWM最低占空比 避免太低启动不了 不能超过占空比最大值
// 转速太低散热效果太低 调高可加强散热效果 可运行test功能测试
#define dc_base 18
// 测速时间 单位毫秒 有2秒加减速时间 总测试时间再加2秒 时间越久越准确
#define TEST_T 4000
// 占空比最大值 根据想要的频率计算
#define Ran_m 50
// 获取cpu温度
int cpu_temp() {
int temp; FILE *fp_rt;
fp_rt=fopen("/sys/class/thermal/thermal_zone0/temp", "r");
fscanf(fp_rt,"%d", &temp); fclose(fp_rt);
return temp;
}
// 获取cpu频率
int cpu_freq() {
int freq; FILE *fp_rf;
fp_rf=fopen("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq", "r");
fscanf(fp_rf,"%d", &freq); fclose(fp_rf);
return freq;
}
// 获取cpu最高频率
int cpu_freq_max() {
int freq; FILE *fp_rfm;
fp_rfm=fopen("/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq", "r");
fscanf(fp_rfm,"%d", &freq); fclose(fp_rfm);
return freq;
}
// 格式化输出当前时间
char timeNow[20];
int time_n() {
time_t t = time(0);
strftime(timeNow, 20, "%Y-%m-%d %H:%M:%S", localtime(&t));
return 0;
}
// 日志记录
char log_t[80];
int log_w() {
FILE *fp_w;
fp_w=fopen("/var/log/pwmfan.log", "at+");
fprintf(fp_w, "%s", log_t); fclose(fp_w);
return 0;
}
// 测速回调函数 记录回调次数
static int num=0;
void my_callback(void) { num++; }
// 风扇测速
// 每转一圈返回两次信号 不同风扇可能不同
// 转速=((信号次数/2)/(时间/1000))*60秒=30000*次数/时间
int speedtest() {
int num_s, num_f, speed;
delay(2000); num_s =num;
delay(TEST_T); num_f =num;
speed = (num_f - num_s) * 30000 / TEST_T;
return speed;
}
// gpio初始化
int gpioset() {
// 使用wiring编码去初始化GPIO序号
wiringPiSetup();
// 设置GPIO电气属性 启动硬件pwm需su权限
// 硬件pwm 只支持1号脚位
pinMode(FAN_PWM, PWM_OUTPUT);
// 对输入引脚上拉电平 PUD_UP
pinMode(FAN_SPE, INPUT);
pullUpDnControl(FAN_SPE, PUD_UP);
// 设置pwm频率
// 时钟主频19.2MHz pwm脉宽范围: 0~50 分频系数: 16
// pwm频率24kHz 19.2MHz / 50 / 16 = 24kHz
// pwmSetRange(Ran_m): 最大空占比, pwmSetClock(16): 分频系数
// pwm模式设置 PWM_MODE_MS/PWM_MODE_BAL 默认BAL 需设置为MS
pwmSetRange(Ran_m); pwmSetClock(16);
pwmSetMode(PWM_MODE_MS);
return 0;
}
// 测速 边缘检测注册 监测输入脚位电平变化 下拉则调用回调函数计数
int speedset() {
wiringPiISR(FAN_SPE, INT_EDGE_FALLING, &my_callback);
return 0;
}
// 启动风扇 开启调速功能
int start_fan(void) {
//定义变量
double temp;
int freq, freq_max, dc, speed;
int is_work, i, dc_t;
// 初始化gpio 开启测速功能
gpioset(); speedset(); time_n();
printf("%s Start Fan Service\n", timeNow);
sprintf(log_t, "%s Start Fan Service\n", timeNow); log_w();
// 风扇启动测试 缓慢启动 10秒达到最大转速
//pwm硬件输出 pwmWrite(FAN_PWM, dc);
for (i = 10; i <= Ran_m; i++) { pwmWrite(FAN_PWM, i); delay(250); }
speed = speedtest(); time_n();
is_work = 1; dc_t = Ran_m;
if (speed > 0) {
printf("%s Speed Tested OK PWM:100%% Speed: %4dRPM\n", timeNow, speed);
sprintf(log_t, "%s Speed Tested OK PWM:100%% Speed: %4dRPM\n", timeNow, speed); log_w();
}
else {
printf("%s Speed Tested ERROR\n", timeNow);
sprintf(log_t, "%s Speed Tested ERROR\n", timeNow); log_w();
}
// 获取cpu温度 循环调速
freq_max = cpu_freq_max() / 1000;
while (1) {
temp = cpu_temp() / 1000.0; freq = cpu_freq() / 1000;
if (is_work == 1) {
// 低于低温阈值 则关闭风扇
if (temp <= MIN_TEMP) { dc = 0; }
// 超过高温阈值 则全速运行
else if (temp >= MAX_TEMP) { dc = Ran_m; }
// 在低温阈值和高温阈值之间时 根据温度线性使用PWM控制风扇转速
else {
dc = dc_base + (temp - MIN_TEMP) / (MAX_TEMP - MIN_TEMP) * (Ran_m - dc_base);
}
// for循环 线性变换速度 避免急启急停
if (dc > 0) {
if (dc < dc_t ) {
for (i = dc_t - 1; i >= dc; i--) { pwmWrite(FAN_PWM, i); delay(400); }
}
else {
for (i = dc_t + 1; i <= dc; i++) { pwmWrite(FAN_PWM, i); delay(400); }
}
is_work = 1; dc_t = dc;
speed = speedtest(); time_n();
printf("%s Temp: %2.1f°C Freq:%4d/%4dMhz PWM:%3d%% Speed: %4dRPM\n", timeNow, temp, freq, freq_max, dc*100/Ran_m, speed);
sprintf(log_t, "%s Temp: %2.1f°C Freq:%4d/%4dMhz PWM:%3d%% Speed: %4dRPM\n", timeNow, temp, freq, freq_max, dc*100/Ran_m, speed);
}
else {
if (dc_t > 0 ) {
for (i = dc_t; i >= 0; i--) { pwmWrite(FAN_PWM, i); delay(400); }
}
is_work = 0; dc_t = 0; time_n();
printf("%s Temp: %2.1f°C Freq:%4d/%4dMhz Stop Running\n", timeNow, temp, freq, freq_max);
sprintf(log_t, "%s Temp: %2.1f°C Freq:%4d/%4dMhz Stop Running\n", timeNow, temp, freq, freq_max);
}
log_w();
}
else {
// 温度低于启动温度则一直停止
if (temp >= STA_TEMP) { is_work = 1; }
delay(SAMPLING);
}
// 接口守护 防止接口被其它程序调用而停止
if (speed==0&&dc!=0) { gpioset(); }
// 设置采样频率
delay(SAMPLING);
}
return 0;
}
// 停止进程 停止风扇
int stop_fan() {
// 杀死进程 需su权限 修改本文件名需修改
system("sudo kill `ps -ef | grep '[p]wmfan start' | awk '{ print $2 }'` >/dev/null 2>&1");
gpioset();
pwmWrite(FAN_PWM, 0); time_n();
printf("%s Stop Fan Service\n", timeNow);
sprintf(log_t, "%s Stop Fan Service\n",timeNow); log_w();
return 0;
}
// 风扇转速测试 不同空占比下的速度测试
// 先杀死风扇服务进程 避免干扰 测试完成需手动启动风扇服务
void test() {
int i, speed;
stop_fan(); gpioset(); speedset();
printf("Fan Speed in Different PWM DutyCycle\nPWM DutyCycle 0 => %d Time Needed: %ds\n", Ran_m, Ran_m*(TEST_T/1000+2));
for (i = 0; i <= Ran_m; i++) {
pwmWrite(FAN_PWM, i);
speed = speedtest(); time_n();
printf("%s Pwm: %2d Speed: %4dRPM\n",timeNow, i, speed);
}
delay(1000);
printf("PWM DutyCycle %d => 0 Time Needed: %ds\n", Ran_m, Ran_m*(TEST_T/1000+2));
for (i = Ran_m; i >= 0; i--) {
pwmWrite(FAN_PWM, i);
speed = speedtest(); time_n();
printf("%s Pwm: %2d Speed: %4dRPM\n",timeNow, i, speed);
}
printf("Test Finished Please Start Pwmfan Service\n");
}
// 主函数
int main(int argc,char **argv) {
if (argc<=1) {printf("Usage: sudo %s [start|stop|restart|speed|test|help|version]\n", argv[0]);}
else {
if (strcmp(argv[1], "start") == 0 ) { start_fan(); }
else if (strcmp(argv[1], "stop") == 0 ) { stop_fan(); }
else if (strcmp(argv[1], "restart") == 0 ) {
stop_fan(); start_fan(); }
else if (strcmp(argv[1], "speed") == 0 ) {
gpioset(); speedset();
printf("Get the Fan Speed...\n");
printf("CPU Temp: %2.1f°C Fan Speed: %4dRPM\n", cpu_temp()/1000.0, speedtest()); }
else if (strcmp(argv[1], "test") == 0 ) { test(); }
else if (strcmp(argv[1], "help") == 0 ) {
printf("使用方法: %s [start|stop|restart|speed|test|help|version]\n", argv[0]);
printf(" start: 启动风扇服务\n");
printf(" stop: 停止风扇服务\n");
printf(" restart: 重启风扇服务\n");
printf(" speed: 测量当前风扇速度\n");
printf(" test: 测试不同占空下风扇速度\n");
printf(" help: 显示帮助\n");
printf(" version: 显示版本\n");
}
else if (strcmp(argv[1], "version") == 0 ) {
printf("PWM Fan Service\nVersion: 1.1.0\nBy: ....\nHave a Nise Day\n"); }
else { printf("Usage: sudo %s [start|stop|restart|speed|test|help|version]\n", argv[0]); }
}
return 0;
}
代码请命名为pwmfan.c
编译命令 动态链接库
`gcc -Wall -o pwmfan pwmfan.c -lwiringPi -lcrypt -lm -lrt -lpthread`
编译命令 动态链接库
`gcc -static -Wall -o pwmfan pwmfan.c -lwiringPi -lcrypt -lm -lrt -lpthread`
###使用方法
使用方法: `sudo ./pwmfan [start|stop|restart|speed|test|help|version]`
start: 启动风扇服务
stop: 停止风扇服务
restart: 重启风扇服务
speed: 测量当前风扇速度
test: 测试不同占空下风扇速度
help: 显示帮助
version: 显示版本
必须以管理员权限运行,普通用户会报错。开启时会均匀加速至最高速度,随后根据cpu温度进行线性调控。低速运转时几乎听不到声音。
风扇测速是在输入引脚上拉电平,转动时霍尔传感器感应磁场变化导通电路下拉电平激活中断处理函数计数,通过一定时间内的次数计算转速,测试时间越久越准确。不同风扇可能方式不一,转一圈返回信号数不一,请自行测试。本风扇转一圈返回2次信号。
###开机启动
建议设置为系统服务开机启动。
设置方法 新建系统服务文件,开机可启动。
`sudo vi /usr/lib/systemd/system/pwmfan.service`
文件内容
[Unit]
Description=Fan Service Control by PWM
#这里填简介
After=systemd-timesyncd.service
#这里填上你这个脚本所需要的前置service,都在/etc/systemd/system/下
[Service]
Type=simple
ExecStart=/home/ubuntu/pwmfan start
ExecStop=/home/ubuntu/pwmfan stop
ExecRestart=/home/ubuntu/pwmfan restart
#这里填脚本或可执行文件路径或命令,后面也可以跟参数,换成自己的路径
[Install]
WantedBy=multi-user.target
开机可启动
`sudo systemctl enable pwmfan`
启动命令 (启动/停止/重启)
`sudo systemctl start pwmfan`
`sudo systemctl stop pwmfan`
`sudo systemctl restart pwmfan`