1. 概述

本文介绍如何使用 ESP8266 控制风扇转速,并通过 Web 服务器调整风扇速度及获取当前转速。该方案适用于支持 PWM 调速的 4Pin 风扇。

2. 所需工具

硬件:

  • ESP8266 开发板(NodeMCU / Wemos D1 Mini)
  • 4Pin PWM 风扇
  • 大 4Pin 转小 4Pin 线(2 个小 4Pin 12V 接口,2 个小 4Pin 5V 接口,每个小4pin有2个引脚,2个空的,空的可以插入杜邦线引脚,某宝几块钱,如果没有这线也可以其他方法实现供电)
  • 杜邦线(公对公)
  • 电源(12V,适配风扇功率)

软件:

  • Arduino IDE(安装 ESP8266 开发板库)
  • Postman 或浏览器(用于发送 HTTP 请求)

3. 连接示意图

风扇引脚说明

pwm风扇 功能 连接
12V 供电 小4pin 12v
GND 小4pin GND
PWM 调速 esp8266 D1
绿 TACHO 反馈 esp8266 D2

ESP8266 供电

线材 功能 连接
5V(红) 供电 小 4Pin 5V -> ESP8266 VIN
GND(黑) 接地 小 4Pin GND -> ESP8266 GND
D1 PWM 调速 接风扇
D2 TACHO 反馈 接风扇

4. 代码实现

代码功能

  • 连接 WiFi
  • 通过 HTTP 请求调整风扇转速(PWM 输出)
  • 读取风扇 TACHO 信号,计算 RPM
  • 通过 Web API 获取当前风扇转速
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

const char* ssid = "yang1234"; // wifi名字
const char* password = "y123456789"; // wifi密码

const int pwmPin = 5;    // D1, 控制风扇速度
const int tachPin = 4;   // D2, 读取风扇转速反馈信号

ESP8266WebServer server(80);
volatile int pulseCount = 0;
unsigned long lastTime = 0;
int fanSpeed = 255; // 默认全速

void IRAM_ATTR countPulse() {
  pulseCount++;
}

int getFanRPM() {
  unsigned long elapsedTime = millis() - lastTime;
  int rpm = (pulseCount * 30) / (elapsedTime / 1000); // 计算 RPM
  pulseCount = 0;
  lastTime = millis();
  return rpm;
}

void handleSetSpeed() {
  if (server.hasArg("speed")) {
    fanSpeed = server.arg("speed").toInt();
    fanSpeed = constrain(fanSpeed, 0, 255);
    analogWrite(pwmPin, fanSpeed);
    server.send(200, "text/plain", "Speed set to(0-255) " + String(fanSpeed));
  } else {
    server.send(400, "text/plain", "Missing 'speed' parameter");
  }
}

void handleGetRPM() {
  int rpm = getFanRPM();
  server.send(200, "text/plain", "Current RPM: " + String(rpm));
}

void setup() {
  Serial.begin(115200);
  pinMode(pwmPin, OUTPUT);
  pinMode(tachPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(tachPin), countPulse, FALLING);

  analogWrite(pwmPin, fanSpeed);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nConnected to WiFi");
  Serial.println(WiFi.localIP());

  server.on("/setSpeed", HTTP_GET, handleSetSpeed);
  server.on("/getRPM", HTTP_GET, handleGetRPM);
  server.begin();
}

void loop() {
  server.handleClient();
}

5. 使用方法

1️⃣ 连接 ESP8266

  • 上电后,ESP8266 连接 yang1234 WiFi,并获取 IP 地址(串口监视器查看)。
  • 假设 IP 地址为 192.168.31.100(可以通过路由器查看esp8266的ip)

2️⃣ 设置风扇速度

在浏览器或 Postman 访问:

http://192.168.31.100/setSpeed?speed=128
  • speed=0:最低速度
  • speed=128:50% 速度
  • speed=255:100% 速度

3️⃣ 获取风扇当前转速

在浏览器或 Postman 访问:

http://192.168.31.100/getRPM

返回:

Current RPM: 1200

