模式、PWM请求和真实转速分别看
本篇从温度读数走到低压执行器:OFF停、MANUAL调占空、AUTO带温度迟滞。GPIO只给控制信号,电机由外部稳压5V直接供电;TACH另提供转速线索。界面命令不是转动,转速脉冲也不是散热足够或空气流量证明。
固定NF-A4x10 5V PWM四线款、TI SN74AHCT125N PDIP-14与SHT40 PID4885,不套12V/24V、两/三线或匿名MOSFET模块。本站没有电压/PWM/RPM/HA实测,故障与自动控制只给清楚策略和待执行方法,不能当关键设备独立热保护。
参考:[1] Noctua NF-A4x10 5V PWM具体规格与线色[2] Noctua PWM白皮书:频率、电平、推挽与TACH[3] TI SN74AHCT125 Rev.R:PDIP-14及推荐条件
电机外供,缓冲器只驱PWM
外部5.0V±2%≥0.5A只接风扇pin2黄线和AHCT pin14;外部负端、风扇pin1黑线、芯片pin7及板GND共地,正端不并板5V/3V3。板由桥USB供电。GPIO5/J1-5经1k到AHCT pin2/1A,另100k下拉;pin1/1OE接地启用,pin3/1Y只接风扇pin4蓝线。100nF贴近芯片电源脚。
按PDIP-14顶视缺口/圆点识别脚位,未用OE 4/10/13拉外部5V禁用,A 5/9/12接地,Y 6/8/11留空。TI的5.5V供电上限不是风扇PWM上限:Noctua PWM绝对上限5.25V,所以本例外供限定4.9–5.1V并要实查瞬态。风扇最大0.07A是规格,电机电流不走GPIO或缓冲输出;Noctua不推荐简单开集驱动,本例用非反相推挽25kHz。
绿线pin3 TACH到GPIO7/J1-7,10k只上拉板3V3,不能拉5V;名义约0.33mA低于厂家输出条件,但仍须核波形。SHT40 VIN板3V3、SDA GPIO8/J1-12、SCL GPIO9/J1-15、地共地,0x44/100kHz。GPIO4经330Ω到WP7113ID阳极、阴极地。断电量通断、固定护网再上电,不用手堵转。
只在USB与外供都已稳定供电后切MANUAL/AUTO。停机先选OFF、确认Applied PWM为0,再断电;调换接线先全部断电。1k/100k电阻不构成断电隔离,部分供电和启动瞬态仍待实际电气验证。
| 信号 | 去向 |
|---|---|
| 外部5V / GND | 只供风扇和AHCT;负端与板共地 |
| GPIO5 → 1k → AHCT 1A | AHCT 1Y → 风扇蓝线PWM |
| 风扇绿线TACH → GPIO7 | 10k上拉板3V3;不接5V上拉 |
| SHT40 SDA/SCL | GPIO8/9;板3V3与共地 |
参考:[1] Noctua NF-A4x10 5V PWM具体规格与线色[2] Noctua PWM白皮书:频率、电平、推挽与TACH[3] TI SN74AHCT125 Rev.R:PDIP-14及推荐条件[6] SHT40具体模块与固定组件[8] 固定官方S3 v1.1引脚[10] PID4885 VIN、3.3V I2C与0x44具体引脚
同一个策略处理迟滞、陈旧与缺少转速
fan_policy.h是唯一策略,与host共用。上电/重启OFF,不恢复旧开启;MANUAL 0停、1..29%抬为30%、其余请求照用。AUTO≥29℃开始、≤27℃停,区间保持;开启后低于31℃为40%,31≤温度<33℃为70%,≥33℃100%。这些是工程演示规则,不是植物/房间/设备散热建议,也非已测最低起转值。
温度NaN、超本例-10..60℃范围或7秒无有效样本时,AUTO给100%请求并TEMP_FAULT闪灯,OFF仍停、MANUAL仍手动。非零请求留15秒起转窗口,之后RPM无效/超过12秒未更新或<200标NO_VALID_RPM;不反复重启电机。丢温度时全速只是明示降级,不能保证失电或卡阻的风扇真转。
计时统一取IDF5.5.5的esp_timer_get_time(),以64位保留启动后微秒再除1000转毫秒,不把32位millis扩大后冒充长期时钟。超时比较不会因约49.7天的32位完整周期让旧状态复活;这是同源注入时刻检查,不是已运行49天的实测。
27<T<29保持的是正常有效温度分支的cooling锁存。实际切换模式会清该锁存;坏温度的100% fallback不把锁存设为开启。因此此前未开启时,坏温度恢复到28℃会回到OFF;此前已开启且未切模式,恢复28℃仍为40%。起转15秒宽限一旦完成会独立锁定,持续非零运行不会重新进入宽限;只有真实请求从0转非零才重新计起转。
internal_filter:13us是配置请求;固定ESPHome实现用PCNT_LL_MAX_GLITCH_WIDTH与实际APB频率计算max_glitch_ns,再取请求与上限的较小值,硬件还会量化。它并非实测或精确13us滤波窗口,也不表示转速测量时间精度。
// SPDX-License-Identifier: MIT
#pragma once
#include <cstdint>
#include <cmath>
#ifdef ESP_PLATFORM
#include "esp_timer.h"
#endif
namespace h07 {
#ifdef ESP_PLATFORM
// IDF monotonic microseconds since boot, widened before conversion; no 32-bit millis cycle.
inline uint64_t now_ms(){return static_cast<uint64_t>(esp_timer_get_time())/1000ULL;}
#endif
enum class Mode { OFF, MANUAL, AUTO };
struct Result {float duty;bool temp_fault,rpm_fault;const char* phase;};
class Controller {
public:
void mode(Mode value){if(value!=mode_)cooling_=false;mode_=value;}
void manual(float percent){manual_=std::isfinite(percent)?(percent<0?0:percent>100?100:percent):0;}
void temperature(uint64_t now,float value){
temp_ok_=std::isfinite(value)&&value>=-10&&value<=60;
if(temp_ok_){temperature_=value;temp_at_=now;}
}
void rpm(uint64_t now,float value){
rpm_ok_=std::isfinite(value)&&value>=0&&value<=10000;
if(rpm_ok_){rpm_=value;rpm_at_=now;}
}
Result tick(uint64_t now){
float duty=0;bool bad_temp=false;
if(mode_==Mode::MANUAL)duty=manual_==0?0:manual_<30?0.3f:manual_/100;
if(mode_==Mode::AUTO){
bad_temp=!temp_ok_||now<temp_at_||now-temp_at_>7000;
if(bad_temp)duty=1; // demonstration policy; not a certified thermal protection
else {
if(temperature_>=29)cooling_=true;else if(temperature_<=27)cooling_=false;
if(cooling_)duty=temperature_>=33?1:temperature_>=31?0.7f:0.4f;
}
}
bool running=duty>0;
if(running&&!running_){run_at_=now;grace_done_=false;}
if(!running)grace_done_=false;
if(running&&!grace_done_&&now>=run_at_&&now-run_at_>15000)grace_done_=true;
running_=running;
bool bad_rpm=running&&grace_done_&&(!rpm_ok_||now<rpm_at_||now-rpm_at_>12000||rpm_<200);
result_={duty,bad_temp,bad_rpm,bad_temp?"TEMP_FAULT":bad_rpm?"NO_VALID_RPM":mode_==Mode::OFF?"OFF":mode_==Mode::MANUAL?"MANUAL":"AUTO"};
return result_;
}
Result result()const{return result_;}
private:
Mode mode_=Mode::OFF;
bool cooling_=false,temp_ok_=false,rpm_ok_=false,running_=false,grace_done_=false;
float manual_=50,temperature_=0,rpm_=0;
uint64_t temp_at_=0,rpm_at_=0,run_at_=0;
Result result_{0,false,false,"OFF"};
};
inline Controller& controller(){static Controller value;return value;}
}
参考:[1] Noctua NF-A4x10 5V PWM具体规格与线色[2] Noctua PWM白皮书:频率、电平、推挽与TACH[7] ESPHome2026.9.1 SHT4x实现与错误路径[11] IDF5.5.5 esp_timer:64位启动后单调微秒
完整YAML:控制输出与TACH输入分开
本节存esp32-low-voltage-fan-control.yaml,与fan_policy.h同目录。Python3.12.13/ESPHome2026.9.1/原生IDF5.5.5及8MB/OPI参数固定;照README建环境、填私有Wi-Fi/API/OTA key,先config、compile,再run --device真实桥口。未经接线检查先保持OFF,不把软件输出测试接到电机电源脚。
SHT40每2秒采样;TACH每5秒计上升沿,pulse_counter本来输出脉冲/分钟,再乘0.5才为RPM,两脉冲/转不重复乘60。Applied PWM Command、Fan RPM Last、Fan Control Phase与Local Fault分列,13us请求会经PCNT/APB上限钳位和硬件量化,250ms策略轮询也不是硬实时保证。
substitutions:
node_name: hixinwu-h07
esphome:
name: ${node_name}
friendly_name: Hixinwu H07
min_version: 2026.9.1
includes:
- fan_policy.h
esp32:
board: esp32-s3-devkitc-1
variant: ESP32S3
flash_size: 8MB
flash_mode: qio
flash_frequency: 80MHz
toolchain: esp-idf
framework:
type: esp-idf
version: 5.5.5
psram:
mode: octal
speed: 80MHz
logger:
baud_rate: 115200
hardware_uart: UART0
level: DEBUG
api:
encryption:
key: !secret api_key
reboot_timeout: 0s
ota:
- platform: esphome
encryption:
key: !secret api_key
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
reboot_timeout: 0s
ap:
ssid: "${node_name} Recovery"
password: !secret fallback_password
ap_timeout: 90s
captive_portal:
i2c:
sda: GPIO8
scl: GPIO9
frequency: 100kHz
scan: true
output:
- platform: ledc
id: fan_pwm
pin: GPIO5
frequency: 25000Hz
switch:
- platform: gpio
id: indicator
pin: GPIO4
internal: true
restore_mode: ALWAYS_OFF
select:
- platform: template
name: Fan Control Mode
options: ["OFF", "MANUAL", "AUTO"]
initial_option: "OFF"
restore_value: false
optimistic: true
on_value:
- lambda: |-
h07::controller().mode(x=="AUTO"?h07::Mode::AUTO:x=="MANUAL"?h07::Mode::MANUAL:h07::Mode::OFF);
number:
- platform: template
name: Manual PWM Request
min_value: 0
max_value: 100
step: 10
initial_value: 50
restore_value: false
optimistic: true
unit_of_measurement: "%"
on_value:
- lambda: h07::controller().manual(x);
sensor:
- platform: sht4x
address: 0x44
precision: High
heater_max_duty: 0
update_interval: 2s
temperature:
name: Control Temperature Last
on_value:
- lambda: h07::controller().temperature(h07::now_ms(),x);
humidity:
name: Relative Humidity Last
- platform: pulse_counter
id: measured_rpm
name: Fan RPM Last
pin:
number: GPIO7
mode: INPUT
use_pcnt: true
internal_filter: 13us
count_mode:
rising_edge: INCREMENT
falling_edge: DISABLE
update_interval: 5s
unit_of_measurement: rpm
accuracy_decimals: 0
filters:
- multiply: 0.5
on_value:
- lambda: h07::controller().rpm(h07::now_ms(),x);
- platform: template
name: Applied PWM Command
unit_of_measurement: "%"
accuracy_decimals: 0
lambda: return h07::controller().result().duty*100;
update_interval: 1s
binary_sensor:
- platform: template
name: Fan Local Fault
lambda: return h07::controller().result().temp_fault || h07::controller().result().rpm_fault;
device_class: problem
text_sensor:
- platform: template
name: Fan Control Phase
lambda: return {h07::controller().result().phase};
update_interval: 1s
interval:
- interval: 250ms
then:
- lambda: |-
auto r=h07::controller().tick(h07::now_ms());
id(fan_pwm).set_level(r.duty);
bool on=(r.temp_fault||r.rpm_fault)?(h07::now_ms()/500)%2:r.duty>0;
if(on)id(indicator).turn_on();else id(indicator).turn_off();
参考:[4] ESPHome2026.9.1 LEDC[5] ESPHome2026.9.1脉冲计数/每分钟单位[7] ESPHome2026.9.1 SHT4x实现与错误路径
HA能改模式,RPM仍须独立核实
HA Core2026.10.2资料范围:添加ESPHome集成、真实IP/key,开发者工具核实际ID。Mode select和Manual PWM Request是入口,不假建一个永远乐观的风扇状态;实际采用占空看Applied PWM Command,1..29会被抬到30。节点/HA/Wi-Fi离线分别记录,reboot_timeout=0s不因离线周期复位,但重启本身仍回OFF。
Fan RPM Last只是一条最近计数,NO_VALID_RPM不能区分TACH断线、外供断电、错针或停转。温度故障优先显示,Local Fault同时报告任一故障;OFF不把零RPM报错。脉冲有效也不证明风量或冷却达标。作者未执行HA集成、实体控制或现场转速比较,不能把模板/API状态当执行确认。
参考:[1] Noctua NF-A4x10 5V PWM具体规格与线色[2] Noctua PWM白皮书:频率、电平、推挽与TACH[5] ESPHome2026.9.1脉冲计数/每分钟单位[9] Home Assistant官方ESPHome集成入口与加密配置
先分档记录,再观察迟滞回程
全断电核所有14脚、风扇黄线电源/蓝线PWM/绿线TACH、共地与不并正极。保持OFF上电,实量供电4.9–5.1V、PWM电平和25kHz;100k下拉只是设计默认,不保证启动瞬态已安全实测。MANUAL依次0/30/50/70/100%,各30秒,记录真实RPM/波形/动作,不能拿厂家最高转速画成本站曲线。
AUTO先用host检查27/29边界,再以远离电路的温度环境变化缓慢升降探头,保存温度、请求与RPM回程;不把注入的数字叫真实温度。固定传感器摆位,避主控热源与直吹,没有整机补偿。另断开HA/Wi-Fi观察本地策略与恢复,同一观察表未执行项保持空。
- 做到这里,你应该看到
- 每个档位同时有请求与独立转速记录;AUTO回程有27/29迟滞证据。
参考:[1] Noctua NF-A4x10 5V PWM具体规格与线色[2] Noctua PWM白皮书:频率、电平、推挽与TACH[3] TI SN74AHCT125 Rev.R:PDIP-14及推荐条件[7] ESPHome2026.9.1 SHT4x实现与错误路径
断信号做故障输入,不需要堵转
先断电预装SHT40单模块SDA的低压SPST:已有样本且AUTO时只断信号,7秒无有效温度后应TEMP_FAULT/全速请求;闭合等好样本,必要EN恢复。TACH支路另预装开关,非零MANUAL稳定后断开,过起转/采样窗口应NO_VALID_RPM;恢复等新计数。冷启动缺探头另轮。不用物件卡叶片,也不带电拔电源杜邦线。
一直全速先核PWM断线、OE/1Y和模式/温度故障;不转先OFF/断电核5V型号、极性和pin4,不接12V试启动。风扇转而RPM零先查绿线/3V3上拉/计数单位,别把故障门限改0掩盖。芯片发热/下陷先断电回最小线路;重启OFF后先重做MANUAL再AUTO。USB/OTA/网络恢复沿README的桥口、BOOT/EN和恢复热点,不碰eFuse。
参考:[1] Noctua NF-A4x10 5V PWM具体规格与线色[2] Noctua PWM白皮书:频率、电平、推挽与TACH[3] TI SN74AHCT125 Rev.R:PDIP-14及推荐条件[7] ESPHome2026.9.1 SHT4x实现与错误路径
构建、纯逻辑和物理验收分开
完整YAML/头/依赖锁/README/MIT/空表可下载;实际config、compile、输入、生成CPP和镜像SHA单列。host直接用fan_policy.h检查模式、迟滞、分档、低占空抬底、NaN/陈旧、起转宽限、零/缺/恢复RPM、OFF与64位跨32位周期,不复制另一套模型。
未模拟GPIO/PWM/TACH电气、温度或电机,也没有实板/HA/长时结果。风扇0%停止是本款规格而非所有型号通用;信号开路可能全速,不能把逻辑OFF当断电隔离。可回到SHT40教程补传感器长期记录,再验本机的完整控制链。
2026-10-18已实际执行config与完整固件compile,均Exit0;OTA 896288字节。三组host同源检查均Exit0;这些是软件结果,未执行烧录/HA/实物试验。
clang++ -std=c++17 -Wall -Wextra -Werror host_check.cpp -o /tmp/h07-host
/tmp/h07-host参考:[1] Noctua NF-A4x10 5V PWM具体规格与线色[2] Noctua PWM白皮书:频率、电平、推挽与TACH[4] ESPHome2026.9.1 LEDC[5] ESPHome2026.9.1脉冲计数/每分钟单位[7] ESPHome2026.9.1 SHT4x实现与错误路径
资料来源与版本说明
固定上游源码和厂家具体章节已阅读,逐字摘要与范围见docs/esp32-evidence/home-devices.md;独立复核/实板结果另记。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。