一、调速核心前提

首先保证你的风扇能压住散热!也就是100%转速下整个系统最后的热平衡温度不超过元器件热失效温度!这是一切调速的前提!

省流直接跳到 七、风扇调速曲线制作方法 部分。

为了方便理解,我们用常见的风扇风冷CPU这个场景来叙述。

想象一下,现在有个风扇对着鳍片散热器,散热器下面是会发热的CPU,CPU运行频率越高,发热量越大,风扇风速越高,散热量越大。

好,我们有了一个基础的散热模型,能动的东西就是风扇转速、CPU频率

转速 -> 散热量
CPU Ghz -> 发热量

二、散热基础模型与核心结论

记住一个重要的结论

散热结构不变情况下,CPU温度越高,单位风量能带走的热量越多。

其实风冷,就是用外界空气(先假设它是25℃)去撞发热的散热器(假设是75℃),他们的温差就是50℃,当环境温度上升至40℃时,他们的温差就只剩35℃了,单位风量能带走的热量相较于常温会变少。

好~现在环境温度25℃,CPU运行在3Ghz,它的发热量是50W,温度目前是80℃,我们启动风扇,直接干满转100%,风量50CFM。

CPU温度会如何变化呢?显而易见,发热量固定,风速固定,散热量会随着CPU与环境的温差减小而减小,直到热平衡温度点热平衡时,散热量=发热量,在温度上表现就是稳定啦!

我们的第二条结论,相同工况下,风速越低,设备(CPU)温度越高。

很符合直觉,是吧?现在我们有一颗温度探头,NTC,直触CPU核心(依旧假设假设…),NTC多少度,CPU就多少度…

三、温控核心参数定义

娇贵的CPU可不能受太高的温度!~~(你从丹东来~

绝大部分电子元器件规格书都给你标明了一个重要参数

热失效温度

我们现在假设CPU的热失效温度为110℃,无论在什么工况下,我们的温控调速程序都要保证CPU不会到达这个温度点。

CPU我们干到满载,5Ghz!发热量125W!
在这里插入图片描述

风速100%!风量50CFM!跑了两个小时,瞅一眼NTC回报温度,稳定啦!稳定在85℃!

好,此时我们降低风速至75%,CPU温度会怎么变化?会上升,但是不会无限制的上升。CPU温度上升的同时,CPU和环境的温差也在变大,单位风量能带走的热量会变得越来越多,直到散热量=发热量,我们先假设这个热平衡点在105℃。
在这里插入图片描述

一般来说,我们不能让元件贴着失效温度跑,取失效温度-15℃作为最高运行温度,我们的安全最高温为 95℃,这涉及我们开头提到的大前提——系统最大发热量最大散热量工况下,元器件温度低于安全最高温

CPU 5Ghz,风速100%,热平衡温度85℃,这个85℃我们称之为

满速最低温

显而易见,满速最低温距离安全最高温还有10℃,现在,我们可以用温度换风速了!

CPU 5Ghz,风速85.7%,热平衡温度95℃,此时风速我们称之为

最低风速


(安全最高温就是图中的预想温度,我懒得改网页了…)
恭喜你!我们发现了一个重要的概念,从满速最低温安全最高温——

自由风速区间

如图,在CPU 5Ghz工况下,我们的自由风速区间为85℃~95℃,在该温度范围内,我们的风速可以从最低风速(85.7%)到100%自由调整!

四、不同工况的风速调节规律

好,我们看另一个工况

CPU 3Ghz,风速100%,热平衡温度61℃
CPU 3Ghz,风速51.4%,热平衡温度95℃

风速调节范围51.4%~100%

在这里插入图片描述
化简对比一下

CPU 5Ghz,风速调整范围85.7%~100%
CPU 3Ghz,风速调整范围51.4%~100%

系统发热量越小,我们调整风速的范围就越大。

温度对应的是元器件寿命,风速对应的是用户使用噪音,各个工况下的风速选择,其实就是在选择这两个取向。理想状态下,我们把CPU空载到满载全部工况都测一遍,找出所有工况下的最低转速,连在一起,就可以得到极致的静音取向风扇调速曲线。

五、工程落地的误差修正

嘿嘿嘿,别高兴太早。现实世界你可没那么多时间测全部工况,你也找不到一个可以直触CPU核心的无热阻NTC探头。

绝大多多多数情况下,我们的NTC都是贴在散热器上,散热器接触CPU外壳,CPU外壳接触CPU核心。

CPU的核心温度(结温)和CPU外壳温度(壳温)之间的热阻可以从datasheet中查阅,但是壳温和散热器温度之间的热阻我们一般不知道,但是可以专门用热电偶或者其他温度探测手段记录这俩的温度,多跑几个工况,把数据丢给豆包,让豆包帮你拟合一个公式出来。

六、温控本质:温度加速度控制

好的,再考虑CPU和散热器的热容量(热惯性),风速从0%到100%这个时刻,CPU温度并不会立刻掉头往下掉,而是会先减缓上升速度,再逐渐逼近热平衡温度

有点像什么呢?对的,就是你开车的油门和刹车,发热量就像油门,散热量就像刹车,热容量就像惯性,我油门顶满,车速在上升,加速度为正值,踩死油门的同时踩死刹车!如果刹车产生的力比全油门产生的力大,那么加速度变成负值!从生活经验也可以轻易知道,车越轻,刹停距离也越短嘛,为什么要引入刹车的概念呢,我们来看一个假设…

CPU 3Ghz,风速0%,当前温度100℃,温度加速度为正,温度持续上升中…

这个时候启动风扇,直接拉满100%,相当于踩死刹车,由于热容量(惯性)的存在,温度会继续上升一段,直到温度加速度把它拉回下降趋势,最后回到CPU 3Ghz满速最低温(59.8℃)。

对的,我们的温控系统,本质上是控制温度的加速度

如果你的MCU有足够多的资源,当然可以采集一长段的温度数据来算温度加速度,加速度为正就加风速,加速度值越大,加的风速也越大,反之亦然。

七、风扇调速曲线制作方法

但是我还有一个有点偷懒取巧的办法——找到最后刹车温度点

假设某一工况,在高于热平衡温度的温度点X℃刹车(风速从0%到100%),最终CPU都会回到热平衡温度,此时从风扇启转的温度点X℃到温度开始下降的温度点X+Y℃之间的温差,也就是刹车距离Y℃,等于热平衡温度安全最高温之间的距离,那么这个X℃就是此工况下的最后刹车温度点

这就是极限,假设为100℃,以此最后刹车点制作一条风扇曲线…哦不,甚至不能称它为曲线了…应该是折线。(2333)

在这里插入图片描述
当然,现实世界的风扇都有一个最低维持转速,具体可以实测或者datasheet里面找。风速为0%的长期工况相当于没有风扇,热传导环境和有风扇不同,不在本文的讨论范围内。我们先假设我们的风扇最低维持转速为20%,随着系统发热量逐渐增大,系统所需的最低转速也逐渐增大…
像这样
在这里插入图片描述
最低转速对应的温度始终是安全最高温,如果你的测试量够大,这条曲线就越贴近最低转速

只要标定好最后刹车点,剩下的曲线随便画。反正能刹住车。
在这里插入图片描述
一般来说,最大发热量的工况不会是常用工况,假设CPU常用4Ghz,测出4Ghz时所需最低风速为68.6%,我们加一点风速凑个整,加到70%,实测此时热平衡点在93.6℃。温度有了,转速有了,加点在原有曲线中…

在这里插入图片描述

继续测试出CPU空载、风速20%工况下热平衡点为60℃…

在这里插入图片描述
诶~上下限特殊点都有了,连线完事。
在这里插入图片描述
只要保证最后刹车点没问题,曲线越平滑,设备实际运行的风速就越平稳。想要优化某个工况下的噪音表现,实测,记录,打点,重新画条线,完事。

八、不同取向的调速方案

哦差点忘了,静音取向有了,那我想让元器件寿命更长,温度更低呢?

在这里插入图片描述
哈哈,开机直接100%转速不就好了。

九、后记

文中只是拿CPU和风扇举例,其实对于任何风扇散热系统基本都适用。

根据设计产品类型的不同,不同温度区间可能有不同的调速曲线,例如笔记本电脑要是一直放在我们说的安全最高温的话,键盘可能会很烫手,这个时候就需要综合考虑温度曲线咯。又或者产品有静音模式,风速不能超过50%,这个时候就要限制产品的产热量。

其实…最后都是面多了加水,水多了加面,车快了踩刹车,车慢了放刹车,就这么简单。

十、热仿真网页

哦对了,为了能直观一点了解这玩意,我用豆包搓了个网页,先说好啊,仅供理解,里面的参数我也不知道对不对,就是看个过程。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>CPU散热热平衡仿真系统 | 修复版</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
<style>
  *{margin:0;padding:0;box-sizing:border-box;font-family:"Segoe UI","Microsoft YaHei",sans-serif}
  html,body{width:100%;height:100%;overflow:hidden}
  body{background:#f8fafc;color:#1e293b;padding:12px}
  
  .wrap{
    max-width:100%;margin:0 auto;
    display:grid;
    grid-template-rows: auto 1fr 140px;
    grid-template-columns: 560px 1fr;
    gap:16px;
    width:100%;height:calc(100vh - 24px);
  }
  .header{
    grid-column:1/-1;text-align:center;
    font-size:20px;color:#2563eb;padding-bottom:8px;
    border-bottom:2px solid #3b82f6;
  }
  .panel{
    background:#ffffff;border-radius:12px;padding:14px;
    box-shadow:0 4px 12px rgba(0,0,0,0.08);
    height:100%;overflow-y:auto;overflow-x:hidden;
  }

  .control-area{
    display:flex;justify-content:center;align-items:flex-end;gap:32px;padding:12px 0;
  }
  .item{display:flex;flex-direction:column;align-items:center;gap:8px}
  .slider-item{width:70px}
  .status-item{width:140px;display:flex;flex-direction:column;align-items:center;justify-content:center;height:220px}
  .label{font-size:13px;font-weight:700;color:#db2777;text-align:center}
  .value{font-size:16px;font-weight:800;color:#2563eb}

  .vert-slider{
    writing-mode:bt-lr;-webkit-appearance:slider-vertical;
    width:8px;height:200px;background:#e2e8f0;border-radius:6px;cursor:pointer;
  }
  .vert-slider:disabled{opacity:0.4;cursor:not-allowed}
  .vert-slider::-webkit-slider-thumb{
    -webkit-appearance:none;width:16px;height:16px;background:#2563eb;border-radius:50%;
  }

  .status-card{
    padding:10px 12px;border-radius:10px;font-weight:700;font-size:13px;
    display:flex;flex-direction:column;align-items:center;gap:6px;width:100%;text-align:center;
  }
  .status-ok{background:#dcfce7;color:#166534;border:2px solid #22c55e}
  .status-warning{background:#fff7ed;color:#c2410c;border:2px solid #f97316}
  .status-danger{background:#fee2e2;color:#991b1b;border:2px solid #ef4444}
  .status-dot{width:12px;height:12px;border-radius:50%}
  .status-ok .status-dot{background:#22c55e}
  .status-warning .status-dot{background:#f97316;animation:pulse 1s infinite}
  .status-danger .status-dot{background:#ef4444;animation:pulse 1s infinite}
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}

  .mode-switch{
    display:flex;align-items:center;justify-content:center;gap:20px;padding:10px;background:#f1f5f9;border-radius:10px;margin:10px 0;
  }
  .mode-item{display:flex;align-items:center;gap:6px;cursor:pointer}
  .mode-item input{width:14px;height:14px;accent-color:#2563eb}
  .mode-item label{font-size:14px;font-weight:600;color:#475569;cursor:pointer}

  .eq-card{
    display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:10px 0;
  }
  .eq-item{
    padding:8px 10px;background:#eff6ff;border-radius:10px;
    border-left:4px solid #2563eb;
  }
  .eq-item.warn{border-left:4px solid #f97316;background:#fff7ed}
  .eq-item.danger{border-left:4px solid #ef4444;background:#fef2f2}
  .eq-item.success{border-left:4px solid #16a34a;background:#f0fdf4}
  .eq-label{font-size:12px;color:#475569}
  .eq-value{font-size:16px;font-weight:800;color:#1e293b;margin-top:2px}
  .eq-sub{font-size:11px;color:#64748b;margin-top:2px}

  .settings{margin-top:10px;display:grid;grid-template-columns:1fr 1fr;gap:8px}
  .set-row{
    display:flex;align-items:center;justify-content:space-between;
    padding:6px 8px;background:#f1f5f9;border-radius:8px;
  }
  .set-row span{color:#475569;font-size:12px;font-weight:500}
  .input-box{
    width:70px;padding:4px 6px;border:1px solid #cbd5e1;border-radius:6px;
    font-size:12px;text-align:center;
  }
  .input-box:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 2px rgba(37,99,235,0.1)}
  .btn-small{
    padding:4px 8px;border:none;border-radius:6px;background:#2563eb;color:#fff;
    font-size:11px;font-weight:600;cursor:pointer;white-space:nowrap;
  }
  .btn-small:disabled{opacity:0.4;cursor:not-allowed;background:#94a3b8}
  .horiz-slider{
    flex:1;margin:0 8px;height:5px;
    -webkit-appearance:none;background:#e2e8f0;border-radius:4px;
  }
  .horiz-slider::-webkit-slider-thumb{
    -webkit-appearance:none;width:14px;height:14px;background:#2563eb;border-radius:50%;
  }

  .formula{
    font-size:12px;line-height:1.6;color:#334155;
    margin-bottom:10px;padding:10px;background:#eff6ff;border-radius:10px;
  }
  .formula h4{color:#db2777;margin-bottom:6px;font-size:14px}
  .formula var{color:#2563eb;font-style:italic;font-weight:600}

  .gauge-container{
    grid-column:1/-1;
    background:#ffffff;border-radius:12px;
    box-shadow:0 4px 12px rgba(0,0,0,0.08);
    padding:15px 20px;
    display:flex;flex-direction:column;
  }
  .gauge-header{
    display:flex;align-items:center;justify-content:space-between;
    margin-bottom:10px;flex-wrap:wrap;gap:10px;
  }
  .gauge-title{font-size:14px;font-weight:700;color:#1e293b}
  .gauge-switch-group{
    display:flex;align-items:center;gap:16px;flex-wrap:wrap;
  }
  .switch-item{display:flex;align-items:center;gap:4px;cursor:pointer}
  .switch-item input{width:13px;height:13px;accent-color:#2563eb}
  .switch-item label{font-size:12px;color:#475569;cursor:pointer;font-weight:500}
  .switch-fail label{color:#ef4444}
  .switch-safe label{color:#f97316}
  .switch-min label{color:#16a34a}
  .switch-current label{color:#1e293b}

  .gauge-scale-wrap{
    position:relative;width:100%;height:60px;
  }
  .gauge-ruler{
    position:absolute;top:20px;left:0;width:100%;height:40px;
    border-left:2px solid #94a3b8;
    border-right:2px solid #94a3b8;
    border-bottom:2px solid #94a3b8;
    background:#f8fafc;
    border-radius:0 0 8px 8px;
  }
  .ruler-tick{
    position:absolute;bottom:0;width:1px;background:#94a3b8;
  }
  .tick-major{height:16px;z-index:1}
  .tick-minor{height:8px;z-index:1}
  .tick-label{
    position:absolute;bottom:18px;transform:translateX(-50%);
    font-size:11px;color:#64748b;font-weight:500;
    white-space:nowrap;
  }
  .temp-mark{
    position:absolute;bottom:0;width:3px;z-index:10;
    transition:left 0.3s ease;
  }
  .mark-fail{background:#ef4444;height:40px}
  .mark-safe{background:#f97316;height:40px}
  .mark-min{background:#16a34a;height:40px}
  .mark-current{
    background:#1e293b;height:45px;z-index:20;
    width:4px;box-shadow:0 0 4px rgba(0,0,0,0.3);
  }
  .temp-tag{
    position:absolute;top:-10px;transform:translateX(-50%);
    display:flex;flex-direction:column;align-items:center;gap:2px;
    font-size:11px;font-weight:700;white-space:nowrap;
    transition:left 0.3s ease;
  }
  .tag-fail{color:#ef4444}
  .tag-safe{color:#f97316}
  .tag-min{color:#16a34a}
  .tag-current{color:#1e293b;font-weight:800;top:-35px}
</style>
</head>
<body>
<div class="wrap">
  <div class="header">CPU散热热平衡仿真系统 | 最低风速修复版</div>

  <!-- 左侧控制面板 -->
  <div class="panel">
    <div class="formula">
      <h4>📐 核心逻辑(经典热平衡模型)</h4>
      1. <b>散热系数K</b>:通过100%风速满载温度校准<br/>
      2. <b>安全余量</b> = 失效温度 − 预想温度<br/>
      3. <b>最低风速</b>:刚好让满载CPU稳定在预想温度的临界风速<br/>
      4. <b>满速最低温度</b>:风扇100%满载时,CPU能达到的最低平衡温度<br/>
      5. 超预想温度仍继续仿真,仅失效温度停止计算<br/>
      6. 仿真时长最大支持5小时(300分钟)
    </div>

    <!-- 热平衡核心参数卡片 -->
    <div class="eq-card">
      <div class="eq-item">
        <div class="eq-label">最终CPU产热量</div>
        <div class="eq-value" id="v_ph">0 W</div>
      </div>
      <div class="eq-item">
        <div class="eq-label">最终风扇解热量</div>
        <div class="eq-value" id="v_pc">0 W</div>
      </div>
      <div class="eq-item warn" id="card_v_min" style="display:none">
        <div class="eq-label">稳定在预想温度的最低风速</div>
        <div class="eq-value" id="v_min">0 %</div>
      </div>
      <div class="eq-item success" id="card_teq_min" style="display:none">
        <div class="eq-label">满速最低热平衡温度</div>
        <div class="eq-value" id="v_teq_min">0 ℃</div>
      </div>
      <!-- 风速自由区间 -->
      <div class="eq-item success" id="card_free_range" style="display:none;grid-column:1/-1">
        <div class="eq-label">风速自由区间</div>
        <div class="eq-value" id="v_free_range">-- ℃</div>
        <div class="eq-sub">上限:<span id="range_upper">--℃</span>(预想温度) | 下限:<span id="range_lower">--℃</span>(满速最低温度)</div>
      </div>
    </div>

    <!-- 风扇模式切换 -->
    <div class="mode-switch">
      <div class="mode-item">
        <input type="radio" name="fanMode" id="modeFixed" checked>
        <label for="modeFixed">固定风速模式</label>
      </div>
      <div class="mode-item">
        <input type="radio" name="fanMode" id="modeCalc">
        <label for="modeCalc">安全风速计算模式</label>
      </div>
    </div>

    <!-- 基础参数设置区 -->
    <div class="settings">
      <!-- 环境温度 -->
      <div class="set-row">
        <span>环境温度(℃)</span>
        <input type="text" class="input-box" id="i_amb" value="25">
      </div>
      <!-- 100%风速校准温度 -->
      <div class="set-row">
        <span>100%风速校准温度(℃)</span>
        <input type="text" class="input-box" id="i_target_temp" value="85">
      </div>
      <!-- CPU失效温度 -->
      <div class="set-row">
        <span>CPU失效温度(℃)</span>
        <input type="text" class="input-box" id="i_fail_temp" value="110">
      </div>
      <!-- 预想温度控制点 -->
      <div class="set-row">
        <span>预想温度控制点(℃)</span>
        <input type="text" class="input-box" id="i_safe_temp" value="95">
      </div>
      <!-- 安全余量 -->
      <div class="set-row">
        <span>安全余量(℃)</span>
        <input type="text" class="input-box" id="i_safety_margin" value="15">
      </div>
      <!-- 仿真时长 -->
      <div class="set-row">
        <span>仿真时长(分钟)</span>
        <div style="display:flex;align-items:center;gap:6px">
          <input type="text" class="input-box" id="i_time" value="15">
          <button class="btn-small" id="btn_set_eq_time">适配热平衡</button>
        </div>
      </div>
      <!-- 满频标定TDP -->
      <div class="set-row">
        <span>满频标定TDP(W)</span>
        <input type="range" class="horiz-slider" id="s_tdp" min="30" max="500" value="125">
        <span id="v_tdp" class="value">125</span>
      </div>
      <!-- 热惯性热容 -->
      <div class="set-row">
        <span>热惯性(热容J/℃)</span>
        <input type="range" class="horiz-slider" id="s_c" min="50" max="1000" value="200">
        <span id="v_c" class="value">200</span>
      </div>
    </div>

    <!-- 核心控制区 -->
    <div class="control-area">
      <div class="item slider-item">
        <div class="label">风扇风速(%)</div>
        <input type="range" class="vert-slider" id="s_fan" min="1" max="100" value="45">
        <div class="value" id="v_fan">45</div>
      </div>
      <div class="item slider-item">
        <div class="label">CPU频率(GHz)</div>
        <input type="range" class="vert-slider" id="s_freq" min="1.0" max="5.0" step="0.1" value="2.8">
        <div class="value" id="v_freq">2.8</div>
      </div>
      <!-- 状态卡片 -->
      <div class="item status-item">
        <div class="status-card status-ok" id="statusCard">
          <div class="status-dot"></div>
          <span id="statusText">散热系统正常</span>
        </div>
        <div class="value" id="v_teq" style="margin-top:12px">0 ℃</div>
        <div class="label">最终CPU温度</div>
        <div class="value" id="v_teq_time" style="margin-top:12px">0 分钟</div>
        <div class="label">热平衡时间</div>
      </div>
    </div>
  </div>

  <!-- 右侧主仿真图表 -->
  <div class="panel main-chart-box">
    <canvas id="mainChart"></canvas>
  </div>

  <!-- 底部全屏水平温度标尺 -->
  <div class="gauge-container">
    <div class="gauge-header">
      <div class="gauge-title">温度标尺(℃)</div>
      <!-- 标尺显示项开关 -->
      <div class="gauge-switch-group">
        <div class="switch-item switch-fail">
          <input type="checkbox" id="switch_fail" checked>
          <label for="switch_fail">失效温度</label>
        </div>
        <div class="switch-item switch-safe">
          <input type="checkbox" id="switch_safe" checked>
          <label for="switch_safe">预想温度</label>
        </div>
        <div class="switch-item switch-min">
          <input type="checkbox" id="switch_min" checked>
          <label for="switch_min">满速最低温</label>
        </div>
        <div class="switch-item switch-current">
          <input type="checkbox" id="switch_current" checked>
          <label for="switch_current">当前温度</label>
        </div>
      </div>
    </div>
    <div class="gauge-scale-wrap" id="gaugeScaleWrap">
      <div class="gauge-ruler" id="gaugeRuler"></div>
      <!-- 关键温度标记线 -->
      <div class="temp-mark mark-fail" id="markFail"></div>
      <div class="temp-mark mark-safe" id="markSafe"></div>
      <div class="temp-mark mark-min" id="markMin"></div>
      <div class="temp-mark mark-current" id="markCurrent"></div>
      <!-- 关键温度标签 -->
      <div class="temp-tag tag-fail" id="tagFail">
        <span>失效温度</span>
        <span id="tagFailText">110℃</span>
      </div>
      <div class="temp-tag tag-safe" id="tagSafe">
        <span>预想温度</span>
        <span id="tagSafeText">95℃</span>
      </div>
      <div class="temp-tag tag-min" id="tagMin">
        <span>满速最低温</span>
        <span id="tagMinText">--℃</span>
      </div>
      <div class="temp-tag tag-current" id="tagCurrent">
        <span>当前最终温度</span>
        <span id="tagCurrentText">0℃</span>
      </div>
    </div>
  </div>
</div>

<script>
window.onload = function() {
  // ===================== DOM元素映射 =====================
  const els = {
    // 状态
    statusCard: document.getElementById('statusCard'),
    statusText: document.getElementById('statusText'),
    // 标尺显示开关
    switch_fail: document.getElementById('switch_fail'),
    switch_safe: document.getElementById('switch_safe'),
    switch_min: document.getElementById('switch_min'),
    switch_current: document.getElementById('switch_current'),
    // 标尺元素
    gaugeRuler: document.getElementById('gaugeRuler'),
    markFail: document.getElementById('markFail'),
    markSafe: document.getElementById('markSafe'),
    markMin: document.getElementById('markMin'),
    markCurrent: document.getElementById('markCurrent'),
    tagFail: document.getElementById('tagFail'),
    tagSafe: document.getElementById('tagSafe'),
    tagMin: document.getElementById('tagMin'),
    tagCurrent: document.getElementById('tagCurrent'),
    tagFailText: document.getElementById('tagFailText'),
    tagSafeText: document.getElementById('tagSafeText'),
    tagMinText: document.getElementById('tagMinText'),
    tagCurrentText: document.getElementById('tagCurrentText'),
    // 区间显示
    v_free_range: document.getElementById('v_free_range'),
    range_upper: document.getElementById('range_upper'),
    range_lower: document.getElementById('range_lower'),
    card_free_range: document.getElementById('card_free_range'),
    // 模式切换
    modeFixed: document.getElementById('modeFixed'),
    modeCalc: document.getElementById('modeCalc'),
    // 模式卡片
    card_v_min: document.getElementById('card_v_min'),
    card_teq_min: document.getElementById('card_teq_min'),
    // 文本输入框
    i_amb: document.getElementById('i_amb'),
    i_target_temp: document.getElementById('i_target_temp'),
    i_safe_temp: document.getElementById('i_safe_temp'),
    i_safety_margin: document.getElementById('i_safety_margin'),
    i_fail_temp: document.getElementById('i_fail_temp'),
    i_time: document.getElementById('i_time'),
    btn_set_eq_time: document.getElementById('btn_set_eq_time'),
    // 滑块输入
    s_fan: document.getElementById('s_fan'),
    s_freq: document.getElementById('s_freq'),
    s_tdp: document.getElementById('s_tdp'),
    s_c: document.getElementById('s_c'),
    // 数值显示
    v_fan: document.getElementById('v_fan'),
    v_freq: document.getElementById('v_freq'),
    v_tdp: document.getElementById('v_tdp'),
    v_c: document.getElementById('v_c'),
    v_ph: document.getElementById('v_ph'),
    v_pc: document.getElementById('v_pc'),
    v_teq: document.getElementById('v_teq'),
    v_teq_time: document.getElementById('v_teq_time'),
    v_min: document.getElementById('v_min'),
    v_teq_min: document.getElementById('v_teq_min'),
  }

  // ===================== 全局常量 =====================
  const F_MAX = 5.0;
  const EQ_THRESHOLD = 0.99;
  const AMB_MIN = -20;
  const AMB_MAX = 60;
  const TIME_MIN = 1;
  const TIME_MAX = 300; // 5小时上限
  const SAFETY_MARGIN_MIN = 5;
  const FAN_MIN = 1; // 风扇最低风速1%,避免除以0
  const FAN_MAX = 100;

  // ===================== 全局变量 =====================
  let K_c = 0;
  let currentEqTime = 0;
  let mainChart = null;
  let currentMode = 'fixed';
  let currentScaleRange = { min: 0, max: 0 };

  // ===================== 核心工具函数 =====================
  function clampValue(val, min, max, def) {
    const num = parseFloat(val);
    return isNaN(num) ? def : Math.max(min, Math.min(max, num));
  }

  // 温度转标尺位置百分比
  function tempToPercent(temp) {
    const { min, max } = currentScaleRange;
    return Math.max(0, Math.min(100, (temp - min) / (max - min) * 100));
  }

  // ===================== 预想温度+安全余量双联动 =====================
  function updateTempLinkage(trigger) {
    const T_fail = clampValue(els.i_fail_temp.value, 60, 200, 110);
    let T_safe = clampValue(els.i_safe_temp.value, AMB_MIN + 10, T_fail - SAFETY_MARGIN_MIN, 95);
    let safetyMargin = clampValue(els.i_safety_margin.value, SAFETY_MARGIN_MIN, T_fail - 30, 15);

    // 联动逻辑
    if (trigger === 'safe_temp') {
      safetyMargin = T_fail - T_safe;
      if (safetyMargin < SAFETY_MARGIN_MIN) {
        safetyMargin = SAFETY_MARGIN_MIN;
        T_safe = T_fail - safetyMargin;
        els.i_safe_temp.value = T_safe.toFixed(0);
      }
      els.i_safety_margin.value = safetyMargin.toFixed(0);
    } else if (trigger === 'safety_margin') {
      T_safe = T_fail - safetyMargin;
      if (T_safe < AMB_MIN + 10) {
        T_safe = AMB_MIN + 10;
        safetyMargin = T_fail - T_safe;
        els.i_safety_margin.value = safetyMargin.toFixed(0);
      }
      els.i_safe_temp.value = T_safe.toFixed(0);
    } else if (trigger === 'fail_temp') {
      if (T_safe >= T_fail - SAFETY_MARGIN_MIN) {
        T_safe = T_fail - 15;
        safetyMargin = 15;
        els.i_safe_temp.value = T_safe.toFixed(0);
        els.i_safety_margin.value = safetyMargin.toFixed(0);
      }
    }

    return { T_fail, T_safe, safetyMargin };
  }

  // ===================== 水平尺子标尺生成 =====================
  function renderRuler(T_amb, T_fail) {
    const scaleMin = Math.floor(T_amb - 10);
    const scaleMax = Math.ceil(T_fail + 10);
    currentScaleRange = { min: scaleMin, max: scaleMax };
    els.gaugeRuler.innerHTML = '';

    // 刻度间隔自动适配
    const range = scaleMax - scaleMin;
    let tickStep = 10;
    if (range > 200) tickStep = 20;
    if (range < 100) tickStep = 5;

    // 生成刻度
    for (let temp = scaleMin; temp <= scaleMax; temp += tickStep/2) {
      const isMajor = temp % tickStep === 0;
      const pct = tempToPercent(temp);
      
      // 刻度线
      const tick = document.createElement('div');
      tick.className = isMajor ? 'ruler-tick tick-major' : 'ruler-tick tick-minor';
      tick.style.left = pct + '%';
      els.gaugeRuler.appendChild(tick);

      // 主刻度数字
      if (isMajor) {
        const label = document.createElement('div');
        label.className = 'tick-label';
        label.style.left = pct + '%';
        label.textContent = temp;
        els.gaugeRuler.appendChild(label);
      }
    }
  }

  // ===================== 标尺标记更新 =====================
  function updateGaugeMarks(finalTemp, T_fail, T_safe, calcData=null) {
    function setMarkPos(el, temp) {
      const pct = tempToPercent(temp);
      el.style.left = pct + '%';
    }

    // 失效温度
    const showFail = els.switch_fail.checked;
    els.markFail.style.display = showFail ? 'block' : 'none';
    els.tagFail.style.display = showFail ? 'flex' : 'none';
    if (showFail) {
      setMarkPos(els.markFail, T_fail);
      setMarkPos(els.tagFail, T_fail);
      els.tagFailText.textContent = `${T_fail.toFixed(0)}`;
    }

    // 预想温度
    const showSafe = els.switch_safe.checked;
    els.markSafe.style.display = showSafe ? 'block' : 'none';
    els.tagSafe.style.display = showSafe ? 'flex' : 'none';
    if (showSafe) {
      setMarkPos(els.markSafe, T_safe);
      setMarkPos(els.tagSafe, T_safe);
      els.tagSafeText.textContent = `${T_safe.toFixed(0)}`;
    }

    // 当前温度
    const showCurrent = els.switch_current.checked;
    els.markCurrent.style.display = showCurrent ? 'block' : 'none';
    els.tagCurrent.style.display = showCurrent ? 'flex' : 'none';
    if (showCurrent) {
      setMarkPos(els.markCurrent, finalTemp);
      setMarkPos(els.tagCurrent, finalTemp);
      els.tagCurrentText.textContent = `${finalTemp.toFixed(1)}`;
    }

    // 满速最低温度
    const showMin = els.switch_min.checked && calcData;
    els.markMin.style.display = showMin ? 'block' : 'none';
    els.tagMin.style.display = showMin ? 'flex' : 'none';
    if (showMin) {
      const teq_min = calcData.teq_min;
      setMarkPos(els.markMin, teq_min);
      setMarkPos(els.tagMin, teq_min);
      els.tagMinText.textContent = `${teq_min.toFixed(1)}`;
    }
  }

  // ===================== 状态更新 =====================
  function updateStatus(isFailed, isOverSafe, message) {
    if (isFailed) {
      els.statusCard.className = 'status-card status-danger';
    } else if (isOverSafe) {
      els.statusCard.className = 'status-card status-warning';
    } else {
      els.statusCard.className = 'status-card status-ok';
    }
    els.statusText.textContent = message;
  }

  // ===================== 散热系数校准(经典单参数模型) =====================
  function calibrateKc() {
    const P_max = clampValue(els.s_tdp.value, 30, 500, 125);
    const T_amb = clampValue(els.i_amb.value, AMB_MIN, AMB_MAX, 25);
    const T_calib = clampValue(els.i_target_temp.value, T_amb + 5, 120, 85);
    
    // 校准逻辑:100%风速时,P_max = K_c * 100 * (T_calib - T_amb)
    if (T_calib < T_amb + 5) {
      els.i_target_temp.value = (T_amb + 5).toFixed(0);
    }
    K_c = P_max / (100 * (T_calib - T_amb));
    return K_c;
  }

  // ===================== 模式切换 =====================
  function switchMode(mode) {
    currentMode = mode;
    els.card_v_min.style.display = 'none';
    els.card_teq_min.style.display = 'none';
    els.card_free_range.style.display = 'none';
    els.s_fan.disabled = false;

    if (mode === 'calc') {
      els.card_v_min.style.display = 'block';
      els.card_teq_min.style.display = 'block';
      els.card_free_range.style.display = 'block';
      els.s_fan.disabled = true;
    }

    runSimulation();
  }

  // 模式切换事件
  els.modeFixed.addEventListener('change', () => switchMode('fixed'));
  els.modeCalc.addEventListener('change', () => switchMode('calc'));

  // 标尺开关事件
  els.switch_fail.addEventListener('change', runSimulation);
  els.switch_safe.addEventListener('change', runSimulation);
  els.switch_min.addEventListener('change', runSimulation);
  els.switch_current.addEventListener('change', runSimulation);

  // ===================== 核心仿真函数(修复最低风速计算) =====================
  function runSimulation() {
    try {
      // 1. 基础参数校准
      calibrateKc();
      const { T_fail, T_safe } = updateTempLinkage();
      const T_amb = clampValue(els.i_amb.value, AMB_MIN, AMB_MAX, 25);
      const f_cpu = clampValue(els.s_freq.value, 1, F_MAX, 2.8);
      const total_min = clampValue(els.i_time.value, TIME_MIN, TIME_MAX, 15);
      const P_max = clampValue(els.s_tdp.value, 30, 500, 125);
      const C = clampValue(els.s_c.value, 50, 1000, 200);
      const P_h = P_max * (f_cpu / F_MAX);

      // 2. 模式处理(修复最低风速计算)
      let fixedFan = clampValue(els.s_fan.value, FAN_MIN, FAN_MAX, 45);
      let calcData = null;
      let isCalcMode = currentMode === 'calc';

      if (isCalcMode) {
        // 核心修复:正确的最低风速计算公式
        const teq_min = T_amb + P_h / (K_c * 100); // 满速最低温度
        const delta_T_safe = Math.max(T_safe - T_amb, 1); // 安全温升
        let v_min = P_h / (K_c * delta_T_safe); // 最低风速核心公式
        v_min = Math.max(v_min, FAN_MIN); // 最低1%,避免0值
        calcData = { teq_min, v_min };
        fixedFan = Math.min(v_min, FAN_MAX); // 最大100%
      }

      // 3. 更新界面显示
      els.v_fan.textContent = fixedFan.toFixed(0);
      els.v_freq.textContent = f_cpu.toFixed(1);
      els.v_tdp.textContent = els.s_tdp.value;
      els.v_c.textContent = els.s_c.value;

      // 4. 仿真配置
      const dt = 0.05;
      const steps = Math.floor(total_min / dt);
      const dt_second = dt * 60;
      const labels = [];
      const data_fan = [];
      const data_Tcpu = [];
      const data_Teq = [];

      // 5. 仿真循环
      let T_cpu = T_amb;
      let eq_time_recorded = false;
      let t_eq_actual = total_min;
      let final_Teq = T_amb;
      let final_Pc = 0;
      let cpuFailed = false;
      let failTime = 0;
      let isOverSafe = false;

      for (let i=0; i<=steps; i++) {
        const t = i * dt;
        const currentFan = fixedFan;
        const delta_T = Math.max(T_cpu - T_amb, 0);
        const P_c = K_c * currentFan * delta_T;
        const current_Teq = T_amb + P_h / (K_c * Math.max(currentFan, FAN_MIN));
        final_Teq = current_Teq;

        // 仅失效温度停止仿真
        if (!cpuFailed && T_cpu >= T_fail) {
          cpuFailed = true;
          failTime = t;
          break;
        }

        // 超预想温度标记
        if (!isOverSafe && T_cpu >= T_safe) {
          isOverSafe = true;
        }

        // 热平衡时间记录
        if (!eq_time_recorded && !cpuFailed && T_cpu >= current_Teq * EQ_THRESHOLD) {
          t_eq_actual = t;
          eq_time_recorded = true;
        }

        // 记录数据
        labels.push(t.toFixed(1));
        data_fan.push(currentFan);
        data_Tcpu.push(T_cpu);
        data_Teq.push(current_Teq);

        // 温度更新
        const dT_dt = (P_h - P_c) / C;
        T_cpu += dT_dt * dt_second;
        T_cpu = Math.max(T_cpu, T_amb);
        final_Pc = P_c;
      }

      // 6. 状态判定
      let statusMsg = '散热系统正常';
      if (cpuFailed) {
        statusMsg = `CPU损坏!失效时间:${failTime.toFixed(1)}分钟`;
      } else if (isOverSafe) {
        statusMsg = '温度超过预想控制点,未达到失效温度';
      } else if (isCalcMode) {
        if (calcData.v_min > 100) {
          statusMsg = '满速也无法稳定在预想温度';
        } else {
          statusMsg = `最低风速:${calcData.v_min.toFixed(1)}%`;
        }
      }

      // 7. 更新标尺
      renderRuler(T_amb, T_fail);
      updateGaugeMarks(final_Teq, T_fail, T_safe, calcData);
      
      // 8. 更新其他UI
      updateStatus(cpuFailed, isOverSafe, statusMsg);
      currentEqTime = t_eq_actual;
      els.v_ph.textContent = P_h.toFixed(1) + ' W';
      els.v_pc.textContent = final_Pc.toFixed(1) + ' W';
      els.v_teq.textContent = final_Teq.toFixed(1) + ' ℃';
      els.v_teq_time.textContent = t_eq_actual.toFixed(1) + ' 分钟';

      // 计算模式结果更新
      if (isCalcMode) {
        els.v_min.textContent = calcData.v_min.toFixed(1) + ' %';
        els.v_teq_min.textContent = calcData.teq_min.toFixed(1) + ' ℃';
        els.v_free_range.textContent = (T_safe - calcData.teq_min).toFixed(1) + ' ℃';
        els.range_upper.textContent = T_safe.toFixed(1) + '℃';
        els.range_lower.textContent = calcData.teq_min.toFixed(1) + '℃';
      }

      // 9. 更新主仿真图表
      if (!mainChart) {
        const ctx = document.getElementById('mainChart').getContext('2d');
        mainChart = new Chart(ctx, {
          type: 'line',
          data: {
            labels: labels,
            datasets: [
              { label: 'CPU温度 (℃)', data: data_Tcpu, borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.1)', tension: 0.3, yAxisID: 'y2', borderWidth: 3 },
              { label: '实时风速 (%)', data: data_fan, borderColor: '#2563eb', backgroundColor: 'rgba(37,99,235,0.1)', tension: 0.3, yAxisID: 'y0' },
              { label: '热平衡温度', data: data_Teq, borderColor: '#16a34a', borderDash: [5,5], tension: 0, yAxisID: 'y2', borderWidth: 2, pointRadius: 0 },
              { label: '预想温度线', data: Array(labels.length).fill(T_safe), borderColor: '#f97316', borderDash: [2,2], tension: 0, yAxisID: 'y2', borderWidth: 1.5, pointRadius: 0 },
              { label: '失效温度线', data: Array(labels.length).fill(T_fail), borderColor: '#ef4444', borderDash: [2,2], tension: 0, yAxisID: 'y2', borderWidth: 1.5, pointRadius: 0 }
            ]
          },
          options: {
            responsive: true,
            maintainAspectRatio: false,
            plugins: {
              title: { display: true, text: `0~${total_min}分钟 热平衡仿真曲线`, font: { size: 16 } },
              legend: { position: 'top' }
            },
            scales: {
              x: { title: { display: true, text: '时间 (分钟)' } },
              y0: { type: 'linear', position: 'left', title: { display: true, text: '风速 (%)' }, min: 0, max: 100 },
              y2: { type: 'linear', position: 'right', title: { display: true, text: '温度 (℃)' }, min: 0, max: Math.max(T_fail * 1.2, 150) }
            }
          }
        });
      } else {
        mainChart.data.labels = labels;
        mainChart.data.datasets[0].data = data_Tcpu;
        mainChart.data.datasets[1].data = data_fan;
        mainChart.data.datasets[2].data = data_Teq;
        mainChart.data.datasets[3].data = Array(labels.length).fill(T_safe);
        mainChart.data.datasets[4].data = Array(labels.length).fill(T_fail);
        mainChart.options.scales.y2.max = Math.max(T_fail * 1.2, 150);
        mainChart.options.plugins.title.text = `0~${total_min}分钟 热平衡仿真曲线`;
        mainChart.update();
      }
    } catch (e) {
      console.error('仿真出错:', e);
    }
  }

  // ===================== 事件绑定 =====================
  // 双参数联动
  els.i_safe_temp.addEventListener('change', () => {
    updateTempLinkage('safe_temp');
    runSimulation();
  });
  els.i_safety_margin.addEventListener('change', () => {
    updateTempLinkage('safety_margin');
    runSimulation();
  });
  els.i_fail_temp.addEventListener('change', () => {
    updateTempLinkage('fail_temp');
    runSimulation();
  });

  // 其他参数
  els.i_amb.addEventListener('change', runSimulation);
  els.i_target_temp.addEventListener('change', runSimulation);
  els.i_time.addEventListener('change', runSimulation);
  els.s_tdp.addEventListener('input', () => {
    els.v_tdp.textContent = els.s_tdp.value;
    runSimulation();
  });
  els.s_c.addEventListener('input', () => {
    els.v_c.textContent = els.s_c.value;
    runSimulation();
  });
  els.s_fan.addEventListener('input', runSimulation);
  els.s_freq.addEventListener('input', runSimulation);

  // 适配热平衡时间
  els.btn_set_eq_time.addEventListener('click', () => {
    const targetTime = Math.min(Math.max(currentEqTime * 1.2, TIME_MIN), TIME_MAX);
    els.i_time.value = targetTime.toFixed(1);
    runSimulation();
  });

  // 初始化
  switchMode('fixed');
}
</script>
</body>
</html>

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