6. 说明

  • analogWrite(pwmPin, speed); 控制风扇转速
  • attachInterrupt(digitalPinToInterrupt(tachPin), countPulse, FALLING); 监听 TACHO 信号,有风扇差异,计算rpm可能不一样,根据情况修改
  • getFanRPM() 计算风扇 RPM

7.控制2个风扇,一个电源风扇一个机箱风扇

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

const char* ssid = "yang1234"; // wifi名字
const char* password = "y123456789"; // wifi密码

// 风扇1配置
const int pwmPin1 = 5;    // D1, 控制风扇1速度
const int tachPin1 = 4;   // D2, 读取风扇1转速反馈信号
volatile int pulseCount1 = 0;
int fanSpeed1 = 50;       // 默认半速

// 风扇2配置
const int pwmPin2 = D5;   // D5, 控制风扇2速度
const int tachPin2 = D6;  // D6, 读取风扇2转速反馈信号
volatile int pulseCount2 = 0;
int fanSpeed2 = 50;       // 默认半速

ESP8266WebServer server(80);
unsigned long lastTime1 = 0;
unsigned long lastTime2 = 0;

void IRAM_ATTR countPulse1() {
  pulseCount1++;
}

void IRAM_ATTR countPulse2() {
  pulseCount2++;
}

int getFan1RPM() {
  unsigned long elapsedTime = millis() - lastTime1;
  int rpm = (pulseCount1 * 30) / (elapsedTime / 1000); // 计算 RPM
  pulseCount1 = 0;
  lastTime1 = millis();
  return rpm;
}

int getFan2RPM() {
  unsigned long elapsedTime = millis() - lastTime2;
  int rpm = (pulseCount2 * 30) / (elapsedTime / 1000); // 计算 RPM
  pulseCount2 = 0;
  lastTime2 = millis();
  return rpm;
}

void handleSetSpeed() {
  if (server.hasArg("fan") && server.hasArg("speed")) {
    int fan = server.arg("fan").toInt();
    int speed = server.arg("speed").toInt();
    speed = constrain(speed, 0, 255);
    
    if (fan == 1) {
      fanSpeed1 = speed;
      analogWrite(pwmPin1, fanSpeed1);
      server.send(200, "text/plain", "Fan 1 speed set to " + String(fanSpeed1));
    } else if (fan == 2) {
      fanSpeed2 = speed;
      analogWrite(pwmPin2, fanSpeed2);
      server.send(200, "text/plain", "Fan 2 speed set to " + String(fanSpeed2));
    } else {
      server.send(400, "text/plain", "Invalid fan number");
    }
  } else {
    server.send(400, "text/plain", "Missing 'fan' or 'speed' parameter");
  }
}

void handleGetRPM() {
  if (server.hasArg("fan")) {
    int fan = server.arg("fan").toInt();
    
    if (fan == 1) {
      int rpm = getFan1RPM();
      server.send(200, "text/plain", "Fan 1 RPM: " + String(rpm));
    } else if (fan == 2) {
      int rpm = getFan2RPM();
      server.send(200, "text/plain", "Fan 2 RPM: " + String(rpm));
    } else {
      server.send(400, "text/plain", "Invalid fan number");
    }
  } else {
    // 返回两个风扇的RPM
    int rpm1 = getFan1RPM();
    int rpm2 = getFan2RPM();
    server.send(200, "text/plain", "Fan 1 RPM: " + String(rpm1) + "\nFan 2 RPM: " + String(rpm2));
  }
}

void setup() {
  Serial.begin(115200);
  
  // 初始化风扇1引脚
  pinMode(pwmPin1, OUTPUT);
  pinMode(tachPin1, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(tachPin1), countPulse1, FALLING);
  
  // 初始化风扇2引脚
  pinMode(pwmPin2, OUTPUT);
  pinMode(tachPin2, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(tachPin2), countPulse2, FALLING);

  // 设置初始风扇速度
  analogWrite(pwmPin1, fanSpeed1);
  analogWrite(pwmPin2, fanSpeed2);
  
  // 初始化WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nConnected to WiFi");
  Serial.println(WiFi.localIP());

  // 设置HTTP路由
  server.on("/setSpeed", HTTP_GET, handleSetSpeed);
  server.on("/getRPM", HTTP_GET, handleGetRPM);
  server.begin();
  
  // 初始化计时
  lastTime1 = millis();
  lastTime2 = millis();
}

