对于风扇来说,只要供电就能工作。最开始的电脑使用的都是这种风扇。但是随着技术的发展,人们发现需要对这个风扇进行控制,因为风扇转的快噪音和功耗都会随之增加。于是,加上一根反馈线,让用户能够得知当前的转速。但是这样会遇到另外的两个问题:第一个问题是风扇电压变化,反馈线上的电压也会随之变化,范围大了读取这个反馈会很麻烦。第二个问题是:电压和风扇转速转速关系并不是线性的。比如,一个 12V 的风扇,12V时转速是 2000CPM,10V供电时转速时1000CPM,但是如果11V 供电时,转速很可能是 1100CPM。风扇的转速和风力噪音直接相关,用户想要得到一个大概的转速非常困难。最终 Intel 推出了一个方案:通过 PWM 来控制风扇转速,然后使用5V 信号作为当前转速反馈引脚。
我们最常见到的CPU的风扇通常都是 4 Pin的。供电要求12V,控制的PWM 为 5V 25KHz,转速反馈引脚为 5V 输出。
有些风扇是存在最低转速的,意思是哪怕有PWM 已经为0仍然能够旋转,有些不存在最低转速,PWM 比较低的时候就会停止转动。
了解了上述知识就可以得知我们为了实现控制 CPU 风扇,需要提供12V 电源,提供25Khz 的 PWM 信号,以及读取 5V 的转速输出。
电路设计如下,我们使用 Arduino Uno作为主控。外部12V 供电进入(DC1)之后,经过一个DC-DC 降压模块(来自DFROBOT 的DFR0831,DC-DC降压模块7~24V转5V/4A),降压为5V提供给 Arduino 作为风扇控制,然后风扇的转速反馈信号经过U3上拉后进入Arduino 即可读取。最终转速和当前的PWM设定显示在一个 1602LCD上。
PCB 设计如下,可以看到板子上带有4个按钮用于控制 PWM ,分别是-1、-10、+1、+10这样用户可以快速的改变 PWM 设置。
代码设计如下:
#include <LiquidCrystal_I2C.h>
// 保存当前设定的 PWM 值
#include<EEPROM.h>
LiquidCrystal_I2C lcd(0x3F, 16, 2);
// PWM 发生器相关定义
const byte OC1A_PIN = 9; //PWM输出引脚为 D9
// 定义 PWM 频率
const word PWM_FREQ_HZ = 25000; //Adjust this value to adjust the frequency
const word TCNT1_TOP = 16000000 / (2 * PWM_FREQ_HZ);
const int BTN1 = 13;
const int BTN2 = 12;
const int BTN3 = 11;
const int BTN4 = 10;
// 计算转速相关定义
const int TOTAL = 10; // 计算10次输出一次,这是一个简单的滤波
int InterruptPin = 2;// 接收风扇中断 D2
volatile long int counter = 0;
volatile long int t[TOTAL];
byte p = 0;
byte SavedPWM = 0;
byte lastPWM = -1;
byte pwm;
long int Elsp=0;
void setup() {
pinMode(BTN1, INPUT_PULLUP);
pinMode(BTN2, INPUT_PULLUP);
pinMode(BTN3, INPUT_PULLUP);
pinMode(BTN4, INPUT_PULLUP);
pinMode(OC1A_PIN, OUTPUT);
// Clear Timer1 control and count registers
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
// Set Timer1 configuration
// COM1A(1:0) = 0b10 (Output A clear rising/set falling)
// COM1B(1:0) = 0b00 (Output B normal operation)
// WGM(13:10) = 0b1010 (Phase correct PWM)
// ICNC1 = 0b0 (Input capture noise canceler disabled)
// ICES1 = 0b0 (Input capture edge select disabled)
// CS(12:10) = 0b001 (Input clock select = clock/1)
TCCR1A |= (1 << COM1A1) | (1 << WGM11);
TCCR1B |= (1 << WGM13) | (1 << CS10);
ICR1 = TCNT1_TOP;
Serial.begin(115200);
Serial.setTimeout(300);
pinMode(InterruptPin, INPUT);
attachInterrupt(0, speedX, FALLING );
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("PWM Fan CNT");
// PWM 存在地址0 上
SavedPWM = EEPROM.read(0);
if (SavedPWM>100) {
SavedPWM=0;
}
// 将上一次保存的 PWM 设定进去
setPwmDuty(SavedPWM);
pwm = SavedPWM;
lastPWM = pwm + 1;
}
void loop() {
if (pwm != lastPWM) {
Serial.println(pwm);
setPwmDuty(pwm);
lastPWM = pwm;
Elsp=millis();
}
// 计算当前转速
long int avg = 0;
counter = 0;
delay(1000);
p = (p + 1) % TOTAL;
t[p] = (long int)(counter * 60 / 2);
for (byte i = 0; i < TOTAL; i++) {
avg = avg + t[i];
}
// 输出转速
Serial.print("Current speed: ");
Serial.println(avg / TOTAL);
lcd.setCursor(0, 1);
lcd.print("Fan Speed:");
lcd.print(avg / TOTAL);
lcd.print(" ");
counter = 0;
pinMode(BTN1, INPUT_PULLUP);
if (digitalRead(BTN1) == LOW) {
delay(5);
if ((digitalRead(BTN1) == LOW) && (pwm > 9)) {
pwm = pwm - 10;
}
}
if (digitalRead(BTN2) == LOW) {
delay(5);
if ((digitalRead(BTN2) == LOW) && (pwm > 0)) {
pwm = pwm - 1;
}
}
if (digitalRead(BTN3) == LOW) {
delay(5);
if ((digitalRead(BTN3) == LOW) && (pwm <=100-10)) {
pwm = pwm + 10;
}
}
if (digitalRead(BTN4) == LOW) {
delay(5);
if ((digitalRead(BTN4) == LOW) && (pwm <100)) {
pwm = pwm + 1;
}
}
// 每隔5秒检查 pwm 是否已经改变,如果改变则保存
if ((SavedPWM!=pwm)&&(millis()-Elsp>5000)) {
Serial.println("Save current pwm");
Elsp=millis();
SavedPWM=pwm;
EEPROM.write(0,pwm);
}
}
void setPwmDuty(byte duty) {
Serial.print("Set pwm "); Serial.println(duty);
lcd.setCursor(0, 0);
lcd.print("Current pwm:");
lcd.print(duty);
lcd.print(" ");
OCR1A = (word) (duty * TCNT1_TOP)/ 100;
}
void speedX()//中断函数
{
counter++;
}
4 Pin fan 的 spec:
本文提到的电路图和PCB: