风扇调速曲线的终点(附仿真web)
文章目录
一、调速核心前提
首先保证你的风扇能压住散热!也就是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>
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)