void loop() {
  server.handleClient();
}

升级版控制风扇,ota+界面优化

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <Updater.h>   // 用于 OTA 固件更新

const char* ssid = "yang1234";
const char* password = "y123456789";

const int pwmPin = 5;   // D1 - PWM 控制风扇
const int tachPin = 4;  // D2 - 风扇转速信号

ESP8266WebServer server(80);

volatile unsigned long pulseCount = 0;
unsigned long lastTime = 0;
int fanSpeed = 255;

// ====== 获取系统信息(JSON) ======
String getSystemInfoJSON() {
  float freeMemMB = ESP.getFreeHeap() / 1024.0 / 1024.0;
  float flashSizeMB = ESP.getFlashChipRealSize() / 1024.0 / 1024.0;
  float sketchSizeMB = ESP.getSketchSize() / 1024.0 / 1024.0;
  float freeSketchMB = ESP.getFreeSketchSpace() / 1024.0 / 1024.0;

  String json = "{";
  json += "\"chip_id\":" + String((uint32_t)ESP.getChipId()) + ",";
  json += "\"cpu_freq_mhz\":" + String(ESP.getCpuFreqMHz()) + ",";
  json += "\"free_mem_mb\":" + String(freeMemMB, 3) + ",";
  json += "\"flash_size_mb\":" + String(flashSizeMB, 3) + ",";
  json += "\"sketch_size_mb\":" + String(sketchSizeMB, 3) + ",";
  json += "\"free_sketch_mb\":" + String(freeSketchMB, 3);
  json += "}";
  return json;
}

// ====== 中断:计数 ======
void IRAM_ATTR countPulse() {
  pulseCount++;
}

// ====== RPM 计算 ======
int getFanRPM() {
  unsigned long now = millis();
  unsigned long elapsedTime = now - lastTime;
  if (elapsedTime < 1000) return -1;
  int rpm = (pulseCount * 60000) / (elapsedTime * 2); 
  pulseCount = 0;
  lastTime = now;
  return rpm;
}

// ====== 网页界面 ======
void handleRoot() {
  String html = R"rawliteral(
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>ESP8266 风扇控制</title>
<style>
body { font-family: sans-serif; text-align: center; background: #f2f2f2; }
.container { background: white; border-radius: 10px; padding: 20px; max-width: 420px; margin: auto; }
input[type=range] { width: 80%; }
.info { margin-top: 15px; font-size: 0.9em; color: #333; text-align: left;}
</style>
</head>
<body>
<div class="container">
<h1>风扇调速面板</h1>
<p>当前速度: <span id="spd">--</span></p>
<input type="range" min="0" max="255" value="0" id="speedSlider">
<p>当前转速: <span id="rpm">--</span> RPM</p>
<h3>系统信息</h3>
<div class="info" id="sysinfo">加载中...</div>
<p><a href="/update">固件更新</a></p>
</div>
<script>
const slider = document.getElementById('speedSlider');
const spdLabel = document.getElementById('spd');

function updateSpeedLabel(val){
  let percent = Math.round(val / 255 * 100);
  spdLabel.textContent = `${val} (${percent}%)`;
}

slider.addEventListener('input', () => {
  updateSpeedLabel(slider.value);
  fetch(`/setSpeed?speed=${slider.value}`);
});

window.addEventListener('load', () => {
  fetch('/getSpeed').then(r=>r.text()).then(val=>{
    slider.value = val;
    updateSpeedLabel(val);
  });
});

setInterval(() => {
  fetch('/getRPM').then(r=>r.text()).then(t=>{
    document.getElementById('rpm').textContent = t.replace(/[^0-9-]/g,"");
  });
  fetch('/sysinfo').then(r=>r.json()).then(info=>{
    document.getElementById('sysinfo').innerHTML = 
      `芯片ID: ${info.chip_id}<br>` +
      `CPU频率: ${info.cpu_freq_mhz} MHz<br>` +
      `空闲内存: ${info.free_mem_mb.toFixed(3)} MB<br>` +
      `Flash容量: ${info.flash_size_mb.toFixed(3)} MB<br>` +
      `固件大小: ${info.sketch_size_mb.toFixed(3)} MB<br>` +
      `可用固件空间: ${info.free_sketch_mb.toFixed(3)} MB`;
  });
}, 2000);
</script>
</body>
</html>
)rawliteral";
  server.send(200, "text/html; charset=UTF-8", html);
}

// ====== 控制接口 ======
void handleSetSpeed() {
  if (server.hasArg("speed")) {
    fanSpeed = constrain(server.arg("speed").toInt(), 0, 255);
    analogWrite(pwmPin, fanSpeed);
    server.send(200, "text/plain; charset=UTF-8", "速度已设置为: " + String(fanSpeed));
  } else {
    server.send(400, "text/plain; charset=UTF-8", "缺少 speed 参数");
  }
}

void handleGetSpeed() {
  server.send(200, "text/plain; charset=UTF-8", String(fanSpeed));
}

void handleGetRPM() {
  int rpm = getFanRPM();
  if (rpm < 0) {
    server.send(200, "text/plain; charset=UTF-8", "正在采样...");
  } else {
    server.send(200, "text/plain; charset=UTF-8", String(rpm));
  }
}

void handleSysInfo() {
  server.send(200, "application/json; charset=UTF-8", getSystemInfoJSON());
}

// ====== OTA 更新页面(中文) ======
void handleUpdatePage() {
  String html = R"rawliteral(
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>固件更新</title>
<style>
body { font-family: sans-serif; background: #f2f2f2; text-align: center; }
.container { background: white; padding: 20px; border-radius: 10px; max-width: 400px; margin: auto; }
input { margin: 10px; }
</style>
</head>
<body>
<div class="container">
<h2>ESP8266 固件更新</h2>
<form method="POST" action="/update" enctype="multipart/form-data">
<p>选择固件文件 (.bin):</p>
<input type="file" name="firmware">
<br>
<input type="submit" value="上传并更新">
</form>
</div>
</body>
</html>
)rawliteral";
  server.send(200, "text/html; charset=UTF-8", html);
}

// 处理 OTA 上传
void handleUpdateUpload() {
  HTTPUpload& upload = server.upload();
  if (upload.status == UPLOAD_FILE_START) {
    Serial.printf("开始更新: %s\n", upload.filename.c_str());
    if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) {
      Update.printError(Serial);
    }
  } else if (upload.status == UPLOAD_FILE_WRITE) {
    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
      Update.printError(Serial);
    }
  } else if (upload.status == UPLOAD_FILE_END) {
    if (Update.end(true)) {
      Serial.printf("更新成功: %u 字节\n重启中...\n", upload.totalSize);
    } else {
      Update.printError(Serial);
    }
  }
}

void handleUpdateDone() {
  if (Update.hasError()) {
    server.send(200, "text/plain; charset=UTF-8", "更新失败!");
  } else {
    server.send(200, "text/plain; charset=UTF-8", "更新成功,设备即将重启。");
    delay(1000);
    ESP.restart();
  }
}

// ====== SETUP ======
void setup() {
  Serial.begin(115200);
  pinMode(pwmPin, OUTPUT);
  pinMode(tachPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(tachPin), countPulse, FALLING);

  analogWriteFreq(25000);   // 设置 PWM 频率 25kHz
  analogWrite(pwmPin, fanSpeed);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\n已连接 WiFi: " + WiFi.localIP().toString());

  server.on("/", handleRoot);
  server.on("/setSpeed", handleSetSpeed);
  server.on("/getSpeed", handleGetSpeed);
  server.on("/getRPM", handleGetRPM);
  server.on("/sysinfo", handleSysInfo);

  // OTA 更新路由
  server.on("/update", HTTP_GET, handleUpdatePage);
  server.on("/update", HTTP_POST, handleUpdateDone, handleUpdateUpload);

  server.begin();
}

// ====== LOOP ======
void loop() {
  server.handleClient();
}

8. 结论

本方案通过 ESP8266 控制风扇 PWM 实现调速,并获取风扇实时转速数据。用户可以通过 Web API 远程控制风扇运行状态,适用于 DIY 智能风扇控制系统。